Bump MRI upstreams
[netconf.git] / restconf / restconf-nb-bierman02 / src / main / java / org / opendaylight / netconf / sal / restconf / impl / ControllerContext.java
1 /*
2  * Copyright (c) 2014 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.netconf.sal.restconf.impl;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.base.Splitter;
15 import com.google.common.base.Strings;
16 import com.google.common.collect.ImmutableMap;
17 import com.google.common.collect.Iterables;
18 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
19 import java.io.Closeable;
20 import java.io.UnsupportedEncodingException;
21 import java.net.URLDecoder;
22 import java.net.URLEncoder;
23 import java.nio.charset.StandardCharsets;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.concurrent.atomic.AtomicReference;
34 import javax.annotation.PreDestroy;
35 import javax.inject.Inject;
36 import javax.inject.Singleton;
37 import javax.ws.rs.core.Response.Status;
38 import org.apache.aries.blueprint.annotation.service.Reference;
39 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
40 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
41 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
42 import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
43 import org.opendaylight.netconf.sal.rest.api.Draft02;
44 import org.opendaylight.netconf.sal.rest.api.Draft02.RestConfModule;
45 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
46 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
47 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
48 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
49 import org.opendaylight.restconf.common.util.RestUtil;
50 import org.opendaylight.yangtools.concepts.IllegalArgumentCodec;
51 import org.opendaylight.yangtools.concepts.ListenerRegistration;
52 import org.opendaylight.yangtools.yang.common.QName;
53 import org.opendaylight.yangtools.yang.common.Revision;
54 import org.opendaylight.yangtools.yang.common.XMLNamespace;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
57 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.InstanceIdentifierBuilder;
58 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
59 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
60 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
61 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
63 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
65 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
66 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
67 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
68 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextListener;
69 import org.opendaylight.yangtools.yang.model.api.GroupingDefinition;
70 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
71 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
72 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
73 import org.opendaylight.yangtools.yang.model.api.Module;
74 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
75 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
78 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
79 import org.slf4j.Logger;
80 import org.slf4j.LoggerFactory;
81
82 @Singleton
83 public final class ControllerContext implements EffectiveModelContextListener, Closeable {
84     // FIXME: this should be in md-sal somewhere
85     public static final String MOUNT = "yang-ext:mount";
86
87     private static final Logger LOG = LoggerFactory.getLogger(ControllerContext.class);
88
89     private static final String NULL_VALUE = "null";
90
91     private static final String MOUNT_MODULE = "yang-ext";
92
93     private static final String MOUNT_NODE = "mount";
94
95     private static final Splitter SLASH_SPLITTER = Splitter.on('/');
96
97     private final AtomicReference<Map<QName, RpcDefinition>> qnameToRpc = new AtomicReference<>(Collections.emptyMap());
98
99     private final DOMMountPointService mountService;
100     private final DOMYangTextSourceProvider yangTextSourceProvider;
101     private final ListenerRegistration<?> listenerRegistration;
102     private volatile EffectiveModelContext globalSchema;
103     private volatile DataNormalizer dataNormalizer;
104
105     @Inject
106     public ControllerContext(final @Reference DOMSchemaService schemaService,
107             final @Reference DOMMountPointService mountService, final @Reference DOMSchemaService domSchemaService) {
108         this.mountService = mountService;
109         this.yangTextSourceProvider = domSchemaService.getExtensions().getInstance(DOMYangTextSourceProvider.class);
110
111         onModelContextUpdated(schemaService.getGlobalContext());
112         listenerRegistration = schemaService.registerSchemaContextListener(this);
113     }
114
115     /**
116      * Factory method.
117      *
118      * @deprecated Just use the
119      *             {@link #ControllerContext(DOMSchemaService, DOMMountPointService, DOMSchemaService)}
120      *             constructor instead.
121      */
122     @Deprecated
123     public static ControllerContext newInstance(final DOMSchemaService schemaService,
124             final DOMMountPointService mountService, final DOMSchemaService domSchemaService) {
125         return new ControllerContext(schemaService, mountService, domSchemaService);
126     }
127
128     private void setGlobalSchema(final EffectiveModelContext globalSchema) {
129         this.globalSchema = globalSchema;
130         this.dataNormalizer = new DataNormalizer(globalSchema);
131     }
132
133     public DOMYangTextSourceProvider getYangTextSourceProvider() {
134         return yangTextSourceProvider;
135     }
136
137     private void checkPreconditions() {
138         if (this.globalSchema == null) {
139             throw new RestconfDocumentedException(Status.SERVICE_UNAVAILABLE);
140         }
141     }
142
143     @Override
144     @PreDestroy
145     public void close() {
146         listenerRegistration.close();
147     }
148
149     public void setSchemas(final EffectiveModelContext schemas) {
150         onModelContextUpdated(schemas);
151     }
152
153     public InstanceIdentifierContext<?> toInstanceIdentifier(final String restconfInstance) {
154         return toIdentifier(restconfInstance, false);
155     }
156
157     public EffectiveModelContext getGlobalSchema() {
158         return this.globalSchema;
159     }
160
161     public InstanceIdentifierContext<?> toMountPointIdentifier(final String restconfInstance) {
162         return toIdentifier(restconfInstance, true);
163     }
164
165     private InstanceIdentifierContext<?> toIdentifier(final String restconfInstance,
166                                                       final boolean toMountPointIdentifier) {
167         checkPreconditions();
168
169         if (restconfInstance == null) {
170             return new InstanceIdentifierContext<>(YangInstanceIdentifier.empty(), this.globalSchema, null,
171                     this.globalSchema);
172         }
173
174         final List<String> pathArgs = urlPathArgsDecode(SLASH_SPLITTER.split(restconfInstance));
175         omitFirstAndLastEmptyString(pathArgs);
176         if (pathArgs.isEmpty()) {
177             return null;
178         }
179
180         final String first = pathArgs.iterator().next();
181         final String startModule = toModuleName(first);
182         if (startModule == null) {
183             throw new RestconfDocumentedException("First node in URI has to be in format \"moduleName:nodeName\"",
184                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
185         }
186
187         final InstanceIdentifierBuilder builder = YangInstanceIdentifier.builder();
188         final Collection<? extends Module> latestModule = this.globalSchema.findModules(startModule);
189
190         if (latestModule.isEmpty()) {
191             throw new RestconfDocumentedException("The module named '" + startModule + "' does not exist.",
192                     ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
193         }
194
195         final InstanceIdentifierContext<?> iiWithSchemaNode =
196                 collectPathArguments(builder, pathArgs, latestModule.iterator().next(), null, toMountPointIdentifier);
197
198         if (iiWithSchemaNode == null) {
199             throw new RestconfDocumentedException("URI has bad format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
200         }
201
202         return iiWithSchemaNode;
203     }
204
205     private static List<String> omitFirstAndLastEmptyString(final List<String> list) {
206         if (list.isEmpty()) {
207             return list;
208         }
209
210         final String head = list.iterator().next();
211         if (head.isEmpty()) {
212             list.remove(0);
213         }
214
215         if (list.isEmpty()) {
216             return list;
217         }
218
219         final String last = list.get(list.size() - 1);
220         if (last.isEmpty()) {
221             list.remove(list.size() - 1);
222         }
223
224         return list;
225     }
226
227     public Module findModuleByName(final String moduleName) {
228         checkPreconditions();
229         checkArgument(moduleName != null && !moduleName.isEmpty());
230         return this.globalSchema.findModules(moduleName).stream().findFirst().orElse(null);
231     }
232
233     public static Module findModuleByName(final DOMMountPoint mountPoint, final String moduleName) {
234         checkArgument(moduleName != null && mountPoint != null);
235
236         final EffectiveModelContext mountPointSchema = getModelContext(mountPoint);
237         return mountPointSchema == null ? null
238             : mountPointSchema.findModules(moduleName).stream().findFirst().orElse(null);
239     }
240
241     public Module findModuleByNamespace(final XMLNamespace namespace) {
242         checkPreconditions();
243         checkArgument(namespace != null);
244         return this.globalSchema.findModules(namespace).stream().findFirst().orElse(null);
245     }
246
247     public static Module findModuleByNamespace(final DOMMountPoint mountPoint, final XMLNamespace namespace) {
248         checkArgument(namespace != null && mountPoint != null);
249
250         final EffectiveModelContext mountPointSchema = getModelContext(mountPoint);
251         return mountPointSchema == null ? null
252             : mountPointSchema.findModules(namespace).stream().findFirst().orElse(null);
253     }
254
255     public Module findModuleByNameAndRevision(final String name, final Revision revision) {
256         checkPreconditions();
257         checkArgument(name != null && revision != null);
258
259         return this.globalSchema.findModule(name, revision).orElse(null);
260     }
261
262     public Module findModuleByNameAndRevision(final DOMMountPoint mountPoint, final String name,
263             final Revision revision) {
264         checkPreconditions();
265         checkArgument(name != null && revision != null && mountPoint != null);
266
267         final EffectiveModelContext schemaContext = getModelContext(mountPoint);
268         return schemaContext == null ? null : schemaContext.findModule(name, revision).orElse(null);
269     }
270
271     public DataNodeContainer getDataNodeContainerFor(final YangInstanceIdentifier path) {
272         checkPreconditions();
273
274         final Iterable<PathArgument> elements = path.getPathArguments();
275         final PathArgument head = elements.iterator().next();
276         final QName startQName = head.getNodeType();
277         final Module initialModule = this.globalSchema.findModule(startQName.getModule()).orElse(null);
278         DataNodeContainer node = initialModule;
279         for (final PathArgument element : elements) {
280             final QName _nodeType = element.getNodeType();
281             final DataSchemaNode potentialNode = childByQName(node, _nodeType);
282             if (potentialNode == null || !isListOrContainer(potentialNode)) {
283                 return null;
284             }
285             node = (DataNodeContainer) potentialNode;
286         }
287
288         return node;
289     }
290
291     public String toFullRestconfIdentifier(final YangInstanceIdentifier path, final DOMMountPoint mount) {
292         checkPreconditions();
293
294         final Iterable<PathArgument> elements = path.getPathArguments();
295         final StringBuilder builder = new StringBuilder();
296         final PathArgument head = elements.iterator().next();
297         final QName startQName = head.getNodeType();
298         final EffectiveModelContext schemaContext;
299         if (mount != null) {
300             schemaContext = getModelContext(mount);
301         } else {
302             schemaContext = this.globalSchema;
303         }
304         final Module initialModule = schemaContext.findModule(startQName.getModule()).orElse(null);
305         DataNodeContainer node = initialModule;
306         for (final PathArgument element : elements) {
307             if (!(element instanceof AugmentationIdentifier)) {
308                 final QName _nodeType = element.getNodeType();
309                 final DataSchemaNode potentialNode = childByQName(node, _nodeType);
310                 if ((!(element instanceof NodeIdentifier) || !(potentialNode instanceof ListSchemaNode))
311                         && !(potentialNode instanceof ChoiceSchemaNode)) {
312                     builder.append(convertToRestconfIdentifier(element, potentialNode, mount));
313                     if (potentialNode instanceof DataNodeContainer) {
314                         node = (DataNodeContainer) potentialNode;
315                     }
316                 }
317             }
318         }
319
320         return builder.toString();
321     }
322
323     public String findModuleNameByNamespace(final XMLNamespace namespace) {
324         checkPreconditions();
325
326         final Module module = this.findModuleByNamespace(namespace);
327         return module == null ? null : module.getName();
328     }
329
330     public String findModuleNameByNamespace(final DOMMountPoint mountPoint, final XMLNamespace namespace) {
331         final Module module = this.findModuleByNamespace(mountPoint, namespace);
332         return module == null ? null : module.getName();
333     }
334
335     public XMLNamespace findNamespaceByModuleName(final String moduleName) {
336         final Module module = this.findModuleByName(moduleName);
337         return module == null ? null : module.getNamespace();
338     }
339
340     public XMLNamespace findNamespaceByModuleName(final DOMMountPoint mountPoint, final String moduleName) {
341         final Module module = this.findModuleByName(mountPoint, moduleName);
342         return module == null ? null : module.getNamespace();
343     }
344
345     public Collection<? extends Module> getAllModules(final DOMMountPoint mountPoint) {
346         checkPreconditions();
347
348         final EffectiveModelContext schemaContext = mountPoint == null ? null : getModelContext(mountPoint);
349         return schemaContext == null ? null : schemaContext.getModules();
350     }
351
352     public Collection<? extends Module> getAllModules() {
353         checkPreconditions();
354         return this.globalSchema.getModules();
355     }
356
357     private static String toRestconfIdentifier(final EffectiveModelContext context, final QName qname) {
358         final Module schema = context.findModule(qname.getModule()).orElse(null);
359         return schema == null ? null : schema.getName() + ':' + qname.getLocalName();
360     }
361
362     public String toRestconfIdentifier(final QName qname, final DOMMountPoint mountPoint) {
363         return mountPoint != null ? toRestconfIdentifier(getModelContext(mountPoint), qname)
364             : toRestconfIdentifier(qname);
365     }
366
367     public String toRestconfIdentifier(final QName qname) {
368         checkPreconditions();
369
370         return toRestconfIdentifier(this.globalSchema, qname);
371     }
372
373     public static String toRestconfIdentifier(final DOMMountPoint mountPoint, final QName qname) {
374         return mountPoint == null ? null : toRestconfIdentifier(getModelContext(mountPoint), qname);
375     }
376
377     public Module getRestconfModule() {
378         return findModuleByNameAndRevision(Draft02.RestConfModule.NAME, Revision.of(Draft02.RestConfModule.REVISION));
379     }
380
381     public DataSchemaNode getRestconfModuleErrorsSchemaNode() {
382         final Module restconfModule = getRestconfModule();
383         if (restconfModule == null) {
384             return null;
385         }
386
387         final Collection<? extends GroupingDefinition> groupings = restconfModule.getGroupings();
388
389         final Iterable<? extends GroupingDefinition> filteredGroups = Iterables.filter(groupings,
390             g -> RestConfModule.ERRORS_GROUPING_SCHEMA_NODE.equals(g.getQName().getLocalName()));
391
392         final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null);
393
394         final List<DataSchemaNode> instanceDataChildrenByName = findInstanceDataChildrenByName(restconfGrouping,
395                 RestConfModule.ERRORS_CONTAINER_SCHEMA_NODE);
396         return Iterables.getFirst(instanceDataChildrenByName, null);
397     }
398
399     public DataSchemaNode getRestconfModuleRestConfSchemaNode(final Module inRestconfModule,
400             final String schemaNodeName) {
401         Module restconfModule = inRestconfModule;
402         if (restconfModule == null) {
403             restconfModule = getRestconfModule();
404         }
405
406         if (restconfModule == null) {
407             return null;
408         }
409
410         final Collection<? extends GroupingDefinition> groupings = restconfModule.getGroupings();
411         final Iterable<? extends GroupingDefinition> filteredGroups = Iterables.filter(groupings,
412             g -> RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE.equals(g.getQName().getLocalName()));
413         final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null);
414
415         final List<DataSchemaNode> instanceDataChildrenByName = findInstanceDataChildrenByName(restconfGrouping,
416                 RestConfModule.RESTCONF_CONTAINER_SCHEMA_NODE);
417         final DataSchemaNode restconfContainer = Iterables.getFirst(instanceDataChildrenByName, null);
418
419         if (RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) {
420             final List<DataSchemaNode> instances = findInstanceDataChildrenByName(
421                     (DataNodeContainer) restconfContainer, RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE);
422             return Iterables.getFirst(instances, null);
423         } else if (RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) {
424             final List<DataSchemaNode> instances = findInstanceDataChildrenByName(
425                     (DataNodeContainer) restconfContainer, RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
426             return Iterables.getFirst(instances, null);
427         } else if (RestConfModule.STREAM_LIST_SCHEMA_NODE.equals(schemaNodeName)) {
428             List<DataSchemaNode> instances = findInstanceDataChildrenByName(
429                     (DataNodeContainer) restconfContainer, RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
430             final DataSchemaNode modules = Iterables.getFirst(instances, null);
431             instances = findInstanceDataChildrenByName((DataNodeContainer) modules,
432                     RestConfModule.STREAM_LIST_SCHEMA_NODE);
433             return Iterables.getFirst(instances, null);
434         } else if (RestConfModule.MODULES_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) {
435             final List<DataSchemaNode> instances = findInstanceDataChildrenByName(
436                     (DataNodeContainer) restconfContainer, RestConfModule.MODULES_CONTAINER_SCHEMA_NODE);
437             return Iterables.getFirst(instances, null);
438         } else if (RestConfModule.MODULE_LIST_SCHEMA_NODE.equals(schemaNodeName)) {
439             List<DataSchemaNode> instances = findInstanceDataChildrenByName(
440                     (DataNodeContainer) restconfContainer, RestConfModule.MODULES_CONTAINER_SCHEMA_NODE);
441             final DataSchemaNode modules = Iterables.getFirst(instances, null);
442             instances = findInstanceDataChildrenByName((DataNodeContainer) modules,
443                     RestConfModule.MODULE_LIST_SCHEMA_NODE);
444             return Iterables.getFirst(instances, null);
445         } else if (RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) {
446             final List<DataSchemaNode> instances = findInstanceDataChildrenByName(
447                     (DataNodeContainer) restconfContainer, RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
448             return Iterables.getFirst(instances, null);
449         }
450
451         return null;
452     }
453
454     private static DataSchemaNode childByQName(final ChoiceSchemaNode container, final QName name) {
455         for (final CaseSchemaNode caze : container.getCases()) {
456             final DataSchemaNode ret = childByQName(caze, name);
457             if (ret != null) {
458                 return ret;
459             }
460         }
461
462         return null;
463     }
464
465     private static DataSchemaNode childByQName(final CaseSchemaNode container, final QName name) {
466         return container.getDataChildByName(name);
467     }
468
469     private static DataSchemaNode childByQName(final ContainerSchemaNode container, final QName name) {
470         return dataNodeChildByQName(container, name);
471     }
472
473     private static DataSchemaNode childByQName(final ListSchemaNode container, final QName name) {
474         return dataNodeChildByQName(container, name);
475     }
476
477     private static DataSchemaNode childByQName(final Module container, final QName name) {
478         return dataNodeChildByQName(container, name);
479     }
480
481     private static DataSchemaNode childByQName(final DataSchemaNode container, final QName name) {
482         return null;
483     }
484
485
486     private static DataSchemaNode childByQName(final Object container, final QName name) {
487         if (container instanceof CaseSchemaNode) {
488             return childByQName((CaseSchemaNode) container, name);
489         } else if (container instanceof ChoiceSchemaNode) {
490             return childByQName((ChoiceSchemaNode) container, name);
491         } else if (container instanceof ContainerSchemaNode) {
492             return childByQName((ContainerSchemaNode) container, name);
493         } else if (container instanceof ListSchemaNode) {
494             return childByQName((ListSchemaNode) container, name);
495         } else if (container instanceof DataSchemaNode) {
496             return childByQName((DataSchemaNode) container, name);
497         } else if (container instanceof Module) {
498             return childByQName((Module) container, name);
499         } else {
500             throw new IllegalArgumentException("Unhandled parameter types: "
501                     + Arrays.asList(container, name).toString());
502         }
503     }
504
505     private static DataSchemaNode dataNodeChildByQName(final DataNodeContainer container, final QName name) {
506         final DataSchemaNode ret = container.getDataChildByName(name);
507         if (ret == null) {
508             for (final DataSchemaNode node : container.getChildNodes()) {
509                 if (node instanceof ChoiceSchemaNode) {
510                     final ChoiceSchemaNode choiceNode = (ChoiceSchemaNode) node;
511                     final DataSchemaNode childByQName = childByQName(choiceNode, name);
512                     if (childByQName != null) {
513                         return childByQName;
514                     }
515                 }
516             }
517         }
518         return ret;
519     }
520
521     private String toUriString(final Object object, final LeafSchemaNode leafNode, final DOMMountPoint mount)
522             throws UnsupportedEncodingException {
523         final IllegalArgumentCodec<Object, Object> codec = RestCodec.from(leafNode.getType(), mount, this);
524         return object == null ? "" : URLEncoder.encode(codec.serialize(object).toString(), StandardCharsets.UTF_8);
525     }
526
527     @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Unrecognised NullableDecl")
528     private InstanceIdentifierContext<?> collectPathArguments(final InstanceIdentifierBuilder builder,
529             final List<String> strings, final DataNodeContainer parentNode, final DOMMountPoint mountPoint,
530             final boolean returnJustMountPoint) {
531         requireNonNull(strings);
532
533         if (parentNode == null) {
534             return null;
535         }
536
537         if (strings.isEmpty()) {
538             return createContext(builder.build(), (DataSchemaNode) parentNode, mountPoint,
539                 mountPoint != null ? getModelContext(mountPoint) : this.globalSchema);
540         }
541
542         final String head = strings.iterator().next();
543
544         if (head.isEmpty()) {
545             final List<String> remaining = strings.subList(1, strings.size());
546             return collectPathArguments(builder, remaining, parentNode, mountPoint, returnJustMountPoint);
547         }
548
549         final String nodeName = toNodeName(head);
550         final String moduleName = toModuleName(head);
551
552         DataSchemaNode targetNode = null;
553         if (!Strings.isNullOrEmpty(moduleName)) {
554             if (MOUNT_MODULE.equals(moduleName) && MOUNT_NODE.equals(nodeName)) {
555                 if (mountPoint != null) {
556                     throw new RestconfDocumentedException("Restconf supports just one mount point in URI.",
557                             ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED);
558                 }
559
560                 if (this.mountService == null) {
561                     throw new RestconfDocumentedException(
562                             "MountService was not found. Finding behind mount points does not work.",
563                             ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED);
564                 }
565
566                 final YangInstanceIdentifier partialPath = this.dataNormalizer.toNormalized(builder.build());
567                 final Optional<DOMMountPoint> mountOpt = this.mountService.getMountPoint(partialPath);
568                 if (mountOpt.isEmpty()) {
569                     LOG.debug("Instance identifier to missing mount point: {}", partialPath);
570                     throw new RestconfDocumentedException("Mount point does not exist.", ErrorType.PROTOCOL,
571                             ErrorTag.DATA_MISSING);
572                 }
573                 final DOMMountPoint mount = mountOpt.get();
574
575                 final EffectiveModelContext mountPointSchema = getModelContext(mount);
576                 if (mountPointSchema == null) {
577                     throw new RestconfDocumentedException("Mount point does not contain any schema with modules.",
578                             ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT);
579                 }
580
581                 if (returnJustMountPoint || strings.size() == 1) {
582                     return new InstanceIdentifierContext<>(YangInstanceIdentifier.empty(), mountPointSchema, mount,
583                         mountPointSchema);
584                 }
585
586                 final String moduleNameBehindMountPoint = toModuleName(strings.get(1));
587                 if (moduleNameBehindMountPoint == null) {
588                     throw new RestconfDocumentedException(
589                             "First node after mount point in URI has to be in format \"moduleName:nodeName\"",
590                             ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
591                 }
592
593                 final Iterator<? extends Module> it = mountPointSchema.findModules(moduleNameBehindMountPoint)
594                         .iterator();
595                 if (!it.hasNext()) {
596                     throw new RestconfDocumentedException("\"" + moduleNameBehindMountPoint
597                             + "\" module does not exist in mount point.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
598                 }
599
600                 final List<String> subList = strings.subList(1, strings.size());
601                 return collectPathArguments(YangInstanceIdentifier.builder(), subList, it.next(), mount,
602                         returnJustMountPoint);
603             }
604
605             Module module = null;
606             if (mountPoint == null) {
607                 checkPreconditions();
608                 module = this.globalSchema.findModules(moduleName).stream().findFirst().orElse(null);
609                 if (module == null) {
610                     throw new RestconfDocumentedException("\"" + moduleName + "\" module does not exist.",
611                             ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
612                 }
613             } else {
614                 final EffectiveModelContext schemaContext = getModelContext(mountPoint);
615                 if (schemaContext != null) {
616                     module = schemaContext.findModules(moduleName).stream().findFirst().orElse(null);
617                 } else {
618                     module = null;
619                 }
620                 if (module == null) {
621                     throw new RestconfDocumentedException("\"" + moduleName
622                             + "\" module does not exist in mount point.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
623                 }
624             }
625
626             targetNode = findInstanceDataChildByNameAndNamespace(parentNode, nodeName, module.getNamespace());
627
628             if (targetNode == null && parentNode instanceof Module) {
629                 final RpcDefinition rpc;
630                 if (mountPoint == null) {
631                     rpc = getRpcDefinition(head, module.getRevision());
632                 } else {
633                     final String rpcName = toNodeName(head);
634                     rpc = getRpcDefinition(module, rpcName);
635                 }
636                 if (rpc != null) {
637                     return new InstanceIdentifierContext<>(builder.build(), rpc, mountPoint,
638                             mountPoint != null ? getModelContext(mountPoint) : this.globalSchema);
639                 }
640             }
641
642             if (targetNode == null) {
643                 throw new RestconfDocumentedException("URI has bad format. Possible reasons:\n" + " 1. \"" + head
644                         + "\" was not found in parent data node.\n" + " 2. \"" + head
645                         + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + head + "\".",
646                         ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
647             }
648         } else {
649             final List<DataSchemaNode> potentialSchemaNodes = findInstanceDataChildrenByName(parentNode, nodeName);
650             if (potentialSchemaNodes.size() > 1) {
651                 final StringBuilder strBuilder = new StringBuilder();
652                 for (final DataSchemaNode potentialNodeSchema : potentialSchemaNodes) {
653                     strBuilder.append("   ").append(potentialNodeSchema.getQName().getNamespace()).append("\n");
654                 }
655
656                 throw new RestconfDocumentedException(
657                         "URI has bad format. Node \""
658                                 + nodeName + "\" is added as augment from more than one module. "
659                                 + "Therefore the node must have module name "
660                                 + "and it has to be in format \"moduleName:nodeName\"."
661                                 + "\nThe node is added as augment from modules with namespaces:\n"
662                                 + strBuilder.toString(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
663             }
664
665             if (potentialSchemaNodes.isEmpty()) {
666                 throw new RestconfDocumentedException("\"" + nodeName + "\" in URI was not found in parent data node",
667                         ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
668             }
669
670             targetNode = potentialSchemaNodes.iterator().next();
671         }
672
673         if (!isListOrContainer(targetNode)) {
674             throw new RestconfDocumentedException("URI has bad format. Node \"" + head
675                     + "\" must be Container or List yang type.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
676         }
677
678         int consumed = 1;
679         if (targetNode instanceof ListSchemaNode) {
680             final ListSchemaNode listNode = (ListSchemaNode) targetNode;
681             final int keysSize = listNode.getKeyDefinition().size();
682             if (strings.size() - consumed < keysSize) {
683                 throw new RestconfDocumentedException("Missing key for list \"" + listNode.getQName().getLocalName()
684                         + "\".", ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
685             }
686
687             final List<String> uriKeyValues = strings.subList(consumed, consumed + keysSize);
688             final HashMap<QName, Object> keyValues = new HashMap<>();
689             int index = 0;
690             for (final QName key : listNode.getKeyDefinition()) {
691                 {
692                     final String uriKeyValue = uriKeyValues.get(index);
693                     if (uriKeyValue.equals(NULL_VALUE)) {
694                         throw new RestconfDocumentedException("URI has bad format. List \""
695                                 + listNode.getQName().getLocalName() + "\" cannot contain \"null\" value as a key.",
696                                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
697                     }
698
699                     addKeyValue(keyValues, listNode.getDataChildByName(key), uriKeyValue, mountPoint);
700                     index++;
701                 }
702             }
703
704             consumed = consumed + index;
705             builder.nodeWithKey(targetNode.getQName(), keyValues);
706         } else {
707             builder.node(targetNode.getQName());
708         }
709
710         if (targetNode instanceof DataNodeContainer) {
711             final List<String> remaining = strings.subList(consumed, strings.size());
712             return collectPathArguments(builder, remaining, (DataNodeContainer) targetNode, mountPoint,
713                     returnJustMountPoint);
714         }
715
716         return createContext(builder.build(), targetNode, mountPoint,
717             mountPoint != null ? getModelContext(mountPoint) : this.globalSchema);
718     }
719
720     private static InstanceIdentifierContext<?> createContext(final YangInstanceIdentifier instance,
721             final DataSchemaNode dataSchemaNode, final DOMMountPoint mountPoint,
722             final EffectiveModelContext schemaContext) {
723         final YangInstanceIdentifier instanceIdentifier = new DataNormalizer(schemaContext).toNormalized(instance);
724         return new InstanceIdentifierContext<>(instanceIdentifier, dataSchemaNode, mountPoint, schemaContext);
725     }
726
727     public static DataSchemaNode findInstanceDataChildByNameAndNamespace(final DataNodeContainer container,
728             final String name, final XMLNamespace namespace) {
729         requireNonNull(namespace);
730
731         final Iterable<DataSchemaNode> result = Iterables.filter(findInstanceDataChildrenByName(container, name),
732             node -> namespace.equals(node.getQName().getNamespace()));
733         return Iterables.getFirst(result, null);
734     }
735
736     public static List<DataSchemaNode> findInstanceDataChildrenByName(final DataNodeContainer container,
737             final String name) {
738         final List<DataSchemaNode> instantiatedDataNodeContainers = new ArrayList<>();
739         collectInstanceDataNodeContainers(instantiatedDataNodeContainers, requireNonNull(container),
740             requireNonNull(name));
741         return instantiatedDataNodeContainers;
742     }
743
744     private static void collectInstanceDataNodeContainers(final List<DataSchemaNode> potentialSchemaNodes,
745             final DataNodeContainer container, final String name) {
746
747         final Iterable<? extends DataSchemaNode> nodes = Iterables.filter(container.getChildNodes(),
748             node -> name.equals(node.getQName().getLocalName()));
749
750         // Can't combine this loop with the filter above because the filter is
751         // lazily-applied by Iterables.filter.
752         for (final DataSchemaNode potentialNode : nodes) {
753             if (isInstantiatedDataSchema(potentialNode)) {
754                 potentialSchemaNodes.add(potentialNode);
755             }
756         }
757
758         final Iterable<ChoiceSchemaNode> choiceNodes = Iterables.filter(container.getChildNodes(),
759             ChoiceSchemaNode.class);
760         final Iterable<Collection<? extends CaseSchemaNode>> map = Iterables.transform(choiceNodes,
761             ChoiceSchemaNode::getCases);
762         for (final CaseSchemaNode caze : Iterables.concat(map)) {
763             collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name);
764         }
765     }
766
767     public static boolean isInstantiatedDataSchema(final DataSchemaNode node) {
768         return node instanceof LeafSchemaNode || node instanceof LeafListSchemaNode
769                 || node instanceof ContainerSchemaNode || node instanceof ListSchemaNode
770                 || node instanceof AnyxmlSchemaNode;
771     }
772
773     private void addKeyValue(final HashMap<QName, Object> map, final DataSchemaNode node, final String uriValue,
774             final DOMMountPoint mountPoint) {
775         checkArgument(node instanceof LeafSchemaNode);
776
777         final EffectiveModelContext schemaContext = mountPoint == null ? globalSchema : getModelContext(mountPoint);
778         final String urlDecoded = urlPathArgDecode(requireNonNull(uriValue));
779         TypeDefinition<?> typedef = ((LeafSchemaNode) node).getType();
780         final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
781         if (baseType instanceof LeafrefTypeDefinition) {
782             typedef = SchemaInferenceStack.ofInstantiatedPath(schemaContext, node.getPath())
783                 .resolveLeafref((LeafrefTypeDefinition) baseType);
784         }
785         final IllegalArgumentCodec<Object, Object> codec = RestCodec.from(typedef, mountPoint, this);
786         Object decoded = codec.deserialize(urlDecoded);
787         String additionalInfo = "";
788         if (decoded == null) {
789             if (typedef instanceof IdentityrefTypeDefinition) {
790                 decoded = toQName(schemaContext, urlDecoded);
791                 additionalInfo =
792                         "For key which is of type identityref it should be in format module_name:identity_name.";
793             }
794         }
795
796         if (decoded == null) {
797             throw new RestconfDocumentedException(uriValue + " from URI can't be resolved. " + additionalInfo,
798                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
799         }
800
801         map.put(node.getQName(), decoded);
802     }
803
804     private static String toModuleName(final String str) {
805         final int idx = str.indexOf(':');
806         if (idx == -1) {
807             return null;
808         }
809
810         // Make sure there is only one occurrence
811         if (str.indexOf(':', idx + 1) != -1) {
812             return null;
813         }
814
815         return str.substring(0, idx);
816     }
817
818     private static String toNodeName(final String str) {
819         final int idx = str.indexOf(':');
820         if (idx == -1) {
821             return str;
822         }
823
824         // Make sure there is only one occurrence
825         if (str.indexOf(':', idx + 1) != -1) {
826             return str;
827         }
828
829         return str.substring(idx + 1);
830     }
831
832     private QName toQName(final EffectiveModelContext schemaContext, final String name,
833             final Optional<Revision> revisionDate) {
834         checkPreconditions();
835         final String module = toModuleName(name);
836         final String node = toNodeName(name);
837         final Module m = schemaContext.findModule(module, revisionDate).orElse(null);
838         return m == null ? null : QName.create(m.getQNameModule(), node);
839     }
840
841     private QName toQName(final EffectiveModelContext schemaContext, final String name) {
842         checkPreconditions();
843         final String module = toModuleName(name);
844         final String node = toNodeName(name);
845         final Collection<? extends Module> modules = schemaContext.findModules(module);
846         return modules.isEmpty() ? null : QName.create(modules.iterator().next().getQNameModule(), node);
847     }
848
849     private static boolean isListOrContainer(final DataSchemaNode node) {
850         return node instanceof ListSchemaNode || node instanceof ContainerSchemaNode;
851     }
852
853     public RpcDefinition getRpcDefinition(final String name, final Optional<Revision> revisionDate) {
854         final QName validName = toQName(this.globalSchema, name, revisionDate);
855         return validName == null ? null : this.qnameToRpc.get().get(validName);
856     }
857
858     public RpcDefinition getRpcDefinition(final String name) {
859         final QName validName = toQName(this.globalSchema, name);
860         return validName == null ? null : this.qnameToRpc.get().get(validName);
861     }
862
863     private static RpcDefinition getRpcDefinition(final Module module, final String rpcName) {
864         final QName rpcQName = QName.create(module.getQNameModule(), rpcName);
865         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
866             if (rpcQName.equals(rpcDefinition.getQName())) {
867                 return rpcDefinition;
868             }
869         }
870         return null;
871     }
872
873     @Override
874     public void onModelContextUpdated(final EffectiveModelContext context) {
875         if (context != null) {
876             final Collection<? extends RpcDefinition> defs = context.getOperations();
877             final Map<QName, RpcDefinition> newMap = new HashMap<>(defs.size());
878
879             for (final RpcDefinition operation : defs) {
880                 newMap.put(operation.getQName(), operation);
881             }
882
883             // FIXME: still not completely atomic
884             this.qnameToRpc.set(ImmutableMap.copyOf(newMap));
885             setGlobalSchema(context);
886         }
887     }
888
889     private static List<String> urlPathArgsDecode(final Iterable<String> strings) {
890         final List<String> decodedPathArgs = new ArrayList<>();
891         for (final String pathArg : strings) {
892             final String _decode = URLDecoder.decode(pathArg, StandardCharsets.UTF_8);
893             decodedPathArgs.add(_decode);
894         }
895         return decodedPathArgs;
896     }
897
898     static String urlPathArgDecode(final String pathArg) {
899         if (pathArg == null) {
900             return null;
901         }
902         return URLDecoder.decode(pathArg, StandardCharsets.UTF_8);
903     }
904
905     private CharSequence convertToRestconfIdentifier(final PathArgument argument, final DataSchemaNode node,
906             final DOMMountPoint mount) {
907         if (argument instanceof NodeIdentifier) {
908             return convertToRestconfIdentifier((NodeIdentifier) argument, mount);
909         } else if (argument instanceof NodeIdentifierWithPredicates && node instanceof ListSchemaNode) {
910             return convertToRestconfIdentifierWithPredicates((NodeIdentifierWithPredicates) argument,
911                 (ListSchemaNode) node, mount);
912         } else if (argument != null && node != null) {
913             throw new IllegalArgumentException("Conversion of generic path argument is not supported");
914         } else {
915             throw new IllegalArgumentException("Unhandled parameter types: " + Arrays.asList(argument, node));
916         }
917     }
918
919     private CharSequence convertToRestconfIdentifier(final NodeIdentifier argument, final DOMMountPoint node) {
920         return "/" + toRestconfIdentifier(argument.getNodeType(),node);
921     }
922
923     private CharSequence convertToRestconfIdentifierWithPredicates(final NodeIdentifierWithPredicates argument,
924             final ListSchemaNode node, final DOMMountPoint mount) {
925         final QName nodeType = argument.getNodeType();
926         final CharSequence nodeIdentifier = this.toRestconfIdentifier(nodeType, mount);
927
928         final StringBuilder builder = new StringBuilder().append('/').append(nodeIdentifier).append('/');
929
930         final List<QName> keyDefinition = node.getKeyDefinition();
931         boolean hasElements = false;
932         for (final QName key : keyDefinition) {
933             for (final DataSchemaNode listChild : node.getChildNodes()) {
934                 if (listChild.getQName().equals(key)) {
935                     if (!hasElements) {
936                         hasElements = true;
937                     } else {
938                         builder.append('/');
939                     }
940
941                     checkState(listChild instanceof LeafSchemaNode,
942                         "List key has to consist of leaves, not %s", listChild);
943
944                     final Object value = argument.getValue(key);
945                     try {
946                         builder.append(toUriString(value, (LeafSchemaNode)listChild, mount));
947                     } catch (final UnsupportedEncodingException e) {
948                         LOG.error("Error parsing URI: {}", value, e);
949                         return null;
950                     }
951                     break;
952                 }
953             }
954         }
955
956         return builder.toString();
957     }
958
959     public YangInstanceIdentifier toNormalized(final YangInstanceIdentifier legacy) {
960         try {
961             return this.dataNormalizer.toNormalized(legacy);
962         } catch (final NullPointerException e) {
963             throw new RestconfDocumentedException("Data normalizer isn't set. Normalization isn't possible", e);
964         }
965     }
966
967     public YangInstanceIdentifier toXpathRepresentation(final YangInstanceIdentifier instanceIdentifier) {
968         try {
969             return this.dataNormalizer.toLegacy(instanceIdentifier);
970         } catch (final NullPointerException e) {
971             throw new RestconfDocumentedException("Data normalizer isn't set. Normalization isn't possible", e);
972         } catch (final DataNormalizationException e) {
973             throw new RestconfDocumentedException("Data normalizer failed. Normalization isn't possible", e);
974         }
975     }
976
977     public boolean isNodeMixin(final YangInstanceIdentifier path) {
978         final DataNormalizationOperation<?> operation;
979         try {
980             operation = this.dataNormalizer.getOperation(path);
981         } catch (final DataNormalizationException e) {
982             throw new RestconfDocumentedException("Data normalizer failed. Normalization isn't possible", e);
983         }
984         return operation.isMixin();
985     }
986
987     private static EffectiveModelContext getModelContext(final DOMMountPoint mountPoint) {
988         return mountPoint.getService(DOMSchemaService.class)
989             .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext()))
990             .orElse(null);
991     }
992 }