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