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