diff options
author | Claude Brisson <cbrisson@apache.org> | 2019-02-26 14:30:46 +0000 |
---|---|---|
committer | Claude Brisson <cbrisson@apache.org> | 2019-02-26 14:30:46 +0000 |
commit | 1a19a60c60668035f33e893bbf9fcd7552275c0b (patch) | |
tree | ac1648c055f25a24d7ce07d9fcf30ebd44251095 /velocity-engine-core/src | |
parent | b408079d78c15edc4bfc9e342019252caf4ef19c (diff) | |
parent | 733a45e075f17bf0c91b3531a95b77a928ae5211 (diff) | |
download | apache-velocity-engine-1a19a60c60668035f33e893bbf9fcd7552275c0b.tar.gz |
[engine] Merge branch VELOCITY-892
git-svn-id: https://svn.apache.org/repos/asf/velocity/engine/trunk@1854386 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'velocity-engine-core/src')
16 files changed, 1211 insertions, 944 deletions
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java index 59a10d5e..3ffa3362 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java @@ -252,22 +252,16 @@ public interface RuntimeConstants /* * ---------------------------------------------------------------------- - * G E N E R A L R U N T I M E C O N F I G U R A T I O N + * I N T R O S P E C T I O N C O N F I G U R A T I O N * ---------------------------------------------------------------------- */ - /** Switch for the interpolation facility for string literals. */ - String INTERPOLATE_STRINGLITERALS = "runtime.interpolate.string.literals"; - - /** The character encoding for the templates. Used by the parser in processing the input streams. */ - String INPUT_ENCODING = "input.encoding"; - - /** Default Encoding is UTF-8. */ - String ENCODING_DEFAULT = "UTF-8"; - /** key name for uberspector. Multiple classnames can be specified,in which case uberspectors will be chained. */ String UBERSPECT_CLASSNAME = "runtime.introspector.uberspect"; + /** key for Conversion Manager instance */ + String CONVERSION_HANDLER_INSTANCE = "runtime.conversion.handler.instance"; + /** key for Conversion Manager class */ String CONVERSION_HANDLER_CLASS = "runtime.conversion.handler.class"; @@ -277,6 +271,22 @@ public interface RuntimeConstants /** A comma separated list of classes to restrict access to in the SecureIntrospector. */ String INTROSPECTOR_RESTRICT_CLASSES = "introspector.restrict.classes"; + + /* + * ---------------------------------------------------------------------- + * G E N E R A L R U N T I M E C O N F I G U R A T I O N + * ---------------------------------------------------------------------- + */ + + /** Switch for the interpolation facility for string literals. */ + String INTERPOLATE_STRINGLITERALS = "runtime.interpolate.string.literals"; + + /** The character encoding for the templates. Used by the parser in processing the input streams. */ + String INPUT_ENCODING = "input.encoding"; + + /** Default Encoding is UTF-8. */ + String ENCODING_DEFAULT = "UTF-8"; + /** Switch for ignoring nulls in math equations vs throwing exceptions. */ String STRICT_MATH = "runtime.strict.math"; diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java index eadf03a0..7c317321 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeServices.java @@ -110,7 +110,7 @@ public interface RuntimeServices * @param key * @param value */ - void addProperty(String key, Object value); + void addProperty(String key, Object value); /** * Clear the values pertaining to a particular diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/Pair.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/Pair.java deleted file mode 100644 index 8d4ce92c..00000000 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/Pair.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.apache.velocity.util; - -/* - * 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. - */ - -/** - * Combine two objects in a hashable pair - * - * @version $Id: Pair.java $ - * @since 2.0 - */ - -public class Pair<A, B> -{ - private final A first; - private final B second; - - public Pair(final A first, final B second) - { - this.first = first; - this.second = second; - } - - public int hashCode() - { - int hashFirst = first != null ? first.hashCode() : 0; - int hashSecond = second != null ? second.hashCode() : 0; - - return (hashFirst + hashSecond) * hashSecond + hashFirst; - } - - public boolean equals(Object other) - { - if (other instanceof Pair) - { - Pair otherPair = (Pair) other; - return - (( this.first == otherPair.first || - ( this.first != null && otherPair.first != null && - this.first.equals(otherPair.first))) && - ( this.second == otherPair.second || - ( this.second != null && otherPair.second != null && - this.second.equals(otherPair.second))) ); - } - - return false; - } - - public String toString() - { - return "(" + first + ", " + second + ")"; - } - - public A getFirst() - { - return first; - } - - public B getSecond() - { - return second; - } -} diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java index fe7416bc..021c3291 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ClassMap.java @@ -73,7 +73,7 @@ public class ClassMap * @param conversionHandler conversion handler * @since 2.0 */ - public ClassMap(final Class clazz, final Logger log, final ConversionHandler conversionHandler) + public ClassMap(final Class clazz, final Logger log, final TypeConversionHandler conversionHandler) { this.clazz = clazz; this.log = log; @@ -121,7 +121,7 @@ public class ClassMap * are taken from all the public methods * that our class, its parents and their implemented interfaces provide. */ - private MethodCache createMethodCache(ConversionHandler conversionHandler) + private MethodCache createMethodCache(TypeConversionHandler conversionHandler) { MethodCache methodCache = new MethodCache(log, conversionHandler); // @@ -231,7 +231,7 @@ public class ClassMap /** Map of methods that are searchable according to method parameters to find a match */ private final MethodMap methodMap; - private MethodCache(Logger log, ConversionHandler conversionHandler) + private MethodCache(Logger log, TypeConversionHandler conversionHandler) { this.log = log; methodMap = new MethodMap(conversionHandler); diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandler.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandler.java index f52eec1b..d38bc7a7 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandler.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandler.java @@ -27,13 +27,17 @@ package org.apache.velocity.util.introspection; * * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a> * @version $Id: ConversionHandler.java $ + * @deprecated use {@link TypeConversionHandler} + * @see TypeConversionHandler * @since 2.0 */ +@Deprecated public interface ConversionHandler { /** * Check to see if the conversion can be done using an explicit conversion + * * @param formal expected formal type * @param actual provided argument type * @return null if no conversion is needed, or the appropriate Converter object @@ -55,10 +59,11 @@ public interface ConversionHandler /** * Add the given converter to the handler. Implementation should be thread-safe. * - * @param formal expected formal type - * @param actual provided argument type + * @param formal expected formal type + * @param actual provided argument type * @param converter converter * @since 2.0 */ void addConverter(Class formal, Class actual, Converter converter); } + diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandlerImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandlerImpl.java deleted file mode 100644 index 23ceb88f..00000000 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/ConversionHandlerImpl.java +++ /dev/null @@ -1,625 +0,0 @@ -package org.apache.velocity.util.introspection; - -/* - * 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 org.apache.commons.lang3.LocaleUtils; -import org.apache.velocity.util.Pair; - -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A conversion handler adds admissible conversions between Java types whenever Velocity introspection has to map - * VTL methods and property accessors to Java methods. This implementation is the default Conversion Handler - * for Velocity. - * - * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a> - * @version $Id: ConversionHandlerImpl.java $ - * @since 2.0 - */ - -public class ConversionHandlerImpl implements ConversionHandler -{ - /** - * standard narrowing and string parsing conversions. - */ - static Map<Pair<? extends Class, ? extends Class>, Converter> standardConverterMap; - - /** - * basic toString converter - */ - static Converter toString; - - /** - * cache miss converter - */ - static Converter cacheMiss; - - /** - * min/max byte/short/int values as long - */ - static final long minByte = Byte.MIN_VALUE, maxByte = Byte.MAX_VALUE, - minShort = Short.MIN_VALUE, maxShort = Short.MAX_VALUE, - minInt = Integer.MIN_VALUE, maxInt = Integer.MAX_VALUE; - - /** - * min/max long values as double - */ - static final double minLong = Long.MIN_VALUE, maxLong = Long.MAX_VALUE; - - /** - * a converters cache map, initialized with the standard narrowing and string parsing conversions. - */ - Map<Pair<? extends Class, ? extends Class>, Converter> converterCacheMap; - - static - { - standardConverterMap = new HashMap<>(); - - cacheMiss = new Converter<Object>() - { - @Override - public Object convert(Object o) - { - return o; - } - }; - - /* number -> boolean */ - Converter<Boolean> numberToBool = new Converter<Boolean>() - { - @Override - public Boolean convert(Object o) - { - return o == null ? null : ((Number) o).intValue() != 0; - } - }; - standardConverterMap.put(new Pair<>(Boolean.class, Byte.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Short.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Integer.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Long.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Float.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Double.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Number.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Byte.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Short.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Integer.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Long.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Float.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.class, Double.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Byte.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Short.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Integer.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Long.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Float.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Double.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Number.class), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Byte.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Short.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Integer.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Long.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Float.TYPE), numberToBool); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Double.TYPE), numberToBool); - - /* character -> boolean */ - Converter<Boolean> charToBoolean = new Converter<Boolean>() - { - @Override - public Boolean convert(Object o) - { - return o == null ? null : (Character) o != 0; - } - }; - standardConverterMap.put(new Pair<>(Boolean.class, Character.class), charToBoolean); - standardConverterMap.put(new Pair<>(Boolean.class, Character.TYPE), charToBoolean); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Character.class), charToBoolean); - standardConverterMap.put(new Pair<>(Boolean.TYPE, Character.TYPE), charToBoolean); - - /* string -> boolean */ - Converter<Boolean> stringToBoolean = new Converter<Boolean>() - { - @Override - public Boolean convert(Object o) - { - return Boolean.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Boolean.class, String.class), stringToBoolean); - standardConverterMap.put(new Pair<>(Boolean.TYPE, String.class), stringToBoolean); - - /* narrowing towards byte */ - Converter<Byte> narrowingToByte = new Converter<Byte>() - { - @Override - public Byte convert(Object o) - { - if (o == null) return null; - long l = ((Number)o).longValue(); - if (l < minByte || l > maxByte) - { - throw new NumberFormatException("value out of range for byte type: " + l); - } - return ((Number) o).byteValue(); - } - }; - standardConverterMap.put(new Pair<>(Byte.class, Short.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Integer.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Long.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Float.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Double.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Number.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Short.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Integer.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Long.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Float.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.class, Double.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Short.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Integer.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Long.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Float.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Double.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Number.class), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Short.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Integer.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Long.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Float.TYPE), narrowingToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Double.TYPE), narrowingToByte); - - /* string to byte */ - Converter<Byte> stringToByte = new Converter<Byte>() - { - @Override - public Byte convert(Object o) - { - return Byte.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Byte.class, String.class), stringToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, String.class), stringToByte); - - /* narrowing towards short */ - Converter<Short> narrowingToShort = new Converter<Short>() - { - @Override - public Short convert(Object o) - { - if (o == null) return null; - long l = ((Number)o).longValue(); - if (l < minShort || l > maxShort) - { - throw new NumberFormatException("value out of range for short type: " + l); - } - return ((Number) o).shortValue(); - } - }; - standardConverterMap.put(new Pair<>(Short.class, Integer.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Long.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Float.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Double.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Number.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Integer.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Long.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Float.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.class, Double.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Integer.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Long.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Float.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Double.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Number.class), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Integer.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Long.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Float.TYPE), narrowingToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Double.TYPE), narrowingToShort); - - /* string to short */ - Converter<Short> stringToShort = new Converter<Short>() - { - @Override - public Short convert(Object o) - { - return Short.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Short.class, String.class), stringToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, String.class), stringToShort); - - /* narrowing towards int */ - Converter<Integer> narrowingToInteger = new Converter<Integer>() - { - @Override - public Integer convert(Object o) - { - if (o == null) return null; - long l = ((Number)o).longValue(); - if (l < minInt || l > maxInt) - { - throw new NumberFormatException("value out of range for integer type: " + l); - } - return ((Number) o).intValue(); - } - }; - standardConverterMap.put(new Pair<>(Integer.class, Long.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Float.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Double.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Number.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Long.TYPE), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Float.TYPE), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Double.TYPE), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Long.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Float.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Double.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Number.class), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Long.TYPE), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Float.TYPE), narrowingToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Double.TYPE), narrowingToInteger); - - /* widening towards Integer */ - Converter<Integer> wideningToInteger = new Converter<Integer>() - { - @Override - public Integer convert(Object o) - { - if (o == null) return null; - return ((Number) o).intValue(); - } - }; - standardConverterMap.put(new Pair<>(Integer.class, Short.class), wideningToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Short.TYPE), wideningToInteger); - - /* string to int */ - Converter<Integer> stringToInteger = new Converter<Integer>() - { - @Override - public Integer convert(Object o) - { - return Integer.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Integer.class, String.class), stringToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, String.class), stringToInteger); - - /* narrowing towards long */ - Converter<Long> narrowingToLong = new Converter<Long>() - { - @Override - public Long convert(Object o) - { - if (o == null) return null; - double d = ((Number)o).doubleValue(); - if (d < minLong || d > maxLong) - { - throw new NumberFormatException("value out of range for long type: " + d); - } - return ((Number) o).longValue(); - } - }; - standardConverterMap.put(new Pair<>(Long.class, Float.class), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.class, Double.class), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.class, Number.class), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.class, Float.TYPE), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.class, Double.TYPE), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Float.class), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Double.class), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Number.class), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Float.TYPE), narrowingToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Double.TYPE), narrowingToLong); - - /* widening towards Long */ - Converter<Long> wideningToLong = new Converter<Long>() - { - @Override - public Long convert(Object o) - { - if (o == null) return null; - return ((Number) o).longValue(); - } - }; - standardConverterMap.put(new Pair<>(Long.class, Short.class), wideningToLong); - standardConverterMap.put(new Pair<>(Long.class, Integer.class), wideningToLong); - standardConverterMap.put(new Pair<>(Long.class, Short.TYPE), wideningToLong); - standardConverterMap.put(new Pair<>(Long.class, Integer.TYPE), wideningToLong); - - /* string to long */ - Converter<Long> stringToLong = new Converter<Long>() - { - @Override - public Long convert(Object o) - { - return Long.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Long.class, String.class), stringToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, String.class), stringToLong); - - /* narrowing towards float */ - Converter<Float> narrowingToFloat = new Converter<Float>() - { - @Override - public Float convert(Object o) - { - return o == null ? null : ((Number) o).floatValue(); - } - }; - standardConverterMap.put(new Pair<>(Float.class, Double.class), narrowingToFloat); - standardConverterMap.put(new Pair<>(Float.class, Number.class), narrowingToFloat); - standardConverterMap.put(new Pair<>(Float.class, Double.TYPE), narrowingToFloat); - standardConverterMap.put(new Pair<>(Float.TYPE, Double.class), narrowingToFloat); - standardConverterMap.put(new Pair<>(Float.TYPE, Number.class), narrowingToFloat); - standardConverterMap.put(new Pair<>(Float.TYPE, Double.TYPE), narrowingToFloat); - - /* exact towards Float */ - Converter<Float> toFloat = new Converter<Float>() - { - @Override - public Float convert(Object o) - { - if (o == null) return null; - return ((Number) o).floatValue(); - } - }; - standardConverterMap.put(new Pair<>(Float.class, Short.class), toFloat); - standardConverterMap.put(new Pair<>(Float.class, Integer.class), toFloat); - standardConverterMap.put(new Pair<>(Float.class, Long.class), toFloat); - standardConverterMap.put(new Pair<>(Float.class, Short.TYPE), toFloat); - standardConverterMap.put(new Pair<>(Float.class, Integer.TYPE), toFloat); - standardConverterMap.put(new Pair<>(Float.class, Long.TYPE), toFloat); - - /* string to float */ - Converter<Float> stringToFloat = new Converter<Float>() - { - @Override - public Float convert(Object o) - { - return Float.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Float.class, String.class), stringToFloat); - standardConverterMap.put(new Pair<>(Float.TYPE, String.class), stringToFloat); - - /* exact or widening towards Double */ - Converter<Double> toDouble = new Converter<Double>() - { - @Override - public Double convert(Object o) - { - if (o == null) return null; - return ((Number) o).doubleValue(); - } - }; - standardConverterMap.put(new Pair<>(Double.class, Short.class), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Integer.class), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Long.class), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Float.class), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Number.class), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Short.TYPE), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Integer.TYPE), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Long.TYPE), toDouble); - standardConverterMap.put(new Pair<>(Double.class, Float.TYPE), toDouble); - standardConverterMap.put(new Pair<>(Double.TYPE, Number.class), toDouble); - - /* string to double */ - Converter<Double> stringToDouble = new Converter<Double>() - { - @Override - public Double convert(Object o) - { - return Double.valueOf(String.valueOf(o)); - } - }; - standardConverterMap.put(new Pair<>(Double.class, String.class), stringToDouble); - standardConverterMap.put(new Pair<>(Double.TYPE, String.class), stringToDouble); - - /* boolean to byte */ - Converter<Byte> booleanToByte = new Converter<Byte>() - { - @Override - public Byte convert(Object o) - { - return o == null ? null : (Boolean) o ? (byte)1 : (byte)0; - } - }; - standardConverterMap.put(new Pair<>(Byte.class, Boolean.class), booleanToByte); - standardConverterMap.put(new Pair<>(Byte.class, Boolean.TYPE), booleanToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Boolean.class), booleanToByte); - standardConverterMap.put(new Pair<>(Byte.TYPE, Boolean.TYPE), booleanToByte); - - /* boolean to short */ - Converter<Short> booleanToShort = new Converter<Short>() - { - @Override - public Short convert(Object o) - { - return o == null ? null : (Boolean) o ? (short)1 : (short)0; - } - }; - standardConverterMap.put(new Pair<>(Short.class, Boolean.class), booleanToShort); - standardConverterMap.put(new Pair<>(Short.class, Boolean.TYPE), booleanToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Boolean.class), booleanToShort); - standardConverterMap.put(new Pair<>(Short.TYPE, Boolean.TYPE), booleanToShort); - - /* boolean to integer */ - Converter<Integer> booleanToInteger = new Converter<Integer>() - { - @Override - public Integer convert(Object o) - { - return o == null ? null : (Boolean) o ? (Integer)1 : (Integer)0; - } - }; - standardConverterMap.put(new Pair<>(Integer.class, Boolean.class), booleanToInteger); - standardConverterMap.put(new Pair<>(Integer.class, Boolean.TYPE), booleanToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Boolean.class), booleanToInteger); - standardConverterMap.put(new Pair<>(Integer.TYPE, Boolean.TYPE), booleanToInteger); - - /* boolean to long */ - Converter<Long> booleanToLong = new Converter<Long>() - { - @Override - public Long convert(Object o) - { - return o == null ? null : (Boolean) o ? 1L : 0L; - } - }; - standardConverterMap.put(new Pair<>(Long.class, Boolean.class), booleanToLong); - standardConverterMap.put(new Pair<>(Long.class, Boolean.TYPE), booleanToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Boolean.class), booleanToLong); - standardConverterMap.put(new Pair<>(Long.TYPE, Boolean.TYPE), booleanToLong); - - /* to string */ - toString = new Converter<String>() - { - @Override - public String convert(Object o) - { - return String.valueOf(o); - } - }; - - /* string to locale */ - Converter<Locale> stringToLocale = new Converter<Locale>() - { - @Override - public Locale convert(Object o) - { - return o == null ? null : LocaleUtils.toLocale((String)o); - } - }; - standardConverterMap.put(new Pair<>(Locale.class, String.class), stringToLocale); - } - - /** - * Constructor - */ - public ConversionHandlerImpl() - { - converterCacheMap = new ConcurrentHashMap<>(); - } - - /** - * Check to see if the conversion can be done using an explicit conversion - * @param actual found argument type - * @param formal expected formal type - * @return null if no conversion is needed, or the appropriate Converter object - * @since 2.0 - */ - @Override - public boolean isExplicitlyConvertible(Class formal, Class actual, boolean possibleVarArg) - { - /* - * for consistency, we also have to check standard implicit convertibility - * since it may not have been checked before by the calling code - */ - if (formal == actual || - IntrospectionUtils.isMethodInvocationConvertible(formal, actual, possibleVarArg) || - getNeededConverter(formal, actual) != null) - { - return true; - } - - /* Check var arg */ - if (possibleVarArg && formal.isArray()) - { - if (actual.isArray()) - { - actual = actual.getComponentType(); - } - return isExplicitlyConvertible(formal.getComponentType(), actual, false); - } - return false; - } - - - /** - * Returns the appropriate Converter object needed for an explicit conversion - * Returns null if no conversion is needed. - * - * @param actual found argument type - * @param formal expected formal type - * @return null if no conversion is needed, or the appropriate Converter object - * @since 2.0 - */ - @Override - public Converter getNeededConverter(final Class formal, final Class actual) - { - Pair<Class, Class> key = new Pair<>(formal, actual); - - /* first check for a standard conversion */ - Converter converter = standardConverterMap.get(key); - if (converter == null) - { - /* then the converters cache map */ - converter = converterCacheMap.get(key); - if (converter == null) - { - /* check for conversion towards string */ - if (formal == String.class) - { - converter = toString; - } - /* check for String -> Enum constant conversion */ - else if (formal.isEnum() && actual == String.class) - { - converter = new Converter() - { - @Override - public Object convert(Object o) - { - return Enum.valueOf((Class<Enum>) formal, (String) o); - } - }; - } - - converterCacheMap.put(key, converter == null ? cacheMiss : converter); - } - } - return converter == cacheMiss ? null : converter; - } - - /** - * Add the given converter to the handler. - * - * @param formal expected formal type - * @param actual provided argument type - * @param converter converter - * @since 2.0 - */ - @Override - public void addConverter(Class formal, Class actual, Converter converter) - { - Pair<Class, Class> key = new Pair<>(formal, actual); - converterCacheMap.put(key, converter); - if (formal.isPrimitive()) - { - key = new Pair<>(IntrospectionUtils.getBoxedClass(formal), actual); - converterCacheMap.put(key, converter); - } - else - { - Class unboxedFormal = IntrospectionUtils.getUnboxedClass(formal); - if (unboxedFormal != formal) - { - key = new Pair<>(unboxedFormal, actual); - converterCacheMap.put(key, converter); - } - } - } -} diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectionUtils.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectionUtils.java index 38dcfde0..78e0ad79 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectionUtils.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectionUtils.java @@ -19,6 +19,14 @@ package org.apache.velocity.util.introspection; * under the License. */ +import org.apache.commons.lang3.reflect.TypeUtils; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -66,7 +74,7 @@ public class IntrospectionUtils * @param clazz input class * @return boxed class */ - static Class getBoxedClass(Class clazz) + public static Class getBoxedClass(Class clazz) { Class boxed = boxingMap.get(clazz); return boxed == null ? clazz : boxed; @@ -77,16 +85,52 @@ public class IntrospectionUtils * @param clazz input class * @return unboxed class */ - static Class getUnboxedClass(Class clazz) + public static Class getUnboxedClass(Class clazz) { Class unboxed = unboxingMap.get(clazz); return unboxed == null ? clazz : unboxed; } /** - * + * returns the Class corresponding to a Type, if possible + * @param type the input Type + * @return found Class, if any */ - + public static Class getTypeClass(Type type) + { + if (type == null) + { + return null; + } + if (type instanceof Class) + { + return (Class)type; + } + else if (type instanceof ParameterizedType) + { + return (Class)((ParameterizedType)type).getRawType(); + } + else if (type instanceof GenericArrayType) + { + Type componentType = ((GenericArrayType)type).getGenericComponentType(); + Class componentClass = getTypeClass(componentType); + if (componentClass != null) + { + return Array.newInstance(componentClass, 0).getClass(); + } + } + else if (type instanceof TypeVariable) + { + Type[] bounds = TypeUtils.getImplicitBounds((TypeVariable)type); + if (bounds.length == 1) return getTypeClass(bounds[0]); + } + else if (type instanceof WildcardType) + { + Type[] bounds = TypeUtils.getImplicitUpperBounds((WildcardType)type); + if (bounds.length == 1) return getTypeClass(bounds[0]); + } + return null; + } /** * Determines whether a type represented by a class object is @@ -108,92 +152,105 @@ public class IntrospectionUtils * type or an object type of a primitive type that can be converted to * the formal type. */ - public static boolean isMethodInvocationConvertible(Class formal, + public static boolean isMethodInvocationConvertible(Type formal, Class actual, boolean possibleVarArg) { - /* if it's a null, it means the arg was null */ - if (actual == null) + Class formalClass = getTypeClass(formal); + if (formalClass != null) { - return !formal.isPrimitive(); - } + /* if it's a null, it means the arg was null */ + if (actual == null) + { + return !formalClass.isPrimitive(); + } - /* Check for identity or widening reference conversion */ - if (formal.isAssignableFrom(actual)) - { - return true; - } + /* Check for identity or widening reference conversion */ + if (formalClass.isAssignableFrom(actual)) + { + return true; + } - /* 2.0: Since MethodMap's comparison functions now use this method with potentially reversed arguments order, - * actual can be a primitive type. */ + /* 2.0: Since MethodMap's comparison functions now use this method with potentially reversed arguments order, + * actual can be a primitive type. */ - /* Check for boxing */ - if (!formal.isPrimitive() && actual.isPrimitive()) - { - Class boxed = boxingMap.get(actual); - if (boxed != null && boxed == formal || formal.isAssignableFrom(boxed)) return true; - } + /* Check for boxing */ + if (!formalClass.isPrimitive() && actual.isPrimitive()) + { + Class boxed = boxingMap.get(actual); + if (boxed != null && boxed == formalClass || formalClass.isAssignableFrom(boxed)) return true; + } - if (formal.isPrimitive()) - { - if (actual.isPrimitive()) + if (formalClass.isPrimitive()) { - /* check for widening primitive conversion */ - if (formal == Short.TYPE && actual == Byte.TYPE) - return true; - if (formal == Integer.TYPE && ( + if (actual.isPrimitive()) + { + /* check for widening primitive conversion */ + if (formalClass == Short.TYPE && actual == Byte.TYPE) + return true; + if (formalClass == Integer.TYPE && ( actual == Byte.TYPE || actual == Short.TYPE)) - return true; - if (formal == Long.TYPE && ( + return true; + if (formalClass == Long.TYPE && ( actual == Byte.TYPE || actual == Short.TYPE || actual == Integer.TYPE)) - return true; - if (formal == Float.TYPE && ( + return true; + if (formalClass == Float.TYPE && ( actual == Byte.TYPE || actual == Short.TYPE || actual == Integer.TYPE || - actual == Long.TYPE)) - return true; - if (formal == Double.TYPE && ( + actual == Long.TYPE)) + return true; + if (formalClass == Double.TYPE && ( actual == Byte.TYPE || actual == Short.TYPE || actual == Integer.TYPE || - actual == Long.TYPE || actual == Float.TYPE)) - return true; - } - else - { - /* Check for unboxing with widening primitive conversion. */ - if (formal == Boolean.TYPE && actual == Boolean.class) - return true; - if (formal == Character.TYPE && actual == Character.class) - return true; - if (formal == Byte.TYPE && actual == Byte.class) - return true; - if (formal == Short.TYPE && (actual == Short.class || actual == Byte.class)) - return true; - if (formal == Integer.TYPE && (actual == Integer.class || actual == Short.class || + actual == Long.TYPE || actual == Float.TYPE)) + return true; + } else + { + /* Check for unboxing with widening primitive conversion. */ + if (formalClass == Boolean.TYPE && actual == Boolean.class) + return true; + if (formalClass == Character.TYPE && actual == Character.class) + return true; + if (formalClass == Byte.TYPE && actual == Byte.class) + return true; + if (formalClass == Short.TYPE && (actual == Short.class || actual == Byte.class)) + return true; + if (formalClass == Integer.TYPE && (actual == Integer.class || actual == Short.class || actual == Byte.class)) - return true; - if (formal == Long.TYPE && (actual == Long.class || actual == Integer.class || + return true; + if (formalClass == Long.TYPE && (actual == Long.class || actual == Integer.class || actual == Short.class || actual == Byte.class)) - return true; - if (formal == Float.TYPE && (actual == Float.class || actual == Long.class || + return true; + if (formalClass == Float.TYPE && (actual == Float.class || actual == Long.class || actual == Integer.class || actual == Short.class || actual == Byte.class)) - return true; - if (formal == Double.TYPE && (actual == Double.class || actual == Float.class || + return true; + if (formalClass == Double.TYPE && (actual == Double.class || actual == Float.class || actual == Long.class || actual == Integer.class || actual == Short.class || actual == Byte.class)) - return true; + return true; + } } - } - /* Check for vararg conversion. */ - if (possibleVarArg && formal.isArray()) + /* Check for vararg conversion. */ + if (possibleVarArg && formalClass.isArray()) + { + if (actual.isArray()) + { + actual = actual.getComponentType(); + } + return isMethodInvocationConvertible(formalClass.getComponentType(), + actual, false); + } + return false; + } + else { - if (actual.isArray()) + // no distinction between strict and implicit, not a big deal in this case + if (TypeUtils.isAssignable(actual, formal)) { - actual = actual.getComponentType(); + return true; } - return isMethodInvocationConvertible(formal.getComponentType(), - actual, false); + return possibleVarArg && TypeUtils.isArrayType(formal) && + TypeUtils.isAssignable(actual, TypeUtils.getArrayComponentType(formal)); } - return false; } /** @@ -212,55 +269,69 @@ public class IntrospectionUtils * or formal and actual are both primitive types and actual can be * subject to widening conversion to formal. */ - public static boolean isStrictMethodInvocationConvertible(Class formal, + public static boolean isStrictMethodInvocationConvertible(Type formal, Class actual, boolean possibleVarArg) { - /* Check for nullity */ - if (actual == null) - { - return !formal.isPrimitive(); - } - - /* Check for identity or widening reference conversion */ - if(formal.isAssignableFrom(actual)) + Class formalClass = getTypeClass(formal); + if (formalClass != null) { - return true; - } + /* Check for nullity */ + if (actual == null) + { + return !formalClass.isPrimitive(); + } - /* Check for widening primitive conversion. */ - if(formal.isPrimitive()) - { - if(formal == Short.TYPE && (actual == Byte.TYPE)) - return true; - if(formal == Integer.TYPE && - (actual == Short.TYPE || actual == Byte.TYPE)) - return true; - if(formal == Long.TYPE && - (actual == Integer.TYPE || actual == Short.TYPE || - actual == Byte.TYPE)) - return true; - if(formal == Float.TYPE && - (actual == Long.TYPE || actual == Integer.TYPE || - actual == Short.TYPE || actual == Byte.TYPE)) - return true; - if(formal == Double.TYPE && - (actual == Float.TYPE || actual == Long.TYPE || - actual == Integer.TYPE || actual == Short.TYPE || - actual == Byte.TYPE)) + /* Check for identity or widening reference conversion */ + if (formalClass.isAssignableFrom(actual)) + { return true; - } + } - /* Check for vararg conversion. */ - if (possibleVarArg && formal.isArray()) + /* Check for widening primitive conversion. */ + if (formalClass.isPrimitive()) + { + if (formal == Short.TYPE && (actual == Byte.TYPE)) + return true; + if (formal == Integer.TYPE && + (actual == Short.TYPE || actual == Byte.TYPE)) + return true; + if (formal == Long.TYPE && + (actual == Integer.TYPE || actual == Short.TYPE || + actual == Byte.TYPE)) + return true; + if (formal == Float.TYPE && + (actual == Long.TYPE || actual == Integer.TYPE || + actual == Short.TYPE || actual == Byte.TYPE)) + return true; + if (formal == Double.TYPE && + (actual == Float.TYPE || actual == Long.TYPE || + actual == Integer.TYPE || actual == Short.TYPE || + actual == Byte.TYPE)) + return true; + } + + /* Check for vararg conversion. */ + if (possibleVarArg && formalClass.isArray()) + { + if (actual.isArray()) + { + actual = actual.getComponentType(); + } + return isStrictMethodInvocationConvertible(formalClass.getComponentType(), + actual, false); + } + return false; + } + else { - if (actual.isArray()) + // no distinction between strict and implicit, not a big deal in this case + if (TypeUtils.isAssignable(actual, formal)) { - actual = actual.getComponentType(); + return true; } - return isStrictMethodInvocationConvertible(formal.getComponentType(), - actual, false); + return possibleVarArg && TypeUtils.isArrayType(formal) && + TypeUtils.isAssignable(actual, TypeUtils.getArrayComponentType(formal)); } - return false; } } diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/Introspector.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/Introspector.java index 1407242a..512d434e 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/Introspector.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/Introspector.java @@ -68,7 +68,7 @@ public class Introspector extends IntrospectorBase * @param conversionHandler conversion handler * @since 2.0 */ - public Introspector(final Logger log, ConversionHandler conversionHandler) + public Introspector(final Logger log, TypeConversionHandler conversionHandler) { super(log, conversionHandler); } diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorBase.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorBase.java index e98ec35b..654a66b6 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorBase.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorBase.java @@ -62,17 +62,13 @@ public abstract class IntrospectorBase /** The Introspector Cache */ private final IntrospectorCache introspectorCache; - /** The Conversion handler */ - private final ConversionHandler conversionHandler; - /** * C'tor. */ - protected IntrospectorBase(final Logger log, final ConversionHandler conversionHandler) + protected IntrospectorBase(final Logger log, final TypeConversionHandler conversionHandler) { this.log = log; introspectorCache = new IntrospectorCache(log, conversionHandler); - this.conversionHandler = conversionHandler; } /** diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorCache.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorCache.java index 742dcc3b..ae59f15a 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorCache.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/IntrospectorCache.java @@ -68,12 +68,12 @@ public final class IntrospectorCache /** * Conversion handler */ - private final ConversionHandler conversionHandler; + private final TypeConversionHandler conversionHandler; /** * C'tor */ - public IntrospectorCache(final Logger log, final ConversionHandler conversionHandler) + public IntrospectorCache(final Logger log, final TypeConversionHandler conversionHandler) { this.log = log; this.conversionHandler = conversionHandler; diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/MethodMap.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/MethodMap.java index 3fcdccee..f9f1e000 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/MethodMap.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/MethodMap.java @@ -19,9 +19,11 @@ package org.apache.velocity.util.introspection; * under the License. */ +import org.apache.commons.lang3.reflect.TypeUtils; import org.apache.velocity.exception.VelocityException; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; @@ -54,7 +56,7 @@ public class MethodMap private static final int IMPLCITLY_CONVERTIBLE = 2; private static final int STRICTLY_CONVERTIBLE = 3; - ConversionHandler conversionHandler; + TypeConversionHandler conversionHandler; /** * Default constructor @@ -69,7 +71,7 @@ public class MethodMap * @param conversionHandler conversion handler * @since 2.0 */ - public MethodMap(ConversionHandler conversionHandler) + public MethodMap(TypeConversionHandler conversionHandler) { this.conversionHandler = conversionHandler; } @@ -173,7 +175,7 @@ public class MethodMap Method method; /* cache arguments classes array */ - Class[] methodTypes; + Type[] methodTypes; /* specificity: how does the best match compare to provided arguments * one one LESS_SPECIFIC, MORE_SPECIFIC or INCOMPARABLE */ @@ -190,9 +192,9 @@ public class MethodMap { this.method = method; this.applicability = applicability; - this.methodTypes = method.getParameterTypes(); + this.methodTypes = method.getGenericParameterTypes(); this.specificity = compare(methodTypes, unboxedArgs); - this.varargs = methodTypes.length > 0 && methodTypes[methodTypes.length - 1].isArray(); + this.varargs = methodTypes.length > 0 && TypeUtils.isArrayType(methodTypes[methodTypes.length - 1]); } } @@ -322,74 +324,74 @@ public class MethodMap /** * Determines which method signature (represented by a class array) is more * specific. This defines a partial ordering on the method signatures. - * @param c1 first signature to compare - * @param c2 second signature to compare + * @param t1 first signature to compare + * @param t2 second signature to compare * @return MORE_SPECIFIC if c1 is more specific than c2, LESS_SPECIFIC if * c1 is less specific than c2, INCOMPARABLE if they are incomparable. */ - private int compare(Class[] c1, Class[] c2) + private int compare(Type[] t1, Type[] t2) { - boolean c1IsVararag = false; - boolean c2IsVararag = false; + boolean t1IsVararag = false; + boolean t2IsVararag = false; boolean fixedLengths = false; // compare lengths to handle comparisons where the size of the arrays // doesn't match, but the methods are both applicable due to the fact // that one is a varargs method - if (c1.length > c2.length) + if (t1.length > t2.length) { - int l2 = c2.length; + int l2 = t2.length; if (l2 == 0) { return MORE_SPECIFIC; } - c2 = Arrays.copyOf(c2, c1.length); - Class itemClass = c2[l2 - 1].getComponentType(); + t2 = Arrays.copyOf(t2, t1.length); + Type itemType = TypeUtils.getArrayComponentType(t2[l2 - 1]); /* if item class is null, then it implies the vaarg is #1 * (and receives an empty array) */ - if (itemClass == null) + if (itemType == null) { /* by construct, we have c1.length = l2 + 1 */ - c1IsVararag = true; - c2[c1.length - 1] = null; + t1IsVararag = true; + t2[t1.length - 1] = null; } else { - c2IsVararag = true; - for (int i = l2 - 1; i < c1.length; ++i) + t2IsVararag = true; + for (int i = l2 - 1; i < t1.length; ++i) { /* also overwrite the vaargs itself */ - c2[i] = itemClass; + t2[i] = itemType; } } fixedLengths = true; } - else if (c2.length > c1.length) + else if (t2.length > t1.length) { - int l1 = c1.length; + int l1 = t1.length; if (l1 == 0) { return LESS_SPECIFIC; } - c1 = Arrays.copyOf(c1, c2.length); - Class itemClass = c1[l1 - 1].getComponentType(); + t1 = Arrays.copyOf(t1, t2.length); + Type itemType = TypeUtils.getArrayComponentType(t1[l1 - 1]); /* if item class is null, then it implies the vaarg is #2 * (and receives an empty array) */ - if (itemClass == null) + if (itemType == null) { /* by construct, we have c2.length = l1 + 1 */ - c2IsVararag = true; - c1[c2.length - 1] = null; + t2IsVararag = true; + t1[t2.length - 1] = null; } else { - c1IsVararag = true; - for (int i = l1 - 1; i < c2.length; ++i) + t1IsVararag = true; + for (int i = l1 - 1; i < t2.length; ++i) { /* also overwrite the vaargs itself */ - c1[i] = itemClass; + t1[i] = itemType; } } fixedLengths = true; @@ -398,52 +400,72 @@ public class MethodMap /* ok, move on and compare those of equal lengths */ int fromC1toC2 = STRICTLY_CONVERTIBLE; int fromC2toC1 = STRICTLY_CONVERTIBLE; - for(int i = 0; i < c1.length; ++i) + for(int i = 0; i < t1.length; ++i) { - boolean last = !fixedLengths && (i == c1.length - 1); - if (c1[i] != c2[i]) + Class c1 = t1[i] == null ? null : IntrospectionUtils.getTypeClass(t1[i]); + Class c2 = t2[i] == null ? null : IntrospectionUtils.getTypeClass(t2[i]); + boolean last = !fixedLengths && (i == t1.length - 1); + if (t1[i] == null && t2[i] != null || t1[i] != null && t2[i] == null || !t1[i].equals(t2[i])) { - if (c1[i] == null) + if (t1[i] == null) { fromC2toC1 = NOT_CONVERTIBLE; - if (c2[i].isPrimitive()) + if (c2 != null && c2.isPrimitive()) { fromC1toC2 = NOT_CONVERTIBLE; } } - else if (c2[i] == null) + else if (t2[i] == null) { fromC1toC2 = NOT_CONVERTIBLE; - if (c1[i].isPrimitive()) + if (c1 != null && c1.isPrimitive()) { fromC2toC1 = NOT_CONVERTIBLE; } } else { - switch (fromC1toC2) + if (c1 != null) { - case STRICTLY_CONVERTIBLE: - if (isStrictConvertible(c2[i], c1[i], last)) break; - fromC1toC2 = IMPLCITLY_CONVERTIBLE; - case IMPLCITLY_CONVERTIBLE: - if (isConvertible(c2[i], c1[i], last)) break; - fromC1toC2 = EXPLICITLY_CONVERTIBLE; - case EXPLICITLY_CONVERTIBLE: - if (isExplicitlyConvertible(c2[i], c1[i], last)) break; - fromC1toC2 = NOT_CONVERTIBLE; + switch (fromC1toC2) + { + case STRICTLY_CONVERTIBLE: + if (isStrictConvertible(t2[i], c1, last)) break; + fromC1toC2 = IMPLCITLY_CONVERTIBLE; + case IMPLCITLY_CONVERTIBLE: + if (isConvertible(t2[i], c1, last)) break; + fromC1toC2 = EXPLICITLY_CONVERTIBLE; + case EXPLICITLY_CONVERTIBLE: + if (isExplicitlyConvertible(t2[i], c1, last)) break; + fromC1toC2 = NOT_CONVERTIBLE; + } + } + else if (fromC1toC2 > NOT_CONVERTIBLE) + { + fromC1toC2 = TypeUtils.isAssignable(t1[i], t2[i]) ? + Math.min(fromC1toC2, IMPLCITLY_CONVERTIBLE) : + NOT_CONVERTIBLE; + } + if (c2 != null) + { + switch (fromC2toC1) + { + case STRICTLY_CONVERTIBLE: + if (isStrictConvertible(t1[i], c2, last)) break; + fromC2toC1 = IMPLCITLY_CONVERTIBLE; + case IMPLCITLY_CONVERTIBLE: + if (isConvertible(t1[i], c2, last)) break; + fromC2toC1 = EXPLICITLY_CONVERTIBLE; + case EXPLICITLY_CONVERTIBLE: + if (isExplicitlyConvertible(t1[i], c2, last)) break; + fromC2toC1 = NOT_CONVERTIBLE; + } } - switch (fromC2toC1) + else if (fromC2toC1 > NOT_CONVERTIBLE) { - case STRICTLY_CONVERTIBLE: - if (isStrictConvertible(c1[i], c2[i], last)) break; - fromC2toC1 = IMPLCITLY_CONVERTIBLE; - case IMPLCITLY_CONVERTIBLE: - if (isConvertible(c1[i], c2[i], last)) break; - fromC2toC1 = EXPLICITLY_CONVERTIBLE; - case EXPLICITLY_CONVERTIBLE: - if (isExplicitlyConvertible(c1[i], c2[i], last)) break; - fromC2toC1 = NOT_CONVERTIBLE; + fromC2toC1 = TypeUtils.isAssignable(t2[i], t1[i]) ? + Math.min(fromC2toC1, IMPLCITLY_CONVERTIBLE) : + NOT_CONVERTIBLE; } } } @@ -472,8 +494,8 @@ public class MethodMap * If one method accepts varargs and the other does not, * call the non-vararg one more specific. */ - boolean last1Array = c1IsVararag || !fixedLengths && c1[c1.length - 1].isArray(); - boolean last2Array = c2IsVararag || !fixedLengths && c2[c2.length - 1].isArray(); + boolean last1Array = t1IsVararag || !fixedLengths && TypeUtils.isArrayType (t1[t1.length - 1]); + boolean last2Array = t2IsVararag || !fixedLengths && TypeUtils.isArrayType(t2[t2.length - 1]); if (last1Array && !last2Array) { return LESS_SPECIFIC; @@ -499,14 +521,13 @@ public class MethodMap */ private int getApplicability(Method method, Class[] classes) { - Class[] methodArgs = method.getParameterTypes(); + Type[] methodArgs = method.getGenericParameterTypes(); int ret = STRICTLY_CONVERTIBLE; if (methodArgs.length > classes.length) { // if there's just one more methodArg than class arg // and the last methodArg is an array, then treat it as a vararg - if (methodArgs.length == classes.length + 1 && - methodArgs[methodArgs.length - 1].isArray()) + if (methodArgs.length == classes.length + 1 && TypeUtils.isArrayType(methodArgs[methodArgs.length - 1])) { // all the args preceding the vararg must match for (int i = 0; i < classes.length; i++) @@ -541,13 +562,14 @@ public class MethodMap // (e.g. String when the method is expecting String...) for(int i = 0; i < classes.length; ++i) { - if (!isStrictConvertible(methodArgs[i], classes[i], i == classes.length - 1 && methodArgs[i].isArray())) + boolean possibleVararg = i == classes.length - 1 && TypeUtils.isArrayType(methodArgs[i]); + if (!isStrictConvertible(methodArgs[i], classes[i], possibleVararg)) { - if (isConvertible(methodArgs[i], classes[i], i == classes.length - 1 && methodArgs[i].isArray())) + if (isConvertible(methodArgs[i], classes[i], possibleVararg)) { ret = Math.min(ret, IMPLCITLY_CONVERTIBLE); } - else if (isExplicitlyConvertible(methodArgs[i], classes[i], i == classes.length - 1 && methodArgs[i].isArray())) + else if (isExplicitlyConvertible(methodArgs[i], classes[i], possibleVararg)) { ret = Math.min(ret, EXPLICITLY_CONVERTIBLE); } @@ -562,8 +584,8 @@ public class MethodMap else if (methodArgs.length > 0) // more arguments given than the method accepts; check for varargs { // check that the last methodArg is an array - Class lastarg = methodArgs[methodArgs.length - 1]; - if (!lastarg.isArray()) + Type lastarg = methodArgs[methodArgs.length - 1]; + if (!TypeUtils.isArrayType(lastarg)) { return NOT_CONVERTIBLE; } @@ -589,7 +611,7 @@ public class MethodMap } // check that all remaining arguments are convertible to the vararg type - Class vararg = lastarg.getComponentType(); + Type vararg = TypeUtils.getArrayComponentType(lastarg); for (int i = methodArgs.length - 1; i < classes.length; ++i) { if (!isStrictConvertible(vararg, classes[i], false)) @@ -621,7 +643,7 @@ public class MethodMap * @param possibleVarArg * @return convertible */ - private boolean isConvertible(Class formal, Class actual, boolean possibleVarArg) + private boolean isConvertible(Type formal, Class actual, boolean possibleVarArg) { return IntrospectionUtils. isMethodInvocationConvertible(formal, actual, possibleVarArg); @@ -636,7 +658,7 @@ public class MethodMap * @param possibleVarArg * @return convertible */ - private static boolean isStrictConvertible(Class formal, Class actual, boolean possibleVarArg) + private static boolean isStrictConvertible(Type formal, Class actual, boolean possibleVarArg) { return IntrospectionUtils. isStrictMethodInvocationConvertible(formal, actual, possibleVarArg); @@ -650,7 +672,7 @@ public class MethodMap * @param possibleVarArg * @return */ - private boolean isExplicitlyConvertible(Class formal, Class actual, boolean possibleVarArg) + private boolean isExplicitlyConvertible(Type formal, Class actual, boolean possibleVarArg) { return conversionHandler != null && conversionHandler.isExplicitlyConvertible(formal, actual, possibleVarArg); } diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/TypeConversionHandler.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/TypeConversionHandler.java new file mode 100644 index 00000000..0df26147 --- /dev/null +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/TypeConversionHandler.java @@ -0,0 +1,67 @@ +package org.apache.velocity.util.introspection; + +/* + * 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.lang.reflect.Type; + +/** + * A conversion handler adds admissible conversions between Java types whenever Velocity introspection has to map + * VTL methods and property accessors to Java methods. + * Both methods must be consistent: <code>getNeededConverter</code> must not return <code>null</code> whenever + * <code>isExplicitlyConvertible</code> returned true with the same arguments. + * + * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a> + * @version $Id: ConversionHandler.java $ + * @since 2.1 + */ + +public interface TypeConversionHandler +{ + /** + * Check to see if the conversion can be done using an explicit conversion + * @param formal expected formal type + * @param actual provided argument type + * @return null if no conversion is needed, or the appropriate Converter object + * @since 2.1 + */ + boolean isExplicitlyConvertible(Type formal, Class actual, boolean possibleVarArg); + + /** + * Returns the appropriate Converter object needed for an explicit conversion + * Returns null if no conversion is needed. + * + * @param formal expected formal type + * @param actual provided argument type + * @return null if no conversion is needed, or the appropriate Converter object + * @since 2.1 + */ + Converter getNeededConverter(Type formal, Class actual); + + /** + * Add the given converter to the handler. Implementation should be thread-safe. + * + * @param formal expected formal type + * @param actual provided argument type + * @param converter converter + * @since 2.1 + */ + void addConverter(Type formal, Class actual, Converter converter); + +} diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/TypeConversionHandlerImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/TypeConversionHandlerImpl.java new file mode 100644 index 00000000..9ee2c61d --- /dev/null +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/TypeConversionHandlerImpl.java @@ -0,0 +1,658 @@ +package org.apache.velocity.util.introspection; + +/* + * 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 org.apache.commons.lang3.LocaleUtils; +import org.apache.commons.lang3.reflect.TypeUtils; +import org.apache.commons.lang3.tuple.Pair; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A conversion handler adds admissible conversions between Java types whenever Velocity introspection has to map + * VTL methods and property accessors to Java methods. This implementation is the default Conversion Handler + * for Velocity. + * + * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a> + * @version $Id: TypeConversionHandlerImpl.java $ + * @since 2.0 + */ + +public class TypeConversionHandlerImpl implements TypeConversionHandler +{ + /** + * standard narrowing and string parsing conversions. + */ + static Map<Pair<String, String>, Converter> standardConverterMap; + + /** + * basic toString converter + */ + static Converter toString; + + /** + * cache miss converter + */ + static Converter cacheMiss; + + /** + * min/max byte/short/int values as long + */ + static final long minByte = Byte.MIN_VALUE, maxByte = Byte.MAX_VALUE, + minShort = Short.MIN_VALUE, maxShort = Short.MAX_VALUE, + minInt = Integer.MIN_VALUE, maxInt = Integer.MAX_VALUE; + + /** + * min/max long values as double + */ + static final double minLong = Long.MIN_VALUE, maxLong = Long.MAX_VALUE; + + /** + * a converters cache map, initialized with the standard narrowing and string parsing conversions. + */ + Map<Pair<String, String>, Converter> converterCacheMap; + + static final String BOOLEAN_TYPE = "boolean"; + static final String BYTE_TYPE = "byte"; + static final String SHORT_TYPE = "short"; + static final String INTEGER_TYPE = "int"; + static final String LONG_TYPE = "long"; + static final String FLOAT_TYPE = "float"; + static final String DOUBLE_TYPE = "double"; + static final String CHARACTER_TYPE = "char"; + static final String BOOLEAN_CLASS = "java.lang.Boolean"; + static final String BYTE_CLASS = "java.lang.Byte"; + static final String SHORT_CLASS = "java.lang.Short"; + static final String INTEGER_CLASS = "java.lang.Integer"; + static final String LONG_CLASS = "java.lang.Long"; + static final String FLOAT_CLASS = "java.lang.Float"; + static final String DOUBLE_CLASS = "java.lang.Double"; + static final String NUMBER_CLASS = "java.lang.Number"; + static final String CHARACTER_CLASS = "java.lang.Character"; + static final String STRING_CLASS = "java.lang.String"; + static final String LOCALE_CLASS = "java.util.Locale"; + + static + { + standardConverterMap = new HashMap<>(); + + cacheMiss = new Converter<Object>() + { + @Override + public Object convert(Object o) + { + return o; + } + }; + + /* number -> boolean */ + Converter<Boolean> numberToBool = new Converter<Boolean>() + { + @Override + public Boolean convert(Object o) + { + return o == null ? null : ((Number) o).intValue() != 0; + } + }; + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, BYTE_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, SHORT_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, INTEGER_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, LONG_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, FLOAT_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, DOUBLE_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, NUMBER_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, BYTE_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, SHORT_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, INTEGER_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, LONG_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, FLOAT_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, DOUBLE_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, BYTE_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, SHORT_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, INTEGER_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, LONG_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, FLOAT_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, DOUBLE_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, NUMBER_CLASS), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, BYTE_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, SHORT_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, INTEGER_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, LONG_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, FLOAT_TYPE), numberToBool); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, DOUBLE_TYPE), numberToBool); + + /* character -> boolean */ + Converter<Boolean> charToBoolean = new Converter<Boolean>() + { + @Override + public Boolean convert(Object o) + { + return o == null ? null : (Character) o != 0; + } + }; + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, CHARACTER_CLASS), charToBoolean); + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, CHARACTER_TYPE), charToBoolean); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, CHARACTER_CLASS), charToBoolean); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, CHARACTER_TYPE), charToBoolean); + + /* string -> boolean */ + Converter<Boolean> stringToBoolean = new Converter<Boolean>() + { + @Override + public Boolean convert(Object o) + { + return Boolean.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(BOOLEAN_CLASS, STRING_CLASS), stringToBoolean); + standardConverterMap.put(Pair.of(BOOLEAN_TYPE, STRING_CLASS), stringToBoolean); + + /* narrowing towards byte */ + Converter<Byte> narrowingToByte = new Converter<Byte>() + { + @Override + public Byte convert(Object o) + { + if (o == null) return null; + long l = ((Number)o).longValue(); + if (l < minByte || l > maxByte) + { + throw new NumberFormatException("value out of range for byte type: " + l); + } + return ((Number) o).byteValue(); + } + }; + standardConverterMap.put(Pair.of(BYTE_CLASS, SHORT_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, INTEGER_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, LONG_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, FLOAT_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, DOUBLE_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, NUMBER_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, SHORT_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, INTEGER_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, LONG_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, FLOAT_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, DOUBLE_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, SHORT_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, INTEGER_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, LONG_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, FLOAT_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, DOUBLE_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, NUMBER_CLASS), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, SHORT_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, INTEGER_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, LONG_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, FLOAT_TYPE), narrowingToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, DOUBLE_TYPE), narrowingToByte); + + /* string to byte */ + Converter<Byte> stringToByte = new Converter<Byte>() + { + @Override + public Byte convert(Object o) + { + return Byte.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(BYTE_CLASS, STRING_CLASS), stringToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, STRING_CLASS), stringToByte); + + /* narrowing towards short */ + Converter<Short> narrowingToShort = new Converter<Short>() + { + @Override + public Short convert(Object o) + { + if (o == null) return null; + long l = ((Number)o).longValue(); + if (l < minShort || l > maxShort) + { + throw new NumberFormatException("value out of range for short type: " + l); + } + return ((Number) o).shortValue(); + } + }; + standardConverterMap.put(Pair.of(SHORT_CLASS, INTEGER_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, LONG_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, FLOAT_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, DOUBLE_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, NUMBER_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, INTEGER_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, LONG_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, FLOAT_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, DOUBLE_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, INTEGER_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, LONG_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, FLOAT_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, DOUBLE_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, NUMBER_CLASS), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, INTEGER_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, LONG_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, FLOAT_TYPE), narrowingToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, DOUBLE_TYPE), narrowingToShort); + + /* string to short */ + Converter<Short> stringToShort = new Converter<Short>() + { + @Override + public Short convert(Object o) + { + return Short.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(SHORT_CLASS, STRING_CLASS), stringToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, STRING_CLASS), stringToShort); + + /* narrowing towards int */ + Converter<Integer> narrowingToInteger = new Converter<Integer>() + { + @Override + public Integer convert(Object o) + { + if (o == null) return null; + long l = ((Number)o).longValue(); + if (l < minInt || l > maxInt) + { + throw new NumberFormatException("value out of range for integer type: " + l); + } + return ((Number) o).intValue(); + } + }; + standardConverterMap.put(Pair.of(INTEGER_CLASS, LONG_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, FLOAT_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, DOUBLE_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, NUMBER_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, LONG_TYPE), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, FLOAT_TYPE), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, DOUBLE_TYPE), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, LONG_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, FLOAT_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, DOUBLE_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, NUMBER_CLASS), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, LONG_TYPE), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, FLOAT_TYPE), narrowingToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, DOUBLE_TYPE), narrowingToInteger); + + /* widening towards Integer */ + Converter<Integer> wideningToInteger = new Converter<Integer>() + { + @Override + public Integer convert(Object o) + { + if (o == null) return null; + return ((Number) o).intValue(); + } + }; + standardConverterMap.put(Pair.of(INTEGER_CLASS, SHORT_CLASS), wideningToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, SHORT_TYPE), wideningToInteger); + + /* string to int */ + Converter<Integer> stringToInteger = new Converter<Integer>() + { + @Override + public Integer convert(Object o) + { + return Integer.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(INTEGER_CLASS, STRING_CLASS), stringToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, STRING_CLASS), stringToInteger); + + /* narrowing towards long */ + Converter<Long> narrowingToLong = new Converter<Long>() + { + @Override + public Long convert(Object o) + { + if (o == null) return null; + double d = ((Number)o).doubleValue(); + if (d < minLong || d > maxLong) + { + throw new NumberFormatException("value out of range for long type: " + d); + } + return ((Number) o).longValue(); + } + }; + standardConverterMap.put(Pair.of(LONG_CLASS, FLOAT_CLASS), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, DOUBLE_CLASS), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, NUMBER_CLASS), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, FLOAT_TYPE), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, DOUBLE_TYPE), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, FLOAT_CLASS), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, DOUBLE_CLASS), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, NUMBER_CLASS), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, FLOAT_TYPE), narrowingToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, DOUBLE_TYPE), narrowingToLong); + + /* widening towards Long */ + Converter<Long> wideningToLong = new Converter<Long>() + { + @Override + public Long convert(Object o) + { + if (o == null) return null; + return ((Number) o).longValue(); + } + }; + standardConverterMap.put(Pair.of(LONG_CLASS, SHORT_CLASS), wideningToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, INTEGER_CLASS), wideningToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, SHORT_TYPE), wideningToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, INTEGER_TYPE), wideningToLong); + + /* string to long */ + Converter<Long> stringToLong = new Converter<Long>() + { + @Override + public Long convert(Object o) + { + return Long.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(LONG_CLASS, STRING_CLASS), stringToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, STRING_CLASS), stringToLong); + + /* narrowing towards float */ + Converter<Float> narrowingToFloat = new Converter<Float>() + { + @Override + public Float convert(Object o) + { + return o == null ? null : ((Number) o).floatValue(); + } + }; + standardConverterMap.put(Pair.of(FLOAT_CLASS, DOUBLE_CLASS), narrowingToFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, NUMBER_CLASS), narrowingToFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, DOUBLE_TYPE), narrowingToFloat); + standardConverterMap.put(Pair.of(FLOAT_TYPE, DOUBLE_CLASS), narrowingToFloat); + standardConverterMap.put(Pair.of(FLOAT_TYPE, NUMBER_CLASS), narrowingToFloat); + standardConverterMap.put(Pair.of(FLOAT_TYPE, DOUBLE_TYPE), narrowingToFloat); + + /* exact towards Float */ + Converter<Float> toFloat = new Converter<Float>() + { + @Override + public Float convert(Object o) + { + if (o == null) return null; + return ((Number) o).floatValue(); + } + }; + standardConverterMap.put(Pair.of(FLOAT_CLASS, SHORT_CLASS), toFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, INTEGER_CLASS), toFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, LONG_CLASS), toFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, SHORT_TYPE), toFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, INTEGER_TYPE), toFloat); + standardConverterMap.put(Pair.of(FLOAT_CLASS, LONG_TYPE), toFloat); + + /* string to float */ + Converter<Float> stringToFloat = new Converter<Float>() + { + @Override + public Float convert(Object o) + { + return Float.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(FLOAT_CLASS, STRING_CLASS), stringToFloat); + standardConverterMap.put(Pair.of(FLOAT_TYPE, STRING_CLASS), stringToFloat); + + /* exact or widening towards Double */ + Converter<Double> toDouble = new Converter<Double>() + { + @Override + public Double convert(Object o) + { + if (o == null) return null; + return ((Number) o).doubleValue(); + } + }; + standardConverterMap.put(Pair.of(DOUBLE_CLASS, SHORT_CLASS), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, INTEGER_CLASS), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, LONG_CLASS), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, FLOAT_CLASS), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, NUMBER_CLASS), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, SHORT_TYPE), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, INTEGER_TYPE), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, LONG_TYPE), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_CLASS, FLOAT_TYPE), toDouble); + standardConverterMap.put(Pair.of(DOUBLE_TYPE, NUMBER_CLASS), toDouble); + + /* string to double */ + Converter<Double> stringToDouble = new Converter<Double>() + { + @Override + public Double convert(Object o) + { + return Double.valueOf(String.valueOf(o)); + } + }; + standardConverterMap.put(Pair.of(DOUBLE_CLASS, STRING_CLASS), stringToDouble); + standardConverterMap.put(Pair.of(DOUBLE_TYPE, STRING_CLASS), stringToDouble); + + /* boolean to byte */ + Converter<Byte> booleanToByte = new Converter<Byte>() + { + @Override + public Byte convert(Object o) + { + return o == null ? null : (Boolean) o ? (byte)1 : (byte)0; + } + }; + standardConverterMap.put(Pair.of(BYTE_CLASS, BOOLEAN_CLASS), booleanToByte); + standardConverterMap.put(Pair.of(BYTE_CLASS, BOOLEAN_TYPE), booleanToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, BOOLEAN_CLASS), booleanToByte); + standardConverterMap.put(Pair.of(BYTE_TYPE, BOOLEAN_TYPE), booleanToByte); + + /* boolean to short */ + Converter<Short> booleanToShort = new Converter<Short>() + { + @Override + public Short convert(Object o) + { + return o == null ? null : (Boolean) o ? (short)1 : (short)0; + } + }; + standardConverterMap.put(Pair.of(SHORT_CLASS, BOOLEAN_CLASS), booleanToShort); + standardConverterMap.put(Pair.of(SHORT_CLASS, BOOLEAN_TYPE), booleanToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, BOOLEAN_CLASS), booleanToShort); + standardConverterMap.put(Pair.of(SHORT_TYPE, BOOLEAN_TYPE), booleanToShort); + + /* boolean to integer */ + Converter<Integer> booleanToInteger = new Converter<Integer>() + { + @Override + public Integer convert(Object o) + { + return o == null ? null : (Boolean) o ? (Integer)1 : (Integer)0; + } + }; + standardConverterMap.put(Pair.of(INTEGER_CLASS, BOOLEAN_CLASS), booleanToInteger); + standardConverterMap.put(Pair.of(INTEGER_CLASS, BOOLEAN_TYPE), booleanToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, BOOLEAN_CLASS), booleanToInteger); + standardConverterMap.put(Pair.of(INTEGER_TYPE, BOOLEAN_TYPE), booleanToInteger); + + /* boolean to lonf */ + Converter<Long> booleanToLong = new Converter<Long>() + { + @Override + public Long convert(Object o) + { + return o == null ? null : (Boolean) o ? 1L : 0L; + } + }; + standardConverterMap.put(Pair.of(LONG_CLASS, BOOLEAN_CLASS), booleanToLong); + standardConverterMap.put(Pair.of(LONG_CLASS, BOOLEAN_TYPE), booleanToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, BOOLEAN_CLASS), booleanToLong); + standardConverterMap.put(Pair.of(LONG_TYPE, BOOLEAN_TYPE), booleanToLong); + + /* to string */ + toString = new Converter<String>() + { + @Override + public String convert(Object o) + { + return String.valueOf(o); + } + }; + + /* string to locale */ + Converter<Locale> stringToLocale = new Converter<Locale>() + { + @Override + public Locale convert(Object o) + { + return o == null ? null : LocaleUtils.toLocale((String)o); + } + }; + standardConverterMap.put(Pair.of(LOCALE_CLASS, STRING_CLASS), stringToLocale); + } + + /** + * Constructor + */ + public TypeConversionHandlerImpl() + { + converterCacheMap = new ConcurrentHashMap<>(); + } + + /** + * Check to see if the conversion can be done using an explicit conversion + * @param actual found argument type + * @param formal expected formal type + * @return true if actual class can be explicitely converted to expected formal type + * @since 2.1 + */ + @Override + public boolean isExplicitlyConvertible(Type formal, Class actual, boolean possibleVarArg) + { + /* + * for consistency, we also have to check standard implicit convertibility + * since it may not have been checked before by the calling code + */ + Class formalClass = IntrospectionUtils.getTypeClass(formal); + if (formalClass != null && formalClass == actual || + IntrospectionUtils.isMethodInvocationConvertible(formal, actual, possibleVarArg) || + getNeededConverter(formal, actual) != null) + { + return true; + } + + /* Check var arg */ + if (possibleVarArg && TypeUtils.isArrayType(formal)) + { + if (actual.isArray()) + { + actual = actual.getComponentType(); + } + return isExplicitlyConvertible(TypeUtils.getArrayComponentType(formal), actual, false); + } + return false; + } + + + /** + * Returns the appropriate Converter object needed for an explicit conversion + * Returns null if no conversion is needed. + * + * @param actual found argument type + * @param formal expected formal type + * @return null if no conversion is needed, or the appropriate Converter object + * @since 2.1 + */ + @Override + public Converter getNeededConverter(Type formal, Class actual) + { + if (actual == null) + { + return null; + } + Pair<String, String> key = Pair.of(formal.getTypeName(), actual.getTypeName()); + + /* first check for a standard conversion */ + Converter converter = standardConverterMap.get(key); + if (converter == null) + { + /* then the converters cache map */ + converter = converterCacheMap.get(key); + if (converter == null) + { + Class formalClass = IntrospectionUtils.getTypeClass(formal); + /* check for conversion towards string */ + if (formal == String.class) + { + converter = toString; + } + /* check for String -> Enum constant conversion */ + else if (formalClass != null && formalClass.isEnum() && actual == String.class) + { + final Class<Enum> enumClass = (Class<Enum>)formalClass; + converter = new Converter() + { + @Override + public Object convert(Object o) + { + return Enum.valueOf(enumClass, (String) o); + } + }; + } + + converterCacheMap.put(key, converter == null ? cacheMiss : converter); + } + } + return converter == cacheMiss ? null : converter; + } + + /** + * Add the given converter to the handler. + * + * @param formal expected formal type + * @param actual provided argument type + * @param converter converter + * @since 2.1 + */ + @Override + public void addConverter(Type formal, Class actual, Converter converter) + { + Pair<String, String> key = Pair.of(formal.getTypeName(), actual.getTypeName()); + converterCacheMap.put(key, converter); + Class formalClass = IntrospectionUtils.getTypeClass(formal); + if (formalClass != null) + { + if (formalClass.isPrimitive()) + { + key = Pair.of(IntrospectionUtils.getBoxedClass(formalClass).getTypeName(), actual.getTypeName()); + converterCacheMap.put(key, converter); + } + else + { + Class unboxedFormal = IntrospectionUtils.getUnboxedClass(formalClass); + if (unboxedFormal != formalClass) + { + key = Pair.of(unboxedFormal.getTypeName(), actual.getTypeName()); + converterCacheMap.put(key, converter); + } + } + } + } +} diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/UberspectImpl.java b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/UberspectImpl.java index cdda364d..03127892 100644 --- a/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/UberspectImpl.java +++ b/velocity-engine-core/src/main/java/org/apache/velocity/util/introspection/UberspectImpl.java @@ -19,6 +19,7 @@ package org.apache.velocity.util.introspection; * under the License. */ +import org.apache.commons.lang3.Conversion; import org.apache.velocity.exception.VelocityException; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeServices; @@ -42,6 +43,7 @@ import org.slf4j.Logger; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; @@ -69,7 +71,7 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware /** * the conversion handler */ - protected ConversionHandler conversionHandler; + protected TypeConversionHandler conversionHandler; /** * runtime services @@ -86,7 +88,7 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware introspector = new Introspector(log, conversionHandler); } - public ConversionHandler getConversionHandler() + public TypeConversionHandler getConversionHandler() { return conversionHandler; } @@ -95,51 +97,86 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware * sets the runtime services * @param rs runtime services */ + @SuppressWarnings("deprecation") public void setRuntimeServices(RuntimeServices rs) { rsvc = rs; log = rsvc.getLog("introspection"); - String conversionHandlerClass = rs.getString(RuntimeConstants.CONVERSION_HANDLER_CLASS); - if (conversionHandlerClass == null || conversionHandlerClass.equals("none")) + Object conversionHandlerInstance = rs.getProperty(RuntimeConstants.CONVERSION_HANDLER_INSTANCE); + if (conversionHandlerInstance == null) { - conversionHandler = null; - } - else - { - Object o = null; - - try + String conversionHandlerClass = rs.getString(RuntimeConstants.CONVERSION_HANDLER_CLASS); + if (conversionHandlerClass != null && !conversionHandlerClass.equals("none")) { - o = ClassUtils.getNewInstance(conversionHandlerClass); - } - catch (ClassNotFoundException cnfe ) - { - String err = "The specified class for ConversionHandler (" + conversionHandlerClass + try + { + conversionHandlerInstance = ClassUtils.getNewInstance(conversionHandlerClass); + } + catch (ClassNotFoundException cnfe ) + { + String err = "The specified class for ConversionHandler (" + conversionHandlerClass + ") does not exist or is not accessible to the current classloader."; - log.error(err); - throw new VelocityException(err, cnfe); - } - catch (InstantiationException ie) - { - throw new VelocityException("Could not instantiate class '" + conversionHandlerClass + "'", ie); + log.error(err); + throw new VelocityException(err, cnfe); + } + catch (InstantiationException ie) + { + throw new VelocityException("Could not instantiate class '" + conversionHandlerClass + "'", ie); + } + catch (IllegalAccessException ae) + { + throw new VelocityException("Cannot access class '" + conversionHandlerClass + "'", ae); + } } - catch (IllegalAccessException ae) + } + + if (conversionHandlerInstance != null) + { + if (conversionHandlerInstance instanceof ConversionHandler) { - throw new VelocityException("Cannot access class '" + conversionHandlerClass + "'", ae); - } + log.warn("The ConversionHandler interface is deprecated - see the TypeConversionHandler interface"); + final ConversionHandler ch = (ConversionHandler)conversionHandlerInstance; + conversionHandler = new TypeConversionHandler() + { + @Override + public boolean isExplicitlyConvertible(Type formal, Class actual, boolean possibleVarArg) + { + Class formalClass = IntrospectionUtils.getTypeClass(formal); + if (formalClass != null) return ch.isExplicitlyConvertible(formalClass, actual, possibleVarArg); + else return false; + } - if (!(o instanceof ConversionHandler)) + @Override + public Converter getNeededConverter(Type formal, Class actual) + { + Class formalClass = IntrospectionUtils.getTypeClass(formal); + if (formalClass != null) return ch.getNeededConverter(formalClass, actual); + else return null; + } + + @Override + public void addConverter(Type formal, Class actual, Converter converter) + { + Class formalClass = IntrospectionUtils.getTypeClass(formal); + if (formalClass != null) ch.addConverter(formalClass, actual, converter); + else throw new UnsupportedOperationException("This conversion handler doesn't know how to handle Type: " + formal.getTypeName()); + } + }; + } + else if (!(conversionHandlerInstance instanceof TypeConversionHandler)) { - String err = "The specified class for ResourceManager (" + conversionHandlerClass - + ") does not implement " + ConversionHandler.class.getName() + String err = "The specified class or provided instance for the conversion handler (" + conversionHandlerInstance.getClass().getName() + + ") does not implement " + TypeConversionHandler.class.getName() + "; Velocity is not initialized correctly."; log.error(err); throw new VelocityException(err); } - - conversionHandler = (ConversionHandler) o; + else + { + conversionHandler = (TypeConversionHandler)conversionHandlerInstance; + } } } @@ -256,7 +293,7 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware Method m = introspector.getMethod(obj.getClass(), methodName, args); if (m != null) { - return new VelMethodImpl(m, false, getNeededConverters(m.getParameterTypes(), args)); + return new VelMethodImpl(m, false, getNeededConverters(m.getGenericParameterTypes(), args)); } Class cls = obj.getClass(); @@ -269,7 +306,7 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware { // and create a method that knows to wrap the value // before invoking the method - return new VelMethodImpl(m, true, getNeededConverters(m.getParameterTypes(), args)); + return new VelMethodImpl(m, true, getNeededConverters(m.getGenericParameterTypes(), args)); } } // watch for classes, to allow calling their static methods (VELOCITY-102) @@ -278,7 +315,7 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware m = introspector.getMethod((Class)obj, methodName, args); if (m != null) { - return new VelMethodImpl(m, false, getNeededConverters(m.getParameterTypes(), args)); + return new VelMethodImpl(m, false, getNeededConverters(m.getGenericParameterTypes(), args)); } } return null; @@ -288,7 +325,7 @@ public class UberspectImpl implements Uberspect, RuntimeServicesAware * get the list of needed converters to adapt passed argument types to method types * @return null if not conversion needed, otherwise an array containing needed converters */ - private Converter[] getNeededConverters(Class[] expected, Object[] provided) + private Converter[] getNeededConverters(Type[] expected, Object[] provided) { if (conversionHandler == null) return null; // var args are not handled here - CB TODO diff --git a/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties b/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties index ffc91fe0..b29d7e66 100644 --- a/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties +++ b/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties @@ -210,7 +210,7 @@ runtime.introspector.uberspect = org.apache.velocity.util.introspection.Uberspec # Sets the data types Conversion Handler used by the default uberspector # ---------------------------------------------------------------------------- -runtime.conversion.handler.class = org.apache.velocity.util.introspection.ConversionHandlerImpl +runtime.conversion.handler.class = org.apache.velocity.util.introspection.TypeConversionHandlerImpl # ---------------------------------------------------------------------------- diff --git a/velocity-engine-core/src/test/java/org/apache/velocity/test/util/introspection/ConversionHandlerTestCase.java b/velocity-engine-core/src/test/java/org/apache/velocity/test/util/introspection/ConversionHandlerTestCase.java index 6856e83e..13142141 100644 --- a/velocity-engine-core/src/test/java/org/apache/velocity/test/util/introspection/ConversionHandlerTestCase.java +++ b/velocity-engine-core/src/test/java/org/apache/velocity/test/util/introspection/ConversionHandlerTestCase.java @@ -19,6 +19,8 @@ package org.apache.velocity.test.util.introspection; import junit.framework.TestSuite; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.TypeUtils; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.Velocity; @@ -28,11 +30,11 @@ import org.apache.velocity.context.Context; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeInstance; import org.apache.velocity.test.BaseTestCase; -import org.apache.velocity.util.introspection.ConversionHandler; -import org.apache.velocity.util.introspection.ConversionHandlerImpl; import org.apache.velocity.util.introspection.Converter; import org.apache.velocity.util.introspection.Info; import org.apache.velocity.util.introspection.IntrospectionUtils; +import org.apache.velocity.util.introspection.TypeConversionHandler; +import org.apache.velocity.util.introspection.TypeConversionHandlerImpl; import org.apache.velocity.util.introspection.Uberspect; import org.apache.velocity.util.introspection.UberspectImpl; @@ -41,6 +43,9 @@ import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.Writer; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -114,7 +119,7 @@ public class ConversionHandlerTestCase extends BaseTestCase Uberspect uberspect = ve.getUberspect(); assertTrue(uberspect instanceof UberspectImpl); UberspectImpl ui = (UberspectImpl)uberspect; - ConversionHandler ch = ui.getConversionHandler(); + TypeConversionHandler ch = ui.getConversionHandler(); assertTrue(ch != null); ch.addConverter(Float.class, Obj.class, new Converter<Float>() { @@ -124,11 +129,87 @@ public class ConversionHandlerTestCase extends BaseTestCase return 4.5f; } }); + ch.addConverter(TypeUtils.parameterize(List.class, Integer.class), String.class, new Converter<List<Integer>>() + { + @Override + public List<Integer> convert(Object o) + { + return Arrays.<Integer>asList(1,2,3); + } + }); + ch.addConverter(TypeUtils.parameterize(List.class, String.class), String.class, new Converter<List<String>>() + { + @Override + public List<String> convert(Object o) + { + return Arrays.<String>asList("a", "b", "c"); + } + }); VelocityContext context = new VelocityContext(); context.put("obj", new Obj()); Writer writer = new StringWriter(); ve.evaluate(context, writer, "test", "$obj.integralFloat($obj) / $obj.objectFloat($obj)"); assertEquals("float ok: 4.5 / Float ok: 4.5", writer.toString()); + writer = new StringWriter(); + ve.evaluate(context, writer, "test", "$obj.iWantAStringList('anything')"); + assertEquals("correct", writer.toString()); + writer = new StringWriter(); + ve.evaluate(context, writer, "test", "$obj.iWantAnIntegerList('anything')"); + assertEquals("correct", writer.toString()); + } + + /* converts *everything* to string "foo" */ + public static class MyCustomConverter implements TypeConversionHandler + { + Converter<String> myCustomConverter = new Converter<String>() + { + + @Override + public String convert(Object o) + { + return "foo"; + } + }; + + @Override + public boolean isExplicitlyConvertible(Type formal, Class actual, boolean possibleVarArg) + { + return true; + } + + @Override + public Converter getNeededConverter(Type formal, Class actual) + { + return myCustomConverter; + } + + @Override + public void addConverter(Type formal, Class actual, Converter converter) + { + throw new RuntimeException("not implemented"); + } + } + + public void testCustomConversionHandlerInstance() + { + RuntimeInstance ve = new RuntimeInstance(); + ve.setProperty( Velocity.VM_PERM_INLINE_LOCAL, Boolean.TRUE); + ve.setProperty(Velocity.RUNTIME_LOG_INSTANCE, log); + ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "file"); + ve.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, TEST_COMPARE_DIR + "/conversion"); + ve.setProperty(RuntimeConstants.CONVERSION_HANDLER_INSTANCE, new MyCustomConverter()); + ve.init(); + Uberspect uberspect = ve.getUberspect(); + assertTrue(uberspect instanceof UberspectImpl); + UberspectImpl ui = (UberspectImpl)uberspect; + TypeConversionHandler ch = ui.getConversionHandler(); + assertTrue(ch != null); + assertTrue(ch instanceof MyCustomConverter); + VelocityContext context = new VelocityContext(); + context.put("obj", new Obj()); + Writer writer = new StringWriter(); + ve.evaluate(context, writer, "test", "$obj.objectString(1.0)"); + assertEquals("String ok: foo", writer.toString()); } /** @@ -174,6 +255,15 @@ public class ConversionHandlerTestCase extends BaseTestCase } } + public void testOtherConversions() throws Exception + { + VelocityEngine ve = createEngine(false); + VelocityContext context = createContext(); + StringWriter writer = new StringWriter(); + ve.evaluate(context, writer,"test", "$strings.join(['foo', 'bar'], ',')"); + assertEquals("foo,bar", writer.toString()); + } + /** * Return and initialize engine * @return @@ -264,6 +354,7 @@ public class ConversionHandlerTestCase extends BaseTestCase }; context.put("types", types); context.put("introspect", new Introspect()); + context.put("strings", new StringUtils()); return context; } @@ -294,14 +385,28 @@ public class ConversionHandlerTestCase extends BaseTestCase public String locale(Locale loc) { return "Locale ok: " + loc; } public String toString() { return "instance of Obj"; } + + public String iWantAStringList(List<String> list) + { + if (list != null && list.size() == 3 && list.get(0).equals("a") && list.get(1).equals("b") && list.get(2).equals("c")) + return "correct"; + else return "wrong"; + } + + public String iWantAnIntegerList(List<Integer> list) + { + if (list != null && list.size() == 3 && list.get(0).equals(1) && list.get(1).equals(2) && list.get(2).equals(3)) + return "correct"; + else return "wrong"; + } } public static class Introspect { - private ConversionHandler handler; + private TypeConversionHandler handler; public Introspect() { - handler = new ConversionHandlerImpl(); + handler = new TypeConversionHandlerImpl(); } public boolean isStrictlyConvertible(Class expected, Class provided) { |