d018f0f79bae18840389b771daa66b970dab255b
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / jaxrs / JaxRsRestconf.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.nb.jaxrs;
9
10 import static java.util.Objects.requireNonNull;
11
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.Reader;
15 import java.lang.annotation.Annotation;
16 import java.lang.reflect.Type;
17 import java.text.ParseException;
18 import java.util.Date;
19 import java.util.List;
20 import javax.inject.Singleton;
21 import javax.ws.rs.BadRequestException;
22 import javax.ws.rs.Consumes;
23 import javax.ws.rs.DELETE;
24 import javax.ws.rs.Encoded;
25 import javax.ws.rs.GET;
26 import javax.ws.rs.PATCH;
27 import javax.ws.rs.POST;
28 import javax.ws.rs.PUT;
29 import javax.ws.rs.Path;
30 import javax.ws.rs.PathParam;
31 import javax.ws.rs.Produces;
32 import javax.ws.rs.QueryParam;
33 import javax.ws.rs.container.AsyncResponse;
34 import javax.ws.rs.container.Suspended;
35 import javax.ws.rs.core.CacheControl;
36 import javax.ws.rs.core.Context;
37 import javax.ws.rs.core.EntityTag;
38 import javax.ws.rs.core.MediaType;
39 import javax.ws.rs.core.Response;
40 import javax.ws.rs.core.Response.ResponseBuilder;
41 import javax.ws.rs.core.UriInfo;
42 import javax.ws.rs.ext.ParamConverter;
43 import javax.ws.rs.ext.ParamConverterProvider;
44 import org.eclipse.jdt.annotation.NonNull;
45 import org.eclipse.jdt.annotation.Nullable;
46 import org.opendaylight.restconf.api.ApiPath;
47 import org.opendaylight.restconf.api.HttpStatusCode;
48 import org.opendaylight.restconf.api.MediaTypes;
49 import org.opendaylight.restconf.api.QueryParameters;
50 import org.opendaylight.restconf.api.query.PrettyPrintParam;
51 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
52 import org.opendaylight.restconf.common.errors.RestconfError;
53 import org.opendaylight.restconf.common.errors.RestconfFuture;
54 import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
55 import org.opendaylight.restconf.nb.rfc8040.URLConstants;
56 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
57 import org.opendaylight.restconf.server.api.ConfigurationMetadata;
58 import org.opendaylight.restconf.server.api.CreateResourceResult;
59 import org.opendaylight.restconf.server.api.DataGetResult;
60 import org.opendaylight.restconf.server.api.DataPatchResult;
61 import org.opendaylight.restconf.server.api.DataPostResult;
62 import org.opendaylight.restconf.server.api.DataPutResult;
63 import org.opendaylight.restconf.server.api.DataYangPatchResult;
64 import org.opendaylight.restconf.server.api.InvokeResult;
65 import org.opendaylight.restconf.server.api.JsonChildBody;
66 import org.opendaylight.restconf.server.api.JsonDataPostBody;
67 import org.opendaylight.restconf.server.api.JsonOperationInputBody;
68 import org.opendaylight.restconf.server.api.JsonPatchBody;
69 import org.opendaylight.restconf.server.api.JsonResourceBody;
70 import org.opendaylight.restconf.server.api.ModulesGetResult;
71 import org.opendaylight.restconf.server.api.OperationInputBody;
72 import org.opendaylight.restconf.server.api.PatchStatusContext;
73 import org.opendaylight.restconf.server.api.RestconfServer;
74 import org.opendaylight.restconf.server.api.ServerRequest;
75 import org.opendaylight.restconf.server.api.XmlChildBody;
76 import org.opendaylight.restconf.server.api.XmlDataPostBody;
77 import org.opendaylight.restconf.server.api.XmlOperationInputBody;
78 import org.opendaylight.restconf.server.api.XmlPatchBody;
79 import org.opendaylight.restconf.server.api.XmlResourceBody;
80 import org.opendaylight.restconf.server.spi.YangPatchStatusBody;
81 import org.opendaylight.yangtools.yang.common.Empty;
82 import org.opendaylight.yangtools.yang.common.YangConstants;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
85
86 /**
87  * Baseline RESTCONF implementation with JAX-RS. Interfaces to a {@link RestconfServer}. Since we need {@link ApiPath}
88  * arguments, we also implement {@link ParamConverterProvider} and provide the appropriate converter. This has the nice
89  * side-effect of suppressing <a href="https://github.com/eclipse-ee4j/jersey/issues/3700">Jersey warnings</a>.
90  */
91 @Path("/")
92 @Singleton
93 public final class JaxRsRestconf implements ParamConverterProvider {
94     private static final Logger LOG = LoggerFactory.getLogger(JaxRsRestconf.class);
95     private static final CacheControl NO_CACHE = CacheControl.valueOf("no-cache");
96     private static final ParamConverter<ApiPath> API_PATH_CONVERTER = new ParamConverter<>() {
97         @Override
98         public ApiPath fromString(final String value) {
99             final var str = nonnull(value);
100             try {
101                 return ApiPath.parseUrl(str);
102             } catch (ParseException e) {
103                 throw new IllegalArgumentException(e.getMessage(), e);
104             }
105         }
106
107         @Override
108         public String toString(final ApiPath value) {
109             return nonnull(value).toString();
110         }
111
112         private static <T> @NonNull T nonnull(final @Nullable T value) {
113             if (value == null) {
114                 throw new IllegalArgumentException("value must not be null");
115             }
116             return value;
117         }
118     };
119
120     private final @NonNull RestconfServer server;
121     private final @NonNull ServerRequest emptyRequest;
122     private final @NonNull PrettyPrintParam prettyPrint;
123     private final @NonNull ErrorTagMapping errorTagMapping;
124
125     public JaxRsRestconf(final RestconfServer server, final ErrorTagMapping errorTagMapping,
126             final PrettyPrintParam prettyPrint) {
127         this.server = requireNonNull(server);
128         this.errorTagMapping = requireNonNull(errorTagMapping);
129         this.prettyPrint = requireNonNull(prettyPrint);
130         emptyRequest = ServerRequest.of(QueryParameters.of(), prettyPrint);
131
132         LOG.info("RESTCONF data-missing condition is reported as HTTP status {}", switch (errorTagMapping) {
133             case ERRATA_5565 -> "404 (Errata 5565)";
134             case RFC8040 -> "409 (RFC8040)";
135         });
136     }
137
138     private @NonNull ServerRequest requestOf(final UriInfo uriInfo) {
139         final QueryParameters params;
140         try {
141             params = QueryParameters.ofMultiValue(uriInfo.getQueryParameters());
142         } catch (IllegalArgumentException e) {
143             throw new BadRequestException(e.getMessage(), e);
144         }
145         return params.isEmpty() ? emptyRequest : ServerRequest.of(params, prettyPrint);
146     }
147
148     @Override
149     @SuppressWarnings("unchecked")
150     public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType,
151             final Annotation[] annotations) {
152         return ApiPath.class.equals(rawType) ? (ParamConverter<T>) API_PATH_CONVERTER : null;
153     }
154
155     /**
156      * Delete the target data resource.
157      *
158      * @param identifier path to target
159      * @param ar {@link AsyncResponse} which needs to be completed
160      */
161     @DELETE
162     @Path("/data/{identifier:.+}")
163     @SuppressWarnings("checkstyle:abbreviationAsWordInName")
164     public void dataDELETE(@Encoded @PathParam("identifier") final ApiPath identifier,
165             @Suspended final AsyncResponse ar) {
166         server.dataDELETE(emptyRequest, identifier).addCallback(new JaxRsRestconfCallback<>(ar) {
167             @Override
168             Response transform(final Empty result) {
169                 return Response.noContent().build();
170             }
171         });
172     }
173
174     /**
175      * Get target data resource from data root.
176      *
177      * @param uriInfo URI info
178      * @param ar {@link AsyncResponse} which needs to be completed
179      */
180     @GET
181     @Path("/data")
182     @Produces({
183         MediaTypes.APPLICATION_YANG_DATA_JSON,
184         MediaTypes.APPLICATION_YANG_DATA_XML,
185         MediaType.APPLICATION_JSON,
186         MediaType.APPLICATION_XML,
187         MediaType.TEXT_XML
188     })
189     public void dataGET(@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
190         completeDataGET(server.dataGET(requestOf(uriInfo)), ar);
191     }
192
193     /**
194      * Get target data resource.
195      *
196      * @param identifier path to target
197      * @param uriInfo URI info
198      * @param ar {@link AsyncResponse} which needs to be completed
199      */
200     @GET
201     @Path("/data/{identifier:.+}")
202     @Produces({
203         MediaTypes.APPLICATION_YANG_DATA_JSON,
204         MediaTypes.APPLICATION_YANG_DATA_XML,
205         MediaType.APPLICATION_JSON,
206         MediaType.APPLICATION_XML,
207         MediaType.TEXT_XML
208     })
209     public void dataGET(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
210             @Suspended final AsyncResponse ar) {
211         completeDataGET(server.dataGET(requestOf(uriInfo), identifier), ar);
212     }
213
214     private static void completeDataGET(final RestconfFuture<DataGetResult> future, final AsyncResponse ar) {
215         future.addCallback(new JaxRsRestconfCallback<>(ar) {
216             @Override
217             Response transform(final DataGetResult result) {
218                 final var builder = Response.ok().entity(result.payload()).cacheControl(NO_CACHE);
219                 fillConfigurationMetadata(builder, result);
220                 return builder.build();
221             }
222         });
223     }
224
225     private static void fillConfigurationMetadata(final ResponseBuilder builder, final ConfigurationMetadata metadata) {
226         final var etag = metadata.entityTag();
227         if (etag != null) {
228             builder.tag(new EntityTag(etag.value(), etag.weak()));
229         }
230         final var lastModified = metadata.lastModified();
231         if (lastModified != null) {
232             builder.lastModified(Date.from(lastModified));
233         }
234     }
235
236     /**
237      * Partially modify the target data store, as defined in
238      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
239      *
240      * @param body data node for put to config DS
241      * @param ar {@link AsyncResponse} which needs to be completed
242      */
243     @PATCH
244     @Path("/data")
245     @Consumes({
246         MediaTypes.APPLICATION_YANG_DATA_XML,
247         MediaType.APPLICATION_XML,
248         MediaType.TEXT_XML
249     })
250     public void dataXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
251         try (var xmlBody = new XmlResourceBody(body)) {
252             completeDataPATCH(server.dataPATCH(emptyRequest, xmlBody), ar);
253         }
254     }
255
256     /**
257      * Partially modify the target data resource, as defined in
258      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
259      *
260      * @param identifier path to target
261      * @param body data node for put to config DS
262      * @param ar {@link AsyncResponse} which needs to be completed
263      */
264     @PATCH
265     @Path("/data/{identifier:.+}")
266     @Consumes({
267         MediaTypes.APPLICATION_YANG_DATA_XML,
268         MediaType.APPLICATION_XML,
269         MediaType.TEXT_XML
270     })
271     public void dataXmlPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
272             @Suspended final AsyncResponse ar) {
273         try (var xmlBody = new XmlResourceBody(body)) {
274             completeDataPATCH(server.dataPATCH(emptyRequest, identifier, xmlBody), ar);
275         }
276     }
277
278     /**
279      * Partially modify the target data store, as defined in
280      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
281      *
282      * @param body data node for put to config DS
283      * @param ar {@link AsyncResponse} which needs to be completed
284      */
285     @PATCH
286     @Path("/data")
287     @Consumes({
288         MediaTypes.APPLICATION_YANG_DATA_JSON,
289         MediaType.APPLICATION_JSON,
290     })
291     public void dataJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
292         try (var jsonBody = new JsonResourceBody(body)) {
293             completeDataPATCH(server.dataPATCH(emptyRequest, jsonBody), ar);
294         }
295     }
296
297     /**
298      * Partially modify the target data resource, as defined in
299      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
300      *
301      * @param identifier path to target
302      * @param body data node for put to config DS
303      * @param ar {@link AsyncResponse} which needs to be completed
304      */
305     @PATCH
306     @Path("/data/{identifier:.+}")
307     @Consumes({
308         MediaTypes.APPLICATION_YANG_DATA_JSON,
309         MediaType.APPLICATION_JSON,
310     })
311     public void dataJsonPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
312             @Suspended final AsyncResponse ar) {
313         try (var jsonBody = new JsonResourceBody(body)) {
314             completeDataPATCH(server.dataPATCH(emptyRequest, identifier, jsonBody), ar);
315         }
316     }
317
318     private static void completeDataPATCH(final RestconfFuture<DataPatchResult> future, final AsyncResponse ar) {
319         future.addCallback(new JaxRsRestconfCallback<>(ar) {
320             @Override
321             Response transform(final DataPatchResult result) {
322                 final var builder = Response.ok();
323                 fillConfigurationMetadata(builder, result);
324                 return builder.build();
325             }
326         });
327     }
328
329     /**
330      * Ordered list of edits that are applied to the datastore by the server, as defined in
331      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
332      *
333      * @param body YANG Patch body
334      * @param uriInfo URI info
335      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
336      */
337     @PATCH
338     @Path("/data")
339     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
340     @Produces({
341         MediaTypes.APPLICATION_YANG_DATA_JSON,
342         MediaTypes.APPLICATION_YANG_DATA_XML
343     })
344     public void dataYangJsonPATCH(final InputStream body, @Context final UriInfo uriInfo,
345             @Suspended final AsyncResponse ar) {
346         try (var jsonBody = new JsonPatchBody(body)) {
347             completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), jsonBody), ar);
348         }
349     }
350
351     /**
352      * Ordered list of edits that are applied to the target datastore by the server, as defined in
353      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
354      *
355      * @param identifier path to target
356      * @param body YANG Patch body
357      * @param uriInfo URI info
358      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
359      */
360     @PATCH
361     @Path("/data/{identifier:.+}")
362     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
363     @Produces({
364         MediaTypes.APPLICATION_YANG_DATA_JSON,
365         MediaTypes.APPLICATION_YANG_DATA_XML
366     })
367     public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
368             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
369         try (var jsonBody = new JsonPatchBody(body)) {
370             completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), identifier, jsonBody), ar);
371         }
372     }
373
374     /**
375      * Ordered list of edits that are applied to the datastore by the server, as defined in
376      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
377      *
378      * @param body YANG Patch body
379      * @param uriInfo URI info
380      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
381      */
382     @PATCH
383     @Path("/data")
384     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
385     @Produces({
386         MediaTypes.APPLICATION_YANG_DATA_JSON,
387         MediaTypes.APPLICATION_YANG_DATA_XML
388     })
389     public void dataYangXmlPATCH(final InputStream body, @Context final UriInfo uriInfo,
390             @Suspended final AsyncResponse ar) {
391         try (var xmlBody = new XmlPatchBody(body)) {
392             completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), xmlBody), ar);
393         }
394     }
395
396     /**
397      * Ordered list of edits that are applied to the target datastore by the server, as defined in
398      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
399      *
400      * @param identifier path to target
401      * @param uriInfo URI info
402      * @param body YANG Patch body
403      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
404      */
405     @PATCH
406     @Path("/data/{identifier:.+}")
407     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
408     @Produces({
409         MediaTypes.APPLICATION_YANG_DATA_JSON,
410         MediaTypes.APPLICATION_YANG_DATA_XML
411     })
412     public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
413             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
414         try (var xmlBody = new XmlPatchBody(body)) {
415             completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), identifier, xmlBody), ar);
416         }
417     }
418
419     private void completeDataYangPATCH(final RestconfFuture<DataYangPatchResult> future,
420             final AsyncResponse ar) {
421         future.addCallback(new JaxRsRestconfCallback<>(ar) {
422             @Override
423             Response transform(final DataYangPatchResult result) {
424                 final var patchStatus = result.status();
425                 final var statusCode = statusOf(patchStatus);
426                 final var builder = Response.status(statusCode.code(), statusCode.phrase())
427                     .entity(new YangPatchStatusBody(patchStatus));
428                 fillConfigurationMetadata(builder, result);
429                 return builder.build();
430             }
431
432             private HttpStatusCode statusOf(final PatchStatusContext result) {
433                 if (result.ok()) {
434                     return HttpStatusCode.OK;
435                 }
436                 final var globalErrors = result.globalErrors();
437                 if (globalErrors != null && !globalErrors.isEmpty()) {
438                     return statusOfFirst(globalErrors);
439                 }
440                 for (var edit : result.editCollection()) {
441                     if (!edit.isOk()) {
442                         final var editErrors = edit.getEditErrors();
443                         if (editErrors != null && !editErrors.isEmpty()) {
444                             return statusOfFirst(editErrors);
445                         }
446                     }
447                 }
448                 return HttpStatusCode.INTERNAL_SERVER_ERROR;
449             }
450
451             private @NonNull HttpStatusCode statusOfFirst(final List<RestconfError> error) {
452                 return errorTagMapping.statusOf(error.get(0).getErrorTag());
453             }
454         });
455     }
456
457     /**
458      * Create a top-level data resource.
459      *
460      * @param body data node for put to config DS
461      * @param uriInfo URI info
462      * @param ar {@link AsyncResponse} which needs to be completed
463      */
464     @POST
465     @Path("/data")
466     @Consumes({
467         MediaTypes.APPLICATION_YANG_DATA_JSON,
468         MediaType.APPLICATION_JSON,
469     })
470     public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
471             @Suspended final AsyncResponse ar) {
472         try (var jsonBody = new JsonChildBody(body)) {
473             final var request = requestOf(uriInfo);
474             completeDataPOST(server.dataPOST(request, jsonBody), request.prettyPrint(), uriInfo, ar);
475         }
476     }
477
478     /**
479      * Create a data resource in target.
480      *
481      * @param identifier path to target
482      * @param body data node for put to config DS
483      * @param uriInfo URI info
484      * @param ar {@link AsyncResponse} which needs to be completed
485      */
486     @POST
487     @Path("/data/{identifier:.+}")
488     @Consumes({
489         MediaTypes.APPLICATION_YANG_DATA_JSON,
490         MediaType.APPLICATION_JSON,
491     })
492     public void postDataJSON(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
493             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
494         final var request = requestOf(uriInfo);
495         completeDataPOST(server.dataPOST(request, identifier, new JsonDataPostBody(body)), request.prettyPrint(),
496             uriInfo, ar);
497     }
498
499     /**
500      * Create a top-level data resource.
501      *
502      * @param body data node for put to config DS
503      * @param uriInfo URI info
504      * @param ar {@link AsyncResponse} which needs to be completed
505      */
506     @POST
507     @Path("/data")
508     @Consumes({
509         MediaTypes.APPLICATION_YANG_DATA_XML,
510         MediaType.APPLICATION_XML,
511         MediaType.TEXT_XML
512     })
513     public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
514         try (var xmlBody = new XmlChildBody(body)) {
515             final var request = requestOf(uriInfo);
516             completeDataPOST(server.dataPOST(request, xmlBody), request.prettyPrint(), uriInfo, ar);
517         }
518     }
519
520     /**
521      * Create a data resource in target.
522      *
523      * @param identifier path to target
524      * @param body data node for put to config DS
525      * @param uriInfo URI info
526      * @param ar {@link AsyncResponse} which needs to be completed
527      */
528     @POST
529     @Path("/data/{identifier:.+}")
530     @Consumes({
531         MediaTypes.APPLICATION_YANG_DATA_XML,
532         MediaType.APPLICATION_XML,
533         MediaType.TEXT_XML
534     })
535     public void postDataXML(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
536             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
537         final var request = requestOf(uriInfo);
538         completeDataPOST(server.dataPOST(request, identifier, new XmlDataPostBody(body)), request.prettyPrint(),
539             uriInfo, ar);
540     }
541
542     private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future,
543             final PrettyPrintParam prettyPrint, final UriInfo uriInfo, final AsyncResponse ar) {
544         future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
545             @Override
546             Response transform(final DataPostResult result) {
547                 if (result instanceof CreateResourceResult createResource) {
548                     final var builder = Response.created(uriInfo.getBaseUriBuilder()
549                         .path("data")
550                         .path(createResource.createdPath().toString())
551                         .build());
552                     fillConfigurationMetadata(builder, createResource);
553                     return builder.build();
554                 }
555                 if (result instanceof InvokeResult invokeOperation) {
556                     final var output = invokeOperation.output();
557                     return output == null ? Response.noContent().build()
558                         : Response.ok().entity(new JaxRsFormattableBody(output, prettyPrint)).build();
559                 }
560                 LOG.error("Unhandled result {}", result);
561                 return Response.serverError().build();
562             }
563         });
564     }
565
566     /**
567      * Replace the data store.
568      *
569      * @param uriInfo request URI information
570      * @param body data node for put to config DS
571      * @param ar {@link AsyncResponse} which needs to be completed
572      */
573     @PUT
574     @Path("/data")
575     @Consumes({
576         MediaTypes.APPLICATION_YANG_DATA_JSON,
577         MediaType.APPLICATION_JSON,
578     })
579     public void dataJsonPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
580         try (var jsonBody = new JsonResourceBody(body)) {
581             completeDataPUT(server.dataPUT(requestOf(uriInfo), jsonBody), ar);
582         }
583     }
584
585     /**
586      * Create or replace the target data resource.
587      *
588      * @param identifier path to target
589      * @param uriInfo request URI information
590      * @param body data node for put to config DS
591      * @param ar {@link AsyncResponse} which needs to be completed
592      */
593     @PUT
594     @Path("/data/{identifier:.+}")
595     @Consumes({
596         MediaTypes.APPLICATION_YANG_DATA_JSON,
597         MediaType.APPLICATION_JSON,
598     })
599     public void dataJsonPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
600             final InputStream body, @Suspended final AsyncResponse ar) {
601         try (var jsonBody = new JsonResourceBody(body)) {
602             completeDataPUT(server.dataPUT(requestOf(uriInfo), identifier, jsonBody), ar);
603         }
604     }
605
606     /**
607      * Replace the data store.
608      *
609      * @param uriInfo request URI information
610      * @param body data node for put to config DS
611      * @param ar {@link AsyncResponse} which needs to be completed
612      */
613     @PUT
614     @Path("/data")
615     @Consumes({
616         MediaTypes.APPLICATION_YANG_DATA_XML,
617         MediaType.APPLICATION_XML,
618         MediaType.TEXT_XML
619     })
620     public void dataXmlPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
621         try (var xmlBody = new XmlResourceBody(body)) {
622             completeDataPUT(server.dataPUT(requestOf(uriInfo), xmlBody), ar);
623         }
624     }
625
626     /**
627      * Create or replace the target data resource.
628      *
629      * @param identifier path to target
630      * @param uriInfo request URI information
631      * @param body data node for put to config DS
632      * @param ar {@link AsyncResponse} which needs to be completed
633      */
634     @PUT
635     @Path("/data/{identifier:.+}")
636     @Consumes({
637         MediaTypes.APPLICATION_YANG_DATA_XML,
638         MediaType.APPLICATION_XML,
639         MediaType.TEXT_XML
640     })
641     public void dataXmlPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
642             final InputStream body, @Suspended final AsyncResponse ar) {
643         try (var xmlBody = new XmlResourceBody(body)) {
644             completeDataPUT(server.dataPUT(requestOf(uriInfo), identifier, xmlBody), ar);
645         }
646     }
647
648     private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
649         future.addCallback(new JaxRsRestconfCallback<>(ar) {
650             @Override
651             Response transform(final DataPutResult result) {
652                 // Note: no Location header, as it matches the request path
653                 final var builder = result.created() ? Response.created(null) : Response.noContent();
654                 fillConfigurationMetadata(builder, result);
655                 return builder.build();
656             }
657         });
658     }
659
660     /**
661      * List RPC and action operations.
662      *
663      * @param ar {@link AsyncResponse} which needs to be completed
664      */
665     @GET
666     @Path("/operations")
667     @Produces({
668         MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML,
669         MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON
670     })
671     public void operationsGET(@Suspended final AsyncResponse ar) {
672         server.operationsGET(emptyRequest).addCallback(new FormattableBodyCallback(ar, prettyPrint));
673     }
674
675     /**
676      * Retrieve list of operations and actions supported by the server or device.
677      *
678      * @param operation path parameter to identify device and/or operation
679      * @param ar {@link AsyncResponse} which needs to be completed
680      */
681     @GET
682     @Path("/operations/{operation:.+}")
683     @Produces({
684         MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML,
685         MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON
686     })
687     public void operationsGET(@PathParam("operation") final ApiPath operation, @Suspended final AsyncResponse ar) {
688         server.operationsGET(emptyRequest, operation)
689             .addCallback(new FormattableBodyCallback(ar, prettyPrint));
690     }
691
692     /**
693      * Invoke RPC operation.
694      *
695      * @param identifier module name and rpc identifier string for the desired operation
696      * @param body the body of the operation
697      * @param uriInfo URI info
698      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
699      */
700     @POST
701     // FIXME: identifier is just a *single* QName
702     @Path("/operations/{identifier:.+}")
703     @Consumes({
704         MediaTypes.APPLICATION_YANG_DATA_XML,
705         MediaType.APPLICATION_XML,
706         MediaType.TEXT_XML
707     })
708     @Produces({
709         MediaTypes.APPLICATION_YANG_DATA_JSON,
710         MediaTypes.APPLICATION_YANG_DATA_XML,
711         MediaType.APPLICATION_JSON,
712         MediaType.APPLICATION_XML,
713         MediaType.TEXT_XML
714     })
715     public void operationsXmlPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
716             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
717         try (var xmlBody = new XmlOperationInputBody(body)) {
718             operationsPOST(identifier, uriInfo, ar, xmlBody);
719         }
720     }
721
722     /**
723      * Invoke RPC operation.
724      *
725      * @param identifier module name and rpc identifier string for the desired operation
726      * @param body the body of the operation
727      * @param uriInfo URI info
728      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
729      */
730     @POST
731     // FIXME: identifier is just a *single* QName
732     @Path("/operations/{identifier:.+}")
733     @Consumes({
734         MediaTypes.APPLICATION_YANG_DATA_JSON,
735         MediaType.APPLICATION_JSON,
736     })
737     @Produces({
738         MediaTypes.APPLICATION_YANG_DATA_JSON,
739         MediaTypes.APPLICATION_YANG_DATA_XML,
740         MediaType.APPLICATION_JSON,
741         MediaType.APPLICATION_XML,
742         MediaType.TEXT_XML
743     })
744     public void operationsJsonPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
745             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
746         try (var jsonBody = new JsonOperationInputBody(body)) {
747             operationsPOST(identifier, uriInfo, ar, jsonBody);
748         }
749     }
750
751     private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
752             final OperationInputBody body) {
753         server.operationsPOST(requestOf(uriInfo), uriInfo.getBaseUri(), identifier, body)
754             .addCallback(new JaxRsRestconfCallback<>(ar) {
755                 @Override
756                 Response transform(final InvokeResult result) {
757                     final var body = result.output();
758                     return body == null ? Response.noContent().build()
759                         : Response.ok().entity(body).build();
760                 }
761             });
762     }
763
764     /**
765      * Get revision of IETF YANG Library module.
766      *
767      * @param ar {@link AsyncResponse} which needs to be completed
768      */
769     @GET
770     @Path("/yang-library-version")
771     @Produces({
772         MediaTypes.APPLICATION_YANG_DATA_JSON,
773         MediaTypes.APPLICATION_YANG_DATA_XML,
774         MediaType.APPLICATION_JSON,
775         MediaType.APPLICATION_XML,
776         MediaType.TEXT_XML
777     })
778     public void yangLibraryVersionGET(@Suspended final AsyncResponse ar) {
779         server.yangLibraryVersionGET(emptyRequest).addCallback(new FormattableBodyCallback(ar, prettyPrint));
780     }
781
782     // FIXME: References to these resources are generated by our yang-library implementation. That means:
783     //        - We really need to formalize the parameter structure so we get some help from JAX-RS during matching
784     //          of three things:
785     //          - optional yang-ext:mount prefix(es)
786     //          - mandatory module name
787     //          - optional module revision
788     //        - We really should use /yang-library-module/{name}(/{revision})?
789     //        - We seem to be lacking explicit support for submodules in there -- and those locations should then point
790     //          to /yang-library-submodule/{moduleName}(/{moduleRevision})?/{name}(/{revision})? so as to look the
791     //          submodule up efficiently and allow for the weird case where there are two submodules with the same name
792     //          (that is currently not supported by the parser, but it will be in the future)
793     //        - It does not make sense to support yang-ext:mount, unless we also intercept mount points and rewrite
794     //          yang-library locations. We most likely want to do that to ensure users are not tempted to connect to
795     //          wild destinations
796
797     /**
798      * Get schema of specific module.
799      *
800      * @param fileName source file name
801      * @param revision source revision
802      * @param ar {@link AsyncResponse} which needs to be completed
803      */
804     @GET
805     @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
806     @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
807     public void modulesYangGET(@PathParam("fileName") final String fileName,
808             @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
809         completeModulesGET(server.modulesYangGET(emptyRequest, fileName, revision), ar);
810     }
811
812     /**
813      * Get schema of specific module.
814      *
815      * @param mountPath mount point path
816      * @param fileName source file name
817      * @param revision source revision
818      * @param ar {@link AsyncResponse} which needs to be completed
819      */
820     @GET
821     @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
822     @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
823     public void modulesYangGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
824             @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
825             @Suspended final AsyncResponse ar) {
826         completeModulesGET(server.modulesYangGET(emptyRequest, mountPath, fileName, revision), ar);
827     }
828
829     /**
830      * Get schema of specific module.
831      *
832      * @param fileName source file name
833      * @param revision source revision
834      * @param ar {@link AsyncResponse} which needs to be completed
835      */
836     @GET
837     @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
838     @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
839     public void modulesYinGET(@PathParam("fileName") final String fileName,
840             @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
841         completeModulesGET(server.modulesYinGET(emptyRequest, fileName, revision), ar);
842     }
843
844     /**
845      * Get schema of specific module.
846      *
847      * @param mountPath mount point path
848      * @param fileName source file name
849      * @param revision source revision
850      * @param ar {@link AsyncResponse} which needs to be completed
851      */
852     @GET
853     @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
854     @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
855     public void modulesYinGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
856             @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
857             @Suspended final AsyncResponse ar) {
858         completeModulesGET(server.modulesYinGET(emptyRequest, mountPath, fileName, revision), ar);
859     }
860
861     private static void completeModulesGET(final RestconfFuture<ModulesGetResult> future, final AsyncResponse ar) {
862         future.addCallback(new JaxRsRestconfCallback<>(ar) {
863             @Override
864             Response transform(final ModulesGetResult result) {
865                 final Reader reader;
866                 try {
867                     reader = result.source().openStream();
868                 } catch (IOException e) {
869                     throw new RestconfDocumentedException("Cannot open source", e);
870                 }
871                 return Response.ok(reader).build();
872             }
873         });
874     }
875 }