BUG-1275: optimize InstanceIdentifierForXmlCodec
[yangtools.git] / yang / yang-data-impl / src / main / java / org / opendaylight / yangtools / yang / data / impl / codec / InstanceIdentifierForXmlCodec.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.data.impl.codec;
9
10 import com.google.common.base.Preconditions;
11 import com.google.common.base.Splitter;
12
13 import java.net.URI;
14 import java.net.URISyntaxException;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.ThreadLocalRandom;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifierWithPredicates;
28 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeWithValue;
29 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument;
30 import org.opendaylight.yangtools.yang.model.api.Module;
31 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
32 import org.w3c.dom.Element;
33
34 public class InstanceIdentifierForXmlCodec {
35     private static final Pattern PREDICATE_PATTERN = Pattern.compile("\\[(.*?)\\]");
36     private static final Splitter SLASH_SPLITTER = Splitter.on('/');
37     private static final Splitter COLON_SPLITTER = Splitter.on(':');
38     private static final String SQUOTE = "'";
39     private static final String DQUOTE = "\"";
40
41     public static final InstanceIdentifierForXmlCodec INSTANCE_IDENTIFIER_FOR_XML_CODEC = new InstanceIdentifierForXmlCodec();
42
43     protected InstanceIdentifierForXmlCodec() {
44     }
45
46     public InstanceIdentifier deserialize(final Element element, final SchemaContext schemaContext) {
47         Preconditions.checkNotNull(element, "Value of element for deserialization can't be null");
48         Preconditions.checkNotNull(schemaContext,
49                 "Schema context for deserialization of instance identifier type can't be null");
50
51         final String valueTrimmed = element.getTextContent().trim();
52         final Iterator<String> xPathParts = SLASH_SPLITTER.split(valueTrimmed).iterator();
53
54         // must be at least "/pr:node"
55         if (!xPathParts.hasNext() || !xPathParts.next().isEmpty() || !xPathParts.hasNext()) {
56             return null;
57         }
58
59         List<PathArgument> result = new ArrayList<>();
60         while (xPathParts.hasNext()) {
61             String xPathPartTrimmed = xPathParts.next().trim();
62
63             PathArgument pathArgument = toPathArgument(xPathPartTrimmed, element, schemaContext);
64             if (pathArgument != null) {
65                 result.add(pathArgument);
66             }
67         }
68         return InstanceIdentifier.create(result);
69     }
70
71     public Element serialize(final InstanceIdentifier data, final Element element) {
72         Preconditions.checkNotNull(data, "Variable should contain instance of instance identifier and can't be null");
73         Preconditions.checkNotNull(element, "DOM element can't be null");
74         Map<String, String> prefixes = new HashMap<>();
75         StringBuilder textContent = new StringBuilder();
76         for (PathArgument pathArgument : data.getPathArguments()) {
77             textContent.append('/');
78             writeIdentifierWithNamespacePrefix(element, textContent, pathArgument.getNodeType(), prefixes);
79             if (pathArgument instanceof NodeIdentifierWithPredicates) {
80                 Map<QName, Object> predicates = ((NodeIdentifierWithPredicates) pathArgument).getKeyValues();
81
82                 for (QName keyValue : predicates.keySet()) {
83                     String predicateValue = String.valueOf(predicates.get(keyValue));
84                     textContent.append('[');
85                     writeIdentifierWithNamespacePrefix(element, textContent, keyValue, prefixes);
86                     textContent.append("='");
87                     textContent.append(predicateValue);
88                     textContent.append("']");
89                 }
90             } else if (pathArgument instanceof NodeWithValue) {
91                 textContent.append("[.='");
92                 textContent.append(((NodeWithValue) pathArgument).getValue());
93                 textContent.append("']");
94             }
95         }
96         element.setTextContent(textContent.toString());
97         return element;
98     }
99
100     private static String getIdAndPrefixAsStr(final String pathPart) {
101         int predicateStartIndex = pathPart.indexOf('[');
102         return predicateStartIndex == -1 ? pathPart : pathPart.substring(0, predicateStartIndex);
103     }
104
105     private PathArgument toPathArgument(final String xPathArgument, final Element element, final SchemaContext schemaContext) {
106         final QName mainQName = toIdentity(xPathArgument, element, schemaContext);
107
108         // predicates
109         final Matcher matcher = PREDICATE_PATTERN.matcher(xPathArgument);
110         final Map<QName, Object> predicates = new HashMap<>();
111         QName currentQName = mainQName;
112
113         while (matcher.find()) {
114             final String predicateStr = matcher.group(1).trim();
115             final int indexOfEqualityMark = predicateStr.indexOf('=');
116             if (indexOfEqualityMark != -1) {
117                 final String predicateValue = toPredicateValue(predicateStr.substring(indexOfEqualityMark + 1));
118                 if (predicateValue == null) {
119                     return null;
120                 }
121
122                 if (predicateStr.charAt(0) != '.') {
123                     // target is not a leaf-list
124                     currentQName = toIdentity(predicateStr.substring(0, indexOfEqualityMark), element, schemaContext);
125                     if (currentQName == null) {
126                         return null;
127                     }
128                 }
129                 predicates.put(currentQName, predicateValue);
130             }
131         }
132
133         if (predicates.isEmpty()) {
134             return new InstanceIdentifier.NodeIdentifier(mainQName);
135         } else {
136             return new InstanceIdentifier.NodeIdentifierWithPredicates(mainQName, predicates);
137         }
138
139     }
140
141     public QName toIdentity(final String xPathArgument, final Element element, final SchemaContext schemaContext) {
142         final String xPathPartTrimmed = getIdAndPrefixAsStr(xPathArgument).trim();
143         final Iterator<String> it = COLON_SPLITTER.split(xPathPartTrimmed).iterator();
144
145         // Empty string
146         if (!it.hasNext()) {
147             return null;
148         }
149
150         final String prefix = it.next().trim();
151         if (prefix.isEmpty()) {
152             return null;
153         }
154
155         // it is not "prefix:value"
156         if (!it.hasNext()) {
157             return null;
158         }
159
160         final String identifier = it.next().trim();
161         if (identifier.isEmpty()) {
162             return null;
163         }
164
165         URI namespace = null;
166         String namespaceStr = null;
167         try {
168             namespaceStr = element.lookupNamespaceURI(prefix);
169             namespace = new URI(namespaceStr);
170         } catch (URISyntaxException e) {
171             throw new IllegalArgumentException("It wasn't possible to convert " + namespaceStr + " to URI object.");
172         } catch (NullPointerException e) {
173             throw new IllegalArgumentException("I wasn't possible to get namespace for prefix " + prefix);
174         }
175
176         Module youngestModule = findYoungestModuleByNamespace(schemaContext, namespace);
177         return QName.create(namespace, youngestModule.getRevision(), identifier);
178     }
179
180     // FIXME: this method should be provided by SchemaContext
181     private static Module findYoungestModuleByNamespace(final SchemaContext schemaContext, final URI namespace) {
182         Module result = null;
183         for (Module module : schemaContext.findModuleByNamespace(namespace)) {
184             if (result != null) {
185                 if (module.getRevision().after(result.getRevision())) {
186                     result = module;
187                 }
188             } else {
189                 result = module;
190             }
191         }
192         return result;
193     }
194
195     private static String toPredicateValue(final String predicatedValue) {
196         String predicatedValueTrimmed = predicatedValue.trim();
197         if ((predicatedValueTrimmed.startsWith(DQUOTE) || predicatedValueTrimmed.startsWith(SQUOTE))
198                 && (predicatedValueTrimmed.endsWith(DQUOTE) || predicatedValueTrimmed.endsWith(SQUOTE))) {
199             return predicatedValueTrimmed.substring(1, predicatedValueTrimmed.length() - 1);
200         }
201         return null;
202     }
203
204     private static void writeIdentifierWithNamespacePrefix(final Element element, final StringBuilder textContent, final QName qName,
205             final Map<String, String> prefixes) {
206         String namespace = qName.getNamespace().toString();
207         String prefix = prefixes.get(namespace);
208         if (prefix == null) {
209             prefix = qName.getPrefix();
210             if (prefix == null || prefix.isEmpty() || prefixes.containsValue(prefix)) {
211                 prefix = generateNewPrefix(prefixes.values());
212             }
213         }
214
215         element.setAttribute("xmlns:" + prefix, namespace.toString());
216         textContent.append(prefix);
217         prefixes.put(namespace, prefix);
218
219         textContent.append(':');
220         textContent.append(qName.getLocalName());
221     }
222
223     private static String generateNewPrefix(final Collection<String> prefixes) {
224         String result;
225
226         final ThreadLocalRandom random = ThreadLocalRandom.current();
227         do {
228             StringBuilder sb = new StringBuilder();
229             for (int i = 0; i < 4; i++) {
230                 sb.append('a' + random.nextInt(25));
231             }
232
233             result = sb.toString();
234         } while (prefixes.contains(result));
235
236         return result;
237     }
238 }