Skip to content

Commit

Permalink
Returning back more intelligent serialization of JSON/YAML. Correctly…
Browse files Browse the repository at this point in the history
… parsing "explicit no-NS" nodes.
  • Loading branch information
mederly committed Oct 4, 2016
1 parent 35ca7a5 commit 0385315
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 28 deletions.
Expand Up @@ -104,7 +104,11 @@ public Collection<XNode> 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<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<>();
JsonParsingContext(@NotNull JsonParser parser, @NotNull ParsingContext prismParsingContext) {
this.parser = parser;
this.prismParsingContext = prismParsingContext;
Expand Down Expand Up @@ -149,7 +153,9 @@ private void processDefaultNamespaces(XNode xnode, String parentDefault, JsonPar
for (Entry<QName, XNode> 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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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));
}
Expand All @@ -281,9 +286,12 @@ private XNode parseJsonObject(JsonParsingContext ctx) throws SchemaException, IO
}
}
} else {
map.put(currentFieldName, valueXNode);
Map.Entry<QName, XNode> 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)
Expand Down Expand Up @@ -347,7 +355,6 @@ private <T> PrimitiveXNode<T> 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);
Expand Down Expand Up @@ -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<QName,XNode> 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<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
}
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<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 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 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);
}
}

Expand Down
Expand Up @@ -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);
Expand Down Expand Up @@ -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<QName, XNode>, Serializable {

private QName key;
Expand Down
Expand Up @@ -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:");
Expand Down
4 changes: 4 additions & 0 deletions infra/util/pom.xml
Expand Up @@ -88,6 +88,10 @@
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations-java5</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.testng</groupId>
Expand Down
26 changes: 19 additions & 7 deletions infra/util/src/main/java/com/evolveum/midpoint/util/QNameUtil.java
Expand Up @@ -18,14 +18,15 @@

import java.util.Arrays;
import java.util.Collection;
import java.util.Set;

import javax.xml.namespace.QName;

import com.evolveum.midpoint.util.logging.Trace;
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;

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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");
}
Expand Down

0 comments on commit 0385315

Please sign in to comment.