Jaxen binding should use DataSchemaContextTree
[yangtools.git] / yang / yang-data-jaxen / src / main / java / org / opendaylight / yangtools / yang / data / jaxen / YangFunctionContext.java
1 /*
2  * Copyright (c) 2015 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.yangtools.yang.data.jaxen;
9
10 import com.google.common.base.Preconditions;
11 import com.google.common.base.Splitter;
12 import com.google.common.base.Verify;
13 import java.util.AbstractMap.SimpleImmutableEntry;
14 import java.util.List;
15 import java.util.Map.Entry;
16 import java.util.Optional;
17 import java.util.Set;
18 import org.eclipse.jdt.annotation.Nullable;
19 import org.jaxen.Context;
20 import org.jaxen.ContextSupport;
21 import org.jaxen.Function;
22 import org.jaxen.FunctionCallException;
23 import org.jaxen.FunctionContext;
24 import org.jaxen.UnresolvableException;
25 import org.jaxen.XPathFunctionContext;
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
29 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
30 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
31 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
33 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodes;
34 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.Module;
37 import org.opendaylight.yangtools.yang.model.api.ModuleImport;
38 import org.opendaylight.yangtools.yang.model.api.RevisionAwareXPath;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
41 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
43 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
44 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
45 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
46 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
47 import org.opendaylight.yangtools.yang.model.util.RegexUtils;
48
49 /**
50  * A {@link FunctionContext} which contains also YANG-specific functions current(), re-match(), deref(),
51  * derived-from(), derived-from-or-self(), enum-value() and bit-is-set().
52  */
53 final class YangFunctionContext implements FunctionContext {
54     private static final Splitter COLON_SPLITTER = Splitter.on(':');
55     private static final Double DOUBLE_NAN = Double.NaN;
56
57     // Core XPath functions, as per http://tools.ietf.org/html/rfc6020#section-6.4.1
58     private static final FunctionContext XPATH_FUNCTION_CONTEXT = new XPathFunctionContext(false);
59
60     // Singleton instance of reuse
61     private static final YangFunctionContext INSTANCE = new YangFunctionContext();
62
63     private YangFunctionContext() {
64     }
65
66     static YangFunctionContext getInstance() {
67         return INSTANCE;
68     }
69
70     @Override
71     public Function getFunction(final String namespaceURI, final String prefix, final String localName)
72             throws UnresolvableException {
73         if (prefix == null) {
74             switch (localName) {
75                 case "bit-is-set":
76                     return YangFunctionContext::bitIsSet;
77                 case "current":
78                     return YangFunctionContext::current;
79                 case "deref":
80                     return YangFunctionContext::deref;
81                 case "derived-from":
82                     return YangFunctionContext::derivedFrom;
83                 case "derived-from-or-self":
84                     return YangFunctionContext::derivedFromOrSelf;
85                 case "enum-value":
86                     return YangFunctionContext::enumValueFunction;
87                 case "re-match":
88                     return YangFunctionContext::reMatch;
89                 default:
90                     break;
91             }
92         }
93
94         return XPATH_FUNCTION_CONTEXT.getFunction(namespaceURI, prefix, localName);
95     }
96
97     // bit-is-set(node-set nodes, string bit-name) function as per
98     // https://tools.ietf.org/html/rfc7950#section-10.6.1
99     private static boolean bitIsSet(final Context context, final List<?> args) throws FunctionCallException {
100         if (args == null || args.size() != 1) {
101             throw new FunctionCallException("bit-is-set() takes two arguments: node-set nodes, string bit-name");
102         }
103
104         if (!(args.get(0) instanceof String)) {
105             throw new FunctionCallException("Argument bit-name of bit-is-set() function should be a String");
106         }
107
108         final String bitName = (String) args.get(0);
109
110         Verify.verify(context instanceof NormalizedNodeContext, "Unhandled context %s", context.getClass());
111
112         final NormalizedNodeContext currentNodeContext = (NormalizedNodeContext) context;
113         final TypedDataSchemaNode correspondingSchemaNode = getCorrespondingTypedSchemaNode(currentNodeContext);
114
115         final TypeDefinition<?> nodeType = correspondingSchemaNode.getType();
116         if (!(nodeType instanceof BitsTypeDefinition)) {
117             return false;
118         }
119
120         final Object nodeValue = currentNodeContext.getNode().getValue();
121         if (!(nodeValue instanceof Set)) {
122             return false;
123         }
124
125         final BitsTypeDefinition bitsType = (BitsTypeDefinition) nodeType;
126         Preconditions.checkState(containsBit(bitsType, bitName), "Bit %s does not belong to bits %s.", bitName,
127             bitsType);
128         return ((Set<?>)nodeValue).contains(bitName);
129     }
130
131     // current() function, as per http://tools.ietf.org/html/rfc6020#section-6.4.1
132     private static NormalizedNodeContext current(final Context context, final List<?> args)
133             throws FunctionCallException {
134         if (!args.isEmpty()) {
135             throw new FunctionCallException("current() takes no arguments.");
136         }
137
138         Verify.verify(context instanceof NormalizedNodeContext, "Unhandled context %s", context.getClass());
139         return (NormalizedNodeContext) context;
140     }
141
142     // deref(node-set nodes) function as per https://tools.ietf.org/html/rfc7950#section-10.3.1
143     private static NormalizedNode<?, ?> deref(final Context context, final List<?> args) throws FunctionCallException {
144         if (!args.isEmpty()) {
145             throw new FunctionCallException("deref() takes only one argument: node-set nodes.");
146         }
147
148         Verify.verify(context instanceof NormalizedNodeContext, "Unhandled context %s", context.getClass());
149         final NormalizedNodeContext currentNodeContext = (NormalizedNodeContext) context;
150         final TypedDataSchemaNode correspondingSchemaNode = getCorrespondingTypedSchemaNode(currentNodeContext);
151
152         final Object nodeValue = currentNodeContext.getNode().getValue();
153         final TypeDefinition<?> type = correspondingSchemaNode.getType();
154         if (type instanceof InstanceIdentifierTypeDefinition) {
155             return nodeValue instanceof YangInstanceIdentifier
156                     ? getNodeReferencedByInstanceIdentifier((YangInstanceIdentifier) nodeValue, currentNodeContext)
157                             : null;
158         }
159         if (type instanceof LeafrefTypeDefinition) {
160             final RevisionAwareXPath xpath = ((LeafrefTypeDefinition) type).getPathStatement();
161             return getNodeReferencedByLeafref(xpath, currentNodeContext, getSchemaContext(currentNodeContext),
162                 correspondingSchemaNode, nodeValue);
163         }
164         return null;
165     }
166
167     // derived-from(node-set nodes, string identity) function as per https://tools.ietf.org/html/rfc7950#section-10.4.1
168     private static boolean derivedFrom(final Context context, final List<?> args) throws FunctionCallException {
169         final Entry<IdentitySchemaNode, IdentitySchemaNode> ids = commonDerivedFrom("derived-from", context, args);
170         return ids != null && isAncestorOf(ids.getKey(), ids.getValue());
171     }
172
173     // derived-from-or-self(node-set nodes, string identity) function as per
174     // https://tools.ietf.org/html/rfc7950#section-10.4.2
175     private static boolean derivedFromOrSelf(final Context context, final List<?> args) throws FunctionCallException {
176         final Entry<IdentitySchemaNode, IdentitySchemaNode> ids = commonDerivedFrom("derived-from-or-self", context,
177             args);
178         return ids != null && (ids.getValue().equals(ids.getKey()) || isAncestorOf(ids.getKey(), ids.getValue()));
179     }
180
181     private static @Nullable Entry<IdentitySchemaNode, IdentitySchemaNode> commonDerivedFrom(final String functionName,
182             final Context context, final List<?> args) throws FunctionCallException {
183         if (args == null || args.size() != 1) {
184             throw new FunctionCallException(functionName + "() takes two arguments: node-set nodes, string identity");
185         }
186         if (!(args.get(0) instanceof String)) {
187             throw new FunctionCallException("Argument 'identity' of " + functionName
188                 + "() function should be a String.");
189         }
190         Verify.verify(context instanceof NormalizedNodeContext, "Unhandled context %s", context.getClass());
191
192         final NormalizedNodeContext currentNodeContext = (NormalizedNodeContext) context;
193         final TypedDataSchemaNode correspondingSchemaNode = getCorrespondingTypedSchemaNode(currentNodeContext);
194
195         final SchemaContext schemaContext = getSchemaContext(currentNodeContext);
196         return correspondingSchemaNode.getType() instanceof IdentityrefTypeDefinition
197                 && currentNodeContext.getNode().getValue() instanceof QName ? new SimpleImmutableEntry<>(
198                         getIdentitySchemaNodeFromString((String) args.get(0), schemaContext, correspondingSchemaNode),
199                         getIdentitySchemaNodeFromQName((QName) currentNodeContext.getNode().getValue(), schemaContext))
200                         : null;
201     }
202
203     // enum-value(node-set nodes) function as per https://tools.ietf.org/html/rfc7950#section-10.5.1
204     private static Object enumValueFunction(final Context context, final List<?> args) throws FunctionCallException {
205         if (!args.isEmpty()) {
206             throw new FunctionCallException("enum-value() takes one argument: node-set nodes.");
207         }
208
209         Verify.verify(context instanceof NormalizedNodeContext, "Unhandled context %s", context.getClass());
210
211         final NormalizedNodeContext currentNodeContext = (NormalizedNodeContext) context;
212         final TypedDataSchemaNode correspondingSchemaNode = getCorrespondingTypedSchemaNode(currentNodeContext);
213
214         final TypeDefinition<?> nodeType = correspondingSchemaNode.getType();
215         if (!(nodeType instanceof EnumTypeDefinition)) {
216             return DOUBLE_NAN;
217         }
218
219         final Object nodeValue = currentNodeContext.getNode().getValue();
220         if (!(nodeValue instanceof String)) {
221             return DOUBLE_NAN;
222         }
223
224         final EnumTypeDefinition enumerationType = (EnumTypeDefinition) nodeType;
225         final String enumName = (String) nodeValue;
226
227         return getEnumValue(enumerationType, enumName);
228     }
229
230     // re-match(string subject, string pattern) function as per https://tools.ietf.org/html/rfc7950#section-10.2.1
231     private static boolean reMatch(final Context context, final List<?> args) throws FunctionCallException {
232         if (args == null || args.size() != 2) {
233             throw new FunctionCallException("re-match() takes two arguments: string subject, string pattern.");
234         }
235         final Object subject = args.get(0);
236         if (!(subject instanceof String)) {
237             throw new FunctionCallException("First argument of re-match() should be a String.");
238         }
239         final Object pattern = args.get(1);
240         if (!(pattern instanceof String)) {
241             throw new FunctionCallException("Second argument of re-match() should be a String.");
242         }
243
244         return ((String) subject).matches(RegexUtils.getJavaRegexFromXSD((String) pattern));
245     }
246
247     private static boolean isAncestorOf(final IdentitySchemaNode identity, final IdentitySchemaNode descendant) {
248         for (IdentitySchemaNode base : descendant.getBaseIdentities()) {
249             if (identity.equals(base) || isAncestorOf(identity, base)) {
250                 return true;
251             }
252         }
253         return false;
254     }
255
256     private static IdentitySchemaNode getIdentitySchemaNodeFromQName(final QName identityQName,
257             final SchemaContext schemaContext) {
258         final Optional<Module> module = schemaContext.findModule(identityQName.getModule());
259         Preconditions.checkArgument(module.isPresent(), "Module for %s not found", identityQName);
260         return findIdentitySchemaNodeInModule(module.get(), identityQName);
261     }
262
263     private static IdentitySchemaNode getIdentitySchemaNodeFromString(final String identity,
264             final SchemaContext schemaContext, final TypedDataSchemaNode correspondingSchemaNode) {
265         final List<String> identityPrefixAndName = COLON_SPLITTER.splitToList(identity);
266         final Module module = schemaContext.findModule(correspondingSchemaNode.getQName().getModule()).get();
267         if (identityPrefixAndName.size() == 2) {
268             // prefix of local module
269             if (identityPrefixAndName.get(0).equals(module.getPrefix())) {
270                 return findIdentitySchemaNodeInModule(module, QName.create(module.getQNameModule(),
271                         identityPrefixAndName.get(1)));
272             }
273
274             // prefix of imported module
275             for (final ModuleImport moduleImport : module.getImports()) {
276                 if (identityPrefixAndName.get(0).equals(moduleImport.getPrefix())) {
277                     final Module importedModule = schemaContext.findModule(moduleImport.getModuleName(),
278                         moduleImport.getRevision()).get();
279                     return findIdentitySchemaNodeInModule(importedModule, QName.create(
280                         importedModule.getQNameModule(), identityPrefixAndName.get(1)));
281                 }
282             }
283
284             throw new IllegalArgumentException(String.format("Cannot resolve prefix '%s' from identity '%s'.",
285                     identityPrefixAndName.get(0), identity));
286         }
287
288         if (identityPrefixAndName.size() == 1) {
289             // without prefix
290             return findIdentitySchemaNodeInModule(module, QName.create(module.getQNameModule(),
291                     identityPrefixAndName.get(0)));
292         }
293
294         throw new IllegalArgumentException(String.format("Malformed identity argument: %s.", identity));
295     }
296
297     private static IdentitySchemaNode findIdentitySchemaNodeInModule(final Module module, final QName identityQName) {
298         for (final IdentitySchemaNode id : module.getIdentities()) {
299             if (identityQName.equals(id.getQName())) {
300                 return id;
301             }
302         }
303
304         throw new IllegalArgumentException(String.format("Identity %s does not have a corresponding"
305                     + " identity schema node in the module %s.", identityQName, module));
306     }
307
308     private static NormalizedNode<?, ?> getNodeReferencedByInstanceIdentifier(final YangInstanceIdentifier path,
309             final NormalizedNodeContext currentNodeContext) {
310         final NormalizedNodeNavigator navigator = (NormalizedNodeNavigator) currentNodeContext.getNavigator();
311         final NormalizedNode<?, ?> rootNode = navigator.getDocument().getRootNode();
312         final List<PathArgument> pathArguments = path.getPathArguments();
313         if (pathArguments.get(0).getNodeType().equals(rootNode.getNodeType())) {
314             final List<PathArgument> relPath = pathArguments.subList(1, pathArguments.size());
315             final Optional<NormalizedNode<?, ?>> possibleNode = NormalizedNodes.findNode(rootNode, relPath);
316             if (possibleNode.isPresent()) {
317                 return possibleNode.get();
318             }
319         }
320
321         return null;
322     }
323
324     private static NormalizedNode<?, ?> getNodeReferencedByLeafref(final RevisionAwareXPath xpath,
325             final NormalizedNodeContext currentNodeContext, final SchemaContext schemaContext,
326             final TypedDataSchemaNode correspondingSchemaNode, final Object nodeValue) {
327         final NormalizedNode<?, ?> referencedNode = xpath.isAbsolute() ? getNodeReferencedByAbsoluteLeafref(xpath,
328                 currentNodeContext, schemaContext, correspondingSchemaNode) : getNodeReferencedByRelativeLeafref(xpath,
329                 currentNodeContext, schemaContext, correspondingSchemaNode);
330
331         if (referencedNode instanceof LeafSetNode) {
332             return getReferencedLeafSetEntryNode((LeafSetNode<?>) referencedNode, nodeValue);
333         }
334
335         if (referencedNode instanceof LeafNode && referencedNode.getValue().equals(nodeValue)) {
336             return referencedNode;
337         }
338
339         return null;
340     }
341
342     private static NormalizedNode<?, ?> getNodeReferencedByAbsoluteLeafref(final RevisionAwareXPath xpath,
343             final NormalizedNodeContext currentNodeContext, final SchemaContext schemaContext,
344             final TypedDataSchemaNode correspondingSchemaNode) {
345         final LeafrefXPathStringParsingPathArgumentBuilder builder = new LeafrefXPathStringParsingPathArgumentBuilder(
346                 xpath.toString(), schemaContext, correspondingSchemaNode, currentNodeContext);
347         final List<PathArgument> pathArguments = builder.build();
348         final NormalizedNodeNavigator navigator = (NormalizedNodeNavigator) currentNodeContext.getNavigator();
349         final NormalizedNode<?, ?> rootNode = navigator.getDocument().getRootNode();
350         if (pathArguments.get(0).getNodeType().equals(rootNode.getNodeType())) {
351             final List<PathArgument> relPath = pathArguments.subList(1, pathArguments.size());
352             final Optional<NormalizedNode<?, ?>> possibleNode = NormalizedNodes.findNode(rootNode, relPath);
353             if (possibleNode.isPresent()) {
354                 return possibleNode.get();
355             }
356         }
357
358         return null;
359     }
360
361     private static NormalizedNode<?, ?> getNodeReferencedByRelativeLeafref(final RevisionAwareXPath xpath,
362             final NormalizedNodeContext currentNodeContext, final SchemaContext schemaContext,
363             final TypedDataSchemaNode correspondingSchemaNode) {
364         NormalizedNodeContext relativeNodeContext = currentNodeContext;
365         final StringBuilder xPathStringBuilder = new StringBuilder(xpath.toString());
366         // strip the relative path of all ../ at the beginning
367         while (xPathStringBuilder.indexOf("../") == 0) {
368             xPathStringBuilder.delete(0, 3);
369             relativeNodeContext = relativeNodeContext.getParent();
370         }
371
372         // add / to the beginning of the path so that it can be processed the same way as an absolute path
373         xPathStringBuilder.insert(0, '/');
374         final LeafrefXPathStringParsingPathArgumentBuilder builder = new LeafrefXPathStringParsingPathArgumentBuilder(
375                 xPathStringBuilder.toString(), schemaContext, correspondingSchemaNode, currentNodeContext);
376         final List<PathArgument> pathArguments = builder.build();
377         final NormalizedNode<?, ?> relativeNode = relativeNodeContext.getNode();
378         final Optional<NormalizedNode<?, ?>> possibleNode = NormalizedNodes.findNode(relativeNode, pathArguments);
379         if (possibleNode.isPresent()) {
380             return possibleNode.get();
381         }
382
383         return null;
384     }
385
386     private static LeafSetEntryNode<?> getReferencedLeafSetEntryNode(final LeafSetNode<?> referencedNode,
387             final Object currentNodeValue) {
388         for (final LeafSetEntryNode<?> entryNode : referencedNode.getValue()) {
389             if (currentNodeValue.equals(entryNode.getValue())) {
390                 return entryNode;
391             }
392         }
393
394         return null;
395     }
396
397
398     private static boolean containsBit(final BitsTypeDefinition bitsType, final String bitName) {
399         for (BitsTypeDefinition.Bit bit : bitsType.getBits()) {
400             if (bitName.equals(bit.getName())) {
401                 return true;
402             }
403         }
404
405         return false;
406     }
407
408     private static int getEnumValue(final EnumTypeDefinition enumerationType, final String enumName) {
409         for (final EnumTypeDefinition.EnumPair enumPair : enumerationType.getValues()) {
410             if (enumName.equals(enumPair.getName())) {
411                 return enumPair.getValue();
412             }
413         }
414
415         throw new IllegalStateException(String.format("Enum %s does not belong to enumeration %s.",
416                 enumName, enumerationType));
417     }
418
419     private static SchemaContext getSchemaContext(final NormalizedNodeContext normalizedNodeContext) {
420         final ContextSupport contextSupport = normalizedNodeContext.getContextSupport();
421         Verify.verify(contextSupport instanceof NormalizedNodeContextSupport, "Unhandled context support %s",
422                 contextSupport.getClass());
423         return ((NormalizedNodeContextSupport) contextSupport).getSchemaContext();
424     }
425
426     private static TypedDataSchemaNode getCorrespondingTypedSchemaNode(final NormalizedNodeContext currentNodeContext) {
427         final DataSchemaNode schemaNode = currentNodeContext.getSchema().getDataSchemaNode();
428         Preconditions.checkState(schemaNode instanceof TypedDataSchemaNode, "Node %s must be a leaf or a leaf-list.",
429                 currentNodeContext.getNode());
430         return (TypedDataSchemaNode) schemaNode;
431     }
432 }