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