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