BeanificationTest.java

/*
 * 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
 *
 *      https://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.
 */

package org.apache.commons.beanutils2;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;

import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * <p>
 * Test Case for changes made during Beanutils Beanification
 * </p>
 */
class BeanificationTest {

    final class Signal {
        private Exception e;
        private int signal;
        private BeanUtilsBean bean;
        private PropertyUtilsBean propertyUtils;
        private ConvertUtilsBean convertUtils;
        private Object marker;

        public BeanUtilsBean getBean() {
            return bean;
        }

        public ConvertUtilsBean getConvertUtils() {
            return convertUtils;
        }

        public Exception getException() {
            return e;
        }

        public Object getMarkerObject() {
            return marker;
        }

        public PropertyUtilsBean getPropertyUtils() {
            return propertyUtils;
        }

        public int getSignal() {
            return signal;
        }

        public void setBean(final BeanUtilsBean bean) {
            this.bean = bean;
        }

        public void setConvertUtils(final ConvertUtilsBean convertUtils) {
            this.convertUtils = convertUtils;
        }

        public void setException(final Exception e) {
            this.e = e;
        }

        public void setMarkerObject(final Object marker) {
            this.marker = marker;
        }

        public void setPropertyUtils(final PropertyUtilsBean propertyUtils) {
            this.propertyUtils = propertyUtils;
        }

        public void setSignal(final int signal) {
            this.signal = signal;
        }
    }

    final class TestClassLoader extends ClassLoader {
        @Override
        public String toString() {
            return "TestClassLoader";
        }
    }

    /** Maximum number of iterations before our test fails */
    public static final int MAX_GC_ITERATIONS = 50;

    /**
     * Sets up instance variables required by this test case.
     */
    @BeforeEach
    public void setUp() {
        ConvertUtils.deregister();
    }

    /**
     * Tear down instance variables required by this test case.
     */
    @AfterEach
    public void tearDown() {
        // No action required
    }

    /** Tests whether different threads can set BeanUtils instances correctly */
    @Test
    void testBeanUtilsBeanSetInstance() throws Exception {

        final class SetInstanceTesterThread extends Thread {

            private final Signal signal;
            private final BeanUtilsBean bean;

            SetInstanceTesterThread(final Signal signal, final BeanUtilsBean bean) {
                this.signal = signal;
                this.bean = bean;
            }

            @Override
            public void run() {
                BeanUtilsBean.setInstance(bean);
                signal.setSignal(21);
                signal.setBean(BeanUtilsBean.getInstance());
            }

            @Override
            public String toString() {
                return "SetInstanceTesterThread";
            }
        }

        final Signal signal = new Signal();
        signal.setSignal(1);

        final BeanUtilsBean beanOne = new BeanUtilsBean();
        final BeanUtilsBean beanTwo = new BeanUtilsBean();

        final SetInstanceTesterThread thread = new SetInstanceTesterThread(signal, beanTwo);
        thread.setContextClassLoader(new TestClassLoader());

        BeanUtilsBean.setInstance(beanOne);
        assertEquals(beanOne, BeanUtilsBean.getInstance(), "Start thread gets right instance");

        thread.start();
        thread.join();

        assertEquals(21, signal.getSignal(), "Signal not set by test thread");
        assertEquals(beanOne, BeanUtilsBean.getInstance(), "Second thread preserves value");
        assertEquals(beanTwo, signal.getBean(), "Second thread gets value it set");
    }

    /** Tests whether calls are independent for different class loaders */
    @Test
    void testContextClassloaderIndependence() throws Exception {

        final class TestIndependenceThread extends Thread {
            private final Signal signal;
            private final PrimitiveBean bean;

            TestIndependenceThread(final Signal signal, final PrimitiveBean bean) {
                this.signal = signal;
                this.bean = bean;
            }

            @Override
            public void run() {
                try {
                    signal.setSignal(3);
                    final Converter c = (type, value) -> ConvertUtils.primitiveToWrapper(Integer.TYPE).cast(new Integer(9));
                    ConvertUtils.register(c, Integer.TYPE);
                    BeanUtils.setProperty(bean, "int", new Integer(1));
                } catch (final Exception e) {
                    e.printStackTrace();
                    signal.setException(e);
                }
            }

            @Override
            public String toString() {
                return "TestIndependenceThread";
            }
        }

        final PrimitiveBean bean = new PrimitiveBean();
        BeanUtils.setProperty(bean, "int", new Integer(1));
        assertEquals(1, bean.getInt(), "Wrong property value (1)");

        final Converter c = (type, value) -> ConvertUtils.primitiveToWrapper(type).cast(new Integer(5));
        ConvertUtils.register(c, Integer.TYPE);
        BeanUtils.setProperty(bean, "int", new Integer(1));
        assertEquals(5, bean.getInt(), "Wrong property value(2)");

        final Signal signal = new Signal();
        signal.setSignal(1);
        final TestIndependenceThread thread = new TestIndependenceThread(signal, bean);
        thread.setContextClassLoader(new TestClassLoader());

        thread.start();
        thread.join();

        assertNull(signal.getException(), "Exception thrown by test thread:" + signal.getException());
        assertEquals(3, signal.getSignal(), "Signal not set by test thread");
        assertEquals(9, bean.getInt(), "Wrong property value(3)");

    }

    /**
     * Tests whether difference instances are loaded by different context class loaders.
     */
    @Test
    void testContextClassLoaderLocal() throws Exception {

        final class CCLLTesterThread extends Thread {

            private final Signal signal;
            private final ContextClassLoaderLocal<Integer> ccll;

            CCLLTesterThread(final Signal signal, final ContextClassLoaderLocal<Integer> ccll) {
                this.signal = signal;
                this.ccll = ccll;
            }

            @Override
            public void run() {
                ccll.set(new Integer(1789));
                signal.setSignal(2);
                signal.setMarkerObject(ccll.get());
            }

            @Override
            public String toString() {
                return "CCLLTesterThread";
            }
        }

        final ContextClassLoaderLocal<Integer> ccll = new ContextClassLoaderLocal<>();
        ccll.set(new Integer(1776));
        assertEquals(new Integer(1776), ccll.get(), "Start thread sets value");

        final Signal signal = new Signal();
        signal.setSignal(1);

        final CCLLTesterThread thread = new CCLLTesterThread(signal, ccll);
        thread.setContextClassLoader(new TestClassLoader());

        thread.start();
        thread.join();

        assertEquals(2, signal.getSignal(), "Signal not set by test thread");
        assertEquals(new Integer(1776), ccll.get(), "Second thread preserves value");
        assertEquals(new Integer(1789), signal.getMarkerObject(), "Second thread gets value it set");
    }

    /** Tests whether the unset method works */
    @Test
    void testContextClassLoaderUnset() throws Exception {
        final BeanUtilsBean beanOne = new BeanUtilsBean();
        final ContextClassLoaderLocal<BeanUtilsBean> ccll = new ContextClassLoaderLocal<>();
        ccll.set(beanOne);
        assertEquals(beanOne, ccll.get(), "Start thread gets right instance");
        ccll.unset();
        assertNotEquals(beanOne, ccll.get(), "Unset works");
    }

    /**
     * Tests whether difference instances are loaded by different context class loaders.
     */
    @Test
    void testGetByContextClassLoader() throws Exception {

        final class GetBeanUtilsBeanThread extends Thread {

            private final Signal signal;

            GetBeanUtilsBeanThread(final Signal signal) {
                this.signal = signal;
            }

            @Override
            public void run() {
                signal.setSignal(2);
                signal.setBean(BeanUtilsBean.getInstance());
                signal.setConvertUtils(ConvertUtilsBean.getInstance());
                signal.setPropertyUtils(PropertyUtilsBean.getInstance());
            }

            @Override
            public String toString() {
                return "GetBeanUtilsBeanThread";
            }
        }

        final Signal signal = new Signal();
        signal.setSignal(1);

        final GetBeanUtilsBeanThread thread = new GetBeanUtilsBeanThread(signal);
        thread.setContextClassLoader(new TestClassLoader());

        thread.start();
        thread.join();

        assertEquals(2, signal.getSignal(), "Signal not set by test thread");
        assertNotEquals(BeanUtilsBean.getInstance(), signal.getBean(), "Different BeanUtilsBean instances per context classloader");
        assertNotEquals(ConvertUtilsBean.getInstance(), signal.getConvertUtils(), "Different ConvertUtilsBean instances per context classloader");
        assertNotEquals(PropertyUtilsBean.getInstance(), signal.getPropertyUtils(), "Different PropertyUtilsBean instances per context classloader");
    }

    /** Tests whether class loaders and beans are released from memory */
    @Test
    void testMemoryLeak() throws Exception {
        // many thanks to Juozas Baliuka for suggesting this methodology
        TestClassLoader loader = new TestClassLoader();
        final WeakReference<ClassLoader> loaderReference = new WeakReference<>(loader);
        BeanUtilsBean.getInstance();

        final class GetBeanUtilsBeanThread extends Thread {

            BeanUtilsBean beanUtils;
            ConvertUtilsBean convertUtils;
            PropertyUtilsBean propertyUtils;

            GetBeanUtilsBeanThread() {
            }

            @Override
            public void run() {
                beanUtils = BeanUtilsBean.getInstance();
                convertUtils = ConvertUtilsBean.getInstance();
                propertyUtils = PropertyUtilsBean.getInstance();
                // XXX Log keeps a reference around!
                LogFactory.releaseAll();
            }

            @Override
            public String toString() {
                return "GetBeanUtilsBeanThread";
            }
        }

        GetBeanUtilsBeanThread thread = new GetBeanUtilsBeanThread();
        @SuppressWarnings("unused")
        final WeakReference<Thread> threadWeakReference = new WeakReference<>(thread);
        thread.setContextClassLoader(loader);

        thread.start();
        thread.join();

        final WeakReference<BeanUtilsBean> beanUtilsReference = new WeakReference<>(thread.beanUtils);
        final WeakReference<PropertyUtilsBean> propertyUtilsReference = new WeakReference<>(thread.propertyUtils);
        final WeakReference<ConvertUtilsBean> convertUtilsReference = new WeakReference<>(thread.convertUtils);

        assertNotNull(loaderReference.get(), "Weak reference released early (1)");
        assertNotNull(beanUtilsReference.get(), "Weak reference released early (2)");
        assertNotNull(propertyUtilsReference.get(), "Weak reference released early (3)");
        assertNotNull(convertUtilsReference.get(), "Weak reference released early (4)");

        // dereference strong references
        loader = null;
        thread.setContextClassLoader(null);
        thread = null;

        int iterations = 0;
        int bytz = 2;
        while (true) {
            BeanUtilsBean.getInstance();
            System.gc();
            assertFalse(iterations++ > MAX_GC_ITERATIONS, "Max iterations reached before resource released.");

            if (loaderReference.get() == null && beanUtilsReference.get() == null && propertyUtilsReference.get() == null
                    && convertUtilsReference.get() == null) {
                break;

            }
            // create garbage:
            @SuppressWarnings("unused")
            final byte[] b = new byte[bytz];
            bytz *= 2;
        }
    }

    /** Tests whether class loaders and beans are released from memory by the map used by BeanUtils. */
    @Test
    void testMemoryLeak2() throws Exception {
        // many thanks to Juozas Baliuka for suggesting this methodology
        TestClassLoader loader = new TestClassLoader();
        final ReferenceQueue<Object> queue = new ReferenceQueue<>();
        final WeakReference<ClassLoader> loaderReference = new WeakReference<>(loader, queue);
        Integer test = new Integer(1);

        final WeakReference<Integer> testReference = new WeakReference<>(test, queue);
        // Map map = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.HARD, true);
        final Map<Object, Object> map = new WeakHashMap<>();
        map.put(loader, test);

        assertEquals(test, map.get(loader), "In map");
        assertNotNull(loaderReference.get(), "Weak reference released early (1)");
        assertNotNull(testReference.get(), "Weak reference released early (2)");

        // dereference strong references
        loader = null;
        test = null;

        int iterations = 0;
        int bytz = 2;
        while (true) {
            System.gc();
            assertFalse(iterations++ > MAX_GC_ITERATIONS, "Max iterations reached before resource released.");

            map.isEmpty();

            if (loaderReference.get() == null && testReference.get() == null) {
                break;

            }
            // create garbage:
            @SuppressWarnings("unused")
            final byte[] b = new byte[bytz];
            bytz *= 2;
        }
    }

    /** Test of the methodology we'll use for some of the later tests */
    @Test
    void testMemoryTestMethodology() throws Exception {
        // test methodology
        // many thanks to Juozas Baliuka for suggesting this method
        ClassLoader loader = new ClassLoader(this.getClass().getClassLoader()) {
        };
        final WeakReference<ClassLoader> reference = new WeakReference<>(loader);
        @SuppressWarnings("unused")
        Class<?> myClass = loader.loadClass("org.apache.commons.beanutils2.BetaBean");

        assertNotNull(reference.get(), "Weak reference released early");

        // dereference class loader and class:
        loader = null;
        myClass = null;

        int iterations = 0;
        int bytz = 2;
        while (true) {
            System.gc();
            assertFalse(iterations++ > MAX_GC_ITERATIONS, "Max iterations reached before resource released.");
            if (reference.get() == null) {
                break;

            }
            // create garbage:
            @SuppressWarnings("unused")
            final byte[] b = new byte[bytz];
            bytz *= 2;
        }
    }
}