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