org.alfresco.bm.test.TestRunServicesCache.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.bm.test.TestRunServicesCache.java

Source

/*
 * Copyright (C) 2005-2014 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */
package org.alfresco.bm.test;

import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.alfresco.bm.event.EventService;
import org.alfresco.bm.event.ResultService;
import org.alfresco.bm.exception.ObjectNotFoundException;
import org.alfresco.bm.report.DataReportService;
import org.alfresco.bm.session.SessionService;
import org.alfresco.bm.test.mongo.MongoTestDAO;
import org.alfresco.bm.util.ArgumentCheck;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;

import com.mongodb.BasicDBList;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import com.mongodb.MongoSocketException;

/**
 * Helper class for instantiating and holding service instances for specific test runs.
 * 
 * @author Derek Hulley
 * @since 2.0
 */
public class TestRunServicesCache implements LifecycleListener, TestConstants {
    /** The time to hold a context open since last access */
    private static final long CONTEXT_ACCESS_TIMEOUT = 120000L;

    private static final Log logger = LogFactory.getLog(TestRunServicesCache.class);

    private final MongoTestDAO dao;
    private final TestService testService;
    private final Map<String, ClassPathXmlApplicationContext> contexts;
    private final Map<String, Long> contextAccessTimes;
    private final ContextCleanerTask contextCleanerTask;
    private final ReentrantReadWriteLock lock;

    /**
     * @param dao
     *            provides access to the state of a specific test run
     */
    public TestRunServicesCache(MongoTestDAO dao) {
        this.dao = dao;
        this.testService = new TestServiceImpl(dao);

        this.contexts = new HashMap<String, ClassPathXmlApplicationContext>(13);
        this.contextAccessTimes = Collections.synchronizedMap(new HashMap<String, Long>(13));
        this.contextCleanerTask = new ContextCleanerTask();
        this.lock = new ReentrantReadWriteLock();
    }

    @Override
    public void start() throws Exception {
        Timer timer = new Timer("TestServicesCache", true);
        timer.schedule(contextCleanerTask, 0L, CONTEXT_ACCESS_TIMEOUT);
    }

    @Override
    public void stop() throws Exception {
        // Stop the timer
        contextCleanerTask.cancel();
        // Shut down all current service instances
        contextCleanerTask.run();
        // Remove contexts to prevent accidental processing by cleaner
        contexts.clear();
    }

    /**
     * Create an application context holding the services for the given test run
     */
    private ClassPathXmlApplicationContext createContext(String test, String run) {
        String testRunFqn = test + "." + run;
        DBObject runObj;
        try {
            runObj = dao.getTestRun(test, run, true);
        } catch (ObjectNotFoundException e1) {
            logger.error("Test '" + test + "." + run + "' not found.", e1);
            return null;
        }

        // Dig the properties out of the test run
        Properties testRunProps = new Properties();
        {
            testRunProps.put(PROP_TEST_RUN_FQN, testRunFqn);

            BasicDBList propObjs = (BasicDBList) runObj.get(FIELD_PROPERTIES);
            for (Object obj : propObjs) {
                DBObject propObj = (DBObject) obj;
                String propName = (String) propObj.get(FIELD_NAME);
                String propDef = (String) propObj.get(FIELD_DEFAULT);
                String propValue = (String) propObj.get(FIELD_VALUE);
                if (propValue == null) {
                    propValue = propDef;
                }
                testRunProps.put(propName, propValue);
            }
        }
        // Construct the properties
        ClassPathXmlApplicationContext testRunCtx = new ClassPathXmlApplicationContext(
                new String[] { PATH_TEST_SERVICES_CONTEXT }, false);
        ConfigurableEnvironment ctxEnv = testRunCtx.getEnvironment();
        ctxEnv.getPropertySources().addFirst(new PropertiesPropertySource("run-props", testRunProps));
        // Bind to shutdown
        testRunCtx.registerShutdownHook();

        // Attempt to start the context
        try {
            testRunCtx.refresh();
            testRunCtx.start();
            // Make sure that the required components are present
            testRunCtx.getBean(EventService.class);
            testRunCtx.getBean(ResultService.class);
            testRunCtx.getBean(SessionService.class);
        } catch (Exception e) {
            Throwable root = ExceptionUtils.getRootCause(e);
            if (root != null && root instanceof MongoSocketException) {
                // We deal with this specifically as it's a simple case of not finding the MongoDB
                logger.error("Failed to start test run services context '" + testRunFqn + "': "
                        + e.getCause().getMessage());
                logger.error(
                        "Set the test run property '" + PROP_MONGO_TEST_HOST + "' (<server>:<port>) as required.");
            } else if (root != null && root instanceof UnknownHostException) {
                // We deal with this specifically as it's a simple case of not finding the MongoDB
                logger.error("Failed to start test run services context '" + testRunFqn + "': "
                        + e.getCause().getCause().getMessage());
                logger.error(
                        "Set the test run property '" + PROP_MONGO_TEST_HOST + "' (<server>:<port>) as required.");
            } else {
                logger.error("Failed to start test run services context '" + testRunFqn + "': ", e);
            }
            testRunCtx = null;
        }
        // Done
        if (testRunCtx == null) {
            logger.warn("Failed to start test run services context: " + testRunFqn);
        } else if (logger.isDebugEnabled()) {
            logger.debug("Started test run services context: " + testRunFqn);
        }
        return testRunCtx;
    }

    /**
     * Get the {@link ResultService} for the given test run
     * 
     * @param test
     *            the name of the test
     * @param run
     *            the name of the run
     * @return the service to access the results or <tt>null</tt> if not available
     */
    private ClassPathXmlApplicationContext getContext(String test, String run) {
        String testRunFqn = test + "." + run;
        Long now = System.currentTimeMillis();

        lock.readLock().lock();
        try {
            ClassPathXmlApplicationContext ctx = contexts.get(testRunFqn);
            if (ctx != null) {
                // Record when we last required the context
                contextAccessTimes.put(testRunFqn, now);
                return ctx;
            }
        } finally {
            lock.readLock().unlock();
        }
        // We didn't have a context
        lock.writeLock().lock();
        try {
            ClassPathXmlApplicationContext ctx = createContext(test, run);
            if (ctx == null) {
                // An error will already have been logged
                return null;
            }
            // Store it for later
            contexts.put(testRunFqn, ctx);
            // Record when we last required the context
            contextAccessTimes.put(testRunFqn, now);
            // Done
            return ctx;
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * Get the {@link MongoTestDAO} for the given test run
     * 
     * @return the DAO for accessing the low-level test config data
     */
    public MongoTestDAO getTestDAO() {
        return dao;
    }

    /**
     * Get the {@link TestService} for the given test run
     * 
     * @return the service for accessing test data
     */
    public TestService getTestService() {
        return testService;
    }

    /**
     * Get the {@link ResultService} for the given test run
     * 
     * @return the service or <tt>null</tt> if it could not be created or accessed
     */
    public ResultService getResultService(String test, String run) {
        ApplicationContext ctx = getContext(test, run);
        if (ctx == null) {
            return null;
        }
        return ctx.getBean(ResultService.class);
    }

    /**
     * Get the {@link EventService} for the given test run
     * 
     * @return the service or <tt>null</tt> if it could not be created or accessed
     */
    public EventService getEventService(String test, String run) {
        ApplicationContext ctx = getContext(test, run);
        if (ctx == null) {
            return null;
        }
        return ctx.getBean(EventService.class);
    }

    /**
     * Get the {@link SessionService} for the given test run
     * 
     * @return the service or <tt>null</tt> if it could not be created or accessed
     */
    public SessionService getSessionService(String test, String run) {
        ApplicationContext ctx = getContext(test, run);
        if (ctx == null) {
            return null;
        }
        return ctx.getBean(SessionService.class);
    }

    /**
     * Returns the data report service or null if no data report service is configured.
     * 
     * @param test
     *            (String) test name
     * @param run
     *            (String) test run name
     * @return (DataReportService or null)
     */
    public DataReportService getDataReportService(String test, String run) {
        ArgumentCheck.checkMandatoryString(test, "test");
        ArgumentCheck.checkMandatoryString(run, "run");

        try {
            ApplicationContext ctx = getContext(test, run);
            if (null != ctx) {
                return ctx.getBean(DataReportService.class);
            }
        } catch (Exception e) {
            logger.debug("No data report service available!", e);
        }
        return null;
    }

    /**
     * Deletes the collections of the test run and the extra data from MongoDB
     * 
     * @param test
     *        (String, mandatory) test name
     * @param run
     *        (String, mandatory) test run name
     * 
     * @since 2.1.4
     */
    public boolean deleteTestRun(String test, String run) {
        boolean removed = true;

        // remove the test run from the data service
        DataReportService dataReportService = getDataReportService(test, run);
        if (null != dataReportService) {
            dataReportService.remove(null, test, run);
            if (logger.isDebugEnabled()) {
                logger.debug("Removed " + test + "." + run + " data from 'DataReportService'.");
            }
        }

        // remove all collections
        EventService ev = getEventService(test, run);
        if (null != ev) {
            removed &= ev.clear();
        }
        ResultService rs = getResultService(test, run);
        if (null != rs) {
            removed &= rs.clear();
        }
        SessionService se = getSessionService(test, run);
        if (null != se) {
            removed &= se.clear();
        }

        return removed;
    }

    /**
     * Checks the access times for all contexts and shuts down and removes any that have not been accessed since it last
     * checked.
     * 
     * @see TestServicesCache#CONTEXT_ACCESS_TIMEOUT
     * 
     * @author Derek Hulley
     * @since 2.0
     */
    private class ContextCleanerTask extends TimerTask {
        @Override
        public void run() {
            long now = System.currentTimeMillis();
            // Copy the keys to avoid concurrent modification while iterating
            Set<String> testRunFqns = new TreeSet<String>(contexts.keySet());
            for (String testRunFqn : testRunFqns) {
                Long lastAccess = contextAccessTimes.get(testRunFqn);
                boolean expired = lastAccess == null || lastAccess < (now - CONTEXT_ACCESS_TIMEOUT);
                if (!expired) {
                    continue;
                }
                // The context has expired
                if (logger.isDebugEnabled()) {
                    logger.debug("Cleaning up unused test services context: " + testRunFqn);
                }
                lock.writeLock().lock();
                try {
                    ClassPathXmlApplicationContext ctx = contexts.get(testRunFqn);
                    if (ctx == null) {
                        logger.error("Expected to remove unused test services context but didn't find the context: "
                                + testRunFqn);
                        continue;
                    }
                    // First remove it
                    contexts.remove(testRunFqn);
                    contextAccessTimes.remove(testRunFqn);
                    // Then shut it down
                    ctx.stop();
                    ctx.close();
                } catch (Exception e) {
                    // Can't fail so just report
                    logger.error("Failed to clean up unused test services context: " + testRunFqn, e);
                } finally {
                    lock.writeLock().unlock();
                }
            }
        }
    }
}