All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.iternine.jeppetto.dao.hibernate.HibernateQueryModelDAO Maven / Gradle / Ivy

/*
 * Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
 *
 * Licensed 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
 *
 *    http://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.iternine.jeppetto.dao.hibernate;


import org.iternine.jeppetto.dao.AccessControlContext;
import org.iternine.jeppetto.dao.AccessControlContextProvider;
import org.iternine.jeppetto.dao.AccessControlDAO;
import org.iternine.jeppetto.dao.AccessControlException;
import org.iternine.jeppetto.dao.AccessType;
import org.iternine.jeppetto.dao.Condition;
import org.iternine.jeppetto.dao.ConditionType;
import org.iternine.jeppetto.dao.FailedBatchException;
import org.iternine.jeppetto.dao.JeppettoException;
import org.iternine.jeppetto.dao.NoSuchItemException;
import org.iternine.jeppetto.dao.OptimisticLockException;
import org.iternine.jeppetto.dao.Projection;
import org.iternine.jeppetto.dao.ProjectionType;
import org.iternine.jeppetto.dao.QueryModel;
import org.iternine.jeppetto.dao.QueryModelDAO;
import org.iternine.jeppetto.dao.Sort;
import org.iternine.jeppetto.dao.SortDirection;
import org.iternine.jeppetto.dao.TooManyItemsException;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StaleObjectStateException;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.loader.criteria.CriteriaQueryTranslator;
import org.hibernate.type.StringType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
 * An implementation of the QueryModelDAO interface that supports Hibernate.
 *
 * @param  persistent class.
 * @param  ID type for the persistent class.
 */
public class HibernateQueryModelDAO
        implements QueryModelDAO, AccessControlDAO {

    //-------------------------------------------------------------
    // Constants
    //-------------------------------------------------------------

    private static final StringType STRING_TYPE = new StringType();


    //-------------------------------------------------------------
    // Variables - Private
    //-------------------------------------------------------------

    private Class persistentClass;
    private SessionFactory sessionFactory;
    private AccessControlHelper accessControlHelper;
    private AccessControlContextProvider accessControlContextProvider;
    private String idField = "id";      // TODO: Allow for configuration...

    private static final Logger logger = LoggerFactory.getLogger(HibernateQueryModelDAO.class);


    //-------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------

    protected HibernateQueryModelDAO(Class persistentClass, Map daoProperties) {
        this(persistentClass, daoProperties, null);
    }


    protected HibernateQueryModelDAO(Class persistentClass, Map daoProperties,
                                     AccessControlContextProvider accessControlContextProvider) {
        this.persistentClass = persistentClass;
        this.sessionFactory = (SessionFactory) daoProperties.get("sessionFactory");
        this.accessControlHelper = (AccessControlHelper) daoProperties.get("accessControlHelper");
        this.accessControlContextProvider = accessControlContextProvider;

        if (accessControlHelper != null) {
            accessControlHelper.registerDAO(persistentClass, this);
        }
    }


    //-------------------------------------------------------------
    // Implementation - GenericDAO
    //-------------------------------------------------------------

    @Override
    public T findById(ID id)
            throws NoSuchItemException, JeppettoException {
        QueryModel queryModel = new QueryModel();
        queryModel.addCondition(buildIdCondition(id));
        
        if (accessControlContextProvider != null) {
            queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
        }

        return findUniqueUsingQueryModel(queryModel);
    }


    @Override
    public Iterable findByIds(ID... ids)
            throws JeppettoException {
        QueryModel queryModel = new QueryModel();
        queryModel.addCondition(buildIdCondition(Arrays.asList(ids)));

        if (accessControlContextProvider != null) {
            queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
        }

        return findUsingQueryModel(queryModel);
    }


    @Override
    public Iterable findAll()
            throws JeppettoException {
        QueryModel queryModel = new QueryModel();

        if (accessControlContextProvider != null) {
            queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
        }

        return findUsingQueryModel(queryModel);
    }


    @Override
    public void save(T entity)
            throws OptimisticLockException, JeppettoException {
        try {
            getCurrentSession().saveOrUpdate(entity);

            flush();
        } catch (org.hibernate.OptimisticLockException e) {
            throw new OptimisticLockException(e);
        } catch (StaleObjectStateException e) {
            throw new OptimisticLockException(e);
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        }
    }


    @Override
    public void delete(T entity)
            throws JeppettoException {
        try {
            getCurrentSession().delete(entity);
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        }
    }


    @Override
    public void deleteById(ID id)
            throws JeppettoException {
        try {
            getCurrentSession().delete(findById(id));
        } catch (NoSuchItemException ignore) {
            // If it doesn't exist, no need to delete.
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        }
    }


    @Override
    public void deleteByIds(ID... ids)
            throws FailedBatchException, JeppettoException {
        Map failedDeletes = new LinkedHashMap();

        for (ID id : ids) {
            try {
                deleteById(id);
            } catch (Exception e) {
                failedDeletes.put(id, e);
            }
        }

        if (failedDeletes.size() > 0) {
            // TODO: fix emptyList()...
            throw new FailedBatchException("Unable to delete all items", Collections.emptyList(), failedDeletes);
        }
    }


    @Override
    public  U getUpdateObject() {
        throw new RuntimeException("getUpdateObject not yet implemented");
    }


    @Override
    public  T updateById(U updateObject, ID id)
            throws JeppettoException {
        throw new RuntimeException("updateById not yet implemented");
    }


    @Override
    public  Iterable updateByIds(U updateObject, ID... ids)
            throws FailedBatchException, JeppettoException {
        throw new RuntimeException("updateByIds not yet implemented");
    }


    @Override
    public void flush()
            throws JeppettoException {
        getCurrentSession().flush();
    }


    //-------------------------------------------------------------
    // Implementation - QueryModelDAO
    //-------------------------------------------------------------

    @Override
    public T findUniqueUsingQueryModel(QueryModel queryModel)
            throws NoSuchItemException, TooManyItemsException, JeppettoException {
        T result;

        try {
            if (accessControlContextProvider == null
                || accessControlHelper.annotationAllowsAccess(persistentClass, queryModel.getAccessControlContext(), AccessType.Read)) {
                // noinspection unchecked
                result = (T) buildCriteria(queryModel).uniqueResult();
            } else {
                // noinspection unchecked
                result = (T) createAccessControlledQuery(queryModel).uniqueResult();
            }
        } catch (NonUniqueObjectException e) {
            throw new TooManyItemsException(e.getMessage());
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        }

        if (result == null) {
            throw new NoSuchItemException(persistentClass.getSimpleName(), queryModel.toString());
        }

        return result;
    }


    @Override
    public Iterable findUsingQueryModel(QueryModel queryModel)
            throws JeppettoException {
        try {
            if (accessControlContextProvider == null
                || accessControlHelper.annotationAllowsAccess(persistentClass, queryModel.getAccessControlContext(), AccessType.Read)) {
                Criteria criteria = buildCriteria(queryModel);

                if (queryModel.getSorts() != null) {
                    for (Sort sort : queryModel.getSorts()) {
                        criteria.addOrder(sort.getSortDirection() == SortDirection.Ascending ? Order.asc(sort.getField())
                                                                                             : Order.desc(sort.getField()));
                    }
                }

                if (queryModel.getMaxResults() > 0) {
                    criteria.setMaxResults(queryModel.getMaxResults());
                }

                if (queryModel.getFirstResult() > 0) {
                    criteria.setFirstResult(queryModel.getFirstResult());
                }

                //noinspection unchecked
                return criteria.list();
            } else {
                //noinspection unchecked
                return createAccessControlledQuery(queryModel).list();
            }
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        }
    }


    @Override
    public Object projectUsingQueryModel(QueryModel queryModel)
            throws JeppettoException {
        try {
            return buildCriteria(queryModel).uniqueResult();
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        }
    }


    @Override
    public void deleteUsingQueryModel(QueryModel queryModel)
            throws JeppettoException {
        throw new RuntimeException("deleteUsingQueryModel not yet implemented");
    }


    @Override
    public  T updateUniqueUsingQueryModel(U updateObject, QueryModel queryModel)
            throws JeppettoException {
        throw new RuntimeException("updateUniqueUsingQueryModel not yet implemented");
    }


    @Override
    public  Iterable updateUsingQueryModel(U updateObject, QueryModel queryModel)
            throws JeppettoException {
        throw new RuntimeException("updateUsingQueryModel not yet implemented");
    }


    @Override
    public Condition buildCondition(String conditionField,
                                    ConditionType conditionType,
                                    Iterator argsIterator) {
        Condition condition = new Condition();

        condition.setField(conditionField);

        switch (conditionType) {
        case Between:
            condition.setConstraint(Restrictions.between(conditionField, argsIterator.next(), argsIterator.next()));
            break;

        case Equal:
            condition.setConstraint(Restrictions.eq(conditionField, argsIterator.next()));
            break;

        case GreaterThan:
            condition.setConstraint(Restrictions.gt(conditionField, argsIterator.next()));
            break;

        case GreaterThanEqual:
            condition.setConstraint(Restrictions.ge(conditionField, argsIterator.next()));
            break;

        case IsNotNull:
            condition.setConstraint(Restrictions.isNotNull(conditionField));
            break;

        case IsNull:
            condition.setConstraint(Restrictions.isNull(conditionField));
            break;

        case LessThan:
            condition.setConstraint(Restrictions.lt(conditionField, argsIterator.next()));
            break;

        case LessThanEqual:
            condition.setConstraint(Restrictions.le(conditionField, argsIterator.next()));
            break;

        case NotEqual:
            condition.setConstraint(Restrictions.ne(conditionField, argsIterator.next()));
            break;

        case NotWithin:
            condition.setConstraint(Restrictions.not(Restrictions.in(conditionField, (Collection) argsIterator.next())));
            break;

        case Within:
            condition.setConstraint(Restrictions.in(conditionField, (Collection) argsIterator.next()));
            break;
        }

        return condition;
    }


    @Override
    public Projection buildProjection(String projectionField,
                                      ProjectionType projectionType,
                                      Iterator argsIterator) {
        Projection projection = new Projection();

        projection.setField(projectionField);

        switch (projectionType) {
        case RowCount:
            projection.setDetails(Projections.rowCount());
            break;

        case Count:
            projection.setDetails(Projections.count(projectionField));
            break;

        case CountDistinct:
            projection.setDetails(Projections.countDistinct(projectionField));
            break;

        case Maximum:
            projection.setDetails(Projections.max(projectionField));
            break;

        case Minimum:
            projection.setDetails(Projections.min(projectionField));
            break;

        case Average:
            projection.setDetails(Projections.avg(projectionField));
            break;

        case Sum:
            projection.setDetails(Projections.sum(projectionField));
            break;

        default:
            throw new RuntimeException("Unexpected projection type: " + projectionType);
        }

        return projection;
    }


    //-------------------------------------------------------------
    // Implementation - AccessControlDAO
    //-------------------------------------------------------------

    @Override
    public void save(T object, AccessControlContext accessControlContext)
            throws OptimisticLockException, AccessControlException, JeppettoException {
        ensureAccessControlEnabled();

        try {
            AccessControlContextOverride.set(accessControlContext);

            getCurrentSession().saveOrUpdate(object);

            // flush() here because we want the AccessControlInterceptor to perform its onSave()/onFlushDirty()
            // checks while the override is in place.
            flush();
        } catch (org.hibernate.OptimisticLockException e) {
            throw new OptimisticLockException(e);
        } catch (HibernateException e) {
            throw new JeppettoException(e);
        } finally {
            AccessControlContextOverride.clear();
        }
    }


    @Override
    public void grantAccess(ID id, String accessId, AccessType accessType)
            throws NoSuchItemException, AccessControlException {
        ensureAccessControlEnabled();

        grantAccess(id, accessId, accessType, accessControlContextProvider.getCurrent());
    }


    @Override
    public void grantAccess(ID id, String accessId, AccessType accessType, AccessControlContext accessControlContext)
            throws NoSuchItemException, AccessControlException {
        ensureAccessControlEnabled();

        if (accessType == AccessType.None) {
            revokeAccess(id, accessId, accessControlContext);

            return;
        }

        accessControlHelper.validateContextAllowsWrite(persistentClass, id, accessControlContext, true);

        accessControlHelper.createEntry(persistentClass, id, accessId, accessType);
    }


    @Override
    public void revokeAccess(ID id, String accessId)
            throws NoSuchItemException, AccessControlException {
        ensureAccessControlEnabled();

        revokeAccess(id, accessId, accessControlContextProvider.getCurrent());
    }


    @Override
    public void revokeAccess(ID id, String accessId, AccessControlContext accessControlContext)
            throws NoSuchItemException, AccessControlException {
        ensureAccessControlEnabled();

        accessControlHelper.validateContextAllowsWrite(persistentClass, id, accessControlContext, true);

        accessControlHelper.deleteEntry(persistentClass, id, accessId);
    }


    @Override
    public Map getGrantedAccesses(ID id)
            throws NoSuchItemException, AccessControlException {
        ensureAccessControlEnabled();

        return getGrantedAccesses(id, accessControlContextProvider.getCurrent());
    }


    @Override
    public Map getGrantedAccesses(ID id, AccessControlContext accessControlContext)
            throws NoSuchItemException, AccessControlException {
        ensureAccessControlEnabled();

        accessControlHelper.validateContextAllowsWrite(persistentClass, id, accessControlContext, true);

        return accessControlHelper.getEntries(persistentClass, id);
    }


    @Override
    public AccessControlContextProvider getAccessControlContextProvider() {
        return accessControlContextProvider;
    }


    //-------------------------------------------------------------
    // Methods - Protected
    //-------------------------------------------------------------

    protected Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }


    protected Condition buildIdCondition(Object argument) {
        if (Collection.class.isAssignableFrom(argument.getClass())) {
            return new Condition(idField, Restrictions.in(idField, (Collection) argument));
        } else {
            return new Condition(idField, Restrictions.eq(idField, argument));
        }
    }


    //-------------------------------------------------------------
    // Methods - Private
    //-------------------------------------------------------------

    private Criteria buildCriteria(QueryModel queryModel) {
        Criteria criteria = getCurrentSession().createCriteria(persistentClass);

        if (queryModel.getConditions() != null) {
            for (Condition condition : queryModel.getConditions()) {
                criteria.add((Criterion) condition.getConstraint());
            }
        }

        for (Map.Entry> associationCriteriaEntry : queryModel.getAssociationConditions().entrySet()) {
            Criteria associationCriteria = criteria.createCriteria(associationCriteriaEntry.getKey());

            criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

            for (Condition condition : associationCriteriaEntry.getValue()) {
                associationCriteria.add((Criterion) condition.getConstraint());
            }
        }

        if (queryModel.getProjection() != null) {
            ProjectionList projectionList = Projections.projectionList();

            projectionList.add((org.hibernate.criterion.Projection) queryModel.getProjection().getDetails());

            criteria.setProjection(projectionList);
        }

        return criteria;
    }



    // TODO: Add projection, maxResults, firstResult support
    private Query createAccessControlledQuery(QueryModel queryModel) {
        Criteria criteria = getCurrentSession().createCriteria(persistentClass);

        for (String associationPath : queryModel.getAssociationConditions().keySet()) {
            criteria.createCriteria(associationPath);
        }

        CriteriaQueryTranslator criteriaQueryTranslator = new CriteriaQueryTranslator((SessionFactoryImplementor) sessionFactory,
                                                                                      (CriteriaImpl) criteria,
                                                                                      persistentClass.getName(),
                                                                                      CriteriaQueryTranslator.ROOT_SQL_ALIAS);

        StringBuilder queryStringBuilder = new StringBuilder();

        buildSelectClause(queryStringBuilder, criteriaQueryTranslator, queryModel.getAssociationConditions().keySet());

        List parameters = buildWhereClause(queryStringBuilder, queryModel, criteria, criteriaQueryTranslator);

        if ((queryModel.getAssociationConditions() == null || queryModel.getAssociationConditions().isEmpty())
            && (queryModel.getSorts() == null || queryModel.getSorts().isEmpty())) {
            buildDefaultOrderClause(queryStringBuilder);
        } else {
            // can't use the default ordering by "ace.id" because of "select distinct..." syntax
            buildOrderClause(queryStringBuilder, queryModel, criteria, criteriaQueryTranslator);
        }

        Query query = getCurrentSession().createQuery(queryStringBuilder.toString());

        setParameters(parameters, query, queryModel.getAccessControlContext());

        return query;
    }


    private void buildSelectClause(StringBuilder queryStringBuilder, CriteriaQueryTranslator criteriaQueryTranslator,
                                   Set associationPaths) {
        // "distinct" is needed only for association criteria, but it prevents specifying ordering by "ace.id"
        if (associationPaths.isEmpty()) {
            queryStringBuilder.append("select ");
        } else {
            queryStringBuilder.append("select distinct ");
        }
        queryStringBuilder.append(criteriaQueryTranslator.getRootSQLALias());
        queryStringBuilder.append(" from AccessControlEntry ace, ");
        queryStringBuilder.append(persistentClass.getSimpleName());
        queryStringBuilder.append(' ');
        queryStringBuilder.append(criteriaQueryTranslator.getRootSQLALias());

        for (String associationPath : associationPaths) {
            queryStringBuilder.append(" join ");
            queryStringBuilder.append(criteriaQueryTranslator.getRootSQLALias());
            queryStringBuilder.append('.');
            queryStringBuilder.append(associationPath);
            queryStringBuilder.append(" as ");
            queryStringBuilder.append(criteriaQueryTranslator.getSQLAlias(criteriaQueryTranslator.getCriteria(associationPath)));
        }
    }


    private List buildWhereClause(StringBuilder queryStringBuilder, QueryModel queryModel,
                                              Criteria criteria, CriteriaQueryTranslator criteriaQueryTranslator) {
        List parameters = new ArrayList();

        queryStringBuilder.append(" where ");

        if (queryModel.getConditions() != null) {
            for (Condition condition : queryModel.getConditions()) {
                Criterion criterion = (Criterion) condition.getConstraint();

                queryStringBuilder.append(criterion.toSqlString(criteria, criteriaQueryTranslator));
                queryStringBuilder.append(" and ");

                parameters.addAll(Arrays.asList(criterion.getTypedValues(criteria, criteriaQueryTranslator)));
            }
        }

        if (queryModel.getAssociationConditions() != null) {
            for (Map.Entry> associationCriteriaEntry : queryModel.getAssociationConditions().entrySet()) {
                CriteriaImpl.Subcriteria associationCriteria
                        = (CriteriaImpl.Subcriteria) criteriaQueryTranslator.getCriteria(associationCriteriaEntry.getKey());

                for (Condition condition : associationCriteriaEntry.getValue()) {
                    Criterion criterion = (Criterion) condition.getConstraint();

                    queryStringBuilder.append(criterion.toSqlString(associationCriteria, criteriaQueryTranslator));
                    queryStringBuilder.append(" and ");

                    parameters.addAll(Arrays.asList(criterion.getTypedValues(associationCriteria, criteriaQueryTranslator)));
                }
            }
        }

        queryStringBuilder.append(" ace.objectType = '");
        queryStringBuilder.append(persistentClass.getSimpleName());
        queryStringBuilder.append("' and ace.objectId = ");
        queryStringBuilder.append(criteriaQueryTranslator.getRootSQLALias());
        queryStringBuilder.append('.');
        queryStringBuilder.append(idField);
        queryStringBuilder.append(" and ace.accessibleBy = ? ");

        return parameters;
    }


    private void buildOrderClause(StringBuilder queryStringBuilder, QueryModel queryModel, Criteria criteria,
                                  CriteriaQueryTranslator criteriaQueryTranslator) {
        boolean firstOrderItem = true;

        if (queryModel.getSorts() != null) {
            for (Sort sort : queryModel.getSorts()) {
                if (firstOrderItem) {
                    queryStringBuilder.append(" order by ");
                } else {
                    queryStringBuilder.append(',');
                }

                Order order = sort.getSortDirection() == SortDirection.Ascending ? Order.asc(sort.getField())
                                                                                 : Order.desc(sort.getField());

                queryStringBuilder.append(order.toSqlString(criteria, criteriaQueryTranslator));

                firstOrderItem = false;
            }
        }
    }


    /**
     * Default ordering by ACE.id field (integer) to ensure older-to-newer entries
     *
     * @param queryStringBuilder main query string tbeing built
     */
    private void buildDefaultOrderClause(StringBuilder queryStringBuilder) {
        queryStringBuilder.append(" order by ace.id asc");
    }


    private void setParameters(List parameters, Query query, AccessControlContext accessControlContext) {
        int position = 0;

        for (TypedValue parameter : parameters) {
            query.setParameter(position++, parameter.getValue(), parameter.getType());
        }

        query.setParameter(position, accessControlContext.getAccessId(), STRING_TYPE);
    }


    private void ensureAccessControlEnabled() {
        if (accessControlContextProvider == null) {
            throw new AccessControlException("Access Control is not enabled. No AccessControlContextProvider specified.");
        }
    }
}