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