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