/**
* Copyright (c) 2012 Cloudsmith Inc. and other contributors, as listed below.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Cloudsmith
*
*/
package org.cloudsmith.geppetto.forge.v2.model;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
* A qualified name that is case insensitive and also separator insensitive when performing comparisons
* and hash code calculations. The instance does however preserve both case and the separator. The
* created instance is immutable and suitable for use as key in hash tables and trees.
*/
public class ModuleName implements Serializable, Comparable<ModuleName> {
public static class BadNameCharactersException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
private static final String STRICT_MSG = "name should only contain lowercase letters, numbers, and underscores, and should begin with a letter";
private static final String LENIENT_MSG = "name should only contain letters, numbers, and underscores, and should begin with a letter";
public BadNameCharactersException(boolean strict) {
super(strict
? STRICT_MSG
: LENIENT_MSG);
}
}
public static class BadNameSyntaxException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
private static final String MSG = "name should be in the form <owner>-<name> or <owner>/<name>";
public BadNameSyntaxException() {
super(MSG);
}
}
public static class BadOwnerCharactersException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
private static final String STRICT_MSG = "owner should only contain letters and numbers, and should begin with a letter";
private static final String LENIENT_MSG = "owner should only contain letters and numbers";
public BadOwnerCharactersException(boolean strict) {
super(strict
? STRICT_MSG
: LENIENT_MSG);
}
}
public static class JsonAdapter implements JsonDeserializer<ModuleName>, JsonSerializer<ModuleName> {
@Override
public ModuleName deserialize(JsonElement json, java.lang.reflect.Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
String name = json.getAsString();
String owner = null;
int sepIdx = name.indexOf('/');
if(sepIdx < 0)
sepIdx = name.indexOf('-');
if(sepIdx >= 0) {
owner = ModuleName.safeOwner(name.substring(0, sepIdx));
name = ModuleName.safeName(name.substring(sepIdx + 1), false);
}
else {
name = ModuleName.safeName(name, false);
}
return new ModuleName(owner, name, false);
}
@Override
public JsonElement serialize(ModuleName src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString());
}
}
private static final long serialVersionUID = 1L;
private static final String NO_VALUE = "";
private static final Pattern OWNER_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$");
private static final Pattern STRICT_OWNER_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*$");
private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_-]*$");
/**
* <p>
* Checks that the given name only contains lowercase letters, numbers and underscores and that it begins with a
* letter. This check is valid for the name part of a full module name.
* </p>
* Certain module names are disallowed:
* <ul>
* <li>main</li>
* <li>settings</li>
* </ul>
*
* @param name
* The name to check
* @param strict
* <code>true</code> means do not allow uppercase letters or dash
* @return The checked name
* @throws BadNameCharactersException
* if the contains illegal characters
*/
public static String checkName(String name, boolean strict) throws BadNameCharactersException {
Pattern p = strict
? STRICT_NAME_PATTERN
: NAME_PATTERN;
Matcher m = p.matcher(name);
if(m.matches()) {
if(name.equals("main") || name.equals("settings"))
throw new BadNameCharactersException(strict);
return name;
}
throw new BadNameCharactersException(strict);
}
/**
* Checks that the given name only contains letters and numbers. This is suitable for the <i>owner</i> part of a
* full module name.
*
* @param owner
* The name to check
* @param strict
* <code>true</code> means that name must start with a letter
* @return The checked owner
* @throws BadOwnerCharactersException
* if the contains illegal characters
*/
public static String checkOwner(String owner, boolean strict) throws BadOwnerCharactersException {
Pattern p = strict
? STRICT_OWNER_PATTERN
: OWNER_PATTERN;
Matcher m = p.matcher(owner);
if(m.matches())
return owner;
throw new BadOwnerCharactersException(strict);
}
private static final StringBuilder createBuilder(String from, int idx) {
StringBuilder bld = new StringBuilder(from.length());
for(int catchUp = 0; catchUp < idx; catchUp++)
bld.append(from.charAt(catchUp));
return bld;
}
/**
* Creates a "safe" name from the given name. The following
* happens:
* <ul>
* <li>If <code>strict</code> is <code>true</code>, then all uppercase characters in the range 'A' - 'Z' are
* lowercased</li>
* <li>All characters that are not underscore, digit, or in the range 'a' - 'z' is replaced with an underscore</li>
* <li>If an underscore or digit is found at the first position (after replacement), then it is replaced by the
* letter 'z'</li>
* </ul>
*
* @param name
* The name to convert. Can be <code>null</code> in which case <code>null</code>it is returned.
* @param strict
* <code>true</code> means do not allow uppercase letters or multiple separators
* @return The safe name or <code>null</code>.
*/
public static String safeName(String name, boolean strict) {
if(name == null)
return name;
int top = name.length();
StringBuilder bld = null;
for(int idx = 0; idx < top; ++idx) {
char c = name.charAt(idx);
// @fmtOff
if(!( c >= 'a' && c <= 'z'
|| !strict && (c == '-' || c >= 'A' && c <= 'Z')
|| idx > 0 && (c == '_' || c >= '0' && c <= '9'))) {
// @fmtOn
if(c >= 'A' && c <= 'Z')
c += 0x20;
else
c = idx == 0
? 'z'
: '_';
if(bld == null)
bld = createBuilder(name, idx);
}
if(bld != null)
bld.append(c);
}
return bld == null
? name
: bld.toString();
}
/**
* Creates a "safe" owner name from the given name. All characters that are a digit or a letter is
* replaced with a 'z'.
*
* @param owner
* The name to convert. Can be <code>null</code> in which case <code>null</code>it is returned.
* @return The safe name or <code>null</code>.
*/
public static String safeOwner(String owner) {
if(owner == null)
return owner;
int top = owner.length();
StringBuilder bld = null;
for(int idx = 0; idx < top; ++idx) {
char c = owner.charAt(idx);
if(!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9')) {
c = 'z';
if(bld == null)
bld = createBuilder(owner, idx);
}
if(bld != null)
bld.append(c);
}
return bld == null
? owner
: bld.toString();
}
/**
* <p>
* Splits the <code>moduleName into two parts. The owner and the name. This method performs no validation
* of the names.
* </p>
* <p>
* The separator may be either '/' or '-' and if more than one separator is present, then one placed first wins.
* Thus<br/>
* "foo-bar-baz" yields owner = "foo", name = "bar-baz", separator '-'<br/>
* "foo/bar-baz" yields owner = "foo", name = "bar-baz", separator '/'<br/>
* "foo/bar/baz" yields owner = "foo", name = "bar/baz", separator '/'<br/>
* "foo-bar/baz" yields owner = "foo", name = "bar/baz", separator '-'<br/>
* </p>
* In case no separator is found, owner will be considered missing and the argument is returned as the
* second element.</p>
*
* @param moduleName
* @return A two element array with the owner and name of the module. The first element in this array may be
* <code>null</code> .
* @see #checkOwner(String)
* @see #checkName(String, boolean)
*/
public static String[] splitName(String moduleName) {
String owner = null;
String name;
int sepIdx = moduleName.indexOf('/');
if(sepIdx < 0)
sepIdx = moduleName.indexOf('-');
if(sepIdx >= 0) {
owner = moduleName.substring(0, sepIdx);
name = moduleName.substring(sepIdx + 1);
}
else
name = moduleName;
return new String[] { owner, name };
}
private final char separator;
private final String owner;
private final String name;
private final String semanticName;
private static final Pattern STRICT_NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9_]*$");
private ModuleName(ModuleName m, char separator) {
this.separator = separator;
this.owner = m.owner;
this.name = m.name;
this.semanticName = m.semanticName;
}
/**
* Creates a name from a string with a separator. This is a equivalent to {@link #ModuleName(String, boolean)
* ModuleName(fullName, false)}
*
* @param fullName
* The name to set
* @throws BadOwnerCharactersException
* @throws BadNameCharactersException
* @throws BadNameSyntaxException
*/
public ModuleName(String fullName) throws BadNameSyntaxException, BadNameCharactersException,
BadOwnerCharactersException {
this(fullName, false);
}
/**
* <p>
* Creates a name from a string with a separator.
* </p>
* <p>
* The separator may be either '/' or '-' and if more than one separator is present, then one placed first wins.
* Thus<br/>
* "foo-bar-baz" yields owner = "foo", name = "bar-baz", separator '-'<br/>
* "foo/bar-baz" yields owner = "foo", name = "bar-baz", separator '/'<br/>
* "foo/bar/baz" yields owner = "foo", name = "bar/baz", separator '/'<br/>
* "foo-bar/baz" yields owner = "foo", name = "bar/baz", separator '-'<br/>
* </p>
*
* @param fullName
* The name to set
* @param strict
* <code>true</code> means do not allow <code>owner</code> that starts with a digit or uppercase letters
* or dash in the <code>name</code>
* @throws BadNameCharactersException
* @throws BadOwnerCharactersException
* @throws BadNameSyntaxException
*/
public ModuleName(String fullName, boolean strict) throws BadNameSyntaxException, BadNameCharactersException,
BadOwnerCharactersException {
int idx = fullName.indexOf('/');
if(idx > 0)
separator = '/';
else {
idx = fullName.indexOf('-');
separator = '-';
}
if(!(idx > 0 && idx < fullName.length() - 1))
throw new BadNameSyntaxException();
this.owner = checkOwner(fullName.substring(0, idx), strict);
this.name = checkName(fullName.substring(idx + 1), strict);
String semName = createSemanticName();
if(semName.equals(fullName))
semName = fullName; // Don't waste string instance here. This will be the common case
this.semanticName = semName;
}
/**
* Creates a name using specified owner, name, and separator.
*
* @param owner
* @param separator
* @param name
* @param strict
* <code>true</code> means do not allow <code>owner</code> that starts with a digit or uppercase letters
* or dash in the <code>name</code>
* @throws BadNameCharactersException
* @throws BadOwnerCharactersException
* @throws BadNameSyntaxException
*/
public ModuleName(String owner, char separator, String name, boolean strict) throws BadNameSyntaxException,
BadOwnerCharactersException, BadNameCharactersException {
this.owner = owner == null
? NO_VALUE
: checkOwner(owner, strict);
if(!(separator == '-' || separator == '/'))
throw new BadNameSyntaxException();
this.separator = separator;
this.name = name == null
? NO_VALUE
: checkName(name, strict);
this.semanticName = createSemanticName();
}
public ModuleName(String qualifier, String name, boolean strict) {
this(qualifier, '/', name, strict);
}
/**
* <p>
* Compare this name to <tt>other</tt> for lexical magnitude using case insensitive comparisons. The separator is
* considered but only after both owner and names are equal.
* </p>
*
* @param other
* The name to compare this name to.
* @return a positive integer to indicate that this name is lexicographically greater than <tt>other</tt>.
*/
@Override
public int compareTo(ModuleName other) {
int cmp = semanticName.compareTo(other.semanticName);
if(cmp == 0)
cmp = separator - other.separator;
return cmp;
}
private String createSemanticName() {
return owner.toLowerCase() + '/' + name.toLowerCase();
}
/**
* Compares the two names for equality. Names can have different separators or different case and still be equal.
*
* @return The result of the comparison.
*/
@Override
public boolean equals(Object o) {
if(this == o)
return true;
if(!(o instanceof ModuleName))
return false;
ModuleName qo = (ModuleName) o;
return semanticName.equals(qo.semanticName);
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the owner
*/
public String getOwner() {
return owner;
}
/**
* @return the separator
*/
public char getSeparator() {
return separator;
}
/**
* Computes the hash value for this qualified name. The separator is excluded from the computation
*
* @return The computed hash code.
*/
@Override
public int hashCode() {
return semanticName.hashCode();
}
/**
* Returns the string representation fo this instance.
*/
@Override
public String toString() {
StringBuilder bld = new StringBuilder();
toString(bld);
return bld.toString();
}
/**
* Present this object as a string onto the given builder.
*
* @param builder
*/
public void toString(StringBuilder builder) {
builder.append(owner);
builder.append(separator);
builder.append(name);
}
/**
* Returns an instance that is guaranteed to have the given
* separator. The returned instance might be this instance or
* a new instance depending on if this instance already has the
* given separator.
*
* @param separator
* @return A name with the given separator, possibly this instance
*/
public ModuleName withSeparator(char separator) {
return this.separator == separator
? this
: new ModuleName(this, separator);
}
}