diff options
Diffstat (limited to 'netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java')
-rw-r--r-- | netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java new file mode 100644 index 0000000..ce5a8c4 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2015 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.netconf.sal.rest.impl; + +import static com.google.common.base.Verify.verify; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchEditOperation; +import org.opendaylight.restconf.common.patch.PatchEntity; +import org.opendaylight.restconf.common.util.RestUtil; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.ErrorType; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; +import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Patch reader for JSON. + * + * @deprecated This class will be replaced by JsonToPatchBodyReader in restconf-nb-rfc8040 + */ +@Deprecated +@Provider +@Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON}) +public class JsonToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider + implements MessageBodyReader<PatchContext> { + + private static final Logger LOG = LoggerFactory.getLogger(JsonToPatchBodyReader.class); + + public JsonToPatchBodyReader(final ControllerContext controllerContext) { + super(controllerContext); + } + + @Override + public boolean isReadable(final Class<?> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType) { + return true; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public PatchContext readFrom(final Class<PatchContext> type, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) + throws WebApplicationException { + try { + return readFrom(getInstanceIdentifierContext(), entityStream); + } catch (final Exception e) { + throw propagateExceptionAs(e); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + public PatchContext readFrom(final String uriPath, final InputStream entityStream) throws + RestconfDocumentedException { + try { + return readFrom(getControllerContext().toInstanceIdentifier(uriPath), entityStream); + } catch (final Exception e) { + propagateExceptionAs(e); + return null; // no-op + } + } + + private PatchContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream) + throws IOException { + final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream); + if (nonEmptyInputStreamOptional.isEmpty()) { + return new PatchContext(path, null, null); + } + + final JsonReader jsonReader = new JsonReader(new InputStreamReader(nonEmptyInputStreamOptional.get(), + StandardCharsets.UTF_8)); + AtomicReference<String> patchId = new AtomicReference<>(); + final List<PatchEntity> resultList = read(jsonReader, path, patchId); + jsonReader.close(); + + return new PatchContext(path, resultList, patchId.get()); + } + + private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException { + Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class); + LOG.debug("Error parsing json input", exception); + + if (exception instanceof ResultAlreadySetException) { + throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. "); + } + + RestconfDocumentedException.throwIfYangError(exception); + throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, exception); + } + + private List<PatchEntity> read(final JsonReader in, final InstanceIdentifierContext path, + final AtomicReference<String> patchId) throws IOException { + final List<PatchEntity> resultCollection = new ArrayList<>(); + final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec( + path.getSchemaContext()); + final JsonToPatchBodyReader.PatchEdit edit = new JsonToPatchBodyReader.PatchEdit(); + + while (in.hasNext()) { + switch (in.peek()) { + case STRING: + case NUMBER: + in.nextString(); + break; + case BOOLEAN: + Boolean.toString(in.nextBoolean()); + break; + case NULL: + in.nextNull(); + break; + case BEGIN_ARRAY: + in.beginArray(); + break; + case BEGIN_OBJECT: + in.beginObject(); + break; + case END_DOCUMENT: + break; + case NAME: + parseByName(in.nextName(), edit, in, path, codec, resultCollection, patchId); + break; + case END_OBJECT: + in.endObject(); + break; + case END_ARRAY: + in.endArray(); + break; + + default: + break; + } + } + + return ImmutableList.copyOf(resultCollection); + } + + /** + * Switch value of parsed JsonToken.NAME and read edit definition or patch id. + * + * @param name value of token + * @param edit PatchEdit instance + * @param in JsonReader reader + * @param path InstanceIdentifierContext context + * @param codec StringModuleInstanceIdentifierCodec codec + * @param resultCollection collection of parsed edits + * @throws IOException if operation fails + */ + private void parseByName(final @NonNull String name, final @NonNull PatchEdit edit, + final @NonNull JsonReader in, final @NonNull InstanceIdentifierContext path, + final @NonNull StringModuleInstanceIdentifierCodec codec, + final @NonNull List<PatchEntity> resultCollection, + final @NonNull AtomicReference<String> patchId) throws IOException { + switch (name) { + case "edit" : + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + + while (in.hasNext()) { + readEditDefinition(edit, in, path, codec); + resultCollection.add(prepareEditOperation(edit)); + edit.clear(); + } + + in.endArray(); + } else { + readEditDefinition(edit, in, path, codec); + resultCollection.add(prepareEditOperation(edit)); + edit.clear(); + } + + break; + case "patch-id" : + patchId.set(in.nextString()); + break; + default: + break; + } + } + + /** + * Read one patch edit object from Json input. + * @param edit PatchEdit instance to be filled with read data + * @param in JsonReader reader + * @param path InstanceIdentifierContext path context + * @param codec StringModuleInstanceIdentifierCodec codec + * @throws IOException if operation fails + */ + private void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in, + final @NonNull InstanceIdentifierContext path, + final @NonNull StringModuleInstanceIdentifierCodec codec) throws IOException { + final StringBuilder value = new StringBuilder(); + in.beginObject(); + + while (in.hasNext()) { + final String editDefinition = in.nextName(); + switch (editDefinition) { + case "edit-id" : + edit.setId(in.nextString()); + break; + case "operation" : + edit.setOperation(PatchEditOperation.valueOf(in.nextString().toUpperCase(Locale.ROOT))); + break; + case "target" : + // target can be specified completely in request URI + final String target = in.nextString(); + if (target.equals("/")) { + edit.setTarget(path.getInstanceIdentifier()); + edit.setTargetSchemaNode(SchemaInferenceStack.of(path.getSchemaContext()).toInference()); + } else { + edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()).concat(target))); + + final var stack = codec.getDataContextTree().enterPath(edit.getTarget()).orElseThrow().stack(); + stack.exit(); + final EffectiveStatement<?, ?> parentStmt = stack.currentStatement(); + verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt); + edit.setTargetSchemaNode(stack.toInference()); + } + + break; + case "value" : + // save data defined in value node for next (later) processing, because target needs to be read + // always first and there is no ordering in Json input + readValueNode(value, in); + break; + default: + break; + } + } + + in.endObject(); + + // read saved data to normalized node when target schema is already known + edit.setData(readEditData(new JsonReader( + new StringReader(value.toString())), edit.getTargetSchemaNode(), path)); + } + + /** + * Parse data defined in value node and saves it to buffer. + * @param value Buffer to read value node + * @param in JsonReader reader + * @throws IOException if operation fails + */ + private void readValueNode(final @NonNull StringBuilder value, final @NonNull JsonReader in) throws IOException { + in.beginObject(); + value.append('{'); + + value.append('"').append(in.nextName()).append("\":"); + + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + value.append('['); + + while (in.hasNext()) { + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + } else { + readValueObject(value, in); + } + if (in.peek() != JsonToken.END_ARRAY) { + value.append(','); + } + } + + in.endArray(); + value.append(']'); + } else { + readValueObject(value, in); + } + + in.endObject(); + value.append('}'); + } + + /** + * Parse one value object of data and saves it to buffer. + * @param value Buffer to read value object + * @param in JsonReader reader + * @throws IOException if operation fails + */ + private void readValueObject(final @NonNull StringBuilder value, final @NonNull JsonReader in) throws IOException { + // read simple leaf value + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + return; + } + + in.beginObject(); + value.append('{'); + + while (in.hasNext()) { + value.append('"').append(in.nextName()).append("\":"); + + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + } else { + if (in.peek() == JsonToken.BEGIN_ARRAY) { + in.beginArray(); + value.append('['); + + while (in.hasNext()) { + if (in.peek() == JsonToken.STRING) { + value.append('"').append(in.nextString()).append('"'); + } else { + readValueObject(value, in); + } + if (in.peek() != JsonToken.END_ARRAY) { + value.append(','); + } + } + + in.endArray(); + value.append(']'); + } else { + readValueObject(value, in); + } + } + + if (in.peek() != JsonToken.END_OBJECT) { + value.append(','); + } + } + + in.endObject(); + value.append('}'); + } + + /** + * Read patch edit data defined in value node to NormalizedNode. + * @param in reader JsonReader reader + * @return NormalizedNode representing data + */ + private static NormalizedNode readEditData(final @NonNull JsonReader in, + final @NonNull Inference targetSchemaNode, final @NonNull InstanceIdentifierContext path) { + final NormalizedNodeResult resultHolder = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder); + final EffectiveModelContext context = path.getSchemaContext(); + JsonParserStream.create(writer, JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context), + targetSchemaNode) + .parse(in); + + return resultHolder.getResult(); + } + + /** + * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception. + * @param edit Instance of PatchEdit + * @return PatchEntity Patch entity + */ + private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) { + if (edit.getOperation() != null && edit.getTargetSchemaNode() != null + && checkDataPresence(edit.getOperation(), edit.getData() != null)) { + if (edit.getOperation().isWithValue()) { + // for lists allow to manipulate with list items through their parent + final YangInstanceIdentifier targetNode; + if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) { + targetNode = edit.getTarget().getParent(); + } else { + targetNode = edit.getTarget(); + } + + return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData()); + } + + return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget()); + } + + throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + /** + * Check if data is present when operation requires it and not present when operation data is not allowed. + * @param operation Name of operation + * @param hasData Data in edit are present/not present + * @return true if data is present when operation requires it or if there are no data when operation does not + * allow it, false otherwise + */ + private static boolean checkDataPresence(final @NonNull PatchEditOperation operation, final boolean hasData) { + return operation.isWithValue() == hasData; + } + + /** + * Helper class representing one patch edit. + */ + private static final class PatchEdit { + private String id; + private PatchEditOperation operation; + private YangInstanceIdentifier target; + private Inference targetSchemaNode; + private NormalizedNode data; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public PatchEditOperation getOperation() { + return operation; + } + + public void setOperation(final PatchEditOperation operation) { + this.operation = operation; + } + + public YangInstanceIdentifier getTarget() { + return target; + } + + public void setTarget(final YangInstanceIdentifier target) { + this.target = target; + } + + public Inference getTargetSchemaNode() { + return targetSchemaNode; + } + + public void setTargetSchemaNode(final Inference targetSchemaNode) { + this.targetSchemaNode = targetSchemaNode; + } + + public NormalizedNode getData() { + return data; + } + + public void setData(final NormalizedNode data) { + this.data = data; + } + + public void clear() { + id = null; + operation = null; + target = null; + targetSchemaNode = null; + data = null; + } + } +} |