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