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