All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.fhir.parser.json.jackson.JacksonStructure Maven / Gradle / Ivy

There is a newer version: 7.4.5
Show newest version
/*-
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package ca.uhn.fhir.parser.json.jackson;

import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.parser.json.BaseJsonLikeArray;
import ca.uhn.fhir.parser.json.BaseJsonLikeObject;
import ca.uhn.fhir.parser.json.BaseJsonLikeValue;
import ca.uhn.fhir.parser.json.BaseJsonLikeWriter;
import ca.uhn.fhir.parser.json.JsonLikeStructure;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class JacksonStructure implements JsonLikeStructure {

	private static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
	private JacksonWriter jacksonWriter;
	private ROOT_TYPE rootType = null;
	private JsonNode nativeRoot = null;
	private JsonNode jsonLikeRoot = null;

	public void setNativeObject(ObjectNode objectNode) {
		this.rootType = ROOT_TYPE.OBJECT;
		this.nativeRoot = objectNode;
	}

	public void setNativeArray(ArrayNode arrayNode) {
		this.rootType = ROOT_TYPE.ARRAY;
		this.nativeRoot = arrayNode;
	}

	@Override
	public JsonLikeStructure getInstance() {
		return new JacksonStructure();
	}

	@Override
	public void load(Reader theReader) throws DataFormatException {
		this.load(theReader, false);
	}

	@Override
	public void load(Reader theReader, boolean allowArray) throws DataFormatException {
		PushbackReader pbr = new PushbackReader(theReader);
		int nextInt;
		try {
			while (true) {
				nextInt = pbr.read();
				if (nextInt == -1) {
					throw new DataFormatException(Msg.code(1857) + "Did not find any content to parse");
				}
				if (nextInt == '{') {
					pbr.unread(nextInt);
					break;
				}
				if (Character.isWhitespace(nextInt)) {
					continue;
				}
				if (allowArray) {
					if (nextInt == '[') {
						pbr.unread(nextInt);
						break;
					}
					throw new DataFormatException(Msg.code(1858)
							+ "Content does not appear to be FHIR JSON, first non-whitespace character was: '"
							+ (char) nextInt + "' (must be '{' or '[')");
				}
				throw new DataFormatException(Msg.code(1859)
						+ "Content does not appear to be FHIR JSON, first non-whitespace character was: '"
						+ (char) nextInt + "' (must be '{')");
			}

			if (nextInt == '{') {
				setNativeObject((ObjectNode) OBJECT_MAPPER.readTree(pbr));
			} else {
				setNativeArray((ArrayNode) OBJECT_MAPPER.readTree(pbr));
			}
		} catch (Exception e) {
			String message;
			if (e instanceof JsonProcessingException) {
				/*
				 * Currently there is no way of preventing Jackson from adding this
				 * annoying REDACTED message from certain messages we get back from
				 * the parser, so we just manually strip them. Hopefully Jackson
				 * will accept this request at some point:
				 * https://github.com/FasterXML/jackson-core/issues/1158
				 */
				JsonProcessingException jpe = (JsonProcessingException) e;
				StringBuilder messageBuilder = new StringBuilder();
				String originalMessage = jpe.getOriginalMessage();
				originalMessage = originalMessage.replace(
						"Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); ", "");
				messageBuilder.append(originalMessage);
				if (jpe.getLocation() != null) {
					messageBuilder.append("\n at [");
					jpe.getLocation().appendOffsetDescription(messageBuilder);
					messageBuilder.append("]");
				}
				message = messageBuilder.toString();
			} else {
				message = e.getMessage();
			}

			if (message.startsWith("Unexpected char 39")) {
				throw new DataFormatException(
						Msg.code(1860) + "Failed to parse JSON encoded FHIR content: " + message + " - "
								+ "This may indicate that single quotes are being used as JSON escapes where double quotes are required",
						e);
			}
			throw new DataFormatException(Msg.code(1861) + "Failed to parse JSON encoded FHIR content: " + message, e);
		}
	}

	@Override
	public BaseJsonLikeWriter getJsonLikeWriter(Writer writer) throws IOException {
		if (null == jacksonWriter) {
			jacksonWriter = new JacksonWriter(OBJECT_MAPPER.getFactory(), writer);
		}

		return jacksonWriter;
	}

	@Override
	public BaseJsonLikeWriter getJsonLikeWriter() {
		if (null == jacksonWriter) {
			jacksonWriter = new JacksonWriter();
		}
		return jacksonWriter;
	}

	@Override
	public BaseJsonLikeObject getRootObject() throws DataFormatException {
		if (rootType == ROOT_TYPE.OBJECT) {
			if (null == jsonLikeRoot) {
				jsonLikeRoot = nativeRoot;
			}

			return new JacksonJsonObject((ObjectNode) jsonLikeRoot);
		}

		throw new DataFormatException(Msg.code(1862) + "Content must be a valid JSON Object. It must start with '{'.");
	}

	private enum ROOT_TYPE {
		OBJECT,
		ARRAY
	}

	private static class JacksonJsonObject extends BaseJsonLikeObject {
		private final ObjectNode nativeObject;

		public JacksonJsonObject(ObjectNode json) {
			this.nativeObject = json;
		}

		@Override
		public Object getValue() {
			return null;
		}

		@Override
		public Iterator keyIterator() {
			return nativeObject.fieldNames();
		}

		@Override
		public BaseJsonLikeValue get(String key) {
			JsonNode child = nativeObject.get(key);
			if (child != null) {
				return new JacksonJsonValue(child);
			}
			return null;
		}
	}

	private static class EntryOrderedSet extends AbstractSet {
		private final transient ArrayList data;

		public EntryOrderedSet() {
			data = new ArrayList<>();
		}

		@Override
		public int size() {
			return data.size();
		}

		@Override
		public boolean contains(Object o) {
			return data.contains(o);
		}

		public T get(int index) {
			return data.get(index);
		}

		@Override
		public boolean add(T element) {
			if (data.contains(element)) {
				return false;
			}
			return data.add(element);
		}

		@Override
		public boolean remove(Object o) {
			return data.remove(o);
		}

		@Override
		public void clear() {
			data.clear();
		}

		@Override
		public Iterator iterator() {
			return data.iterator();
		}
	}

	private static class JacksonJsonArray extends BaseJsonLikeArray {
		private final ArrayNode nativeArray;
		private final Map jsonLikeMap = new LinkedHashMap();

		public JacksonJsonArray(ArrayNode json) {
			this.nativeArray = json;
		}

		@Override
		public Object getValue() {
			return null;
		}

		@Override
		public int size() {
			return nativeArray.size();
		}

		@Override
		public BaseJsonLikeValue get(int index) {
			Integer key = index;
			BaseJsonLikeValue result = null;
			if (jsonLikeMap.containsKey(key)) {
				result = jsonLikeMap.get(key);
			} else {
				JsonNode child = nativeArray.get(index);
				if (child != null) {
					result = new JacksonJsonValue(child);
				}
				jsonLikeMap.put(key, result);
			}
			return result;
		}
	}

	private static class JacksonJsonValue extends BaseJsonLikeValue {
		private final JsonNode nativeValue;
		private BaseJsonLikeObject jsonLikeObject = null;
		private BaseJsonLikeArray jsonLikeArray = null;

		public JacksonJsonValue(JsonNode jsonNode) {
			this.nativeValue = jsonNode;
		}

		@Override
		public Object getValue() {
			if (nativeValue != null && nativeValue.isValueNode()) {
				if (nativeValue.isNumber()) {
					return nativeValue.numberValue();
				}

				if (nativeValue.isBoolean()) {
					return nativeValue.booleanValue();
				}

				return nativeValue.asText();
			}
			return null;
		}

		@Override
		public ValueType getJsonType() {
			if (null == nativeValue) {
				return ValueType.NULL;
			}

			switch (nativeValue.getNodeType()) {
				case NULL:
				case MISSING:
					return ValueType.NULL;
				case OBJECT:
					return ValueType.OBJECT;
				case ARRAY:
					return ValueType.ARRAY;
				case POJO:
				case BINARY:
				case STRING:
				case NUMBER:
				case BOOLEAN:
				default:
					break;
			}

			return ValueType.SCALAR;
		}

		@Override
		public ScalarType getDataType() {
			if (nativeValue != null && nativeValue.isValueNode()) {
				if (nativeValue.isNumber()) {
					return ScalarType.NUMBER;
				}
				if (nativeValue.isTextual()) {
					return ScalarType.STRING;
				}
				if (nativeValue.isBoolean()) {
					return ScalarType.BOOLEAN;
				}
			}
			return null;
		}

		@Override
		public BaseJsonLikeArray getAsArray() {
			if (nativeValue != null && nativeValue.isArray()) {
				if (null == jsonLikeArray) {
					jsonLikeArray = new JacksonJsonArray((ArrayNode) nativeValue);
				}
			}
			return jsonLikeArray;
		}

		@Override
		public BaseJsonLikeObject getAsObject() {
			if (nativeValue != null && nativeValue.isObject()) {
				if (null == jsonLikeObject) {
					jsonLikeObject = new JacksonJsonObject((ObjectNode) nativeValue);
				}
			}
			return jsonLikeObject;
		}

		@Override
		public Number getAsNumber() {
			return nativeValue != null ? nativeValue.numberValue() : null;
		}

		@Override
		public String getAsString() {
			if (nativeValue != null) {
				if (nativeValue instanceof DecimalNode) {
					BigDecimal value = nativeValue.decimalValue();
					return value.toPlainString();
				}
				return nativeValue.asText();
			}
			return null;
		}

		@Override
		public boolean getAsBoolean() {
			if (nativeValue != null && nativeValue.isValueNode() && nativeValue.isBoolean()) {
				return nativeValue.asBoolean();
			}
			return super.getAsBoolean();
		}
	}

	private static ObjectMapper createObjectMapper() {
		ObjectMapper retVal = JsonMapper.builder()
				.enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS)
				.build();
		retVal = retVal.setNodeFactory(new JsonNodeFactory(true));
		retVal = retVal.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
		retVal = retVal.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
		retVal = retVal.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION);
		retVal = retVal.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
		retVal = retVal.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
		retVal = retVal.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);

		retVal.getFactory().setStreamReadConstraints(createStreamReadConstraints());

		return retVal;
	}

	private static StreamReadConstraints createStreamReadConstraints() {
		return StreamReadConstraints.builder()
				.maxStringLength(Integer.MAX_VALUE)
				.build();
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy