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