Use OpenApiServiceImpl#DEFAULT_PAGESIZE after rewrite
[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.SECURITY;
13 import static org.opendaylight.restconf.openapi.impl.OpenApiServiceImpl.DEFAULT_PAGESIZE;
14
15 import java.io.IOException;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.Map.Entry;
19 import java.util.Set;
20 import java.util.TreeSet;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ConcurrentSkipListMap;
23 import java.util.concurrent.atomic.AtomicLong;
24 import javax.ws.rs.core.UriInfo;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.mdsal.dom.api.DOMMountPointListener;
27 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
28 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
29 import org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator;
30 import org.opendaylight.restconf.openapi.impl.OpenApiInputStream;
31 import org.opendaylight.yangtools.concepts.ListenerRegistration;
32 import org.opendaylight.yangtools.yang.common.QName;
33 import org.opendaylight.yangtools.yang.common.Revision;
34 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
35 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
36 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
39 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.Module;
41 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 public class MountPointOpenApi implements DOMMountPointListener, AutoCloseable {
46
47     private static final Logger LOG = LoggerFactory.getLogger(MountPointOpenApi.class);
48
49     private static final String DATASTORES_REVISION = "-";
50     private static final String DATASTORES_LABEL = "Datastores";
51
52     private final DOMSchemaService globalSchema;
53     private final DOMMountPointService mountService;
54     private final BaseYangOpenApiGenerator openApiGenerator;
55     private final Map<YangInstanceIdentifier, Long> instanceIdToLongId =
56             new ConcurrentSkipListMap<>((o1, o2) -> o1.toString().compareToIgnoreCase(o2.toString()));
57     private final Map<Long, YangInstanceIdentifier> longIdToInstanceId = new ConcurrentHashMap<>();
58
59     private final AtomicLong idKey = new AtomicLong(0);
60
61     private ListenerRegistration<DOMMountPointListener> registration;
62
63     public MountPointOpenApi(final DOMSchemaService globalSchema, final DOMMountPointService mountService,
64             final BaseYangOpenApiGenerator openApiGenerator) {
65         this.globalSchema = requireNonNull(globalSchema);
66         this.mountService = requireNonNull(mountService);
67         this.openApiGenerator = requireNonNull(openApiGenerator);
68     }
69
70     public void init() {
71         registration = mountService.registerProvisionListener(this);
72     }
73
74     @Override
75     public void close() {
76         if (registration != null) {
77             registration.close();
78         }
79     }
80
81     public Map<String, Long> getInstanceIdentifiers() {
82         final Map<String, Long> urlToId = new HashMap<>();
83         final SchemaContext context = globalSchema.getGlobalContext();
84         for (final Entry<YangInstanceIdentifier, Long> entry : instanceIdToLongId.entrySet()) {
85             final String modName = findModuleName(entry.getKey(), context);
86             urlToId.put(generateUrlPrefixFromInstanceID(entry.getKey(), modName), entry.getValue());
87         }
88         return urlToId;
89     }
90
91     private static String findModuleName(final YangInstanceIdentifier id, final SchemaContext context) {
92         final PathArgument rootQName = id.getPathArguments().iterator().next();
93         for (final Module mod : context.getModules()) {
94             if (mod.findDataChildByName(rootQName.getNodeType()).isPresent()) {
95                 return mod.getName();
96             }
97         }
98         return null;
99     }
100
101     private String getYangMountUrl(final YangInstanceIdentifier key) {
102         final String modName = findModuleName(key, globalSchema.getGlobalContext());
103         return generateUrlPrefixFromInstanceID(key, modName) + "yang-ext:mount";
104     }
105
106     private static String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
107         final StringBuilder builder = new StringBuilder();
108         builder.append("/");
109         if (moduleName != null) {
110             builder.append(moduleName).append(':');
111         }
112         for (final PathArgument arg : key.getPathArguments()) {
113             final String name = arg.getNodeType().getLocalName();
114             if (arg instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates nodeId) {
115                 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
116                     builder.deleteCharAt(builder.length() - 1).append("=").append(entry.getValue()).append('/');
117                 }
118             } else {
119                 builder.append(name).append('/');
120             }
121         }
122         return builder.toString();
123     }
124
125     private EffectiveModelContext getSchemaContext(final YangInstanceIdentifier id) {
126         if (id == null) {
127             return null;
128         }
129
130         checkState(mountService != null);
131         return mountService.getMountPoint(id)
132             .flatMap(mountPoint -> mountPoint.getService(DOMSchemaService.class))
133             .map(DOMSchemaService::getGlobalContext)
134             .orElse(null);
135     }
136
137     public OpenApiInputStream getMountPointApi(final UriInfo uriInfo, final Long id, final String module,
138             final String revision) throws IOException  {
139         final YangInstanceIdentifier iid = longIdToInstanceId.get(id);
140         final EffectiveModelContext context = getSchemaContext(iid);
141         final String urlPrefix = getYangMountUrl(iid);
142         final String deviceName = extractDeviceName(iid);
143
144         if (context == null) {
145             return null;
146         }
147
148         if (DATASTORES_LABEL.equals(module) && DATASTORES_REVISION.equals(revision)) {
149             return generateDataStoreOpenApi(context, uriInfo, urlPrefix, deviceName);
150         }
151         return openApiGenerator.getApiDeclaration(module, revision, uriInfo, context, urlPrefix, deviceName);
152     }
153
154     public OpenApiInputStream getMountPointApi(final UriInfo uriInfo, final Long id, final @Nullable String strPageNum)
155             throws IOException {
156         final var iid = longIdToInstanceId.get(id);
157         final var context = getSchemaContext(iid);
158         final var urlPrefix = getYangMountUrl(iid);
159         final var deviceName = extractDeviceName(iid);
160
161         if (context == null) {
162             return null;
163         }
164
165         boolean includeDataStore = true;
166         var modules = context.getModules();
167         if (strPageNum != null) {
168             final var pageNum = Integer.parseInt(strPageNum);
169             final var end = DEFAULT_PAGESIZE * pageNum - 1;
170             int start = end - DEFAULT_PAGESIZE;
171             if (pageNum == 1) {
172                 start++;
173             } else {
174                 includeDataStore = false;
175             }
176             modules = filterByRange(context, start);
177         }
178
179         final var schema = openApiGenerator.createSchemaFromUriInfo(uriInfo);
180         final var host = openApiGenerator.createHostFromUriInfo(uriInfo);
181         final var title = deviceName + " modules of RESTCONF";
182         final var url = schema + "://" + host + "/";
183         final var basePath = openApiGenerator.getBasePath();
184         return new OpenApiInputStream(context, title, url, SECURITY, deviceName, urlPrefix, false, includeDataStore,
185             modules, basePath);
186     }
187
188     private static String extractDeviceName(final YangInstanceIdentifier iid) {
189         return ((YangInstanceIdentifier.NodeIdentifierWithPredicates.Singleton)iid.getLastPathArgument())
190                 .values().getElement().toString();
191     }
192
193     private OpenApiInputStream generateDataStoreOpenApi(EffectiveModelContext modelContext,
194             final UriInfo uriInfo, final String urlPrefix, final String deviceName) throws IOException {
195         final var schema = openApiGenerator.createSchemaFromUriInfo(uriInfo);
196         final var host = openApiGenerator.createHostFromUriInfo(uriInfo);
197         final var url = schema + "://" + host + "/";
198         final var basePath = openApiGenerator.getBasePath();
199         final var modules = modelContext.getModules();
200         return new OpenApiInputStream(modelContext, urlPrefix, url, SECURITY, deviceName, urlPrefix, true, false,
201             modules, basePath);
202     }
203
204     @Override
205     public void onMountPointCreated(final YangInstanceIdentifier path) {
206         LOG.debug("Mount point {} created", path);
207         final Long idLong = idKey.incrementAndGet();
208         instanceIdToLongId.put(path, idLong);
209         longIdToInstanceId.put(idLong, path);
210     }
211
212     @Override
213     public void onMountPointRemoved(final YangInstanceIdentifier path) {
214         LOG.debug("Mount point {} removed", path);
215         final Long id = instanceIdToLongId.remove(path);
216         longIdToInstanceId.remove(id);
217     }
218
219     private static Set<Module> filterByRange(final EffectiveModelContext schemaContext, final Integer start) {
220         final var sortedModules = new TreeSet<Module>((module1, module2) -> {
221             int result = module1.getName().compareTo(module2.getName());
222             if (result == 0) {
223                 result = Revision.compare(module1.getRevision(), module2.getRevision());
224             }
225             if (result == 0) {
226                 result = module1.getNamespace().compareTo(module2.getNamespace());
227             }
228             return result;
229         });
230         sortedModules.addAll(schemaContext.getModules());
231
232         final int end = start + DEFAULT_PAGESIZE - 1;
233
234         var firstModule = sortedModules.first();
235
236         final var iterator = sortedModules.iterator();
237         int counter = 0;
238         while (iterator.hasNext() && counter < end) {
239             final var module = iterator.next();
240             if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
241                 if (counter == start) {
242                     firstModule = module;
243                 }
244                 counter++;
245             }
246         }
247
248         return iterator.hasNext()
249             ? sortedModules.subSet(firstModule, iterator.next()) : sortedModules.tailSet(firstModule);
250     }
251
252     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
253         for (final var child : nodes) {
254             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
255                 return true;
256             }
257         }
258         return false;
259     }
260 }