| DescriptorBuilder.java |
/*
* $Id: DescriptorBuilder.java,v 1.33 2012/05/12 15:14:47 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.common.service;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.StringTokenizer;
import org.xins.common.Log;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.collections.InvalidPropertyValueException;
import org.xins.common.collections.MapStringUtils;
import org.xins.common.collections.MissingRequiredPropertyException;
/**
* Builder that can build a <code>Descriptor</code> object based on a set of
* properties.
*
* <h3>Examples</h3>
*
* <p>The following example is the definition of a single back-end at
* <code>http://somehost/</code>, identified by the property name
* <code>"s1"</code>, the time-out is set to 20 seconds:
*
* <blockquote><code>s1=service, http://somehost/, 20000</code></blockquote>
*
* <p>The next example is the definition of 4 back-ends, of which one will be
* chosen randomly. This setting is identified by the property name
* <code>"capi.sso"</code>:
*
* <blockquote><code># The root definition "capi.sso"
* <br>capi.sso=group, random, target1, target2, target3, target4
* <br>
* <br># Total time-out is 12.5 seconds, no connection time-out and no socket
* <br># time-out
* <br>capi.sso.target1=service, http://somehost/, 12500
* <br>
* <br># Total time-out is 12.5 seconds, connection time-out is 4 seconds and
* <br># no socket time-out
* <br>capi.sso.target2=service, http://othrhost/, 12500, 4000
* <br>
* <br># Total time-out is 12.5 seconds, connection time-out is 4 seconds,
* <br># socket time-out is 2 seconds
* <br>capi.sso.target3=service, http://othrhost:2001/, 12500, 4000, 2000
* <br>
* <br># Total time-out is not set, connection time-out is not set and socket
* <br># time-out is 2 seconds
* <br>capi.sso.target4=service, http://othrhost:2002/, 0, 0,
* 2000</code></blockquote>
*
* <p>The last example defines 2 back-ends at a more preferred location and 1
* at a less-preferred location. Normally one of the 2 back-ends at the
* preferred location will be chosen randomly, but if none is available, then
* the back-end at the less preferred location will be tried. The time-out for
* all back-ends in 8 seconds. The name of the property is <code>"ldap"</code>:
*
* <blockquote><code>ldap=group, ordered, loc1, host2a
* <br>ldap.loc1=group, random, host1a, host1b
* <br>ldap.host1a=service, ldap://host1a/, 8000
* <br>ldap.host1b=service, ldap://host1b/, 8000
* <br>ldap.host2a=service, ldap://host2a/, 8000</code></blockquote>
*
* @version $Revision: 1.33 $ $Date: 2012/05/12 15:14:47 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
*
* @since XINS 1.0.0
*/
public final class DescriptorBuilder {
/**
* Delimiter between tokens within a property value. This is the comma
* character <code>','</code>.
*/
public static final char DELIMITER = ',';
/**
* Delimiter between property lines. This is the carriage return
* character <code>'\n'</code>.
*/
public static final char LINE_DELIMITER = '\n';
/**
* Delimiters between tokens within a property value.
*/
private static final String DELIMITER_AS_STRING =
String.valueOf(DELIMITER);
/**
* Name identifying an actual target descriptor.
*/
public static final String TARGET_DESCRIPTOR_TYPE = "service";
/**
* Name identifying a group of descriptors.
*/
public static final String GROUP_DESCRIPTOR_TYPE = "group";
/**
* Constructs a new <code>DescriptorBuilder</code>.
*/
private DescriptorBuilder() {
// empty
// NOTE: No tracing is performed, since this constructor is never used
}
/**
* Tokenizes the specified string. The {@link #DELIMITER_AS_STRING} will be
* used as the token delimiter. Every token will be one element in the
* returned {@link String} array.
*
* @param s
* the {@link String} to tokenize, cannot be <code>null</code>.
*
* @return
* the list of tokens as a {@link String} array, never
* <code>null</code>.
*/
private static String[] tokenize(String s) {
// Create a StringTokenizer
StringTokenizer tokenizer = new StringTokenizer(s, DELIMITER_AS_STRING);
// Create a new array to store the tokens in
int count = tokenizer.countTokens();
String[] tokens = new String[count];
// Copy all tokens into the array
for (int i = 0; i < count; i++) {
tokens[i] = tokenizer.nextToken().trim();
}
return tokens;
}
/**
* Builds a <code>Descriptor</code> based on the specified set of
* properties, for the specified service caller.
*
* @param caller
* the caller to create a {@link Descriptor} for, can be
* <code>null</code> if unknown.
*
* @param properties
* the properties to read from, cannot be <code>null</code>.
*
* @param propertyName
* the base for the property names, cannot be <code>null</code>.
*
* @return
* the {@link Descriptor} that was built, never <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>properties == null || propertyName == null</code>.
*
* @throws MissingRequiredPropertyException
* if the property named <code>propertyName</code> cannot be found in
* <code>properties</code>, or if a referenced property cannot be found.
*
* @throws InvalidPropertyValueException
* if the property named <code>propertyName</code> is found in
* <code>properties</code>, but the format of this property or the
* format of a referenced property is invalid.
*
* @since XINS 1.2.0
*/
public static Descriptor build(ServiceCaller caller,
Map<String, String> properties,
String propertyName)
throws IllegalArgumentException,
MissingRequiredPropertyException,
InvalidPropertyValueException {
// Check preconditions
MandatoryArgumentChecker.check("properties", properties,
"propertyName", propertyName);
return build(caller, properties, propertyName, null);
}
/**
* Builds a <code>Descriptor</code> based on the specified set of
* properties.
*
* @param properties
* the properties to read from, cannot be <code>null</code>.
*
* @param propertyName
* the base for the property names, cannot be <code>null</code>.
*
* @return
* the {@link Descriptor} that was built, never <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>properties == null || propertyName == null</code>.
*
* @throws MissingRequiredPropertyException
* if the property named <code>propertyName</code> cannot be found in
* <code>properties</code>, or if a referenced property cannot be found.
*
* @throws InvalidPropertyValueException
* if the property named <code>propertyName</code> is found in
* <code>properties</code>, but the format of this property or the
* format of a referenced property is invalid.
*/
public static Descriptor build(Map<String, String> properties,
String propertyName)
throws IllegalArgumentException,
MissingRequiredPropertyException,
InvalidPropertyValueException {
// Check preconditions
MandatoryArgumentChecker.check("properties", properties,
"propertyName", propertyName);
return build((ServiceCaller) null, properties, propertyName);
}
/**
* Builds a <code>Descriptor</code> based on the specified value.
*
* @param descriptorValue
* the value of the descriptor, cannot be <code>null</code>.
* the value must have the same value as specified at the top, the lines
* should be separated with '\n' and the first line must start with
* the name of the property followed by the sign '='.
*
* @return
* the {@link Descriptor} that was built, never <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>descriptorValue == null</code> or the property name cannot
* be found in the value.
*
* @throws MissingRequiredPropertyException
* if the property named <code>propertyName</code> cannot be found in
* <code>properties</code>, or if a referenced property cannot be found.
*
* @throws InvalidPropertyValueException
* if the property named <code>propertyName</code> is found in
* <code>properties</code>, but the format of this property or the
* format of a referenced property is invalid.
*/
public static Descriptor build(String descriptorValue)
throws IllegalArgumentException,
MissingRequiredPropertyException,
InvalidPropertyValueException {
// Check preconditions
MandatoryArgumentChecker.check("descriptorValue", descriptorValue);
int equalsPos = descriptorValue.indexOf('=');
int crPos = descriptorValue.indexOf(LINE_DELIMITER);
if (equalsPos <= 0 || (crPos > 0 && equalsPos > crPos)) {
throw new IllegalArgumentException(
"No property name found in \"" + descriptorValue + "\".");
}
String propertyName = descriptorValue.substring(0, equalsPos);
final String ENCODING = "ISO-8859-1";
try {
byte[] bytes = descriptorValue.getBytes(ENCODING);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Map<String, String> properties = MapStringUtils.createMapString(bais);
bais.close();
return build((ServiceCaller) null, properties, propertyName);
} catch (IOException ioe) {
throw new InvalidPropertyValueException(propertyName, descriptorValue);
}
}
/**
* Builds a <code>Descriptor</code> based on the specified set of
* properties, specifying base property and reference.
*
* @param caller
* the service caller to build a descriptor for, or <code>null</code> if
* unknown.
*
* @param properties
* the properties to read from, should not be <code>null</code>.
*
* @param baseProperty
* the name of the base property, should not be <code>null</code>.
*
* @param reference
* the name of the reference, relative to the base property, can be
* <code>null</code>.
*
* @return
* the {@link Descriptor} that was built, never <code>null</code>.
*
* @throws NullPointerException
* if <code>properties == null</code>.
*
* @throws MissingRequiredPropertyException
* if a required property cannot be found.
*
* @throws InvalidPropertyValueException
* if the property named <code>propertyName</code> is found in
* <code>properties</code>, but the format of this property or the
* format of a referenced property is invalid.
*/
private static Descriptor build(ServiceCaller caller,
Map<String, String> properties,
String baseProperty,
String reference)
throws NullPointerException,
MissingRequiredPropertyException,
InvalidPropertyValueException {
// Determine the property name
String propertyName = reference == null
? baseProperty
: baseProperty + '.' + reference;
// Get the value of the property
String value = properties.get(propertyName);
if (value == null) {
throw new MissingRequiredPropertyException(propertyName);
}
// Tokenize the value
String[] tokens = tokenize(value);
int tokenCount = tokens.length;
if (tokenCount < 3) {
throw new InvalidPropertyValueException(propertyName, value, "Expected at least 3 tokens.");
}
// Determine the type
String descriptorType = tokens[0];
// Parse target descriptor
if (TARGET_DESCRIPTOR_TYPE.equals(descriptorType)) {
if (tokenCount > 5) {
throw new InvalidPropertyValueException(propertyName, value, "Expected URL and time-out.");
}
// Determine URL
String url = tokens[1];
// Determine the total time-out (mandatory)
int timeOut;
try {
timeOut = Integer.parseInt(tokens[2]);
} catch (NumberFormatException nfe) {
throw new InvalidPropertyValueException(propertyName, value, "Unable to parse total time-out as a 32-bit integer number.");
}
if (timeOut < 0) {
throw new InvalidPropertyValueException(propertyName, value, "Total time-out is negative.");
}
// Determine the connection time-out (optional)
int connectionTimeOut;
if (tokenCount > 3) {
try {
connectionTimeOut = Integer.parseInt(tokens[3]);
} catch (NumberFormatException nfe) {
throw new InvalidPropertyValueException(propertyName, value, "Unable to parse connection time-out as a 32-bit integer number.");
}
if (connectionTimeOut < 0) {
throw new InvalidPropertyValueException(propertyName, value, "Connection time-out is negative.");
}
} else {
connectionTimeOut = 0;
}
// Determine the socket time-out (optional)
int socketTimeOut;
if (tokenCount > 4) {
try {
socketTimeOut = Integer.parseInt(tokens[4]);
} catch (NumberFormatException nfe) {
throw new InvalidPropertyValueException(propertyName, value, "Unable to parse socket time-out as a 32-bit integer number.");
}
if (socketTimeOut < 0) {
throw new InvalidPropertyValueException(propertyName, value, "Socket time-out is negative.");
}
} else {
socketTimeOut = 0;
}
// Construct a TargetDescriptor instance
TargetDescriptor td;
try {
td = new TargetDescriptor(url, timeOut, connectionTimeOut, socketTimeOut);
} catch (MalformedURLException exception) {
Log.log_1300(exception, url);
throw new InvalidPropertyValueException(propertyName, value, "Malformed URL.");
}
// Test the protocol
if (caller != null) {
try {
caller.testTargetDescriptor(td);
} catch (UnsupportedProtocolException cause) {
Log.log_1308(url);
InvalidPropertyValueException exception = new InvalidPropertyValueException(propertyName, value, "Unsupported protocol.");
exception.initCause(cause);
throw exception;
}
}
return td;
// Parse group descriptor
} else if (GROUP_DESCRIPTOR_TYPE.equals(descriptorType)) {
GroupDescriptor.Type groupType = GroupDescriptor.getType(tokens[1]);
if (groupType == null) {
throw new InvalidPropertyValueException(propertyName, value, "Unrecognized group descriptor type \"" + tokens[1] + "\".");
}
int memberCount = tokenCount - 2;
if (memberCount < 2) {
throw new InvalidPropertyValueException(propertyName, value, "Group descriptor member count is " + memberCount + ", while minimum is 2.");
}
Descriptor[] members = new Descriptor[memberCount];
for (int i = 0; i < memberCount; i++) {
members[i] = build(caller, properties, baseProperty, tokens[i + 2]);
}
return new GroupDescriptor(groupType, members);
// Unrecognized descriptor type
} else {
throw new InvalidPropertyValueException(propertyName, value, "Expected valid descriptor type: either \"" + TARGET_DESCRIPTOR_TYPE + "\" or \"" + GROUP_DESCRIPTOR_TYPE + "\".");
}
}
}