Clean up YangModelDependencyInfo
[yangtools.git] / parser / yang-parser-rfc7950 / src / main / java / org / opendaylight / yangtools / yang / parser / rfc7950 / repo / YangModelDependencyInfo.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.parser.rfc7950.repo;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.base.MoreObjects;
13 import com.google.common.collect.ImmutableSet;
14 import java.io.IOException;
15 import java.util.Collection;
16 import java.util.Comparator;
17 import java.util.Objects;
18 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.yang.common.Revision;
22 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
23 import org.opendaylight.yangtools.yang.common.XMLNamespace;
24 import org.opendaylight.yangtools.yang.common.YangVersion;
25 import org.opendaylight.yangtools.yang.ir.IRKeyword;
26 import org.opendaylight.yangtools.yang.ir.IRStatement;
27 import org.opendaylight.yangtools.yang.ir.YangIRSchemaSource;
28 import org.opendaylight.yangtools.yang.model.api.ModuleImport;
29 import org.opendaylight.yangtools.yang.model.api.YangStmtMapping;
30 import org.opendaylight.yangtools.yang.model.api.meta.StatementSourceReference;
31 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
32 import org.opendaylight.yangtools.yang.model.api.stmt.ImportEffectiveStatement;
33 import org.opendaylight.yangtools.yang.model.spi.source.ModuleSourceInfo;
34 import org.opendaylight.yangtools.yang.model.spi.source.SourceInfo;
35 import org.opendaylight.yangtools.yang.model.spi.source.SourceInfo.Import;
36 import org.opendaylight.yangtools.yang.model.spi.source.SourceInfo.Include;
37 import org.opendaylight.yangtools.yang.model.spi.source.SubmoduleSourceInfo;
38 import org.opendaylight.yangtools.yang.model.spi.source.YangTextSource;
39 import org.opendaylight.yangtools.yang.parser.api.YangSyntaxErrorException;
40 import org.opendaylight.yangtools.yang.parser.spi.source.ExplicitStatement;
41
42 /**
43  * Helper transfer object which holds basic and dependency information for YANG
44  * model.
45  *
46  * <p>
47  * There are two concrete implementations of this interface:
48  * <ul>
49  * <li>{@link ModuleDependencyInfo} - Dependency information for module</li>
50  * <li>{@link SubmoduleDependencyInfo} - Dependency information for submodule</li>
51  * </ul>
52  *
53  * @see ModuleDependencyInfo
54  * @see SubmoduleDependencyInfo
55  */
56 public abstract sealed class YangModelDependencyInfo {
57     /**
58      * Dependency information for a YANG module.
59      */
60     public static final class ModuleDependencyInfo extends YangModelDependencyInfo {
61         private ModuleDependencyInfo(final String name, final Revision revision,
62                 final ImmutableSet<ModuleImport> imports, final ImmutableSet<ModuleImport> includes) {
63             super(name, revision, imports, includes);
64         }
65     }
66
67     /**
68      * Dependency information for a YANG submodule, also provides name for parent module.
69      */
70     public static final class SubmoduleDependencyInfo extends YangModelDependencyInfo {
71         private final @NonNull Unqualified belongsTo;
72
73         private SubmoduleDependencyInfo(final String name, final Revision revision, final Unqualified belongsTo,
74                 final ImmutableSet<ModuleImport> imports, final ImmutableSet<ModuleImport> includes) {
75             super(name, revision, imports, includes);
76             this.belongsTo = requireNonNull(belongsTo);
77         }
78
79         /**
80          * Returns name of parent module.
81          *
82          * @return The module this info belongs to
83          */
84         public @NonNull Unqualified getParentModule() {
85             return belongsTo;
86         }
87     }
88
89     private static final String BELONGS_TO = YangStmtMapping.BELONGS_TO.getStatementName().getLocalName();
90     private static final String IMPORT = YangStmtMapping.IMPORT.getStatementName().getLocalName();
91     private static final String INCLUDE = YangStmtMapping.INCLUDE.getStatementName().getLocalName();
92     private static final String MODULE = YangStmtMapping.MODULE.getStatementName().getLocalName();
93     private static final String NAMESPACE = YangStmtMapping.NAMESPACE.getStatementName().getLocalName();
94     private static final String PREFIX = YangStmtMapping.PREFIX.getStatementName().getLocalName();
95     private static final String REVISION = YangStmtMapping.REVISION.getStatementName().getLocalName();
96     private static final String REVISION_DATE = YangStmtMapping.REVISION_DATE.getStatementName().getLocalName();
97     private static final String SUBMODULE = YangStmtMapping.SUBMODULE.getStatementName().getLocalName();
98     private static final String YANG_VERSION = YangStmtMapping.YANG_VERSION.getStatementName().getLocalName();
99
100     private final @NonNull String name;
101     private final @Nullable Revision revision;
102     private final @NonNull ImmutableSet<ModuleImport> submoduleIncludes;
103     private final @NonNull ImmutableSet<ModuleImport> moduleImports;
104     private final @NonNull ImmutableSet<ModuleImport> dependencies;
105
106     YangModelDependencyInfo(final String name, final Revision revision, final ImmutableSet<ModuleImport> imports,
107             final ImmutableSet<ModuleImport> includes) {
108         this.name = requireNonNull(name);
109         this.revision = revision;
110         moduleImports = requireNonNull(imports);
111         submoduleIncludes = requireNonNull(includes);
112         dependencies = ImmutableSet.<ModuleImport>builder().addAll(moduleImports).addAll(submoduleIncludes).build();
113     }
114
115     /**
116      * Returns model name.
117      *
118      * @return model name
119      */
120     public final String getName() {
121         return name;
122     }
123
124     /**
125      * Returns formatted revision string.
126      *
127      * @return formatted revision string, or {@code null}
128      */
129     public final @Nullable String getFormattedRevision() {
130         final var local = revision;
131         return local != null ? local.toString() : null;
132     }
133
134     /**
135      * Returns revision.
136      *
137      * @return revision, potentially null
138      */
139     public final Optional<Revision> getRevision() {
140         return Optional.ofNullable(revision);
141     }
142
143     /**
144      * Returns immutable collection of all module imports. This collection contains both <code>import</code> statements
145      * and <code>include</code> statements for submodules.
146      *
147      * @return Immutable collection of imports.
148      */
149     public final ImmutableSet<ModuleImport> getDependencies() {
150         return dependencies;
151     }
152
153     @Override
154     public final int hashCode() {
155         return Objects.hash(name, revision);
156     }
157
158     @Override
159     public final boolean equals(final Object obj) {
160         return this == obj || obj instanceof YangModelDependencyInfo other
161             && Objects.equals(name, other.name) && Objects.equals(revision, other.revision);
162     }
163
164     @Override
165     public final String toString() {
166         return MoreObjects.toStringHelper(this).omitNullValues()
167             .add("name", getName())
168             .add("revision", getRevision())
169             .add("dependencies", getDependencies())
170             .toString();
171     }
172
173     /**
174      * Extracts {@link YangModelDependencyInfo} from an intermediate representation root statement of a YANG model.
175      *
176      * @param source Schema source
177      * @return {@link YangModelDependencyInfo}
178      * @throws IllegalArgumentException If the root statement is not a valid YANG module/submodule
179      */
180     public static @NonNull YangModelDependencyInfo forIR(final YangIRSchemaSource source) {
181         return forIR(source.getRootStatement(), source.sourceId());
182     }
183
184     /**
185      * Extracts {@link YangModelDependencyInfo} from an intermediate representation root statement of a YANG model.
186      *
187      * @param sourceId Source identifier
188      * @param rootStatement root statement
189      * @return {@link YangModelDependencyInfo}
190      * @throws IllegalArgumentException If the root statement is not a valid YANG module/submodule
191      */
192     static @NonNull YangModelDependencyInfo forIR(final IRStatement rootStatement,
193             final SourceIdentifier sourceId) {
194         final var keyword = rootStatement.keyword();
195         if (!(keyword instanceof IRKeyword.Unqualified)) {
196             throw new IllegalArgumentException("Invalid root statement " + keyword);
197         }
198
199         final String arg = keyword.identifier();
200         if (MODULE.equals(arg)) {
201             return forSourceInfo(moduleForIR(rootStatement, sourceId));
202         }
203         if (SUBMODULE.equals(arg)) {
204             return forSourceInfo(submmoduleForIR(rootStatement, sourceId));
205         }
206         throw new IllegalArgumentException("Root of parsed AST must be either module or submodule");
207     }
208
209     public static @NonNull YangModelDependencyInfo forSourceInfo(final SourceInfo info) {
210         if (info instanceof ModuleSourceInfo module) {
211             return forSourceInfo(module);
212         } else if (info instanceof SubmoduleSourceInfo submodule) {
213             return forSourceInfo(submodule);
214         } else {
215             throw new IllegalArgumentException("Unhandled source info " + requireNonNull(info));
216         }
217     }
218
219     public static @NonNull ModuleDependencyInfo forSourceInfo(final @NonNull ModuleSourceInfo info) {
220         return new ModuleDependencyInfo(info.name().getLocalName(), latestRevision(info.revisions()),
221             info.imports().stream().map(ModuleImportImpl::new).collect(ImmutableSet.toImmutableSet()),
222             info.includes().stream().map(ModuleImportImpl::new).collect(ImmutableSet.toImmutableSet()));
223     }
224
225     public static @NonNull SubmoduleDependencyInfo forSourceInfo(final @NonNull SubmoduleSourceInfo info) {
226         return new SubmoduleDependencyInfo(info.name().getLocalName(), latestRevision(info.revisions()),
227             info.belongsTo(), info.imports().stream().map(ModuleImportImpl::new).collect(ImmutableSet.toImmutableSet()),
228             info.includes().stream().map(ModuleImportImpl::new).collect(ImmutableSet.toImmutableSet()));
229     }
230
231     /**
232      * Extracts {@link YangModelDependencyInfo} from a {@link YangTextSource}. This parsing does not validate full YANG
233      * module, only parses header up to the revisions and imports.
234      *
235      * @param yangText {@link YangTextSource}
236      * @return {@link YangModelDependencyInfo}
237      * @throws YangSyntaxErrorException If the resource does not pass syntactic analysis
238      * @throws IOException When the resource cannot be read
239      */
240     public static YangModelDependencyInfo forYangText(final YangTextSource yangText)
241             throws IOException, YangSyntaxErrorException {
242         final var source = YangStatementStreamSource.create(yangText);
243         return forIR(source.rootStatement(), source.getIdentifier());
244     }
245
246     private static @NonNull ModuleSourceInfo moduleForIR(final IRStatement root, final SourceIdentifier sourceId) {
247         return new ModuleSourceInfo(Unqualified.of(safeStringArgument(sourceId, root, "module name")),
248             extractYangVersion(root, sourceId), extractNamespace(root, sourceId), extractPrefix(root, sourceId),
249             extractRevisions(root, sourceId), extractImports(root, sourceId), extractIncludes(root, sourceId));
250     }
251
252     private static @NonNull SubmoduleSourceInfo submmoduleForIR(final IRStatement root,
253             final SourceIdentifier sourceId) {
254         return new SubmoduleSourceInfo(Unqualified.of(safeStringArgument(sourceId, root, "submodule name")),
255             extractYangVersion(root, sourceId), extractBelongsTo(root, sourceId),
256             extractRevisions(root, sourceId), extractImports(root, sourceId), extractIncludes(root, sourceId));
257     }
258
259     private static @Nullable Revision latestRevision(final Collection<Revision> revision) {
260         return revision.stream().sorted(Comparator.reverseOrder()).findFirst().orElse(null);
261     }
262
263     private static YangVersion extractYangVersion(final IRStatement root, final SourceIdentifier sourceId) {
264         return root.statements().stream()
265             .filter(stmt -> isBuiltin(stmt, YANG_VERSION))
266             .findFirst()
267             .map(stmt -> safeStringArgument(sourceId, stmt, "yang-version argument"))
268             .map(YangVersion::forString)
269             .orElse(YangVersion.VERSION_1);
270     }
271
272     private static @NonNull XMLNamespace extractNamespace(final IRStatement root, final SourceIdentifier sourceId) {
273         return root.statements().stream()
274             .filter(stmt -> isBuiltin(stmt, NAMESPACE))
275             .findFirst()
276             .map(stmt -> safeStringArgument(sourceId, stmt, "namespace argument"))
277             .map(XMLNamespace::of)
278             .orElseThrow(() -> new IllegalArgumentException("No namespace statement in " + refOf(sourceId, root)));
279     }
280
281     private static @NonNull String extractPrefix(final IRStatement root, final SourceIdentifier sourceId) {
282         return root.statements().stream()
283             .filter(stmt -> isBuiltin(stmt, PREFIX))
284             .findFirst()
285             .map(stmt -> safeStringArgument(sourceId, stmt, "prefix argument"))
286             .orElseThrow(() -> new IllegalArgumentException("No prefix statement in " + refOf(sourceId, root)));
287     }
288
289     private static @NonNull Unqualified extractBelongsTo(final IRStatement root, final SourceIdentifier sourceId) {
290         return root.statements().stream()
291             .filter(stmt -> isBuiltin(stmt, BELONGS_TO))
292             .findFirst()
293             .map(stmt -> Unqualified.of(safeStringArgument(sourceId, stmt, "belongs-to module name")))
294             .orElseThrow(() -> new IllegalArgumentException("No belongs-to statement in " + refOf(sourceId, root)));
295     }
296
297     private static @NonNull ImmutableSet<Revision> extractRevisions(final IRStatement root,
298             final SourceIdentifier sourceId) {
299         return root.statements().stream()
300             .filter(stmt -> isBuiltin(stmt, REVISION))
301             .map(stmt -> Revision.of(safeStringArgument(sourceId, stmt, "revision argument")))
302             .collect(ImmutableSet.toImmutableSet());
303     }
304
305     private static @Nullable Revision extractRevisionDate(final IRStatement root, final SourceIdentifier sourceId) {
306         return root.statements().stream()
307             .filter(stmt -> isBuiltin(stmt, REVISION_DATE))
308             .findFirst()
309             .map(stmt -> Revision.of(safeStringArgument(sourceId, stmt, "revision date argument")))
310             .orElse(null);
311     }
312
313     private static @NonNull ImmutableSet<Import> extractImports(final IRStatement root,
314             final SourceIdentifier sourceId) {
315         return root.statements().stream()
316             .filter(stmt -> isBuiltin(stmt, IMPORT))
317             .map(stmt -> new Import(Unqualified.of(safeStringArgument(sourceId, stmt, "imported module name")),
318                 extractPrefix(stmt, sourceId), extractRevisionDate(stmt, sourceId)))
319             .collect(ImmutableSet.toImmutableSet());
320     }
321
322     private static @NonNull ImmutableSet<Include> extractIncludes(final IRStatement root,
323             final SourceIdentifier sourceId) {
324         return root.statements().stream()
325             .filter(stmt -> isBuiltin(stmt, INCLUDE))
326             .map(stmt -> new Include(Unqualified.of(safeStringArgument(sourceId, stmt, "included submodule name")),
327                 extractRevisionDate(stmt, sourceId)))
328             .collect(ImmutableSet.toImmutableSet());
329     }
330
331     private static boolean isBuiltin(final IRStatement stmt, final String localName) {
332         return stmt.keyword() instanceof IRKeyword.Unqualified keyword && localName.equals(keyword.identifier());
333     }
334
335     public static String getLatestRevision(final IRStatement module, final SourceIdentifier source) {
336         String latestRevision = null;
337         for (final IRStatement substatement : module.statements()) {
338             if (isBuiltin(substatement, REVISION)) {
339                 final String currentRevision = safeStringArgument(source, substatement, "revision date");
340                 if (latestRevision == null || latestRevision.compareTo(currentRevision) < 0) {
341                     latestRevision = currentRevision;
342                 }
343             }
344         }
345         return latestRevision;
346     }
347
348     static @NonNull String safeStringArgument(final SourceIdentifier source, final IRStatement stmt,
349             final String desc) {
350         final var ref = refOf(source, stmt);
351         final var arg = stmt.argument();
352         if (arg == null) {
353             throw new IllegalArgumentException("Missing " + desc + " at " + ref);
354         }
355
356         // TODO: we probably need to understand yang version first....
357         return ArgumentContextUtils.rfc6020().stringFromStringContext(arg, ref);
358     }
359
360     private static StatementSourceReference refOf(final SourceIdentifier source, final IRStatement stmt) {
361         return ExplicitStatement.atPosition(source.name().getLocalName(), stmt.startLine(), stmt.startColumn() + 1);
362     }
363
364     /**
365      * Utility implementation of {@link ModuleImport} to be used by {@link YangModelDependencyInfo}.
366      */
367     // FIXME: this is a rather nasty misuse of APIs :(
368     private static final class ModuleImportImpl implements ModuleImport {
369         private final @NonNull Unqualified moduleName;
370         private final Revision revision;
371
372         ModuleImportImpl(final Import importSpec) {
373             moduleName = importSpec.name();
374             revision = importSpec.revision();
375         }
376
377         ModuleImportImpl(final Include includeSpec) {
378             moduleName = includeSpec.name();
379             revision = includeSpec.revision();
380         }
381
382         @Override
383         public Unqualified getModuleName() {
384             return moduleName;
385         }
386
387         @Override
388         public Optional<Revision> getRevision() {
389             return Optional.ofNullable(revision);
390         }
391
392         @Override
393         public String getPrefix() {
394             throw new UnsupportedOperationException();
395         }
396
397         @Override
398         public Optional<String> getDescription() {
399             return Optional.empty();
400         }
401
402         @Override
403         public Optional<String> getReference() {
404             return Optional.empty();
405         }
406
407         @Override
408         public ImportEffectiveStatement asEffectiveStatement() {
409             throw new UnsupportedOperationException();
410         }
411
412         @Override
413         public int hashCode() {
414             final int prime = 31;
415             int result = 1;
416             result = prime * result + Objects.hashCode(moduleName);
417             result = prime * result + Objects.hashCode(revision);
418             return result;
419         }
420
421         @Override
422         public boolean equals(final Object obj) {
423             return this == obj || obj instanceof ModuleImportImpl other
424                 && moduleName.equals(other.moduleName) && Objects.equals(revision, other.revision);
425         }
426
427         @Override
428         public String toString() {
429             return "ModuleImportImpl [name=" + moduleName + ", revision=" + revision + "]";
430         }
431     }
432 }