chengfan
Committed by Brian O'Connor

[ONOS-5012] implement RESTconf server

 - fix javadoc longer than 80 char limit
 - fix javadoc that missing @params
 - chain calls to StringBuilder.append()
 - combine constant strings in place

Change-Id: Ie2ef4fd4c19e955ad2d5a5584f5017a842abb790
Showing 30 changed files with 1680 additions and 78 deletions
......@@ -2,6 +2,7 @@ BUNDLES = [
'//protocols/restconf/server/api:onos-protocols-restconf-server-api',
'//protocols/restconf/server/restconfmgr:onos-protocols-restconf-server-restconfmgr',
'//protocols/restconf/server/rpp:onos-protocols-restconf-server-rpp',
'//protocols/restconf/server/utils:onos-protocols-restconf-server-utils',
]
onos_app (
......
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.api;
import javax.ws.rs.HttpMethod;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated method responds to HTTP PATCH requests.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("PATCH")
public @interface Patch {
}
\ No newline at end of file
......@@ -25,59 +25,82 @@ import org.glassfish.jersey.server.ChunkedOutput;
public interface RestconfService {
/**
* Processes a GET request against a data resource. The
* target data resource is identified by its URI.
* target data resource is identified by its URI. If the
* GET operation cannot be fulfilled due to reasons such
* as the nonexistence of the target resource, then a
* RestconfException exception is raised. The proper
* HTTP error status code is enclosed in the exception, so
* that the caller may return it to the RESTCONF client to
* display.
*
* @param uri URI of the target data resource
* @return JSON representation of the data resource
* @throws RestconfException if the GET operation cannot be fulfilled due
* reasons such as the nonexistence of the target
* resource. The proper HTTP error status code is
* enclosed in the exception, so that the caller
* may return it to the RESTCONF client
* @throws RestconfException if the GET operation cannot be fulfilled
*/
ObjectNode runGetOperationOnDataResource(String uri) throws RestconfException;
ObjectNode runGetOperationOnDataResource(String uri)
throws RestconfException;
/**
* Processes a POST request against a data resource. The location of
* the target resource is passed in as a URI. And the resource's
* content is passed in as a JSON ObjectNode.
* content is passed in as a JSON ObjectNode. If the POST operation
* cannot be fulfilled due to reasons such as wrong input URIs or
* syntax errors in the JSON payloads, a RestconfException exception
* is raised. The proper HTTP error status code is enclosed in the
* exception.
*
* @param uri URI of the data resource to be created
* @param rootNode JSON representation of the data resource
* @throws RestconfException if the POST operation cannot be fulfilled due
* reasons such as wrong URI or syntax error
* in JSON payload. The proper HTTP error status
* code is enclosed in the exception
* @throws RestconfException if the POST operation cannot be fulfilled
*/
void runPostOperationOnDataResource(String uri, ObjectNode rootNode) throws RestconfException;
void runPostOperationOnDataResource(String uri, ObjectNode rootNode)
throws RestconfException;
/**
* Processes a PUT request against a data resource. The location of
* the target resource is passed in as a URI. And the resource's
* content is passed in as a JSON ObjectNode.
* content is passed in as a JSON ObjectNode. If the PUT operation
* cannot be fulfilled due to reasons such as wrong input URIs or
* syntax errors in the JSON payloads, a RestconfException exception
* is raised. The proper HTTP error status code is enclosed in the
* exception.
*
* @param uri URI of the data resource to be created or updated
* @param rootNode JSON representation of the data resource
* @throws RestconfException if the PUT operation cannot be fulfilled due
* reasons such as wrong URI or syntax error
* in JSON payload. The proper HTTP error status
* code is enclosed in the exception
* @throws RestconfException if the PUT operation cannot be fulfilled
*/
void runPutOperationOnDataResource(String uri, ObjectNode rootNode) throws RestconfException;
void runPutOperationOnDataResource(String uri, ObjectNode rootNode)
throws RestconfException;
/**
* Processes the DELETE operation against a data resource. The target
* data resource is identified by its URI.
* data resource is identified by its URI. If the DELETE operation
* cannot be fulfilled due reasons such as the nonexistence of the
* target resource, a RestconfException exception is raised. The
* proper HTTP error status code is enclosed in the exception.
*
* @param uri URI of the data resource to be deleted
* @throws RestconfException if the DELETE operation cannot be fulfilled due
* reasons such as the nonexistence of the target
* resource. The proper HTTP error status code is
* enclosed in the exception
* @throws RestconfException if the DELETE operation cannot be fulfilled
*/
void runDeleteOperationOnDataResource(String uri) throws RestconfException;
/**
* Processes a PATCH operation on a data resource. The target data
* resource is identified by its URI passed in by the caller.
* And the content of the data resource is passed in as a JSON ObjectNode.
* If the PATCH operation cannot be fulfilled due reasons such as
* the nonexistence of the target resource, a RestconfException
* exception is raised. The proper HTTP error status code is
* enclosed in the exception.
*
* @param uri URI of the data resource to be patched
* @param rootNode JSON representation of the data resource
* @throws RestconfException if the PATCH operation cannot be fulfilled
*/
void runPatchOperationOnDataResource(String uri, ObjectNode rootNode)
throws RestconfException;
/**
* Retrieves the RESTCONF Root directory.
*
* @return the RESTCONF Root directory
......@@ -90,13 +113,18 @@ public interface RestconfService {
* which is passed in from the caller. (The worker thread blocks if
* no events arrive.) The ChuckedOutput is a pipe to which this
* function acts as the writer and the caller the reader.
* <p>
* If the Event Stream cannot be subscribed due to reasons such as
* the nonexistence of the target stream or failure to allocate
* worker thread to handle the request, a RestconfException exception
* is raised. The proper HTTP error status code is enclosed in the
* exception, so that the caller may return it to the RESTCONF client
* to display.
*
* @param streamId ID of the RESTCONF stream to subscribe
* @param output A string data stream
* @throws RestconfException if the Event Stream cannot be subscribed due to
* reasons such as the nonexistence of the target
* stream or unable to allocate any free worker
* thread to handle the request
* @throws RestconfException if the Event Stream cannot be subscribed
*/
void subscribeEventStream(String streamId, ChunkedOutput<String> output) throws RestconfException;
void subscribeEventStream(String streamId, ChunkedOutput<String> output)
throws RestconfException;
}
......
......@@ -22,4 +22,5 @@
<artifact>mvn:${project.groupId}/onos-restconf-server-api/${project.version}</artifact>
<artifact>mvn:${project.groupId}/onos-restconf-server-restconfmanager/${project.version}</artifact>
<artifact>mvn:${project.groupId}/onos-restconf-server-rpp/${project.version}</artifact>
<artifact>mvn:${project.groupId}/onos-restconf-server-utils/${project.version}</artifact>
</app>
......
......@@ -21,5 +21,6 @@
<bundle>mvn:${project.groupId}/onos-restconf-server-api/${project.version}</bundle>
<bundle>mvn:${project.groupId}/onos-restconf-server-restconfmanager/${project.version}</bundle>
<bundle>mvn:${project.groupId}/onos-restconf-server-rpp/${project.version}</bundle>
<bundle>mvn:${project.groupId}/onos-restconf-server-utils/${project.version}</bundle>
</feature>
</features>
......
......@@ -51,5 +51,10 @@
<artifactId>onos-restconf-server-rpp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.onosproject</groupId>
<artifactId>onos-restconf-server-utils</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
......
......@@ -34,6 +34,7 @@
<module>restconfmgr</module>
<module>rpp</module>
<module>app</module>
<module>utils</module>
</modules>
<description>RESTCONF Server Module</description>
......
......@@ -6,6 +6,8 @@ COMPILE_DEPS = [
'//utils/rest:onlab-rest',
'//core/store/serializers:onos-core-serializers',
'//protocols/restconf/server/api:onos-protocols-restconf-server-api',
'//protocols/restconf/server/utils:onos-protocols-restconf-server-utils',
'//apps/yms/api:onos-apps-yms-api',
]
osgi_jar_with_tests (
......
......@@ -58,6 +58,16 @@
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.scr.annotations</artifactId>
</dependency>
<dependency>
<groupId>org.onosproject</groupId>
<artifactId>onos-restconf-server-utils</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.onosproject</groupId>
<artifactId>onos-app-yms-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
......
......@@ -15,20 +15,30 @@
*/
package org.onosproject.protocol.restconf.server.restconfmanager;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.Service;
import org.glassfish.jersey.server.ChunkedOutput;
import org.onosproject.event.ListenerTracker;
import org.onosproject.protocol.restconf.server.api.RestconfException;
import org.onosproject.protocol.restconf.server.api.RestconfService;
import org.onosproject.yms.ydt.YdtBuilder;
import org.onosproject.yms.ydt.YdtContext;
import org.onosproject.yms.ydt.YdtContextOperationType;
import org.onosproject.yms.ydt.YdtResponse;
import org.onosproject.yms.ydt.YmsOperationExecutionStatus;
import org.onosproject.yms.ydt.YmsOperationType;
import org.onosproject.yms.ymsm.YmsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
......@@ -37,21 +47,37 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.onosproject.yms.ydt.YmsOperationType.QUERY_REQUEST;
import static org.onosproject.yms.ydt.YmsOperationType.EDIT_CONFIG_REQUEST;
import static org.onosproject.yms.ydt.YdtContextOperationType.NONE;
import static org.onosproject.yms.ydt.YdtContextOperationType.CREATE;
import static org.onosproject.yms.ydt.YdtContextOperationType.DELETE;
import static org.onosproject.yms.ydt.YdtContextOperationType.REPLACE;
import static org.onosproject.yms.ydt.YdtContextOperationType.MERGE;
import static org.onosproject.yms.ydt.YdtType.SINGLE_INSTANCE_LEAF_VALUE_NODE;
import static org.onosproject.yms.ydt.YmsOperationExecutionStatus.EXECUTION_SUCCESS;
import static org.onosproject.protocol.restconf.server.utils.parser.json.ParserUtils.convertYdtToJson;
import static org.onosproject.protocol.restconf.server.utils.parser.json.ParserUtils.convertUriToYdt;
import static org.onosproject.protocol.restconf.server.utils.parser.json.ParserUtils.convertJsonToYdt;
import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static java.util.concurrent.TimeUnit.SECONDS;
/*
* Skeletal ONOS RESTCONF Server application. The RESTCONF Manager
* implements the main logic of the RESTCONF Server.
*
* The design of the RESTCONF subsystem contains 2 major bundles:
*
* 1. RESTCONF Protocol Proxy (RPP). This bundle is implemented as a JAX-RS application.
* It acts as the frond-end of the the RESTCONF server. It handles
* HTTP requests that are sent to the RESTCONF Root Path. It then calls the RESTCONF Manager
* to process the requests.
* 1. RESTCONF Protocol Proxy (RPP). This bundle is implemented as a
* JAX-RS application. It acts as the frond-end of the RESTCONF server.
* It intercepts/handles HTTP requests that are sent to the RESTCONF
* Root Path. It then calls the RESTCONF Manager to process the requests.
*
* 2. RESTCONF Manager. This is the back-end. It provides the main logic of the RESTCONF server.
* It calls the YMS (YANG Management System) to operate on the YANG data objects.
* 2. RESTCONF Manager. This bundle module is the back-end of the server.
* It provides the main logic of the RESTCONF server. It interacts with
* the YMS (YANG Management System) to run operations on the YANG data
* objects (i.e., data resources).
*/
/**
......@@ -73,26 +99,25 @@ public class RestconfManager implements RestconfService {
private final Logger log = LoggerFactory.getLogger(getClass());
//TODO: YMS service
//@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
//protected YmsService ymsService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected YmsService ymsService;
private ListenerTracker listeners;
private ConcurrentMap<String, BlockingQueue<ObjectNode>> eventQueueList =
new ConcurrentHashMap<String, BlockingQueue<ObjectNode>>();
new ConcurrentHashMap<>();
private ExecutorService workerThreadPool;
@Activate
protected void activate() {
workerThreadPool = Executors.newFixedThreadPool(maxNumOfWorkerThreads,
new ThreadFactoryBuilder()
.setNameFormat("restconf-worker")
.build());
workerThreadPool = Executors
.newFixedThreadPool(maxNumOfWorkerThreads,
new ThreadFactoryBuilder()
.setNameFormat("restconf-worker")
.build());
listeners = new ListenerTracker();
//TODO: YMS notification
//listeners.addListener(ymsService, new InternalYangNotificationListener());
log.info("Started");
}
......@@ -104,34 +129,117 @@ public class RestconfManager implements RestconfService {
}
@Override
public ObjectNode runGetOperationOnDataResource(String uri) throws RestconfException {
//TODO: YMS integration
return null;
public ObjectNode runGetOperationOnDataResource(String uri)
throws RestconfException {
YdtBuilder ydtBuilder = getYdtBuilder(QUERY_REQUEST);
//Convert the URI to ydtBuilder
convertUriToYdt(uri, ydtBuilder, NONE);
YdtResponse ydtResponse = ymsService.executeOperation(ydtBuilder);
YmsOperationExecutionStatus status = ydtResponse
.getYmsOperationResult();
if (status != EXECUTION_SUCCESS) {
throw new RestconfException("YMS GET operation failed",
INTERNAL_SERVER_ERROR);
}
YdtContext rootNode = ydtResponse.getRootNode();
YdtContext curNode = ydtBuilder.getCurNode();
ObjectNode result = convertYdtToJson(curNode.getName(), rootNode,
ymsService.getYdtWalker());
//if the query URI contain a key, something like list=key
//here should only get get child with the specific key
YdtContext child = curNode.getFirstChild();
if (child != null &&
child.getYdtType() == SINGLE_INSTANCE_LEAF_VALUE_NODE) {
ArrayNode jsonNode = (ArrayNode) result.get(curNode.getName());
for (JsonNode next : jsonNode) {
if (next.findValue(child.getName())
.asText().equals(child.getValue())) {
return (ObjectNode) next;
}
}
throw new RestconfException(String.format("No content for %s = %s",
child.getName(),
child.getValue()),
INTERNAL_SERVER_ERROR);
}
return result;
}
private YmsOperationExecutionStatus
invokeYmsOp(String uri, ObjectNode rootNode,
YdtContextOperationType opType) {
YdtBuilder ydtBuilder = getYdtBuilder(EDIT_CONFIG_REQUEST);
//Convert the URI to ydtBuilder
convertUriToYdt(uri, ydtBuilder, opType);
//set default operation type for the payload node
ydtBuilder.setDefaultEditOperationType(opType);
//convert the payload json body to ydt
convertJsonToYdt(rootNode, ydtBuilder);
return ymsService
.executeOperation(ydtBuilder)
.getYmsOperationResult();
}
@Override
public void runPostOperationOnDataResource(String uri, ObjectNode rootNode) throws RestconfException {
//TODO: YMS integration
public void runPostOperationOnDataResource(String uri, ObjectNode rootNode)
throws RestconfException {
YmsOperationExecutionStatus status =
invokeYmsOp(uri, rootNode, CREATE);
if (status != EXECUTION_SUCCESS) {
throw new RestconfException("YMS post operation failed.",
INTERNAL_SERVER_ERROR);
}
}
@Override
public void runPutOperationOnDataResource(String uri, ObjectNode rootNode) throws RestconfException {
//TODO: YMS integration
public void runPutOperationOnDataResource(String uri, ObjectNode rootNode)
throws RestconfException {
YmsOperationExecutionStatus status =
invokeYmsOp(uri, rootNode, REPLACE);
if (status != EXECUTION_SUCCESS) {
throw new RestconfException("YMS put operation failed.",
INTERNAL_SERVER_ERROR);
}
}
@Override
public void runDeleteOperationOnDataResource(String uri)
throws RestconfException {
//Get a root ydtBuilder
YdtBuilder ydtBuilder = getYdtBuilder(EDIT_CONFIG_REQUEST);
//Convert the URI to ydtBuilder
convertUriToYdt(uri, ydtBuilder, DELETE);
//Execute the delete operation
YmsOperationExecutionStatus status = ymsService
.executeOperation(ydtBuilder)
.getYmsOperationResult();
if (status != EXECUTION_SUCCESS) {
throw new RestconfException("YMS delete operation failed.",
INTERNAL_SERVER_ERROR);
}
}
/**
* Process the delete operation on a data resource.
*
* @param uri URI of the data resource to be deleted.
*/
@Override
public void runDeleteOperationOnDataResource(String uri) throws RestconfException {
//TODO: YMS integration
public void runPatchOperationOnDataResource(String uri, ObjectNode rootNode)
throws RestconfException {
YmsOperationExecutionStatus status = invokeYmsOp(uri, rootNode, MERGE);
if (status != EXECUTION_SUCCESS) {
throw new RestconfException("YMS patch operation failed.",
INTERNAL_SERVER_ERROR);
}
}
@Override
public String getRestconfRootPath() {
return this.RESTCONF_ROOT;
return RESTCONF_ROOT;
}
/**
......@@ -143,16 +251,21 @@ public class RestconfManager implements RestconfService {
* @throws RestconfException if the worker thread fails to create
*/
@Override
public void subscribeEventStream(String streamId, ChunkedOutput<String> output) throws RestconfException {
BlockingQueue<ObjectNode> eventQueue = new LinkedBlockingQueue<ObjectNode>();
public void subscribeEventStream(String streamId,
ChunkedOutput<String> output)
throws RestconfException {
BlockingQueue<ObjectNode> eventQueue = new LinkedBlockingQueue<>();
if (workerThreadPool instanceof ThreadPoolExecutor) {
if (((ThreadPoolExecutor) workerThreadPool).getActiveCount() >= maxNumOfWorkerThreads) {
throw new RestconfException("no more work threads left to handle event subscription",
Response.Status.INTERNAL_SERVER_ERROR);
if (((ThreadPoolExecutor) workerThreadPool).getActiveCount() >=
maxNumOfWorkerThreads) {
throw new RestconfException("no more work threads left to " +
"handle event subscription",
INTERNAL_SERVER_ERROR);
}
} else {
throw new RestconfException("Server ERROR: workerThreadPool NOT instanceof ThreadPoolExecutor",
Response.Status.INTERNAL_SERVER_ERROR);
throw new RestconfException("Server ERROR: workerThreadPool NOT " +
"instanceof ThreadPoolExecutor",
INTERNAL_SERVER_ERROR);
}
......@@ -169,10 +282,11 @@ public class RestconfManager implements RestconfService {
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(THREAD_TERMINATION_TIMEOUT, TimeUnit.SECONDS)) {
if (!pool.awaitTermination(THREAD_TERMINATION_TIMEOUT, SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(THREAD_TERMINATION_TIMEOUT, TimeUnit.SECONDS)) {
if (!pool.awaitTermination(THREAD_TERMINATION_TIMEOUT,
SECONDS)) {
log.error("Pool did not terminate");
}
}
......@@ -190,7 +304,8 @@ public class RestconfManager implements RestconfService {
private final ChunkedOutput<String> output;
private final BlockingQueue<ObjectNode> bqueue;
public EventConsumer(ChunkedOutput<String> output, BlockingQueue<ObjectNode> q) {
public EventConsumer(ChunkedOutput<String> output,
BlockingQueue<ObjectNode> q) {
this.queueId = Thread.currentThread().getName();
this.output = output;
this.bqueue = q;
......@@ -212,7 +327,8 @@ public class RestconfManager implements RestconfService {
*/
eventQueueList.remove(this.queueId);
} catch (InterruptedException e) {
log.error("ERROR: EventConsumer: bqueue.take() has been interrupted.");
log.error("ERROR: EventConsumer: bqueue.take() " +
"has been interrupted.");
log.debug("EventConsumer Exception:", e);
} finally {
try {
......@@ -226,6 +342,10 @@ public class RestconfManager implements RestconfService {
}
private YdtBuilder getYdtBuilder(YmsOperationType ymsOperationType) {
return ymsService.getYdtBuilder(RESTCONF_ROOT, null, ymsOperationType);
}
/**
* The listener class acts as the event producer for the event queues. The
* queues are created by the event consumer threads and are removed when the
......
......@@ -19,6 +19,7 @@ package org.onosproject.protocol.restconf.server.rpp;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.glassfish.jersey.server.ChunkedOutput;
import org.onosproject.protocol.restconf.server.api.Patch;
import org.onosproject.protocol.restconf.server.api.RestconfException;
import org.onosproject.protocol.restconf.server.api.RestconfService;
import org.onosproject.rest.AbstractWebResource;
......@@ -39,8 +40,11 @@ import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static org.slf4j.LoggerFactory.getLogger;
/*
* This class is the main implementation of the RESTCONF Protocol
* Proxy module. Currently it only handles some basic operations
......@@ -134,7 +138,8 @@ public class RestconfWebResource extends AbstractWebResource {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("data/{identifier : .+}")
public Response handlePostRequest(@PathParam("identifier") String uriString, InputStream stream) {
public Response handlePostRequest(@PathParam("identifier") String uriString,
InputStream stream) {
log.debug("handlePostRequest: {}", uriString);
......@@ -145,14 +150,14 @@ public class RestconfWebResource extends AbstractWebResource {
return Response.created(uriInfo.getRequestUri()).build();
} catch (JsonProcessingException e) {
log.error("ERROR: handlePostRequest ", e);
return Response.status(Response.Status.BAD_REQUEST).build();
return Response.status(BAD_REQUEST).build();
} catch (RestconfException e) {
log.error("ERROR: handlePostRequest: {}", e.getMessage());
log.debug("Exception in handlePostRequest:", e);
return e.getResponse();
} catch (IOException ex) {
log.error("ERROR: handlePostRequest ", ex);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
return Response.status(INTERNAL_SERVER_ERROR).build();
}
}
......@@ -174,7 +179,8 @@ public class RestconfWebResource extends AbstractWebResource {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("data/{identifier : .+}")
public Response handlePutRequest(@PathParam("identifier") String uriString, InputStream stream) {
public Response handlePutRequest(@PathParam("identifier") String uriString,
InputStream stream) {
log.debug("handlePutRequest: {}", uriString);
......@@ -185,14 +191,14 @@ public class RestconfWebResource extends AbstractWebResource {
return Response.created(uriInfo.getRequestUri()).build();
} catch (JsonProcessingException e) {
log.error("ERROR: handlePutRequest ", e);
return Response.status(Response.Status.BAD_REQUEST).build();
return Response.status(BAD_REQUEST).build();
} catch (RestconfException e) {
log.error("ERROR: handlePutRequest: {}", e.getMessage());
log.debug("Exception in handlePutRequest:", e);
return e.getResponse();
} catch (IOException ex) {
log.error("ERROR: handlePutRequest ", ex);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
return Response.status(INTERNAL_SERVER_ERROR).build();
}
}
......@@ -223,4 +229,41 @@ public class RestconfWebResource extends AbstractWebResource {
}
}
/**
* Handles a RESTCONF PATCH operation against a data resource.
* If the PATCH request succeeds, a "200 OK" status-line is returned if
* there is a message-body, and "204 No Content" is returned if no
* response message-body is sent.
*
* @param uriString URI of the data resource
* @param stream Input JSON object
* @return HTTP response
*/
@Patch
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("data/{identifier : .+}")
public Response handlePatchRequest(@PathParam("identifier") String uriString,
InputStream stream) {
log.debug("handlePatchRequest: {}", uriString);
try {
ObjectNode rootNode = (ObjectNode) mapper().readTree(stream);
service.runPatchOperationOnDataResource(uriString, rootNode);
return Response.ok().build();
} catch (JsonProcessingException e) {
log.error("ERROR: handlePatchRequest ", e);
return Response.status(BAD_REQUEST).build();
} catch (RestconfException e) {
log.error("ERROR: handlePatchRequest: {}", e.getMessage());
log.debug("Exception in handlePatchRequest:", e);
return e.getResponse();
} catch (IOException ex) {
log.error("ERROR: handlePatchRequest ", ex);
return Response.status(INTERNAL_SERVER_ERROR).build();
}
}
}
......
COMPILE_DEPS = [
'//lib:CORE_DEPS',
'//apps/yms/api:onos-apps-yms-api',
]
osgi_jar_with_tests (
deps = COMPILE_DEPS,
)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>onos-restconf-server</artifactId>
<groupId>org.onosproject</groupId>
<version>1.8.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>onos-restconf-server-utils</artifactId>
<packaging>bundle</packaging>
<dependencies>
<dependency>
<groupId>org.onosproject</groupId>
<artifactId>onos-app-yms-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
\ No newline at end of file
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.exceptions;
/**
* Represents class of errors related to Json parse utils.
*/
public class JsonParseException extends RuntimeException {
/**
* Constructs an exception with the specified message.
*
* @param message the message describing the specific nature of the error
*/
public JsonParseException(String message) {
super(message);
}
/**
* Constructs an exception with the specified message and the underlying
* cause.
*
* @param message the message describing the specific nature of the error
* @param cause the underlying cause of this error
*/
public JsonParseException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.exceptions;
/**
* Represents class of errors related to YDT parse utils.
*/
public class YdtParseException extends RuntimeException {
/**
* Constructs an exception with the specified message.
*
* @param message the message describing the specific nature of the error
*/
public YdtParseException(String message) {
super(message);
}
/**
* Constructs an exception with the specified message and the underlying
* cause.
*
* @param message the message describing the specific nature of the error
* @param cause the underlying cause of this error
*/
public YdtParseException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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.
*/
/**
* Parse utils custom exceptions.
*/
package org.onosproject.protocol.restconf.server.utils.exceptions;
\ No newline at end of file
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.api;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Set;
/**
* Abstraction of an entity which provides interfaces to build and obtain JSON
* data tree.
*/
public interface JsonBuilder {
/**
* Adds a to half (a left brace/bracket and the field name) of a JSON
* object/array to the JSON tree. This method is used by protocols which
* knows the nature (object/array) of node.
*
* @param fieldName name of child to be added
* @param nodeType the type of the child
*/
void addNodeTopHalf(String fieldName, JsonNodeType nodeType);
/**
* Adds a child with value and a comma to the JSON tree.
* Protocols unaware of nature of node (single/multiple) will use it to add
* both single instance and multi instance node. Protocols aware of nature
* of node will use it for single instance value node addition.
*
* @param fieldName name of child to be added
* @param value the type of the child
*/
void addNodeWithValueTopHalf(String fieldName, String value);
/**
* Adds a child with list of values to JSON data tree. This method is
* used by protocols which knows the nature (object/array) of node for
* ArrayNode addition.
*
* @param fieldName name of child to be added
* @param sets the value list of the child
*/
void addNodeWithSetTopHalf(String fieldName, Set<String> sets);
/**
* Adds the bottom half(a right brace/bracket) of a JSON object/array to
* the JSON tree. for the text, a comma should be taken out.
*
* @param nodeType the type of the child
*/
void addNodeBottomHalf(JsonNodeType nodeType);
/**
* Returns the JSON tree after build operations in the format of string.
*
* @return the final string JSON tree after build operations
*/
String getTreeString();
/**
* Returns the JSON tree after build operations in the format of ObjectNode.
*
* @return the final ObjectNode JSON tree after build operations
*/
ObjectNode getTreeNode();
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.api;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Abstraction of an entity which provide call back methods which are called
* by JSON walker while walking the JSON data tree. This interface needs to be
* implemented by protocol implementing listener's based call backs while JSON
* walk.
*/
public interface JsonListener {
/**
* Callback invoked during a node entry.
* All the related information about the node can be obtain from the JSON
* object.
*
* @param fieldName the field name of the JSON Node value
* @param node the JsonNode which is walked through
*/
void enterJsonNode(String fieldName, JsonNode node);
/**
* Callback invoked during a node exit.
* All the related information about the node can be obtain from the JSON
* node.
*
* @param jsonNode JSON node which has been walked through
*/
void exitJsonNode(JsonNode jsonNode);
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.api;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Abstraction of an entity which provides interfaces for Json walk.
* This interface serve as common tools for anyone who needs to parse
* the json node with depth-first algorithm.
*/
public interface JsonWalker {
/**
* Walks the JSON data tree. Protocols implements JSON listener service
* and walks JSON tree with input as implemented object. JSON walker
* provides call backs to implemented methods. For the original json
* node(come from NB), there is a field name which is something like the
* module name of a YANG model. If not, the fieldName can be null.
*
* @param jsonListener Json listener implemented by the user
* @param fieldName the original object node field
* @param node the json node which needs to be walk
*/
void walk(JsonListener jsonListener, String fieldName, ObjectNode node);
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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.
*/
/**
* Provider json related process interface.
*/
package org.onosproject.protocol.restconf.server.utils.parser.api;
\ No newline at end of file
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.json;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onosproject.protocol.restconf.server.utils.exceptions.JsonParseException;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Set;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
/**
* Represents implementation of interfaces to build and obtain JSON data tree.
*/
public class DefaultJsonBuilder implements JsonBuilder {
private static final String LEFT_BRACE = "{";
private static final String RIGHT_BRACE = "}";
private static final String LEFT_BRACKET = "[";
private static final String RIGHT_BRACKET = "]";
private static final String COMMA = ",";
private static final String COLON = ":";
private static final String QUOTE = "\"";
private static final String E_UNSUP_TYPE = "Unsupported node type %s " +
"field name is %s fieldName";
private Logger log = LoggerFactory.getLogger(getClass());
private StringBuilder treeString;
/**
* Creates a Default Json Builder with a specific root name.
*
* @param rootName the start string of the Json builder
*/
public DefaultJsonBuilder(String rootName) {
checkNotNull(rootName);
treeString = new StringBuilder(rootName);
}
/**
* Creates a Default Json Builder with a default root name.
*/
public DefaultJsonBuilder() {
treeString = new StringBuilder(LEFT_BRACE);
}
@Override
public void addNodeTopHalf(String fieldName, JsonNodeType nodeType) {
appendField(fieldName);
switch (nodeType) {
case OBJECT:
treeString.append(LEFT_BRACE);
break;
case ARRAY:
treeString.append(LEFT_BRACKET);
break;
default:
throw new JsonParseException(String.format(E_UNSUP_TYPE,
nodeType, fieldName));
}
}
@Override
public void addNodeWithValueTopHalf(String fieldName, String value) {
if (isNullOrEmpty(fieldName)) {
return;
}
appendField(fieldName);
if (value.isEmpty()) {
return;
}
treeString.append(QUOTE)
.append(value)
.append(QUOTE + COMMA);
}
@Override
public void addNodeWithSetTopHalf(String fieldName, Set<String> sets) {
if (isNullOrEmpty(fieldName)) {
return;
}
appendField(fieldName);
treeString.append(LEFT_BRACKET);
for (String el : sets) {
treeString.append(QUOTE)
.append(el)
.append(QUOTE + COMMA);
}
}
@Override
public void addNodeBottomHalf(JsonNodeType nodeType) {
switch (nodeType) {
case OBJECT:
removeCommaIfExist();
treeString.append(RIGHT_BRACE + COMMA);
break;
case ARRAY:
removeCommaIfExist();
treeString.append(RIGHT_BRACKET + COMMA);
break;
case BINARY:
case BOOLEAN:
case MISSING:
case NULL:
case NUMBER:
case POJO:
case STRING:
log.debug("Unimplemented node type {}", nodeType);
break;
default:
throw new JsonParseException("Unsupported json node type " +
nodeType);
}
}
@Override
public String getTreeString() {
removeCommaIfExist();
return treeString.append(RIGHT_BRACE).toString();
}
@Override
public ObjectNode getTreeNode() {
ObjectNode node = null;
try {
node = (ObjectNode) (new ObjectMapper()).readTree(getTreeString());
} catch (IOException e) {
log.error("Parse json string failed {}", e.getMessage());
}
return node;
}
private void appendField(String fieldName) {
if (!isNullOrEmpty(fieldName)) {
treeString.append(QUOTE)
.append(fieldName)
.append(QUOTE + COLON);
}
}
private void removeCommaIfExist() {
int lastIndex = treeString.length() - 1;
if (treeString.charAt(lastIndex) == COMMA.charAt(0)) {
treeString.deleteCharAt(lastIndex);
}
}
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.json;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonWalker;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonListener;
import java.util.Iterator;
import java.util.Map;
/**
* Represents implementation of JSON walk, which walks the JSON object node.
*/
public class DefaultJsonWalker implements JsonWalker {
@Override
public void walk(JsonListener jsonListener, String fieldName,
ObjectNode objectNode) {
//enter the object node, the original ObjectNode should have a module
//name as fieldName.
jsonListener.enterJsonNode(fieldName, objectNode);
//the node has no children, then exist and return.
if (!objectNode.isContainerNode()) {
jsonListener.exitJsonNode(objectNode);
return;
}
Iterator<Map.Entry<String, JsonNode>> fields = objectNode.fields();
while (fields.hasNext()) {
//get the children entry of the node
Map.Entry<String, JsonNode> currentChild = fields.next();
String key = currentChild.getKey();
JsonNode value = currentChild.getValue();
//if the entry's value has its own children, do a recursion.
//if the entry has no children, store the key and value.
//for we don't know the specific type of the entry's value, we
// should give it to a method which can handle JsonNode
if (value.isContainerNode()) {
walkJsonNode(jsonListener, key, value);
} else {
jsonListener.enterJsonNode(key, value);
jsonListener.exitJsonNode(value);
}
}
jsonListener.exitJsonNode(objectNode);
}
/**
* Walks the JSON data tree. This method is called when we don't know
* the exact type of a json node.
*
* @param jsonListener Json listener implemented by the user
* @param fieldName the original object node field
* @param rootNode the json node which needs to be walk
*/
private void walkJsonNode(JsonListener jsonListener, String fieldName,
JsonNode rootNode) {
if (rootNode.isObject()) {
walk(jsonListener, fieldName, (ObjectNode) rootNode);
return;
}
if (rootNode.isArray()) {
walkArrayNode(jsonListener, fieldName, (ArrayNode) rootNode);
}
}
/**
* Walks the JSON data tree. This method is called when the user knows the
* json node type is ArrayNode.
*
* @param jsonListener Json listener implemented by the user
* @param fieldName the original object node field
* @param rootNode the json node which needs to be walk
*/
private void walkArrayNode(JsonListener jsonListener, String fieldName,
ArrayNode rootNode) {
if (rootNode == null) {
return;
}
//enter the array node.
jsonListener.enterJsonNode(fieldName, rootNode);
Iterator<JsonNode> children = rootNode.elements();
while (children.hasNext()) {
JsonNode currentChild = children.next();
if (currentChild.isContainerNode()) {
walkJsonNode(jsonListener, null, currentChild);
}
}
jsonListener.exitJsonNode(rootNode);
}
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.json;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import org.onosproject.protocol.restconf.server.utils.exceptions.JsonParseException;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonListener;
import org.onosproject.yms.ydt.YdtBuilder;
import org.onosproject.yms.ydt.YdtContext;
import org.slf4j.Logger;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import static com.fasterxml.jackson.databind.node.JsonNodeType.ARRAY;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.onosproject.yms.ydt.YdtType.MULTI_INSTANCE_NODE;
import static org.onosproject.yms.ydt.YdtType.SINGLE_INSTANCE_NODE;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Represents default implementation of codec JSON listener.
*/
public class JsonToYdtListener implements JsonListener {
private static final String INPUT_FIELD_NAME = "input";
private static final String COLON = ":";
private static final int INPUT_FIELD_LENGTH = 2;
private static final String E_UNSUP_TYPE = "Unsupported node type %s " +
"field name is %s fieldName";
private Logger log = getLogger(getClass());
private YdtBuilder ydtBuilder;
private String defaultMultiInsNodeName;
private YdtContext rpcModule;
/**
* Creates a listener for the process of a Json Object, the listener will
* try to transfer the JSON object to a Ydt builder.
*
* @param ydtBuilder the YDT builder to build a YDT object
*/
public JsonToYdtListener(YdtBuilder ydtBuilder) {
this.ydtBuilder = ydtBuilder;
}
@Override
public void enterJsonNode(String fieldName, JsonNode node) {
if (isNullOrEmpty(fieldName)) {
if (!isNullOrEmpty(defaultMultiInsNodeName)) {
ydtBuilder.addChild(defaultMultiInsNodeName, null,
MULTI_INSTANCE_NODE);
}
return;
}
JsonNodeType nodeType = node.getNodeType();
switch (nodeType) {
case OBJECT:
processObjectNode(fieldName);
break;
case ARRAY:
processArrayNode(fieldName, node);
break;
//TODO for now, just process the following three node type
case STRING:
case NUMBER:
case BOOLEAN:
ydtBuilder.addLeaf(fieldName, null, node.asText());
break;
case BINARY:
case MISSING:
case NULL:
case POJO:
log.debug("Unimplemented node type {}", nodeType);
break;
default:
throw new JsonParseException(String.format(E_UNSUP_TYPE,
nodeType, fieldName));
}
}
@Override
public void exitJsonNode(JsonNode jsonNode) {
if (jsonNode.getNodeType() == ARRAY) {
return;
}
ydtBuilder.traverseToParent();
YdtContext curNode = ydtBuilder.getCurNode();
//if the current node is the RPC node, then should go to the father
//for we have enter the RPC node and Input node at the same time
//and the input is the only child of RPC node.
if (curNode == null) {
return;
}
String name = curNode.getName();
if (rpcModule != null && name.equals(rpcModule.getName())) {
ydtBuilder.traverseToParent();
}
}
private void processObjectNode(String fieldName) {
String[] segments = fieldName.split(COLON);
Boolean isLastInput = segments.length == INPUT_FIELD_LENGTH &&
segments[INPUT_FIELD_LENGTH - 1].equals(INPUT_FIELD_NAME);
int first = 0;
int second = 1;
if (isLastInput) {
ydtBuilder.addChild(segments[first], null, SINGLE_INSTANCE_NODE);
rpcModule = ydtBuilder.getCurNode();
ydtBuilder.addChild(segments[second], null, SINGLE_INSTANCE_NODE);
} else {
ydtBuilder.addChild(fieldName, null, SINGLE_INSTANCE_NODE);
}
}
private void processArrayNode(String fieldName, JsonNode node) {
ArrayNode arrayNode = (ArrayNode) node;
Set<String> sets = new HashSet<>();
Iterator<JsonNode> elements = arrayNode.elements();
boolean isLeafList = true;
while (elements.hasNext()) {
JsonNode element = elements.next();
JsonNodeType eleType = element.getNodeType();
if (eleType == JsonNodeType.STRING ||
eleType == JsonNodeType.NUMBER ||
eleType == JsonNodeType.BOOLEAN) {
sets.add(element.asText());
} else {
isLeafList = false;
}
}
if (isLeafList) {
//leaf-list
ydtBuilder.addLeaf(fieldName, null, sets);
} else {
this.defaultMultiInsNodeName = fieldName;
}
}
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.json;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.onosproject.protocol.restconf.server.utils.exceptions.JsonParseException;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonBuilder;
import org.onosproject.yms.ydt.YdtBuilder;
import org.onosproject.yms.ydt.YdtContext;
import org.onosproject.yms.ydt.YdtContextOperationType;
import org.onosproject.yms.ydt.YdtListener;
import org.onosproject.yms.ydt.YdtType;
import org.onosproject.yms.ydt.YdtWalker;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.onosproject.yms.ydt.YdtContextOperationType.NONE;
/**
* Utils to complete the conversion between JSON and YDT(YANG DATA MODEL).
*/
public final class ParserUtils {
private static final Splitter SLASH_SPLITTER = Splitter.on('/');
private static final Splitter COMMA_SPLITTER = Splitter.on(',');
private static final String EQUAL = "=";
private static final String COMMA = ",";
private static final String COLON = ":";
private static final String URI_ENCODING_CHAR_SET = "ISO-8859-1";
private static final String ERROR_LIST_MSG = "List/Leaf-list node should be " +
"in format \"nodeName=key\"or \"nodeName=instance-value\"";
private static final String ERROR_MODULE_MSG = "First node should be in " +
"format \"moduleName:nodeName\"";
// no instantiation
private ParserUtils() {
}
/**
* Converts URI identifier to YDT builder.
*
* @param id the uri identifier from web request
* @param builder the base ydt builder
* @param opType the ydt operation type for the uri
*/
public static void convertUriToYdt(String id,
YdtBuilder builder,
YdtContextOperationType opType) {
checkNotNull(id, "uri identifier should not be null");
List<String> paths = urlPathArgsDecode(SLASH_SPLITTER.split(id));
if (!paths.isEmpty()) {
processPathSegments(paths, builder, opType);
}
}
/**
* Converts JSON objectNode to YDT builder. The objectNode can be any
* standard JSON node, node just for RESTconf payload.
*
* @param objectNode the objectNode from web request
* @param builder the base ydt builder
*/
public static void convertJsonToYdt(ObjectNode objectNode,
YdtBuilder builder) {
JsonToYdtListener listener = new JsonToYdtListener(builder);
new DefaultJsonWalker().walk(listener, null, objectNode);
}
/**
* Converts a Ydt context tree to a JSON object.
*
* @param rootName the name of the YdtContext from which the YdtListener
* start to builder a Json Object
* @param context a abstract data model for YANG data
* @param walker abstraction of an entity which provides interfaces for
* YDT walk
* @return the JSON node corresponding the YANG data
*/
public static ObjectNode convertYdtToJson(String rootName,
YdtContext context,
YdtWalker walker) {
JsonBuilder builder = new DefaultJsonBuilder();
YdtListener listener = new YdtToJsonListener(rootName, builder);
walker.walk(listener, context);
return builder.getTreeNode();
}
/**
* Converts a list of path segments to a YDT builder tree.
*
* @param paths the list of path segments split from URI
* @param builder the base YDT builder
* @param opType the YDT operation type for the Path segment
* @return the YDT builder with the tree info of paths
*/
private static YdtBuilder processPathSegments(List<String> paths,
YdtBuilder builder,
YdtContextOperationType opType) {
if (paths.isEmpty()) {
return builder;
}
boolean isLastNode = paths.size() == 1;
YdtContextOperationType opTypeForThisNode = isLastNode ? opType : NONE;
String path = paths.iterator().next();
if (path.contains(COLON)) {
addModule(builder, path);
addNode(path, builder, opTypeForThisNode);
} else if (path.contains(EQUAL)) {
addListOrLeafList(path, builder, opTypeForThisNode);
} else {
addLeaf(path, builder, opTypeForThisNode);
}
if (isLastNode) {
return builder;
}
List<String> remainPaths = paths.subList(1, paths.size());
processPathSegments(remainPaths, builder, opType);
return builder;
}
private static YdtBuilder addModule(YdtBuilder builder, String path) {
String moduleName = getPreSegment(path, COLON);
if (moduleName == null) {
throw new JsonParseException(ERROR_MODULE_MSG);
}
builder.addChild(moduleName, null, YdtType.SINGLE_INSTANCE_NODE);
return builder;
}
private static YdtBuilder addNode(String path, YdtBuilder builder,
YdtContextOperationType opType) {
String nodeName = getLatterSegment(path, COLON);
builder.addChild(nodeName,
null,
YdtType.SINGLE_INSTANCE_NODE,
opType);
return builder;
}
private static YdtBuilder addListOrLeafList(String path,
YdtBuilder builder,
YdtContextOperationType opType) {
String nodeName = getPreSegment(path, EQUAL);
String keyStr = getLatterSegment(path, EQUAL);
if (keyStr == null) {
throw new JsonParseException(ERROR_LIST_MSG);
}
builder.setDefaultEditOperationType(opType);
if (keyStr.contains(COMMA)) {
List<String> keys = Lists.
newArrayList(COMMA_SPLITTER.split(keyStr));
builder.addMultiInstanceChild(nodeName, null, keys);
} else {
builder.addMultiInstanceChild(nodeName, null,
Lists.newArrayList(keyStr));
}
return builder;
}
private static YdtBuilder addLeaf(String path, YdtBuilder builder,
YdtContextOperationType opType) {
checkNotNull(path);
builder.addChild(path, null, opType);
return builder;
}
/**
* Returns the previous segment of a path which is separated by a split char.
* For example:
* <pre>
* "foo:bar", ":" --> "foo"
* </pre>
*
* @param path the original path string
* @param splitChar char used to split the path
* @return the previous segment of the path
*/
private static String getPreSegment(String path, String splitChar) {
int idx = path.indexOf(splitChar);
if (idx == -1) {
return null;
}
if (path.indexOf(splitChar, idx + 1) != -1) {
return null;
}
return path.substring(0, idx);
}
/**
* Returns the latter segment of a path which is separated by a split char.
* For example:
* <pre>
* "foo:bar", ":" --> "bar"
* </pre>
*
* @param path the original path string
* @param splitChar char used to split the path
* @return the latter segment of the path
*/
private static String getLatterSegment(String path, String splitChar) {
int idx = path.indexOf(splitChar);
if (idx == -1) {
return path;
}
if (path.indexOf(splitChar, idx + 1) != -1) {
return null;
}
return path.substring(idx + 1);
}
/**
* Converts a list of path from the original format to ISO-8859-1 code.
*
* @param paths the original paths
* @return list of decoded paths
*/
public static List<String> urlPathArgsDecode(Iterable<String> paths) {
try {
List<String> decodedPathArgs = new ArrayList<>();
for (String pathArg : paths) {
String decode = URLDecoder.decode(pathArg,
URI_ENCODING_CHAR_SET);
decodedPathArgs.add(decode);
}
return decodedPathArgs;
} catch (UnsupportedEncodingException e) {
throw new JsonParseException("Invalid URL path arg '" +
paths + "': ", e);
}
}
}
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.protocol.restconf.server.utils.parser.json;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import org.onosproject.protocol.restconf.server.utils.exceptions.YdtParseException;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonBuilder;
import org.onosproject.yms.ydt.YdtContext;
import org.onosproject.yms.ydt.YdtListener;
import static com.google.common.base.Strings.isNullOrEmpty;
/**
* Represents implementation of codec YDT listener.
*/
public class YdtToJsonListener implements YdtListener {
private static final String EMPTY = "";
private JsonBuilder jsonBuilder;
//the root name of the json
//the input YdtContext is usually a total tree of a YANG resource
//this property is used to mark the start of the request node.
private String rootName;
//the parse state
private boolean isBegin;
/**
* Creates a listener for the process of a Ydt, the listener will try to
* transfer the Ydt to a JSON Object.
*
* @param rootName the name of a specific YdtContext begin to process
* @param jsonBuilder the JSON builder to build a JSON object
*/
public YdtToJsonListener(String rootName, JsonBuilder jsonBuilder) {
this.jsonBuilder = jsonBuilder;
this.rootName = rootName;
this.isBegin = isNullOrEmpty(rootName);
}
@Override
public void enterYdtNode(YdtContext ydtContext) {
String name = ydtContext.getName();
if (!isBegin && name.equals(rootName)) {
isBegin = true;
}
if (!isBegin) {
return;
}
switch (ydtContext.getYdtType()) {
case SINGLE_INSTANCE_NODE:
jsonBuilder.addNodeTopHalf(name, JsonNodeType.OBJECT);
break;
case MULTI_INSTANCE_NODE:
YdtContext preNode = ydtContext.getPreviousSibling();
if (preNode == null || !preNode.getName().equals(name)) {
jsonBuilder.addNodeTopHalf(name, JsonNodeType.ARRAY);
}
jsonBuilder.addNodeTopHalf(EMPTY, JsonNodeType.OBJECT);
break;
case SINGLE_INSTANCE_LEAF_VALUE_NODE:
jsonBuilder.addNodeWithValueTopHalf(name, ydtContext.getValue());
break;
case MULTI_INSTANCE_LEAF_VALUE_NODE:
jsonBuilder.addNodeWithSetTopHalf(name, ydtContext.getValueSet());
break;
default:
throw new YdtParseException("unknown Ydt type " +
ydtContext.getYdtType());
}
}
@Override
public void exitYdtNode(YdtContext ydtContext) {
if (!isBegin) {
return;
}
String curName = ydtContext.getName();
YdtContext nextNode = ydtContext.getNextSibling();
switch (ydtContext.getYdtType()) {
case SINGLE_INSTANCE_NODE:
jsonBuilder.addNodeBottomHalf(JsonNodeType.OBJECT);
break;
case MULTI_INSTANCE_NODE:
if (nextNode == null || !nextNode.getName().equals(curName)) {
jsonBuilder.addNodeBottomHalf(JsonNodeType.OBJECT);
jsonBuilder.addNodeBottomHalf(JsonNodeType.ARRAY);
} else {
jsonBuilder.addNodeBottomHalf(JsonNodeType.OBJECT);
}
break;
case SINGLE_INSTANCE_LEAF_VALUE_NODE:
jsonBuilder.addNodeBottomHalf(JsonNodeType.STRING);
break;
case MULTI_INSTANCE_LEAF_VALUE_NODE:
jsonBuilder.addNodeBottomHalf(JsonNodeType.ARRAY);
break;
default:
throw new YdtParseException("Unknown Ydt type " +
ydtContext.getYdtType());
}
if (curName.equals(rootName) &&
(nextNode == null || !nextNode.getName().equals(rootName))) {
isBegin = false;
}
}
}
\ No newline at end of file
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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.
*/
/**
* Provider utilities to support the data format based encoding and decoding,
* used by YMSC to operate on different data format and YDT(YANG DATA TYPE).
*/
package org.onosproject.protocol.restconf.server.utils.parser.json;
\ No newline at end of file
/*
* Copyright 2016-present Open Networking Laboratory
*
* 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 org.onosproject.restconf.utils.parser.json;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Before;
import org.junit.Test;
import org.onosproject.protocol.restconf.server.utils.parser.api.JsonListener;
import org.onosproject.protocol.restconf.server.utils.parser.json.DefaultJsonWalker;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
public class DefaultJsonWalkerTest {
private static final String[] EXP_TEXT_ARRAY = {
"Enter: type is OBJECT name is null",
"Enter: type is STRING name is name",
"Exit: type is STRING",
"Enter: type is STRING name is surname",
"Exit: type is STRING",
"Exit: type is OBJECT"
};
private static final String[] EXP_NODE_LIST_ARRAY = {
"Enter: type is OBJECT name is null",
"Enter: type is STRING name is surname",
"Exit: type is STRING",
"Enter: type is ARRAY name is networklist",
"Exit: type is ARRAY",
"Exit: type is OBJECT"
};
private static final String[] EXP_NODE_WITH_ARRAY = {
"Enter: type is OBJECT name is null",
"Enter: type is STRING name is surname",
"Exit: type is STRING",
"Enter: type is ARRAY name is networklist",
"Enter: type is OBJECT name is null",
"Enter: type is STRING name is network-id",
"Exit: type is STRING",
"Enter: type is STRING name is server-provided",
"Exit: type is STRING",
"Exit: type is OBJECT",
"Enter: type is OBJECT name is null",
"Enter: type is STRING name is network-id",
"Exit: type is STRING",
"Enter: type is STRING name is server-provided",
"Exit: type is STRING",
"Exit: type is OBJECT",
"Enter: type is OBJECT name is null",
"Enter: type is STRING name is network-id",
"Exit: type is STRING",
"Enter: type is STRING name is server-provided",
"Exit: type is STRING",
"Exit: type is OBJECT",
"Exit: type is ARRAY",
"Exit: type is OBJECT"
};
private static final String WRONG_CONTENT_MSG = "Wrong content in array";
private final List<String> logger = new ArrayList<>();
DefaultJsonWalker defaultJsonWalker = new DefaultJsonWalker();
InternalJsonListener jsonListener = new InternalJsonListener();
ObjectNode arrayNode;
ObjectNode textNode;
ObjectNode listNode;
@Before
public void setup() throws Exception {
textNode = loadJsonFile("/textNode.json");
listNode = loadJsonFile("/listNode.json");
arrayNode = loadJsonFile("/arrayNode.json");
}
private ObjectNode loadJsonFile(String path) throws Exception {
InputStream jsonStream = getClass().getResourceAsStream(path);
ObjectMapper mapper = new ObjectMapper();
return (ObjectNode) mapper.readTree(jsonStream);
}
private void assertWalkResult(String[] expectArray, List<String> logger) {
for (int i = 0; i < expectArray.length; i++) {
assertThat(WRONG_CONTENT_MSG,
true,
is(logger.get(i).contentEquals(expectArray[i])));
}
}
@Test
public void testWalkTextNode() throws Exception {
defaultJsonWalker.walk(jsonListener, null, textNode);
assertWalkResult(EXP_TEXT_ARRAY, logger);
}
@Test
public void testWalkNodeWithList() throws Exception {
defaultJsonWalker.walk(jsonListener, null, listNode);
assertWalkResult(EXP_NODE_LIST_ARRAY, logger);
}
@Test
public void testWalkNodeWithArray() throws Exception {
defaultJsonWalker.walk(jsonListener, null, arrayNode);
assertWalkResult(EXP_NODE_WITH_ARRAY, logger);
}
private class InternalJsonListener implements JsonListener {
@Override
public void enterJsonNode(String fieldName, JsonNode node) {
logger.add("Enter: type is " + node.getNodeType() + " name is " +
fieldName);
}
@Override
public void exitJsonNode(JsonNode node) {
logger.add("Exit: type is " + node.getNodeType());
}
}
}
\ No newline at end of file
{
"surname": "Bangalore",
"networklist":[
{
"network-id": "520",
"server-provided": "123"
},
{
"network-id": "521",
"server-provided": "124"
},
{
"network-id": "523",
"server-provided": "125"
}
]
}
\ No newline at end of file
{
"surname": "Bangalore",
"networklist":[
"0.79",
"1.04",
"3.14"
]
}
\ No newline at end of file
{
"name": "Huawei",
"surname": "Bangalore"
}
\ No newline at end of file