/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* 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 au.gov.ga.earthsci.worldwind.common.layers.styled;
import static au.gov.ga.earthsci.worldwind.common.util.Util.*;
import gov.nasa.worldwind.avlist.AVList;
import gov.nasa.worldwind.render.Material;
import gov.nasa.worldwind.util.Logging;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Point;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Generalised property setter. Defines a collection of properties that are set
* on an object using reflection.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class PropertySetter
{
protected final Map<String, String> properties = new HashMap<String, String>();
protected final Map<String, String[]> typeOverrides = new HashMap<String, String[]>();
/**
* Add a property that this setter will set.
*
* @param property
* Property name
* @param value
* Value to set the property to (can replace with attribute
* values by using a %attributeName% placeholder)
* @param typeOverride
* Type overrides to use when setting the property
*/
public void addProperty(String property, String value, String... typeOverrides)
{
properties.put(property, value);
if (typeOverrides != null && typeOverrides.length > 0)
{
this.typeOverrides.put(property, typeOverrides);
}
}
/**
* Add a property that this setter will set.
*
* @param property
* Property name
* @param value
* Value to set the property to (can replace with attribute
* values by using a %attributeName% placeholder)
* @param typeOverride
* Type to use when setting the property. Pipe separated lists
* supported.
*/
public void addProperty(String property, String value, String typeOverrides)
{
addProperty(property, value, splitPipeSeparatedString(typeOverrides));
}
/**
* Set the objects properties to the values in this setter. Iterates through
* each of the properties in this setter, searches for a matching setter
* method for the property, and if found, calls the setter with this
* object's property value. Can also insert values from the attributes
* themselves, by using the %attributeName% placeholder in the value string.
*
* @param context
* Layer's context url
* @param attributeValues
* Attribute values
* @param objects
* Objects to search for matching setter properties, using
* reflection
*/
public void setPropertiesFromAttributes(URL context, AVList attributeValues, Object... objects)
{
Map<String, Method> methods = new HashMap<String, Method>();
Map<Method, Object> methodToObject = new HashMap<Method, Object>();
//create a list of the methods in the objects
for (Object object : objects)
{
for (Method method : object.getClass().getMethods())
{
methods.put(method.getName(), method);
methodToObject.put(method, object);
}
}
//for each of the properties in this setter
for (Entry<String, String> entry : properties.entrySet())
{
//search for the setter method for this property
String property = entry.getKey();
String methodName = constructSetterName(property);
if (!methods.containsKey(methodName))
{
String message = "Could not find setter method '" + methodName + "' in class: ";
for (Object object : objects)
{
message += object.getClass() + ", ";
}
message = message.substring(0, message.length() - 2);
Logging.logger().warning(message);
continue;
}
//find out the method's parameters
Method setter = methods.get(methodName);
Object object = methodToObject.get(setter);
Class<?>[] parameters = setter.getParameterTypes();
//get the string value to pass to the method
String stringValue = entry.getValue();
stringValue = replaceVariablesWithAttributeValues(stringValue, attributeValues);
String[] paramValueStrings = splitPipeSeparatedString(stringValue);
if (parameters.length != paramValueStrings.length)
{
String message =
"Setter method '" + methodName + "' in class " + object.getClass() + " doesn't take "
+ paramValueStrings.length + " parameter(s)";
Logging.logger().severe(message);
// Continue on incase this is an overloaded method
continue;
}
Object[] parameterValues = new Object[paramValueStrings.length];
String[] typeOverrides = getTypeOverridesForProperty(property, parameterValues.length);
// Convert each parameter value string into a parameter
for (int i = 0; i < paramValueStrings.length; i++)
{
//find out the type to pass to the method
Class<?> parameterType = parameters[i];
Class<?> type = parameterType;
//check if the type has been overridden (useful if the type above is just 'Object')
String typeOverride = typeOverrides[i];
if (!isBlank(typeOverride))
{
type = convertTypeToClass(typeOverride);
if (type == null)
{
String message = "Could not find class for type " + type;
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
else if (!parameterType.isAssignableFrom(type))
{
String message =
"Setter method '" + methodName + "' in class " + object.getClass() + " parameter type "
+ parameterType + " not assignable from type " + type;
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
}
//convert the string value to a valid type
Object value = convertStringToType(context, paramValueStrings[i], type);
if (value == null)
{
String message = "Error converting '" + paramValueStrings[i] + "' to type " + type;
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
parameterValues[i] = value;
}
//invoke the setter with the value
try
{
setter.invoke(object, parameterValues);
}
catch (Exception e)
{
String message = "Error invoking '" + methodName + "' in class " + object.getClass() + ": " + e;
Logging.logger().severe(message);
throw new IllegalArgumentException(message, e);
}
}
}
/**
* @return the type overrides for the provided property, populated to ensure
* there are the correct number of overrides for the parameters of
* the property.
*/
private String[] getTypeOverridesForProperty(String property, int numberOfParameters)
{
String[] result = typeOverrides.get(property);
if (result == null)
{
return new String[numberOfParameters];
}
if (result.length == numberOfParameters)
{
return result;
}
String[] propertyOverrides = typeOverrides.get(property);
result = new String[numberOfParameters];
for (int i = 0; i < result.length; i++)
{
if (i < propertyOverrides.length)
{
result[i] = propertyOverrides[i];
}
else
{
result[i] = null;
}
}
return result;
}
private static String[] splitPipeSeparatedString(String stringValue)
{
// Split on '|' and trim whitespace at the same time
return stringValue.trim().split("[ \t]*[|][ \t]*");
}
private static String constructSetterName(String property)
{
return "set" + capitalizeFirstLetter(property);
}
/**
* Replaces attribute placeholders in a string with the attribute value
*
* @param string
* String to replace placeholders in
* @param attributesValues
* Attribute values
* @return Replaced string
*/
protected static String replaceVariablesWithAttributeValues(String string, AVList attributesValues)
{
if (attributesValues == null)
return string;
Pattern pattern = Pattern.compile("%[^%]+%");
Matcher matcher = pattern.matcher(string);
StringBuffer replacement = new StringBuffer();
int start = 0;
while (matcher.find(start))
{
replacement.append(string.substring(start, matcher.start()));
String attribute = matcher.group();
attribute = attribute.substring(1, attribute.length() - 1);
if (!attributesValues.hasKey(attribute))
{
String message = "Could not find attribute '" + attribute + "'";
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
String value = attributesValues.getValue(attribute).toString();
replacement.append(value);
start = matcher.end();
}
replacement.append(string.substring(start));
return replacement.toString();
}
/**
* Convert a type string to a class
*
* @param type
* @return Class represented by the type string
*/
protected static Class<?> convertTypeToClass(String type)
{
if ("String".equalsIgnoreCase(type))
return String.class;
if ("Integer".equalsIgnoreCase(type))
return Integer.class;
if ("Float".equalsIgnoreCase(type))
return Float.class;
if ("Long".equalsIgnoreCase(type))
return Long.class;
if ("Double".equalsIgnoreCase(type))
return Double.class;
if ("Character".equalsIgnoreCase(type))
return Character.class;
if ("Byte".equalsIgnoreCase(type))
return Byte.class;
if ("URL".equalsIgnoreCase(type))
return URL.class;
if ("File".equalsIgnoreCase(type))
return File.class;
if ("Color".equalsIgnoreCase(type))
return Color.class;
if ("Insets".equalsIgnoreCase(type))
return Insets.class;
if ("Dimension".equalsIgnoreCase(type))
return Dimension.class;
if ("Point".equalsIgnoreCase(type))
return Point.class;
if ("Font".equalsIgnoreCase(type))
return Font.class;
if ("Material".equalsIgnoreCase(type))
return Material.class;
if ("Boolean".equalsIgnoreCase(type))
return Boolean.class;
return null;
}
/**
* Convert a string to a certain type, parsing the string if required
*
* @param context
* If creating a URL, use this as the URL's context
* @param string
* String to convert
* @param type
* Type to convert to
* @return Converted string, or null if failed
*/
protected static Object convertStringToType(URL context, String string, Class<?> type)
{
try
{
if (type.isAssignableFrom(String.class))
{
return string;
}
else if (type.isAssignableFrom(Double.class) || type.isAssignableFrom(double.class))
{
return Double.valueOf(string);
}
else if (type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class))
{
return Integer.decode(string);
}
else if (type.isAssignableFrom(Float.class) || type.isAssignableFrom(float.class))
{
return Float.valueOf(string);
}
else if (type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class))
{
return Long.decode(string);
}
else if (type.isAssignableFrom(Character.class) || type.isAssignableFrom(char.class))
{
return string.charAt(0);
}
else if (type.isAssignableFrom(Byte.class) || type.isAssignableFrom(byte.class))
{
return Byte.decode(string);
}
else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class))
{
return Boolean.valueOf(string);
}
else if (type.isAssignableFrom(URL.class))
{
try
{
return new URL(context, string);
}
catch (MalformedURLException e)
{
}
}
else if (type.isAssignableFrom(File.class))
{
return new File(string);
}
else if (type.isAssignableFrom(Color.class))
{
int[] ints = splitInts(string);
if (ints.length == 1)
return new Color(ints[0]);
else if (ints.length == 3)
return new Color(ints[0], ints[1], ints[2]);
else if (ints.length == 4)
return new Color(ints[0], ints[1], ints[2], ints[3]);
}
else if (type.isAssignableFrom(Dimension.class))
{
int[] ints = splitInts(string);
if (ints.length == 1)
return new Dimension(ints[0], ints[0]);
else if (ints.length == 2)
return new Dimension(ints[0], ints[1]);
}
else if (type.isAssignableFrom(Point.class))
{
int[] ints = splitInts(string);
if (ints.length == 1)
return new Point(ints[0], ints[0]);
else if (ints.length == 2)
return new Point(ints[0], ints[1]);
}
else if (type.isAssignableFrom(Font.class))
{
return Font.decode(string);
}
else if (type.isAssignableFrom(Material.class))
{
Color color = null;
int[] ints = splitInts(string);
if (ints.length == 1)
color = new Color(ints[0]);
else if (ints.length == 3)
color = new Color(ints[0], ints[1], ints[2]);
else if (ints.length == 4)
color = new Color(ints[0], ints[1], ints[2], ints[3]);
if (color != null)
return new Material(color);
}
else if (type.isAssignableFrom(Insets.class))
{
int[] ints = splitInts(string);
if (ints.length == 4)
return new Insets(ints[0], ints[1], ints[2], ints[3]);
}
}
catch (Exception e)
{
}
return null;
}
/**
* Split a string into an array of integers
*
* @param string
* @return Array of ints
*/
protected static int[] splitInts(String string)
{
String[] split = string.trim().split(",");
List<Integer> ints = new ArrayList<Integer>(split.length);
for (String s : split)
{
try
{
ints.add(Integer.valueOf(s.trim()));
}
catch (Exception e)
{
}
}
int[] is = new int[ints.size()];
for (int i = 0; i < is.length; i++)
{
is[i] = ints.get(i);
}
return is;
}
}