/**
* Copyright (C) 2009-2015 Dell, Inc.
* See annotations for authorship information
*
* ====================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*/
package org.dasein.cloud.util;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Random;
import java.util.logging.Logger;
/**
* Implements naming conventions for a type of resource within a given cloud. This method enables a client
* both to learn about the naming conventions for different types of resources in a cloud as well as make names
* that conform to those naming conventions.
* <p>Created by George Reese: 3/4/14 9:31 AM</p>
* @author George Reese
* @version 2014.03 initial version (issue #134 and #121)
* @since 2014.03
*/
public class NamingConstraints {
/**
* Indicates case restrictions on alphabetic components.
*/
static public enum Case {
/**
* Names must contain only lower case letters
*/
LOWER,
/**
* Names must contain only upper case letters
*/
UPPER,
/**
* Mixed case letters are allowed
*/
MIXED;
/**
* Converts a name so that it matches the case aligned with the constraints behind this enum.
* @param baseName the raw name to be converted
* @param locale the locale for which case is to be applied
* @return a string converted to conform with the constraints of this enum instance
*/
public @Nonnull String convert(@Nonnull String baseName, @Nonnull Locale locale) {
switch( this ) {
case LOWER: return baseName.toLowerCase(locale);
case UPPER: return baseName.toUpperCase(locale);
default: return baseName;
}
}
}
/**
* Constructs a baseline set of naming conventions that supports only alphabetic characters. The resulting
* naming conventions will be mixed case, full unicode spectrum, allowing spaces and symbols. The first character
* allowed must be alphabetic.
* @param minLength the minimum length of a valid name
* @param maxLength the maximum length of a valid name (will be adjusted to min length if less than min length)
* @return naming conventions that match the characteristics described above
*/
static public @Nonnull NamingConstraints getAlphaOnly(@Nonnegative int minLength, @Nonnegative int maxLength) {
NamingConstraints n = new NamingConstraints();
n.alpha = true;
n.alphaCase = Case.MIXED;
n.latin1Constrained = false;
n.minimumLength = minLength;
n.maximumLength = maxLength;
if( n.maximumLength < n.minimumLength ) {
n.maximumLength = n.minimumLength;
}
n.numeric = false;
n.spaces = true;
n.symbolConstraints = null;
n.symbols = true;
n.firstCharacterNumericAllowed = false;
n.firstCharacterSymbolAllowed = false;
return n;
}
/**
* Constructs a baseline set of naming conventions for full alphanumeric support. The resulting
* naming conventions will be mixed case, full unicode spectrum, allowing numbers, spaces, and symbols. The first
* character allowed must be a letter.
* @param minLength the minimum length of a valid name
* @param maxLength the maximum length of a valid name (will be adjusted to min length if less than min length)
* @return naming conventions that match the characteristics described above
*/
static public @Nonnull NamingConstraints getAlphaNumeric(@Nonnegative int minLength, @Nonnegative int maxLength) {
NamingConstraints n = new NamingConstraints();
n.alpha = true;
n.alphaCase = Case.MIXED;
n.latin1Constrained = false;
n.minimumLength = minLength;
n.maximumLength = maxLength;
if( n.maximumLength < n.minimumLength ) {
n.maximumLength = n.minimumLength;
}
n.numeric = true;
n.spaces = true;
n.symbolConstraints = null;
n.symbols = true;
n.firstCharacterNumericAllowed = false;
n.firstCharacterSymbolAllowed = false;
return n;
}
/**
* Provides a convenient set of naming constraints for naming hosts on a network.
* @param forWindowsNetwork true if you are targeting a windows host, false otherwise
* @return naming constraints supporting host names
*/
static public @Nonnull NamingConstraints getHostNameInstance(boolean forWindowsNetwork) {
NamingConstraints n = new NamingConstraints();
n.minimumLength = 3;
n.maximumLength = (forWindowsNetwork ? 15 : 30);
n.alpha = true;
n.alphaCase = (forWindowsNetwork ? Case.UPPER : Case.MIXED);
n.latin1Constrained = true;
n.numeric = true;
n.spaces = false;
n.symbols = !forWindowsNetwork;
n.symbolConstraints = (forWindowsNetwork ? null : new char[] { '-' });
n.firstCharacterNumericAllowed = false;
n.firstCharacterSymbolAllowed = false;
return n;
}
/**
* Constructs a baseline set of naming conventions that match strict naming often seen in old file systems and programming
* languages. The resulting conventions will be alphanumeric lower-case and latin-1 only. The first
* character allowed must be a letter.
* @param minimumLength the minimum length of a valid name
* @param maximumLength the maximum length of a valid name (will be adjusted to min length if less than min length)
* @return naming conventions that match the characteristics described above
*/
static public @Nonnull NamingConstraints getStrictInstance(@Nonnegative int minimumLength, @Nonnegative int maximumLength) {
NamingConstraints n = new NamingConstraints();
n.alpha = true;
n.alphaCase = Case.LOWER;
n.latin1Constrained = true;
n.minimumLength = minimumLength;
n.maximumLength = maximumLength;
if( n.maximumLength < n.minimumLength ) {
n.maximumLength = n.minimumLength;
}
n.numeric = true;
n.spaces = false;
n.symbolConstraints = null;
n.symbols = false;
n.firstCharacterNumericAllowed = false;
n.firstCharacterSymbolAllowed = false;
return n;
}
private boolean alpha;
private Case alphaCase;
private boolean firstCharacterNumericAllowed;
private boolean firstCharacterSymbolAllowed;
private boolean latin1Constrained;
private int maximumLength;
private int minimumLength;
private boolean numeric;
private boolean spaces;
private char[] symbolConstraints;
private boolean symbols;
private String regularExpression;
private boolean lastCharacterSymbolAllowed;
private String description;
private NamingConstraints() { }
/**
* Alters the naming conventions so that any support for symbols is specifically limited to a certain set of
* symbols.
* @param symbols the valid symbols for names conforming to these conventions
* @return this
*/
public @Nonnull
NamingConstraints constrainedBy(@Nonnull char ... symbols) {
symbolConstraints = symbols;
this.symbols = true;
return this;
}
/**
* Converts a raw name into a valid name supported by these naming conventions. If the raw name already
* conforms, it will be returned as-is. If it is too short, random characters will be appended to make it
* conform to minimum length requirements. If it is too long, it will be truncated. It will also be
* converted to the appropriate case with any invalid characters being removed.
* @param baseName the raw name to be converted
* @param locale the locale according to which conversion rules will be applied
* @return a valid name based on the base name conforming to these naming conventions
*/
public @Nullable String convertToValidName(@Nonnull String baseName, @Nonnull Locale locale) {
StringBuilder str = new StringBuilder();
int i = 0;
baseName = alphaCase.convert(baseName.trim(), locale);
for( char c : baseName.toCharArray() ) {
if( str.length() == maximumLength ) {
return str.toString();
}
if( isValid(c, i) ) {
str.append(c);
}
else if( c == ' ' && i != 0 ) {
c = getValidSymbol('_', '-');
if( c != (char)0 ) {
str.append(c);
}
}
i++;
}
if( str.length() < 1 ) {
return null;
}
boolean dashed = false;
while( str.length() < minimumLength ) {
if( !dashed ) {
dashed = true;
char c = getValidSymbol('-', '_');
if( c != (char)0 ) {
str.append(c);
}
}
else {
str.append(getRandomCharacter(true, str.length()));
}
}
if( str.length() > maximumLength ) {
return str.toString().substring(0, maximumLength);
}
char[] nameArray = str.toString().toCharArray();
if (lastCharacterSymbolAllowed == false) {
while (!Character.isLetterOrDigit(nameArray[str.length() - 1])) {
str.deleteCharAt(str.length() - 1);
}
}
if (null != regularExpression) {
if (!str.toString().matches(regularExpression)) {
Logger logger = Logger.getLogger("" + NamingConstraints.class) ;
logger.warning("WARNING: regularExpression fails to validate cleaned name. NAME=" + str.toString() + " regularExpression=" + regularExpression);
}
}
return str.toString();
}
/**
* @return the case constraints that names supported by these naming conventions must conform
*/
public @Nonnull Case getAlphaCase() {
return alphaCase;
}
/**
* @return the maximum number of characters allowed in names supported by these naming conventions
*/
public @Nonnegative int getMaximumLength() {
return maximumLength;
}
/**
* @return the minimum number of characters allowed in names supported by these naming conventions
*/
public @Nonnegative int getMinimumLength() {
return minimumLength;
}
static private Random random = new Random();
/**
* Generates a random character that will conform to these naming conventions.
* @param alphanumericOnly the resulting character should be alphanumeric even if symbols and spaces are normally allowed
* @param forPosition the string position for which the character is being generated
* @return a random character conforming to these naming conventions
*/
public char getRandomCharacter(boolean alphanumericOnly, int forPosition) {
char c = (char)random.nextInt(128);
while( !isValid(c, forPosition) || (alphanumericOnly && !Character.isLetterOrDigit(c)) ) {
c = (char)random.nextInt(128);
}
return c;
}
/**
* @return a list of allowed symbols when {@link #isSymbols()} is true (<code>null</code> if any symbol is allowed)
*/
public @Nullable char[] getSymbolConstraints() {
if( !symbols ) {
return new char[0];
}
return symbolConstraints;
}
/**
* Provides a symbol conforming to these naming conventions based on a list of desired symbols. This method is
* useful, for example, in picking a symbol to replace a space. The first symbol in the list considered valid
* will be returned.
* @param fromList the list of symbols from which a valid symbol is being sought
* @return the first symbol in the list that is considered valid or <code>(char)0</code> if none are valid
*/
public char getValidSymbol(char ... fromList) {
if( !symbols ) {
return (char)0;
}
if( symbolConstraints == null ) {
return fromList[0];
}
for( char l : fromList ) {
for( char c : symbolConstraints ) {
if( c == l ) {
return l;
}
}
}
return (char)0;
}
/**
* A tool for unique name generators to generate unique names. A calling client will maintain a count of times it
* has called this method and then check if the result is unique in its namespace. If not unique, it will
* increment the count and call it again. It repeats this process until it finds a unique name. For example:
* <pre>
* String name = "somename";
*
* if( !isUnique(name) ) {
* int i = 1;
*
* String test = conventions.incrementName(name, Locale.getDefault(), i++);
*
* while( test != null && !isUnique(test) ) {
* test = conventions.increment(name, Locale.getDefault(), i++);
* }
* if( test == null ) {
* // CAN'T FIND A VALID, UNIQUE NAME!!!
* }
* else {
* name = test;
* }
* }
* </pre>
* @param baseName the original name that you hope is unique (this name is assumed to be valid already)
* @param count the call count for this call (1 is the first call)
* @return a name matching these naming conventions with a postfix that should hopefully make it unique
*/
public @Nullable String incrementName(@Nonnull String baseName, @Nonnegative int count) {
baseName = baseName.trim();
char[] alphabet;
if( numeric ) {
if( alpha ) {
if( alphaCase.equals(Case.MIXED) ) {
alphabet = new char[] {
'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
};
}
else if( alphaCase.equals(Case.LOWER) ) {
alphabet = new char[] {
'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
};
}
else {
alphabet = new char[] {
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
};
}
}
else {
alphabet = new char[] {
'0','1','2','3','4','5','6','7','8','9'
};
}
}
else if( !alpha ) {
return null;
}
else {
if( alphaCase.equals(Case.MIXED) ) {
alphabet = new char[] {
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
};
}
else if( alphaCase.equals(Case.LOWER) ) {
alphabet = new char[] {
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
};
}
else {
alphabet = new char[] {
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
};
}
}
StringBuilder postfix = new StringBuilder();
int div, mod;
div = count/alphabet.length;
mod = count%alphabet.length;
if( div < 1 ) {
postfix.append(alphabet[mod]);
}
else if( div == 1 ) {
postfix.append(alphabet[mod]);
postfix.append(alphabet[1]);
}
else {
while( div > 0 ) {
postfix.append(alphabet[mod]);
if( div == count ) {
postfix.append(alphabet[0]);
postfix.append(alphabet[1]);
div = 0;
mod = 0;
}
else {
count = div;
div = count/alphabet.length;
mod = count%alphabet.length;
}
}
if( mod > 0 ) {
postfix.append(alphabet[mod]);
}
}
if( spaces ) {
postfix.append(' ');
}
else {
char divider = getValidSymbol('-', '_');
if( divider != (char)0 ) {
postfix.append(divider);
}
}
String tmp = postfix.reverse().toString();
if( (baseName.length() + tmp.length()) > maximumLength ) {
if( tmp.length() >= maximumLength ) {
if( baseName.length() == 1 ) {
return null;
}
return incrementName(baseName.substring(0,baseName.length()-1), count);
}
else {
int cut = (baseName.length() + tmp.length()) - maximumLength;
baseName = baseName.substring(0, baseName.length() - cut);
}
}
return (baseName + tmp);
}
/**
* @return true if these naming conventions allow for letters
*/
public boolean isAlpha() {
return alpha;
}
/**
* @return true if the first character in a name may be a number
*/
public boolean isFirstCharacterNumericAllowed() {
return firstCharacterNumericAllowed;
}
/**
* @return true if the first character in a name is allowed to be a symbol (constrained by {@link #getSymbolConstraints()})
*/
public boolean isFirstCharacterSymbolAllowed() {
return firstCharacterSymbolAllowed;
}
/**
* @return true if the last character in a name is allowed to be a symbol
*/
public boolean isLastCharacterSymbolAllowed() { return lastCharacterSymbolAllowed; }
/**
* @return true if these naming conventions support only Latin 1 characters
*/
public boolean isLatin1Constrained() {
return latin1Constrained;
}
/**
* @return true if these naming conventions allow numbers in a name
*/
public boolean isNumeric() {
return numeric;
}
/**
* @return true if these naming conventions allow spaces in names (spaces are never allowed at the beginning or end of names)
*/
public boolean isSpaces() {
return spaces;
}
/**
* @return true if these naming conventions allow symbols in names (symbols may be constrained by {@link #getSymbolConstraints()})
*/
public boolean isSymbols() {
return symbols;
}
/**
* Validates the specified character against these naming conventions and returns true if it is valid in the specified
* position.
* @param c the character to be tested
* @param position the position in the name
* @return true if the character is valid according to these naming conventions for the specified position
*/
public boolean isValid(char c, int position) {
if( latin1Constrained && ((int)c) > 255 ) {
return false;
}
if( Character.isLetter(c) ) {
if( !alpha ) {
return false;
}
if( alphaCase.equals(Case.MIXED) ) {
return true;
}
else if( alphaCase.equals(Case.LOWER) ) {
return Character.isLowerCase(c);
}
return !Character.isLowerCase(c);
}
else if( Character.isDigit(c) ) {
return (numeric && (position > 0 || firstCharacterNumericAllowed));
}
if( Character.isSpaceChar(c) ) {
return (position > 0 && spaces);
}
if( position == 0 && !firstCharacterSymbolAllowed ) {
return false;
}
if( symbolConstraints != null ) {
for( char option : symbolConstraints ) {
if( c == option ) {
return true;
}
}
return false;
}
return true;
}
/**
* Checks if the specified name is valid according to these naming constraints.
* @param name the name to be checked
* @return true if the name is a valid name
*/
public boolean isValidName(@Nonnull String name) {
int len = name.length();
if( len < minimumLength || len > maximumLength ) {
return false;
}
int i = 0;
for( char c : name.toCharArray() ) {
if( !isValid(c, i++) ) {
return false;
}
}
return true;
}
/**
* Constrains these naming conventions to the latin 1 character set.
* @return this
*/
public @Nonnull
NamingConstraints limitedToLatin1() {
latin1Constrained = true;
return this;
}
/**
* Constrains the letters supported by these naming conventions to lower case letters.
* @return this
*/
public @Nonnull
NamingConstraints lowerCaseOnly() {
alphaCase = Case.LOWER;
return this;
}
/**
* Constrains the letters supported by these naming conventions to upper case letters.
* @return this
*/
public @Nonnull
NamingConstraints upperCaseOnly() {
alphaCase = Case.UPPER;
return this;
}
/**
* Disallows spaces in names conforming to these naming conventions.
* @return this
*/
public @Nonnull
NamingConstraints withNoSpaces() {
spaces = false;
return this;
}
/**
* Disallows spaces in names conforming to these naming conventions.
* @return this
*/
public @Nonnull
NamingConstraints withNoSymbols() {
symbols = false;
return this;
}
public @Nonnull NamingConstraints withRegularExpression(String regularExpression){
this.regularExpression = regularExpression;
return this;
}
public @Nullable String getRegularExpression(){
return regularExpression;
}
public @Nonnull NamingConstraints withLastCharacterSymbolAllowed(boolean lastCharacterSymbolAllowed){
this.lastCharacterSymbolAllowed = lastCharacterSymbolAllowed;
return this;
}
public @Nonnull NamingConstraints withDescription(String description){
this.description = description;
return this;
}
public String getDescription(){
return description;
}
}