From 0385315011e94e9ee972b7aeb4ff9cb5434fe290 Mon Sep 17 00:00:00 2001 From: Pavol Mederly Date: Tue, 4 Oct 2016 17:47:00 +0200 Subject: [PATCH] Returning back more intelligent serialization of JSON/YAML. Correctly parsing "explicit no-NS" nodes. --- .../prism/parser/json/AbstractJsonParser.java | 100 ++++++++++++++---- .../midpoint/prism/xnode/MapXNode.java | 19 +++- .../midpoint/prism/TestPrismParsing.java | 2 +- infra/util/pom.xml | 4 + .../com/evolveum/midpoint/util/QNameUtil.java | 26 +++-- 5 files changed, 123 insertions(+), 28 deletions(-) diff --git a/infra/prism/src/main/java/com/evolveum/midpoint/prism/parser/json/AbstractJsonParser.java b/infra/prism/src/main/java/com/evolveum/midpoint/prism/parser/json/AbstractJsonParser.java index 9ff13785bef..21ea290ebb4 100644 --- a/infra/prism/src/main/java/com/evolveum/midpoint/prism/parser/json/AbstractJsonParser.java +++ b/infra/prism/src/main/java/com/evolveum/midpoint/prism/parser/json/AbstractJsonParser.java @@ -104,7 +104,11 @@ public Collection parseCollection(String dataString, ParsingContext parsi class JsonParsingContext { @NotNull final JsonParser parser; @NotNull final ParsingContext prismParsingContext; + // Definitions of namespaces ('@ns') within maps; to be applied after parsing. @NotNull final IdentityHashMap 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, Object> noNamespaceEntries = new IdentityHashMap<>(); JsonParsingContext(@NotNull JsonParser parser, @NotNull ParsingContext prismParsingContext) { this.parser = parser; this.prismParsingContext = prismParsingContext; @@ -149,7 +153,9 @@ private void processDefaultNamespaces(XNode xnode, String parentDefault, JsonPar for (Entry entry : map.entrySet()) { QName fieldName = entry.getKey(); XNode subnode = entry.getValue(); - if (StringUtils.isNotEmpty(currentDefault) && StringUtils.isEmpty(fieldName.getNamespaceURI())) { + if (StringUtils.isNotEmpty(currentDefault) + && StringUtils.isEmpty(fieldName.getNamespaceURI()) + && !ctx.noNamespaceEntries.containsKey(entry)) { map.qualifyKey(fieldName, currentDefault); } processDefaultNamespaces(subnode, currentDefault, ctx); @@ -229,14 +235,13 @@ private XNode parseJsonObject(JsonParsingContext ctx) throws SchemaException, IO Object tid = ctx.parser.getTypeId(); if (tid != null) { -// System.out.println("tid for object = '" + tid + "' for " + ctx.parser.getText()); typeName = tagToTypeName(tid, ctx); } final MapXNode map = new MapXNode(); PrimitiveXNode primitiveValue = null; boolean defaultNamespaceDefined = false; - QName currentFieldName = null; + QNameUtil.QNameInfo currentFieldNameInfo = null; for (;;) { JsonToken token = ctx.parser.nextToken(); if (token == null) { @@ -246,33 +251,33 @@ private XNode parseJsonObject(JsonParsingContext ctx) throws SchemaException, IO break; } else if (token == JsonToken.FIELD_NAME) { String newFieldName = ctx.parser.getCurrentName(); - if (currentFieldName != null) { - ctx.prismParsingContext.warnOrThrow(LOGGER, "Two field names in succession: " + currentFieldName + " and " + newFieldName); + if (currentFieldNameInfo != null) { + ctx.prismParsingContext.warnOrThrow(LOGGER, "Two field names in succession: " + currentFieldNameInfo + " and " + newFieldName); } - currentFieldName = QNameUtil.uriToQName(newFieldName, true); + currentFieldNameInfo = QNameUtil.uriToQNameInfo(newFieldName, true); } else { XNode valueXNode = parseValue(ctx); - if (isSpecial(currentFieldName)) { + if (isSpecial(currentFieldNameInfo.name)) { String stringValue; if (!(valueXNode instanceof PrimitiveXNode)) { - ctx.prismParsingContext.warnOrThrow(LOGGER, "Value of '" + currentFieldName + "' attribute must be a primitive one. It is " + valueXNode + " instead. At " + getPositionSuffix(ctx)); + 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(); } - if (isNamespaceDeclaration(currentFieldName)) { + if (isNamespaceDeclaration(currentFieldNameInfo.name)) { if (defaultNamespaceDefined) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Default namespace defined more than once at " + getPositionSuffix(ctx)); } ctx.defaultNamespaces.put(map, stringValue); defaultNamespaceDefined = true; - } else if (isTypeDeclaration(currentFieldName)) { + } else if (isTypeDeclaration(currentFieldNameInfo.name)) { if (typeName != null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Value type defined more than once at " + getPositionSuffix(ctx)); } typeName = QNameUtil.uriToQName(stringValue, true); - } else if (isValue(currentFieldName)) { + } else if (isValue(currentFieldNameInfo.name)) { if (primitiveValue != null) { ctx.prismParsingContext.warnOrThrow(LOGGER, "Primitive value ('" + PROP_VALUE + "') defined more than once at " + getPositionSuffix(ctx)); } @@ -281,9 +286,12 @@ private XNode parseJsonObject(JsonParsingContext ctx) throws SchemaException, IO } } } else { - map.put(currentFieldName, valueXNode); + Map.Entry entry = map.putReturningEntry(currentFieldNameInfo.name, valueXNode); + if (currentFieldNameInfo.explicitEmptyNamespace) { + ctx.noNamespaceEntries.put(entry, null); + } } - currentFieldName = null; + currentFieldNameInfo = null; } } // Return either map or primitive value (in case of @type/@value) @@ -347,7 +355,6 @@ private PrimitiveXNode parseToPrimitive(JsonParsingContext ctx) throws IO Object tid = ctx.parser.getTypeId(); if (tid != null) { -// System.out.println("tid = '" + tid + "' for " + ctx.parser.getText()); QName typeName = tagToTypeName(tid, ctx); primitive.setTypeQName(typeName); primitive.setExplicitTypeDeclaration(true); @@ -479,17 +486,72 @@ private void serializeFromMap(MapXNode map, JsonSerializationContext ctx) throws if (explicitType != null) { writeExplicitType(explicitType, ctx.generator); } + String oldDefaultNamespace = ctx.currentNamespace; + generateNsDeclarationIfNeeded(map, ctx); for (Entry entry : map.entrySet()) { - ctx.generator.writeFieldName(createKeyUri(entry.getKey(), ctx)); + ctx.generator.writeFieldName(createKeyUri(entry, ctx)); serialize(entry.getValue(), ctx); } ctx.generator.writeEndObject(); + ctx.currentNamespace = oldDefaultNamespace; + } + + 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 createKeyUri(QName key, JsonSerializationContext ctx) { - final SerializationOptions opts = ctx.prismSerializationContext.getOptions(); - if (SerializationOptions.isFullItemNameUris(opts)) { - return QNameUtil.qNameToUri(key, false); + private String determineNewCurrentNamespace(MapXNode map, JsonSerializationContext ctx) { + Map 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 + } + Integer c = counts.get(childNs); + counts.put(childNs, c != null ? c+1 : 1); + } + // otherwise, take the URI that occurs the most in the map + Entry max = null; + for (Entry count : counts.entrySet()) { + if (max == null || count.getValue() > max.getValue()) { + max = count; + } + } + return max != null ? max.getKey() : null; + } + + private String createKeyUri(Entry 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 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); } } diff --git a/infra/prism/src/main/java/com/evolveum/midpoint/prism/xnode/MapXNode.java b/infra/prism/src/main/java/com/evolveum/midpoint/prism/xnode/MapXNode.java index 7da8a3de3dc..a421d0d7997 100644 --- a/infra/prism/src/main/java/com/evolveum/midpoint/prism/xnode/MapXNode.java +++ b/infra/prism/src/main/java/com/evolveum/midpoint/prism/xnode/MapXNode.java @@ -86,6 +86,13 @@ public XNode put(QName key, XNode value) { return value; } + public Entry putReturningEntry(QName key, XNode value) { + removeEntry(key); + Entry e = new Entry(key, value); + subnodes.add(e); + return e; + } + public XNode remove(Object key) { if (!(key instanceof QName)) { throw new IllegalArgumentException("Key must be QName, but it is "+key); @@ -354,7 +361,17 @@ public void qualifyKey(QName key, String newNamespace) { } } } - + + public void replace(QName key, XNode value) { + for (Entry entry : subnodes) { + if (entry.getKey().equals(key)) { + entry.setValue(value); + return; + } + } + put(key, value); + } + private class Entry implements Map.Entry, Serializable { private QName key; diff --git a/infra/prism/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java b/infra/prism/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java index f608ca25fcf..54fed080609 100644 --- a/infra/prism/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java +++ b/infra/prism/src/test/java/com/evolveum/midpoint/prism/TestPrismParsing.java @@ -184,7 +184,7 @@ private void roundTrip(File file) throws SchemaException, SAXException, IOExcept // WHEN // We need to serialize with composite objects during roundtrip, otherwise the result will not be equal - String userXml = prismContext.serializeObjectToString(originalUser, getOutputFormat(), SerializationOptions.createQualifiedNames()); + String userXml = prismContext.serializeObjectToString(originalUser, getOutputFormat(), null); // THEN System.out.println("Serialized user:"); diff --git a/infra/util/pom.xml b/infra/util/pom.xml index c36776a3d49..23bee7e75f6 100644 --- a/infra/util/pom.xml +++ b/infra/util/pom.xml @@ -88,6 +88,10 @@ aopalliance aopalliance + + org.jetbrains + annotations-java5 + org.testng diff --git a/infra/util/src/main/java/com/evolveum/midpoint/util/QNameUtil.java b/infra/util/src/main/java/com/evolveum/midpoint/util/QNameUtil.java index 70ddde25516..9c2f494ea05 100644 --- a/infra/util/src/main/java/com/evolveum/midpoint/util/QNameUtil.java +++ b/infra/util/src/main/java/com/evolveum/midpoint/util/QNameUtil.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Collection; -import java.util.Set; import javax.xml.namespace.QName; @@ -26,6 +25,8 @@ import com.evolveum.midpoint.util.logging.TraceManager; import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.Validate; +import org.jetbrains.annotations.NotNull; import org.w3c.dom.Node; /** @@ -83,16 +84,27 @@ public static QName uriToQName(String uri) { return uriToQName(uri, false); } + public static class QNameInfo { + @NotNull public final QName name; + public final boolean explicitEmptyNamespace; + public QNameInfo(@NotNull QName name, boolean explicitEmptyNamespace) { + this.name = name; + this.explicitEmptyNamespace = explicitEmptyNamespace; + } + } + public static QName uriToQName(String uri, boolean allowUnqualified) { + return uriToQNameInfo(uri, allowUnqualified).name; + } - if (uri == null) { - throw new IllegalArgumentException("URI is null"); - } + @NotNull + public static QNameInfo uriToQNameInfo(@NotNull String uri, boolean allowUnqualified) { + Validate.notNull(uri); int index = uri.lastIndexOf("#"); if (index != -1) { String ns = uri.substring(0, index); String name = uri.substring(index+1); - return new QName(ns,name); + return new QNameInfo(new QName(ns, name), "".equals(ns)); } index = uri.lastIndexOf("/"); // TODO check if this is still in the path section, e.g. @@ -101,10 +113,10 @@ public static QName uriToQName(String uri, boolean allowUnqualified) { if (index != -1) { String ns = uri.substring(0, index); String name = uri.substring(index+1); - return new QName(ns,name); + return new QNameInfo(new QName(ns, name), "".equals(ns)); } if (allowUnqualified) { - return new QName(uri); + return new QNameInfo(new QName(uri), false); } else { throw new IllegalArgumentException("The URI (" + uri + ") does not contain slash character"); }