Coverage Report - com.jcabi.urn.URN
 
Classes in this File Line Coverage Branch Coverage Complexity
URN
84%
99/117
79%
62/78
3.565
 
 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 com.jcabi.aspects.Tv;
 34  
 import java.io.Serializable;
 35  
 import java.io.UnsupportedEncodingException;
 36  
 import java.net.URI;
 37  
 import java.net.URISyntaxException;
 38  
 import java.net.URLDecoder;
 39  
 import java.util.Map;
 40  
 import java.util.TreeMap;
 41  
 import lombok.EqualsAndHashCode;
 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  0
 @Immutable
 64  20
 @EqualsAndHashCode
 65  
 @SuppressWarnings({
 66  
     "PMD.TooManyMethods", "PMD.UseConcurrentHashMap", "PMD.GodClass"
 67  
 })
 68  
 public final class URN implements Comparable<URN>, Serializable {
 69  
 
 70  
     /**
 71  
      * Serialization marker.
 72  
      */
 73  
     private static final long serialVersionUID = 0xBF46AFCD9612A6DFL;
 74  
 
 75  
     /**
 76  
      * Encoding to use.
 77  
      */
 78  
     private static final String ENCODING = "UTF-8";
 79  
 
 80  
     /**
 81  
      * NID of an empty URN.
 82  
      */
 83  
     private static final String EMPTY = "void";
 84  
 
 85  
     /**
 86  
      * The leading sequence.
 87  
      */
 88  
     private static final String PREFIX = "urn";
 89  
 
 90  
     /**
 91  
      * The separator.
 92  
      */
 93  
     private static final String SEP = ":";
 94  
 
 95  
     /**
 96  
      * Validating regular expr.
 97  
      */
 98  
     private static final String REGEX =
 99  
         // @checkstyle LineLength (1 line)
 100  
         "^(?i)^urn(?-i):[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})*)?)*)?\\*?$";
 101  
 
 102  
     /**
 103  
      * The URI.
 104  
      */
 105  
     @SuppressWarnings("PMD.BeanMembersShouldSerialize")
 106  
     private final String uri;
 107  
 
 108  
     /**
 109  
      * Public ctor (for JAXB mostly) that creates an "empty" URN.
 110  
      */
 111  
     public URN() {
 112  1
         this(URN.EMPTY, "");
 113  1
     }
 114  
 
 115  
     /**
 116  
      * Public ctor.
 117  
      * @param text The text of the URN
 118  
      * @throws URISyntaxException If syntax is not correct
 119  
      */
 120  89
     public URN(final String text) throws URISyntaxException {
 121  89
         if (text == null) {
 122  1
             throw new IllegalArgumentException("text can't be NULL");
 123  
         }
 124  88
         if (!text.matches(URN.REGEX)) {
 125  13
             throw new URISyntaxException(text, "Invalid format of URN");
 126  
         }
 127  75
         this.uri = text;
 128  75
         this.validate();
 129  73
     }
 130  
 
 131  
     /**
 132  
      * Public ctor.
 133  
      * @param nid The namespace ID
 134  
      * @param nss The namespace specific string
 135  
      */
 136  8
     public URN(final String nid, final String nss) {
 137  8
         if (nid == null) {
 138  1
             throw new IllegalArgumentException("NID can't be NULL");
 139  
         }
 140  7
         if (nss == null) {
 141  1
             throw new IllegalArgumentException("NSS can't be NULL");
 142  
         }
 143  6
         this.uri = String.format(
 144  
             "%s%s%s%2$s%s",
 145  
             URN.PREFIX,
 146  
             URN.SEP,
 147  
             nid,
 148  
             URN.encode(nss)
 149  
         );
 150  
         try {
 151  6
             this.validate();
 152  1
         } catch (final URISyntaxException ex) {
 153  1
             throw new IllegalArgumentException(ex);
 154  5
         }
 155  5
     }
 156  
 
 157  
     /**
 158  
      * Creates an instance of URN and throws a runtime exception if
 159  
      * its syntax is not valid.
 160  
      * @param text The text of the URN
 161  
      * @return The URN created
 162  
      */
 163  
     public static URN create(final String text) {
 164  60
         if (text == null) {
 165  0
             throw new IllegalArgumentException("URN can't be NULL");
 166  
         }
 167  
         try {
 168  60
             return new URN(text);
 169  12
         } catch (final URISyntaxException ex) {
 170  12
             throw new IllegalArgumentException(ex);
 171  
         }
 172  
     }
 173  
 
 174  
     @Override
 175  
     public String toString() {
 176  76
         return this.uri;
 177  
     }
 178  
 
 179  
     @Override
 180  
     public int compareTo(final URN urn) {
 181  0
         return this.uri.compareTo(urn.uri);
 182  
     }
 183  
 
 184  
     /**
 185  
      * Is it a valid URN?
 186  
      * @param text The text to validate
 187  
      * @return Yes of no
 188  
      */
 189  
     public static boolean isValid(final String text) {
 190  14
         boolean valid = true;
 191  
         try {
 192  14
             new URN(text);
 193  0
         } catch (final URISyntaxException ex) {
 194  0
             valid = false;
 195  14
         }
 196  14
         return valid;
 197  
     }
 198  
 
 199  
     /**
 200  
      * Does it match the pattern?
 201  
      * @param pattern The pattern to match
 202  
      * @return Yes of no
 203  
      */
 204  
     public boolean matches(final String pattern) {
 205  1
         if (pattern == null) {
 206  0
             throw new IllegalArgumentException("pattern can't be NULL");
 207  
         }
 208  1
         boolean matches = false;
 209  1
         if (this.toString().equals(pattern)) {
 210  0
             matches = true;
 211  1
         } else if (pattern.endsWith("*")) {
 212  1
             final String body = pattern.substring(0, pattern.length() - 1);
 213  1
             matches = this.uri.startsWith(body);
 214  
         }
 215  1
         return matches;
 216  
     }
 217  
 
 218  
     /**
 219  
      * Is it empty?
 220  
      * @return Yes of no
 221  
      */
 222  
     public boolean isEmpty() {
 223  82
         return URN.EMPTY.equals(this.nid());
 224  
     }
 225  
 
 226  
     /**
 227  
      * Convert it to URI.
 228  
      * @return The URI
 229  
      */
 230  
     public URI toURI() {
 231  1
         return URI.create(this.uri);
 232  
     }
 233  
 
 234  
     /**
 235  
      * Get namespace ID.
 236  
      * @return Namespace ID
 237  
      */
 238  
     public String nid() {
 239  163
         return this.segment(1);
 240  
     }
 241  
 
 242  
     /**
 243  
      * Get namespace specific string.
 244  
      * @return Namespace specific string
 245  
      */
 246  
     public String nss() {
 247  
         try {
 248  5
             return URLDecoder.decode(this.segment(2), URN.ENCODING);
 249  0
         } catch (final UnsupportedEncodingException ex) {
 250  0
             throw new IllegalStateException(ex);
 251  
         }
 252  
     }
 253  
 
 254  
     /**
 255  
      * Get all params.
 256  
      * @return The params
 257  
      */
 258  
     public Map<String, String> params() {
 259  20
         return URN.demap(this.toString());
 260  
     }
 261  
 
 262  
     /**
 263  
      * Get query param by name.
 264  
      * @param name Name of parameter
 265  
      * @return The value of it
 266  
      */
 267  
     public String param(final String name) {
 268  2
         if (name == null) {
 269  0
             throw new IllegalArgumentException("param name can't be NULL");
 270  
         }
 271  2
         final Map<String, String> params = this.params();
 272  2
         if (!params.containsKey(name)) {
 273  0
             throw new IllegalArgumentException(
 274  
                 String.format(
 275  
                     "Param '%s' not found in '%s', among %s",
 276  
                     name,
 277  
                     this,
 278  
                     params.keySet()
 279  
                 )
 280  
             );
 281  
         }
 282  2
         return params.get(name);
 283  
     }
 284  
 
 285  
     /**
 286  
      * Add (overwrite) a query param and return a new URN.
 287  
      * @param name Name of parameter
 288  
      * @param value The value of parameter
 289  
      * @return New URN
 290  
      */
 291  
     public URN param(final String name, final Object value) {
 292  18
         if (name == null) {
 293  0
             throw new IllegalArgumentException("param can't be NULL");
 294  
         }
 295  18
         if (value == null) {
 296  0
             throw new IllegalArgumentException("param value can't be NULL");
 297  
         }
 298  18
         final Map<String, String> params = this.params();
 299  18
         params.put(name, value.toString());
 300  18
         return URN.create(
 301  
             String.format(
 302  
                 "%s%s",
 303  
                 StringUtils.split(this.toString(), '?')[0],
 304  
                 URN.enmap(params)
 305  
             )
 306  
         );
 307  
     }
 308  
 
 309  
     /**
 310  
      * Get just body of URN, without params.
 311  
      * @return Clean version of it
 312  
      */
 313  
     public URN pure() {
 314  1
         String urn = this.toString();
 315  1
         if (this.hasParams()) {
 316  
             // @checkstyle MultipleStringLiterals (1 line)
 317  1
             urn = urn.substring(0, urn.indexOf('?'));
 318  
         }
 319  1
         return URN.create(urn);
 320  
     }
 321  
 
 322  
     /**
 323  
      * Whether this URN has params?
 324  
      * @return Has them?
 325  
      */
 326  
     public boolean hasParams() {
 327  
         // @checkstyle MultipleStringLiterals (1 line)
 328  1
         return this.toString().contains("?");
 329  
     }
 330  
 
 331  
     /**
 332  
      * Get segment by position.
 333  
      * @param pos Its position
 334  
      * @return The segment
 335  
      */
 336  
     private String segment(final int pos) {
 337  168
         return StringUtils.splitPreserveAllTokens(
 338  
             this.uri,
 339  
             URN.SEP,
 340  
             // @checkstyle MagicNumber (1 line)
 341  
             3
 342  
         )[pos];
 343  
     }
 344  
 
 345  
     /**
 346  
      * Validate URN.
 347  
      * @throws URISyntaxException If it's not valid
 348  
      */
 349  
     private void validate() throws URISyntaxException {
 350  81
         if (this.isEmpty() && !this.nss().isEmpty()) {
 351  2
             throw new URISyntaxException(
 352  
                 this.toString(),
 353  
                 "Empty URN can't have NSS"
 354  
             );
 355  
         }
 356  79
         final String nid = this.nid();
 357  79
         if (!nid.matches("^[a-z]{1,31}$")) {
 358  0
             throw new IllegalArgumentException(
 359  
                 String.format(
 360  
                     "NID '%s' can contain up to 31 low case letters",
 361  
                     this.nid()
 362  
                 )
 363  
             );
 364  
         }
 365  79
         if (StringUtils.equalsIgnoreCase(URN.PREFIX, nid)) {
 366  1
             throw new IllegalArgumentException(
 367  
                 "NID can't be 'urn' according to RFC 2141, section 2.1"
 368  
             );
 369  
         }
 370  78
     }
 371  
 
 372  
     /**
 373  
      * Decode query part of the URN into Map.
 374  
      * @param urn The URN to demap
 375  
      * @return The map of values
 376  
      */
 377  
     private static Map<String, String> demap(final String urn) {
 378  20
         final Map<String, String> map = new TreeMap<String, String>();
 379  20
         final String[] sectors = StringUtils.split(urn, '?');
 380  20
         if (sectors.length == 2) {
 381  18
             final String[] parts = StringUtils.split(sectors[1], '&');
 382  83
             for (final String part : parts) {
 383  65
                 final String[] pair = StringUtils.split(part, '=');
 384  
                 final String value;
 385  65
                 if (pair.length == 2) {
 386  
                     try {
 387  5
                         value = URLDecoder.decode(pair[1], URN.ENCODING);
 388  0
                     } catch (final UnsupportedEncodingException ex) {
 389  0
                         throw new IllegalStateException(ex);
 390  5
                     }
 391  
                 } else {
 392  60
                     value = "";
 393  
                 }
 394  65
                 map.put(pair[0], value);
 395  
             }
 396  
         }
 397  20
         return map;
 398  
     }
 399  
 
 400  
     /**
 401  
      * Encode map of params into query part of URN.
 402  
      * @param params Map of params to convert to query suffix
 403  
      * @return The suffix of URN, starting with "?"
 404  
      */
 405  
     private static String enmap(final Map<String, String> params) {
 406  18
         final StringBuilder query = new StringBuilder(Tv.HUNDRED);
 407  18
         if (!params.isEmpty()) {
 408  18
             query.append('?');
 409  18
             boolean first = true;
 410  18
             for (final Map.Entry<String, String> param : params.entrySet()) {
 411  77
                 if (!first) {
 412  59
                     query.append('&');
 413  
                 }
 414  77
                 query.append(param.getKey());
 415  77
                 if (!param.getValue().isEmpty()) {
 416  3
                     query.append('=').append(URN.encode(param.getValue()));
 417  
                 }
 418  77
                 first = false;
 419  77
             }
 420  
         }
 421  18
         return query.toString();
 422  
     }
 423  
 
 424  
     /**
 425  
      * Perform proper URL encoding with the text.
 426  
      * @param text The text to encode
 427  
      * @return The encoded text
 428  
      */
 429  
     private static String encode(final String text) {
 430  
         final byte[] bytes;
 431  
         try {
 432  9
             bytes = text.getBytes(URN.ENCODING);
 433  0
         } catch (final UnsupportedEncodingException ex) {
 434  0
             throw new IllegalStateException(ex);
 435  9
         }
 436  9
         final StringBuilder encoded = new StringBuilder(Tv.HUNDRED);
 437  192
         for (final byte chr : bytes) {
 438  183
             if (URN.allowed(chr)) {
 439  137
                 encoded.append((char) chr);
 440  
             } else {
 441  46
                 encoded.append('%').append(String.format("%X", chr));
 442  
             }
 443  
         }
 444  9
         return encoded.toString();
 445  
     }
 446  
 
 447  
     /**
 448  
      * This char is allowed in URN's NSS part?
 449  
      * @param chr The character
 450  
      * @return It is allowed?
 451  
      */
 452  
     private static boolean allowed(final byte chr) {
 453  
         // @checkstyle BooleanExpressionComplexity (4 lines)
 454  183
         return chr >= 'A' && chr <= 'Z'
 455  
             || chr >= '0' && chr <= '9'
 456  
             || chr >= 'a' && chr <= 'z'
 457  
             || (chr == '/') || chr == '-';
 458  
     }
 459  
 
 460  
 }