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  package org.apache.commons.beanutils2.bugs;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotNull;
21  import static org.junit.jupiter.api.Assertions.assertNotSame;
22  import static org.junit.jupiter.api.Assertions.assertSame;
23  import static org.junit.jupiter.api.Assertions.fail;
24  
25  import java.beans.IntrospectionException;
26  import java.lang.ref.SoftReference;
27  import java.lang.reflect.Method;
28  import java.net.MalformedURLException;
29  import java.net.URL;
30  import java.net.URLClassLoader;
31  import java.util.ArrayList;
32  import java.util.Objects;
33  
34  import org.apache.commons.beanutils2.MappedPropertyDescriptor;
35  import org.apache.commons.beanutils2.memoryleaktests.MemoryLeakTest;
36  import org.junit.jupiter.api.Test;
37  
38  /**
39   * Test case for Jira issue# BEANUTILS-347.
40   *
41   * @see <a href="https://issues.apache.org/jira/browse/BEANUTILS-347">https://issues.apache.org/jira/browse/BEANUTILS-347</a>
42   */
43  public class Jira347Test {
44  
45      /**
46       * Create a new class loader instance.
47       */
48      private static URLClassLoader newClassLoader() throws MalformedURLException {
49  
50          final String dataFilePath = MemoryLeakTest.class.getResource("pojotests").getFile();
51          // System.out.println("dataFilePath: " + dataFilePath);
52          final String location = "file://"
53                  + dataFilePath.substring(0, dataFilePath.length() - "org.apache.commons.beanutils2.memoryleaktests.pojotests".length());
54          // System.out.println("location: " + location);
55  
56          final StringBuilder newString = new StringBuilder();
57          for (int i = 0; i < location.length(); i++) {
58              if (location.charAt(i) == '\\') {
59                  newString.append("/");
60              } else {
61                  newString.append(location.charAt(i));
62              }
63          }
64          final String classLocation = newString.toString();
65          // System.out.println("classlocation: " + classLocation);
66  
67          final URLClassLoader theLoader = URLClassLoader.newInstance(new URL[] { new URL(classLocation) }, null);
68          return theLoader;
69      }
70  
71      /**
72       * Try to force the garbage collector to run by filling up memory and calling System.gc().
73       */
74      private void forceGarbageCollection() throws Exception {
75          // Fill up memory
76          final SoftReference<Object> ref = new SoftReference<>(new Object());
77          int count = 0;
78          while (ref.get() != null && count++ < 5) {
79              ArrayList<Object> list = new ArrayList<>();
80              try {
81                  long i = 0;
82                  while (ref.get() != null) {
83                      list.add(
84                              "A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String "
85                                      + i++);
86                  }
87              } catch (final Throwable ignored) {
88              }
89              list.clear();
90              list = null;
91              // System.out.println("Count " + count + " : " + getMemoryStats());
92              System.gc();
93              Thread.sleep(1000);
94          }
95          // System.out.println("After GC: " + getMemoryStats());
96  
97          if (ref.get() != null) {
98              throw new IllegalStateException("Your JVM is not releasing SoftReference, try running the test with less memory (-Xmx)");
99          }
100     }
101 
102     /**
103      * Gets the string representation of the mapped write method for the given descriptor. This conversion is needed as there must not be strong reference
104      * to the Method object outside of this method as otherwise the garbage collector will not clean up the soft reference within the MappedPropertyDescriptor.
105      *
106      * @return the string representation or null if mapped write method does not exist
107      */
108     private String getMappedWriteMethod(final MappedPropertyDescriptor descriptor) {
109         final Method m = descriptor.getMappedWriteMethod();
110         return Objects.toString(m, null);
111     }
112 
113     /**
114      * Tests that MappedPropertyDescriptor does not throw an exception while re-creating a Method reference after it has been garbage collected under the
115      * following circumstances. - a class has a property 'mappedProperty' - there is no getter for this property - there is method
116      * setMappedProperty(MappedPropertyTestBean,MappedPropertyTestBean)
117      *
118      * In this case getMappedWriteMethod should not throw an exception after the method reference has been garbage collected. It is essential that in both cases
119      * the same method is returned or in both cases null. If the constructor of the MappedPropertyDescriptor would recognize the situation (of the first param
120      * not being of type String) this would be fine as well.
121      */
122     @Test
123     public void testMappedPropertyDescriptor_AnyArgsProperty() throws Exception {
124         final String className = "org.apache.commons.beanutils2.MappedPropertyTestBean";
125         try (final URLClassLoader loader = newClassLoader()) {
126             final Class<?> beanClass = loader.loadClass(className);
127             beanClass.newInstance();
128 
129             // Sanity checks only
130             assertNotNull(loader, "ClassLoader is null");
131             assertNotNull(beanClass, "BeanClass is null");
132             assertNotSame(getClass().getClassLoader(), beanClass.getClassLoader(), "ClassLoaders should be different..");
133             assertSame(beanClass.getClassLoader(), loader, "BeanClass ClassLoader incorrect");
134 
135             // now start the test
136             MappedPropertyDescriptor descriptor = null;
137             try {
138                 descriptor = new MappedPropertyDescriptor("anyMapped", beanClass);
139             } catch (final IntrospectionException e) {
140                 // this would be fine as well
141             }
142 
143             if (descriptor != null) {
144                 final String m1 = getMappedWriteMethod(descriptor);
145                 forceGarbageCollection();
146                 try {
147                     final String m2 = getMappedWriteMethod(descriptor);
148                     assertEquals(m1, m2, "Method returned post garbage collection differs from Method returned prior to gc");
149                 } catch (final RuntimeException e) {
150                     fail("getMappedWriteMethod threw an exception after garbage collection " + e);
151                 }
152             }
153         }
154     }
155 }