blob: 607c4b4e67d476511d4772cc8778e40550a58b51 [file] [log] [blame]
package org.apache.velocity.tools.config;
/*
* 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.
*/
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.converters.BooleanConverter;
import org.apache.commons.beanutils.converters.StringConverter;
import org.apache.velocity.tools.ClassUtils;
import org.apache.velocity.tools.ConversionUtils;
/**
* <p>This class represents configured data. If added to a
* {@link FactoryConfiguration}, its values will be made
* available in the application-scoped toolboxes
* produced by any ToolboxFactory configured using
* that configuration.</p>
* <p>This class also implements all the functionality of
* {@link Property}s, which may added to <strong>any</strong>
* {@link Configuration} subclass, including
* {@link ToolConfiguration}, {@link ToolboxConfiguration},
* and {@link FactoryConfiguration}. In other words,
* anything you can do in a {@link Data} configuration, you
* can do with a {@link Property}.</p>
* <p>Some features supported here are:</p>
* <ul>
* <li>built in {@link Type}s for strings, booleans, numbers, fields
* and lists thereof</li>
* <li>auto-conversion of numbers, booleans and fields in data
* with no explicit type</li>
* <li>support for any Commons-BeanUtils {@link Converter} implementation</li>
* </ul>
*
* @author Nathan Bubna
* @version $Id: Data.java 511959 2007-02-26 19:24:39Z nbubna $
*/
public class Data implements Comparable<Data>
{
protected static final Type DEFAULT_TYPE = Type.AUTO;
private String key;
private String typeValue;
private Object value;
private boolean isList;
private Class target;
private Converter converter;
public Data()
{
setType(DEFAULT_TYPE);
}
public void setKey(String key)
{
this.key = key;
}
public void setValue(Object value)
{
this.value = value;
}
public void setClassname(String classname)
{
try
{
setTargetClass(ClassUtils.getClass(classname));
}
catch (ClassNotFoundException cnfe)
{
throw new IllegalArgumentException("Class "+classname+" could not be found.", cnfe);
}
}
/**
* This doesn't take a {@link Class} parameter because
* this class was not created for all-java configuration.
* @param classname class name
*/
public void setClass(String classname)
{
setClassname(classname);
}
protected void setType(Type type)
{
this.isList = type.isList();
// make sure we don't override a custom target or converter
if (!type.isCustom())
{
this.typeValue = type.value();
this.target = type.getTarget();
this.converter = type.getConverter();
}
}
public void setType(String t)
{
// save the set type value (good for error feedback and whatnot)
this.typeValue = t;
// and try to convert it to a Type
Type type = Type.get(this.typeValue);
if (type != null)
{
setType(type);
}
}
public void setTargetClass(Class clazz)
{
this.target = clazz;
}
public void setConverter(Class clazz)
{
try
{
convertWith((Converter)clazz.newInstance());
}
catch (Exception e)
{
throw new IllegalArgumentException("Class "+clazz+" is not a valid "+Converter.class, e);
}
}
public void setConverter(String classname)
{
try
{
convertWith((Converter)ClassUtils.getInstance(classname));
}
catch (Exception e)
{
throw new IllegalArgumentException("Class "+classname+" is not a valid "+Converter.class, e);
}
}
/**
* This is a convenience method for those doing configuration in java.
* It cannot be named setConverter(), or else it would confuse BeanUtils.
* @param converter value converter
*/
public void convertWith(Converter converter)
{
this.converter = converter;
}
public String getKey()
{
return this.key;
}
public String getType()
{
return this.typeValue;
}
public Object getValue()
{
return this.value;
}
public Class getTargetClass()
{
return this.target;
}
public Converter getConverter()
{
return this.converter;
}
public Object getConvertedValue()
{
return convert(this.value);
}
public void validate()
{
// make sure the key is not null
if (getKey() == null)
{
throw new NullKeyException(this);
}
// make sure we have value and that it can be converted
if (getValue() == null)
{
throw new ConfigurationException(this, "No value has been set for '"+getKey()+'\'');
}
else if (this.converter != null)
{
try
{
if (getConvertedValue() == null && getValue() != null)
{
throw new ConfigurationException(this, "Conversion of "+getValue()+" for '"+getKey()+"' failed and returned null");
}
}
catch (Throwable t)
{
throw new ConfigurationException(this, t);
}
}
}
public int compareTo(Data datum)
{
if (getKey() == null && datum.getKey() == null)
{
return 0;
}
else if (getKey() == null)
{
return -1;
}
else if (datum.getKey() == null)
{
return 1;
}
else
{
return getKey().compareTo(datum.getKey());
}
}
@Override
public int hashCode()
{
if (getKey() == null)
{
return super.hashCode();
}
return getKey().hashCode();
}
@Override
public boolean equals(Object obj)
{
if (getKey() == null || !(obj instanceof Data))
{
return super.equals(obj);
}
return getKey().equals(((Data)obj).getKey());
}
@Override
public String toString()
{
StringBuilder out = new StringBuilder();
out.append("Data '");
out.append(key);
out.append('\'');
out.append(" -");
out.append(this.typeValue);
out.append("-> ");
out.append(value);
return out.toString();
}
protected Object convert(Object value)
{
if (this.isList)
{
return convertList(value);
}
else if (this.converter == null)
{
return value;
}
else
{
return convertValue(value);
}
}
private Object convertValue(Object value)
{
return this.converter.convert(this.target, value);
}
private List convertList(Object val)
{
// we assume it is a string
String value = (String)val;
if (value == null || value.trim().length() == 0)
{
return null;
}
else
{
List<String> list = Arrays.asList(value.split(","));
if (this.converter == null || this.target.equals(String.class))
{
return list;
}
else
{
List convertedList = new ArrayList();
for (String item : list)
{
convertedList.add(convertValue(item));
}
return convertedList;
}
}
}
// ------------- Subclasses -----------------
/**
* Delineates the standard, known types and their
* associated target classes ({@link #setTargetClass} and
* converters ({@link #setConverter}).
*/
protected static enum Type
{
AUTO(Object.class, new AutoConverter()),
BOOLEAN(Boolean.class, new BooleanConverter()),
CUSTOM(null, null),
FIELD(Object.class, new FieldConverter()),
NUMBER(Number.class, new NumberConverter()),
STRING(String.class, new StringConverter()),
LIST(Object.class, null, true),
LIST_AUTO(Object.class, AUTO.getConverter(), true),
LIST_BOOLEAN(Boolean.class, BOOLEAN.getConverter(), true),
LIST_FIELD(Object.class, FIELD.getConverter(), true),
LIST_NUMBER(Number.class, NUMBER.getConverter(), true),
LIST_STRING(String.class, STRING.getConverter(), true);
private boolean isList;
private Class target;
private Converter converter;
Type(Class t, Converter c)
{
this(t, c, false);
}
Type(Class t, Converter c, boolean lst)
{
this.target = t;
this.converter = c;
this.isList = lst;
}
public boolean isCustom()
{
// custom ones require the user to provide the converter
return (this.target == null);
}
public boolean isList()
{
// all list types return lists
return isList;
}
public Class getTarget()
{
return this.target;
}
public Converter getConverter()
{
return this.converter;
}
public String value()
{
// make 'LIST_AUTO' into 'list.auto'
return name().replace('_','.').toLowerCase();
}
public static Type get(String type)
{
if (type == null || type.length() == 0)
{
return CUSTOM;
}
// make 'list.auto' eq 'LIST_AUTO'
return valueOf(type.replace('.','_').toUpperCase());
}
}
protected static class FieldConverter implements Converter
{
public Object convert(Class type, Object value)
{
String fieldpath = (String)value;
try
{
return ClassUtils.getFieldValue(fieldpath);
}
catch (Exception e)
{
throw new IllegalArgumentException("Could not retrieve value for field at "+fieldpath, e);
}
}
}
protected static class AutoConverter implements Converter
{
public Object convert(Class type, Object obj)
{
// only bother with strings for now
if (obj instanceof String)
{
try
{
return convert((String)obj);
}
catch (Exception e)
{
return obj;
}
}
return obj;
}
public Object convert(String value)
{
// check if this looks like a typical boolean type
if (value.matches("true|false|yes|no|y|n|on|off"))
{
return Type.BOOLEAN.getConverter().convert(Boolean.class, value);
}
// check if this looks like a typical number
else if (value.matches("-?[0-9]+(\\.[0-9]+)?"))
{
return Type.NUMBER.getConverter().convert(Number.class, value);
}
// check if this looks like a typical field
else if (value.matches("(\\w+\\.)+\\w+"))
{
return Type.FIELD.getConverter().convert(Object.class, value);
}
return value;
}
}
protected static class NumberConverter implements Converter
{
public Object convert(Class type, Object obj)
{
Number num = ConversionUtils.toNumber(obj,"default",Locale.US);
if (num == null)
{
throw new IllegalArgumentException("Could not convert "+obj+" to a number");
}
// now, let's return integers for integer values
else if (obj.toString().indexOf('.') < 0)
{
// unless, of course, we need a long
if (num.doubleValue() > Integer.MAX_VALUE ||
num.doubleValue() < Integer.MIN_VALUE)
{
num = Long.valueOf(num.longValue());
}
else
{
num = Integer.valueOf(num.intValue());
}
}
return num;
}
}
}