View Javadoc
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.netty5.util;
17  
18  import java.util.LinkedHashMap;
19  import java.util.Map;
20  
21  import static java.util.Objects.requireNonNull;
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 = requireNonNull(defaultValue, "defaultValue");
52          map = new LinkedHashMap<>(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                  requireNonNull(output, "output"));
75          return this;
76      }
77  
78      private String normalizeHostName(String hostname) {
79          requireNonNull(hostname, "hostname");
80          if (hostname.isEmpty() || hostname.charAt(0) == '.') {
81              throw new IllegalArgumentException("Hostname '" + hostname + "' not valid");
82          }
83          hostname = ImmutableDomainWildcardMapping.normalize(requireNonNull(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<>(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<>(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 }