Promote SchemaSourceRepresentation
[yangtools.git] / model / yang-model-spi / src / main / java / org / opendaylight / yangtools / yang / model / spi / source / YinDomSource.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.model.spi.source;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
12 import static org.opendaylight.yangtools.yang.common.YangConstants.RFC6020_YIN_MODULE;
13 import static org.opendaylight.yangtools.yang.model.api.YangStmtMapping.MODULE;
14 import static org.opendaylight.yangtools.yang.model.api.YangStmtMapping.REVISION;
15 import static org.opendaylight.yangtools.yang.model.api.YangStmtMapping.SUBMODULE;
16
17 import com.google.common.base.MoreObjects;
18 import com.google.common.base.MoreObjects.ToStringHelper;
19 import javax.xml.transform.Source;
20 import javax.xml.transform.TransformerException;
21 import javax.xml.transform.TransformerFactory;
22 import javax.xml.transform.dom.DOMResult;
23 import javax.xml.transform.dom.DOMSource;
24 import org.eclipse.jdt.annotation.NonNull;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.common.YangConstants;
28 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.w3c.dom.Attr;
32 import org.w3c.dom.Element;
33 import org.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35
36 /**
37  * Utility {@link YinXmlSource} exposing a W3C {@link DOMSource} representation of YIN model.
38  */
39 public abstract sealed class YinDomSource implements YinXmlSource {
40     private static final Logger LOG = LoggerFactory.getLogger(YinDomSource.class);
41     private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
42     private static final QName REVISION_STMT = REVISION.getStatementName();
43     private static final String MODULE_ARG = MODULE.getArgumentDefinition().orElseThrow()
44         .argumentName().getLocalName();
45     private static final String REVISION_ARG = REVISION.getArgumentDefinition().orElseThrow()
46         .argumentName().getLocalName();
47
48     /**
49      * Create a new {@link YinDomSource} using an identifier and a source.
50      *
51      * @param identifier Schema source identifier
52      * @param source W3C DOM source
53      * @param symbolicName Source symbolic name
54      * @return A new {@link YinDomSource} instance.
55      */
56     public static @NonNull YinDomSource create(final @NonNull SourceIdentifier identifier,
57             final @NonNull DOMSource source, final @Nullable String symbolicName) {
58
59         final Node root = source.getNode().getFirstChild();
60         final String rootNs = root.getNamespaceURI();
61         if (rootNs == null) {
62             // Let whoever is using this deal with this
63             return new Simple(identifier, source, symbolicName);
64         }
65
66         final QName qname = QName.create(rootNs, root.getLocalName());
67         checkArgument(RFC6020_YIN_MODULE.equals(qname.getModule()),
68             "Root node namepsace %s does not match %s", rootNs, YangConstants.RFC6020_YIN_NAMESPACE);
69         checkArgument(MODULE.getStatementName().equals(qname)
70             || SUBMODULE.getStatementName().equals(qname), "Root element %s is not a module nor a submodule", qname);
71
72         checkArgument(root instanceof Element, "Root node %s is not an element", root);
73         final Element element = (Element)root;
74
75         final Attr nameAttr = element.getAttributeNode(MODULE_ARG);
76         checkArgument(nameAttr != null, "No %s name argument found in %s", element.getLocalName());
77
78         final NodeList revisions = element.getElementsByTagNameNS(REVISION_STMT.getNamespace().toString(),
79             REVISION_STMT.getLocalName());
80         if (revisions.getLength() == 0) {
81             // FIXME: is module name important (as that may have changed)
82             return new Simple(identifier, source, symbolicName);
83         }
84
85         final Element revisionStmt = (Element) revisions.item(0);
86         final Attr dateAttr = revisionStmt.getAttributeNode(REVISION_ARG);
87         checkArgument(dateAttr != null, "No revision statement argument found in %s", revisionStmt);
88
89         final var parsedId = new SourceIdentifier(nameAttr.getValue(), dateAttr.getValue());
90         final SourceIdentifier id;
91         if (!parsedId.equals(identifier)) {
92             LOG.debug("Changed identifier from {} to {}", identifier, parsedId);
93             id = parsedId;
94         } else {
95             id = identifier;
96         }
97
98         return new Simple(id, source, symbolicName);
99     }
100
101     /**
102      * Create a {@link YinDomSource} from a {@link YinXmlSource}. If the argument is already a
103      * YinDomSchemaSource, this method returns the same instance. The source will be translated on first access,
104      * at which point an {@link IllegalStateException} may be raised.
105      *
106      * @param xmlSchemaSource Backing schema source
107      * @return A {@link YinDomSource} instance
108      */
109     public static @NonNull YinDomSource lazyTransform(final YinXmlSource xmlSchemaSource) {
110         final var cast = castSchemaSource(xmlSchemaSource);
111         return cast != null ? cast : new Transforming(xmlSchemaSource);
112     }
113
114     /**
115      * Create a {@link YinDomSource} from a {@link YinXmlSource}. If the argument is already a
116      * YinDomSchemaSource, this method returns the same instance. The source will be translated immediately.
117      *
118      * @param xmlSchemaSource Backing schema source
119      * @return A {@link YinDomSource} instance
120      * @throws TransformerException when the provided source fails to transform
121      */
122     public static @NonNull YinDomSource transform(final YinXmlSource xmlSchemaSource)
123             throws TransformerException {
124         final var cast = castSchemaSource(xmlSchemaSource);
125         return cast != null ? cast :
126             create(xmlSchemaSource.sourceId(), transformSource(xmlSchemaSource.getSource()),
127                 xmlSchemaSource.symbolicName());
128     }
129
130     @Override
131     public final Class<YinDomSource> getType() {
132         return YinDomSource.class;
133     }
134
135     @Override
136     public abstract DOMSource getSource();
137
138     @Override
139     public final String toString() {
140         return addToStringAttributes(MoreObjects.toStringHelper(this).add("sourceId", sourceId())).toString();
141     }
142
143     /**
144      * Add subclass-specific attributes to the output {@link #toString()} output. Since
145      * subclasses are prevented from overriding {@link #toString()} for consistency
146      * reasons, they can add their specific attributes to the resulting string by attaching
147      * attributes to the supplied {@link ToStringHelper}.
148      *
149      * @param toStringHelper ToStringHelper onto the attributes can be added
150      * @return ToStringHelper supplied as input argument.
151      */
152     protected abstract ToStringHelper addToStringAttributes(ToStringHelper toStringHelper);
153
154     static @NonNull DOMSource transformSource(final Source source) throws TransformerException {
155         final DOMResult result = new DOMResult();
156         TRANSFORMER_FACTORY.newTransformer().transform(source, result);
157
158         return new DOMSource(result.getNode(), result.getSystemId());
159     }
160
161     private static @Nullable YinDomSource castSchemaSource(final YinXmlSource xmlSchemaSource) {
162         if (xmlSchemaSource instanceof YinDomSource yinDom) {
163             return yinDom;
164         }
165
166         final var source = xmlSchemaSource.getSource();
167         if (source instanceof DOMSource dom) {
168             return create(xmlSchemaSource.sourceId(), dom, xmlSchemaSource.symbolicName());
169         }
170         return null;
171     }
172
173     private static final class Simple extends YinDomSource {
174         private final @NonNull SourceIdentifier sourceId;
175         private final @NonNull DOMSource source;
176         private final String symbolicName;
177
178         Simple(final @NonNull SourceIdentifier sourceId, final @NonNull DOMSource source,
179                 final @Nullable String symbolicName) {
180             this.sourceId = requireNonNull(sourceId);
181             this.source = requireNonNull(source);
182             this.symbolicName = symbolicName;
183         }
184
185         @Override
186         public SourceIdentifier sourceId() {
187             return sourceId;
188         }
189
190         @Override
191         public DOMSource getSource() {
192             return source;
193         }
194
195         @Override
196         public String symbolicName() {
197             return symbolicName;
198         }
199
200         @Override
201         protected ToStringHelper addToStringAttributes(final ToStringHelper toStringHelper) {
202             return toStringHelper.add("source", source);
203         }
204     }
205
206     private static final class Transforming extends YinDomSource {
207         private final YinXmlSource xmlSchemaSource;
208
209         private volatile DOMSource source;
210
211         Transforming(final YinXmlSource xmlSchemaSource) {
212             this.xmlSchemaSource = requireNonNull(xmlSchemaSource);
213         }
214
215         @Override
216         public SourceIdentifier sourceId() {
217             return xmlSchemaSource.sourceId();
218         }
219
220         @Override
221         public String symbolicName() {
222             return xmlSchemaSource.symbolicName();
223         }
224
225         @Override
226         public DOMSource getSource() {
227             DOMSource ret = source;
228             if (ret == null) {
229                 synchronized (this) {
230                     ret = source;
231                     if (ret == null) {
232                         try {
233                             ret = transformSource(xmlSchemaSource.getSource());
234                         } catch (TransformerException e) {
235                             throw new IllegalStateException("Failed to transform schema source " + xmlSchemaSource, e);
236                         }
237                         source = ret;
238                     }
239                 }
240             }
241
242             return ret;
243         }
244
245         @Override
246         protected ToStringHelper addToStringAttributes(final ToStringHelper toStringHelper) {
247             return toStringHelper.add("xmlSchemaSource", xmlSchemaSource);
248         }
249     }
250 }