Switch RestconfSchemaServiceImpl to RestconfFuture
[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.InputStream;
13 import java.time.Clock;
14 import java.time.LocalDateTime;
15 import java.time.format.DateTimeFormatter;
16 import java.util.List;
17 import java.util.function.Function;
18 import javax.ws.rs.Consumes;
19 import javax.ws.rs.DELETE;
20 import javax.ws.rs.Encoded;
21 import javax.ws.rs.GET;
22 import javax.ws.rs.PATCH;
23 import javax.ws.rs.POST;
24 import javax.ws.rs.PUT;
25 import javax.ws.rs.Path;
26 import javax.ws.rs.PathParam;
27 import javax.ws.rs.Produces;
28 import javax.ws.rs.container.AsyncResponse;
29 import javax.ws.rs.container.Suspended;
30 import javax.ws.rs.core.Context;
31 import javax.ws.rs.core.MediaType;
32 import javax.ws.rs.core.Response;
33 import javax.ws.rs.core.Response.Status;
34 import javax.ws.rs.core.UriInfo;
35 import org.opendaylight.restconf.api.ApiPath;
36 import org.opendaylight.restconf.api.MediaTypes;
37 import org.opendaylight.restconf.common.errors.RestconfError;
38 import org.opendaylight.restconf.common.errors.RestconfFuture;
39 import org.opendaylight.restconf.common.patch.PatchStatusContext;
40 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
41 import org.opendaylight.restconf.nb.rfc8040.databind.JsonChildBody;
42 import org.opendaylight.restconf.nb.rfc8040.databind.JsonDataPostBody;
43 import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
44 import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
45 import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
46 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
47 import org.opendaylight.restconf.nb.rfc8040.databind.XmlChildBody;
48 import org.opendaylight.restconf.nb.rfc8040.databind.XmlDataPostBody;
49 import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
50 import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
51 import org.opendaylight.restconf.nb.rfc8040.databind.XmlResourceBody;
52 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
53 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
54 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
55 import org.opendaylight.restconf.server.api.DataPostResult;
56 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
57 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
58 import org.opendaylight.restconf.server.api.DataPutResult;
59 import org.opendaylight.restconf.server.api.OperationsGetResult;
60 import org.opendaylight.restconf.server.api.RestconfServer;
61 import org.opendaylight.restconf.server.spi.OperationOutput;
62 import org.opendaylight.yangtools.yang.common.Empty;
63 import org.opendaylight.yangtools.yang.common.Revision;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67 /**
68  * Baseline RESTCONF implementation with JAX-RS. Interfaces to a {@link RestconfServer}.
69  */
70 @Path("/")
71 public final class JaxRsRestconf {
72     private static final Logger LOG = LoggerFactory.getLogger(JaxRsRestconf.class);
73     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
74
75     private final RestconfServer server;
76
77     public JaxRsRestconf(final RestconfServer server) {
78         this.server = requireNonNull(server);
79     }
80
81     /**
82      * Delete the target data resource.
83      *
84      * @param identifier path to target
85      * @param ar {@link AsyncResponse} which needs to be completed
86      */
87     @DELETE
88     @Path("/data/{identifier:.+}")
89     @SuppressWarnings("checkstyle:abbreviationAsWordInName")
90     public void dataDELETE(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
91             @Suspended final AsyncResponse ar) {
92         server.dataDELETE(identifier.apiPath).addCallback(new JaxRsRestconfCallback<>(ar) {
93             @Override
94             protected Response transform(final Empty result) {
95                 return Response.noContent().build();
96             }
97         });
98     }
99
100     /**
101      * Get target data resource from data root.
102      *
103      * @param uriInfo URI info
104      * @param ar {@link AsyncResponse} which needs to be completed
105      */
106     @GET
107     @Path("/data")
108     @Produces({
109         MediaTypes.APPLICATION_YANG_DATA_JSON,
110         MediaTypes.APPLICATION_YANG_DATA_XML,
111         MediaType.APPLICATION_JSON,
112         MediaType.APPLICATION_XML,
113         MediaType.TEXT_XML
114     })
115     public void dataGET(@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
116         final var readParams = QueryParams.newReadDataParams(uriInfo);
117         completeDataGET(server.dataGET(readParams), readParams, ar);
118     }
119
120     /**
121      * Get target data resource.
122      *
123      * @param identifier path to target
124      * @param uriInfo URI info
125      * @param ar {@link AsyncResponse} which needs to be completed
126      */
127     @GET
128     @Path("/data/{identifier:.+}")
129     @Produces({
130         MediaTypes.APPLICATION_YANG_DATA_JSON,
131         MediaTypes.APPLICATION_YANG_DATA_XML,
132         MediaType.APPLICATION_JSON,
133         MediaType.APPLICATION_XML,
134         MediaType.TEXT_XML
135     })
136     public void dataGET(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
137             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
138         final var readParams = QueryParams.newReadDataParams(uriInfo);
139         completeDataGET(server.dataGET(identifier.apiPath, readParams), readParams, ar);
140     }
141
142     private static void completeDataGET(final RestconfFuture<NormalizedNodePayload> future,
143             final ReadDataParams readParams, final AsyncResponse ar) {
144         future.addCallback(new JaxRsRestconfCallback<>(ar) {
145             @Override
146             protected Response transform(final NormalizedNodePayload result) {
147                 return switch (readParams.content()) {
148                     case ALL, CONFIG -> {
149                         final var type = result.data().name().getNodeType();
150                         yield Response.status(Status.OK)
151                             .entity(result)
152                             // FIXME: is this ETag okay?
153                             .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null)
154                                 + "-" + type.getLocalName() + '"')
155                             .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
156                             .build();
157                     }
158                     case NONCONFIG -> Response.status(Status.OK).entity(result).build();
159                 };
160             }
161         });
162     }
163
164     /**
165      * Partially modify the target data store, as defined in
166      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
167      *
168      * @param body data node for put to config DS
169      * @param ar {@link AsyncResponse} which needs to be completed
170      */
171     @PATCH
172     @Path("/data")
173     @Consumes({
174         MediaTypes.APPLICATION_YANG_DATA_XML,
175         MediaType.APPLICATION_XML,
176         MediaType.TEXT_XML
177     })
178     public void dataXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
179         try (var xmlBody = new XmlResourceBody(body)) {
180             completeDataPATCH(server.dataPATCH(xmlBody), ar);
181         }
182     }
183
184     /**
185      * Partially modify the target data resource, as defined in
186      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
187      *
188      * @param identifier path to target
189      * @param body data node for put to config DS
190      * @param ar {@link AsyncResponse} which needs to be completed
191      */
192     @PATCH
193     @Path("/data/{identifier:.+}")
194     @Consumes({
195         MediaTypes.APPLICATION_YANG_DATA_XML,
196         MediaType.APPLICATION_XML,
197         MediaType.TEXT_XML
198     })
199     public void dataXmlPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
200             @Suspended final AsyncResponse ar) {
201         try (var xmlBody = new XmlResourceBody(body)) {
202             completeDataPATCH(server.dataPATCH(identifier.apiPath, xmlBody), ar);
203         }
204     }
205
206     /**
207      * Partially modify the target data store, as defined in
208      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
209      *
210      * @param body data node for put to config DS
211      * @param ar {@link AsyncResponse} which needs to be completed
212      */
213     @PATCH
214     @Path("/data")
215     @Consumes({
216         MediaTypes.APPLICATION_YANG_DATA_JSON,
217         MediaType.APPLICATION_JSON,
218     })
219     public void dataJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
220         try (var jsonBody = new JsonResourceBody(body)) {
221             completeDataPATCH(server.dataPATCH(jsonBody), ar);
222         }
223     }
224
225     /**
226      * Partially modify the target data resource, as defined in
227      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
228      *
229      * @param identifier path to target
230      * @param body data node for put to config DS
231      * @param ar {@link AsyncResponse} which needs to be completed
232      */
233     @PATCH
234     @Path("/data/{identifier:.+}")
235     @Consumes({
236         MediaTypes.APPLICATION_YANG_DATA_JSON,
237         MediaType.APPLICATION_JSON,
238     })
239     public void dataJsonPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
240             @Suspended final AsyncResponse ar) {
241         try (var jsonBody = new JsonResourceBody(body)) {
242             completeDataPATCH(server.dataPATCH(identifier.apiPath, jsonBody), ar);
243         }
244     }
245
246     private static void completeDataPATCH(final RestconfFuture<Empty> future, final AsyncResponse ar) {
247         future.addCallback(new JaxRsRestconfCallback<>(ar) {
248             @Override
249             protected Response transform(final Empty result) {
250                 return Response.ok().build();
251             }
252         });
253     }
254
255     /**
256      * Ordered list of edits that are applied to the datastore by the server, as defined in
257      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
258      *
259      * @param body YANG Patch body
260      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
261      */
262     @PATCH
263     @Path("/data")
264     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
265     @Produces({
266         MediaTypes.APPLICATION_YANG_DATA_JSON,
267         MediaTypes.APPLICATION_YANG_DATA_XML
268     })
269     public void dataYangJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
270         try (var jsonBody = new JsonPatchBody(body)) {
271             completeDataYangPATCH(server.dataPATCH(jsonBody), ar);
272         }
273     }
274
275     /**
276      * Ordered list of edits that are applied to the target datastore by the server, as defined in
277      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
278      *
279      * @param identifier path to target
280      * @param body YANG Patch body
281      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
282      */
283     @PATCH
284     @Path("/data/{identifier:.+}")
285     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
286     @Produces({
287         MediaTypes.APPLICATION_YANG_DATA_JSON,
288         MediaTypes.APPLICATION_YANG_DATA_XML
289     })
290     public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
291             final InputStream body, @Suspended final AsyncResponse ar) {
292         try (var jsonBody = new JsonPatchBody(body)) {
293             completeDataYangPATCH(server.dataPATCH(identifier.apiPath, jsonBody), ar);
294         }
295     }
296
297     /**
298      * Ordered list of edits that are applied to the datastore by the server, as defined in
299      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
300      *
301      * @param body YANG Patch body
302      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
303      */
304     @PATCH
305     @Path("/data")
306     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
307     @Produces({
308         MediaTypes.APPLICATION_YANG_DATA_JSON,
309         MediaTypes.APPLICATION_YANG_DATA_XML
310     })
311     public void dataYangXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
312         try (var xmlBody = new XmlPatchBody(body)) {
313             completeDataYangPATCH(server.dataPATCH(xmlBody), ar);
314         }
315     }
316
317     /**
318      * Ordered list of edits that are applied to the target datastore by the server, as defined in
319      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
320      *
321      * @param identifier path to target
322      * @param body YANG Patch body
323      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
324      */
325     @PATCH
326     @Path("/data/{identifier:.+}")
327     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
328     @Produces({
329         MediaTypes.APPLICATION_YANG_DATA_JSON,
330         MediaTypes.APPLICATION_YANG_DATA_XML
331     })
332     public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
333             final InputStream body, @Suspended final AsyncResponse ar) {
334         try (var xmlBody = new XmlPatchBody(body)) {
335             completeDataYangPATCH(server.dataPATCH(identifier.apiPath, xmlBody), ar);
336         }
337     }
338
339     private static void completeDataYangPATCH(final RestconfFuture<PatchStatusContext> future, final AsyncResponse ar) {
340         future.addCallback(new JaxRsRestconfCallback<>(ar) {
341             @Override
342             protected Response transform(final PatchStatusContext result) {
343                 return Response.status(statusOf(result)).entity(result).build();
344             }
345
346             private static Status statusOf(final PatchStatusContext result) {
347                 if (result.ok()) {
348                     return Status.OK;
349                 }
350                 final var globalErrors = result.globalErrors();
351                 if (globalErrors != null && !globalErrors.isEmpty()) {
352                     return statusOfFirst(globalErrors);
353                 }
354                 for (var edit : result.editCollection()) {
355                     if (!edit.isOk()) {
356                         final var editErrors = edit.getEditErrors();
357                         if (editErrors != null && !editErrors.isEmpty()) {
358                             return statusOfFirst(editErrors);
359                         }
360                     }
361                 }
362                 return Status.INTERNAL_SERVER_ERROR;
363             }
364
365             private static Status statusOfFirst(final List<RestconfError> error) {
366                 return ErrorTags.statusOf(error.get(0).getErrorTag());
367             }
368         });
369     }
370
371     /**
372      * Create a top-level data resource.
373      *
374      * @param body data node for put to config DS
375      * @param uriInfo URI info
376      * @param ar {@link AsyncResponse} which needs to be completed
377      */
378     @POST
379     @Path("/data")
380     @Consumes({
381         MediaTypes.APPLICATION_YANG_DATA_JSON,
382         MediaType.APPLICATION_JSON,
383     })
384     public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
385             @Suspended final AsyncResponse ar) {
386         try (var jsonBody = new JsonChildBody(body)) {
387             completeDataPOST(server.dataPOST(jsonBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
388         }
389     }
390
391     /**
392      * Create a data resource in target.
393      *
394      * @param identifier path to target
395      * @param body data node for put to config DS
396      * @param uriInfo URI info
397      * @param ar {@link AsyncResponse} which needs to be completed
398      */
399     @POST
400     @Path("/data/{identifier:.+}")
401     @Consumes({
402         MediaTypes.APPLICATION_YANG_DATA_JSON,
403         MediaType.APPLICATION_JSON,
404     })
405     public void postDataJSON(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
406             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
407         completeDataPOST(server.dataPOST(identifier.apiPath, new JsonDataPostBody(body),
408             QueryParams.normalize(uriInfo)), uriInfo, ar);
409     }
410
411     /**
412      * Create a top-level data resource.
413      *
414      * @param body data node for put to config DS
415      * @param uriInfo URI info
416      * @param ar {@link AsyncResponse} which needs to be completed
417      */
418     @POST
419     @Path("/data")
420     @Consumes({
421         MediaTypes.APPLICATION_YANG_DATA_XML,
422         MediaType.APPLICATION_XML,
423         MediaType.TEXT_XML
424     })
425     public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
426         try (var xmlBody = new XmlChildBody(body)) {
427             completeDataPOST(server.dataPOST(xmlBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
428         }
429     }
430
431     /**
432      * Create a data resource in target.
433      *
434      * @param identifier path to target
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/{identifier:.+}")
441     @Consumes({
442         MediaTypes.APPLICATION_YANG_DATA_XML,
443         MediaType.APPLICATION_XML,
444         MediaType.TEXT_XML
445     })
446     public void postDataXML(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
447             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
448         completeDataPOST(server.dataPOST(identifier.apiPath, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
449             uriInfo, ar);
450     }
451
452     private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future, final UriInfo uriInfo,
453             final AsyncResponse ar) {
454         future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
455             @Override
456             protected Response transform(final DataPostResult result) {
457                 if (result instanceof CreateResource createResource) {
458                     return Response.created(uriInfo.getBaseUriBuilder()
459                             .path("data")
460                             .path(createResource.createdPath())
461                             .build())
462                         .build();
463                 }
464                 if (result instanceof InvokeOperation invokeOperation) {
465                     final var output = invokeOperation.output();
466                     return output == null ? Response.status(Status.NO_CONTENT).build()
467                         : Response.status(Status.OK).entity(output).build();
468                 }
469                 LOG.error("Unhandled result {}", result);
470                 return Response.serverError().build();
471             }
472         });
473     }
474
475     /**
476      * Replace the data store.
477      *
478      * @param uriInfo request URI information
479      * @param body data node for put to config DS
480      * @param ar {@link AsyncResponse} which needs to be completed
481      */
482     @PUT
483     @Path("/data")
484     @Consumes({
485         MediaTypes.APPLICATION_YANG_DATA_JSON,
486         MediaType.APPLICATION_JSON,
487     })
488     public void dataJsonPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
489         try (var jsonBody = new JsonResourceBody(body)) {
490             completeDataPUT(server.dataPUT(jsonBody, QueryParams.normalize(uriInfo)), ar);
491         }
492     }
493
494     /**
495      * Create or replace the target data resource.
496      *
497      * @param identifier path to target
498      * @param uriInfo request URI information
499      * @param body data node for put to config DS
500      * @param ar {@link AsyncResponse} which needs to be completed
501      */
502     @PUT
503     @Path("/data/{identifier:.+}")
504     @Consumes({
505         MediaTypes.APPLICATION_YANG_DATA_JSON,
506         MediaType.APPLICATION_JSON,
507     })
508     public void dataJsonPUT(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
509             @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
510         try (var jsonBody = new JsonResourceBody(body)) {
511             completeDataPUT(server.dataPUT(identifier.apiPath, jsonBody, QueryParams.normalize(uriInfo)), ar);
512         }
513     }
514
515     /**
516      * Replace the data store.
517      *
518      * @param uriInfo request URI information
519      * @param body data node for put to config DS
520      * @param ar {@link AsyncResponse} which needs to be completed
521      */
522     @PUT
523     @Path("/data")
524     @Consumes({
525         MediaTypes.APPLICATION_YANG_DATA_XML,
526         MediaType.APPLICATION_XML,
527         MediaType.TEXT_XML
528     })
529     public void dataXmlPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
530         try (var xmlBody = new XmlResourceBody(body)) {
531             completeDataPUT(server.dataPUT(xmlBody, QueryParams.normalize(uriInfo)), ar);
532         }
533     }
534
535     /**
536      * Create or replace the target data resource.
537      *
538      * @param identifier path to target
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/{identifier:.+}")
545     @Consumes({
546         MediaTypes.APPLICATION_YANG_DATA_XML,
547         MediaType.APPLICATION_XML,
548         MediaType.TEXT_XML
549     })
550     public void dataXmlPUT(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
551             @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
552         try (var xmlBody = new XmlResourceBody(body)) {
553             completeDataPUT(server.dataPUT(identifier.apiPath, xmlBody, QueryParams.normalize(uriInfo)), ar);
554         }
555     }
556
557     private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
558         future.addCallback(new JaxRsRestconfCallback<>(ar) {
559             @Override
560             protected Response transform(final DataPutResult result) {
561                 return switch (result) {
562                     // Note: no Location header, as it matches the request path
563                     case CREATED -> Response.status(Status.CREATED).build();
564                     case REPLACED -> Response.noContent().build();
565                 };
566             }
567         });
568     }
569
570     /**
571      * List RPC and action operations in RFC7951 format.
572      *
573      * @param ar {@link AsyncResponse} which needs to be completed
574      */
575     @GET
576     @Path("/operations")
577     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
578     public void operationsJsonGET(@Suspended final AsyncResponse ar) {
579         completeOperationsJsonGet(server.operationsGET(), ar);
580     }
581
582     /**
583      * Retrieve list of operations and actions supported by the server or device in JSON format.
584      *
585      * @param operation path parameter to identify device and/or operation
586      * @param ar {@link AsyncResponse} which needs to be completed
587      */
588     @GET
589     @Path("/operations/{operation:.+}")
590     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
591     public void operationsJsonGET(@PathParam("operation") final JaxRsApiPath operation, final AsyncResponse ar) {
592         completeOperationsGet(server.operationsGET(operation.apiPath), ar, OperationsGetResult::toJSON);
593     }
594
595     private static void completeOperationsJsonGet(final RestconfFuture<OperationsGetResult> future,
596             final AsyncResponse ar) {
597         completeOperationsGet(future, ar, OperationsGetResult::toJSON);
598     }
599
600     /**
601      * List RPC and action operations in RFC8040 XML format.
602      *
603      * @param ar {@link AsyncResponse} which needs to be completed
604      */
605     @GET
606     @Path("/operations")
607     @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
608     public void operationsXmlGET(@Suspended final AsyncResponse ar) {
609         completeOperationsXmlGet(server.operationsGET(), ar);
610     }
611
612     /**
613      * Retrieve list of operations and actions supported by the server or device in XML format.
614      *
615      * @param operation path parameter to identify device and/or operation
616      * @param ar {@link AsyncResponse} which needs to be completed
617      */
618     @GET
619     @Path("/operations/{operation:.+}")
620     @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
621     public void operationsXmlGET(@PathParam("operation") final JaxRsApiPath operation, final AsyncResponse ar) {
622         completeOperationsXmlGet(server.operationsGET(operation.apiPath), ar);
623     }
624
625     private static void completeOperationsXmlGet(final RestconfFuture<OperationsGetResult> future,
626             final AsyncResponse ar) {
627         completeOperationsGet(future, ar, OperationsGetResult::toXML);
628     }
629
630     private static void completeOperationsGet(final RestconfFuture<OperationsGetResult> future, final AsyncResponse ar,
631             final Function<OperationsGetResult, String> toString) {
632         future.addCallback(new JaxRsRestconfCallback<OperationsGetResult>(ar) {
633             @Override
634             protected Response transform(final OperationsGetResult result) {
635                 return Response.ok().entity(toString.apply(result)).build();
636             }
637         });
638     }
639
640     /**
641      * Invoke RPC operation.
642      *
643      * @param identifier module name and rpc identifier string for the desired operation
644      * @param body the body of the operation
645      * @param uriInfo URI info
646      * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
647      */
648     @POST
649     // FIXME: identifier is just a *single* QName
650     @Path("/operations/{identifier:.+}")
651     @Consumes({
652         MediaTypes.APPLICATION_YANG_DATA_XML,
653         MediaType.APPLICATION_XML,
654         MediaType.TEXT_XML
655     })
656     @Produces({
657         MediaTypes.APPLICATION_YANG_DATA_JSON,
658         MediaTypes.APPLICATION_YANG_DATA_XML,
659         MediaType.APPLICATION_JSON,
660         MediaType.APPLICATION_XML,
661         MediaType.TEXT_XML
662     })
663     public void operationsXmlPOST(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
664             final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
665         try (var xmlBody = new XmlOperationInputBody(body)) {
666             operationsPOST(identifier.apiPath, uriInfo, ar, xmlBody);
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_JSON,
683         MediaType.APPLICATION_JSON,
684     })
685     @Produces({
686         MediaTypes.APPLICATION_YANG_DATA_JSON,
687         MediaTypes.APPLICATION_YANG_DATA_XML,
688         MediaType.APPLICATION_JSON,
689         MediaType.APPLICATION_XML,
690         MediaType.TEXT_XML
691     })
692     public void operationsJsonPOST(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
693             final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
694         try (var jsonBody = new JsonOperationInputBody(body)) {
695             operationsPOST(identifier.apiPath, uriInfo, ar, jsonBody);
696         }
697     }
698
699     private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
700             final OperationInputBody body) {
701         server.operationsPOST(uriInfo.getBaseUri(), identifier, body)
702             .addCallback(new JaxRsRestconfCallback<OperationOutput>(ar) {
703                 @Override
704                 protected Response transform(final OperationOutput result) {
705                     final var body = result.output();
706                     return body == null ? Response.noContent().build()
707                         : Response.ok().entity(new NormalizedNodePayload(result.operation(), body)).build();
708                 }
709             });
710     }
711
712     /**
713      * Get revision of IETF YANG Library module.
714      *
715      * @param ar {@link AsyncResponse} which needs to be completed
716      */
717     @GET
718     @Path("/yang-library-version")
719     @Produces({
720         MediaTypes.APPLICATION_YANG_DATA_JSON,
721         MediaTypes.APPLICATION_YANG_DATA_XML,
722         MediaType.APPLICATION_JSON,
723         MediaType.APPLICATION_XML,
724         MediaType.TEXT_XML
725     })
726     public void yangLibraryVersionGET(@Suspended final AsyncResponse ar) {
727         server.yangLibraryVersionGET().addCallback(new JaxRsRestconfCallback<NormalizedNodePayload>(ar) {
728             @Override
729             protected Response transform(final NormalizedNodePayload result) {
730                 return Response.ok().entity(result).build();
731             }
732         });
733     }
734 }