/* * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.yangtools.yang.data.codec.gson; import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; import com.google.gson.JsonIOException; import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; import com.google.gson.stream.MalformedJsonException; import java.io.Closeable; import java.io.EOFException; import java.io.Flushable; import java.io.IOException; import java.net.URI; import java.util.ArrayDeque; import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.Set; import javax.xml.transform.dom.DOMSource; import org.opendaylight.yangtools.util.xml.UntrustedXML; import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.util.AbstractNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.AnyXmlNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.CompositeNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.LeafListEntryNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.LeafListNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.LeafNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.ListEntryNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.ListNodeDataWithSchema; import org.opendaylight.yangtools.yang.data.util.ParserStreamUtils; import org.opendaylight.yangtools.yang.data.util.RpcAsContainer; import org.opendaylight.yangtools.yang.data.util.SimpleNodeDataWithSchema; import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode; import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.api.SchemaNode; import org.opendaylight.yangtools.yang.model.api.TypedSchemaNode; import org.opendaylight.yangtools.yang.model.api.YangModeledAnyXmlSchemaNode; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Text; /** * This class parses JSON elements from a GSON JsonReader. It disallows multiple elements of the same name unlike the * default GSON JsonParser. */ @Beta public final class JsonParserStream implements Closeable, Flushable { static final String ANYXML_ARRAY_ELEMENT_ID = "array-element"; private final Deque<URI> namespaces = new ArrayDeque<>(); private final NormalizedNodeStreamWriter writer; private final JSONCodecFactory codecs; private final SchemaContext schema; private final DataSchemaNode parentNode; private JsonParserStream(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext, final DataSchemaNode parentNode) { this.schema = Preconditions.checkNotNull(schemaContext); this.writer = Preconditions.checkNotNull(writer); this.codecs = JSONCodecFactory.create(schemaContext); this.parentNode = parentNode; } public static JsonParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext, final SchemaNode parentNode ) { if (parentNode instanceof RpcDefinition) { return new JsonParserStream(writer, schemaContext, new RpcAsContainer((RpcDefinition) parentNode)); } Preconditions.checkArgument(parentNode instanceof DataSchemaNode, "Instance of DataSchemaNode class awaited."); return new JsonParserStream(writer, schemaContext, (DataSchemaNode) parentNode); } public static JsonParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext) { return new JsonParserStream(writer, schemaContext, schemaContext); } public JsonParserStream parse(final JsonReader reader) { // code copied from gson's JsonParser and Stream classes final boolean lenient = reader.isLenient(); reader.setLenient(true); boolean isEmpty = true; try { reader.peek(); isEmpty = false; final CompositeNodeDataWithSchema compositeNodeDataWithSchema = new CompositeNodeDataWithSchema(parentNode); read(reader, compositeNodeDataWithSchema); compositeNodeDataWithSchema.write(writer); return this; } catch (final EOFException e) { if (isEmpty) { return this; } // The stream ended prematurely so it is likely a syntax error. throw new JsonSyntaxException(e); } catch (final MalformedJsonException | NumberFormatException e) { throw new JsonSyntaxException(e); } catch (final IOException e) { throw new JsonIOException(e); } catch (StackOverflowError | OutOfMemoryError e) { throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e); } finally { reader.setLenient(lenient); } } private void traverseAnyXmlValue(final JsonReader in, final Document doc, final Element parentElement) throws IOException { switch (in.peek()) { case STRING: case NUMBER: Text textNode = doc.createTextNode(in.nextString()); parentElement.appendChild(textNode); break; case BOOLEAN: textNode = doc.createTextNode(Boolean.toString(in.nextBoolean())); parentElement.appendChild(textNode); break; case NULL: in.nextNull(); textNode = doc.createTextNode("null"); parentElement.appendChild(textNode); break; case BEGIN_ARRAY: in.beginArray(); while (in.hasNext()) { final Element childElement = doc.createElement(ANYXML_ARRAY_ELEMENT_ID); parentElement.appendChild(childElement); traverseAnyXmlValue(in, doc, childElement); } in.endArray(); break; case BEGIN_OBJECT: in.beginObject(); while (in.hasNext()) { final Element childElement = doc.createElement(in.nextName()); parentElement.appendChild(childElement); traverseAnyXmlValue(in, doc, childElement); } in.endObject(); case END_DOCUMENT: case NAME: case END_OBJECT: case END_ARRAY: break; } } private void readAnyXmlValue(final JsonReader in, final AnyXmlNodeDataWithSchema parent, final String anyXmlObjectName) throws IOException { final String anyXmlObjectNS = getCurrentNamespace().toString(); final Document doc = UntrustedXML.newDocumentBuilder().newDocument(); final Element rootElement = doc.createElementNS(anyXmlObjectNS, anyXmlObjectName); doc.appendChild(rootElement); traverseAnyXmlValue(in, doc, rootElement); final DOMSource domSource = new DOMSource(doc.getDocumentElement()); parent.setValue(domSource); } public void read(final JsonReader in, AbstractNodeDataWithSchema parent) throws IOException { switch (in.peek()) { case STRING: case NUMBER: setValue(parent, in.nextString()); break; case BOOLEAN: setValue(parent, Boolean.toString(in.nextBoolean())); break; case NULL: in.nextNull(); setValue(parent, null); break; case BEGIN_ARRAY: in.beginArray(); while (in.hasNext()) { if (parent instanceof LeafNodeDataWithSchema) { read(in, parent); } else { final AbstractNodeDataWithSchema newChild = newArrayEntry(parent); read(in, newChild); } } in.endArray(); return; case BEGIN_OBJECT: final Set<String> namesakes = new HashSet<>(); in.beginObject(); /* * This allows parsing of incorrectly /as showcased/ * in testconf nesting of list items - eg. * lists with one value are sometimes serialized * without wrapping array. * */ if (isArray(parent)) { parent = newArrayEntry(parent); } while (in.hasNext()) { final String jsonElementName = in.nextName(); DataSchemaNode parentSchema = parent.getSchema(); if (parentSchema instanceof YangModeledAnyXmlSchemaNode) { parentSchema = ((YangModeledAnyXmlSchemaNode) parentSchema).getSchemaOfAnyXmlData(); } final NamespaceAndName namespaceAndName = resolveNamespace(jsonElementName, parentSchema); final String localName = namespaceAndName.getName(); addNamespace(namespaceAndName.getUri()); if (namesakes.contains(jsonElementName)) { throw new JsonSyntaxException("Duplicate name " + jsonElementName + " in JSON input."); } namesakes.add(jsonElementName); final Deque<DataSchemaNode> childDataSchemaNodes = ParserStreamUtils.findSchemaNodeByNameAndNamespace(parentSchema, localName, getCurrentNamespace()); if (childDataSchemaNodes.isEmpty()) { throw new IllegalStateException("Schema for node with name " + localName + " and namespace " + getCurrentNamespace() + " doesn't exist."); } final AbstractNodeDataWithSchema newChild = ((CompositeNodeDataWithSchema) parent).addChild(childDataSchemaNodes); if (newChild instanceof AnyXmlNodeDataWithSchema) { readAnyXmlValue(in, (AnyXmlNodeDataWithSchema) newChild, jsonElementName); } else { read(in, newChild); } removeNamespace(); } in.endObject(); return; case END_DOCUMENT: case NAME: case END_OBJECT: case END_ARRAY: break; } } private static boolean isArray(final AbstractNodeDataWithSchema parent) { return parent instanceof ListNodeDataWithSchema || parent instanceof LeafListNodeDataWithSchema; } private static AbstractNodeDataWithSchema newArrayEntry(final AbstractNodeDataWithSchema parent) { AbstractNodeDataWithSchema newChild; if (parent instanceof ListNodeDataWithSchema) { newChild = new ListEntryNodeDataWithSchema(parent.getSchema()); } else if (parent instanceof LeafListNodeDataWithSchema) { newChild = new LeafListEntryNodeDataWithSchema(parent.getSchema()); } else { throw new IllegalStateException("Found an unexpected array nested under "+ parent.getSchema().getQName()); } ((CompositeNodeDataWithSchema) parent).addChild(newChild); return newChild; } private void setValue(final AbstractNodeDataWithSchema parent, final String value) { Preconditions.checkArgument(parent instanceof SimpleNodeDataWithSchema, "Node %s is not a simple type", parent.getSchema().getQName()); final SimpleNodeDataWithSchema parentSimpleNode = (SimpleNodeDataWithSchema) parent; Preconditions.checkArgument(parentSimpleNode.getValue() == null, "Node '%s' has already set its value to '%s'", parentSimpleNode.getSchema().getQName(), parentSimpleNode.getValue()); final Object translatedValue = translateValueByType(value, parentSimpleNode.getSchema()); parentSimpleNode.setValue(translatedValue); } private Object translateValueByType(final String value, final DataSchemaNode node) { Preconditions.checkArgument(node instanceof TypedSchemaNode); return codecs.codecFor((TypedSchemaNode) node).parseValue(null, value); } private void removeNamespace() { namespaces.pop(); } private void addNamespace(final URI namespace) { namespaces.push(namespace); } private NamespaceAndName resolveNamespace(final String childName, final DataSchemaNode dataSchemaNode) { final int lastIndexOfColon = childName.lastIndexOf(':'); String moduleNamePart = null; String nodeNamePart = null; URI namespace = null; if (lastIndexOfColon != -1) { moduleNamePart = childName.substring(0, lastIndexOfColon); nodeNamePart = childName.substring(lastIndexOfColon + 1); final Module m = schema.findModuleByName(moduleNamePart, null); namespace = m == null ? null : m.getNamespace(); } else { nodeNamePart = childName; } if (namespace == null) { Set<URI> potentialUris = Collections.emptySet(); potentialUris = resolveAllPotentialNamespaces(nodeNamePart, dataSchemaNode); if (potentialUris.contains(getCurrentNamespace())) { namespace = getCurrentNamespace(); } else if (potentialUris.size() == 1) { namespace = potentialUris.iterator().next(); } else if (potentialUris.size() > 1) { throw new IllegalStateException("Choose suitable module name for element "+nodeNamePart+":"+toModuleNames(potentialUris)); } else if (potentialUris.isEmpty()) { throw new IllegalStateException("Schema node with name "+nodeNamePart+" wasn't found under "+dataSchemaNode.getQName()+"."); } } return new NamespaceAndName(nodeNamePart, namespace); } private String toModuleNames(final Set<URI> potentialUris) { final StringBuilder builder = new StringBuilder(); for (final URI potentialUri : potentialUris) { builder.append("\n"); //FIXME how to get information about revision from JSON input? currently first available is used. builder.append(schema.findModuleByNamespace(potentialUri).iterator().next().getName()); } return builder.toString(); } private Set<URI> resolveAllPotentialNamespaces(final String elementName, final DataSchemaNode dataSchemaNode) { final Set<URI> potentialUris = new HashSet<>(); final Set<ChoiceSchemaNode> choices = new HashSet<>(); if (dataSchemaNode instanceof DataNodeContainer) { for (final DataSchemaNode childSchemaNode : ((DataNodeContainer) dataSchemaNode).getChildNodes()) { if (childSchemaNode instanceof ChoiceSchemaNode) { choices.add((ChoiceSchemaNode)childSchemaNode); } else if (childSchemaNode.getQName().getLocalName().equals(elementName)) { potentialUris.add(childSchemaNode.getQName().getNamespace()); } } for (final ChoiceSchemaNode choiceNode : choices) { for (final ChoiceCaseNode concreteCase : choiceNode.getCases()) { potentialUris.addAll(resolveAllPotentialNamespaces(elementName, concreteCase)); } } } return potentialUris; } private URI getCurrentNamespace() { return namespaces.peek(); } private static class NamespaceAndName { private final URI uri; private final String name; public NamespaceAndName(final String name, final URI uri) { this.name = name; this.uri = uri; } public String getName() { return name; } public URI getUri() { return uri; } } @Override public void flush() throws IOException { writer.flush(); } @Override public void close() throws IOException { writer.flush(); writer.close(); } }