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