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