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