bea3dfe82fe0ccc478f15be7343b7e2f9422f176
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / YangInstanceIdentifierSerializer.java
1 /*
2  * Copyright (c) 2016 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.restconf.nb.rfc8040.utils.parser;
9
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.base.CharMatcher;
12 import java.util.Iterator;
13 import java.util.Locale;
14 import java.util.Map.Entry;
15 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
16 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
17 import org.opendaylight.yangtools.concepts.Serializer;
18 import org.opendaylight.yangtools.yang.common.ErrorType;
19 import org.opendaylight.yangtools.yang.common.QName;
20 import org.opendaylight.yangtools.yang.common.QNameModule;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
25 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
26 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
27 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
28 import org.opendaylight.yangtools.yang.model.api.Module;
29 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
30
31 /**
32  * Serializer for {@link YangInstanceIdentifier} to {@link String} for restconf.
33  */
34 public final class YangInstanceIdentifierSerializer {
35     // RFC8040 specifies that reserved characters need to be percent-encoded
36     @VisibleForTesting
37     static final CharMatcher PERCENT_ENCODE_CHARS =
38             CharMatcher.anyOf(ParserConstants.RFC3986_RESERVED_CHARACTERS).precomputed();
39
40     private YangInstanceIdentifierSerializer() {
41         // Hidden on purpose
42     }
43
44     /**
45      * Method to create String from {@link Iterable} of {@link PathArgument}
46      * which are parsing from data by {@link SchemaContext}.
47      *
48      * @param schemaContext
49      *             for validate of parsing path arguments
50      * @param data
51      *             path to data
52      * @return {@link String}
53      */
54     public static String create(final EffectiveModelContext schemaContext, final YangInstanceIdentifier data) {
55         final DataSchemaContextNode<?> current = DataSchemaContextTree.from(schemaContext).getRoot();
56         final MainVarsWrapper variables = new MainVarsWrapper(current);
57         final StringBuilder path = new StringBuilder();
58
59         QNameModule parentModule = null;
60         for (final PathArgument arg : data.getPathArguments()) {
61             // get module of the parent
62             if (!variables.getCurrent().isMixin()) {
63                 parentModule = variables.getCurrent().getDataSchemaNode().getQName().getModule();
64             }
65
66             variables.setCurrent(variables.getCurrent().getChild(arg));
67             RestconfDocumentedException.throwIf(variables.getCurrent() == null, ErrorType.APPLICATION,
68                     ErrorTag.UNKNOWN_ELEMENT, "Invalid input '%s': schema for argument '%s' (after '%s') not found",
69                     data, arg, path);
70             if (variables.getCurrent().isMixin()) {
71                 continue;
72             }
73
74             // append namespace before every node which is defined in other module than its parent
75             // condition is satisfied also for the first path argument
76             if (!arg.getNodeType().getModule().equals(parentModule)) {
77                 // append slash if it is not the first path argument
78                 if (path.length() > 0) {
79                     path.append('/');
80                 }
81
82                 path.append(prefixForNamespace(arg.getNodeType(), schemaContext)).append(':');
83             } else {
84                 path.append('/');
85             }
86
87             if (arg instanceof NodeIdentifierWithPredicates) {
88                 prepareNodeWithPredicates(path, arg);
89             } else if (arg instanceof NodeWithValue) {
90                 prepareNodeWithValue(path, arg);
91             } else {
92                 appendQName(path, arg.getNodeType());
93             }
94         }
95
96         return path.toString();
97     }
98
99     private static void prepareNodeWithValue(final StringBuilder path, final PathArgument arg) {
100         path.append(arg.getNodeType().getLocalName());
101         path.append('=');
102
103         String value = String.valueOf(((NodeWithValue<String>) arg).getValue());
104         if (PERCENT_ENCODE_CHARS.matchesAnyOf(value)) {
105             value = parsePercentEncodeChars(value);
106         }
107         path.append(value);
108     }
109
110     private static void prepareNodeWithPredicates(final StringBuilder path, final PathArgument arg) {
111         path.append(arg.getNodeType().getLocalName());
112
113         final Iterator<Entry<QName, Object>> iterator = ((NodeIdentifierWithPredicates) arg).entrySet().iterator();
114         if (iterator.hasNext()) {
115             path.append('=');
116         }
117
118         while (iterator.hasNext()) {
119             String valueOf = String.valueOf(iterator.next().getValue());
120             if (PERCENT_ENCODE_CHARS.matchesAnyOf(valueOf)) {
121                 valueOf = parsePercentEncodeChars(valueOf);
122             }
123             path.append(valueOf);
124             if (iterator.hasNext()) {
125                 path.append(',');
126             }
127         }
128     }
129
130     /**
131      * Encode {@link Serializer#DISABLED_CHARS} chars to percent encoded chars.
132      *
133      * @param valueOf
134      *             string to encode
135      * @return encoded {@link String}
136      */
137     private static String parsePercentEncodeChars(final String valueOf) {
138         final StringBuilder sb = new StringBuilder();
139
140         for (int i = 0; i < valueOf.length(); ++i) {
141             final char ch = valueOf.charAt(i);
142
143             if (PERCENT_ENCODE_CHARS.matches(ch)) {
144                 final String upperCase = String.format("%x", (int) ch).toUpperCase(Locale.ROOT);
145                 sb.append('%').append(upperCase);
146             } else {
147                 sb.append(ch);
148             }
149         }
150         return sb.toString();
151     }
152
153     /**
154      * Add {@link QName} to the serialized string.
155      *
156      * @param path
157      *             {@link StringBuilder}
158      * @param qname
159      *             {@link QName} node
160      * @return {@link StringBuilder}
161      */
162     private static StringBuilder appendQName(final StringBuilder path, final QName qname) {
163         path.append(qname.getLocalName());
164         return path;
165     }
166
167     /**
168      * Create prefix of namespace from {@link QName}.
169      *
170      * @param qname
171      *             {@link QName}
172      * @return {@link String}
173      */
174     private static String prefixForNamespace(final QName qname, final SchemaContext schemaContext) {
175         final Module module = schemaContext.findModule(qname.getModule()).orElse(null);
176         return module.getName();
177     }
178
179     private static final class MainVarsWrapper {
180
181         private DataSchemaContextNode<?> current;
182
183         MainVarsWrapper(final DataSchemaContextNode<?> current) {
184             this.setCurrent(current);
185         }
186
187         public DataSchemaContextNode<?> getCurrent() {
188             return this.current;
189         }
190
191         public void setCurrent(final DataSchemaContextNode<?> current) {
192             this.current = current;
193         }
194
195     }
196 }