Make model prefix handling optional
[yangtools.git] / codec / yang-data-codec-xml / src / main / java / org / opendaylight / yangtools / yang / data / codec / xml / NamespacePrefixes.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.data.codec.xml;
9
10 import com.google.common.annotations.VisibleForTesting;
11 import com.google.common.collect.BiMap;
12 import com.google.common.collect.HashBiMap;
13 import java.util.Comparator;
14 import java.util.List;
15 import java.util.Map.Entry;
16 import java.util.stream.Collectors;
17 import javax.xml.XMLConstants;
18 import javax.xml.namespace.NamespaceContext;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.concepts.Mutable;
22 import org.opendaylight.yangtools.yang.common.XMLNamespace;
23
24 /**
25  * Interface to prefix assignment based on a {@link NamespaceContext} and advice from {@link PreferredPrefixes}.
26  */
27 final class NamespacePrefixes implements Mutable {
28     // 32 characters, carefully chosen
29     @VisibleForTesting
30     static final String LOOKUP = "abcdefghiknoprstABCDEFGHIKNOPRST";
31     @VisibleForTesting
32     static final int SHIFT = 5;
33     private static final int MASK = 0x1f;
34
35     private int counter = 0;
36
37     // BiMap to make values lookup faster
38     private final BiMap<XMLNamespace, String> emittedPrefixes = HashBiMap.create();
39     private final PreferredPrefixes pref;
40     private final NamespaceContext context;
41
42     NamespacePrefixes(final NamespaceContext context, final @Nullable PreferredPrefixes pref) {
43         this.context = context;
44         this.pref = pref;
45     }
46
47     List<Entry<XMLNamespace, String>> emittedPrefixes() {
48         return emittedPrefixes.entrySet().stream()
49             // Order by prefix
50             .sorted(Comparator.comparing(Entry::getValue))
51             .collect(Collectors.toList());
52     }
53
54     @NonNull String encodePrefix(final XMLNamespace namespace) {
55         var prefix = emittedPrefixes.get(namespace);
56         if (prefix != null) {
57             return prefix;
58         }
59
60         if (context != null) {
61             prefix = context.getPrefix(namespace.toString());
62             if (prefix != null) {
63                 return prefix;
64             }
65         }
66
67         prefix = createPrefix(namespace);
68         emittedPrefixes.put(namespace, prefix);
69         return prefix;
70     }
71
72     private @NonNull String createPrefix(final @NonNull XMLNamespace namespace) {
73         if (pref != null) {
74             final var prefix = pref.prefixForNamespace(namespace);
75             if (prefix != null) {
76                 return prefix;
77             }
78         }
79
80         String prefix;
81         do {
82             prefix = encode(counter++);
83         } while (alreadyUsedPrefix(prefix));
84         return prefix;
85     }
86
87     private boolean alreadyUsedPrefix(final String prefix) {
88         if (pref != null && pref.isUsed(prefix)) {
89             return true;
90         }
91         if (context == null) {
92             return false;
93         }
94
95         // It seems JDK8 is violating the API contract of NamespaceContext by returning null for unbound prefixes,
96         // rather than specified NULL_NS_URI. Work this around by checking explicitly for null.
97         final var str = context.getNamespaceURI(prefix);
98         return str != null && !XMLConstants.NULL_NS_URI.equals(str);
99     }
100
101     @VisibleForTesting
102     static @NonNull String encode(int num) {
103         final var sb = new StringBuilder();
104
105         do {
106             sb.append(LOOKUP.charAt(num & MASK));
107             num >>>= SHIFT;
108         } while (num != 0);
109
110         return sb.reverse().toString();
111     }
112 }