Introduce YangPatchStatusBody
[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.common.errors.RestconfDocumentedException;
50 import org.opendaylight.restconf.common.errors.RestconfError;
51 import org.opendaylight.restconf.common.errors.RestconfFuture;
52 import org.opendaylight.restconf.nb.rfc8040.URLConstants;
53 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
54 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
55 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
56 import org.opendaylight.restconf.server.api.ConfigurationMetadata;
57 import org.opendaylight.restconf.server.api.CreateResourceResult;
58 import org.opendaylight.restconf.server.api.DataGetResult;
59 import org.opendaylight.restconf.server.api.DataPatchResult;
60 import org.opendaylight.restconf.server.api.DataPostResult;
61 import org.opendaylight.restconf.server.api.DataPutResult;
62 import org.opendaylight.restconf.server.api.DataYangPatchResult;
63 import org.opendaylight.restconf.server.api.InvokeResult;
64 import org.opendaylight.restconf.server.api.JsonChildBody;
65 import org.opendaylight.restconf.server.api.JsonDataPostBody;
66 import org.opendaylight.restconf.server.api.JsonOperationInputBody;
67 import org.opendaylight.restconf.server.api.JsonPatchBody;
68 import org.opendaylight.restconf.server.api.JsonResourceBody;
69 import org.opendaylight.restconf.server.api.ModulesGetResult;
70 import org.opendaylight.restconf.server.api.OperationInputBody;
71 import org.opendaylight.restconf.server.api.PatchStatusContext;
72 import org.opendaylight.restconf.server.api.RestconfServer;
73 import org.opendaylight.restconf.server.api.XmlChildBody;
74 import org.opendaylight.restconf.server.api.XmlDataPostBody;
75 import org.opendaylight.restconf.server.api.XmlOperationInputBody;
76 import org.opendaylight.restconf.server.api.XmlPatchBody;
77 import org.opendaylight.restconf.server.api.XmlResourceBody;
78 import org.opendaylight.restconf.server.spi.YangPatchStatusBody;
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.ok().entity(result.payload()).cacheControl(NO_CACHE);
195                 fillConfigurationMetadata(builder, result);
196                 return builder.build();
197             }
198         });
199     }
200
201     private static void fillConfigurationMetadata(final ResponseBuilder builder, final ConfigurationMetadata metadata) {
202         final var etag = metadata.entityTag();
203         if (etag != null) {
204             builder.tag(new EntityTag(etag.value(), etag.weak()));
205         }
206         final var lastModified = metadata.lastModified();
207         if (lastModified != null) {
208             builder.lastModified(Date.from(lastModified));
209         }
210     }
211
212     /**
213      * Partially modify the target data store, as defined in
214      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
215      *
216      * @param body data node for put to config DS
217      * @param ar {@link AsyncResponse} which needs to be completed
218      */
219     @PATCH
220     @Path("/data")
221     @Consumes({
222         MediaTypes.APPLICATION_YANG_DATA_XML,
223         MediaType.APPLICATION_XML,
224         MediaType.TEXT_XML
225     })
226     public void dataXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
227         try (var xmlBody = new XmlResourceBody(body)) {
228             completeDataPATCH(server.dataPATCH(xmlBody), ar);
229         }
230     }
231
232     /**
233      * Partially modify the target data resource, as defined in
234      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
235      *
236      * @param identifier path to target
237      * @param body data node for put to config DS
238      * @param ar {@link AsyncResponse} which needs to be completed
239      */
240     @PATCH
241     @Path("/data/{identifier:.+}")
242     @Consumes({
243         MediaTypes.APPLICATION_YANG_DATA_XML,
244         MediaType.APPLICATION_XML,
245         MediaType.TEXT_XML
246     })
247     public void dataXmlPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
248             @Suspended final AsyncResponse ar) {
249         try (var xmlBody = new XmlResourceBody(body)) {
250             completeDataPATCH(server.dataPATCH(identifier, xmlBody), ar);
251         }
252     }
253
254     /**
255      * Partially modify the target data store, as defined in
256      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
257      *
258      * @param body data node for put to config DS
259      * @param ar {@link AsyncResponse} which needs to be completed
260      */
261     @PATCH
262     @Path("/data")
263     @Consumes({
264         MediaTypes.APPLICATION_YANG_DATA_JSON,
265         MediaType.APPLICATION_JSON,
266     })
267     public void dataJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
268         try (var jsonBody = new JsonResourceBody(body)) {
269             completeDataPATCH(server.dataPATCH(jsonBody), ar);
270         }
271     }
272
273     /**
274      * Partially modify the target data resource, as defined in
275      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
276      *
277      * @param identifier path to target
278      * @param body data node for put to config DS
279      * @param ar {@link AsyncResponse} which needs to be completed
280      */
281     @PATCH
282     @Path("/data/{identifier:.+}")
283     @Consumes({
284         MediaTypes.APPLICATION_YANG_DATA_JSON,
285         MediaType.APPLICATION_JSON,
286     })
287     public void dataJsonPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
288             @Suspended final AsyncResponse ar) {
289         try (var jsonBody = new JsonResourceBody(body)) {
290             completeDataPATCH(server.dataPATCH(identifier, jsonBody), ar);
291         }
292     }
293
294     private static void completeDataPATCH(final RestconfFuture<DataPatchResult> future, final AsyncResponse ar) {
295         future.addCallback(new JaxRsRestconfCallback<>(ar) {
296             @Override
297             Response transform(final DataPatchResult result) {
298                 final var builder = Response.ok();
299                 fillConfigurationMetadata(builder, result);
300                 return builder.build();
301             }
302         });
303     }
304
305     /**
306      * Ordered list of edits that are applied to the datastore by the server, as defined in
307      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
308      *
309      * @param body YANG Patch body
310      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
311      */
312     @PATCH
313     @Path("/data")
314     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
315     @Produces({
316         MediaTypes.APPLICATION_YANG_DATA_JSON,
317         MediaTypes.APPLICATION_YANG_DATA_XML
318     })
319     public void dataYangJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
320         try (var jsonBody = new JsonPatchBody(body)) {
321             completeDataYangPATCH(server.dataPATCH(jsonBody), ar);
322         }
323     }
324
325     /**
326      * Ordered list of edits that are applied to the target datastore by the server, as defined in
327      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
328      *
329      * @param identifier path to target
330      * @param body YANG Patch body
331      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
332      */
333     @PATCH
334     @Path("/data/{identifier:.+}")
335     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
336     @Produces({
337         MediaTypes.APPLICATION_YANG_DATA_JSON,
338         MediaTypes.APPLICATION_YANG_DATA_XML
339     })
340     public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
341             @Suspended final AsyncResponse ar) {
342         try (var jsonBody = new JsonPatchBody(body)) {
343             completeDataYangPATCH(server.dataPATCH(identifier, jsonBody), ar);
344         }
345     }
346
347     /**
348      * Ordered list of edits that are applied to the datastore by the server, as defined in
349      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
350      *
351      * @param body YANG Patch body
352      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
353      */
354     @PATCH
355     @Path("/data")
356     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
357     @Produces({
358         MediaTypes.APPLICATION_YANG_DATA_JSON,
359         MediaTypes.APPLICATION_YANG_DATA_XML
360     })
361     public void dataYangXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
362         try (var xmlBody = new XmlPatchBody(body)) {
363             completeDataYangPATCH(server.dataPATCH(xmlBody), ar);
364         }
365     }
366
367     /**
368      * Ordered list of edits that are applied to the target datastore by the server, as defined in
369      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
370      *
371      * @param identifier path to target
372      * @param body YANG Patch body
373      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
374      */
375     @PATCH
376     @Path("/data/{identifier:.+}")
377     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
378     @Produces({
379         MediaTypes.APPLICATION_YANG_DATA_JSON,
380         MediaTypes.APPLICATION_YANG_DATA_XML
381     })
382     public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
383             @Suspended final AsyncResponse ar) {
384         try (var xmlBody = new XmlPatchBody(body)) {
385             completeDataYangPATCH(server.dataPATCH(identifier, xmlBody), ar);
386         }
387     }
388
389     private static void completeDataYangPATCH(final RestconfFuture<DataYangPatchResult> future,
390             final AsyncResponse ar) {
391         future.addCallback(new JaxRsRestconfCallback<>(ar) {
392             @Override
393             Response transform(final DataYangPatchResult result) {
394                 final var status = result.status();
395                 final var builder = Response.status(statusOf(status)).entity(new YangPatchStatusBody(status));
396                 fillConfigurationMetadata(builder, result);
397                 return builder.build();
398             }
399
400             private static Status statusOf(final PatchStatusContext result) {
401                 if (result.ok()) {
402                     return Status.OK;
403                 }
404                 final var globalErrors = result.globalErrors();
405                 if (globalErrors != null && !globalErrors.isEmpty()) {
406                     return statusOfFirst(globalErrors);
407                 }
408                 for (var edit : result.editCollection()) {
409                     if (!edit.isOk()) {
410                         final var editErrors = edit.getEditErrors();
411                         if (editErrors != null && !editErrors.isEmpty()) {
412                             return statusOfFirst(editErrors);
413                         }
414                     }
415                 }
416                 return Status.INTERNAL_SERVER_ERROR;
417             }
418
419             private static Status statusOfFirst(final List<RestconfError> error) {
420                 return ErrorTags.statusOf(error.get(0).getErrorTag());
421             }
422         });
423     }
424
425     /**
426      * Create a top-level data resource.
427      *
428      * @param body data node for put to config DS
429      * @param uriInfo URI info
430      * @param ar {@link AsyncResponse} which needs to be completed
431      */
432     @POST
433     @Path("/data")
434     @Consumes({
435         MediaTypes.APPLICATION_YANG_DATA_JSON,
436         MediaType.APPLICATION_JSON,
437     })
438     public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
439             @Suspended final AsyncResponse ar) {
440         try (var jsonBody = new JsonChildBody(body)) {
441             completeDataPOST(server.dataPOST(jsonBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
442         }
443     }
444
445     /**
446      * Create a data resource in target.
447      *
448      * @param identifier path to target
449      * @param body data node for put to config DS
450      * @param uriInfo URI info
451      * @param ar {@link AsyncResponse} which needs to be completed
452      */
453     @POST
454     @Path("/data/{identifier:.+}")
455     @Consumes({
456         MediaTypes.APPLICATION_YANG_DATA_JSON,
457         MediaType.APPLICATION_JSON,
458     })
459     public void postDataJSON(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
460             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
461         completeDataPOST(server.dataPOST(identifier, new JsonDataPostBody(body), QueryParams.normalize(uriInfo)),
462             uriInfo, ar);
463     }
464
465     /**
466      * Create a top-level data resource.
467      *
468      * @param body data node for put to config DS
469      * @param uriInfo URI info
470      * @param ar {@link AsyncResponse} which needs to be completed
471      */
472     @POST
473     @Path("/data")
474     @Consumes({
475         MediaTypes.APPLICATION_YANG_DATA_XML,
476         MediaType.APPLICATION_XML,
477         MediaType.TEXT_XML
478     })
479     public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
480         try (var xmlBody = new XmlChildBody(body)) {
481             completeDataPOST(server.dataPOST(xmlBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
482         }
483     }
484
485     /**
486      * Create a data resource in target.
487      *
488      * @param identifier path to target
489      * @param body data node for put to config DS
490      * @param uriInfo URI info
491      * @param ar {@link AsyncResponse} which needs to be completed
492      */
493     @POST
494     @Path("/data/{identifier:.+}")
495     @Consumes({
496         MediaTypes.APPLICATION_YANG_DATA_XML,
497         MediaType.APPLICATION_XML,
498         MediaType.TEXT_XML
499     })
500     public void postDataXML(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
501             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
502         completeDataPOST(server.dataPOST(identifier, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
503             uriInfo, ar);
504     }
505
506     private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future, final UriInfo uriInfo,
507             final AsyncResponse ar) {
508         future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
509             @Override
510             Response transform(final DataPostResult result) {
511                 if (result instanceof CreateResourceResult createResource) {
512                     final var builder = Response.created(uriInfo.getBaseUriBuilder()
513                         .path("data")
514                         .path(createResource.createdPath().toString())
515                         .build());
516                     fillConfigurationMetadata(builder, createResource);
517                     return builder.build();
518                 }
519                 if (result instanceof InvokeResult invokeOperation) {
520                     final var output = invokeOperation.output();
521                     return output == null ? Response.noContent().build() : Response.ok().entity(output).build();
522                 }
523                 LOG.error("Unhandled result {}", result);
524                 return Response.serverError().build();
525             }
526         });
527     }
528
529     /**
530      * Replace the data store.
531      *
532      * @param uriInfo request URI information
533      * @param body data node for put to config DS
534      * @param ar {@link AsyncResponse} which needs to be completed
535      */
536     @PUT
537     @Path("/data")
538     @Consumes({
539         MediaTypes.APPLICATION_YANG_DATA_JSON,
540         MediaType.APPLICATION_JSON,
541     })
542     public void dataJsonPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
543         try (var jsonBody = new JsonResourceBody(body)) {
544             completeDataPUT(server.dataPUT(jsonBody, QueryParams.normalize(uriInfo)), ar);
545         }
546     }
547
548     /**
549      * Create or replace the target data resource.
550      *
551      * @param identifier path to target
552      * @param uriInfo request URI information
553      * @param body data node for put to config DS
554      * @param ar {@link AsyncResponse} which needs to be completed
555      */
556     @PUT
557     @Path("/data/{identifier:.+}")
558     @Consumes({
559         MediaTypes.APPLICATION_YANG_DATA_JSON,
560         MediaType.APPLICATION_JSON,
561     })
562     public void dataJsonPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
563             final InputStream body, @Suspended final AsyncResponse ar) {
564         try (var jsonBody = new JsonResourceBody(body)) {
565             completeDataPUT(server.dataPUT(identifier, jsonBody, QueryParams.normalize(uriInfo)), ar);
566         }
567     }
568
569     /**
570      * Replace the data store.
571      *
572      * @param uriInfo request URI information
573      * @param body data node for put to config DS
574      * @param ar {@link AsyncResponse} which needs to be completed
575      */
576     @PUT
577     @Path("/data")
578     @Consumes({
579         MediaTypes.APPLICATION_YANG_DATA_XML,
580         MediaType.APPLICATION_XML,
581         MediaType.TEXT_XML
582     })
583     public void dataXmlPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
584         try (var xmlBody = new XmlResourceBody(body)) {
585             completeDataPUT(server.dataPUT(xmlBody, QueryParams.normalize(uriInfo)), ar);
586         }
587     }
588
589     /**
590      * Create or replace the target data resource.
591      *
592      * @param identifier path to target
593      * @param uriInfo request URI information
594      * @param body data node for put to config DS
595      * @param ar {@link AsyncResponse} which needs to be completed
596      */
597     @PUT
598     @Path("/data/{identifier:.+}")
599     @Consumes({
600         MediaTypes.APPLICATION_YANG_DATA_XML,
601         MediaType.APPLICATION_XML,
602         MediaType.TEXT_XML
603     })
604     public void dataXmlPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
605             final InputStream body, @Suspended final AsyncResponse ar) {
606         try (var xmlBody = new XmlResourceBody(body)) {
607             completeDataPUT(server.dataPUT(identifier, xmlBody, QueryParams.normalize(uriInfo)), ar);
608         }
609     }
610
611     private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
612         future.addCallback(new JaxRsRestconfCallback<>(ar) {
613             @Override
614             Response transform(final DataPutResult result) {
615                 // Note: no Location header, as it matches the request path
616                 final var builder = result.created() ? Response.created(null) : Response.noContent();
617                 fillConfigurationMetadata(builder, result);
618                 return builder.build();
619             }
620         });
621     }
622
623     /**
624      * List RPC and action operations.
625      *
626      * @param ar {@link AsyncResponse} which needs to be completed
627      */
628     @GET
629     @Path("/operations")
630     @Produces({
631         MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML,
632         MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON
633     })
634     public void operationsGET(@Suspended final AsyncResponse ar) {
635         completeOperationsGet(server.operationsGET(), ar);
636     }
637
638     /**
639      * Retrieve list of operations and actions supported by the server or device.
640      *
641      * @param operation path parameter to identify device and/or operation
642      * @param ar {@link AsyncResponse} which needs to be completed
643      */
644     @GET
645     @Path("/operations/{operation:.+}")
646     @Produces({
647         MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML,
648         MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON
649     })
650     public void operationsGET(@PathParam("operation") final ApiPath operation, @Suspended final AsyncResponse ar) {
651         completeOperationsGet(server.operationsGET(operation), ar);
652     }
653
654     private static void completeOperationsGet(final RestconfFuture<FormattableBody> future, final AsyncResponse ar) {
655         future.addCallback(new JaxRsRestconfCallback<FormattableBody>(ar) {
656             @Override
657             Response transform(final FormattableBody result) {
658                 return Response.ok().entity(result).build();
659             }
660         });
661     }
662
663     /**
664      * Invoke RPC operation.
665      *
666      * @param identifier module name and rpc identifier string for the desired operation
667      * @param body the body of the operation
668      * @param uriInfo URI info
669      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
670      */
671     @POST
672     // FIXME: identifier is just a *single* QName
673     @Path("/operations/{identifier:.+}")
674     @Consumes({
675         MediaTypes.APPLICATION_YANG_DATA_XML,
676         MediaType.APPLICATION_XML,
677         MediaType.TEXT_XML
678     })
679     @Produces({
680         MediaTypes.APPLICATION_YANG_DATA_JSON,
681         MediaTypes.APPLICATION_YANG_DATA_XML,
682         MediaType.APPLICATION_JSON,
683         MediaType.APPLICATION_XML,
684         MediaType.TEXT_XML
685     })
686     public void operationsXmlPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
687             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
688         try (var xmlBody = new XmlOperationInputBody(body)) {
689             operationsPOST(identifier, uriInfo, ar, xmlBody);
690         }
691     }
692
693     /**
694      * Invoke RPC operation.
695      *
696      * @param identifier module name and rpc identifier string for the desired operation
697      * @param body the body of the operation
698      * @param uriInfo URI info
699      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
700      */
701     @POST
702     // FIXME: identifier is just a *single* QName
703     @Path("/operations/{identifier:.+}")
704     @Consumes({
705         MediaTypes.APPLICATION_YANG_DATA_JSON,
706         MediaType.APPLICATION_JSON,
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 operationsJsonPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
716             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
717         try (var jsonBody = new JsonOperationInputBody(body)) {
718             operationsPOST(identifier, uriInfo, ar, jsonBody);
719         }
720     }
721
722     private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
723             final OperationInputBody body) {
724         server.operationsPOST(uriInfo.getBaseUri(), identifier, QueryParams.normalize(uriInfo), body)
725             .addCallback(new JaxRsRestconfCallback<>(ar) {
726                 @Override
727                 Response transform(final InvokeResult result) {
728                     final var body = result.output();
729                     return body == null ? Response.noContent().build()
730                         : Response.ok().entity(body).build();
731                 }
732             });
733     }
734
735     /**
736      * Get revision of IETF YANG Library module.
737      *
738      * @param ar {@link AsyncResponse} which needs to be completed
739      */
740     @GET
741     @Path("/yang-library-version")
742     @Produces({
743         MediaTypes.APPLICATION_YANG_DATA_JSON,
744         MediaTypes.APPLICATION_YANG_DATA_XML,
745         MediaType.APPLICATION_JSON,
746         MediaType.APPLICATION_XML,
747         MediaType.TEXT_XML
748     })
749     public void yangLibraryVersionGET(@Suspended final AsyncResponse ar) {
750         server.yangLibraryVersionGET().addCallback(new JaxRsRestconfCallback<NormalizedNodePayload>(ar) {
751             @Override
752             Response transform(final NormalizedNodePayload result) {
753                 return Response.ok().entity(result).build();
754             }
755         });
756     }
757
758     // FIXME: References to these resources are generated by our yang-library implementation. That means:
759     //        - We really need to formalize the parameter structure so we get some help from JAX-RS during matching
760     //          of three things:
761     //          - optional yang-ext:mount prefix(es)
762     //          - mandatory module name
763     //          - optional module revision
764     //        - We really should use /yang-library-module/{name}(/{revision})?
765     //        - We seem to be lacking explicit support for submodules in there -- and those locations should then point
766     //          to /yang-library-submodule/{moduleName}(/{moduleRevision})?/{name}(/{revision})? so as to look the
767     //          submodule up efficiently and allow for the weird case where there are two submodules with the same name
768     //          (that is currently not supported by the parser, but it will be in the future)
769     //        - It does not make sense to support yang-ext:mount, unless we also intercept mount points and rewrite
770     //          yang-library locations. We most likely want to do that to ensure users are not tempted to connect to
771     //          wild destinations
772
773     /**
774      * Get schema of specific module.
775      *
776      * @param fileName source file name
777      * @param revision source revision
778      * @param ar {@link AsyncResponse} which needs to be completed
779      */
780     @GET
781     @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
782     @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
783     public void modulesYangGET(@PathParam("fileName") final String fileName,
784             @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
785         completeModulesGET(server.modulesYangGET(fileName, revision), ar);
786     }
787
788     /**
789      * Get schema of specific module.
790      *
791      * @param mountPath mount point path
792      * @param fileName source file name
793      * @param revision source revision
794      * @param ar {@link AsyncResponse} which needs to be completed
795      */
796     @GET
797     @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
798     @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
799     public void modulesYangGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
800             @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
801             @Suspended final AsyncResponse ar) {
802         completeModulesGET(server.modulesYangGET(mountPath, fileName, revision), ar);
803     }
804
805     /**
806      * Get schema of specific module.
807      *
808      * @param fileName source file name
809      * @param revision source revision
810      * @param ar {@link AsyncResponse} which needs to be completed
811      */
812     @GET
813     @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
814     @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
815     public void modulesYinGET(@PathParam("fileName") final String fileName,
816             @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
817         completeModulesGET(server.modulesYinGET(fileName, revision), ar);
818     }
819
820     /**
821      * Get schema of specific module.
822      *
823      * @param mountPath mount point path
824      * @param fileName source file name
825      * @param revision source revision
826      * @param ar {@link AsyncResponse} which needs to be completed
827      */
828     @GET
829     @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
830     @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
831     public void modulesYinGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
832             @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
833             @Suspended final AsyncResponse ar) {
834         completeModulesGET(server.modulesYinGET(mountPath, fileName, revision), ar);
835     }
836
837     private static void completeModulesGET(final RestconfFuture<ModulesGetResult> future, final AsyncResponse ar) {
838         future.addCallback(new JaxRsRestconfCallback<>(ar) {
839             @Override
840             Response transform(final ModulesGetResult result) {
841                 final Reader reader;
842                 try {
843                     reader = result.source().openStream();
844                 } catch (IOException e) {
845                     throw new RestconfDocumentedException("Cannot open source", e);
846                 }
847                 return Response.ok(reader).build();
848             }
849         });
850     }
851 }