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