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