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