1 /*
2 * Copyright 2020 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package io.netty.util;
17
18 import java.util.LinkedHashMap;
19 import java.util.Map;
20
21 import static io.netty.util.internal.ObjectUtil.checkNotNull;
22
23 /**
24 * Builder that allows to build {@link Mapping}s that support
25 * <a href="https://tools.ietf.org/search/rfc6125#section-6.4">DNS wildcard</a> matching.
26 * @param <V> the type of the value that we map to.
27 */
28 public class DomainWildcardMappingBuilder<V> {
29
30 private final V defaultValue;
31 private final Map<String, V> map;
32
33 /**
34 * Constructor with default initial capacity of the map holding the mappings
35 *
36 * @param defaultValue the default value for {@link Mapping#map(Object)} )} to return
37 * when nothing matches the input
38 */
39 public DomainWildcardMappingBuilder(V defaultValue) {
40 this(4, defaultValue);
41 }
42
43 /**
44 * Constructor with initial capacity of the map holding the mappings
45 *
46 * @param initialCapacity initial capacity for the internal map
47 * @param defaultValue the default value for {@link Mapping#map(Object)} to return
48 * when nothing matches the input
49 */
50 public DomainWildcardMappingBuilder(int initialCapacity, V defaultValue) {
51 this.defaultValue = checkNotNull(defaultValue, "defaultValue");
52 map = new LinkedHashMap<String, V>(initialCapacity);
53 }
54
55 /**
56 * Adds a mapping that maps the specified (optionally wildcard) host name to the specified output value.
57 * {@code null} values are forbidden for both hostnames and values.
58 * <p>
59 * <a href="https://tools.ietf.org/search/rfc6125#section-6.4">DNS wildcard</a> is supported as hostname. The
60 * wildcard will only match one sub-domain deep and only when wildcard is used as the most-left label.
61 *
62 * For example:
63 *
64 * <p>
65 * *.netty.io will match xyz.netty.io but NOT abc.xyz.netty.io
66 * </p>
67 *
68 * @param hostname the host name (optionally wildcard)
69 * @param output the output value that will be returned by {@link Mapping#map(Object)}
70 * when the specified host name matches the specified input host name
71 */
72 public DomainWildcardMappingBuilder<V> add(String hostname, V output) {
73 map.put(normalizeHostName(hostname),
74 checkNotNull(output, "output"));
75 return this;
76 }
77
78 private String normalizeHostName(String hostname) {
79 checkNotNull(hostname, "hostname");
80 if (hostname.isEmpty() || hostname.charAt(0) == '.') {
81 throw new IllegalArgumentException("Hostname '" + hostname + "' not valid");
82 }
83 hostname = ImmutableDomainWildcardMapping.normalize(checkNotNull(hostname, "hostname"));
84 if (hostname.charAt(0) == '*') {
85 if (hostname.length() < 3 || hostname.charAt(1) != '.') {
86 throw new IllegalArgumentException("Wildcard Hostname '" + hostname + "'not valid");
87 }
88 return hostname.substring(1);
89 }
90 return hostname;
91 }
92 /**
93 * Creates a new instance of an immutable {@link Mapping}.
94 *
95 * @return new {@link Mapping} instance
96 */
97 public Mapping<String, V> build() {
98 return new ImmutableDomainWildcardMapping<V>(defaultValue, map);
99 }
100
101 private static final class ImmutableDomainWildcardMapping<V> implements Mapping<String, V> {
102 private static final String REPR_HEADER = "ImmutableDomainWildcardMapping(default: ";
103 private static final String REPR_MAP_OPENING = ", map: ";
104 private static final String REPR_MAP_CLOSING = ")";
105
106 private final V defaultValue;
107 private final Map<String, V> map;
108
109 ImmutableDomainWildcardMapping(V defaultValue, Map<String, V> map) {
110 this.defaultValue = defaultValue;
111 this.map = new LinkedHashMap<String, V>(map);
112 }
113
114 @Override
115 public V map(String hostname) {
116 if (hostname != null) {
117 hostname = normalize(hostname);
118
119 // Let's try an exact match first
120 V value = map.get(hostname);
121 if (value != null) {
122 return value;
123 }
124
125 // No exact match, let's try a wildcard match.
126 int idx = hostname.indexOf('.');
127 if (idx != -1) {
128 value = map.get(hostname.substring(idx));
129 if (value != null) {
130 return value;
131 }
132 }
133 }
134
135 return defaultValue;
136 }
137
138 @SuppressWarnings("deprecation")
139 static String normalize(String hostname) {
140 return DomainNameMapping.normalizeHostname(hostname);
141 }
142
143 @Override
144 public String toString() {
145 StringBuilder sb = new StringBuilder();
146 sb.append(REPR_HEADER).append(defaultValue).append(REPR_MAP_OPENING).append('{');
147
148 for (Map.Entry<String, V> entry : map.entrySet()) {
149 String hostname = entry.getKey();
150 if (hostname.charAt(0) == '.') {
151 hostname = '*' + hostname;
152 }
153 sb.append(hostname).append('=').append(entry.getValue()).append(", ");
154 }
155 sb.setLength(sb.length() - 2);
156 return sb.append('}').append(REPR_MAP_CLOSING).toString();
157 }
158 }
159 }