org.jahia.services.search.jcr.JahiaJCRSearchProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.search.jcr.JahiaJCRSearchProvider.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program 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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.search.jcr;

import com.google.common.collect.Sets;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.math.util.MathUtils;
import org.apache.jackrabbit.util.ISO8601;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.jahia.api.Constants;
import org.jahia.exceptions.JahiaException;
import org.jahia.registries.ServicesRegistry;
import org.jahia.services.categories.Category;
import org.jahia.services.content.*;
import org.jahia.services.render.RenderContext;
import org.jahia.services.search.*;
import org.jahia.services.search.SearchCriteria.DateValue;
import org.jahia.services.search.SearchCriteria.NodeProperty;
import org.jahia.services.search.SearchCriteria.Ordering;
import org.jahia.services.search.SearchCriteria.Ordering.CaseConversion;
import org.jahia.services.search.SearchCriteria.Ordering.Order;
import org.jahia.services.search.SearchCriteria.Term;
import org.jahia.services.search.SearchCriteria.Term.MatchType;
import org.jahia.services.search.SearchCriteria.Term.SearchFields;
import org.jahia.services.search.spell.CompositeSpellChecker;
import org.jahia.services.sites.JahiaSitesService;
import org.jahia.services.tags.TaggingService;
import org.jahia.settings.SettingsBean;
import org.jahia.utils.DateUtils;
import org.jahia.utils.Patterns;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.*;
import javax.jcr.query.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;

import static org.jahia.services.content.JCRContentUtils.stringToJCRSearchExp;
import static org.jahia.services.content.JCRContentUtils.stringToQueryLiteral;

/**
 * This is the default search provider used by Jahia and used the index created by Jahia's main
 * repository, which is based on Apache Jackrabbit. The search request is also done on mounted
 * external repositories.
 * <p/>
 * For now the search criteria is converted to XPATH queries, which is despite of the deprecation
 * still the most stable and performance means to use search in Jackrabbit.
 * <p/>
 * For future versions we may change to either SQL-2 or directly the QueryObejctModel specified
 * in JSR-283.
 *
 * @author Benjamin Papez
 */
public class JahiaJCRSearchProvider implements SearchProvider, SearchProvider.SupportsSuggestion {

    private static final Pattern AND_PATTERN = Pattern.compile(" AND ");

    private static final Pattern MULTIPLE_SPACES_PATTERN = Pattern.compile("\\s{2,}");

    private static final Pattern NOT_PATTERN = Pattern.compile(" NOT ");

    private static final Pattern OR_PATTERN = Pattern.compile(" OR ");
    private static final String AND = "and";
    private static final String OR = "or";

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

    private TaggingService taggingService = null;

    private String name;

    private Set<String> typesToHideFromSearchResults;

    public SearchResponse search(SearchCriteria criteria, RenderContext context) {
        SearchResponse response = new SearchResponse();
        List<Hit<?>> results = new ArrayList<Hit<?>>();
        response.setResults(results);

        try {
            JCRSessionWrapper session = ServicesRegistry.getInstance().getJCRStoreService().getSessionFactory()
                    .getCurrentUserSession(context.getMainResource().getWorkspace(),
                            context.getMainResource().getLocale(), context.getFallbackLocale());
            Query query = buildQuery(criteria, session);
            final int offset = criteria.getOffset() < 0 ? 0 : (int) criteria.getOffset();
            final int limit = criteria.getLimit() <= 0 ? Integer.MAX_VALUE : (int) criteria.getLimit();
            response.setOffset(offset);
            response.setLimit(limit);
            int count = 0;
            if (query != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Executing search query [{}]", query.getStatement());
                }
                QueryResult queryResult = query.execute();
                RowIterator it = queryResult.getRows();
                Set<String> languages = new HashSet<String>();
                if (it.hasNext()) {
                    if (!criteria.getLanguages().isEmpty()) {
                        for (String languageCode : criteria.getLanguages().getValues()) {
                            if (!StringUtils.isEmpty(languageCode)) {
                                languages.add(languageCode);
                            }
                        }
                    } else {
                        if (session.getLocale() != null) {
                            languages.add(session.getLocale().toString());
                        }
                    }
                }

                Set<String> usageFilterSites = !criteria.getSites().isEmpty()
                        && !criteria.getSites().getValue().equals("-all-")
                        && !criteria.getSitesForReferences().isEmpty()
                                ? Sets.newHashSet(criteria.getSites().getValues())
                                : null;

                Set<String> addedNodes = new HashSet<String>();
                Map<String, JCRNodeHit> addedHits = new HashMap<String, JCRNodeHit>();
                List<JCRNodeHit> hitsToAdd = new ArrayList<JCRNodeHit>();
                final int requiredHits = limit + offset;

                boolean displayableNodeCompat = Boolean.valueOf(
                        SettingsBean.getInstance().getPropertiesFile().getProperty("search.displayableNodeCompat"));

                while (it.hasNext()) {
                    count++;
                    Row row = it.nextRow();
                    try {
                        JCRNodeWrapper node = (JCRNodeWrapper) row.getNode();
                        if (node != null && (node.isNodeType(Constants.JAHIANT_TRANSLATION)
                                || Constants.JCR_CONTENT.equals(node.getName()))) {
                            node = node.getParent();
                        }
                        if (node != null && node.isNodeType("jnt:vanityUrl")) {
                            node = node.getParent().getParent();
                        }
                        if (node != null && addedNodes.add(node.getIdentifier())) {
                            boolean skipNode = isNodeToSkip(node, criteria, languages);

                            JCRNodeHit hit = !skipNode ? buildHit(row, node, context, usageFilterSites) : null;

                            if (!skipNode && !displayableNodeCompat) {
                                //check if node is invisible (or don't have a displayable parent or reference)
                                skipNode = hit.getDisplayableNode() == null;
                            }
                            if (!skipNode && usageFilterSites != null
                                    && !usageFilterSites.contains(node.getResolveSite().getName())) {
                                skipNode = hit.getUsages().isEmpty();
                            }
                            if (!skipNode) {
                                hitsToAdd.add(hit);

                                if (hitsToAdd.size() + addedHits.size() >= requiredHits) {
                                    SearchServiceImpl.executeURLModificationRules(hitsToAdd, context);
                                    addHitsToResults(hitsToAdd, results, addedHits, offset);
                                    hitsToAdd.clear();

                                    if (addedHits.size() >= requiredHits) {
                                        response.setHasMore(true);
                                        if (it.getSize() > 0) {
                                            int approxCount = ((int) it.getSize() * addedHits.size() / count);
                                            approxCount = (int) Math.ceil(MathUtils.round(approxCount,
                                                    approxCount < 1000 ? -1 : (approxCount < 10000 ? -2 : -3),
                                                    BigDecimal.ROUND_UP));
                                            response.setApproxCount(approxCount);
                                        }
                                        return response;
                                    }
                                }
                            }
                        }
                    } catch (ItemNotFoundException e) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Found node is not visible or published: " + row.getPath(), e);
                        }
                    } catch (PathNotFoundException e) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Found node is not visible or published: " + row.getPath(), e);
                        }
                    } catch (Exception e) {
                        logger.warn("Error resolving search hit", e);
                    }
                }
                if (hitsToAdd.size() > 0) {
                    SearchServiceImpl.executeURLModificationRules(hitsToAdd, context);
                    addHitsToResults(hitsToAdd, results, addedHits, offset);
                }
            }
        } catch (RepositoryException e) {
            if (e.getMessage() != null && e.getMessage().contains(ParseException.class.getName())) {
                logger.warn(e.getMessage());
                if (logger.isDebugEnabled()) {
                    logger.debug(e.getMessage(), e);
                }
            } else {
                logger.error("Error while trying to perform a search", e);
            }
        } catch (Exception e) {
            logger.error("Error while trying to perform a search", e);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Search query has {} results", results.size());
        }
        return response;
    }

    private void addHitsToResults(List<JCRNodeHit> collectedHits, List<Hit<?>> results,
            Map<String, JCRNodeHit> addedHits, int offset) {
        for (JCRNodeHit hit : collectedHits) {
            if (!addedHits.containsKey(hit.getLink())) {
                addedHits.put(hit.getLink(), hit);
                if (addedHits.size() >= offset) {
                    results.add(hit);
                }
            } else {
                JCRNodeHit previousHit = addedHits.get(hit.getLink());
                for (Row row : hit.getRows()) {
                    previousHit.addRow(row);
                }
            }
        }
    }

    private boolean isNodeToSkip(JCRNodeWrapper node, SearchCriteria criteria, Set<String> languages) {
        boolean skipNode = false;
        try {
            if (typesToHideFromSearchResults.contains(node.getPrimaryNodeTypeName())) {
                return true;
            }
            if (!languages.isEmpty() && isSiteSearch(criteria)
                    && (node.isFile() || node.isNodeType(Constants.NT_FOLDER))) {
                // if just site-search and no file-search, then skip the node unless it is referred
                // by a node in the wanted language - unreferenced files are skipped
                skipNode = !isFileSearch(criteria) ? true : skipNode;
                for (PropertyIterator it = node.getWeakReferences(); it.hasNext();) {
                    // if site-search and file-search, then skip the node unless it is referred
                    // by a node in the wanted language - unreferenced files are not skipped
                    skipNode = isFileSearch(criteria) ? true : skipNode;

                    try {
                        JCRNodeWrapper refNode = (JCRNodeWrapper) it.nextProperty().getParent();
                        if (languages.contains(refNode.getLanguage())) {
                            skipNode = false;
                            break;
                        }
                    } catch (Exception e) {
                        logger.debug("Error while trying to check for node language", e);
                    }
                }
            }
        } catch (RepositoryException e) {
            logger.debug("Error while trying to check for node language", e);
        }

        return skipNode;
    }

    private JCRNodeHit buildHit(Row row, JCRNodeWrapper node, RenderContext context, Set<String> usageFilterSites)
            throws RepositoryException {
        JCRNodeHit searchHit = null;
        if (node.isFile() || node.isNodeType(Constants.NT_FOLDER)) {
            searchHit = new FileHit(node, context);
        } else {
            searchHit = new JCRNodeHit(node, context);
        }

        try {
            searchHit.setUsageFilterSites(usageFilterSites);
            searchHit.setScore((float) (row.getScore() / 1000.));

            searchHit.addRow(row);
        } catch (Exception e) {
            logger.warn("Search details cannot be retrieved", e);
        }
        return searchHit;
    }

    private String buildSQLQuery(SearchCriteria params, JCRSessionWrapper session) {
        StringBuilder query = new StringBuilder("select * from ");
        query.append("[");
        query.append(getNodeType(params));
        query.append("] as n ");

        String path = null;
        boolean includeChildren = false;
        if (!params.getFilePath().isEmpty()) {
            path = params.getFilePath().getValue().trim();
            includeChildren = params.getFilePath().isIncludeChildren();
        } else if (!params.getPagePath().isEmpty()) {
            path = params.getPagePath().getValue().trim();
            includeChildren = params.getPagePath().isIncludeChildren();
        }
        if (path != null) {
            query.append("where (");
            if (includeChildren) {
                query.append("isdescendantnode(n,'");
            } else {
                query.append("ischildnode(n,'");
            }
            query.append(path).append("')");
            query.append(")");
        } else if (!params.getSites().isEmpty()) {
            query.append("where (");
            if ("-all-".equals(params.getSites().getValue())) {
                query.append("isdescendantnode(n,'/sites/')"); // ticket PEU-468
            } else {
                Set<String> sites = new LinkedHashSet<String>();
                for (String site : params.getSites().getValues()) {
                    sites.add(site);
                }
                if (!params.getSitesForReferences().isEmpty()) {
                    for (String site : params.getSitesForReferences().getValues()) {
                        sites.add(site);
                    }
                }
                for (String site : sites) {
                    query.append("isdescendantnode(n,'/sites/").append(site).append("') or ");
                }
                query.delete(query.length() - 4, query.length());
            }
            if (isSiteSearch(params)) {
                //                query.append("/*[@j:isHomePage='true' or fn:name() = 'files' or fn:name() = 'contents']");
            }
            query.append(")");
        }

        query = appendConstraints(params, query, false);
        query = appendOrdering(params, query, false);

        return query.toString();
    }

    private String buildXpathQuery(SearchCriteria params, JCRSessionWrapper session) {
        String xpathQuery = null;

        StringBuilder query = new StringBuilder(256);
        String path = null;
        boolean includeChildren = false;
        if (!params.getFilePath().isEmpty()) {
            path = params.getFilePath().getValue().trim();
            includeChildren = params.getFilePath().isIncludeChildren();
        } else if (!params.getPagePath().isEmpty()) {
            path = params.getPagePath().getValue().trim();
            includeChildren = params.getPagePath().isIncludeChildren();
        }
        if (path != null) {
            String[] pathTokens = path != null ? Patterns.SLASH.split(StringEscapeUtils.unescapeHtml(path))
                    : ArrayUtils.EMPTY_STRING_ARRAY;
            String lastFolder = null;
            StringBuilder jcrPath = new StringBuilder(64);
            jcrPath.append("/jcr:root/");
            for (String folder : pathTokens) {
                if (folder.length() == 0) {
                    continue;
                }
                if (!includeChildren) {
                    if (lastFolder != null) {
                        jcrPath.append(lastFolder).append("/");
                    }
                    lastFolder = folder;
                } else {
                    jcrPath.append(folder).append("/");
                }
            }
            if (includeChildren) {
                jcrPath.append("/");
                lastFolder = "*";
            }
            query.append(ISO9075.encodePath(jcrPath.toString())).append("element(").append(lastFolder).append(",")
                    .append(getNodeType(params)).append(")");
        } else if (!params.getSites().isEmpty()) {
            query.append("/jcr:root/sites/");
            if ("-all-".equals(params.getSites().getValue())) {
                query.append("*");
            } else {
                Set<String> sites = new LinkedHashSet<String>();
                for (String site : params.getSites().getValues()) {
                    sites.add(site);
                }
                if (!params.getSitesForReferences().isEmpty()) {
                    for (String site : params.getSitesForReferences().getValues()) {
                        sites.add(site);
                    }
                }
                if (sites.size() == 1) {
                    query.append(sites.iterator().next());
                } else {
                    query.append("*[");
                    int i = 0;
                    for (String site : sites) {
                        if (i > 0) {
                            query.append(" or ");
                        }
                        query.append("fn:name() = '");
                        query.append(site);
                        query.append("'");
                        i++;
                    }
                    query.append("]");
                }
            }

            if (isSiteSearch(params)) {
                query.append("/*[@j:isHomePage='true' or fn:name() = 'files' or fn:name() = 'contents']");
            }

            query.append("//element(*,").append(getNodeType(params)).append(")");
        } else {
            query.append("//element(*,").append(getNodeType(params)).append(")");
        }

        query = appendConstraints(params, query, true);
        query = appendOrdering(params, query, true);

        xpathQuery = query.toString();

        if (logger.isDebugEnabled()) {
            logger.debug("XPath query built: " + xpathQuery);
        }

        return xpathQuery;
    }

    private boolean isFileSearch(SearchCriteria params) {
        for (Term term : params.getTerms()) {
            if (term.getFields() != null
                    && (term.getFields().isSiteContent() || (!term.getFields().isDescription()
                            && !term.getFields().isFileContent() && !term.getFields().isFilename()
                            && !term.getFields().isKeywords() && !term.getFields().isTitle()))
                    && !(term.getFields().isDescription() && term.getFields().isFileContent()
                            && term.getFields().isFilename() && term.getFields().isKeywords()
                            && term.getFields().isTitle())) {
                return false;
            }
        }
        return true;
    }

    private boolean isSiteSearch(SearchCriteria params) {
        for (Term term : params.getTerms()) {
            if (term.getFields() != null && term.getFields().isSiteContent()) {
                return true;
            }
        }
        return false;
    }

    private String getNodeType(SearchCriteria params) {
        if (StringUtils.isNotEmpty(params.getNodeType())) {
            return params.getNodeType();
        }

        if (isFileSearch(params) && !isSiteSearch(params)) {
            return Constants.NT_HIERARCHYNODE;
            //        } else if (!isFileSearch(params) && isSiteSearch(params)) {
            //            return Constants.JAHIANT_CONTENT;
        }

        return Constants.JAHIAMIX_SEARCHABLE;
    }

    private StringBuilder appendConstraints(SearchCriteria params, StringBuilder query, boolean xpath) {
        StringBuilder constraints = new StringBuilder(64);

        addTermConstraints(params, constraints, xpath);

        addDateAndAuthorConstraints(params, constraints, xpath);

        addFileTypeConstraints(params, constraints, xpath);

        addLanguageConstraints(params, constraints, xpath);

        List<NodeProperty> props = params.getPropertiesAll();
        if (!props.isEmpty()) {
            addPropertyConstraints(constraints, props, xpath);
        }

        if (constraints.length() > 0) {
            if (xpath) {
                query.append("[").append(constraints).append("]");
            } else {
                if (query.indexOf("where") > -1) {
                    query.append(" and ");
                } else {
                    query.append(" where ");
                }
                query.append("(").append(constraints).append(")");
            }
        }

        return query;
    }

    private StringBuilder appendOrdering(SearchCriteria params, StringBuilder query, boolean xpath) {
        StringBuilder orderByClause = new StringBuilder();
        if (params.getOrderings().isEmpty()) {
            orderByClause.append(xpath ? " order by jcr:score() descending" : " order by score() desc");
        } else {
            for (Ordering ordering : params.getOrderings()) {
                StringBuilder orderingBuilder = new StringBuilder();
                switch (ordering.getOperand()) {
                case SCORE:
                    orderingBuilder.append(xpath ? "jcr:score()" : "SCORE()");
                    break;
                case PROPERTY:
                    orderingBuilder.append(xpath ? "@" : "[").append(ordering.getPropertyName())
                            .append(xpath ? "" : "]");
                    break;
                }
                if (ordering.getCaseConversion() != null) {
                    orderingBuilder.insert(0,
                            ordering.getCaseConversion() == CaseConversion.LOWER
                                    ? xpath ? "fn:lower-case(" : "LOWER("
                                    : xpath ? "fn:upper-case(" : "UPPER(");
                    orderingBuilder.append(")");
                }
                // there is no normalize function for SQL-2 yet, so for now we use it only for xpath and ignore it for SQL-2
                if (ordering.isNormalize() && xpath) {
                    orderingBuilder.insert(0, "rep:normalize(");
                    orderingBuilder.append(")");
                }

                orderingBuilder.append(ordering.getOrder() == Order.ASCENDING ? xpath ? " ascending" : " asc"
                        : xpath ? " descending" : " desc");
                if (orderByClause.length() > 0) {
                    orderByClause.append(", ");
                }
                orderByClause.append(orderingBuilder);
            }
            orderByClause.insert(0, " order by ");
        }
        query.append(orderByClause);
        return query;
    }

    private StringBuilder addConstraint(StringBuilder constraints, String operand, String constraint) {
        if (constraints.length() > 0) {
            constraints.append(" ").append(operand).append(" ");
        }
        return constraints.append(constraint);
    }

    private void addDateAndAuthorConstraints(SearchCriteria params, StringBuilder constraints, boolean xpath) {

        if (params.getCreatedBy() != null && params.getCreatedBy().length() > 0) {
            addConstraint(constraints, AND, getContainsExpr(Constants.JCR_CREATEDBY,
                    stringToJCRSearchExp(params.getCreatedBy().trim()), xpath));
        }

        if (params.getLastModifiedBy() != null && params.getLastModifiedBy().length() > 0) {
            addConstraint(constraints, AND, getContainsExpr(Constants.JCR_LASTMODIFIEDBY,
                    stringToJCRSearchExp(params.getLastModifiedBy().trim()), xpath));
        }

        if (!params.getCreated().isEmpty() && DateValue.Type.ANYTIME != params.getCreated().getType()) {
            addDateConstraint(constraints, params.getCreated(), Constants.JCR_CREATED, xpath);
        }

        if (!params.getLastModified().isEmpty() && DateValue.Type.ANYTIME != params.getLastModified().getType()) {
            addDateConstraint(constraints, params.getLastModified(), Constants.JCR_LASTMODIFIED, xpath);
        }

    }

    private void addDateConstraint(StringBuilder constraints, SearchCriteria.DateValue dateValue, String paramName,
            boolean xpath) {
        Calendar greaterThanDate = Calendar.getInstance();
        Calendar smallerThanDate = null;

        if (DateValue.Type.TODAY == dateValue.getType()) {
            // no date adjustment needed
        } else if (DateValue.Type.LAST_WEEK == dateValue.getType()) {
            greaterThanDate.add(Calendar.DATE, -7);
        } else if (DateValue.Type.LAST_MONTH == dateValue.getType()) {
            greaterThanDate.add(Calendar.MONTH, -1);
        } else if (DateValue.Type.LAST_THREE_MONTHS == dateValue.getType()) {
            greaterThanDate.add(Calendar.MONTH, -3);
        } else if (DateValue.Type.LAST_SIX_MONTHS == dateValue.getType()) {
            greaterThanDate.add(Calendar.MONTH, -6);
        } else if (DateValue.Type.RANGE == dateValue.getType()) {
            greaterThanDate = null;
            smallerThanDate = null;
            if (dateValue.getFromAsDate() != null) {
                greaterThanDate = Calendar.getInstance();
                greaterThanDate.setTime(dateValue.getFromAsDate());
            }
            if (dateValue.getToAsDate() != null) {
                smallerThanDate = Calendar.getInstance();
                smallerThanDate.setTime(dateValue.getToAsDate());
            }
        } else {
            throw new IllegalArgumentException("Unknown date value type '" + dateValue.getType() + "'");
        }

        try {
            if (greaterThanDate != null) {
                addConstraint(constraints, AND, getPropertyName(paramName, xpath) + " >= "
                        + getDateLiteral(DateUtils.dayStart(greaterThanDate), xpath));
            }
            if (smallerThanDate != null) {
                addConstraint(constraints, AND, getPropertyName(paramName, xpath) + " <= "
                        + getDateLiteral(DateUtils.dayEnd(smallerThanDate), xpath));
            }
        } catch (IllegalStateException e) {
            logger.warn(e.getMessage(), e);
        }
    }

    private void addFileTypeConstraints(SearchCriteria params, StringBuilder constraints, boolean xpath) {

        if (StringUtils.isNotEmpty(params.getFileType())) {
            List<String> mimeTypes = JCRContentUtils.getInstance().getMimeTypes().get(params.getFileType());
            if (mimeTypes != null && !mimeTypes.isEmpty()) {
                if (mimeTypes.size() > 1) {
                    StringBuilder fileTypeConstraints = new StringBuilder(128);
                    for (String mimeType : mimeTypes) {
                        addConstraint(fileTypeConstraints, OR, getMimeTypeConstraint(mimeType, xpath));
                    }
                    addConstraint(constraints, AND, fileTypeConstraints.insert(0, "(").append(")").toString());
                } else {
                    addConstraint(constraints, AND, getMimeTypeConstraint(mimeTypes.get(0), xpath));
                }
            } else {
                logger.warn("Unsupported file type '" + params.getFileType()
                        + "'. See applicationcontext-basejahiaconfig.xml file" + " for configured file types.");
            }
        }
    }

    private void addPropertyConstraintCategory(StringBuilder categoryConstraints, String name, String value,
            boolean includeChildren, boolean xpath) {
        try {
            Category cat = Category.getCategoryByPath(value, JCRSessionFactory.getInstance().getCurrentUser());
            if (cat == null) {
                logger.warn("User " + JCRSessionFactory.getInstance().getCurrentUser().getUsername()
                        + " has no right to read the category");
                return;
            }
            addConstraint(categoryConstraints, OR,
                    getPropertyName(name, xpath) + "=" + stringToJCRSearchExp(cat.getID()));
            if (includeChildren) {
                addSubCategoriesConstraints(categoryConstraints, cat, name, xpath);
            }
        } catch (JahiaException e) {
            logger.warn("Category: " + value + " could not be retrieved", e);
        }
    }

    private void addSubCategoriesConstraints(StringBuilder categoryConstraints, Category category, String name,
            boolean xpath) throws JahiaException {

        List<Category> childs = category.getChildCategories();
        if (childs != null && childs.size() > 0) {
            for (Category cat : childs) {
                addConstraint(categoryConstraints, OR,
                        getPropertyName(name, xpath) + "=" + stringToJCRSearchExp(cat.getID()));
                addSubCategoriesConstraints(categoryConstraints, cat, name, xpath);
            }
        }
    }

    private void addPropertyConstraints(StringBuilder constraints, List<NodeProperty> properties, boolean xpath) {
        for (NodeProperty property : properties) {
            if (!property.isEmpty()) {
                if (NodeProperty.Type.CATEGORY == property.getType()) {
                    StringBuilder categoryConstraints = new StringBuilder(64);
                    for (String value : property.getCategoryValue().getValues()) {
                        addPropertyConstraintCategory(categoryConstraints, property.getName(), value,
                                property.getCategoryValue().isIncludeChildren(), xpath);
                    }
                    if (categoryConstraints.length() > 0) {
                        addConstraint(constraints, AND, categoryConstraints.insert(0, "(").append(")").toString());
                    }
                } else if (NodeProperty.Type.DATE == property.getType()) {
                    addDateConstraint(constraints, property.getDateValue(), property.getName(), xpath);
                } else if (NodeProperty.Type.TEXT == property.getType()) {
                    StringBuilder propertyConstraints = new StringBuilder(64);
                    for (String value : property.getValues()) {
                        if (property.isConstrained()) {
                            String matchType = "=";
                            if (property.getMatch() == MatchType.WITHOUT_WORDS) {
                                matchType = "!=";
                            }
                            addConstraint(propertyConstraints, OR, getPropertyName(property.getName(), xpath)
                                    + matchType + stringToJCRSearchExp(value));
                        } else {
                            addConstraint(propertyConstraints, OR, getContainsExpr(property.getName(),
                                    getSearchExpressionForMatchType(value, property.getMatch(), false), xpath));
                        }
                    }
                    if (propertyConstraints.length() > 0) {
                        if (property.getValues().length == 1) {
                            addConstraint(constraints, AND, propertyConstraints.toString());
                        } else {
                            addConstraint(constraints, AND, "(" + propertyConstraints.toString() + ")");
                        }
                    }
                } else if (NodeProperty.Type.BOOLEAN == property.getType()) {
                    // only handle 'true' case
                    if (Boolean.parseBoolean(property.getValue())) {
                        addConstraint(constraints, AND, getPropertyName(property.getName(), xpath) + "='true'");
                    }
                } else {
                    throw new IllegalArgumentException(
                            "Unknown document property type '" + property.getType() + "'");
                }
            }
        }
    }

    private void addTermConstraints(SearchCriteria params, StringBuilder constraints, boolean xpath) {

        for (Term textSearch : params.getTerms()) {

            if (!textSearch.isEmpty()) {
                String searchExpression = getSearchExpressionForMatchType(textSearch.getTerm(),
                        textSearch.getMatch(), textSearch.isApplyFilter());

                SearchFields searchFields = textSearch.getFields();
                StringBuilder textSearchConstraints = new StringBuilder(256);
                if (searchFields.isSiteContent() || (!searchFields.isTags() && !searchFields.isFileContent()
                        && !searchFields.isDescription() && !searchFields.isTitle() && !searchFields.isKeywords()
                        && !searchFields.isFilename())) {
                    addConstraint(textSearchConstraints, OR, xpath ? ("jcr:contains(., " + searchExpression + ")")
                            : ("contains(n, " + searchExpression + ")"));
                }
                if (searchFields.isFileContent()) {
                    if (xpath) {
                        addConstraint(textSearchConstraints, OR,
                                "jcr:contains(jcr:content," + searchExpression + ")");
                    } else {
                        addConstraint(textSearchConstraints, OR,
                                getContainsExpr("jcr:content", searchExpression, xpath));
                    }
                }
                if (searchFields.isDescription()) {
                    addConstraint(textSearchConstraints, OR,
                            getContainsExpr(Constants.JCR_DESCRIPTION, searchExpression, xpath));
                }
                if (searchFields.isTitle()) {
                    addConstraint(textSearchConstraints, OR,
                            getContainsExpr(Constants.JCR_TITLE, searchExpression, xpath));
                }
                if (searchFields.isKeywords()) {
                    addConstraint(textSearchConstraints, OR,
                            getContainsExpr(Constants.KEYWORDS, searchExpression, xpath));
                }

                String[] terms = null;
                String constraint = "or";
                if (searchFields.isFilename() || searchFields.isTags()) {
                    if (textSearch.getMatch() == MatchType.ANY_WORD || textSearch.getMatch() == MatchType.ALL_WORDS
                            || textSearch.getMatch() == MatchType.WITHOUT_WORDS) {
                        terms = Patterns.SPACE.split(cleanMultipleWhiteSpaces(textSearch.getTerm()));
                        if (textSearch.getMatch() == MatchType.ALL_WORDS
                                || textSearch.getMatch() == MatchType.WITHOUT_WORDS) {
                            constraint = AND;
                        }
                    } else {
                        terms = new String[] { textSearch.getTerm() };
                    }
                }
                if (searchFields.isFilename()) {
                    StringBuilder nameSearchConstraints = new StringBuilder(256);
                    for (String term : terms) {
                        final String likeTerm = term.contains("*")
                                ? stringToQueryLiteral(StringUtils.replaceChars(term, '*', '%'))
                                : stringToQueryLiteral("%" + term + "%");
                        String termConstraint = xpath ? ("jcr:like(fn:name(), " + likeTerm + ")")
                                : ("localname(n) like " + likeTerm);
                        if (textSearch.getMatch() == MatchType.WITHOUT_WORDS) {
                            termConstraint = "not(" + termConstraint + ")";
                        }
                        addConstraint(nameSearchConstraints, constraint, termConstraint);
                    }
                    addConstraint(textSearchConstraints, OR, nameSearchConstraints.toString());
                }
                if (searchFields.isTags() && getTaggingService() != null
                        && (params.getSites().getValue() != null || params.getOriginSiteKey() != null)
                        && !StringUtils.containsAny(textSearch.getTerm(), "?*")) {
                    for (String term : terms) {
                        String tag = taggingService.getTagHandler().execute(term);
                        if (!StringUtils.isEmpty(tag)) {
                            addConstraint(textSearchConstraints, OR,
                                    getPropertyName("j:tagList", xpath) + "=" + stringToJCRSearchExp(tag));
                        }
                    }
                    if (terms.length > 1) {
                        String tag = taggingService.getTagHandler().execute(textSearch.getTerm());
                        if (!StringUtils.isEmpty(tag)) {
                            addConstraint(textSearchConstraints, OR,
                                    getPropertyName("j:tagList", xpath) + "=" + stringToJCRSearchExp(tag));
                        }
                    }
                }
                if (textSearchConstraints.length() > 0) {
                    addConstraint(constraints, AND, "(" + textSearchConstraints.toString() + ")");
                }
            }
        }
    }

    private void addLanguageConstraints(SearchCriteria params, StringBuilder constraints, boolean xpath) {
        StringBuilder languageSearchConstraints = new StringBuilder(256);
        if (!params.getLanguages().isEmpty()) {
            for (String languageCode : params.getLanguages().getValues()) {
                if (languageCode != null && languageCode.length() != 0) {
                    addConstraint(languageSearchConstraints, OR, getPropertyName("jcr:language", xpath) + "="
                            + stringToJCRSearchExp(languageCode.trim()));
                }
            }
        } else {
            try {
                JCRStoreService jcrService = ServicesRegistry.getInstance().getJCRStoreService();
                JCRSessionWrapper session = jcrService.getSessionFactory().getCurrentUserSession();
                if (session.getLocale() != null) {
                    addConstraint(languageSearchConstraints, OR, getPropertyName("jcr:language", xpath) + "="
                            + stringToJCRSearchExp(session.getLocale().toString()));
                }
            } catch (RepositoryException e) {
            }
        }
        if (languageSearchConstraints.length() > 0) {
            addConstraint(languageSearchConstraints, OR, xpath ? "not(@jcr:language)" : "[jcr:language] is null");
            addConstraint(constraints, AND, "(" + languageSearchConstraints.toString() + ")");
        }
    }

    private String getMimeTypeConstraint(String mimeType, boolean xpath) {
        if (xpath) {
            return mimeType.contains("*")
                    ? "jcr:like(jcr:content/@jcr:mimeType,"
                            + stringToQueryLiteral(StringUtils.replaceChars(mimeType, '*', '%')) + ")"
                    : "jcr:content/@jcr:mimeType=" + stringToQueryLiteral(mimeType);
        } else {
            return "n.[jcr:mimetype]=" + stringToQueryLiteral(mimeType);
        }

    }

    private String getDateLiteral(Calendar date, boolean xpath) {
        return xpath ? ("xs:dateTime('" + ISO8601.format(date) + "')") : ("'" + ISO8601.format(date) + "'");
    }

    private String getPropertyName(String paramName, boolean xpath) {
        return xpath ? ("@" + paramName) : ("n.[" + paramName + "]");
    }

    private String getContainsExpr(String paramName, String expr, boolean xpath) {
        if (xpath) {
            return "jcr:contains(" + getPropertyName(paramName, xpath) + "," + expr + ")";
        } else {
            return "contains(" + getPropertyName(paramName, xpath) + "," + expr + ")";
        }
    }

    private String getSearchExpressionForMatchType(String term, MatchType matchType, boolean applyFilter) {
        return getSearchExpressionForMatchType(term, matchType, applyFilter, null);
    }

    private String getSearchExpressionForMatchType(String term, MatchType matchType, boolean applyFilter,
            String postfix) {
        // as Lucene does not analyze wildcard terms, check whether integrator requested Jahia
        // to apply this accent filter 
        if (applyFilter && StringUtils.containsAny(term, "?*")) {
            term = removeAccents(term);
        }

        if (Term.MatchType.AS_IS != matchType) {
            term = QueryParser.escape(NOT_PATTERN
                    .matcher(OR_PATTERN.matcher(AND_PATTERN.matcher(term).replaceAll(" and ")).replaceAll(" or "))
                    .replaceAll(" not "));
        }

        if (MatchType.ANY_WORD == matchType) {
            term = StringUtils.replace(cleanMultipleWhiteSpaces(term), " ", " OR ");

        } else if (MatchType.EXACT_PHRASE == matchType) {
            term = "\"" + term.trim() + "\"";
        } else if (MatchType.WITHOUT_WORDS == matchType) {
            // because of the underlying Lucene limitations a star '*' (means
            // 'match all') is added to the query string
            term = "* -" + StringUtils.replace(cleanMultipleWhiteSpaces(term), " ", " -");
        }

        return stringToJCRSearchExp(postfix != null ? term + postfix : term);
    }

    /**
     * To replace accented characters in a String by unaccented equivalents.
     */
    private String removeAccents(String term) {
        int length = term.length();
        char[] input = new char[length];
        term.getChars(0, length, input, 0);

        char[] output = new char[length];
        // Worst-case length required:
        final int maxSizeNeeded = 2 * length;

        int size = output.length;
        while (size < maxSizeNeeded)
            size *= 2;

        if (size != output.length)
            output = new char[size];

        int outputPos = 0;

        int pos = 0;

        for (int i = 0; i < length; i++, pos++) {
            final char c = input[pos];

            // Quick test: if it's not in range then just keep
            // current character
            if (c < '\u00c0' || c > '\uFB06')
                output[outputPos++] = c;
            else {
                switch (c) {
                case '\u00C0': // 
                case '\u00C1': // 
                case '\u00C2': // 
                case '\u00C3': // 
                case '\u00C4': // 
                case '\u00C5': // 
                    output[outputPos++] = 'A';
                    break;
                case '\u00C6': // 
                    output[outputPos++] = 'A';
                    output[outputPos++] = 'E';
                    break;
                case '\u00C7': // 
                    output[outputPos++] = 'C';
                    break;
                case '\u00C8': // 
                case '\u00C9': // 
                case '\u00CA': // 
                case '\u00CB': // 
                    output[outputPos++] = 'E';
                    break;
                case '\u00CC': // 
                case '\u00CD': // 
                case '\u00CE': // 
                case '\u00CF': // 
                    output[outputPos++] = 'I';
                    break;
                case '\u0132': // 
                    output[outputPos++] = 'I';
                    output[outputPos++] = 'J';
                    break;
                case '\u00D0': // 
                    output[outputPos++] = 'D';
                    break;
                case '\u00D1': // 
                    output[outputPos++] = 'N';
                    break;
                case '\u00D2': // 
                case '\u00D3': // 
                case '\u00D4': // ?
                case '\u00D5': // 
                case '\u00D6': // 
                case '\u00D8': // 
                    output[outputPos++] = 'O';
                    break;
                case '\u0152': // 
                    output[outputPos++] = 'O';
                    output[outputPos++] = 'E';
                    break;
                case '\u00DE': // 
                    output[outputPos++] = 'T';
                    output[outputPos++] = 'H';
                    break;
                case '\u00D9': // 
                case '\u00DA': // 
                case '\u00DB': // 
                case '\u00DC': // 
                    output[outputPos++] = 'U';
                    break;
                case '\u00DD': // 
                case '\u0178': // 
                    output[outputPos++] = 'Y';
                    break;
                case '\u00E0': // 
                case '\u00E1': // 
                case '\u00E2': // 
                case '\u00E3': // 
                case '\u00E4': // 
                case '\u00E5': // 
                    output[outputPos++] = 'a';
                    break;
                case '\u00E6': // 
                    output[outputPos++] = 'a';
                    output[outputPos++] = 'e';
                    break;
                case '\u00E7': // 
                    output[outputPos++] = 'c';
                    break;
                case '\u00E8': // 
                case '\u00E9': // 
                case '\u00EA': // 
                case '\u00EB': // 
                    output[outputPos++] = 'e';
                    break;
                case '\u00EC': // 
                case '\u00ED': // 
                case '\u00EE': // 
                case '\u00EF': // 
                    output[outputPos++] = 'i';
                    break;
                case '\u0133': // 
                    output[outputPos++] = 'i';
                    output[outputPos++] = 'j';
                    break;
                case '\u00F0': // 
                    output[outputPos++] = 'd';
                    break;
                case '\u00F1': // 
                    output[outputPos++] = 'n';
                    break;
                case '\u00F2': // 
                case '\u00F3': // 
                case '\u00F4': // 
                case '\u00F5': // 
                case '\u00F6': // 
                case '\u00F8': // 
                    output[outputPos++] = 'o';
                    break;
                case '\u0153': // 
                    output[outputPos++] = 'o';
                    output[outputPos++] = 'e';
                    break;
                case '\u00DF': // 
                    output[outputPos++] = 's';
                    output[outputPos++] = 's';
                    break;
                case '\u00FE': // 
                    output[outputPos++] = 't';
                    output[outputPos++] = 'h';
                    break;
                case '\u00F9': // 
                case '\u00FA': // 
                case '\u00FB': // 
                case '\u00FC': // 
                    output[outputPos++] = 'u';
                    break;
                case '\u00FD': // 
                case '\u00FF': // 
                    output[outputPos++] = 'y';
                    break;
                case '\uFB00': // 
                    output[outputPos++] = 'f';
                    output[outputPos++] = 'f';
                    break;
                case '\uFB01': // 
                    output[outputPos++] = 'f';
                    output[outputPos++] = 'i';
                    break;
                case '\uFB02': // 
                    output[outputPos++] = 'f';
                    output[outputPos++] = 'l';
                    break;
                // following 2 are commented as they can break the maxSizeNeeded (and doing *3 could be expensive)
                //          case '\uFB03': // 
                //              output[outputPos++] = 'f';
                //              output[outputPos++] = 'f';
                //              output[outputPos++] = 'i';
                //              break;
                //          case '\uFB04': // 
                //              output[outputPos++] = 'f';
                //              output[outputPos++] = 'f';
                //              output[outputPos++] = 'l';
                //              break;
                case '\uFB05': // 
                    output[outputPos++] = 'f';
                    output[outputPos++] = 't';
                    break;
                case '\uFB06': // 
                    output[outputPos++] = 's';
                    output[outputPos++] = 't';
                    break;
                default:
                    output[outputPos++] = c;
                    break;
                }
            }
        }
        return (new String(output)).trim();
    }

    private String cleanMultipleWhiteSpaces(String term) {
        return MULTIPLE_SPACES_PATTERN.matcher(term).replaceAll(" ");
    }

    /* (non-Javadoc)
    * @see org.jahia.services.search.SearchProvider#suggest(java.lang.String, java.lang.String, java.util.Locale)
    */
    public Suggestion suggest(String originalQuery, RenderContext context, int maxTerms) {
        return suggest(originalQuery, new String[] { context.getSite().getSiteKey() }, context, maxTerms);
    }

    public Suggestion suggest(SearchCriteria originalQuery, RenderContext context, int maxTermsToSuggest) {
        return suggest(originalQuery.getTerms().get(0).getTerm(), originalQuery.getSites().getValues(), context,
                maxTermsToSuggest);
    }

    public Suggestion suggest(String originalQuery, String[] sites, RenderContext context, int maxTermsToSuggest) {
        if (StringUtils.isBlank(originalQuery)) {
            return null;
        }

        if (sites.length == 1 && sites[0].equals("-all-")) {
            List<String> sitesNames = JahiaSitesService.getInstance().getSitesNames();
            sites = sitesNames.toArray(new String[sitesNames.size()]);
        }

        Locale locale = context.getMainResourceLocale();

        Suggestion suggestion = null;
        JCRSessionWrapper session;
        try {
            session = ServicesRegistry.getInstance().getJCRStoreService().getSessionFactory()
                    .getCurrentUserSession(context.getWorkspace(), locale);
            QueryManager qm = session.getWorkspace().getQueryManager();
            StringBuilder xpath = new StringBuilder(64);
            xpath.append("/jcr:root[rep:spellcheck(")
                    .append(stringToJCRSearchExp(originalQuery + CompositeSpellChecker.SEPARATOR_IN_SUGGESTION
                            + CompositeSpellChecker.MAX_TERMS_PARAM + "=" + maxTermsToSuggest
                            + CompositeSpellChecker.SEPARATOR_IN_SUGGESTION + CompositeSpellChecker.SITES_PARAM
                            + "=" + StringUtils.join(sites, "*")))
                    .append(")");
            if (locale != null) {
                xpath.append(" or @jcr:language='").append(locale).append("'");
            }
            xpath.append("]");
            xpath.append("/(rep:spellcheck())");

            Query query = qm.createQuery(xpath.toString(), Query.XPATH);
            RowIterator rows = query.execute().getRows();
            if (rows.hasNext()) {
                Row r = rows.nextRow();
                Value v = r.getValue("rep:spellcheck()");
                if (v != null) {
                    String[] suggestions = StringUtils.splitByWholeSeparator(v.getString(),
                            CompositeSpellChecker.SEPARATOR_IN_SUGGESTION);
                    suggestion = new Suggestion(originalQuery, suggestions[0], Arrays.asList(suggestions));
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Making spell check suggestion for '" + originalQuery + "' site '"
                            + Arrays.asList(sites) + "' and locale '" + locale + "' using XPath query ["
                            + xpath.toString() + "]. Result suggestion: " + suggestion);
                }
            }
        } catch (RepositoryException e) {
            logger.error(e.getMessage(), e);
        }

        return suggestion;
    }

    /**
     * Creates the {@link Query} instance by converting the provided
     * {@link SearchCriteria} bean into XPath query.
     *
     * @param criteria the search criteria to use for the query
     * @param session  current JCR session
     * @return the {@link Query} instance created by converting the provided
     *         {@link SearchCriteria} bean into XPath query or <code>null</code>
     *         if the query cannot be created
     * @throws InvalidQueryException
     * @throws RepositoryException
     */
    public Query buildQuery(SearchCriteria criteria, JCRSessionWrapper session)
            throws InvalidQueryException, RepositoryException {
        Query query = null;
        String xpathQuery = buildXpathQuery(criteria, session);
        String sql = buildSQLQuery(criteria, session);
        if (!StringUtils.isEmpty(xpathQuery)) {
            QueryManagerWrapper qm = session.getWorkspace().getQueryManager();
            query = qm.createDualQuery(xpathQuery, Query.XPATH, sql);
        }

        return query;
    }

    public TaggingService getTaggingService() {
        return taggingService;
    }

    public void setTaggingService(TaggingService taggingService) {
        this.taggingService = taggingService;
    }

    public void setTypesToHideFromSearchResults(Set<String> typesToHideFromSearchResults) {
        this.typesToHideFromSearchResults = typesToHideFromSearchResults;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}