From 7894c20433fc979d6bf8a2ec3b58dd9edf589cc5 Mon Sep 17 00:00:00 2001 From: Nick Babcock Date: Fri, 30 Oct 2015 11:16:42 -0400 Subject: [PATCH] Verify CSV headers are as expected --- .../jackson/dataformat/csv/CsvParser.java | 20 ++++- .../jackson/dataformat/csv/CsvSchema.java | 31 ++++++++ .../dataformat/csv/deser/BasicParserTest.java | 76 +++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvParser.java b/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvParser.java index e925828..4ea4714 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvParser.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvParser.java @@ -838,8 +838,24 @@ protected void _readHeaderLine() throws IOException { */ if (_schema.size() > 0 && !_schema.reordersColumns()) { - //noinspection StatementWithEmptyBody - while (_reader.nextString() != null) { /* does nothing */ } + if (_schema.strictHeaders()) { + String name; + for (CsvSchema.Column column : _schema._columns) { + name = _reader.nextString(); + if (name == null) { + _reportError(String.format("Missing header %s", column.getName())); + } else if (!column.getName().equals(name)) { + _reportError(String.format("Expected header %s, actual header %s", column.getName(), name)); + } + } + if ((name = _reader.nextString()) != null) { + _reportError(String.format("Extra header %s", name)); + } + } + else { + //noinspection StatementWithEmptyBody + while (_reader.nextString() != null) { /* does nothing */ } + } return; } diff --git a/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java b/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java index fdc67de..abe9f3d 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvSchema.java @@ -83,6 +83,7 @@ public class CsvSchema protected final static int ENCODING_FEATURE_SKIP_FIRST_DATA_ROW = 0x0002; protected final static int ENCODING_FEATURE_ALLOW_COMMENTS = 0x0004; protected final static int ENCODING_FEATURE_REORDER_COLUMNS = 0x0008; + protected final static int ENCODING_FEATURE_STRICT_HEADERS = 0x0016; protected final static int DEFAULT_ENCODING_FEATURES = 0; @@ -471,6 +472,22 @@ public Builder setReorderColumns(boolean b) { return this; } + + /** + * Use in combination with setUseHeader. When use header flag is + * is set, this setting will ensure the headers are in the order + * of the schema + * + * @param b Enable / Disable this setting + * @return This Builder instance + * + * @since 2.7 + */ + public Builder setStrictHeaders(boolean b) { + _feature(ENCODING_FEATURE_STRICT_HEADERS, b); + return this; + } + /** * Method for specifying whether Schema should indicate that * the first line that is not a header (if header handling enabled) @@ -804,6 +821,19 @@ public CsvSchema withColumnReordering(boolean state) { return _withFeature(ENCODING_FEATURE_REORDER_COLUMNS, state); } + /** + * Returns a clone of this instance by changing or setting the + * strict headers flag + * + * @param state New value for setting + * @return A copy of itself, ensuring the setting for + * the strict headers feature. + * @since 2.7 + */ + public CsvSchema withStrictColumns(boolean state) { + return _withFeature(ENCODING_FEATURE_STRICT_HEADERS, state); + } + /** * Helper method for constructing and returning schema instance that * is similar to this one, except that it will be using header line. @@ -1001,6 +1031,7 @@ public String getSchemaType() { public boolean reordersColumns() { return (_features & ENCODING_FEATURE_REORDER_COLUMNS) != 0; } public boolean skipsFirstDataRow() { return (_features & ENCODING_FEATURE_SKIP_FIRST_DATA_ROW) != 0; } public boolean allowsComments() { return (_features & ENCODING_FEATURE_ALLOW_COMMENTS) != 0; } + public boolean strictHeaders() { return (_features & ENCODING_FEATURE_STRICT_HEADERS) != 0; } /** * @deprecated Use {@link #usesHeader()} instead diff --git a/src/test/java/com/fasterxml/jackson/dataformat/csv/deser/BasicParserTest.java b/src/test/java/com/fasterxml/jackson/dataformat/csv/deser/BasicParserTest.java index 9be6679..56fc798 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/csv/deser/BasicParserTest.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/csv/deser/BasicParserTest.java @@ -5,6 +5,7 @@ import java.util.*; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.dataformat.csv.*; @@ -328,4 +329,79 @@ public void testColumnReordering() throws IOException { assertEquals(JsonToken.END_OBJECT, parser.nextToken()); parser.close(); } + + public void testColumnFailsOnOutOfOrder() throws IOException { + CsvFactory factory = new CsvFactory(); + String CSV = "b,a,c\nvb,va,vc\n"; + + CsvSchema schema = CsvSchema.builder() + .addColumn("a") + .addColumn("b") + .addColumn("c") + .setLineSeparator('\n') + .setUseHeader(true) + .setStrictHeaders(true) + .build(); + + CsvParser parser = factory.createParser(CSV); + parser.setSchema(schema); + + try { + parser.nextToken(); + fail("Should have failed"); + } catch (JsonProcessingException e) { + verifyException(e, "Expected header a, actual header b"); + } + parser.close(); + } + + public void testColumnFailsOnTooFew() throws IOException { + CsvFactory factory = new CsvFactory(); + String CSV = "a,b\nvb,va,vc\n"; + + CsvSchema schema = CsvSchema.builder() + .addColumn("a") + .addColumn("b") + .addColumn("c") + .setLineSeparator('\n') + .setUseHeader(true) + .setStrictHeaders(true) + .build(); + + CsvParser parser = factory.createParser(CSV); + parser.setSchema(schema); + + try { + parser.nextToken(); + fail("Should have failed"); + } catch (JsonProcessingException e) { + verifyException(e, "Missing header c"); + } + parser.close(); + } + + public void testColumnFailsOnTooMany() throws IOException { + CsvFactory factory = new CsvFactory(); + String CSV = "a,b,c,d\nvb,va,vc\n"; + + CsvSchema schema = CsvSchema.builder() + .addColumn("a") + .addColumn("b") + .addColumn("c") + .setLineSeparator('\n') + .setUseHeader(true) + .setStrictHeaders(true) + .build(); + + CsvParser parser = factory.createParser(CSV); + parser.setSchema(schema); + + try { + parser.nextToken(); + fail("Should have failed"); + } catch (JsonProcessingException e) { + verifyException(e, "Extra header d"); + } + parser.close(); + } }