View Javadoc

1   /**
2    * Copyright (c) 2012-2013, JCabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.urn;
31  
32  import com.jcabi.aspects.Immutable;
33  import java.io.Serializable;
34  import java.net.URI;
35  import java.net.URISyntaxException;
36  import java.net.URLDecoder;
37  import java.util.Map;
38  import java.util.TreeMap;
39  import javax.validation.constraints.NotNull;
40  import lombok.EqualsAndHashCode;
41  import org.apache.commons.lang3.CharEncoding;
42  import org.apache.commons.lang3.StringUtils;
43  
44  /**
45   * Uniform Resource Name (URN) as in
46   * <a href="http://tools.ietf.org/html/rfc2141">RFC 2141</a>.
47   *
48   * <p>Usage is similar to {@link java.net.URI} or {@link java.net.URL}:
49   *
50   * <pre> URN urn = new URN("urn:foo:A123,456");
51   * assert urn.nid().equals("foo");
52   * assert urn.nss().equals("A123,456");</pre>
53   *
54   * <p><b>NOTICE:</b> the implementation is not fully compliant with RFC 2141.
55   * It will become compliant in one of our future versions. Once it becomes
56   * fully compliant this notice will be removed.
57   *
58   * @author Yegor Bugayenko (yegor@tpc2.com)
59   * @version $Id$
60   * @since 0.6
61   * @see <a href="http://tools.ietf.org/html/rfc2141">RFC 2141</a>
62   */
63  @Immutable
64  @EqualsAndHashCode
65  @SuppressWarnings({ "PMD.TooManyMethods", "PMD.UseConcurrentHashMap" })
66  public final class URN implements Comparable<URN>, Serializable {
67  
68      /**
69       * Serialization marker.
70       */
71      private static final long serialVersionUID = 0xBF46AFCD9612A6DFL;
72  
73      /**
74       * NID of an empty URN.
75       */
76      private static final String EMPTY = "void";
77  
78      /**
79       * The leading sequence.
80       */
81      private static final String PREFIX = "urn";
82  
83      /**
84       * The separator.
85       */
86      private static final String SEP = ":";
87  
88      /**
89       * Validating regular expr.
90       */
91      private static final String REGEX =
92          // @checkstyle LineLength (1 line)
93          "^urn:[a-z]{1,31}(:([\\-a-zA-Z0-9/]|%[0-9a-fA-F]{2})*)+(\\?\\w+(=([\\-a-zA-Z0-9/]|%[0-9a-fA-F]{2})*)?(&\\w+(=([\\-a-zA-Z0-9/]|%[0-9a-fA-F]{2})*)?)*)?\\*?$";
94  
95      /**
96       * The URI.
97       */
98      @SuppressWarnings("PMD.BeanMembersShouldSerialize")
99      private final String uri;
100 
101     /**
102      * Public ctor (for JAXB mostly) that creates an "empty" URN.
103      */
104     public URN() {
105         this(URN.EMPTY, "");
106     }
107 
108     /**
109      * Public ctor.
110      * @param text The text of the URN
111      * @throws URISyntaxException If syntax is not correct
112      */
113     public URN(@NotNull final String text) throws URISyntaxException {
114         if (!text.matches(URN.REGEX)) {
115             throw new URISyntaxException(text, "Invalid format of URN");
116         }
117         this.uri = text;
118         this.validate();
119     }
120 
121     /**
122      * Public ctor.
123      * @param nid The namespace ID
124      * @param nss The namespace specific string
125      */
126     public URN(@NotNull final String nid, @NotNull final String nss) {
127         this.uri = String.format(
128             "%s%s%s%2$s%s",
129             URN.PREFIX,
130             URN.SEP,
131             nid,
132             URN.encode(nss)
133         );
134         try {
135             this.validate();
136         } catch (URISyntaxException ex) {
137             throw new IllegalArgumentException(ex);
138         }
139     }
140 
141     /**
142      * Creates an instance of URN and throws a runtime exception if
143      * its syntax is not valid.
144      * @param text The text of the URN
145      * @return The URN created
146      */
147     public static URN create(@NotNull final String text) {
148         try {
149             return new URN(text);
150         } catch (URISyntaxException ex) {
151             throw new IllegalArgumentException(ex);
152         }
153     }
154 
155     /**
156      * {@inheritDoc}
157      */
158     @Override
159     public String toString() {
160         return this.uri;
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
167     public int compareTo(final URN urn) {
168         return this.uri.compareTo(urn.uri);
169     }
170 
171     /**
172      * Is it a valid URN?
173      * @param text The text to validate
174      * @return Yes of no
175      */
176     public static boolean isValid(final String text) {
177         boolean valid = true;
178         try {
179             new URN(text);
180         } catch (URISyntaxException ex) {
181             valid = false;
182         }
183         return valid;
184     }
185 
186     /**
187      * Does it match the pattern?
188      * @param pattern The pattern to match
189      * @return Yes of no
190      */
191     public boolean matches(@NotNull final String pattern) {
192         boolean matches = false;
193         if (this.equals(pattern)) {
194             matches = true;
195         } else if (pattern.endsWith("*")) {
196             final String body = pattern.substring(0, pattern.length() - 1);
197             matches = this.uri.startsWith(body);
198         }
199         return matches;
200     }
201 
202     /**
203      * Is it empty?
204      * @return Yes of no
205      */
206     public boolean isEmpty() {
207         return URN.EMPTY.equals(this.nid());
208     }
209 
210     /**
211      * Convert it to URI.
212      * @return The URI
213      */
214     public URI toURI() {
215         return URI.create(this.uri);
216     }
217 
218     /**
219      * Get namespace ID.
220      * @return Namespace ID
221      */
222     public String nid() {
223         return this.segment(1);
224     }
225 
226     /**
227      * Get namespace specific string.
228      * @return Namespace specific string
229      */
230     public String nss() {
231         try {
232             return URLDecoder.decode(this.segment(2), CharEncoding.UTF_8);
233         } catch (java.io.UnsupportedEncodingException ex) {
234             throw new IllegalStateException(ex);
235         }
236     }
237 
238     /**
239      * Get all params.
240      * @return The params
241      */
242     public Map<String, String> params() {
243         return URN.demap(this.toString());
244     }
245 
246     /**
247      * Get query param by name.
248      * @param name Name of parameter
249      * @return The value of it
250      */
251     public String param(@NotNull final String name) {
252         final Map<String, String> params = this.params();
253         if (!params.containsKey(name)) {
254             throw new IllegalArgumentException(
255                 String.format(
256                     "Param '%s' not found in '%s', among %s",
257                     name,
258                     this,
259                     params.keySet()
260                 )
261             );
262         }
263         return params.get(name);
264     }
265 
266     /**
267      * Add (overwrite) a query param and return a new URN.
268      * @param name Name of parameter
269      * @param value The value of parameter
270      * @return New URN
271      */
272     public URN param(@NotNull final String name, @NotNull final Object value) {
273         final Map<String, String> params = this.params();
274         params.put(name, value.toString());
275         return URN.create(
276             String.format(
277                 "%s%s",
278                 StringUtils.split(this.toString(), '?')[0],
279                 URN.enmap(params)
280             )
281         );
282     }
283 
284     /**
285      * Get just body of URN, without params.
286      * @return Clean version of it
287      */
288     public URN pure() {
289         String urn = this.toString();
290         if (this.hasParams()) {
291             // @checkstyle MultipleStringLiterals (1 line)
292             urn = urn.substring(0, urn.indexOf('?'));
293         }
294         return URN.create(urn);
295     }
296 
297     /**
298      * Whether this URN has params?
299      * @return Has them?
300      */
301     public boolean hasParams() {
302         // @checkstyle MultipleStringLiterals (1 line)
303         return this.toString().contains("?");
304     }
305 
306     /**
307      * Get segment by position.
308      * @param pos Its position
309      * @return The segment
310      */
311     private String segment(final int pos) {
312         return StringUtils.splitPreserveAllTokens(
313             this.uri,
314             URN.SEP,
315             // @checkstyle MagicNumber (1 line)
316             3
317         )[pos];
318     }
319 
320     /**
321      * Validate URN.
322      * @throws URISyntaxException If it's not valid
323      */
324     private void validate() throws URISyntaxException {
325         if (this.isEmpty() && !this.nss().isEmpty()) {
326             throw new URISyntaxException(
327                 this.toString(),
328                 "Empty URN can't have NSS"
329             );
330         }
331         if (!this.nid().matches("^[a-z]{1,31}$")) {
332             throw new IllegalArgumentException(
333                 String.format(
334                     "NID '%s' can contain up to 31 low case letters",
335                     this.nid()
336                 )
337             );
338         }
339     }
340 
341     /**
342      * Decode query part of the URN into Map.
343      * @param urn The URN to demap
344      * @return The map of values
345      */
346     private static Map<String, String> demap(final String urn) {
347         final Map<String, String> map = new TreeMap<String, String>();
348         final String[] sectors = StringUtils.split(urn, '?');
349         if (sectors.length == 2) {
350             final String[] parts = StringUtils.split(sectors[1], '&');
351             for (String part : parts) {
352                 final String[] pair = StringUtils.split(part, '=');
353                 String value;
354                 if (pair.length == 2) {
355                     try {
356                         value = URLDecoder.decode(pair[1], CharEncoding.UTF_8);
357                     } catch (java.io.UnsupportedEncodingException ex) {
358                         throw new IllegalStateException(ex);
359                     }
360                 } else {
361                     value = "";
362                 }
363                 map.put(pair[0], value);
364             }
365         }
366         return map;
367     }
368 
369     /**
370      * Encode map of params into query part of URN.
371      * @param params Map of params to convert to query suffix
372      * @return The suffix of URN, starting with "?"
373      */
374     private static String enmap(final Map<String, String> params) {
375         final StringBuilder query = new StringBuilder();
376         if (!params.isEmpty()) {
377             query.append("?");
378             boolean first = true;
379             for (Map.Entry<String, String> param : params.entrySet()) {
380                 if (!first) {
381                     query.append("&");
382                 }
383                 query.append(param.getKey());
384                 if (!param.getValue().isEmpty()) {
385                     query.append("=").append(URN.encode(param.getValue()));
386                 }
387                 first = false;
388             }
389         }
390         return query.toString();
391     }
392 
393     /**
394      * Perform proper URL encoding with the text.
395      * @param text The text to encode
396      * @return The encoded text
397      */
398     private static String encode(final String text) {
399         final StringBuilder encoded = new StringBuilder();
400         byte[] bytes;
401         try {
402             bytes = text.getBytes(CharEncoding.UTF_8);
403         } catch (java.io.UnsupportedEncodingException ex) {
404             throw new IllegalStateException(ex);
405         }
406         for (byte chr : bytes) {
407             if (URN.allowed(chr)) {
408                 encoded.append((char) chr);
409             } else {
410                 encoded.append("%").append(String.format("%X", chr));
411             }
412         }
413         return encoded.toString();
414     }
415 
416     /**
417      * This char is allowed in URN's NSS part?
418      * @param chr The character
419      * @return It is allowed?
420      */
421     private static boolean allowed(final byte chr) {
422         // @checkstyle BooleanExpressionComplexity (4 lines)
423         return (chr >= 'A' && chr <= 'Z')
424             || (chr >= '0' && chr <= '9')
425             || (chr >= 'a' && chr <= 'z')
426             || (chr == '/') || (chr == '-');
427     }
428 
429 }