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