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