blob: bea6208238aa1df1a07b9c05014dbb3683203453 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.util.text;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
public class Identifiers {
private static Random random = new Random();
public static final String UPPER_CASE_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String LOWER_CASE_ALPHA = "abcdefghijklmnopqrstuvwxyz";
public static final String NUMERIC = "1234567890";
public static final String NON_ALPHA_NUMERIC = "!@$%^&*()-_=+[]{};:\\|/?,.<>~";
/** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
public static final String JAVA_GOOD_START_CHARS = UPPER_CASE_ALPHA + LOWER_CASE_ALPHA +"_";
/** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
public static final String JAVA_GOOD_NONSTART_CHARS = JAVA_GOOD_START_CHARS+NUMERIC;
/** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
public static final String JAVA_GOOD_SEGMENT_REGEX = "["+JAVA_GOOD_START_CHARS+"]"+"["+JAVA_GOOD_NONSTART_CHARS+"]*";
/** regex for a java package or class name using "good" chars, that is no accents or funny unicodes.
* see http://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.8 for the full set supported by the spec;
* but it's rare to deviate from this subset and it causes problems when charsets aren't respected (tsk tsk but not uncommon!).
* our use cases so far only require testing for "good" names. */
public static final String JAVA_GOOD_PACKAGE_OR_CLASS_REGEX = "("+JAVA_GOOD_SEGMENT_REGEX+"\\."+")*"+JAVA_GOOD_SEGMENT_REGEX;
/** as {@link #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX} but allowing a dollar sign inside a class name (e.g. Foo$1) */
public static final String JAVA_GOOD_BINARY_REGEX = JAVA_GOOD_PACKAGE_OR_CLASS_REGEX+"(\\$["+JAVA_GOOD_NONSTART_CHARS+"]+)*";
public static final String JAVA_GENERATED_IDENTIFIER_START_CHARS = UPPER_CASE_ALPHA + LOWER_CASE_ALPHA;
public static final String JAVA_GENERATED_IDENTIFIERNONSTART_CHARS = JAVA_GENERATED_IDENTIFIER_START_CHARS+NUMERIC;
public static final String BASE64_VALID_CHARS = JAVA_GENERATED_IDENTIFIERNONSTART_CHARS+"+=";
public static final String ID_VALID_START_CHARS = UPPER_CASE_ALPHA + LOWER_CASE_ALPHA;
public static final String ID_VALID_NONSTART_CHARS = ID_VALID_START_CHARS+ NUMERIC;
public static final String PASSWORD_VALID_CHARS = NON_ALPHA_NUMERIC + ID_VALID_NONSTART_CHARS;
// We only create a secure random when it is first used
private static Random secureRandom = null;
/** makes a random id string (letters and numbers) of the given length;
* starts with letter (upper or lower) so can be used as java-id;
* tests ensure random distribution, so random ID of length 5
* is about 2^29 possibilities
* <p>
* With ID of length 4 it is not unlikely (15% chance) to get
* duplicates in the first 2000 attempts.
* With ID of length 8 there is 1% chance to get duplicates
* in the first 1M attempts and 50% for the first 16M.
* <p>
* implementation is efficient, uses char array, and
* makes one call to random per 5 chars; makeRandomId(5)
* takes about 4 times as long as a simple Math.random call,
* or about 50 times more than a simple x++ instruction;
* in other words, it's appropriate for contexts where random id's are needed,
* but use efficiently (ie cache it per object), and
* prefer to use a counter where feasible
* <p>
* in general this is preferable to base64 as is more portable,
* can be used throughout javascript (as ID's which don't allow +)
* or as java identifiers (which don't allow numbers in the first char)
**/
public static String makeRandomId(int l) {
//this version is 30-50% faster than the old double-based one,
//which computed a random every 3 turns --
//takes about 600 ns to do id of len 10, compared to 10000 ns for old version [on 1.6ghz machine]
if (l<=0) return "";
char[] id = new char[l];
int d = random.nextInt( (26+26) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10));
int i = 0;
id[i] = ID_VALID_START_CHARS.charAt(d % (26+26));
d /= (26+26);
if (++i<l) do {
id[i] = ID_VALID_NONSTART_CHARS.charAt(d%(26+26+10));
if (++i>=l) break;
if (i%5==0) {
d = random.nextInt( (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10));
} else {
d /= (26+26+10);
}
} while (true);
//Message.message("random id is " + id);
return new String(id);
}
/**
*
* @param length of password to be returned
* @return randomly generated password containing at least one of each upper case,
* lower case, numeric, and non alpha-numeric characters. Hopefully this is acceptible
* for most password schemes.
*/
public static String makeRandomPassword(final int length) {
return makeRandomPassword(length, UPPER_CASE_ALPHA, LOWER_CASE_ALPHA, NUMERIC, NON_ALPHA_NUMERIC, PASSWORD_VALID_CHARS);
}
/**
* A fairly slow but hopefully secure way to randomly select characters for a password
* Takes a pool of acceptible characters using the first set in the pool for the first character,
* second set for the second character, ..., nth set for all remaining character.
*
* @param length length of password
* @param passwordValidCharsPool pool of acceptable character sets
* @return a randomly generated password
*/
public static String makeRandomPassword(final int length, String... passwordValidCharsPool) {
Preconditions.checkState(length >= passwordValidCharsPool.length);
int l = 0;
Character[] password = new Character[length];
for(int i = 0; i < passwordValidCharsPool.length; i++){
password[l++] = pickRandomCharFrom(passwordValidCharsPool[i]);
}
String remainingValidChars = mergeCharacterSets(passwordValidCharsPool);
while(l < length) {
password[l++] = pickRandomCharFrom(remainingValidChars);
}
List<Character> list = Arrays.asList(password);
Collections.shuffle(list);
return Joiner.on("").join(list);
}
protected static String mergeCharacterSets(String... s) {
Set characters = new HashSet<Character>();
for (String characterSet : s) {
characters.addAll(Arrays.asList(characterSet.split("")));
}
return Joiner.on("").join(characters);
}
/** creates a short identifier comfortable in java and OS's, given an input hash code
* <p>
* result is always at least of length 1, shorter if the hash is smaller */
public static String makeIdFromHash(long d) {
StringBuffer result = new StringBuffer();
if (d<0) d=-d;
// correction for Long.MIN_VALUE
if (d<0) d=-(d+1000);
result.append(ID_VALID_START_CHARS.charAt((int)(d % (26+26))));
d /= (26+26);
while (d!=0) {
result.append(ID_VALID_NONSTART_CHARS.charAt((int)(d%(26+26+10))));
d /= (26+26+10);
}
return result.toString();
}
/** makes a random id string (letters and numbers) of the given length;
* starts with letter (upper or lower) so can be used as java-id;
* tests ensure random distribution, so random ID of length 5
* is about 2^29 possibilities
* <p>
* implementation is efficient, uses char array, and
* makes one call to random per 5 chars; makeRandomId(5)
* takes about 4 times as long as a simple Math.random call,
* or about 50 times more than a simple x++ instruction;
* in other words, it's appropriate for contexts where random id's are needed,
* but use efficiently (ie cache it per object), and
* prefer to use a counter where feasible
**/
public static String makeRandomJavaId(int l) {
// copied from Monterey util's com.cloudsoftcorp.util.StringUtils.
// TODO should share code with makeRandomId, just supplying different char sets (though the char sets in fact are the same..)
//this version is 30-50% faster than the old double-based one,
//which computed a random every 3 turns --
//takes about 600 ns to do id of len 10, compared to 10000 ns for old version [on 1.6ghz machine]
if (l<=0) return "";
char[] id = new char[l];
int d = random.nextInt( (26+26) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10));
int i = 0;
id[i] = JAVA_GENERATED_IDENTIFIER_START_CHARS.charAt(d % (26+26));
d /= (26+26);
if (++i<l) do {
id[i] = JAVA_GENERATED_IDENTIFIERNONSTART_CHARS.charAt(d%(26+26+10));
if (++i>=l) break;
if (i%5==0) {
d = random.nextInt( (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10));
} else {
d /= (26+26+10);
}
} while (true);
//Message.message("random id is " + id);
return new String(id);
}
public static double randomDouble() {
return random.nextDouble();
}
public static long randomLong() {
return random.nextLong();
}
public static boolean randomBoolean() {
return random.nextBoolean();
}
public static int randomInt() {
return random.nextInt();
}
/** returns in [0,upbound) */
public static int randomInt(int upbound) {
return random.nextInt(upbound);
}
/** returns the array passed in */
public static byte[] randomBytes(byte[] buf) {
random.nextBytes(buf);
return buf;
}
public static byte[] randomBytes(int length) {
byte[] buf = new byte[length];
return randomBytes(buf);
}
public static String makeRandomBase64Id(int length) {
StringBuilder s = new StringBuilder();
while (length>0) {
appendBase64IdFromValueOfLength(randomLong(), length>10 ? 10 : length, s);
length -= 10;
}
return s.toString();
}
public static String getBase64IdFromValue(long value) {
return getBase64IdFromValue(value, 10);
}
public static String getBase64IdFromValue(long value, int length) {
StringBuilder s = new StringBuilder();
appendBase64IdFromValueOfLength(value, length, s);
return s.toString();
}
public static void appendBase64IdFromValueOfLength(long value, int length, StringBuffer sb) {
if (length>11)
throw new IllegalArgumentException("can't get a Base64 string longer than 11 chars from a long");
long idx = value;
for (int i=0; i<length; i++) {
byte x = (byte)(idx & 63);
sb.append(BASE64_VALID_CHARS.charAt(x));
idx = idx >> 6;
}
}
public static void appendBase64IdFromValueOfLength(long value, int length, StringBuilder sb) {
if (length>11)
throw new IllegalArgumentException("can't get a Base64 string longer than 11 chars from a long");
long idx = value;
for (int i=0; i<length; i++) {
byte x = (byte)(idx & 63);
sb.append(BASE64_VALID_CHARS.charAt(x));
idx = idx >> 6;
}
}
public static boolean isValidToken(String token, String validStartChars, String validSubsequentChars) {
if (token==null || token.length()==0) return false;
if (validStartChars.indexOf(token.charAt(0))==-1) return false;
for (int i=1; i<token.length(); i++)
if (validSubsequentChars.indexOf(token.charAt(i))==-1) return false;
return true;
}
private static char pickRandomCharFrom(String validChars) {
return validChars.charAt(getSecureRandom().nextInt(validChars.length()));
}
private static Random getSecureRandom() {
if(secureRandom == null) {
secureRandom = new SecureRandom();
}
return secureRandom;
}
}