com.xpn.xwiki.store.hibernate.query.HqlQueryExecutor.java Source code

Java tutorial

Introduction

Here is the source code for com.xpn.xwiki.store.hibernate.query.HqlQueryExecutor.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.store.hibernate.query;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;

import org.apache.commons.lang.StringUtils;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.NamedQueryDefinition;
import org.hibernate.engine.NamedSQLQueryDefinition;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLookupException;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.context.Execution;
import org.xwiki.job.event.status.JobProgressManager;
import org.xwiki.query.Query;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryExecutor;
import org.xwiki.query.QueryFilter;
import org.xwiki.query.QueryParameter;
import org.xwiki.query.SecureQuery;
import org.xwiki.query.WrappingQuery;
import org.xwiki.security.authorization.ContextualAuthorizationManager;
import org.xwiki.security.authorization.Right;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.internal.store.hibernate.query.HqlQueryUtils;
import com.xpn.xwiki.store.XWikiHibernateBaseStore.HibernateCallback;
import com.xpn.xwiki.store.XWikiHibernateStore;
import com.xpn.xwiki.store.hibernate.HibernateSessionFactory;
import com.xpn.xwiki.util.Util;

/**
 * QueryExecutor implementation for Hibernate Store.
 *
 * @version $Id: 38f688c8842c2ecf8a066f97b84f4c24eb12deb6 $
 * @since 1.6M1
 */
@Component
@Named("hql")
@Singleton
public class HqlQueryExecutor implements QueryExecutor, Initializable {
    /**
     * Path to Hibernate mapping with named queries. Configured via component manager.
     */
    private static final String MAPPING_PATH = "queries.hbm.xml";

    private static final String ESCAPE_LIKE_PARAMETERS_FILTER = "escapeLikeParameters";

    /**
     * Session factory needed for register named queries mapping.
     */
    @Inject
    private HibernateSessionFactory sessionFactory;

    /**
     * Used for access to XWikiContext.
     */
    @Inject
    private Execution execution;

    @Inject
    private ContextualAuthorizationManager authorization;

    @Inject
    private JobProgressManager progress;

    @Inject
    @Named("context")
    private Provider<ComponentManager> componentManagerProvider;

    private volatile Set<String> allowedNamedQueries;

    @Override
    public void initialize() throws InitializationException {
        Configuration configuration = this.sessionFactory.getConfiguration();

        configuration.addInputStream(Util.getResourceAsStream(MAPPING_PATH));
    }

    private Set<String> getAllowedNamedQueries() {
        if (this.allowedNamedQueries == null) {
            synchronized (this) {
                if (this.allowedNamedQueries == null) {
                    this.allowedNamedQueries = new HashSet<>();

                    Configuration configuration = this.sessionFactory.getConfiguration();

                    // Gather the list of allowed named queries
                    Map<String, NamedQueryDefinition> namedQueries = configuration.getNamedQueries();
                    for (Map.Entry<String, NamedQueryDefinition> query : namedQueries.entrySet()) {
                        if (HqlQueryUtils.isSafe(query.getValue().getQuery())) {
                            this.allowedNamedQueries.add(query.getKey());
                        }
                    }
                }
            }
        }

        return this.allowedNamedQueries;
    }

    /**
     * @param statementString the statement to evaluate
     * @return true if the select is allowed for user without PR
     */
    protected static boolean isSafeSelect(String statementString) {
        return HqlQueryUtils.isShortFormStatement(statementString) || HqlQueryUtils.isSafe(statementString);
    }

    protected void checkAllowed(final Query query) throws QueryException {
        if (query instanceof SecureQuery && ((SecureQuery) query).isCurrentAuthorChecked()) {
            if (!this.authorization.hasAccess(Right.PROGRAM)) {
                if (query.isNamed() && !getAllowedNamedQueries().contains(query.getStatement())) {
                    throw new QueryException("Named queries requires programming right", query, null);
                }

                if (!isSafeSelect(query.getStatement())) {
                    throw new QueryException("The query requires programming right", query, null);
                }
            }
        }
    }

    @Override
    public <T> List<T> execute(final Query query) throws QueryException {
        // Make sure the query is allowed in the current context
        checkAllowed(query);

        String oldDatabase = getContext().getWikiId();
        try {
            this.progress.startStep(query, "query.hql.progress.execute", "Execute HQL query [{}]", query);

            if (query.getWiki() != null) {
                getContext().setWikiId(query.getWiki());
            }
            return getStore().executeRead(getContext(), new HibernateCallback<List<T>>() {
                @SuppressWarnings("unchecked")
                @Override
                public List<T> doInHibernate(Session session) {
                    org.hibernate.Query hquery = createHibernateQuery(session, query);

                    List<T> results = hquery.list();
                    if (query.getFilters() != null && !query.getFilters().isEmpty()) {
                        for (QueryFilter filter : query.getFilters()) {
                            results = filter.filterResults(results);
                        }
                    }
                    return results;
                }
            });
        } catch (XWikiException e) {
            throw new QueryException("Exception while executing query", query, e);
        } finally {
            getContext().setWikiId(oldDatabase);

            this.progress.endStep(query);
        }
    }

    protected org.hibernate.Query createHibernateQuery(Session session, Query query) {
        org.hibernate.Query hquery;

        Query filteredQuery = query;
        if (!filteredQuery.isNamed()) {
            // For non-named queries, convert the short form into long form before we apply the filters.
            filteredQuery = new WrappingQuery(filteredQuery) {
                @Override
                public String getStatement() {
                    // handle short queries
                    return completeShortFormStatement(getWrappedQuery().getStatement());
                }
            };
            filteredQuery = filterQuery(filteredQuery, Query.HQL);
            hquery = session.createQuery(filteredQuery.getStatement());
            populateParameters(hquery, filteredQuery);
        } else {
            hquery = createNamedHibernateQuery(session, filteredQuery);
        }

        return hquery;
    }

    private Query filterQuery(Query query, String language) {
        Query filteredQuery = query;

        // If there are Query parameters of type QueryParameter then, for convenience, automatically add the
        // "escapeLikeParameters" filter (if not already there)
        addEscapeLikeParametersFilter(query);

        if (query.getFilters() != null && !query.getFilters().isEmpty()) {
            for (QueryFilter filter : query.getFilters()) {
                // Step 1: For backward-compatibility reasons call #filterStatement() first
                String filteredStatement = filter.filterStatement(filteredQuery.getStatement(), language);
                // Prevent unnecessary creation of WrappingQuery objects when the QueryFilter doesn't modify the
                // statement.
                if (!filteredStatement.equals(filteredQuery.getStatement())) {
                    filteredQuery = new WrappingQuery(filteredQuery) {
                        @Override
                        public String getStatement() {
                            return filteredStatement;
                        }
                    };
                }
                // Step 2: Run #filterQuery()
                filteredQuery = filter.filterQuery(filteredQuery);
            }
        }
        return filteredQuery;
    }

    private void addEscapeLikeParametersFilter(Query query) {
        if (!hasQueryParametersType(query)) {
            return;
        }

        // Find the component class for the "escapeLikeParameters" filter
        QueryFilter escapeFilter;
        try {
            escapeFilter = this.componentManagerProvider.get().getInstance(QueryFilter.class,
                    ESCAPE_LIKE_PARAMETERS_FILTER);
        } catch (ComponentLookupException e) {
            // Shouldn't happen!
            throw new RuntimeException(
                    String.format("Failed to locate [%s] Query Filter", ESCAPE_LIKE_PARAMETERS_FILTER), e);
        }

        boolean found = false;
        for (QueryFilter filter : query.getFilters()) {
            if (escapeFilter.getClass().getName().equals(filter.getClass().getName())) {
                found = true;
                break;
            }
        }

        if (!found) {
            query.addFilter(escapeFilter);
        }
    }

    private boolean hasQueryParametersType(Query query) {
        boolean found = false;

        for (Object value : query.getNamedParameters().values()) {
            if (value instanceof QueryParameter) {
                found = true;
                break;
            }
        }
        if (!found) {
            for (Object value : query.getPositionalParameters().values()) {
                if (value instanceof QueryParameter) {
                    found = true;
                    break;
                }
            }
        }

        return found;
    }

    /**
     * Append the required select clause to HQL short query statements. Short statements are the only way for users
     * without programming rights to perform queries. Such statements can be for example:
     * <ul>
     * <li>{@code , BaseObject obj where doc.fullName=obj.name and obj.className='XWiki.MyClass'}</li>
     * <li>{@code where doc.creationDate > '2008-01-01'}</li>
     * </ul>
     *
     * @param statement the statement to complete if required.
     * @return the complete statement if it had to be completed, the original one otherwise.
     */
    protected String completeShortFormStatement(String statement) {
        String lcStatement = statement.toLowerCase().trim();
        if (lcStatement.isEmpty() || lcStatement.startsWith(",") || lcStatement.startsWith("where ")
                || lcStatement.startsWith("order by ")) {
            return "select doc.fullName from XWikiDocument doc " + statement.trim();
        }

        return statement;
    }

    private org.hibernate.Query createNamedHibernateQuery(Session session, Query query) {
        org.hibernate.Query hQuery = session.getNamedQuery(query.getStatement());
        Query filteredQuery = query;
        if (filteredQuery.getFilters() != null && !filteredQuery.getFilters().isEmpty()) {
            // Since we can't modify the Hibernate query statement at this point we need to create a new one to apply
            // the query filter. This comes with a performance cost, we could fix it by handling named queries ourselves
            // and not delegate them to Hibernate. This way we would always get a statement that we can transform before
            // the execution.
            boolean isNative = hQuery instanceof SQLQuery;
            String language = isNative ? "sql" : Query.HQL;
            final String statement = hQuery.getQueryString();

            // Run filters
            filteredQuery = filterQuery(new WrappingQuery(filteredQuery) {
                @Override
                public String getStatement() {
                    return statement;
                }
            }, language);

            if (isNative) {
                hQuery = session.createSQLQuery(filteredQuery.getStatement());
                // Copy the information about the return column types, if possible.
                NamedSQLQueryDefinition definition = (NamedSQLQueryDefinition) this.sessionFactory
                        .getConfiguration().getNamedSQLQueries().get(query.getStatement());
                if (!StringUtils.isEmpty(definition.getResultSetRef())) {
                    ((SQLQuery) hQuery).setResultSetMapping(definition.getResultSetRef());
                }
            } else {
                hQuery = session.createQuery(filteredQuery.getStatement());
            }
        }
        populateParameters(hQuery, filteredQuery);
        return hQuery;
    }

    /**
     * @param hquery query to populate parameters
     * @param query query from to populate.
     */
    protected void populateParameters(org.hibernate.Query hquery, Query query) {
        if (query.getOffset() > 0) {
            hquery.setFirstResult(query.getOffset());
        }
        if (query.getLimit() > 0) {
            hquery.setMaxResults(query.getLimit());
        }
        for (Entry<String, Object> e : query.getNamedParameters().entrySet()) {
            setNamedParameter(hquery, e.getKey(), e.getValue());
        }
        if (query.getPositionalParameters().size() > 0) {
            int start = Collections.min(query.getPositionalParameters().keySet());
            if (start == 0) {
                // jdbc-style positional parameters. "?"
                for (Entry<Integer, Object> e : query.getPositionalParameters().entrySet()) {
                    hquery.setParameter(e.getKey(), e.getValue());
                }
            } else {
                // jpql-style. "?index"
                for (Entry<Integer, Object> e : query.getPositionalParameters().entrySet()) {
                    // hack. hibernate assume "?1" is named parameter, so use string "1".
                    setNamedParameter(hquery, String.valueOf(e.getKey()), e.getValue());
                }
            }
        }
    }

    /**
     * Sets the value of the specified named parameter, taking into account the type of the given value.
     *
     * @param query the query to set the parameter for
     * @param name the parameter name
     * @param value the non-null parameter value
     */
    protected void setNamedParameter(org.hibernate.Query query, String name, Object value) {
        if (value instanceof Collection) {
            query.setParameterList(name, (Collection<?>) value);
        } else if (value.getClass().isArray()) {
            query.setParameterList(name, (Object[]) value);
        } else {
            query.setParameter(name, value);
        }
    }

    /**
     * @return Store component
     */
    protected XWikiHibernateStore getStore() {
        return getContext().getWiki().getHibernateStore();
    }

    /**
     * @return XWiki Context
     */
    protected XWikiContext getContext() {
        return (XWikiContext) this.execution.getContext().getProperty(XWikiContext.EXECUTIONCONTEXT_KEY);
    }
}