Refactor modulesGET() methods
[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.time.Clock;
19 import java.time.LocalDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.util.List;
22 import java.util.function.Function;
23 import javax.inject.Singleton;
24 import javax.ws.rs.Consumes;
25 import javax.ws.rs.DELETE;
26 import javax.ws.rs.Encoded;
27 import javax.ws.rs.GET;
28 import javax.ws.rs.PATCH;
29 import javax.ws.rs.POST;
30 import javax.ws.rs.PUT;
31 import javax.ws.rs.Path;
32 import javax.ws.rs.PathParam;
33 import javax.ws.rs.Produces;
34 import javax.ws.rs.QueryParam;
35 import javax.ws.rs.container.AsyncResponse;
36 import javax.ws.rs.container.Suspended;
37 import javax.ws.rs.core.Context;
38 import javax.ws.rs.core.MediaType;
39 import javax.ws.rs.core.Response;
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.MediaTypes;
48 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
49 import org.opendaylight.restconf.common.errors.RestconfError;
50 import org.opendaylight.restconf.common.errors.RestconfFuture;
51 import org.opendaylight.restconf.common.patch.PatchStatusContext;
52 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
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.DataPostResult;
69 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
70 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
71 import org.opendaylight.restconf.server.api.DataPutResult;
72 import org.opendaylight.restconf.server.api.ModulesGetResult;
73 import org.opendaylight.restconf.server.api.OperationsGetResult;
74 import org.opendaylight.restconf.server.api.RestconfServer;
75 import org.opendaylight.restconf.server.spi.OperationOutput;
76 import org.opendaylight.yangtools.yang.common.Empty;
77 import org.opendaylight.yangtools.yang.common.Revision;
78 import org.opendaylight.yangtools.yang.common.YangConstants;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
81
82 /**
83  * Baseline RESTCONF implementation with JAX-RS. Interfaces to a {@link RestconfServer}. Since we need {@link ApiPath}
84  * arguments, we also implement {@link ParamConverterProvider} and provide the appropriate converter. This has the nice
85  * side-effect of suppressing <a href="https://github.com/eclipse-ee4j/jersey/issues/3700">Jersey warnings</a>.
86  */
87 @Path("/")
88 @Singleton
89 public final class JaxRsRestconf implements ParamConverterProvider {
90     private static final Logger LOG = LoggerFactory.getLogger(JaxRsRestconf.class);
91     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
92     private static final ParamConverter<ApiPath> API_PATH_CONVERTER = new ParamConverter<>() {
93         @Override
94         public ApiPath fromString(final String value) {
95             final var str = nonnull(value);
96             try {
97                 return ApiPath.parseUrl(str);
98             } catch (ParseException e) {
99                 throw new IllegalArgumentException(e.getMessage(), e);
100             }
101         }
102
103         @Override
104         public String toString(final ApiPath value) {
105             return nonnull(value).toString();
106         }
107
108         private static <T> @NonNull T nonnull(final @Nullable T value) {
109             if (value == null) {
110                 throw new IllegalArgumentException("value must not be null");
111             }
112             return value;
113         }
114     };
115
116     private final RestconfServer server;
117
118     public JaxRsRestconf(final RestconfServer server) {
119         this.server = requireNonNull(server);
120     }
121
122     @Override
123     @SuppressWarnings("unchecked")
124     public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType,
125             final Annotation[] annotations) {
126         return ApiPath.class.equals(rawType) ? (ParamConverter<T>) API_PATH_CONVERTER : null;
127     }
128
129     /**
130      * Delete the target data resource.
131      *
132      * @param identifier path to target
133      * @param ar {@link AsyncResponse} which needs to be completed
134      */
135     @DELETE
136     @Path("/data/{identifier:.+}")
137     @SuppressWarnings("checkstyle:abbreviationAsWordInName")
138     public void dataDELETE(@Encoded @PathParam("identifier") final ApiPath identifier,
139             @Suspended final AsyncResponse ar) {
140         server.dataDELETE(identifier).addCallback(new JaxRsRestconfCallback<>(ar) {
141             @Override
142             Response transform(final Empty result) {
143                 return Response.noContent().build();
144             }
145         });
146     }
147
148     /**
149      * Get target data resource from data root.
150      *
151      * @param uriInfo URI info
152      * @param ar {@link AsyncResponse} which needs to be completed
153      */
154     @GET
155     @Path("/data")
156     @Produces({
157         MediaTypes.APPLICATION_YANG_DATA_JSON,
158         MediaTypes.APPLICATION_YANG_DATA_XML,
159         MediaType.APPLICATION_JSON,
160         MediaType.APPLICATION_XML,
161         MediaType.TEXT_XML
162     })
163     public void dataGET(@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
164         final var readParams = QueryParams.newReadDataParams(uriInfo);
165         completeDataGET(server.dataGET(readParams), readParams, ar);
166     }
167
168     /**
169      * Get target data resource.
170      *
171      * @param identifier path to target
172      * @param uriInfo URI info
173      * @param ar {@link AsyncResponse} which needs to be completed
174      */
175     @GET
176     @Path("/data/{identifier:.+}")
177     @Produces({
178         MediaTypes.APPLICATION_YANG_DATA_JSON,
179         MediaTypes.APPLICATION_YANG_DATA_XML,
180         MediaType.APPLICATION_JSON,
181         MediaType.APPLICATION_XML,
182         MediaType.TEXT_XML
183     })
184     public void dataGET(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
185             @Suspended final AsyncResponse ar) {
186         final var readParams = QueryParams.newReadDataParams(uriInfo);
187         completeDataGET(server.dataGET(identifier, readParams), readParams, ar);
188     }
189
190     private static void completeDataGET(final RestconfFuture<NormalizedNodePayload> future,
191             final ReadDataParams readParams, final AsyncResponse ar) {
192         future.addCallback(new JaxRsRestconfCallback<>(ar) {
193             @Override
194             Response transform(final NormalizedNodePayload result) {
195                 return switch (readParams.content()) {
196                     case ALL, CONFIG -> {
197                         final var type = result.data().name().getNodeType();
198                         yield Response.status(Status.OK)
199                             .entity(result)
200                             // FIXME: is this ETag okay?
201                             // FIXME: use tag() method instead
202                             .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null)
203                                 + "-" + type.getLocalName() + '"')
204                             // FIXME: use lastModified() method instead
205                             .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
206                             .build();
207                     }
208                     case NONCONFIG -> Response.status(Status.OK).entity(result).build();
209                 };
210             }
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<Empty> future, final AsyncResponse ar) {
297         future.addCallback(new JaxRsRestconfCallback<>(ar) {
298             @Override
299             Response transform(final Empty result) {
300                 return Response.ok().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<PatchStatusContext> future, final AsyncResponse ar) {
390         future.addCallback(new JaxRsRestconfCallback<>(ar) {
391             @Override
392             Response transform(final PatchStatusContext result) {
393                 return Response.status(statusOf(result)).entity(result).build();
394             }
395
396             private static Status statusOf(final PatchStatusContext result) {
397                 if (result.ok()) {
398                     return Status.OK;
399                 }
400                 final var globalErrors = result.globalErrors();
401                 if (globalErrors != null && !globalErrors.isEmpty()) {
402                     return statusOfFirst(globalErrors);
403                 }
404                 for (var edit : result.editCollection()) {
405                     if (!edit.isOk()) {
406                         final var editErrors = edit.getEditErrors();
407                         if (editErrors != null && !editErrors.isEmpty()) {
408                             return statusOfFirst(editErrors);
409                         }
410                     }
411                 }
412                 return Status.INTERNAL_SERVER_ERROR;
413             }
414
415             private static Status statusOfFirst(final List<RestconfError> error) {
416                 return ErrorTags.statusOf(error.get(0).getErrorTag());
417             }
418         });
419     }
420
421     /**
422      * Create a top-level data resource.
423      *
424      * @param body data node for put to config DS
425      * @param uriInfo URI info
426      * @param ar {@link AsyncResponse} which needs to be completed
427      */
428     @POST
429     @Path("/data")
430     @Consumes({
431         MediaTypes.APPLICATION_YANG_DATA_JSON,
432         MediaType.APPLICATION_JSON,
433     })
434     public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
435             @Suspended final AsyncResponse ar) {
436         try (var jsonBody = new JsonChildBody(body)) {
437             completeDataPOST(server.dataPOST(jsonBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
438         }
439     }
440
441     /**
442      * Create a data resource in target.
443      *
444      * @param identifier path to target
445      * @param body data node for put to config DS
446      * @param uriInfo URI info
447      * @param ar {@link AsyncResponse} which needs to be completed
448      */
449     @POST
450     @Path("/data/{identifier:.+}")
451     @Consumes({
452         MediaTypes.APPLICATION_YANG_DATA_JSON,
453         MediaType.APPLICATION_JSON,
454     })
455     public void postDataJSON(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
456             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
457         completeDataPOST(server.dataPOST(identifier, new JsonDataPostBody(body), QueryParams.normalize(uriInfo)),
458             uriInfo, ar);
459     }
460
461     /**
462      * Create a top-level data resource.
463      *
464      * @param body data node for put to config DS
465      * @param uriInfo URI info
466      * @param ar {@link AsyncResponse} which needs to be completed
467      */
468     @POST
469     @Path("/data")
470     @Consumes({
471         MediaTypes.APPLICATION_YANG_DATA_XML,
472         MediaType.APPLICATION_XML,
473         MediaType.TEXT_XML
474     })
475     public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
476         try (var xmlBody = new XmlChildBody(body)) {
477             completeDataPOST(server.dataPOST(xmlBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
478         }
479     }
480
481     /**
482      * Create a data resource in target.
483      *
484      * @param identifier path to target
485      * @param body data node for put to config DS
486      * @param uriInfo URI info
487      * @param ar {@link AsyncResponse} which needs to be completed
488      */
489     @POST
490     @Path("/data/{identifier:.+}")
491     @Consumes({
492         MediaTypes.APPLICATION_YANG_DATA_XML,
493         MediaType.APPLICATION_XML,
494         MediaType.TEXT_XML
495     })
496     public void postDataXML(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
497             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
498         completeDataPOST(server.dataPOST(identifier, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
499             uriInfo, ar);
500     }
501
502     private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future, final UriInfo uriInfo,
503             final AsyncResponse ar) {
504         future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
505             @Override
506             Response transform(final DataPostResult result) {
507                 if (result instanceof CreateResource createResource) {
508                     return Response.created(uriInfo.getBaseUriBuilder()
509                             .path("data")
510                             .path(createResource.createdPath())
511                             .build())
512                         .build();
513                 }
514                 if (result instanceof InvokeOperation invokeOperation) {
515                     final var output = invokeOperation.output();
516                     return output == null ? Response.status(Status.NO_CONTENT).build()
517                         : Response.status(Status.OK).entity(output).build();
518                 }
519                 LOG.error("Unhandled result {}", result);
520                 return Response.serverError().build();
521             }
522         });
523     }
524
525     /**
526      * Replace the data store.
527      *
528      * @param uriInfo request URI information
529      * @param body data node for put to config DS
530      * @param ar {@link AsyncResponse} which needs to be completed
531      */
532     @PUT
533     @Path("/data")
534     @Consumes({
535         MediaTypes.APPLICATION_YANG_DATA_JSON,
536         MediaType.APPLICATION_JSON,
537     })
538     public void dataJsonPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
539         try (var jsonBody = new JsonResourceBody(body)) {
540             completeDataPUT(server.dataPUT(jsonBody, QueryParams.normalize(uriInfo)), ar);
541         }
542     }
543
544     /**
545      * Create or replace the target data resource.
546      *
547      * @param identifier path to target
548      * @param uriInfo request URI information
549      * @param body data node for put to config DS
550      * @param ar {@link AsyncResponse} which needs to be completed
551      */
552     @PUT
553     @Path("/data/{identifier:.+}")
554     @Consumes({
555         MediaTypes.APPLICATION_YANG_DATA_JSON,
556         MediaType.APPLICATION_JSON,
557     })
558     public void dataJsonPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
559             final InputStream body, @Suspended final AsyncResponse ar) {
560         try (var jsonBody = new JsonResourceBody(body)) {
561             completeDataPUT(server.dataPUT(identifier, jsonBody, QueryParams.normalize(uriInfo)), ar);
562         }
563     }
564
565     /**
566      * Replace the data store.
567      *
568      * @param uriInfo request URI information
569      * @param body data node for put to config DS
570      * @param ar {@link AsyncResponse} which needs to be completed
571      */
572     @PUT
573     @Path("/data")
574     @Consumes({
575         MediaTypes.APPLICATION_YANG_DATA_XML,
576         MediaType.APPLICATION_XML,
577         MediaType.TEXT_XML
578     })
579     public void dataXmlPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
580         try (var xmlBody = new XmlResourceBody(body)) {
581             completeDataPUT(server.dataPUT(xmlBody, QueryParams.normalize(uriInfo)), ar);
582         }
583     }
584
585     /**
586      * Create or replace the target data resource.
587      *
588      * @param identifier path to target
589      * @param uriInfo request URI information
590      * @param body data node for put to config DS
591      * @param ar {@link AsyncResponse} which needs to be completed
592      */
593     @PUT
594     @Path("/data/{identifier:.+}")
595     @Consumes({
596         MediaTypes.APPLICATION_YANG_DATA_XML,
597         MediaType.APPLICATION_XML,
598         MediaType.TEXT_XML
599     })
600     public void dataXmlPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
601             final InputStream body, @Suspended final AsyncResponse ar) {
602         try (var xmlBody = new XmlResourceBody(body)) {
603             completeDataPUT(server.dataPUT(identifier, xmlBody, QueryParams.normalize(uriInfo)), ar);
604         }
605     }
606
607     private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
608         future.addCallback(new JaxRsRestconfCallback<>(ar) {
609             @Override
610             Response transform(final DataPutResult result) {
611                 return switch (result) {
612                     // Note: no Location header, as it matches the request path
613                     case CREATED -> Response.status(Status.CREATED).build();
614                     case REPLACED -> Response.noContent().build();
615                 };
616             }
617         });
618     }
619
620     /**
621      * List RPC and action operations in RFC7951 format.
622      *
623      * @param ar {@link AsyncResponse} which needs to be completed
624      */
625     @GET
626     @Path("/operations")
627     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
628     public void operationsJsonGET(@Suspended final AsyncResponse ar) {
629         completeOperationsJsonGet(server.operationsGET(), ar);
630     }
631
632     /**
633      * Retrieve list of operations and actions supported by the server or device in JSON format.
634      *
635      * @param operation path parameter to identify device and/or operation
636      * @param ar {@link AsyncResponse} which needs to be completed
637      */
638     @GET
639     @Path("/operations/{operation:.+}")
640     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
641     public void operationsJsonGET(@PathParam("operation") final ApiPath operation, @Suspended final AsyncResponse ar) {
642         completeOperationsGet(server.operationsGET(operation), ar, OperationsGetResult::toJSON);
643     }
644
645     private static void completeOperationsJsonGet(final RestconfFuture<OperationsGetResult> future,
646             final AsyncResponse ar) {
647         completeOperationsGet(future, ar, OperationsGetResult::toJSON);
648     }
649
650     /**
651      * List RPC and action operations in RFC8040 XML format.
652      *
653      * @param ar {@link AsyncResponse} which needs to be completed
654      */
655     @GET
656     @Path("/operations")
657     @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
658     public void operationsXmlGET(@Suspended final AsyncResponse ar) {
659         completeOperationsXmlGet(server.operationsGET(), ar);
660     }
661
662     /**
663      * Retrieve list of operations and actions supported by the server or device in XML format.
664      *
665      * @param operation path parameter to identify device and/or operation
666      * @param ar {@link AsyncResponse} which needs to be completed
667      */
668     @GET
669     @Path("/operations/{operation:.+}")
670     @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
671     public void operationsXmlGET(@PathParam("operation") final ApiPath operation, @Suspended final AsyncResponse ar) {
672         completeOperationsXmlGet(server.operationsGET(operation), ar);
673     }
674
675     private static void completeOperationsXmlGet(final RestconfFuture<OperationsGetResult> future,
676             final AsyncResponse ar) {
677         completeOperationsGet(future, ar, OperationsGetResult::toXML);
678     }
679
680     private static void completeOperationsGet(final RestconfFuture<OperationsGetResult> future, final AsyncResponse ar,
681             final Function<OperationsGetResult, String> toString) {
682         future.addCallback(new JaxRsRestconfCallback<OperationsGetResult>(ar) {
683             @Override
684             Response transform(final OperationsGetResult result) {
685                 return Response.ok().entity(toString.apply(result)).build();
686             }
687         });
688     }
689
690     /**
691      * Invoke RPC operation.
692      *
693      * @param identifier module name and rpc identifier string for the desired operation
694      * @param body the body of the operation
695      * @param uriInfo URI info
696      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
697      */
698     @POST
699     // FIXME: identifier is just a *single* QName
700     @Path("/operations/{identifier:.+}")
701     @Consumes({
702         MediaTypes.APPLICATION_YANG_DATA_XML,
703         MediaType.APPLICATION_XML,
704         MediaType.TEXT_XML
705     })
706     @Produces({
707         MediaTypes.APPLICATION_YANG_DATA_JSON,
708         MediaTypes.APPLICATION_YANG_DATA_XML,
709         MediaType.APPLICATION_JSON,
710         MediaType.APPLICATION_XML,
711         MediaType.TEXT_XML
712     })
713     public void operationsXmlPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
714             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
715         try (var xmlBody = new XmlOperationInputBody(body)) {
716             operationsPOST(identifier, uriInfo, ar, xmlBody);
717         }
718     }
719
720     /**
721      * Invoke RPC operation.
722      *
723      * @param identifier module name and rpc identifier string for the desired operation
724      * @param body the body of the operation
725      * @param uriInfo URI info
726      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
727      */
728     @POST
729     // FIXME: identifier is just a *single* QName
730     @Path("/operations/{identifier:.+}")
731     @Consumes({
732         MediaTypes.APPLICATION_YANG_DATA_JSON,
733         MediaType.APPLICATION_JSON,
734     })
735     @Produces({
736         MediaTypes.APPLICATION_YANG_DATA_JSON,
737         MediaTypes.APPLICATION_YANG_DATA_XML,
738         MediaType.APPLICATION_JSON,
739         MediaType.APPLICATION_XML,
740         MediaType.TEXT_XML
741     })
742     public void operationsJsonPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
743             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
744         try (var jsonBody = new JsonOperationInputBody(body)) {
745             operationsPOST(identifier, uriInfo, ar, jsonBody);
746         }
747     }
748
749     private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
750             final OperationInputBody body) {
751         server.operationsPOST(uriInfo.getBaseUri(), identifier, body)
752             .addCallback(new JaxRsRestconfCallback<OperationOutput>(ar) {
753                 @Override
754                 Response transform(final OperationOutput result) {
755                     final var body = result.output();
756                     return body == null ? Response.noContent().build()
757                         : Response.ok().entity(new NormalizedNodePayload(result.operation(), body)).build();
758                 }
759             });
760     }
761
762     /**
763      * Get revision of IETF YANG Library module.
764      *
765      * @param ar {@link AsyncResponse} which needs to be completed
766      */
767     @GET
768     @Path("/yang-library-version")
769     @Produces({
770         MediaTypes.APPLICATION_YANG_DATA_JSON,
771         MediaTypes.APPLICATION_YANG_DATA_XML,
772         MediaType.APPLICATION_JSON,
773         MediaType.APPLICATION_XML,
774         MediaType.TEXT_XML
775     })
776     public void yangLibraryVersionGET(@Suspended final AsyncResponse ar) {
777         server.yangLibraryVersionGET().addCallback(new JaxRsRestconfCallback<NormalizedNodePayload>(ar) {
778             @Override
779             Response transform(final NormalizedNodePayload result) {
780                 return Response.ok().entity(result).build();
781             }
782         });
783     }
784
785     // FIXME: References to these resources are generated by our yang-library implementation. That means:
786     //        - We really need to formalize the parameter structure so we get some help from JAX-RS during matching
787     //          of three things:
788     //          - optional yang-ext:mount prefix(es)
789     //          - mandatory module name
790     //          - optional module revision
791     //        - We really should use /yang-library-module/{name}(/{revision})?
792     //        - We seem to be lacking explicit support for submodules in there -- and those locations should then point
793     //          to /yang-library-submodule/{moduleName}(/{moduleRevision})?/{name}(/{revision})? so as to look the
794     //          submodule up efficiently and allow for the weird case where there are two submodules with the same name
795     //          (that is currently not supported by the parser, but it will be in the future)
796     //        - It does not make sense to support yang-ext:mount, unless we also intercept mount points and rewrite
797     //          yang-library locations. We most likely want to do that to ensure users are not tempted to connect to
798     //          wild destinations
799
800     /**
801      * Get schema of specific module.
802      *
803      * @param fileName source file name
804      * @param revision source revision
805      * @param ar {@link AsyncResponse} which needs to be completed
806      */
807     @GET
808     @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
809     @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
810     public void modulesYangGET(@PathParam("fileName") final String fileName,
811             @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
812         completeModulesGET(server.modulesYangGET(fileName, revision), ar);
813     }
814
815     /**
816      * Get schema of specific module.
817      *
818      * @param mountPath mount point path
819      * @param fileName source file name
820      * @param revision source revision
821      * @param ar {@link AsyncResponse} which needs to be completed
822      */
823     @GET
824     @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
825     @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
826     public void modulesYangGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
827             @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
828             @Suspended final AsyncResponse ar) {
829         completeModulesGET(server.modulesYangGET(mountPath, fileName, revision), ar);
830     }
831
832     /**
833      * Get schema of specific module.
834      *
835      * @param fileName source file name
836      * @param revision source revision
837      * @param ar {@link AsyncResponse} which needs to be completed
838      */
839     @GET
840     @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
841     @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
842     public void modulesYinGET(@PathParam("fileName") final String fileName,
843             @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
844         completeModulesGET(server.modulesYinGET(fileName, revision), ar);
845     }
846
847     /**
848      * Get schema of specific module.
849      *
850      * @param mountPath mount point path
851      * @param fileName source file name
852      * @param revision source revision
853      * @param ar {@link AsyncResponse} which needs to be completed
854      */
855     @GET
856     @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
857     @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
858     public void modulesYinGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
859             @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
860             @Suspended final AsyncResponse ar) {
861         completeModulesGET(server.modulesYinGET(mountPath, fileName, revision), ar);
862     }
863
864     private static void completeModulesGET(final RestconfFuture<ModulesGetResult> future, final AsyncResponse ar) {
865         future.addCallback(new JaxRsRestconfCallback<>(ar) {
866             @Override
867             Response transform(final ModulesGetResult result) {
868                 final Reader reader;
869                 try {
870                     reader = result.source().openStream();
871                 } catch (IOException e) {
872                     throw new RestconfDocumentedException("Cannot open source", e);
873                 }
874                 return Response.ok(reader).build();
875             }
876         });
877     }
878 }