Rework tags creation in OpenApi
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / mountpoints / MountPointOpenApi.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. 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.openapi.mountpoints;
9
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
12 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.API_VERSION;
13 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.BASE_PATH;
14 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.OPEN_API_BASIC_AUTH;
15 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.OPEN_API_VERSION;
16 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.SECURITY;
17 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.filterByRange;
18 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.getSortedModules;
19 import static org.opendaylight.restconf.openapi.impl.OpenApiServiceImpl.DEFAULT_PAGESIZE;
20 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.DESCRIPTION_KEY;
21 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.SUMMARY_SEPARATOR;
22
23 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
24 import com.fasterxml.jackson.databind.node.ObjectNode;
25 import com.google.common.collect.Range;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Optional;
31 import java.util.TreeMap;
32 import java.util.concurrent.atomic.AtomicLong;
33 import javax.ws.rs.HttpMethod;
34 import javax.ws.rs.core.Response;
35 import javax.ws.rs.core.UriInfo;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.opendaylight.mdsal.dom.api.DOMMountPointListener;
38 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
39 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
40 import org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator;
41 import org.opendaylight.restconf.openapi.impl.DefinitionNames;
42 import org.opendaylight.restconf.openapi.model.Components;
43 import org.opendaylight.restconf.openapi.model.Info;
44 import org.opendaylight.restconf.openapi.model.OpenApiObject;
45 import org.opendaylight.restconf.openapi.model.Operation;
46 import org.opendaylight.restconf.openapi.model.Path;
47 import org.opendaylight.restconf.openapi.model.Schema;
48 import org.opendaylight.restconf.openapi.model.SecuritySchemes;
49 import org.opendaylight.restconf.openapi.model.Server;
50 import org.opendaylight.yangtools.concepts.ListenerRegistration;
51 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
52 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
53 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
54 import org.opendaylight.yangtools.yang.model.api.Module;
55 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 public class MountPointOpenApi implements DOMMountPointListener, AutoCloseable {
60
61     private static final Logger LOG = LoggerFactory.getLogger(MountPointOpenApi.class);
62
63     private static final String DATASTORES_REVISION = "-";
64     private static final String DATASTORES_LABEL = "Datastores";
65
66     private final DOMSchemaService globalSchema;
67     private final DOMMountPointService mountService;
68     private final BaseYangOpenApiGenerator openApiGenerator;
69     private final Map<YangInstanceIdentifier, Long> instanceIdToLongId =
70             new TreeMap<>((o1, o2) -> o1.toString().compareToIgnoreCase(o2.toString()));
71     private final Map<Long, YangInstanceIdentifier> longIdToInstanceId = new HashMap<>();
72
73     private final Object lock = new Object();
74
75     private final AtomicLong idKey = new AtomicLong(0);
76
77     private ListenerRegistration<DOMMountPointListener> registration;
78
79     public MountPointOpenApi(final DOMSchemaService globalSchema, final DOMMountPointService mountService,
80             final BaseYangOpenApiGenerator openApiGenerator) {
81         this.globalSchema = requireNonNull(globalSchema);
82         this.mountService = requireNonNull(mountService);
83         this.openApiGenerator = requireNonNull(openApiGenerator);
84     }
85
86     public void init() {
87         registration = mountService.registerProvisionListener(this);
88     }
89
90     @Override
91     public void close() {
92         if (registration != null) {
93             registration.close();
94         }
95     }
96
97     public Map<String, Long> getInstanceIdentifiers() {
98         final Map<String, Long> urlToId = new HashMap<>();
99         synchronized (lock) {
100             final SchemaContext context = globalSchema.getGlobalContext();
101             for (final Entry<YangInstanceIdentifier, Long> entry : instanceIdToLongId.entrySet()) {
102                 final String modName = findModuleName(entry.getKey(), context);
103                 urlToId.put(openApiGenerator.generateUrlPrefixFromInstanceID(entry.getKey(), modName),
104                         entry.getValue());
105             }
106         }
107         return urlToId;
108     }
109
110     private static String findModuleName(final YangInstanceIdentifier id, final SchemaContext context) {
111         final PathArgument rootQName = id.getPathArguments().iterator().next();
112         for (final Module mod : context.getModules()) {
113             if (mod.findDataChildByName(rootQName.getNodeType()).isPresent()) {
114                 return mod.getName();
115             }
116         }
117         return null;
118     }
119
120     private String getYangMountUrl(final YangInstanceIdentifier key) {
121         final String modName = findModuleName(key, globalSchema.getGlobalContext());
122         return openApiGenerator.generateUrlPrefixFromInstanceID(key, modName) + "yang-ext:mount";
123     }
124
125     private YangInstanceIdentifier getInstanceId(final Long id) {
126         final YangInstanceIdentifier instanceId;
127         synchronized (lock) {
128             instanceId = longIdToInstanceId.get(id);
129         }
130         return instanceId;
131     }
132
133     private EffectiveModelContext getSchemaContext(final YangInstanceIdentifier id) {
134         if (id == null) {
135             return null;
136         }
137
138         checkState(mountService != null);
139         return mountService.getMountPoint(id)
140             .flatMap(mountPoint -> mountPoint.getService(DOMSchemaService.class))
141             .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext()))
142             .orElse(null);
143     }
144
145     public OpenApiObject getMountPointApi(final UriInfo uriInfo, final Long id, final String module,
146             final String revision) {
147         final YangInstanceIdentifier iid = getInstanceId(id);
148         final EffectiveModelContext context = getSchemaContext(iid);
149         final String urlPrefix = getYangMountUrl(iid);
150         final String deviceName  = extractDeviceName(iid);
151
152         if (context == null) {
153             return null;
154         }
155
156         if (DATASTORES_LABEL.equals(module) && DATASTORES_REVISION.equals(revision)) {
157             return generateDataStoreOpenApi(uriInfo, urlPrefix, deviceName);
158         }
159         return openApiGenerator.getApiDeclaration(module, revision, uriInfo, context, urlPrefix, deviceName);
160     }
161
162     public OpenApiObject getMountPointApi(final UriInfo uriInfo, final Long id, final @Nullable String strPageNum) {
163         final var iid = getInstanceId(id);
164         final var context = getSchemaContext(iid);
165         final var urlPrefix = getYangMountUrl(iid);
166         final var deviceName = extractDeviceName(iid);
167
168         if (context == null) {
169             return null;
170         }
171         final var definitionNames = new DefinitionNames();
172
173         boolean includeDataStore = true;
174         Range<Integer> range = Range.all();
175         if (strPageNum != null) {
176             final var pageNum = Integer.parseInt(strPageNum);
177             final var end = DEFAULT_PAGESIZE * pageNum - 1;
178             int start = end - DEFAULT_PAGESIZE;
179             if (pageNum == 1) {
180                 start++;
181             } else {
182                 includeDataStore = false;
183             }
184             range = Range.closed(start, end);
185         }
186
187         final var schema = openApiGenerator.createSchemaFromUriInfo(uriInfo);
188         final var host = openApiGenerator.createHostFromUriInfo(uriInfo);
189         final var title = deviceName + " modules of RESTCONF";
190         final var info = new Info(API_VERSION, title);
191         final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
192
193         final var modules = getSortedModules(context);
194         final var filteredModules = filterByRange(modules, range);
195         final var paths = new HashMap<String, Path>();
196         final var schemas = new HashMap<String, Schema>();
197         for (final var module : filteredModules) {
198             LOG.debug("Working on [{},{}]...", module.getName(), module.getQNameModule().getRevision().orElse(null));
199             schemas.putAll(openApiGenerator.getSchemas(module, context, definitionNames, false));
200             paths.putAll(openApiGenerator.getPaths(module, urlPrefix, deviceName, context, definitionNames, false));
201         }
202         final var components = new Components(schemas, new SecuritySchemes(OPEN_API_BASIC_AUTH));
203         if (includeDataStore) {
204             paths.putAll(getDataStoreApiPaths(urlPrefix, deviceName));
205         }
206         return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
207     }
208
209     private static String extractDeviceName(final YangInstanceIdentifier iid) {
210         return ((YangInstanceIdentifier.NodeIdentifierWithPredicates.Singleton)iid.getLastPathArgument())
211                 .values().getElement().toString();
212     }
213
214     private OpenApiObject generateDataStoreOpenApi(final UriInfo uriInfo, final String context,
215             final String deviceName) {
216         final var info = new Info(API_VERSION, context);
217         final var schema = openApiGenerator.createSchemaFromUriInfo(uriInfo);
218         final var host = openApiGenerator.createHostFromUriInfo(uriInfo);
219         final var servers = List.of(new Server(schema + "://" + host + BASE_PATH));
220         final var components = new Components(new HashMap<>(), new SecuritySchemes(OPEN_API_BASIC_AUTH));
221         final var paths = getDataStoreApiPaths(context, deviceName);
222         return new OpenApiObject(OPEN_API_VERSION, info, servers, paths, components, SECURITY);
223     }
224
225     private Map<String, Path> getDataStoreApiPaths(final String context, final String deviceName) {
226         final var dataBuilder = new Path.Builder();
227         dataBuilder.get(createGetPathItem("data",
228                 "Queries the config (startup) datastore on the mounted hosted.", deviceName));
229
230         final var operationsBuilder = new Path.Builder();
231         operationsBuilder.get(createGetPathItem("operations",
232                 "Queries the available operations (RPC calls) on the mounted hosted.", deviceName));
233
234         return Map.of(openApiGenerator.getResourcePath("data", context), dataBuilder.build(),
235             openApiGenerator.getResourcePath("operations", context), operationsBuilder.build());
236     }
237
238     private static Operation createGetPathItem(final String resourceType, final String description,
239             final String deviceName) {
240         final String summary = HttpMethod.GET + SUMMARY_SEPARATOR + deviceName + SUMMARY_SEPARATOR + resourceType;
241         final List<String> tags = List.of(deviceName + " GET root");
242         final ObjectNode okResponse = JsonNodeFactory.instance.objectNode();
243         okResponse.put(DESCRIPTION_KEY, Response.Status.OK.getReasonPhrase());
244         final ObjectNode responses = JsonNodeFactory.instance.objectNode();
245         responses.set(String.valueOf(Response.Status.OK.getStatusCode()), okResponse);
246         return new Operation.Builder()
247             .tags(tags)
248             .responses(responses)
249             .description(description)
250             .summary(summary)
251             .build();
252     }
253
254     @Override
255     public void onMountPointCreated(final YangInstanceIdentifier path) {
256         synchronized (lock) {
257             LOG.debug("Mount point {} created", path);
258             final Long idLong = idKey.incrementAndGet();
259             instanceIdToLongId.put(path, idLong);
260             longIdToInstanceId.put(idLong, path);
261         }
262     }
263
264     @Override
265     public void onMountPointRemoved(final YangInstanceIdentifier path) {
266         synchronized (lock) {
267             LOG.debug("Mount point {} removed", path);
268             final Long id = instanceIdToLongId.remove(path);
269             longIdToInstanceId.remove(id);
270         }
271     }
272 }