netbout-spi-1.5.6.src.main.java.com.netbout.spi.Urn Maven / Gradle / Ivy
/**
* Copyright (c) 2009-2012, Netbout.com
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met: 1) Redistributions of source code must retain the above
* copyright notice, this list of conditions and the following
* disclaimer. 2) Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution. 3) Neither the name of the NetBout.com nor
* the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.netbout.spi;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang.CharEncoding;
import org.apache.commons.lang.StringUtils;
/**
* Universal resource locator (URN).
*
* The class is immutable and thread-safe.
*
* @author Yegor Bugayenko ([email protected])
* @version $Id: Urn.java 3579 2012-11-06 13:58:15Z guard $
* @see RFC2141
*/
@SuppressWarnings({ "PMD.TooManyMethods", "PMD.UseConcurrentHashMap" })
public final class Urn implements Comparable, Serializable {
/**
* Serialization marker.
*/
private static final long serialVersionUID = 0x4243AFCD9812ABDCL;
/**
* Marker of an empty URN.
*/
private static final String EMPTY = "void";
/**
* The prefix.
*/
private static final String PREFIX = "urn";
/**
* The separator.
*/
private static final String SEP = ":";
/**
* Validating regular expr.
*/
private static final String REGEX =
// @checkstyle LineLength (1 line)
"^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})*)?)*)?\\*?$";
/**
* The URI.
*/
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
private final URI uri;
/**
* Public ctor, for JAXB mostly.
*/
public Urn() {
this(Urn.EMPTY, "");
}
/**
* Public ctor.
* @param text The text of the URN
* @throws URISyntaxException If syntax is not correct
*/
public Urn(final String text) throws URISyntaxException {
if (text == null) {
throw new IllegalArgumentException("Text can't be NULL");
}
if (!text.matches(Urn.REGEX)) {
throw new URISyntaxException(text, "Invalid format of URN");
}
this.uri = new URI(text);
this.validate();
}
/**
* Public ctor.
* @param nid The namespace ID
* @param nss The namespace specific string
*/
public Urn(final String nid, final String nss) {
if (nid == null) {
throw new IllegalArgumentException("NID can't be NULL");
}
if (nss == null) {
throw new IllegalArgumentException("NSS can't be NULL");
}
this.uri = URI.create(
String.format(
"%s%s%s%2$s%s",
Urn.PREFIX,
Urn.SEP,
nid,
Urn.encode(nss)
)
);
try {
this.validate();
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
}
/**
* Static ctor.
* @param text The text of the URN
* @return The URN
*/
public static Urn create(final String text) {
try {
return new Urn(text);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return this.uri.toString();
}
/**
* {@inheritDoc}
*/
@Override
public int compareTo(final Urn urn) {
return this.uri.compareTo(urn.uri);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(final Object obj) {
boolean equals = false;
if (obj == this) {
equals = true;
} else if (obj instanceof Urn) {
equals = this.uri.equals(((Urn) obj).uri);
} else if (obj instanceof String) {
equals = this.uri.toString().equals((String) obj);
} else if (obj instanceof URI) {
equals = this.uri.equals((URI) obj);
}
return equals;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.uri.hashCode();
}
/**
* Is it URN?
* @param text The text to validate
* @return Yes of no
*/
public static boolean isValid(final String text) {
boolean valid = true;
try {
new Urn(text);
} catch (URISyntaxException ex) {
valid = false;
}
return valid;
}
/**
* Does it match the pattern?
* @param pattern The pattern to match
* @return Yes of no
*/
public boolean matches(final Urn pattern) {
boolean matches = false;
if (this.equals(pattern)) {
matches = true;
} else if (pattern.toString().endsWith("*")) {
final String body = pattern.toString().substring(
0, pattern.toString().length() - 1
);
matches = this.uri.toString().startsWith(body);
}
return matches;
}
/**
* Does it match the pattern?
* @param pattern The pattern to match
* @return Yes of no
*/
public boolean matches(final String pattern) {
return this.matches(Urn.create(pattern));
}
/**
* Is it empty?
* @return Yes of no
*/
public boolean isEmpty() {
return Urn.EMPTY.equals(this.nid());
}
/**
* Convert it to URI.
* @return The URI
*/
public URI toURI() {
return URI.create(this.uri.toString());
}
/**
* Get namespace ID.
* @return Namespace ID
*/
public String nid() {
return this.segment(1);
}
/**
* Get namespace specific string.
* @return Namespace specific string
*/
public String nss() {
try {
return URLDecoder.decode(this.segment(2), CharEncoding.UTF_8);
} catch (java.io.UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
/**
* Get all params.
* @return The params
*/
public Map params() {
return Urn.demap(this.toString());
}
/**
* Get query param by name.
* @param name Name of parameter
* @return The value of it
*/
public String param(final String name) {
final Map params = this.params();
if (!params.containsKey(name)) {
throw new IllegalArgumentException(
String.format(
"Param '%s' not found in '%s', among %s",
name,
this,
params.keySet()
)
);
}
return params.get(name);
}
/**
* Add query param and return new URN.
* @param name Name of parameter
* @param value The value of parameter
* @return New URN
*/
public Urn param(final String name, final Object value) {
final Map params = this.params();
params.put(name, value.toString());
return Urn.create(
String.format(
"%s%s",
StringUtils.split(this.toString(), '?')[0],
Urn.enmap(params)
)
);
}
/**
* Get just body of URN, without params.
* @return Clean version of it
*/
public Urn pure() {
String urn = this.toString();
if (this.hasParams()) {
// @checkstyle MultipleStringLiterals (1 line)
urn = urn.substring(0, urn.indexOf('?'));
}
return Urn.create(urn);
}
/**
* Whether this URN has params?
* @return Has them?
*/
public boolean hasParams() {
// @checkstyle MultipleStringLiterals (1 line)
return this.toString().contains("?");
}
/**
* Get segment by position.
* @param pos Its position
* @return The segment
*/
private String segment(final int pos) {
return StringUtils.splitPreserveAllTokens(
this.uri.toString(),
Urn.SEP,
// @checkstyle MagicNumber (1 line)
3
)[pos];
}
/**
* Validate URN.
* @throws URISyntaxException If it's not valid
*/
private void validate() throws URISyntaxException {
if (this.isEmpty() && !this.nss().isEmpty()) {
throw new URISyntaxException(
this.toString(),
"Empty URN can't have NSS"
);
}
if (!this.nid().matches("^[a-z]{1,31}$")) {
throw new IllegalArgumentException(
String.format(
"NID '%s' can contain up to 31 low case letters",
this.nid()
)
);
}
}
/**
* Decode query part of the URN into Map.
* @param urn The URN to demap
* @return The map of values
*/
private static Map demap(final String urn) {
final Map map = new TreeMap();
final String[] sectors = StringUtils.split(urn, '?');
if (sectors.length == 2) {
final String[] parts = StringUtils.split(sectors[1], '&');
for (String part : parts) {
final String[] pair = StringUtils.split(part, '=');
String value;
if (pair.length == 2) {
try {
value = URLDecoder.decode(pair[1], CharEncoding.UTF_8);
} catch (java.io.UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
} else {
value = "";
}
map.put(pair[0], value);
}
}
return map;
}
/**
* Encode map of params into query part of URN.
* @param params Map of params to convert to query suffix
* @return The suffix of URN, starting with "?"
*/
private static String enmap(final Map params) {
final StringBuilder query = new StringBuilder();
if (!params.isEmpty()) {
query.append("?");
boolean first = true;
for (Map.Entry param : params.entrySet()) {
if (!first) {
query.append("&");
}
query.append(param.getKey());
if (!param.getValue().isEmpty()) {
query.append("=").append(Urn.encode(param.getValue()));
}
first = false;
}
}
return query.toString();
}
/**
* Perform proper URL encoding with the text.
* @param text The text to encode
* @return The encoded text
*/
private static String encode(final String text) {
final StringBuilder encoded = new StringBuilder();
byte[] bytes;
try {
bytes = text.getBytes(CharEncoding.UTF_8);
} catch (java.io.UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
for (byte chr : bytes) {
if (Urn.allowed(chr)) {
encoded.append((char) chr);
} else {
encoded.append("%").append(String.format("%X", chr));
}
}
return encoded.toString();
}
/**
* This char is allowed in URN's NSS part?
* @param chr The character
* @return It is allowed?
*/
private static boolean allowed(final byte chr) {
// @checkstyle BooleanExpressionComplexity (4 lines)
return (chr >= 'A' && chr <= 'Z')
|| (chr >= '0' && chr <= '9')
|| (chr >= 'a' && chr <= 'z')
|| (chr == '/') || (chr == '-');
}
}