/* * Copyright (c) 2010-2016 Evolveum * * 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. */ package com.evolveum.midpoint.prism.lex.json; import java.io.*; import java.util.*; import java.util.Map.Entry; import javax.xml.namespace.QName; import com.evolveum.midpoint.prism.ParserSource; import com.evolveum.midpoint.prism.ParsingContext; import com.evolveum.midpoint.prism.SerializationContext; import com.evolveum.midpoint.prism.SerializationOptions; import com.evolveum.midpoint.prism.lex.LexicalProcessor; import com.evolveum.midpoint.prism.lex.LexicalUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.JsonParser; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.prism.xnode.ListXNode; import com.evolveum.midpoint.prism.xnode.MapXNode; import com.evolveum.midpoint.prism.xnode.PrimitiveXNode; import com.evolveum.midpoint.prism.xnode.RootXNode; import com.evolveum.midpoint.prism.xnode.SchemaXNode; import com.evolveum.midpoint.prism.xnode.ValueParser; import com.evolveum.midpoint.prism.xnode.XNode; import com.evolveum.midpoint.util.DOMUtil; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.prism.xml.ns._public.types_3.ItemPathType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; public abstract class AbstractJsonLexicalProcessor implements LexicalProcessor<String> { private static final Trace LOGGER = TraceManager.getTrace(AbstractJsonLexicalProcessor.class); private static final String PROP_NAMESPACE = "@ns"; private static final String PROP_TYPE = "@type"; private static final String PROP_ELEMENT = "@element"; private static final String PROP_VALUE = "@value"; //region Parsing implementation @NotNull @Override public RootXNode read(@NotNull ParserSource source, @NotNull ParsingContext parsingContext) throws SchemaException, IOException { InputStream is = source.getInputStream(); try { JsonParser parser = createJacksonParser(is); return parseFromStart(parser, parsingContext); } finally { if (source.closeStreamAfterParsing()) { IOUtils.closeQuietly(is); } } } @NotNull @Override public List<RootXNode> readObjects(ParserSource source, ParsingContext parsingContext) throws SchemaException, IOException { throw new UnsupportedOperationException("Parse objects not supported for json and yaml."); // why? } protected abstract JsonParser createJacksonParser(InputStream stream) throws SchemaException, IOException; class JsonParsingContext { @NotNull final JsonParser parser; @NotNull final ParsingContext prismParsingContext; // Definitions of namespaces ('@ns') within maps; to be applied after parsing. @NotNull final IdentityHashMap<MapXNode, String> defaultNamespaces = new IdentityHashMap<>(); // Entries that should be skipped when filling-in default namespaces - those that are explicitly set with no-NS ('#name'). // (Values for these entries are not important. Only key presence is relevant.) @NotNull final IdentityHashMap<Entry<QName,XNode>, Object> noNamespaceEntries = new IdentityHashMap<>(); @NotNull final IdentityHashMap<XNode, Object> noNamespaceElementNames = new IdentityHashMap<>(); JsonParsingContext(@NotNull JsonParser parser, @NotNull ParsingContext prismParsingContext) { this.parser = parser; this.prismParsingContext = prismParsingContext; } } @NotNull private RootXNode parseFromStart(JsonParser unconfiguredParser, ParsingContext parsingContext) throws SchemaException { JsonParsingContext ctx = null; try { JsonParser parser = configureParser(unconfiguredParser); parser.nextToken(); if (parser.currentToken() == null) { throw new SchemaException("Nothing to parse: the input is empty."); } ctx = new JsonParsingContext(parser, parsingContext); XNode xnode = parseValue(ctx); if (!(xnode instanceof MapXNode) || ((MapXNode) xnode).size() != 1) { throw new SchemaException("Expected MapXNode with a single key; got " + xnode + " instead. At " + getPositionSuffix(ctx)); } processDefaultNamespaces(xnode, null, ctx); processSchemaNodes(xnode); Entry<QName, XNode> entry = ((MapXNode) xnode).entrySet().iterator().next(); RootXNode root = new RootXNode(entry.getKey(), entry.getValue()); if (entry.getValue() != null) { root.setTypeQName(entry.getValue().getTypeQName()); // TODO - ok ???? } return root; } catch (IOException e) { throw new SchemaException("Cannot parse JSON/YAML object: " + e.getMessage() + (ctx != null ? " At: " + getPositionSuffix(ctx) : ""), e); } } // Default namespaces (@ns properties) are processed in the second pass, because they might be present within an object // at any place, even at the end. private void processDefaultNamespaces(XNode xnode, String parentDefault, JsonParsingContext ctx) { if (xnode instanceof MapXNode) { MapXNode map = (MapXNode) xnode; final String currentDefault = ctx.defaultNamespaces.containsKey(map) ? ctx.defaultNamespaces.get(map) : parentDefault; for (Entry<QName, XNode> entry : map.entrySet()) { QName fieldName = entry.getKey(); XNode subnode = entry.getValue(); if (StringUtils.isNotEmpty(currentDefault) && StringUtils.isEmpty(fieldName.getNamespaceURI()) && !ctx.noNamespaceEntries.containsKey(entry)) { map.qualifyKey(fieldName, currentDefault); } processDefaultNamespaces(subnode, currentDefault, ctx); } qualifyElementNameIfNeeded(map, currentDefault, ctx); } else { qualifyElementNameIfNeeded(xnode, parentDefault, ctx); if (xnode instanceof ListXNode) { for (XNode item : (ListXNode) xnode) { processDefaultNamespaces(item, parentDefault, ctx); } } } } private void qualifyElementNameIfNeeded(XNode node, String namespace, JsonParsingContext ctx) { if (node.getElementName() != null && QNameUtil.noNamespace(node.getElementName()) && StringUtils.isNotEmpty(namespace) && !ctx.noNamespaceElementNames.containsKey(node)) { node.setElementName(new QName(namespace, node.getElementName().getLocalPart())); } } // Schema nodes can be detected only after namespaces are resolved. // We simply convert primitive nodes to schema ones. private void processSchemaNodes(XNode xnode) throws SchemaException, IOException { if (xnode instanceof MapXNode) { MapXNode map = (MapXNode) xnode; XNode schemaNode = null; for (Entry<QName, XNode> entry : map.entrySet()) { QName fieldName = entry.getKey(); XNode subnode = entry.getValue(); if (DOMUtil.XSD_SCHEMA_ELEMENT.equals(fieldName)) { schemaNode = subnode; } else { processSchemaNodes(subnode); } } if (schemaNode != null) { if (schemaNode instanceof PrimitiveXNode) { PrimitiveXNode<?> primitiveXNode = (PrimitiveXNode<?>) schemaNode ; if (primitiveXNode.isParsed()) { throw new SchemaException("Cannot convert from PrimitiveXNode to SchemaXNode: node is already parsed: " + primitiveXNode); } SchemaXNode schemaXNode = new SchemaXNode(); map.replace(DOMUtil.XSD_SCHEMA_ELEMENT, schemaXNode); schemaXNode.setSchemaElement(((JsonValueParser) primitiveXNode.getValueParser()).asDomElement()); } else { throw new SchemaException("Cannot convert 'schema' field to SchemaXNode: not a PrimitiveNode but " + schemaNode); } } } else if (xnode instanceof ListXNode) { for (XNode item : (ListXNode) xnode) { processSchemaNodes(item); } } } @NotNull private XNode parseValue(JsonParsingContext ctx) throws IOException, SchemaException { Validate.notNull(ctx.parser.currentToken()); switch (ctx.parser.currentToken()) { case START_OBJECT: return parseJsonObject(ctx); case START_ARRAY: return parseToList(ctx); case VALUE_STRING: case VALUE_TRUE: case VALUE_FALSE: case VALUE_NUMBER_FLOAT: case VALUE_NUMBER_INT: return parseToPrimitive(ctx); case VALUE_NULL: return parseToEmptyPrimitive(); default: throw new SchemaException("Unexpected current token: " + ctx.parser.currentToken()); } } /** * Normally returns a MapXNode. However, JSON primitives/lists can be simulated by two-member object (@type + @value); in these cases we return respective XNode. */ @NotNull private XNode parseJsonObject(JsonParsingContext ctx) throws SchemaException, IOException { Validate.notNull(ctx.parser.currentToken()); QName typeName = null; QNameUtil.QNameInfo elementNameInfo = null; Object tid = ctx.parser.getTypeId(); if (tid != null) { typeName = tagToTypeName(tid, ctx); } final MapXNode map = new MapXNode(); XNode wrappedValue = null; boolean defaultNamespaceDefined = false; QNameUtil.QNameInfo currentFieldNameInfo = null; for (;;) { JsonToken token = ctx.parser.nextToken(); if (token == null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Unexpected end of data while parsing a map structure at " + getPositionSuffix(ctx)); break; } else if (token == JsonToken.END_OBJECT) { break; } else if (token == JsonToken.FIELD_NAME) { String newFieldName = ctx.parser.getCurrentName(); if (currentFieldNameInfo != null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Two field names in succession: " + currentFieldNameInfo + " and " + newFieldName); } currentFieldNameInfo = QNameUtil.uriToQNameInfo(newFieldName, true); } else { XNode valueXNode = parseValue(ctx); assert currentFieldNameInfo != null; if (isSpecial(currentFieldNameInfo.name)) { if (isNamespaceDeclaration(currentFieldNameInfo.name)) { if (defaultNamespaceDefined) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Default namespace defined more than once at " + getPositionSuffix(ctx)); } ctx.defaultNamespaces.put(map, getStringValue(valueXNode, currentFieldNameInfo, ctx)); defaultNamespaceDefined = true; } else if (isTypeDeclaration(currentFieldNameInfo.name)) { if (typeName != null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Value type defined more than once at " + getPositionSuffix(ctx)); } typeName = QNameUtil.uriToQName(getStringValue(valueXNode, currentFieldNameInfo, ctx), true); } else if (isElementDeclaration(currentFieldNameInfo.name)) { if (elementNameInfo != null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Element name defined more than once at " + getPositionSuffix(ctx)); } elementNameInfo = QNameUtil.uriToQNameInfo(getStringValue(valueXNode, currentFieldNameInfo, ctx), true); } else if (isValue(currentFieldNameInfo.name)) { if (wrappedValue != null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Value ('" + PROP_VALUE + "') defined more than once at " + getPositionSuffix(ctx)); } wrappedValue = valueXNode; } } else { Map.Entry<QName, XNode> entry = map.putReturningEntry(currentFieldNameInfo.name, valueXNode); if (currentFieldNameInfo.explicitEmptyNamespace) { ctx.noNamespaceEntries.put(entry, null); } } currentFieldNameInfo = null; } } // Return either map or primitive value (in case of @type/@value) XNode rv; if (wrappedValue != null) { if (!map.isEmpty()) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Both '" + PROP_VALUE + "' and regular content present at " + getPositionSuffix(ctx)); rv = map; } else { rv = wrappedValue; } } else { rv = map; } if (typeName != null) { if (wrappedValue != null && wrappedValue.getTypeQName() != null && !wrappedValue.getTypeQName().equals(typeName)) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Conflicting type names for '" + PROP_VALUE + "' (" + wrappedValue.getTypeQName() + ") and regular content (" + typeName + ") present at " + getPositionSuffix(ctx)); } rv.setTypeQName(typeName); rv.setExplicitTypeDeclaration(true); } if (elementNameInfo != null) { if (wrappedValue != null && wrappedValue.getElementName() != null) { boolean wrappedValueElementNoNamespace = ctx.noNamespaceElementNames.containsKey(wrappedValue); if (!wrappedValue.getElementName().equals(elementNameInfo.name) || wrappedValueElementNoNamespace != elementNameInfo.explicitEmptyNamespace) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Conflicting element names for '" + PROP_VALUE + "' (" + wrappedValue.getElementName() + "; no NS=" + wrappedValueElementNoNamespace + ") and regular content (" + elementNameInfo.name + "; no NS=" + elementNameInfo.explicitEmptyNamespace + ") present at " + getPositionSuffix(ctx)); } } rv.setElementName(elementNameInfo.name); if (elementNameInfo.explicitEmptyNamespace) { ctx.noNamespaceElementNames.put(rv, null); } } return rv; } private String getStringValue(XNode valueXNode, QNameUtil.QNameInfo currentFieldNameInfo, JsonParsingContext ctx) throws SchemaException { String stringValue; if (!(valueXNode instanceof PrimitiveXNode)) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Value of '" + currentFieldNameInfo + "' attribute must be a primitive one. It is " + valueXNode + " instead. At " + getPositionSuffix(ctx)); stringValue = ""; } else { stringValue = ((PrimitiveXNode<?>) valueXNode).getStringValue(); } return stringValue; } private boolean isSpecial(QName fieldName) { return isTypeDeclaration(fieldName) || isElementDeclaration(fieldName) || isNamespaceDeclaration(fieldName) || isValue(fieldName); } private boolean isTypeDeclaration(QName fieldName) { return new QName(PROP_TYPE).equals(fieldName); } private boolean isElementDeclaration(QName fieldName) { return new QName(PROP_ELEMENT).equals(fieldName); } private boolean isNamespaceDeclaration(QName fieldName) { return new QName(PROP_NAMESPACE).equals(fieldName); } private boolean isValue(QName fieldName) { return new QName(PROP_VALUE).equals(fieldName); } private String getPositionSuffix(JsonParsingContext ctx) { return String.valueOf(ctx.parser.getCurrentLocation()); } private ListXNode parseToList(JsonParsingContext ctx) throws SchemaException, IOException { Validate.notNull(ctx.parser.currentToken()); ListXNode list = new ListXNode(); Object tid = ctx.parser.getTypeId(); if (tid != null) { list.setTypeQName(tagToTypeName(tid, ctx)); } for (;;) { JsonToken token = ctx.parser.nextToken(); if (token == null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Unexpected end of data while parsing a list structure at " + getPositionSuffix(ctx)); return list; } else if (token == JsonToken.END_ARRAY) { return list; } else { list.add(parseValue(ctx)); } } } private <T> PrimitiveXNode<T> parseToPrimitive(JsonParsingContext ctx) throws IOException, SchemaException { PrimitiveXNode<T> primitive = new PrimitiveXNode<T>(); Object tid = ctx.parser.getTypeId(); if (tid != null) { QName typeName = tagToTypeName(tid, ctx); primitive.setTypeQName(typeName); primitive.setExplicitTypeDeclaration(true); } else { // We don't try to determine XNode type from the implicit JSON/YAML type (integer, number, ...), // because XNode type prescribes interpretation in midPoint. E.g. YAML string type would be interpreted // as xsd:string, even if the schema would expect e.g. timestamp. } JsonNode jn = ctx.parser.readValueAs(JsonNode.class); ValueParser<T> vp = new JsonValueParser<T>(ctx.parser, jn); primitive.setValueParser(vp); return primitive; } private <T> PrimitiveXNode<T> parseToEmptyPrimitive() throws IOException, SchemaException { PrimitiveXNode<T> primitive = new PrimitiveXNode<T>(); primitive.setValueParser(new JsonNullValueParser<T>()); return primitive; } // TODO remove if not needed private QName getCurrentTypeName(JsonParsingContext ctx) throws IOException, SchemaException { switch (ctx.parser.currentToken()) { case VALUE_NUMBER_INT: case VALUE_NUMBER_FLOAT: return determineNumberType(ctx.parser.getNumberType()); case VALUE_FALSE: case VALUE_TRUE: return DOMUtil.XSD_BOOLEAN; case VALUE_STRING: return DOMUtil.XSD_STRING; case VALUE_NULL: return null; // TODO? default: throw new SchemaException("Unexpected current token type: " + ctx.parser.currentToken() + "/" + ctx.parser.getText() + " at " + getPositionSuffix(ctx)); } } QName determineNumberType(JsonParser.NumberType numberType) throws SchemaException { switch (numberType) { case BIG_DECIMAL: return DOMUtil.XSD_DECIMAL; case BIG_INTEGER: return DOMUtil.XSD_INTEGER; case LONG: return DOMUtil.XSD_LONG; case INT: return DOMUtil.XSD_INT; case FLOAT: return DOMUtil.XSD_FLOAT; case DOUBLE: return DOMUtil.XSD_DOUBLE; default: throw new SchemaException("Unsupported number type: " + numberType); } } protected abstract QName tagToTypeName(Object tid, JsonParsingContext ctx) throws IOException, SchemaException; private JsonParser configureParser(JsonParser parser) { ObjectMapper mapper = new ObjectMapper(); SimpleModule sm = new SimpleModule(); sm.addDeserializer(QName.class, new QNameDeserializer()); sm.addDeserializer(ItemPath.class, new ItemPathDeserializer()); sm.addDeserializer(PolyString.class, new PolyStringDeserializer()); sm.addDeserializer(ItemPathType.class, new ItemPathTypeDeserializer()); mapper.registerModule(sm); parser.setCodec(mapper); return parser; } //endregion //region Serialization implementation class JsonSerializationContext { @NotNull final JsonGenerator generator; @NotNull private final SerializationContext prismSerializationContext; private String currentNamespace; private JsonSerializationContext(@NotNull JsonGenerator generator, @Nullable SerializationContext prismSerializationContext) { this.generator = generator; this.prismSerializationContext = prismSerializationContext != null ? prismSerializationContext : new SerializationContext(null); } } @NotNull @Override public String write(@NotNull XNode xnode, @NotNull QName rootElementName, SerializationContext serializationContext) throws SchemaException { return write(LexicalUtils.createRootXNode(xnode, rootElementName), serializationContext); } protected abstract JsonGenerator createJacksonGenerator(StringWriter out) throws SchemaException; @NotNull @Override public String write(@NotNull RootXNode root, SerializationContext prismSerializationContext) throws SchemaException { StringWriter out = new StringWriter(); try ( JsonGenerator generator = createJacksonGenerator(out) ) { JsonSerializationContext ctx = new JsonSerializationContext(generator, prismSerializationContext); serialize(root.toMapXNode(), ctx, false); // TODO default namespace } catch (IOException ex) { throw new SchemaException("Error during serializing to JSON/YAML: " + ex.getMessage(), ex); } return out.toString(); } private void serialize(XNode xnode, JsonSerializationContext ctx, boolean inValueWrapMode) throws IOException { if (xnode instanceof MapXNode) { serializeFromMap((MapXNode) xnode, ctx); } else if (xnode == null) { serializeFromNull(ctx); } else if (needsValueWrapping(xnode) && !inValueWrapMode) { ctx.generator.writeStartObject(); resetInlineTypeIfPossible(ctx); writeAuxiliaryInformation(xnode, ctx); ctx.generator.writeFieldName(PROP_VALUE); serialize(xnode, ctx, true); ctx.generator.writeEndObject(); } else if (xnode instanceof ListXNode) { serializeFromList((ListXNode) xnode, ctx); } else if (xnode instanceof PrimitiveXNode) { serializeFromPrimitive((PrimitiveXNode<?>) xnode, ctx); } else if (xnode instanceof SchemaXNode) { serializeFromSchema((SchemaXNode) xnode, ctx); } else { throw new UnsupportedOperationException("Cannot serialize from " + xnode); } } private void serializeFromNull(JsonSerializationContext ctx) throws IOException { ctx.generator.writeNull(); } private void writeAuxiliaryInformation(XNode xnode, JsonSerializationContext ctx) throws IOException { QName elementName = xnode.getElementName(); if (elementName != null) { ctx.generator.writeObjectField(PROP_ELEMENT, createElementNameUri(elementName, ctx)); } QName typeName = getExplicitType(xnode); if (typeName != null) { if (!supportsInlineTypes()) { ctx.generator.writeObjectField(PROP_TYPE, typeName); } } } private boolean needsValueWrapping(XNode xnode) { return xnode.getElementName() != null || getExplicitType(xnode) != null && !supportsInlineTypes(); } protected abstract boolean supportsInlineTypes(); protected abstract void writeInlineType(QName typeName, JsonSerializationContext ctx) throws IOException; private void serializeFromMap(MapXNode map, JsonSerializationContext ctx) throws IOException { writeInlineTypeIfNeeded(map, ctx); ctx.generator.writeStartObject(); resetInlineTypeIfPossible(ctx); String oldDefaultNamespace = ctx.currentNamespace; generateNsDeclarationIfNeeded(map, ctx); writeAuxiliaryInformation(map, ctx); for (Entry<QName,XNode> entry : map.entrySet()) { if (entry.getValue() == null) { continue; } ctx.generator.writeFieldName(createKeyUri(entry, ctx)); serialize(entry.getValue(), ctx, false); } ctx.generator.writeEndObject(); ctx.currentNamespace = oldDefaultNamespace; } protected void resetInlineTypeIfPossible(JsonSerializationContext ctx) { } private void generateNsDeclarationIfNeeded(MapXNode map, JsonSerializationContext ctx) throws IOException { SerializationOptions opts = ctx.prismSerializationContext.getOptions(); if (!SerializationOptions.isUseNsProperty(opts) || map.isEmpty()) { return; } String namespace = determineNewCurrentNamespace(map, ctx); if (namespace != null && !StringUtils.equals(namespace, ctx.currentNamespace)) { ctx.currentNamespace = namespace; ctx.generator.writeFieldName(PROP_NAMESPACE); ctx.generator.writeString(namespace); } } private String determineNewCurrentNamespace(MapXNode map, JsonSerializationContext ctx) { Map<String,Integer> counts = new HashMap<>(); for (QName childName : map.keySet()) { String childNs = childName.getNamespaceURI(); if (StringUtils.isEmpty(childNs)) { continue; } if (childNs.equals(ctx.currentNamespace)) { return ctx.currentNamespace; // found existing => continue with it } increaseCounter(counts, childNs); } if (map.getElementName() != null && QNameUtil.hasNamespace(map.getElementName())) { increaseCounter(counts, map.getElementName().getNamespaceURI()); } // otherwise, take the URI that occurs the most in the map Entry<String,Integer> max = null; for (Entry<String,Integer> count : counts.entrySet()) { if (max == null || count.getValue() > max.getValue()) { max = count; } } return max != null ? max.getKey() : null; } private void increaseCounter(Map<String, Integer> counts, String childNs) { Integer c = counts.get(childNs); counts.put(childNs, c != null ? c+1 : 1); } private String createKeyUri(Entry<QName,XNode> entry, JsonSerializationContext ctx) { QName key = entry.getKey(); if (namespaceMatch(ctx.currentNamespace, key.getNamespaceURI())) { return key.getLocalPart(); } else if (StringUtils.isNotEmpty(ctx.currentNamespace) && !isAttribute(entry.getValue())) { return QNameUtil.qNameToUri(key, true); // items with no namespace should be written as such (starting with '#') } else { return QNameUtil.qNameToUri(key, false); // items with no namespace can be written in plain } } private String createElementNameUri(QName elementName, JsonSerializationContext ctx) { if (namespaceMatch(ctx.currentNamespace, elementName.getNamespaceURI())) { return elementName.getLocalPart(); } else { return QNameUtil.qNameToUri(elementName, StringUtils.isNotEmpty(ctx.currentNamespace)); } } private boolean isAttribute(XNode node) { return node instanceof PrimitiveXNode && ((PrimitiveXNode) node).isAttribute(); } private boolean namespaceMatch(String currentNamespace, String itemNamespace) { if (StringUtils.isEmpty(currentNamespace)) { return StringUtils.isEmpty(itemNamespace); } else { return currentNamespace.equals(itemNamespace); } } private void serializeFromList(ListXNode list, JsonSerializationContext ctx) throws IOException { writeInlineTypeIfNeeded(list, ctx); ctx.generator.writeStartArray(); resetInlineTypeIfPossible(ctx); for (XNode item : list) { serialize(item, ctx, false); } ctx.generator.writeEndArray(); } private void writeInlineTypeIfNeeded(XNode node, JsonSerializationContext ctx) throws IOException { QName explicitType = getExplicitType(node); if (supportsInlineTypes() && explicitType != null) { writeInlineType(explicitType, ctx); } } private void serializeFromSchema(SchemaXNode node, JsonSerializationContext ctx) throws IOException { writeInlineTypeIfNeeded(node, ctx); ctx.generator.writeObject(node.getSchemaElement()); } private <T> void serializeFromPrimitive(PrimitiveXNode<T> primitive, JsonSerializationContext ctx) throws IOException { writeInlineTypeIfNeeded(primitive, ctx); if (primitive.isParsed()) { ctx.generator.writeObject(primitive.getValue()); } else { ctx.generator.writeObject(primitive.getStringValue()); } } protected QName getExplicitType(XNode xnode) { return xnode.isExplicitTypeDeclaration() ? xnode.getTypeQName() : null; } private String serializeNsIfNeeded(QName subNodeName, String globalNamespace, JsonGenerator generator) throws JsonGenerationException, IOException{ if (subNodeName == null){ return globalNamespace; } String subNodeNs = subNodeName.getNamespaceURI(); if (StringUtils.isNotBlank(subNodeNs)){ if (!subNodeNs.equals(globalNamespace)){ globalNamespace = subNodeNs; generator.writeStringField(PROP_NAMESPACE, globalNamespace); } } return globalNamespace; } //endregion }