Make model prefix handling optional
[yangtools.git] / codec / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / PreferredPrefixes.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.codec.xml;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.base.MoreObjects;
13 import com.google.common.collect.ImmutableBiMap;
14 import com.google.common.collect.Maps;
15 import java.util.Map;
16 import java.util.Optional;
17 import java.util.concurrent.ConcurrentHashMap;
18 import java.util.concurrent.ConcurrentMap;
19 import java.util.stream.Stream;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.yangtools.yang.common.XMLNamespace;
23 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
24 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
25
26 /**
27  * Prefixes preferred by an {@link EffectiveModelContext}. This acts as an advisory to {@link NamespacePrefixes} for
28  * picking namespace prefixes. This works with IETF guidelines, which prefer XML prefix names coming from {@code prefix}
29  * statement's argument. This, unfortunately, is not sufficient, as these are not guaranteed to be unique, but we deal
30  * with those ambiguities.
31  */
32 abstract sealed class PreferredPrefixes {
33     static final class Precomputed extends PreferredPrefixes {
34         private final ImmutableBiMap<XMLNamespace, String> mappings;
35
36         Precomputed(final Map<XMLNamespace, String> mappings) {
37             this.mappings = ImmutableBiMap.copyOf(mappings);
38         }
39
40         @Override
41         String prefixForNamespace(final XMLNamespace namespace) {
42             return mappings.get(namespace);
43         }
44
45         @Override
46         boolean isUsed(final String prefix) {
47             return mappings.inverse().containsKey(prefix);
48         }
49
50         @Override
51         Map<XMLNamespace, ?> mappings() {
52             return mappings;
53         }
54     }
55
56     static final class Shared extends PreferredPrefixes {
57         private final ConcurrentMap<XMLNamespace, Optional<String>> mappings = new ConcurrentHashMap<>();
58         private final EffectiveModelContext modelContext;
59
60         Shared(final EffectiveModelContext modelContext) {
61             this.modelContext = requireNonNull(modelContext);
62         }
63
64         @Override
65         String prefixForNamespace(final XMLNamespace namespace) {
66             final var existing = mappings.get(namespace);
67             if (existing != null) {
68                 return existing.orElse(null);
69             }
70
71             final var modules = modelContext.findModuleStatements(namespace).iterator();
72             // Note: we are not caching anything if we do not find the module
73             return modules.hasNext() ? loadPrefix(namespace, modules.next().prefix().argument()) : null;
74         }
75
76         @Override
77         boolean isUsed(final String prefix) {
78             return modulesForPrefix(prefix).findAny().isPresent();
79         }
80
81         /**
82          * Completely populate known mappings and return an optimized version equivalent of this object.
83          *
84          * @return A pre-computed {@link PreferredPrefixes} instance
85          */
86         @NonNull PreferredPrefixes toPrecomputed() {
87             for (var module : modelContext.getModuleStatements().values()) {
88                 prefixForNamespace(module.namespace().argument());
89             }
90             return new Precomputed(
91                 Maps.transformValues(Maps.filterValues(mappings, Optional::isPresent), Optional::orElseThrow));
92         }
93
94         @Override
95         Map<XMLNamespace, ?> mappings() {
96             return mappings;
97         }
98
99         private @Nullable String loadPrefix(final XMLNamespace namespace, final String prefix) {
100             final var mapping = isValidMapping(namespace, prefix) ? Optional.of(prefix) : Optional.<String>empty();
101             final var raced = mappings.putIfAbsent(namespace, mapping);
102             return (raced != null ? raced : mapping).orElse(null);
103         }
104
105         // Validate that all modules which have the same prefix have also the name namespace
106         private boolean isValidMapping(final XMLNamespace namespace, final String prefix) {
107             return !startsWithXml(prefix) && modulesForPrefix(prefix)
108                 .allMatch(module -> namespace.equals(module.namespace().argument()));
109         }
110
111         private Stream<ModuleEffectiveStatement> modulesForPrefix(final String prefix) {
112             return modelContext.getModuleStatements().values().stream()
113                 .filter(module -> prefix.equals(module.prefix().argument()));
114         }
115
116         // https://www.w3.org/TR/xml-names/#xmlReserved
117         private static boolean startsWithXml(final String prefix) {
118             if (prefix.length() < 3) {
119                 return false;
120             }
121             final var first = prefix.charAt(0);
122             if (first != 'x' && first != 'X') {
123                 return false;
124             }
125             final var second = prefix.charAt(1);
126             if (second != 'm' && second != 'M') {
127                 return false;
128             }
129             final var third = prefix.charAt(2);
130             return third == 'l' || third == 'L';
131         }
132     }
133
134     private PreferredPrefixes() {
135         // Hidden on purpose
136     }
137
138     abstract @Nullable String prefixForNamespace(@NonNull XMLNamespace namespace);
139
140     abstract boolean isUsed(String prefix);
141
142     @Override
143     public final String toString() {
144         return MoreObjects.toStringHelper(this).add("mappings", mappings()).toString();
145     }
146
147     abstract Map<XMLNamespace, ?> mappings();
148
149 }