package org.marketcetera.core;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.net.URL;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
import org.marketcetera.util.log.SLF4JLoggerProxy;
import org.marketcetera.util.ws.tags.AppId;
/**
* Collection of random utilities.
*
* @author Toli Kuznets
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: Util.java 16841 2014-02-20 19:59:04Z colin $
*/
@ClassVersion("$Id: Util.java 16841 2014-02-20 19:59:04Z colin $")
public class Util
{
/** Tries to load the named file from a classpath
* If the file name doesn't start with a leading / then
* if we fail to load it we prepend the / and try again
* @param inFileName
* @param inObj some object to get the classloader from
* @return The URL to the resource or NULL if it's not found
*/
public static URL loadFileFromClasspath(String inFileName, Object inObj)
{
if((inFileName == null) || ("".equals(inFileName))) return null; //$NON-NLS-1$
URL resource = inObj.getClass().getResource(inFileName);
if((resource == null) && !inFileName.startsWith("/")) { //$NON-NLS-1$
resource = inObj.getClass().getResource("/"+inFileName); //$NON-NLS-1$
}
return resource;
}
/**
* Reads the entire file and stuffs it into a StringBuffer and returns the string
* The file is loaded from classpath.
* Use wisely, this will choke on very large files.
*
* @param inFileName
* @param inObj some object to get the classloader from
* @return The entire contents of the file
*/
public static String getStringFromFile(String inFileName, Object inObj) throws Exception
{
URL url = Util.loadFileFromClasspath(inFileName, inObj);
BufferedReader reader = new BufferedReader(new FileReader(new File(url.getPath())));
String line = null;
StringBuffer result = new StringBuffer(2000);
while((line = reader.readLine()) != null) {
result.append(line);
}
reader.close();
return result.toString();
}
/**
* the character used to prevent a delimiter or separator from being interpreted literally
*/
public static final String ESCAPE_CHARACTER = "\\"; //$NON-NLS-1$
/**
* the version of {@link #ESCAPE_CHARACTER} to use within a key or value
*/
public static final String ESCAPED_ESCAPE_CHARACTER = ESCAPE_CHARACTER + ESCAPE_CHARACTER;
/**
* the delimiter used to distinguish key/value pairs in the string representation of properties
*/
public static final String KEY_VALUE_DELIMITER = ":"; //$NON-NLS-1$
/**
* the version of {@link #KEY_VALUE_DELIMITER} to use within a key or value
*/
public static final String ESCAPED_KEY_VALUE_DELIMITER = ESCAPE_CHARACTER + KEY_VALUE_DELIMITER;
/**
* the separator used to separate key/value pairs in the string representation of properties
*/
public static final String KEY_VALUE_SEPARATOR = "="; //$NON-NLS-1$
/**
* the version of {@link #KEY_VALUE_SEPARATOR} to use within a key or value
*/
public static final String ESCAPED_KEY_VALUE_SEPARATOR = ESCAPE_CHARACTER + KEY_VALUE_SEPARATOR;
/**
* base value used to replace a delimiter or separator in a key or value
*/
private static final String PROCESSING_TOKEN = "$TOKEN-%s$"; //$NON-NLS-1$
/**
* Creates a <code>Properties</code> object from the given <code>String</code>.
*
* <p>This function assumes that the <code>String</code> consists of a series of key/value pairs separated by
* the {@link #KEY_VALUE_DELIMITER}. The key/value pairs themselves are separated by the {@link #KEY_VALUE_SEPARATOR}.
* Any malformed entries are discarded. A best-effort will be made to retain as many key/value pairs as possible.
*
* <p>If either the key or value contains {@link #ESCAPE_CHARACTER}, {@link #KEY_VALUE_DELIMITER}, or {@link #KEY_VALUE_SEPARATOR},
* these characters must be escaped using the {@link #ESCAPE_CHARACTER}.
*
* @param inCondensedProperties a <code>String</code> value
* @return a <code>Properties</code> value or null if <code>inCondensedProperties</code> is null or empty
*/
public static final Properties propertiesFromString(String inCondensedProperties)
{
if(inCondensedProperties == null ||
inCondensedProperties.isEmpty()) {
return null;
}
// first, replace any escaped escape characters with a token ("\" -> "\\", e.g., path=c:\value -> path=c:$TOKEN-1$value)
Pair<String,String> processedForEscapeCharacter = tokenizeEscapedDelimiters(inCondensedProperties,
ESCAPED_ESCAPE_CHARACTER);
// next, replace any key/value delimiters with another token (":" -> "\:", e.g. path=c:$TOKEN-1$value -> path=c$TOKEN-2$$TOKEN-1$value)
Pair<String,String> processedForKeyValueDelimiter = tokenizeEscapedDelimiters(processedForEscapeCharacter.getFirstMember(),
ESCAPED_KEY_VALUE_DELIMITER);
// the result has now replaced any instances of the escaped key value delimiter with a token that will not
// be noticed when we split the properties string
String[] statements = processedForKeyValueDelimiter.getFirstMember().split(KEY_VALUE_DELIMITER);
Properties props = new Properties();
// each statement should be "x=y"
for(String statement : statements) {
// the statement may contain a token which stands for an escaped key value delimiter or an escaped escape character
// replace them with the intended unescaped character, this is what the user intended
statement = untokenizeEscapedDelimiters(statement,
processedForKeyValueDelimiter.getSecondMember(),
KEY_VALUE_DELIMITER);
Pair<String,String> processedForKeyValueSeparator = tokenizeEscapedDelimiters(statement,
ESCAPED_KEY_VALUE_SEPARATOR);
String processedKeyValueSeparatorProperties = processedForKeyValueSeparator.getFirstMember();
String keyValueSeparatorToken = processedForKeyValueSeparator.getSecondMember();
if(processedKeyValueSeparatorProperties.contains(KEY_VALUE_SEPARATOR)) {
String[] subStatements = processedKeyValueSeparatorProperties.split(KEY_VALUE_SEPARATOR);
if(subStatements != null &&
(subStatements.length >= 1 &&
subStatements.length <= 2)) {
String key = untokenizeEscapedDelimiters(subStatements[0],
processedForEscapeCharacter.getSecondMember(),
ESCAPE_CHARACTER);
String value = ""; //$NON-NLS-1$
if(subStatements.length == 2) {
value = untokenizeEscapedDelimiters(subStatements[1],
processedForEscapeCharacter.getSecondMember(),
ESCAPE_CHARACTER);
}
key = untokenizeEscapedDelimiters(key,
keyValueSeparatorToken,
KEY_VALUE_SEPARATOR);
value = untokenizeEscapedDelimiters(value,
keyValueSeparatorToken,
KEY_VALUE_SEPARATOR);
props.setProperty(key,
value);
} else {
SLF4JLoggerProxy.debug(Util.class,
"Putative key/value \"{}\" discarded", //$NON-NLS-1$
(subStatements == null ? "null" : Arrays.toString(subStatements))); //$NON-NLS-1$
}
} else {
SLF4JLoggerProxy.debug(Util.class,
"Putative key/value \"{}\" discarded because it does not contain '{}'", //$NON-NLS-1$
processedKeyValueSeparatorProperties,
KEY_VALUE_SEPARATOR);
}
}
return props;
}
/**
* Takes the given source and replaces the given escaped delimiter with an arbitrary token guaranteed to not already exist
* in the given source.
*
* <p>The token chosen to replace the escaped delimiter will not otherwise exist in the source. All occurrences
* of the escaped delimiter will be replaced by the same token.
*
* <p>The returned value is the tuple of the processed source (escaped delimiter replaced by token) and the actual
* token used to replace the escaped delimiter. Non-escaped delimiters will not be replaced. If the source does
* not contain the escaped delimiter, the source is returned unmodified. The second member of the returned tuple
* is a generated token that may be ignored.
*
* @param inSource a <code>String</code> value containing the source to be processed
* @param inEscapedDelimiter a <code>String</code> value containing the escaped delimiter to be removed
* @return a <code>Pair<String,String></code> value containing the tuple of the processed source and the token used
*/
private static Pair<String,String> tokenizeEscapedDelimiters(String inSource,
String inEscapedDelimiter)
{
String generatedToken = String.format(PROCESSING_TOKEN,
counter.incrementAndGet());
String result = inSource;
// create a token that is guaranteed not to be in the source
while(inSource.contains(generatedToken)) {
generatedToken = String.format(PROCESSING_TOKEN,
counter.incrementAndGet());
}
result = inSource.replace(inEscapedDelimiter,
generatedToken);
return new Pair<String,String>(result,
generatedToken);
}
/**
* Takes the given source and replaces the given token with the given delimiter.
*
* <p>If the source does not contain the given token, the source will be returned unmodified.
*
* @param inSource a <code>String</code> value containing source to be untokenized
* @param inToken a <code>String</code> value containing the token to replace
* @param inDelimiter a <code>String</code> value containing the value to replace the tokens with
* @return
*/
private static String untokenizeEscapedDelimiters(String inSource,
String inToken,
String inDelimiter)
{
return inSource.replace(inToken,
inDelimiter);
}
/**
* Creates a <code>String</code> object from the given <code>Properties</code> object.
*
* <p>This function returns a <code>String</code> containing a series of key/value pairs representing this object.
* Each key/value pair is separated by the {@link #KEY_VALUE_DELIMITER}. The pairs themselves are separated by
* {@link #KEY_VALUE_SEPARATOR}.
*
* <p>Note that if any of the keys or values of the <code>Properties</code> object contains any of
* {@link #KEY_VALUE_DELIMITER}, {@link #KEY_VALUE_SEPARATOR}, or {@link #ESCAPE_CHARACTER} character,
* the resulting String will have these values escaped with the {@link #ESCAPE_CHARACTER}.
*
* @param inProperties a <code>Properties</code> value
* @return a <code>String</code> value or null if <code>inProperties</code> is null or empty
*/
public static String propertiesToString(Properties inProperties)
{
if(inProperties == null ||
inProperties.isEmpty()) {
return null;
}
StringBuffer output = new StringBuffer();
boolean delimiterNeeded = false;
for(Object keyObject : inProperties.keySet()) {
if(delimiterNeeded) {
output.append(KEY_VALUE_DELIMITER);
} else {
delimiterNeeded = true;
}
String key = (String)keyObject;
String value = (String)inProperties.getProperty(key);
// escape ESCAPE_CHARACTER, KEY_VALUE_DELIMITER, and KEY_VALUE_SEPARATOR
String escapedKey = untokenizeEscapedDelimiters(key,
ESCAPE_CHARACTER,
ESCAPED_ESCAPE_CHARACTER);
escapedKey = untokenizeEscapedDelimiters(escapedKey,
KEY_VALUE_DELIMITER,
ESCAPED_KEY_VALUE_DELIMITER);
escapedKey = untokenizeEscapedDelimiters(escapedKey,
KEY_VALUE_SEPARATOR,
ESCAPED_KEY_VALUE_SEPARATOR);
String escapedValue = untokenizeEscapedDelimiters(value,
ESCAPE_CHARACTER,
ESCAPED_ESCAPE_CHARACTER);
escapedValue = untokenizeEscapedDelimiters(escapedValue,
KEY_VALUE_DELIMITER,
ESCAPED_KEY_VALUE_DELIMITER);
escapedValue = untokenizeEscapedDelimiters(escapedValue,
KEY_VALUE_SEPARATOR,
ESCAPED_KEY_VALUE_SEPARATOR);
output.append(escapedKey).append(KEY_VALUE_SEPARATOR).append(escapedValue);
}
return output.toString();
}
/**
* Returns the name portion of the given application ID.
*
* @param id The application ID. It may be null.
* @return The name portion. It may be null if the provided ID
* lacks a name.
*/
public static String getName(AppId id) {
if ((id == null) || (id.getValue() == null)) {
return null;
}
int index = id.getValue().indexOf(APP_ID_VERSION_SEPARATOR);
String value = index == -1
? id.getValue()
: id.getValue().substring(0, index);
return value.isEmpty()
? null
: value;
}
/**
* Returns the version portion of the given application ID.
*
* @param id The application ID. It may be null.
*
* @return The version portion. It may be null if the provided ID
* lacks a version.
*/
public static String getVersion(AppId id)
{
if ((id==null) || (id.getValue()==null)) {
return null;
}
int index=id.getValue().indexOf(APP_ID_VERSION_SEPARATOR);
if (index==-1) {
return null;
}
index++;
if (index>=id.getValue().length()) {
return null;
}
return id.getValue().substring(index);
}
/**
* Returns an AppId, given the app name and version.
*
* @param inName the application name.
* @param inVersion the application version.
*
* @return the application version instance.
*/
public static AppId getAppId(String inName, String inVersion)
{
return new AppId(inName + APP_ID_VERSION_SEPARATOR + inVersion);
}
/**
* The the version separator used to separate Application Name and Version
* number in application IDs.
*/
private static final String APP_ID_VERSION_SEPARATOR="/"; //$NON-NLS-1$
/**
* counter used to guarantee unique tokens
*/
private static final AtomicLong counter = new AtomicLong(0);
}