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