org.openmrs.api.db.hibernate.HibernateContextDAO.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.api.db.hibernate.HibernateContextDAO.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
 *
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
 * graphic logo is a trademark of OpenMRS Inc.
 */
package org.openmrs.api.db.hibernate;

import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.stat.QueryStatistics;
import org.hibernate.stat.Statistics;
import org.hibernate.type.StandardBasicTypes;
import org.openmrs.GlobalProperty;
import org.openmrs.User;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.ContextAuthenticationException;
import org.openmrs.api.db.ContextDAO;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;
import org.openmrs.util.Security;
import org.springframework.orm.hibernate4.SessionFactoryUtils;
import org.springframework.orm.hibernate4.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * Hibernate specific implementation of the {@link ContextDAO}. These methods should not be used
 * directly, instead, the methods on the static {@link Context} file should be used.
 * 
 * @see ContextDAO
 * @see Context
 */
public class HibernateContextDAO implements ContextDAO {

    private static Log log = LogFactory.getLog(HibernateContextDAO.class);

    /**
     * Hibernate session factory
     */
    private SessionFactory sessionFactory;

    /**
     * Session factory to use for this DAO. This is usually injected by spring and its application
     * context.
     * 
     * @param sessionFactory
     */
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    /**
     * @see org.openmrs.api.db.ContextDAO#authenticate(java.lang.String, java.lang.String)
     */
    public User authenticate(String login, String password) throws ContextAuthenticationException {

        String errorMsg = "Invalid username and/or password: " + login;

        Session session = sessionFactory.getCurrentSession();

        User candidateUser = null;

        if (login != null) {
            //if username is blank or white space character(s)
            if (StringUtils.isEmpty(login) || StringUtils.isWhitespace(login)) {
                throw new ContextAuthenticationException(errorMsg);
            }

            // loginWithoutDash is used to compare to the system id
            String loginWithDash = login;
            if (login.matches("\\d{2,}")) {
                loginWithDash = login.substring(0, login.length() - 1) + "-" + login.charAt(login.length() - 1);
            }

            try {
                candidateUser = (User) session.createQuery(
                        "from User u where (u.username = ? or u.systemId = ? or u.systemId = ?) and u.retired = '0'")
                        .setString(0, login).setString(1, login).setString(2, loginWithDash).uniqueResult();
            } catch (HibernateException he) {
                log.error("Got hibernate exception while logging in: '" + login + "'", he);
            } catch (Exception e) {
                log.error("Got regular exception while logging in: '" + login + "'", e);
            }
        }

        // only continue if this is a valid username and a nonempty password
        if (candidateUser != null && password != null) {
            if (log.isDebugEnabled()) {
                log.debug("Candidate user id: " + candidateUser.getUserId());
            }

            String lockoutTimeString = candidateUser
                    .getUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP, null);
            Long lockoutTime = null;
            if (lockoutTimeString != null && !lockoutTimeString.equals("0")) {
                try {
                    // putting this in a try/catch in case the admin decided to put junk into the property
                    lockoutTime = Long.valueOf(lockoutTimeString);
                } catch (NumberFormatException e) {
                    log.debug("bad value stored in " + OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP
                            + " user property: " + lockoutTimeString);
                }
            }

            // if they've been locked out, don't continue with the authentication
            if (lockoutTime != null) {
                // unlock them after 5 mins, otherwise reset the timestamp
                // to now and make them wait another 5 mins
                if (System.currentTimeMillis() - lockoutTime > 300000) {
                    candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, "0");
                    candidateUser.removeUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP);
                    saveUserProperties(candidateUser);
                } else {
                    candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP,
                            String.valueOf(System.currentTimeMillis()));
                    throw new ContextAuthenticationException(
                            "Invalid number of connection attempts. Please try again later.");
                }
            }

            String passwordOnRecord = (String) session
                    .createSQLQuery("select password from users where user_id = ?")
                    .addScalar("password", StandardBasicTypes.STRING).setInteger(0, candidateUser.getUserId())
                    .uniqueResult();

            String saltOnRecord = (String) session.createSQLQuery("select salt from users where user_id = ?")
                    .addScalar("salt", StandardBasicTypes.STRING).setInteger(0, candidateUser.getUserId())
                    .uniqueResult();

            // if the username and password match, hydrate the user and return it
            if (passwordOnRecord != null && Security.hashMatches(passwordOnRecord, password + saltOnRecord)) {
                // hydrate the user object
                candidateUser.getAllRoles().size();
                candidateUser.getUserProperties().size();
                candidateUser.getPrivileges().size();

                // only clean up if the were some login failures, otherwise all should be clean
                Integer attempts = getUsersLoginAttempts(candidateUser);
                if (attempts > 0) {
                    candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, "0");
                    candidateUser.removeUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP);
                    saveUserProperties(candidateUser);
                }

                // skip out of the method early (instead of throwing the exception)
                // to indicate that this is the valid user
                return candidateUser;
            } else {
                // the user failed the username/password, increment their
                // attempts here and set the "lockout" timestamp if necessary
                Integer attempts = getUsersLoginAttempts(candidateUser);

                attempts++;

                Integer allowedFailedLoginCount = 7;

                try {
                    allowedFailedLoginCount = Integer.valueOf(Context.getAdministrationService()
                            .getGlobalProperty(OpenmrsConstants.GP_ALLOWED_FAILED_LOGINS_BEFORE_LOCKOUT).trim());
                } catch (Exception ex) {
                    log.error("Unable to convert the global property "
                            + OpenmrsConstants.GP_ALLOWED_FAILED_LOGINS_BEFORE_LOCKOUT
                            + "to a valid integer. Using default value of 7");
                }

                if (attempts > allowedFailedLoginCount) {
                    // set the user as locked out at this exact time
                    candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOCKOUT_TIMESTAMP,
                            String.valueOf(System.currentTimeMillis()));
                } else {
                    candidateUser.setUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS,
                            String.valueOf(attempts));
                }

                saveUserProperties(candidateUser);
            }
        }

        // throw this exception only once in the same place with the same
        // message regardless of username/pw combo entered
        log.info("Failed login attempt (login=" + login + ") - " + errorMsg);
        throw new ContextAuthenticationException(errorMsg);

    }

    /**
     * @see org.openmrs.api.db.ContextDAO#getUserByUuid(java.lang.String)
     */
    public User getUserByUuid(String uuid) {

        // don't flush here in case we're in the AuditableInterceptor.  Will cause a StackOverflowEx otherwise
        FlushMode flushMode = sessionFactory.getCurrentSession().getFlushMode();
        sessionFactory.getCurrentSession().setFlushMode(FlushMode.MANUAL);

        User u = (User) sessionFactory.getCurrentSession().createQuery("from User u where u.uuid = :uuid")
                .setString("uuid", uuid).uniqueResult();

        // reset the flush mode to whatever it was before
        sessionFactory.getCurrentSession().setFlushMode(flushMode);

        return u;
    }

    /**
     * Call the UserService to save the given user while proxying the privileges needed to do so.
     * 
     * @param user the User to save
     */
    private void saveUserProperties(User user) {
        sessionFactory.getCurrentSession().update(user);
    }

    /**
     * Get the integer stored for the given user that is their number of login attempts
     * 
     * @param user the user to check
     * @return the # of login attempts for this user defaulting to zero if none defined
     */
    private Integer getUsersLoginAttempts(User user) {
        String attemptsString = user.getUserProperty(OpenmrsConstants.USER_PROPERTY_LOGIN_ATTEMPTS, "0");
        Integer attempts = 0;
        try {
            attempts = Integer.valueOf(attemptsString);
        } catch (NumberFormatException e) {
            // skip over errors and leave the attempts at zero
        }
        return attempts;
    }

    /**
     * @see org.openmrs.api.context.Context#openSession()
     */
    private boolean participate = false;

    public void openSession() {
        log.debug("HibernateContext: Opening Hibernate Session");
        if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
            if (log.isDebugEnabled()) {
                log.debug("Participating in existing session (" + sessionFactory.hashCode() + ")");
            }
            participate = true;
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Registering session with synchronization manager (" + sessionFactory.hashCode() + ")");
            }
            Session session = sessionFactory.openSession();
            session.setFlushMode(FlushMode.MANUAL);
            TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));
        }
    }

    /**
     * @see org.openmrs.api.context.Context#closeSession()
     */
    public void closeSession() {
        log.debug("HibernateContext: closing Hibernate Session");
        if (!participate) {
            log.debug("Unbinding session from synchronization manager (" + sessionFactory.hashCode() + ")");

            if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
                Object value = TransactionSynchronizationManager.unbindResource(sessionFactory);
                try {
                    if (value instanceof SessionHolder) {
                        Session session = ((SessionHolder) value).getSession();
                        SessionFactoryUtils.closeSession(session);
                    }
                } catch (RuntimeException e) {
                    log.error("Unexpected exception on closing Hibernate Session", e);
                }
            }
        } else {
            log.debug(
                    "Participating in existing session, so not releasing session through synchronization manager");
        }
    }

    /**
     * @see org.openmrs.api.db.ContextDAO#clearSession()
     */
    public void clearSession() {
        sessionFactory.getCurrentSession().clear();
    }

    /**
     * @see org.openmrs.api.db.ContextDAO#evictFromSession(java.lang.Object)
     */
    public void evictFromSession(Object obj) {
        sessionFactory.getCurrentSession().evict(obj);
    }

    /**
     * @see org.openmrs.api.db.ContextDAO#flushSession()
     */
    public void flushSession() {
        sessionFactory.getCurrentSession().flush();
    }

    /**
     * @see org.openmrs.api.context.Context#startup(Properties)
     */
    public void startup(Properties properties) {
    }

    /**
     * @see org.openmrs.api.context.Context#shutdown()
     */
    public void shutdown() {
        if (log.isInfoEnabled()) {
            showUsageStatistics();
        }

        if (sessionFactory != null) {

            log.debug("Closing any open sessions");
            closeSession();

            log.debug("Shutting down threadLocalSession factory");
            if (!sessionFactory.isClosed()) {
                sessionFactory.close();
            }

            log.debug("The threadLocalSession has been closed");

        } else {
            log.error("SessionFactory is null");
        }

    }

    /**
     * Convenience method to print out the hibernate cache usage stats to the log
     */
    private void showUsageStatistics() {
        if (sessionFactory.getStatistics().isStatisticsEnabled()) {
            log.debug("Getting query statistics: ");
            Statistics stats = sessionFactory.getStatistics();
            for (String query : stats.getQueries()) {
                log.info("QUERY: " + query);
                QueryStatistics qstats = stats.getQueryStatistics(query);
                log.info("Cache Hit Count : " + qstats.getCacheHitCount());
                log.info("Cache Miss Count: " + qstats.getCacheMissCount());
                log.info("Cache Put Count : " + qstats.getCachePutCount());
                log.info("Execution Count : " + qstats.getExecutionCount());
                log.info("Average time    : " + qstats.getExecutionAvgTime());
                log.info("Row Count       : " + qstats.getExecutionRowCount());
            }
        }
    }

    /**
     * Takes the default properties defined in /metadata/api/hibernate/hibernate.default.properties
     * and merges it into the user-defined runtime properties
     * 
     * @see org.openmrs.api.db.ContextDAO#mergeDefaultRuntimeProperties(java.util.Properties)
     * @should merge default runtime properties
     */
    public void mergeDefaultRuntimeProperties(Properties runtimeProperties) {

        Map<String, String> cache = new HashMap<String, String>();
        // loop over runtime properties and precede each with "hibernate" if
        // it isn't already
        for (Map.Entry<Object, Object> entry : runtimeProperties.entrySet()) {
            Object key = entry.getKey();
            String prop = (String) key;
            String value = (String) entry.getValue();
            log.trace("Setting property: " + prop + ":" + value);
            if (!prop.startsWith("hibernate") && !runtimeProperties.containsKey("hibernate." + prop)) {
                cache.put("hibernate." + prop, value);
            }
        }
        runtimeProperties.putAll(cache);

        // load in the default hibernate properties from hibernate.default.properties
        Properties props = new Properties();
        URL url = getClass().getResource("/hibernate.default.properties");
        File file = new File(url.getPath());
        OpenmrsUtil.loadProperties(props, file);

        // add in all default properties that don't exist in the runtime
        // properties yet
        for (Map.Entry<Object, Object> entry : props.entrySet()) {
            if (!runtimeProperties.containsKey(entry.getKey())) {
                runtimeProperties.put(entry.getKey(), entry.getValue());
            }
        }
    }

    @Override
    public void updateSearchIndexForType(Class<?> type) {
        //From http://docs.jboss.org/hibernate/search/3.3/reference/en-US/html/manual-index-changes.html#search-batchindex-flushtoindexes
        FullTextSession session = Search.getFullTextSession(sessionFactory.getCurrentSession());
        session.purgeAll(type);

        //Prepare session for batch work
        session.flush();
        session.clear();

        FlushMode flushMode = session.getFlushMode();
        CacheMode cacheMode = session.getCacheMode();
        try {
            session.setFlushMode(FlushMode.MANUAL);
            session.setCacheMode(CacheMode.IGNORE);

            //Scrollable results will avoid loading too many objects in memory
            ScrollableResults results = session.createCriteria(type).setFetchSize(1000)
                    .scroll(ScrollMode.FORWARD_ONLY);
            int index = 0;
            while (results.next()) {
                index++;
                session.index(results.get(0)); //index each element
                if (index % 1000 == 0) {
                    session.flushToIndexes(); //apply changes to indexes
                    session.clear(); //free memory since the queue is processed
                }
            }
            session.flushToIndexes();
            session.clear();
        } finally {
            session.setFlushMode(flushMode);
            session.setCacheMode(cacheMode);
        }
    }

    /**
     * @see org.openmrs.api.db.ContextDAO#updateSearchIndexForObject(java.lang.Object)
     */
    @Override
    public void updateSearchIndexForObject(Object object) {
        FullTextSession session = Search.getFullTextSession(sessionFactory.getCurrentSession());
        session.index(object);
        session.flushToIndexes();
    }

    /**
     * @see org.openmrs.api.db.ContextDAO#setupSearchIndex()
     */
    @Override
    public void setupSearchIndex() {
        String gp = Context.getAdministrationService().getGlobalProperty(OpenmrsConstants.GP_SEARCH_INDEX_VERSION,
                "");

        if (!OpenmrsConstants.SEARCH_INDEX_VERSION.toString().equals(gp)) {
            updateSearchIndex();
        }
    }

    /**
     * @see ContextDAO#updateSearchIndex()
     */
    @Override
    public void updateSearchIndex() {
        try {
            log.info("Updating the search index... It may take a few minutes.");
            Search.getFullTextSession(sessionFactory.getCurrentSession()).createIndexer().startAndWait();

            GlobalProperty gp = Context.getAdministrationService()
                    .getGlobalPropertyObject(OpenmrsConstants.GP_SEARCH_INDEX_VERSION);
            if (gp == null) {
                gp = new GlobalProperty(OpenmrsConstants.GP_SEARCH_INDEX_VERSION);
            }
            gp.setPropertyValue(OpenmrsConstants.SEARCH_INDEX_VERSION.toString());
            Context.getAdministrationService().saveGlobalProperty(gp);
            log.info("Finished updating the search index");
        } catch (Exception e) {
            throw new RuntimeException("Failed to update the search index", e);
        }
    }

}