1 package org.apache.velocity.test.util.introspection; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import junit.framework.TestSuite; 23 import org.apache.commons.lang3.StringUtils; 24 import org.apache.commons.lang3.reflect.TypeUtils; 25 import org.apache.velocity.Template; 26 import org.apache.velocity.VelocityContext; 27 import org.apache.velocity.app.Velocity; 28 import org.apache.velocity.app.VelocityEngine; 29 import org.apache.velocity.app.event.MethodExceptionEventHandler; 30 import org.apache.velocity.context.Context; 31 import org.apache.velocity.runtime.RuntimeConstants; 32 import org.apache.velocity.runtime.RuntimeInstance; 33 import org.apache.velocity.test.BaseTestCase; 34 import org.apache.velocity.test.misc.TestLogger; 35 import org.apache.velocity.util.introspection.Converter; 36 import org.apache.velocity.util.introspection.Info; 37 import org.apache.velocity.util.introspection.IntrospectionUtils; 38 import org.apache.velocity.util.introspection.TypeConversionHandler; 39 import org.apache.velocity.util.introspection.TypeConversionHandlerImpl; 40 import org.apache.velocity.util.introspection.Uberspect; 41 import org.apache.velocity.util.introspection.UberspectImpl; 42 43 import java.io.BufferedWriter; 44 import java.io.FileOutputStream; 45 import java.io.OutputStreamWriter; 46 import java.io.StringWriter; 47 import java.io.Writer; 48 import java.lang.reflect.Type; 49 import java.math.BigDecimal; 50 import java.math.BigInteger; 51 import java.util.Arrays; 52 import java.util.List; 53 import java.util.Locale; 54 import java.util.Map; 55 import java.util.TreeMap; 56 57 /** 58 * Test case for conversion handler 59 */ 60 public class ConversionHandlerTestCase extends BaseTestCase 61 { 62 private static final String RESULT_DIR = TEST_RESULT_DIR + "/conversion"; 63 64 private static final String COMPARE_DIR = TEST_COMPARE_DIR + "/conversion/compare"; 65 ConversionHandlerTestCase(String name)66 public ConversionHandlerTestCase(String name) 67 { 68 super(name); 69 } 70 71 @Override setUp()72 public void setUp() 73 throws Exception 74 { 75 super.setUp(); 76 } 77 78 /** 79 * Test suite 80 * @return test suite 81 */ suite()82 public static junit.framework.Test suite() 83 { 84 return new TestSuite(ConversionHandlerTestCase.class); 85 } 86 testConversionsWithoutHandler()87 public void testConversionsWithoutHandler() 88 throws Exception 89 { 90 /* 91 * local scope, cache on 92 */ 93 VelocityEngine ve = createEngine(false); 94 95 testConversions(ve, "test_conv.vtl", "test_conv_without_handler"); 96 } 97 testConversionsWithHandler()98 public void testConversionsWithHandler() 99 throws Exception 100 { 101 /* 102 * local scope, cache on 103 */ 104 VelocityEngine ve = createEngine(true); 105 106 testConversions(ve, "test_conv.vtl", "test_conv_with_handler"); 107 } 108 testConversionMatrix()109 public void testConversionMatrix() 110 throws Exception 111 { 112 VelocityEngine ve = createEngine(true); 113 testConversions(ve, "matrix.vhtml", "matrix"); 114 } 115 testCustomConverter()116 public void testCustomConverter() 117 { 118 RuntimeInstance ve = new RuntimeInstance(); 119 ve.setProperty( Velocity.VM_PERM_INLINE_LOCAL, Boolean.TRUE); 120 ve.setProperty(Velocity.RUNTIME_LOG_INSTANCE, log); 121 ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "file"); 122 ve.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, TEST_COMPARE_DIR + "/conversion"); 123 ve.init(); 124 Uberspect uberspect = ve.getUberspect(); 125 assertTrue(uberspect instanceof UberspectImpl); 126 UberspectImpl ui = (UberspectImpl)uberspect; 127 TypeConversionHandler ch = ui.getConversionHandler(); 128 assertTrue(ch != null); 129 ch.addConverter(Float.class, Obj.class, new Converter<Float>() 130 { 131 @Override 132 public Float convert(Object o) 133 { 134 return 4.5f; 135 } 136 }); 137 ch.addConverter(TypeUtils.parameterize(List.class, Integer.class), String.class, new Converter<List<Integer>>() 138 { 139 @Override 140 public List<Integer> convert(Object o) 141 { 142 return Arrays.<Integer>asList(1,2,3); 143 } 144 }); 145 ch.addConverter(TypeUtils.parameterize(List.class, String.class), String.class, new Converter<List<String>>() 146 { 147 @Override 148 public List<String> convert(Object o) 149 { 150 return Arrays.<String>asList("a", "b", "c"); 151 } 152 }); 153 VelocityContext context = new VelocityContext(); 154 context.put("obj", new Obj()); 155 Writer writer = new StringWriter(); 156 ve.evaluate(context, writer, "test", "$obj.integralFloat($obj) / $obj.objectFloat($obj)"); 157 assertEquals("float ok: 4.5 / Float ok: 4.5", writer.toString()); 158 writer = new StringWriter(); 159 ve.evaluate(context, writer, "test", "$obj.iWantAStringList('anything')"); 160 assertEquals("correct", writer.toString()); 161 writer = new StringWriter(); 162 ve.evaluate(context, writer, "test", "$obj.iWantAnIntegerList('anything')"); 163 assertEquals("correct", writer.toString()); 164 } 165 166 /* converts *everything* to string "foo" */ 167 public static class MyCustomConverter implements TypeConversionHandler 168 { 169 Converter<String> myCustomConverter = new Converter<String>() 170 { 171 172 @Override 173 public String convert(Object o) 174 { 175 return "foo"; 176 } 177 }; 178 179 @Override isExplicitlyConvertible(Type formal, Class<?> actual, boolean possibleVarArg)180 public boolean isExplicitlyConvertible(Type formal, Class<?> actual, boolean possibleVarArg) 181 { 182 return true; 183 } 184 185 @Override getNeededConverter(Type formal, Class<?> actual)186 public Converter<?> getNeededConverter(Type formal, Class<?> actual) 187 { 188 return myCustomConverter; 189 } 190 191 @Override addConverter(Type formal, Class<?> actual, Converter<?> converter)192 public void addConverter(Type formal, Class<?> actual, Converter<?> converter) 193 { 194 throw new RuntimeException("not implemented"); 195 } 196 } 197 testCustomConversionHandlerInstance()198 public void testCustomConversionHandlerInstance() 199 { 200 RuntimeInstance ve = new RuntimeInstance(); 201 ve.setProperty( Velocity.VM_PERM_INLINE_LOCAL, Boolean.TRUE); 202 ve.setProperty(Velocity.RUNTIME_LOG_INSTANCE, log); 203 ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "file"); 204 ve.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, TEST_COMPARE_DIR + "/conversion"); 205 ve.setProperty(RuntimeConstants.CONVERSION_HANDLER_INSTANCE, new MyCustomConverter()); 206 ve.init(); 207 Uberspect uberspect = ve.getUberspect(); 208 assertTrue(uberspect instanceof UberspectImpl); 209 UberspectImpl ui = (UberspectImpl)uberspect; 210 TypeConversionHandler ch = ui.getConversionHandler(); 211 assertTrue(ch != null); 212 assertTrue(ch instanceof MyCustomConverter); 213 VelocityContext context = new VelocityContext(); 214 context.put("obj", new Obj()); 215 Writer writer = new StringWriter(); 216 ve.evaluate(context, writer, "test", "$obj.objectString(1.0)"); 217 assertEquals("String ok: foo", writer.toString()); 218 } 219 220 /** 221 * Test conversions 222 * @param ve 223 * @param templateFile template 224 * @param outputBaseFileName 225 * @throws Exception 226 */ testConversions(VelocityEngine ve, String templateFile, String outputBaseFileName)227 private void testConversions(VelocityEngine ve, String templateFile, String outputBaseFileName) 228 throws Exception 229 { 230 assureResultsDirectoryExists(RESULT_DIR); 231 232 FileOutputStream fos = new FileOutputStream (getFileName( 233 RESULT_DIR, outputBaseFileName, RESULT_FILE_EXT)); 234 235 VelocityContext context = createContext(); 236 237 Writer writer = new BufferedWriter(new OutputStreamWriter(fos)); 238 239 log.setEnabledLevel(TestLogger.LOG_LEVEL_ERROR); 240 241 Template template = ve.getTemplate(templateFile); 242 template.merge(context, writer); 243 244 /* 245 * Write to the file 246 */ 247 writer.flush(); 248 writer.close(); 249 250 if (!isMatch(RESULT_DIR, COMPARE_DIR, outputBaseFileName, 251 RESULT_FILE_EXT,CMP_FILE_EXT)) 252 { 253 String result = getFileContents(RESULT_DIR, outputBaseFileName, RESULT_FILE_EXT); 254 String compare = getFileContents(COMPARE_DIR, outputBaseFileName, CMP_FILE_EXT); 255 256 String msg = "Processed template did not match expected output\n"+ 257 "-----Result-----\n"+ result + 258 "----Expected----\n"+ compare + 259 "----------------"; 260 261 fail(msg); 262 } 263 } 264 testOtherConversions()265 public void testOtherConversions() throws Exception 266 { 267 VelocityEngine ve = createEngine(false); 268 VelocityContext context = createContext(); 269 StringWriter writer = new StringWriter(); 270 ve.evaluate(context, writer,"test", "$strings.join(['foo', 'bar'], ',')"); 271 assertEquals("foo,bar", writer.toString()); 272 } 273 274 /** 275 * Return and initialize engine 276 * @return 277 */ createEngine(boolean withConversionsHandler)278 private VelocityEngine createEngine(boolean withConversionsHandler) 279 throws Exception 280 { 281 VelocityEngine ve = new VelocityEngine(); 282 ve.setProperty( Velocity.VM_PERM_INLINE_LOCAL, Boolean.TRUE); 283 ve.setProperty(Velocity.RUNTIME_LOG_INSTANCE, log); 284 ve.setProperty(RuntimeConstants.RESOURCE_LOADERS, "file"); 285 ve.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, TEST_COMPARE_DIR + "/conversion"); 286 if (withConversionsHandler) 287 { 288 ve.setProperty(RuntimeConstants.EVENTHANDLER_METHODEXCEPTION, PrintException.class.getName()); 289 } 290 else 291 { 292 ve.setProperty(RuntimeConstants.CONVERSION_HANDLER_CLASS, "none"); 293 } 294 ve.init(); 295 296 return ve; 297 } 298 299 public static class PrintException implements MethodExceptionEventHandler 300 { 301 @Override methodException(Context context, Class claz, String method, Exception e, Info info)302 public Object methodException(Context context, 303 Class claz, 304 String method, 305 Exception e, 306 Info info) 307 { 308 // JDK 11+ changed the exception message for big decimal conversion exceptions, 309 // which breaks the (brittle) tests. Clearly, it would be preferred to fix this 310 // right by comparing the result according to the JDK version, this is just a 311 // quick fix to get the build to pass on JDK 11+ 312 // 313 if (e.getClass() == NumberFormatException.class && e.getMessage() != null && e.getMessage().startsWith("Character")) 314 { 315 return method + " -> " + e.getClass().getSimpleName() + ": null"; // compatible with JDK8 316 } 317 318 return method + " -> " + e.getClass().getSimpleName() + ": " + e.getMessage(); 319 } 320 } 321 createContext()322 private VelocityContext createContext() 323 { 324 VelocityContext context = new VelocityContext(); 325 Map<String, Object> map = new TreeMap<>(); 326 map.put("A. bool-true", true); 327 map.put("B. bool-false", false); 328 map.put("C. byte-0", (byte)0); 329 map.put("D. byte-1", (byte)1); 330 map.put("E. short", (short)125); 331 map.put("F. int", 24323); 332 map.put("G. long", 5235235L); 333 map.put("H. float", 34523.345f); 334 map.put("I. double", 54235.3253d); 335 map.put("J. char", '@'); 336 map.put("K. object", new Obj()); 337 map.put("L. enum", Obj.Color.GREEN); 338 map.put("M. string", new String("foo")); 339 map.put("M. string-green", new String("green")); 340 map.put("N. string-empty", new String()); 341 map.put("O. string-false", new String("false")); 342 map.put("P. string-true", new String("true")); 343 map.put("Q. string-zero", new String("0")); 344 map.put("R. string-integral", new String("123")); 345 map.put("S. string-big-integral", new String("12345678")); 346 map.put("T. string-floating", new String("123.345")); 347 map.put("U. null", null); 348 map.put("V. locale", "fr_FR"); 349 map.put("W. BigInteger zero", BigInteger.ZERO); 350 map.put("X. BigInteger one", BigInteger.ONE); 351 map.put("Y. BigInteger ten", BigInteger.TEN); 352 map.put("Y. BigInteger bigint", new BigInteger("12345678901234567890")); 353 map.put("Z. BigDecimal zero", BigDecimal.ZERO); 354 map.put("ZA. BigDecimal one", BigDecimal.ONE); 355 map.put("ZB. BigDecimal ten", BigDecimal.TEN); 356 map.put("ZC. BigDecimal bigdec", new BigDecimal("12345678901234567890.01234567890123456789")); 357 context.put("map", map); 358 context.put("target", new Obj()); 359 Class[] types = 360 { 361 Boolean.TYPE, 362 Character.TYPE, 363 Byte.TYPE, 364 Short.TYPE, 365 Integer.TYPE, 366 Long.TYPE, 367 Float.TYPE, 368 Double.TYPE, 369 Boolean.class, 370 Character.class, 371 Byte.class, 372 Short.class, 373 Integer.class, 374 Long.class, 375 BigInteger.class, 376 Float.class, 377 Double.class, 378 BigDecimal.class, 379 Number.class, 380 String.class, 381 Object.class 382 }; 383 context.put("types", types); 384 context.put("introspect", new Introspect()); 385 context.put("strings", new StringUtils()); 386 return context; 387 } 388 389 public static class Obj 390 { 391 public enum Color { RED, GREEN } 392 integralBoolean(boolean b)393 public String integralBoolean(boolean b) { return "boolean ok: " + b; } integralByte(byte b)394 public String integralByte(byte b) { return "byte ok: " + b; } integralShort(short s)395 public String integralShort(short s) { return "short ok: " + s; } integralInt(int i)396 public String integralInt(int i) { return "int ok: " + i; } integralLong(long l)397 public String integralLong(long l) { return "long ok: " + l; } integralFloat(float f)398 public String integralFloat(float f) { return "float ok: " + f; } integralDouble(double d)399 public String integralDouble(double d) { return "double ok: " + d; } integralChar(char c)400 public String integralChar(char c) { return "char ok: " + c; } objectBoolean(Boolean b)401 public String objectBoolean(Boolean b) { return "Boolean ok: " + b; } objectByte(Byte b)402 public String objectByte(Byte b) { return "Byte ok: " + b; } objectShort(Short s)403 public String objectShort(Short s) { return "Short ok: " + s; } objectInt(Integer i)404 public String objectInt(Integer i) { return "Integer ok: " + i; } objectLong(Long l)405 public String objectLong(Long l) { return "Long ok: " + l; } objectBigInteger(BigInteger bi)406 public String objectBigInteger(BigInteger bi) { return "BigInteger ok: " + bi; } objectFloat(Float f)407 public String objectFloat(Float f) { return "Float ok: " + f; } objectDouble(Double d)408 public String objectDouble(Double d) { return "Double ok: " + d; } objectBigDecimal(BigDecimal bd)409 public String objectBigDecimal(BigDecimal bd) { return "BigDecimal ok: " + bd; } objectCharacter(Character c)410 public String objectCharacter(Character c) { return "Character ok: " + c; } objectNumber(Number b)411 public String objectNumber(Number b) { return "Number ok: " + b; } objectObject(Object o)412 public String objectObject(Object o) { return "Object ok: " + o; } objectString(String s)413 public String objectString(String s) { return "String ok: " + s; } objectEnum(Color c)414 public String objectEnum(Color c) { return "Enum ok: " + c; } locale(Locale loc)415 public String locale(Locale loc) { return "Locale ok: " + loc; } 416 toString()417 public String toString() { return "instance of Obj"; } 418 iWantAStringList(List<String> list)419 public String iWantAStringList(List<String> list) 420 { 421 if (list != null && list.size() == 3 && list.get(0).equals("a") && list.get(1).equals("b") && list.get(2).equals("c")) 422 return "correct"; 423 else return "wrong"; 424 } 425 iWantAnIntegerList(List<Integer> list)426 public String iWantAnIntegerList(List<Integer> list) 427 { 428 if (list != null && list.size() == 3 && list.get(0).equals(1) && list.get(1).equals(2) && list.get(2).equals(3)) 429 return "correct"; 430 else return "wrong"; 431 } 432 } 433 434 public static class Introspect 435 { 436 private TypeConversionHandler handler; Introspect()437 public Introspect() 438 { 439 handler = new TypeConversionHandlerImpl(); 440 } isStrictlyConvertible(Class expected, Class provided)441 public boolean isStrictlyConvertible(Class expected, Class provided) 442 { 443 return IntrospectionUtils.isStrictMethodInvocationConvertible(expected, provided, false); 444 } isImplicitlyConvertible(Class expected, Class provided)445 public boolean isImplicitlyConvertible(Class expected, Class provided) 446 { 447 return IntrospectionUtils.isMethodInvocationConvertible(expected, provided, false); 448 } isExplicitlyConvertible(Class expected, Class provided)449 public boolean isExplicitlyConvertible(Class expected, Class provided) 450 { 451 return handler.isExplicitlyConvertible(expected, provided, false); 452 } 453 } 454 } 455