All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opendaylight.restconf.server.DataRequestProcessor Maven / Gradle / Ivy

/*
 * Copyright (c) 2024 PANTHEON.tech s.r.o. 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.restconf.server;

import static org.opendaylight.restconf.server.Method.DELETE;
import static org.opendaylight.restconf.server.Method.GET;
import static org.opendaylight.restconf.server.Method.HEAD;
import static org.opendaylight.restconf.server.Method.OPTIONS;
import static org.opendaylight.restconf.server.Method.PATCH;
import static org.opendaylight.restconf.server.Method.POST;
import static org.opendaylight.restconf.server.Method.PUT;
import static org.opendaylight.restconf.server.NettyMediaTypes.ACCEPT_PATCH_HEADER_VALUE;
import static org.opendaylight.restconf.server.NettyMediaTypes.RESTCONF_TYPES;
import static org.opendaylight.restconf.server.NettyMediaTypes.YANG_PATCH_TYPES;
import static org.opendaylight.restconf.server.RequestUtils.extractApiPath;
import static org.opendaylight.restconf.server.RequestUtils.requestBody;
import static org.opendaylight.restconf.server.ResponseUtils.allowHeaderValue;
import static org.opendaylight.restconf.server.ResponseUtils.responseBuilder;
import static org.opendaylight.restconf.server.ResponseUtils.responseStatus;
import static org.opendaylight.restconf.server.ResponseUtils.simpleErrorResponse;
import static org.opendaylight.restconf.server.ResponseUtils.simpleResponse;
import static org.opendaylight.restconf.server.ResponseUtils.unmappedRequestErrorResponse;
import static org.opendaylight.restconf.server.ResponseUtils.unsupportedMediaTypeErrorResponse;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.FutureCallback;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.server.api.ChildBody;
import org.opendaylight.restconf.server.api.CreateResourceResult;
import org.opendaylight.restconf.server.api.DataGetResult;
import org.opendaylight.restconf.server.api.DataPatchResult;
import org.opendaylight.restconf.server.api.DataPostBody;
import org.opendaylight.restconf.server.api.DataPostResult;
import org.opendaylight.restconf.server.api.DataPutResult;
import org.opendaylight.restconf.server.api.DataYangPatchResult;
import org.opendaylight.restconf.server.api.InvokeResult;
import org.opendaylight.restconf.server.api.JsonChildBody;
import org.opendaylight.restconf.server.api.JsonDataPostBody;
import org.opendaylight.restconf.server.api.JsonPatchBody;
import org.opendaylight.restconf.server.api.JsonResourceBody;
import org.opendaylight.restconf.server.api.PatchStatusContext;
import org.opendaylight.restconf.server.api.RestconfServer;
import org.opendaylight.restconf.server.api.ServerRequest;
import org.opendaylight.restconf.server.api.XmlChildBody;
import org.opendaylight.restconf.server.api.XmlDataPostBody;
import org.opendaylight.restconf.server.api.XmlPatchBody;
import org.opendaylight.restconf.server.api.XmlResourceBody;
import org.opendaylight.restconf.server.spi.ErrorTagMapping;
import org.opendaylight.restconf.server.spi.YangPatchStatusBody;
import org.opendaylight.yangtools.yang.common.Empty;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Static request processor serving RESTCONF and Yang-Patch requests for data resource.
 *
 * @see RFC 8040.
 * Section 3.3.1. {+restconf}/data
 */
final class DataRequestProcessor {
    private static final Logger LOG = LoggerFactory.getLogger(DataRequestProcessor.class);

    @VisibleForTesting
    static final String ALLOW_METHODS_ROOT = allowHeaderValue(OPTIONS, HEAD, GET, POST, PUT, PATCH);
    @VisibleForTesting
    static final String ALLOW_METHODS = allowHeaderValue(OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE);

    private DataRequestProcessor() {
        // hidden on purpose
    }

    static void processDataRequest(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback) {
        final var contentType = params.contentType();
        final var apiPath = extractApiPath(params);
        switch (params.method()) {
            // resource options -> https://datatracker.ietf.org/doc/html/rfc8040#section-4.1
            case OPTIONS -> options(params, callback, apiPath);
            // retrieve data and metadata for a resource -> https://datatracker.ietf.org/doc/html/rfc8040#section-4.3
            // HEAD is same as GET but without content -> https://datatracker.ietf.org/doc/html/rfc8040#section-4.2
            case HEAD, GET -> getData(params, service, callback, apiPath);
            case POST -> {
                if (RESTCONF_TYPES.contains(contentType)) {
                    // create resource -> https://datatracker.ietf.org/doc/html/rfc8040#section-4.4.1
                    // or invoke an action -> https://datatracker.ietf.org/doc/html/rfc8040#section-3.6
                    postData(params, service, callback, apiPath);
                } else {
                    callback.onSuccess(unsupportedMediaTypeErrorResponse(params));
                }
            }
            case PUT -> {
                if (RESTCONF_TYPES.contains(contentType)) {
                    // create or replace target resource -> https://datatracker.ietf.org/doc/html/rfc8040#section-4.5
                    putData(params, service, callback, apiPath);
                } else {
                    callback.onSuccess(unsupportedMediaTypeErrorResponse(params));
                }
            }
            case PATCH -> {
                if (RESTCONF_TYPES.contains(contentType)) {
                    // Plain RESTCONF patch = merge target resource content ->
                    // https://datatracker.ietf.org/doc/html/rfc8040#section-4.6.1
                    patchData(params, service, callback, apiPath);
                } else if (YANG_PATCH_TYPES.contains(contentType)) {
                    // YANG Patch = ordered list of edits that are applied to the target datastore ->
                    // https://datatracker.ietf.org/doc/html/draft-ietf-netconf-yang-patch-14#section-2
                    yangPatchData(params, service, callback, apiPath);
                } else {
                    callback.onSuccess(unsupportedMediaTypeErrorResponse(params));
                }
            }
            // delete target resource -> https://datatracker.ietf.org/doc/html/rfc8040#section-4.7
            case DELETE -> deleteData(params, service, callback, apiPath);
            default -> callback.onSuccess(unmappedRequestErrorResponse(params));
        }
    }

    private static void options(final RequestParameters params, final FutureCallback callback,
            ApiPath apiPath) {
        callback.onSuccess(
            responseBuilder(params, HttpResponseStatus.OK)
                .setHeader(HttpHeaderNames.ALLOW, apiPath.isEmpty() ? ALLOW_METHODS_ROOT : ALLOW_METHODS)
                .setHeader(HttpHeaderNames.ACCEPT_PATCH, ACCEPT_PATCH_HEADER_VALUE).build());
    }

    private static void getData(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback, final ApiPath apiPath) {
        final var request = new NettyServerRequest(params, callback,
            result -> responseBuilder(params, HttpResponseStatus.OK)
                .setHeader(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE)
                .setMetadataHeaders(result).setBody(result.body()).build());
        if (apiPath.isEmpty()) {
            service.dataGET(request);
        } else {
            service.dataGET(request, apiPath);
        }
    }

    private static void postData(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback, final ApiPath apiPath) {
        if (apiPath.isEmpty()) {
            final ChildBody childBody = requestBody(params, JsonChildBody::new, XmlChildBody::new);
            service.dataPOST(postRequest(params, callback), childBody);
        } else {
            final DataPostBody dataPostBody = requestBody(params, JsonDataPostBody::new, XmlDataPostBody::new);
            service.dataPOST(postRequest(params, callback), apiPath, dataPostBody);
        }
    }

    private static  ServerRequest postRequest(final RequestParameters params,
            final FutureCallback callback) {
        return new NettyServerRequest<>(params, callback, result -> {
            if (result instanceof CreateResourceResult createResult) {
                final var location = params.baseUri() + PathParameters.DATA + "/" + createResult.createdPath();
                return responseBuilder(params, HttpResponseStatus.CREATED)
                    .setHeader(HttpHeaderNames.LOCATION, location)
                    .setMetadataHeaders(createResult).build();
            }
            if (result instanceof InvokeResult invokeResult) {
                final var output = invokeResult.output();
                return output == null ? simpleResponse(params, HttpResponseStatus.NO_CONTENT)
                    : responseBuilder(params, HttpResponseStatus.OK).setBody(output).build();
            }
            // below is not expected
            LOG.error("Unexpected response {}", result);
            return simpleErrorResponse(params, ErrorTag.OPERATION_FAILED,
                "Internal error. See server logs for details");
        });
    }

    private static void putData(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback, final ApiPath apiPath) {
        final var request = new NettyServerRequest(params, callback,
            result -> {
                final var status = result.created() ? HttpResponseStatus.CREATED : HttpResponseStatus.NO_CONTENT;
                return responseBuilder(params, status).setMetadataHeaders(result).build();
            });
        final var dataResourceBody = requestBody(params, JsonResourceBody::new, XmlResourceBody::new);
        if (apiPath.isEmpty()) {
            service.dataPUT(request, dataResourceBody);
        } else {
            service.dataPUT(request, apiPath, dataResourceBody);
        }
    }

    private static void patchData(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback, final ApiPath apiPath) {
        final var request = new NettyServerRequest(params, callback, result ->
            responseBuilder(params, HttpResponseStatus.OK).setMetadataHeaders(result).build());
        final var dataResourceBody = requestBody(params, JsonResourceBody::new, XmlResourceBody::new);
        if (apiPath.isEmpty()) {
            service.dataPATCH(request, dataResourceBody);
        } else {
            service.dataPATCH(request, apiPath, dataResourceBody);
        }
    }

    private static void yangPatchData(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback, final ApiPath apiPath) {
        final var request = new NettyServerRequest(params, callback,
            result -> {
                final var patchStatus = result.status();
                final var status = patchResponseStatus(patchStatus, params.errorTagMapping());
                return responseBuilder(params, status)
                    .setBody(new YangPatchStatusBody(patchStatus))
                    .setMetadataHeaders(result).build();
            });
        final var yangPatchBody = requestBody(params, JsonPatchBody::new, XmlPatchBody::new);
        if (apiPath.isEmpty()) {
            service.dataPATCH(request, yangPatchBody);
        } else {
            service.dataPATCH(request, apiPath, yangPatchBody);
        }
    }

    private static HttpResponseStatus patchResponseStatus(final PatchStatusContext statusContext,
            final ErrorTagMapping errorTagMapping) {
        if (statusContext.ok()) {
            return HttpResponseStatus.OK;
        }
        final var globalErrors = statusContext.globalErrors();
        if (globalErrors != null && !globalErrors.isEmpty()) {
            return responseStatus(globalErrors.getFirst().tag(), errorTagMapping);
        }
        for (var edit : statusContext.editCollection()) {
            if (!edit.isOk()) {
                final var editErrors = edit.getEditErrors();
                if (editErrors != null && !editErrors.isEmpty()) {
                    return responseStatus(editErrors.getFirst().tag(), errorTagMapping);
                }
            }
        }
        return HttpResponseStatus.INTERNAL_SERVER_ERROR;
    }

    private static void deleteData(final RequestParameters params, final RestconfServer service,
            final FutureCallback callback, final ApiPath apiPath) {
        final var request = new NettyServerRequest(params, callback,
            result -> simpleResponse(params, HttpResponseStatus.NO_CONTENT));
        service.dataDELETE(request, apiPath);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy