Add support for NDJSON format, and fix bug where JSON parser was not checking for the end of content
This commit is contained in:
parent
437687f5cd
commit
a209c13c1a
|
@ -134,6 +134,10 @@ public class JsonParser extends ParserBase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ValidatedFragment> parse(InputStream inStream) throws IOException, FHIRException {
|
public List<ValidatedFragment> parse(InputStream inStream) throws IOException, FHIRException {
|
||||||
|
return parse(inStream, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ValidatedFragment> parse(InputStream inStream, int line) throws IOException, FHIRException {
|
||||||
// long start = System.currentTimeMillis();
|
// long start = System.currentTimeMillis();
|
||||||
byte[] content = TextFile.streamToBytes(inStream);
|
byte[] content = TextFile.streamToBytes(inStream);
|
||||||
ValidatedFragment focusFragment = new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "json", content, false);
|
ValidatedFragment focusFragment = new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "json", content, false);
|
||||||
|
@ -146,12 +150,12 @@ public class JsonParser extends ParserBase {
|
||||||
|
|
||||||
if (policy == ValidationPolicy.EVERYTHING) {
|
if (policy == ValidationPolicy.EVERYTHING) {
|
||||||
try {
|
try {
|
||||||
obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true);
|
obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true, line);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logError(focusFragment.getErrors(), ValidationMessage.NO_RULE_DATE, -1, -1,context.formatMessage(I18nConstants.DOCUMENT), IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_, e.getMessage()), IssueSeverity.FATAL);
|
logError(focusFragment.getErrors(), ValidationMessage.NO_RULE_DATE, -1, -1, null, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_, e.getMessage()), IssueSeverity.FATAL);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true);
|
obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj != null) {
|
if (obj != null) {
|
||||||
|
|
|
@ -47,7 +47,7 @@ import org.hl7.fhir.r5.model.StructureDefinition;
|
||||||
public class Manager {
|
public class Manager {
|
||||||
|
|
||||||
//TODO use EnumMap
|
//TODO use EnumMap
|
||||||
public enum FhirFormat { XML, JSON, TURTLE, TEXT, VBAR, SHC, SHL, FML;
|
public enum FhirFormat { XML, JSON, TURTLE, TEXT, VBAR, SHC, SHL, FML, NDJSON;
|
||||||
// SHC = smart health cards, including as text versions of QR codes
|
// SHC = smart health cards, including as text versions of QR codes
|
||||||
// SHL = smart health links, also a text version of the QR code
|
// SHL = smart health links, also a text version of the QR code
|
||||||
|
|
||||||
|
@ -69,6 +69,8 @@ public class Manager {
|
||||||
return "shl";
|
return "shl";
|
||||||
case FML:
|
case FML:
|
||||||
return "fml";
|
return "fml";
|
||||||
|
case NDJSON:
|
||||||
|
return "ndjson";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -91,6 +93,8 @@ public class Manager {
|
||||||
return SHL;
|
return SHL;
|
||||||
case "fml":
|
case "fml":
|
||||||
return FML;
|
return FML;
|
||||||
|
case "ndjson":
|
||||||
|
return NDJSON;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -127,6 +131,7 @@ public class Manager {
|
||||||
}
|
}
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case JSON : return new JsonParser(context);
|
case JSON : return new JsonParser(context);
|
||||||
|
case NDJSON : return new NDJsonParser(context);
|
||||||
case XML : return new XmlParser(context);
|
case XML : return new XmlParser(context);
|
||||||
case TURTLE : return new TurtleParser(context);
|
case TURTLE : return new TurtleParser(context);
|
||||||
case VBAR : return new VerticalBarParser(context);
|
case VBAR : return new VerticalBarParser(context);
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
package org.hl7.fhir.r5.elementmodel;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (c) 2011+, HL7, Inc.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of HL7 nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||||
|
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||||
|
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||||
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hl7.fhir.exceptions.FHIRException;
|
||||||
|
import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
|
||||||
|
import org.hl7.fhir.r5.context.IWorkerContext;
|
||||||
|
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
|
||||||
|
import org.hl7.fhir.r5.formats.JsonCreator;
|
||||||
|
import org.hl7.fhir.utilities.TextFile;
|
||||||
|
import org.hl7.fhir.utilities.Utilities;
|
||||||
|
import org.hl7.fhir.utilities.i18n.I18nConstants;
|
||||||
|
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||||
|
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
|
||||||
|
import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
|
||||||
|
|
||||||
|
|
||||||
|
public class NDJsonParser extends ParserBase {
|
||||||
|
|
||||||
|
public NDJsonParser(IWorkerContext context, ProfileUtilities utilities) {
|
||||||
|
super(context, utilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NDJsonParser(IWorkerContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidatedFragment processLine(int lineCount, String line) throws FHIRException, IOException {
|
||||||
|
List<ValidatedFragment> list = new JsonParser(context).parse(new ByteArrayInputStream(line.getBytes(StandardCharsets.UTF_8)), lineCount);
|
||||||
|
return list.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ValidatedFragment> parse(InputStream inStream) throws IOException, FHIRException {
|
||||||
|
String source = TextFile.streamToString(inStream);
|
||||||
|
int length = source.length();
|
||||||
|
int start = 0;
|
||||||
|
int cursor = start;
|
||||||
|
int lineCount = 0;
|
||||||
|
|
||||||
|
List<ValidatedFragment> res = new ArrayList<>();
|
||||||
|
while (cursor < length) {
|
||||||
|
while (cursor < length && source.charAt(cursor) != '\n') {
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
if (cursor < length) {
|
||||||
|
String line = source.substring(start, cursor);
|
||||||
|
if (line.endsWith("\r")) {
|
||||||
|
line = line.substring(0, line.length()-1);
|
||||||
|
}
|
||||||
|
if (Utilities.noString(line.trim())) {
|
||||||
|
ValidatedFragment vf = new ValidatedFragment(ValidatedFragment.ITEM_NAME, null, null, false);
|
||||||
|
logError(vf.getErrors(), "2024-06-30", lineCount+1, 1, null, IssueType.INFORMATIONAL, context.formatMessage(I18nConstants.NDJSON_EMPTY_LINE_WARNING), IssueSeverity.WARNING);
|
||||||
|
res.add(vf);
|
||||||
|
} else {
|
||||||
|
res.add(processLine(lineCount, line));
|
||||||
|
}
|
||||||
|
start = cursor+1;
|
||||||
|
} else if (cursor > start) {
|
||||||
|
String line = source.substring(start, cursor);
|
||||||
|
if (line.endsWith("\r")) {
|
||||||
|
line = line.substring(0, line.length()-1);
|
||||||
|
}
|
||||||
|
if (Utilities.noString(line.trim())) {
|
||||||
|
ValidatedFragment vf = new ValidatedFragment(ValidatedFragment.ITEM_NAME, null, null, false);
|
||||||
|
logError(vf.getErrors(), "2024-06-30", lineCount+1, 1, null, IssueType.INFORMATIONAL, context.formatMessage(I18nConstants.NDJSON_EMPTY_LINE_WARNING), IssueSeverity.WARNING);
|
||||||
|
res.add(vf);
|
||||||
|
} else {
|
||||||
|
res.add(processLine(lineCount, line));
|
||||||
|
}
|
||||||
|
start = cursor+1;
|
||||||
|
}
|
||||||
|
lineCount++;
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void compose(Element e, OutputStream stream, OutputStyle style, String identity) throws FHIRException, IOException {
|
||||||
|
throw new Error("Not done yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ public class ValidatedFragment {
|
||||||
@Getter
|
@Getter
|
||||||
private final boolean isDerivedContent;
|
private final boolean isDerivedContent;
|
||||||
public final static String FOCUS_NAME = "focus";
|
public final static String FOCUS_NAME = "focus";
|
||||||
|
public final static String ITEM_NAME = "item";
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private List<ValidationMessage> errors = new ArrayList<>();
|
private List<ValidationMessage> errors = new ArrayList<>();
|
||||||
|
|
|
@ -1091,4 +1091,5 @@ public class I18nConstants {
|
||||||
public static final String IG_DEPENDENCY_VERSION_WARNING = "IG_DEPENDENCY_VERSION_WARNING";
|
public static final String IG_DEPENDENCY_VERSION_WARNING = "IG_DEPENDENCY_VERSION_WARNING";
|
||||||
public static final String IG_DEPENDENCY_EXCEPTION = "IG_DEPENDENCY_EXCEPTION";
|
public static final String IG_DEPENDENCY_EXCEPTION = "IG_DEPENDENCY_EXCEPTION";
|
||||||
public static final String IG_DEPENDENCY_PACKAGE_UNKNOWN = "IG_DEPENDENCY_PACKAGE_UNKNOWN";
|
public static final String IG_DEPENDENCY_PACKAGE_UNKNOWN = "IG_DEPENDENCY_PACKAGE_UNKNOWN";
|
||||||
|
public static final String NDJSON_EMPTY_LINE_WARNING = "NDJSON_EMPTY_LINE_WARNING";
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,12 +51,12 @@ public class JsonLexer {
|
||||||
private boolean isUnquoted;
|
private boolean isUnquoted;
|
||||||
private String sourceName;
|
private String sourceName;
|
||||||
|
|
||||||
public JsonLexer(String source, boolean allowComments, boolean allowUnquotedStrings) throws IOException {
|
public JsonLexer(String source, boolean allowComments, boolean allowUnquotedStrings, int line) throws IOException {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.allowComments = allowComments;
|
this.allowComments = allowComments;
|
||||||
this.allowUnquotedStrings = allowUnquotedStrings;
|
this.allowUnquotedStrings = allowUnquotedStrings;
|
||||||
cursor = -1;
|
cursor = -1;
|
||||||
location = new JsonLocationData(1, 1);
|
location = new JsonLocationData(line+1, 1);
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,15 @@ import org.hl7.fhir.utilities.json.parser.JsonLexer.TokenType;
|
||||||
*/
|
*/
|
||||||
public class JsonParser {
|
public class JsonParser {
|
||||||
|
|
||||||
|
protected JsonParser() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JsonParser(int line) {
|
||||||
|
super();
|
||||||
|
this.line = line;
|
||||||
|
}
|
||||||
|
|
||||||
public static JsonObject parseObject(InputStream stream) throws IOException, JsonException {
|
public static JsonObject parseObject(InputStream stream) throws IOException, JsonException {
|
||||||
return new JsonParser().parseJsonObject(TextFile.streamToString(stream), false, false);
|
return new JsonParser().parseJsonObject(TextFile.streamToString(stream), false, false);
|
||||||
}
|
}
|
||||||
|
@ -107,6 +116,10 @@ public class JsonParser {
|
||||||
return new JsonParser().parseJsonObject(source, isJson5, allowDuplicates);
|
return new JsonParser().parseJsonObject(source, isJson5, allowDuplicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JsonObject parseObject(String source, boolean isJson5, boolean allowDuplicates, int line) throws IOException, JsonException {
|
||||||
|
return new JsonParser(line).parseJsonObject(source, isJson5, allowDuplicates);
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
public static JsonElement parse(InputStream stream) throws IOException, JsonException {
|
public static JsonElement parse(InputStream stream) throws IOException, JsonException {
|
||||||
|
@ -221,6 +234,7 @@ public class JsonParser {
|
||||||
private boolean itemUnquoted;
|
private boolean itemUnquoted;
|
||||||
private boolean valueUnquoted;
|
private boolean valueUnquoted;
|
||||||
private String sourceName;
|
private String sourceName;
|
||||||
|
private int line = 0;
|
||||||
|
|
||||||
private JsonObject parseJsonObject(String source, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException {
|
private JsonObject parseJsonObject(String source, boolean isJson5, boolean allowDuplicates) throws IOException, JsonException {
|
||||||
this.allowDuplicates = allowDuplicates;
|
this.allowDuplicates = allowDuplicates;
|
||||||
|
@ -231,7 +245,7 @@ public class JsonParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonObject parseSource(String source) throws IOException, JsonException {
|
private JsonObject parseSource(String source) throws IOException, JsonException {
|
||||||
lexer = new JsonLexer(source, allowComments, allowUnquotedStrings);
|
lexer = new JsonLexer(source, allowComments, allowUnquotedStrings, line);
|
||||||
lexer.setSourceName(sourceName);
|
lexer.setSourceName(sourceName);
|
||||||
JsonObject result = new JsonObject();
|
JsonObject result = new JsonObject();
|
||||||
lexer.takeComments(result);
|
lexer.takeComments(result);
|
||||||
|
@ -249,8 +263,14 @@ public class JsonParser {
|
||||||
if (lexer.getType() != TokenType.Close) {
|
if (lexer.getType() != TokenType.Close) {
|
||||||
parseProperty();
|
parseProperty();
|
||||||
readObject("$", result, true);
|
readObject("$", result, true);
|
||||||
|
result.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy());
|
||||||
|
} else {
|
||||||
|
result.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy());
|
||||||
|
lexer.next();
|
||||||
|
}
|
||||||
|
if (lexer.getType() != TokenType.Eof) {
|
||||||
|
throw lexer.error("Unexpected content at end of JSON: "+lexer.getType().toString());
|
||||||
}
|
}
|
||||||
result.setEnd(endProperty != null ? endProperty.copy() : lexer.getLocation().copy());
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +283,7 @@ public class JsonParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonElement parseSourceElement(String source) throws IOException, JsonException {
|
private JsonElement parseSourceElement(String source) throws IOException, JsonException {
|
||||||
lexer = new JsonLexer(source, allowComments, allowUnquotedStrings);
|
lexer = new JsonLexer(source, allowComments, allowUnquotedStrings, line);
|
||||||
switch (lexer.getType()) {
|
switch (lexer.getType()) {
|
||||||
case Boolean:
|
case Boolean:
|
||||||
JsonBoolean bool = new JsonBoolean(lexer.getValue().equals("true"));
|
JsonBoolean bool = new JsonBoolean(lexer.getValue().equals("true"));
|
||||||
|
|
|
@ -1119,3 +1119,4 @@ IG_DEPENDENCY_PACKAGE_UNKNOWN = The package {0} could not be found so the versio
|
||||||
IG_DEPENDENCY_VERSION_ERROR = The ImplementationGuide is based on FHIR version {0} but package {1} is based on FHIR version {2}. Use the package {3} instead
|
IG_DEPENDENCY_VERSION_ERROR = The ImplementationGuide is based on FHIR version {0} but package {1} is based on FHIR version {2}. Use the package {3} instead
|
||||||
IG_DEPENDENCY_VERSION_WARNING = The ImplementationGuide is based on FHIR version {0} but package {1} is based on FHIR version {2}. In general, this version mismatch should be avoided - some tools will try to make this work with variable degrees of success, but others will not even try
|
IG_DEPENDENCY_VERSION_WARNING = The ImplementationGuide is based on FHIR version {0} but package {1} is based on FHIR version {2}. In general, this version mismatch should be avoided - some tools will try to make this work with variable degrees of success, but others will not even try
|
||||||
IG_DEPENDENCY_EXCEPTION = Exception checking package version consistency: {0}
|
IG_DEPENDENCY_EXCEPTION = Exception checking package version consistency: {0}
|
||||||
|
NDJSON_EMPTY_LINE_WARNING = The NDJSON source contains an empty line. This may not be accepted by some processors
|
||||||
|
|
Loading…
Reference in New Issue