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