View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.beanutils2.locale;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  
26  import java.lang.ref.ReferenceQueue;
27  import java.lang.ref.WeakReference;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.WeakHashMap;
31  
32  import org.apache.commons.beanutils2.BeanUtilsBean;
33  import org.apache.commons.beanutils2.ContextClassLoaderLocal;
34  import org.apache.commons.beanutils2.ConvertUtils;
35  import org.apache.commons.beanutils2.PrimitiveBean;
36  import org.apache.commons.beanutils2.locale.converters.LongLocaleConverter;
37  import org.apache.commons.logging.LogFactory;
38  import org.junit.jupiter.api.BeforeEach;
39  import org.junit.jupiter.api.Test;
40  
41  /**
42   * <p>
43   * Test Case for changes made during LocaleBeanutils Beanification. This is basically a cut-and-correct version of the BeanUtils beanifications tests.
44   * </p>
45   */
46  public class LocaleBeanificationTest {
47  
48      final class Signal {
49          private Exception e;
50          private int signal;
51          private LocaleBeanUtilsBean bean;
52          private LocaleConvertUtilsBean convertUtils;
53          private Object marker;
54  
55          public LocaleBeanUtilsBean getBean() {
56              return bean;
57          }
58  
59          public LocaleConvertUtilsBean getConvertUtils() {
60              return convertUtils;
61          }
62  
63          public Exception getException() {
64              return e;
65          }
66  
67          public Object getMarkerObject() {
68              return marker;
69          }
70  
71          public int getSignal() {
72              return signal;
73          }
74  
75          public void setBean(final LocaleBeanUtilsBean bean) {
76              this.bean = bean;
77          }
78  
79          public void setConvertUtils(final LocaleConvertUtilsBean convertUtils) {
80              this.convertUtils = convertUtils;
81          }
82  
83          public void setException(final Exception e) {
84              this.e = e;
85          }
86  
87          public void setMarkerObject(final Object marker) {
88              this.marker = marker;
89          }
90  
91          public void setSignal(final int signal) {
92              this.signal = signal;
93          }
94      }
95  
96      final class TestClassLoader extends ClassLoader {
97          @Override
98          public String toString() {
99              return "TestClassLoader";
100         }
101     }
102 
103     /** Maximum number of iterations before our test fails */
104     public static final int MAX_GC_ITERATIONS = 50;
105 
106     /**
107      * Sets up instance variables required by this test case.
108      */
109     @BeforeEach
110     public void setUp() {
111         LocaleConvertUtils.deregister();
112     }
113 
114     /** Tests whether different threads can set BeanUtils instances correctly */
115     @Test
116     public void testBeanUtilsBeanSetInstance() throws Exception {
117 
118         final class SetInstanceTesterThread extends Thread {
119 
120             private final Signal signal;
121             private final LocaleBeanUtilsBean bean;
122 
123             SetInstanceTesterThread(final Signal signal, final LocaleBeanUtilsBean bean) {
124                 this.signal = signal;
125                 this.bean = bean;
126             }
127 
128             @Override
129             public void run() {
130                 LocaleBeanUtilsBean.setInstance(bean);
131                 signal.setSignal(21);
132                 signal.setBean(LocaleBeanUtilsBean.getLocaleBeanUtilsInstance());
133             }
134 
135             @Override
136             public String toString() {
137                 return "SetInstanceTesterThread";
138             }
139         }
140 
141         final Signal signal = new Signal();
142         signal.setSignal(1);
143 
144         final LocaleBeanUtilsBean beanOne = new LocaleBeanUtilsBean();
145         final LocaleBeanUtilsBean beanTwo = new LocaleBeanUtilsBean();
146 
147         final SetInstanceTesterThread thread = new SetInstanceTesterThread(signal, beanTwo);
148         thread.setContextClassLoader(new TestClassLoader());
149 
150         LocaleBeanUtilsBean.setInstance(beanOne);
151         assertEquals(beanOne, LocaleBeanUtilsBean.getLocaleBeanUtilsInstance(), "Start thread gets right instance");
152 
153         thread.start();
154         thread.join();
155 
156         assertEquals(21, signal.getSignal(), "Signal not set by test thread");
157         assertEquals(beanOne, LocaleBeanUtilsBean.getLocaleBeanUtilsInstance(), "Second thread preserves value");
158         assertEquals(beanTwo, signal.getBean(), "Second thread gets value it set");
159     }
160 
161     /** Tests whether calls are independent for different class loaders */
162     @Test
163     public void testContextClassloaderIndependence() throws Exception {
164 
165         final class TestIndependenceThread extends Thread {
166             private final Signal signal;
167             private final PrimitiveBean bean;
168 
169             TestIndependenceThread(final Signal signal, final PrimitiveBean bean) {
170                 this.signal = signal;
171                 this.bean = bean;
172             }
173 
174             @Override
175             public void run() {
176                 try {
177                     signal.setSignal(3);
178                     LocaleConvertUtils.register(new LocaleConverter<Integer>() {
179                         @Override
180                         public <R> R convert(final Class<R> type, final Object value) {
181                             return ConvertUtils.primitiveToWrapper(type).cast(9);
182                         }
183 
184                         @Override
185                         public <R> R convert(final Class<R> type, final Object value, final String pattern) {
186                             return ConvertUtils.primitiveToWrapper(type).cast(9);
187                         }
188                     }, Integer.TYPE, Locale.getDefault());
189                     LocaleBeanUtils.setProperty(bean, "int", "1");
190                 } catch (final Exception e) {
191                     e.printStackTrace();
192                     signal.setException(e);
193                 }
194             }
195 
196             @Override
197             public String toString() {
198                 return "TestIndependenceThread";
199             }
200         }
201 
202         final PrimitiveBean bean = new PrimitiveBean();
203         LocaleBeanUtils.setProperty(bean, "int", new Integer(1));
204         assertEquals(1, bean.getInt(), "Wrong property value (1)");
205 
206         LocaleConvertUtils.register(new LocaleConverter<Integer>() {
207             @Override
208             public <R> R convert(final Class<R> type, final Object value) {
209                 return ConvertUtils.primitiveToWrapper(type).cast(5);
210             }
211 
212             @Override
213             public <R> R convert(final Class<R> type, final Object value, final String pattern) {
214                 return ConvertUtils.primitiveToWrapper(type).cast(5);
215             }
216         }, Integer.TYPE, Locale.getDefault());
217         LocaleBeanUtils.setProperty(bean, "int", "1");
218         assertEquals(5, bean.getInt(), "Wrong property value(2)");
219 
220         final Signal signal = new Signal();
221         signal.setSignal(1);
222         final TestIndependenceThread thread = new TestIndependenceThread(signal, bean);
223         thread.setContextClassLoader(new TestClassLoader());
224 
225         thread.start();
226         thread.join();
227 
228         assertNull(signal.getException(), "Exception thrown by test thread:" + signal.getException());
229         assertEquals(3, signal.getSignal(), "Signal not set by test thread");
230         assertEquals(9, bean.getInt(), "Wrong property value(3)");
231 
232     }
233 
234     /**
235      * Tests whether difference instances are loaded by different context class loaders.
236      */
237     @Test
238     public void testContextClassLoaderLocal() throws Exception {
239 
240         final class CCLLTesterThread extends Thread {
241 
242             private final Signal signal;
243             private final ContextClassLoaderLocal<Integer> ccll;
244 
245             CCLLTesterThread(final Signal signal, final ContextClassLoaderLocal<Integer> ccll) {
246                 this.signal = signal;
247                 this.ccll = ccll;
248             }
249 
250             @Override
251             public void run() {
252                 ccll.set(new Integer(1789));
253                 signal.setSignal(2);
254                 signal.setMarkerObject(ccll.get());
255             }
256 
257             @Override
258             public String toString() {
259                 return "CCLLTesterThread";
260             }
261         }
262 
263         final ContextClassLoaderLocal<Integer> ccll = new ContextClassLoaderLocal<>();
264         ccll.set(1776);
265         assertEquals(new Integer(1776), ccll.get(), "Start thread sets value");
266 
267         final Signal signal = new Signal();
268         signal.setSignal(1);
269 
270         final CCLLTesterThread thread = new CCLLTesterThread(signal, ccll);
271         thread.setContextClassLoader(new TestClassLoader());
272 
273         thread.start();
274         thread.join();
275 
276         assertEquals(2, signal.getSignal(), "Signal not set by test thread");
277         assertEquals(new Integer(1776), ccll.get(), "Second thread preserves value");
278         assertEquals(new Integer(1789), signal.getMarkerObject(), "Second thread gets value it set");
279     }
280 
281     /** Tests whether the unset method works */
282     @Test
283     public void testContextClassLoaderUnset() {
284         final LocaleBeanUtilsBean beanOne = new LocaleBeanUtilsBean();
285         final ContextClassLoaderLocal<LocaleBeanUtilsBean> ccll = new ContextClassLoaderLocal<>();
286         ccll.set(beanOne);
287         assertEquals(beanOne, ccll.get(), "Start thread gets right instance");
288         ccll.unset();
289         assertTrue(!beanOne.equals(ccll.get()), "Unset works");
290     }
291 
292     /**
293      * Tests whether difference instances are loaded by different context class loaders.
294      */
295     @Test
296     public void testGetByContextClassLoader() throws Exception {
297 
298         final class GetBeanUtilsBeanThread extends Thread {
299 
300             private final Signal signal;
301 
302             GetBeanUtilsBeanThread(final Signal signal) {
303                 this.signal = signal;
304             }
305 
306             @Override
307             public void run() {
308                 signal.setSignal(2);
309                 signal.setBean(LocaleBeanUtilsBean.getLocaleBeanUtilsInstance());
310                 signal.setConvertUtils(LocaleConvertUtilsBean.getInstance());
311             }
312 
313             @Override
314             public String toString() {
315                 return "GetBeanUtilsBeanThread";
316             }
317         }
318 
319         final Signal signal = new Signal();
320         signal.setSignal(1);
321 
322         final GetBeanUtilsBeanThread thread = new GetBeanUtilsBeanThread(signal);
323         thread.setContextClassLoader(new TestClassLoader());
324 
325         thread.start();
326         thread.join();
327 
328         assertEquals(2, signal.getSignal(), "Signal not set by test thread");
329         assertTrue(BeanUtilsBean.getInstance() != signal.getBean(), "Different LocaleBeanUtilsBean instances per context classloader");
330         assertTrue(LocaleConvertUtilsBean.getInstance() != signal.getConvertUtils(), "Different LocaleConvertUtilsBean instances per context classloader");
331     }
332 
333     /**
334      * Test registering a locale-aware converter with the standard ConvertUtils.
335      */
336     @Test
337     public void testLocaleAwareConverterInConvertUtils() {
338         try {
339             // first use the default non-locale-aware converter
340             Long data = (Long) ConvertUtils.convert("777", Long.class);
341             assertEquals(777, data.longValue(), "Standard format long converted ok");
342 
343             // now try default converter with special delimiters
344             // This conversion will cause an error. But the default
345             // Long converter is set up to return a default value of
346             // zero on error.
347             data = (Long) ConvertUtils.convert("1.000.000", Long.class);
348             assertEquals(0, data.longValue(), "Standard format behaved as expected");
349 
350             // Now try using a locale-aware converter together with
351             // locale-specific input string. Note that in the german locale,
352             // large numbers can be split up into groups of three digits
353             // using a dot character (and comma is the decimal-point indicator).
354             final Locale germanLocale = Locale.GERMAN;
355             final LongLocaleConverter longLocaleConverter = LongLocaleConverter.builder().setLocale(germanLocale).get();
356             ConvertUtils.register(longLocaleConverter, Long.class);
357 
358             data = (Long) ConvertUtils.convert("1.000.000", Long.class);
359             assertEquals(1000000, data.longValue(), "German-format long converted ok");
360         } finally {
361             ConvertUtils.deregister();
362         }
363     }
364 
365     /** Tests whether class loaders and beans are released from memory */
366     @Test
367     public void testMemoryLeak() throws Exception {
368         // many thanks to Juozas Baliuka for suggesting this methodology
369         TestClassLoader loader = new TestClassLoader();
370         final WeakReference<TestClassLoader> loaderReference = new WeakReference<>(loader);
371         LocaleBeanUtilsBean.getLocaleBeanUtilsInstance();
372 
373         final class GetBeanUtilsBeanThread extends Thread {
374 
375             LocaleBeanUtilsBean beanUtils;
376             LocaleConvertUtilsBean convertUtils;
377 
378             GetBeanUtilsBeanThread() {
379             }
380 
381             @Override
382             public void run() {
383                 beanUtils = LocaleBeanUtilsBean.getLocaleBeanUtilsInstance();
384                 convertUtils = LocaleConvertUtilsBean.getInstance();
385                 // XXX Log keeps a reference around!
386                 LogFactory.releaseAll();
387             }
388 
389             @Override
390             public String toString() {
391                 return "GetBeanUtilsBeanThread";
392             }
393         }
394 
395         GetBeanUtilsBeanThread thread = new GetBeanUtilsBeanThread();
396         final WeakReference<GetBeanUtilsBeanThread> threadWeakReference = new WeakReference<>(thread);
397         thread.setContextClassLoader(loader);
398 
399         thread.start();
400         thread.join();
401 
402         final WeakReference<LocaleBeanUtilsBean> beanUtilsReference = new WeakReference<>(thread.beanUtils);
403         final WeakReference<LocaleConvertUtilsBean> convertUtilsReference = new WeakReference<>(thread.convertUtils);
404 
405         assertNotNull(loaderReference.get(), "Weak reference released early (1)");
406         assertNotNull(beanUtilsReference.get(), "Weak reference released early (2)");
407         assertNotNull(convertUtilsReference.get(), "Weak reference released early (4)");
408 
409         // dereference strong references
410         loader = null;
411         thread.setContextClassLoader(null);
412         thread = null;
413 
414         int iterations = 0;
415         int bytz = 2;
416         while (true) {
417             LocaleBeanUtilsBean.getLocaleBeanUtilsInstance();
418             System.gc();
419 
420             assertFalse(iterations++ > MAX_GC_ITERATIONS, "Max iterations reached before resource released.");
421 
422             if (loaderReference.get() == null && beanUtilsReference.get() == null && convertUtilsReference.get() == null) {
423                 break;
424 
425             }
426             // create garbage:
427             final byte[] b = new byte[bytz];
428             bytz *= 2;
429         }
430     }
431 
432     /** Tests whether class loaders and beans are released from memory by the map used by BeanUtils */
433     @Test
434     public void testMemoryLeak2() {
435         // many thanks to Juozas Baliuka for suggesting this methodology
436         TestClassLoader loader = new TestClassLoader();
437         final ReferenceQueue<Object> queue = new ReferenceQueue<>();
438         final WeakReference<ClassLoader> loaderReference = new WeakReference<>(loader, queue);
439         Integer test = new Integer(1);
440 
441         final WeakReference<Integer> testReference = new WeakReference<>(test, queue);
442         // Map map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.HARD, true);
443         final Map<TestClassLoader, Integer> map = new WeakHashMap<>();
444         map.put(loader, test);
445 
446         assertEquals(test, map.get(loader), "In map");
447         assertNotNull(loaderReference.get(), "Weak reference released early (1)");
448         assertNotNull(testReference.get(), "Weak reference released early (2)");
449 
450         // dereference strong references
451         loader = null;
452         test = null;
453 
454         int iterations = 0;
455         int bytz = 2;
456         while (true) {
457             System.gc();
458             assertFalse(iterations++ > MAX_GC_ITERATIONS, "Max iterations reached before resource released.");
459             map.isEmpty();
460 
461             if (loaderReference.get() == null && testReference.get() == null) {
462                 break;
463 
464             }
465             // create garbage:
466             final byte[] b = new byte[bytz];
467             bytz *= 2;
468         }
469     }
470 
471     /** Test of the methodology we'll use for some of the later tests */
472     @Test
473     public void testMemoryTestMethodology() throws Exception {
474         // test methodology
475         // many thanks to Juozas Baliuka for suggesting this method
476         ClassLoader loader = new ClassLoader(this.getClass().getClassLoader()) {
477         };
478         final WeakReference<ClassLoader> reference = new WeakReference<>(loader);
479         Class<?> myClass = loader.loadClass("org.apache.commons.beanutils2.BetaBean");
480 
481         assertNotNull(reference.get(), "Weak reference released early");
482 
483         // dereference class loader and class:
484         loader = null;
485         myClass = null;
486 
487         int iterations = 0;
488         int bytz = 2;
489         while (true) {
490             System.gc();
491             assertFalse(iterations++ > MAX_GC_ITERATIONS, "Max iterations reached before resource released.");
492             if (reference.get() == null) {
493                 break;
494 
495             }
496             // create garbage:
497             final byte[] b = new byte[bytz];
498             bytz *= 2;
499         }
500     }
501 }