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