/*
 * Copyright (C) 2003-2008 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero 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/>.
 */
package org.exoplatform.services.wcm.search;

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.jcr.*;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.query.*;

import org.apache.commons.lang.StringUtils;
import org.exoplatform.commons.api.search.data.SearchResult;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.portal.config.UserACL;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cms.documents.TrashService;
import org.exoplatform.services.cms.impl.Utils;
import org.exoplatform.services.cms.templates.TemplateService;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.security.IdentityConstants;
import org.exoplatform.services.wcm.core.NodeLocation;
import org.exoplatform.services.wcm.core.NodetypeConstant;
import org.exoplatform.services.wcm.core.WCMConfigurationService;
import org.exoplatform.services.wcm.portal.LivePortalManagerService;
import org.exoplatform.services.wcm.publication.WCMComposer;
import org.exoplatform.services.wcm.search.QueryCriteria.DATE_RANGE_SELECTED;
import org.exoplatform.services.wcm.search.QueryCriteria.DatetimeRange;
import org.exoplatform.services.wcm.search.QueryCriteria.QueryProperty;
import org.exoplatform.services.wcm.search.base.*;
import org.exoplatform.services.wcm.search.connector.BaseSearchServiceConnector;
import org.exoplatform.services.wcm.utils.SQLQueryBuilder;
import org.exoplatform.services.wcm.utils.WCMCoreUtils;
import org.exoplatform.services.wcm.utils.AbstractQueryBuilder.COMPARISON_TYPE;
import org.exoplatform.services.wcm.utils.AbstractQueryBuilder.LOGICAL;
import org.exoplatform.services.wcm.utils.AbstractQueryBuilder.ORDERBY;
import org.exoplatform.services.wcm.utils.AbstractQueryBuilder.PATH_TYPE;
import org.exoplatform.services.wcm.utils.AbstractQueryBuilder.QueryTermHelper;
/**
 * The SiteSearchService component is used in the Search portlet that allows users
 * to find all information matching with your given keyword.
 */
public class SiteSearchServiceImpl implements SiteSearchService {

  private static final String  SITE_SEARCH_FOUND_CACHE = "ecms.SiteSearchService.found";

  private static final String  SITE_SEARCH_DROP_CACHE = "ecms.SiteSearchService.drop";

  /** Allow administrators to enable/disable the fuzzy search mechanism. */
  private static final String IS_ENABLED_FUZZY_SEARCH = "isEnabledFuzzySearch";

  /** Allow the approximate level between the input keyword and the found key results.
   * In case of the invalid configuration, the default value is set to 0.8. */
  private static final String FUZZY_SEARCH_INDEX = "fuzzySearchIndex";
  
  /** The live portal manager service. */
  private LivePortalManagerService livePortalManagerService;

  /** The ecm template service. */
  private TemplateService templateService;

  /** The wcm configuration service. */
  private WCMConfigurationService configurationService;

  /** The jcr repository service. */
  private RepositoryService repositoryService;

  /** The exclude node types. */
  private CopyOnWriteArraySet<String> excludeNodeTypes = new CopyOnWriteArraySet<>();

  /** The include node types. */
  private CopyOnWriteArraySet<String> includeNodeTypes = new CopyOnWriteArraySet<>();

  /** The exclude mime types. */
  private CopyOnWriteArraySet<String> excludeMimeTypes = new CopyOnWriteArraySet<>();

  /** The include mime types. */
  private CopyOnWriteArraySet<String> includeMimeTypes = new CopyOnWriteArraySet<>();

  private boolean isEnabledFuzzySearch = true;
  
  private double fuzzySearchIndex = 0.8;
  
  /** The log. */
  private static final Log LOG = ExoLogger.getLogger(SiteSearchServiceImpl.class.getName());
  
  private ExoCache<String, Map<?, Integer>> foundNodeCache;
  private ExoCache<String, Map<Integer, Integer>> dropNodeCache;

  /**
   * Instantiates a new site search service impl.
   *
   * @param portalManagerService the portal manager service
   * @param templateService the template service
   * @param configurationService the configuration service
   * @param repositoryService the repository service
   * @param initParams the init params
   *
   * @throws Exception the exception
   */
  public SiteSearchServiceImpl(LivePortalManagerService portalManagerService,
                               TemplateService templateService,
                               WCMConfigurationService configurationService,
                               RepositoryService repositoryService,
                               CacheService caService,
                               InitParams initParams) throws Exception {
    this.livePortalManagerService = portalManagerService;
    this.templateService = templateService;
    this.repositoryService = repositoryService;
    this.configurationService = configurationService;
    this.foundNodeCache = caService.getCacheInstance(SITE_SEARCH_FOUND_CACHE);
    this.dropNodeCache = caService.getCacheInstance(SITE_SEARCH_DROP_CACHE);
    if (initParams != null) {
      ValueParam isEnabledFuzzySearchValue = initParams.getValueParam(IS_ENABLED_FUZZY_SEARCH);
      if (isEnabledFuzzySearchValue != null)
        isEnabledFuzzySearch = Boolean.parseBoolean(isEnabledFuzzySearchValue.getValue());
      ValueParam enabledFuzzySearchValue = initParams.getValueParam(FUZZY_SEARCH_INDEX);
      if (enabledFuzzySearchValue != null) {
        try {
          fuzzySearchIndex = Double.parseDouble(enabledFuzzySearchValue.getValue());
        } catch (NumberFormatException e) {
//          log.warn("The current fuzzySearchIndex value is not a number, default value 0.8 will be used");
          fuzzySearchIndex = 0.8;
        }
      }
      if (fuzzySearchIndex < 0 || fuzzySearchIndex >= 1) {
//        log.warn("The current fuzzySearchIndex value is out of range from 0 to 1, default value 0.8 will be used");
        fuzzySearchIndex = 0.8;
      }
    }

  }

  /*
   * (non-Javadoc)
   * @seeorg.exoplatform.services.wcm.search.SiteSearchService#
   * addExcludeIncludeDataTypePlugin
   * (org.exoplatform.services.wcm.search.ExcludeIncludeDataTypePlugin)
   */
  @Override
  public void addExcludeIncludeDataTypePlugin(ExcludeIncludeDataTypePlugin plugin) {
    excludeNodeTypes.addAll(plugin.getExcludeNodeTypes());
    excludeMimeTypes.addAll(plugin.getExcludeMimeTypes());
    includeMimeTypes.addAll(plugin.getIncludeMimeTypes());
    includeNodeTypes.addAll(plugin.getIncludeNodeTypes());
  }

  /*
   * (non-Javadoc)
   * @see
   * org.exoplatform.services.wcm.search.SiteSearchService#searchSiteContents
   * (org.exoplatform.services.wcm.search.QueryCriteria,
   * org.exoplatform.services.jcr.ext.common.SessionProvider, int)
   */
  @Override
  public AbstractPageList<ResultNode> searchSiteContents(SessionProvider sessionProvider,
                                                    QueryCriteria queryCriteria,
                                                    Locale locale,
                                                    int pageSize,
                                                    boolean isSearchContent) throws Exception {
    ManageableRepository currentRepository = repositoryService.getCurrentRepository();
    NodeLocation location = configurationService.getLivePortalsLocation();
    Session session = sessionProvider.getSession(location.getWorkspace(),currentRepository);
    if (queryCriteria.isSearchWebpage()) {
      session = sessionProvider.getSession("portal-system", WCMCoreUtils.getRepository());
    }
    QueryManager queryManager = session.getWorkspace().getQueryManager();
    long startTime = System.currentTimeMillis();
    Query query = createQuery(queryCriteria, queryManager);
    String suggestion = getSpellSuggestion(queryCriteria.getKeyword(),currentRepository);
    AbstractPageList<ResultNode> pageList = null;
    if (LOG.isDebugEnabled()) {
      LOG.debug("execute query: " + query.getStatement().toLowerCase());
    }
    pageList = PageListFactory.createPageList(query.getStatement(),
                                              locale,
                                              session.getWorkspace().getName(),
                                              query.getLanguage(),
                                              IdentityConstants.SYSTEM.equals(session.getUserID()),
                                              new NodeFilter(isSearchContent, queryCriteria),
                                              new DataCreator(),
                                              pageSize,
                                              (int)queryCriteria.getLimit(), queryCriteria);
    
    long queryTime = System.currentTimeMillis() - startTime;
    pageList.setQueryTime(queryTime);
    pageList.setSpellSuggestion(suggestion);
    return pageList;
  }
  
  /**
   * 
   */
  @Override
  public AbstractPageList<ResultNode> searchPageContents(SessionProvider sessionProvider,
                                                      QueryCriteria queryCriteria,
                                                      Locale locale,
                                                      int pageSize,
                                                      boolean isSearchContent) throws Exception {
    ManageableRepository currentRepository = repositoryService.getCurrentRepository();
    Session session = sessionProvider.getSession("portal-system", currentRepository);
    QueryManager queryManager = session.getWorkspace().getQueryManager();
    long startTime = System.currentTimeMillis();
    Query query = createSearchPageQuery(queryCriteria, queryManager);
    if (query == null) {
      return new ArrayNodePageList<>(pageSize);
    }
    String suggestion = getSpellSuggestion(queryCriteria.getKeyword(), currentRepository);
    if (LOG.isDebugEnabled()) {
      LOG.debug("execute query: " + query.getStatement().toLowerCase());
    }
    AbstractPageList<ResultNode> pageList = PageListFactory.createPageList(query.getStatement(),
                                                                           locale,
                                                                           session.getWorkspace()
                                                                                  .getName(),
                                                                           query.getLanguage(),
                                                                           true,
                                                                           new PageNodeFilter(),
                                                                           new PageDataCreator(),
                                                                           pageSize,
                                                                           0);

    long queryTime = System.currentTimeMillis() - startTime;
    pageList.setQueryTime(queryTime);
    pageList.setSpellSuggestion(suggestion);
    return pageList;
  }

  @Override
  public Map<?, Integer> getFoundNodes(String userId, String queryStatement) {
    String key = new StringBuilder('(').append(userId).append(';').append(queryStatement).append(')').toString();
    Map<?, Integer> ret = foundNodeCache.get(key);
    if (ret == null) {
      ret = new HashMap<Integer, Integer>();
      foundNodeCache.put(key, ret);
    }
    return ret;
  }

  @Override
  public Map<Integer, Integer> getDropNodes(String userId, String queryStatement) {
    String key = new StringBuilder('(').append(userId).append(';').append(queryStatement).append(')').toString();
    Map<Integer, Integer> ret = dropNodeCache.get(key);
    if (ret == null) {
      ret = new HashMap<>();
      dropNodeCache.put(key, ret);
    }
    return ret;
  }

  @Override
  public void clearCache(String userId, String queryStatement) {
    String key = new StringBuilder('(').append(userId).append(';').append(queryStatement).append(')').toString();
    foundNodeCache.remove(key);
    dropNodeCache.remove(key);
  }
  
  private Query createSearchPageQuery(QueryCriteria queryCriteria, QueryManager queryManager) throws Exception {
    SQLQueryBuilder queryBuilder = new SQLQueryBuilder();
    List<String> mopPages = this.searchPageByTitle(queryCriteria.getSiteName(),
                                                   queryCriteria.getKeyword());
    if (mopPages.size() == 0) {
      return null;
    }
    List<QueryProperty> queryProps = new ArrayList<>();
    for (String page : mopPages) {
      QueryProperty prop = queryCriteria.new QueryProperty();
      prop.setName("mop:page");
      prop.setValue(page);
      prop.setComparisonType(COMPARISON_TYPE.EQUAL);
      queryProps.add(prop);
    }
    QueryProperty prop = queryCriteria.new QueryProperty();
    prop.setName("exo:name");
    prop.setValue("mop:" + queryCriteria.getKeyword().toLowerCase());
    queryProps.add(prop);    
    queryCriteria.setQueryMetadatas(queryProps.toArray(new QueryProperty[queryProps.size()]));
    mapQueryTypes(queryCriteria, queryBuilder);
    if (queryCriteria.isFulltextSearch()) {
      mapQueryPath(queryCriteria, queryBuilder);
      mapFulltextQueryTearm(queryCriteria, queryBuilder, LOGICAL.OR);
    } else {
      searchByNodeName(queryCriteria, queryBuilder);
    }
    mapCategoriesCondition(queryCriteria,queryBuilder);
    mapDatetimeRangeSelected(queryCriteria,queryBuilder);
    mapMetadataProperties(queryCriteria,queryBuilder, LOGICAL.OR);
    orderBy(queryCriteria, queryBuilder);
    String queryStatement = queryBuilder.createQueryStatement();
    Query query = queryManager.createQuery(queryStatement, Query.SQL);
    return query;
  }

  /**
   * @return return the JCR path of the mop:page nodes that have gtn:name
   *         (page's title) containing the given specified <code>keyword</code>
   * @throws Exception
   */
  private List<String> searchPageByTitle(String siteName, String keyword) throws Exception {
    SessionProvider sessionProvider = WCMCoreUtils.getSystemSessionProvider();
    ManageableRepository currentRepository = repositoryService.getCurrentRepository();
    Session session = sessionProvider.getSession("portal-system", currentRepository);
    QueryManager queryManager = session.getWorkspace().getQueryManager();
    QueryCriteria queryCriteria = new QueryCriteria();
    queryCriteria.setSiteName(siteName);
    queryCriteria.setKeyword(keyword);
    queryCriteria.setSearchWebpage(true);
    Query query = createSearchPageByTitleQuery(queryCriteria, queryManager);
    if (LOG.isDebugEnabled()) {
      LOG.debug("execute query: " + query.getStatement().toLowerCase());
    }
    List<String> pageList = PageListFactory.createPageList(query.getStatement(),
                                                           session.getWorkspace().getName(),
                                                           query.getLanguage(),
                                                           true,
                                                           new PageTitleDataCreator());
    return pageList;
  }
  
  /**
   * 
   * @param queryCriteria
   * @param queryManager
   * @return
   * @throws Exception
   */
  private Query createSearchPageByTitleQuery(QueryCriteria queryCriteria, QueryManager queryManager) throws Exception {
    SQLQueryBuilder queryBuilder = new SQLQueryBuilder();

    // select *
    queryBuilder.selectTypes(null);

    // from mop:page node type
    queryBuilder.fromNodeTypes(new String[] { "mop:page" });

    mapQueryPath(queryCriteria, queryBuilder);

    queryCriteria.setFulltextSearchProperty(new String[] {"gtn:name"});

    mapFulltextQueryTearm(queryCriteria, queryBuilder, LOGICAL.OR);

    String queryStatement = queryBuilder.createQueryStatement();
    Query query = queryManager.createQuery(queryStatement, Query.SQL);

    return query;
  }

  /**
   * Gets the spell suggestion.
   *
   * @param checkingWord the checking word
   * @param manageableRepository the manageable repository
   *
   * @return the spell suggestion
   *
   * @throws Exception the exception
   */
  private String getSpellSuggestion(String checkingWord, ManageableRepository manageableRepository) throws Exception {
    //Retrieve spell suggestion in special way to avoid access denied exception
    String suggestion = null;
    Session session = null;
    try{
      session = manageableRepository.getSystemSession(manageableRepository.getConfiguration().getDefaultWorkspaceName());
      QueryManager queryManager = session.getWorkspace().getQueryManager();
      Query query = queryManager.createQuery("SELECT rep:spellcheck() FROM nt:base WHERE jcr:path like '/' AND SPELLCHECK('"
                                                 + checkingWord + "')",
                                             Query.SQL);
      RowIterator rows = query.execute().getRows();
      Value value = rows.nextRow().getValue("rep:spellcheck()");
      if (value != null) {
        suggestion = value.getString();
      }
    } catch (Exception e) {
      if (LOG.isWarnEnabled()) {
        LOG.warn(e.getMessage());
      }
    } finally {
      if (session != null)
        session.logout();
    }
    return suggestion;
  }

  /**
   * Search site content.
   *
   * @param queryCriteria the query criteria
   * @param queryManager the query manager
   *
   * @return the query result
   *
   * @throws Exception the exception
   */
  private Query createQuery(QueryCriteria queryCriteria, QueryManager queryManager) throws Exception {
    SQLQueryBuilder queryBuilder = new SQLQueryBuilder();
    mapQueryTypes(queryCriteria, queryBuilder);
    if (queryCriteria.isFulltextSearch()) {
      mapQueryPath(queryCriteria, queryBuilder);
      mapFulltextQueryTearm(queryCriteria, queryBuilder, LOGICAL.OR);
    } else {
      searchByNodeName(queryCriteria, queryBuilder);
    }
    mapCategoriesCondition(queryCriteria,queryBuilder);
    mapDatetimeRangeSelected(queryCriteria, queryBuilder);
    mapMetadataProperties(queryCriteria, queryBuilder, LOGICAL.AND);
    orderBy(queryCriteria, queryBuilder);
    String queryStatement = queryBuilder.createQueryStatement();
    Query query = queryManager.createQuery(queryStatement, Query.SQL);
//    System.out.println(queryStatement);
    return query;
  }

  /**
   * Map query path.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   *
   * @throws Exception the exception
   */
  private void mapQueryPath(final QueryCriteria queryCriteria, final SQLQueryBuilder queryBuilder) throws Exception {
    queryBuilder.setQueryPath(getPath(queryCriteria), PATH_TYPE.DECENDANTS);
  }

  /**
   * Gets the site path.
   *
   * @param queryCriteria the query criteria
   *
   * @return the site path
   *
   * @throws Exception the exception
   */
  private String getPath(final QueryCriteria queryCriteria) throws Exception {
    String siteName = queryCriteria.getSiteName();
    //search page path
    if (queryCriteria.isSearchWebpage()) {
      if ("all".equals(siteName) || siteName == null || siteName.trim().length() == 0) {
        return PATH_PORTAL_SITES;
      }
      return PATH_PORTAL_SITES.concat("/mop:").concat(siteName);
    }
    //search document path
    if (queryCriteria.getSearchPath() != null) {
      return queryCriteria.getSearchPath();
    }
    String sitePath = null;
    if (siteName != null) {
      sitePath = livePortalManagerService.getPortalPathByName(siteName);
    } else {
      sitePath = configurationService.getLivePortalsLocation().getPath();
    }
    return sitePath;
  }

  /**
   * Map query term.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   */
  private void mapFulltextQueryTearm(final QueryCriteria queryCriteria,
                                     final SQLQueryBuilder queryBuilder, LOGICAL condition) {
    String keyword = queryCriteria.getKeyword();
    if (keyword == null || keyword.length() == 0)
      return;

    keyword = Utils.escapeIllegalCharacterInQuery(keyword);

    QueryTermHelper queryTermHelper = new QueryTermHelper();
    String queryTerm = null;
    if (isEnabledFuzzySearch) {
      if (keyword.contains("*") || keyword.contains("?") || keyword.contains("~") || keyword.contains("\"")) {
        queryTerm = queryTermHelper.contains(keyword).buildTerm();
      } else if(queryCriteria.isFuzzySearch()) {
        queryTerm = queryTermHelper.contains(keyword).allowFuzzySearch(fuzzySearchIndex).buildTerm();
      } else {
        queryTerm = queryTermHelper.contains(keyword).buildTerm();
      }
    } else {
      if(!queryCriteria.isFuzzySearch()) {
        keyword = keyword.replace("~", "\\~");
        keyword = keyword.replace("*", "\\*");
        keyword = keyword.replace("?", "\\?");
      }
      queryTerm = queryTermHelper.contains(keyword).buildTerm();
    }
    String[] props = queryCriteria.getFulltextSearchProperty();
    if (props == null || props.length == 0 || QueryCriteria.ALL_PROPERTY_SCOPE.equals(props[0])) {
      queryBuilder.contains(null, queryTerm, LOGICAL.NULL);
    } else {
      queryBuilder.contains(props[0], queryTerm, LOGICAL.NULL);
      for (int i = 1; i < props.length; i++) {
        queryBuilder.contains(props[i], queryTerm, condition);
      }
    }
  }
  
  /**
   * Search by node name.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   *
   * @throws Exception the exception
   */
  private void searchByNodeName(final QueryCriteria queryCriteria,
                                final SQLQueryBuilder queryBuilder) throws Exception {
    queryBuilder.queryByNodeName(getPath(queryCriteria), queryCriteria.getKeyword());
  }

  /**
   * Map datetime range selected.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   */
  private void mapDatetimeRangeSelected(final QueryCriteria queryCriteria,
                                        final SQLQueryBuilder queryBuilder) {
    DATE_RANGE_SELECTED selectedDateRange = queryCriteria.getDateRangeSelected();
    if (selectedDateRange == null)
      return;
    if (DATE_RANGE_SELECTED.CREATED == selectedDateRange) {
      DatetimeRange createdDateRange = queryCriteria.getCreatedDateRange();
      queryBuilder.betweenDates("exo:dateCreated",
                                createdDateRange.getFromDate(),
                                createdDateRange.getToDate(),
                                LOGICAL.AND);
    } else if (DATE_RANGE_SELECTED.MODIFIDED == selectedDateRange) {
      DatetimeRange modifiedDateRange = queryCriteria.getLastModifiedDateRange();
      queryBuilder.betweenDates("exo:dateModified",
                                modifiedDateRange.getFromDate(),
                                modifiedDateRange.getToDate(),
                                LOGICAL.AND);
    } else if (DATE_RANGE_SELECTED.START_PUBLICATION == selectedDateRange) {
      throw new UnsupportedOperationException();
    } else if (DATE_RANGE_SELECTED.END_PUBLICATION == selectedDateRange) {
      throw new UnsupportedOperationException();
    }
  }

  /**
   * Map categories condition.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   */
  private void mapCategoriesCondition(QueryCriteria queryCriteria, SQLQueryBuilder queryBuilder) {
    String[] categoryUUIDs = queryCriteria.getCategoryUUIDs();
    if (categoryUUIDs == null)
      return;
    queryBuilder.openGroup(LOGICAL.AND);
    queryBuilder.like("exo:category", categoryUUIDs[0], LOGICAL.NULL);
    if (categoryUUIDs.length > 1) {
      for (int i = 1; i < categoryUUIDs.length; i++) {
        queryBuilder.like("exo:category", categoryUUIDs[i], LOGICAL.OR);
      }
    }
    queryBuilder.closeGroup();
  }

  /**
   * Map metadata properties.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   */
  private void mapMetadataProperties(final QueryCriteria queryCriteria, SQLQueryBuilder queryBuilder, LOGICAL condition) {
    QueryProperty[] queryProperty = queryCriteria.getQueryMetadatas();
    if (queryProperty == null || queryProperty.length == 0)
      return;
    queryBuilder.openGroup(condition);
    if (queryProperty[0].getComparisonType() == COMPARISON_TYPE.EQUAL) {
      queryBuilder.equal(queryProperty[0].getName(), queryProperty[0].getValue(), LOGICAL.NULL);
    } else {
      queryBuilder.like(queryProperty[0].getName(), queryProperty[0].getValue(), LOGICAL.NULL);
    }
    if (queryProperty.length > 1) {
      for (int i = 1; i < queryProperty.length; i++) {
        if (queryProperty[i].getComparisonType() == COMPARISON_TYPE.EQUAL) {
          queryBuilder.equal(queryProperty[i].getName(), queryProperty[i].getValue(), LOGICAL.OR);
        } else {
          queryBuilder.like(queryProperty[i].getName(), queryProperty[i].getValue(), LOGICAL.OR);
        }
      }
    }
    queryBuilder.closeGroup();
  }

  /**
   * Map query specific node types.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   * @param nodeTypeManager the node type manager
   *
   * @throws Exception the exception
   */
  private void mapQuerySpecificNodeTypes(final QueryCriteria queryCriteria,
                                         final SQLQueryBuilder queryBuilder,
                                         final NodeTypeManager nodeTypeManager) throws Exception {
    String[] contentTypes = queryCriteria.getContentTypes();
    NodeType fistType = nodeTypeManager.getNodeType(contentTypes[0]);
    queryBuilder.openGroup(LOGICAL.AND);
    if (fistType.isMixin()) {
      queryBuilder.like("jcr:mixinTypes", contentTypes[0], LOGICAL.NULL);
    } else {
      queryBuilder.equal("jcr:primaryType", contentTypes[0], LOGICAL.NULL);
    }
    if(contentTypes.length>1) {
      for (int i=1; i<contentTypes.length; i++) {
        String type = contentTypes[i];
        NodeType nodetype = nodeTypeManager.getNodeType(type);
        if (nodetype.isMixin()) {
          queryBuilder.like("jcr:mixinTypes", type, LOGICAL.OR);
        } else {
          queryBuilder.equal("jcr:primaryType", type, LOGICAL.OR);
        }
      }
    }
    queryBuilder.closeGroup();
    //Remove some specific mimtype
    queryBuilder.openGroup(LOGICAL.AND_NOT);
    queryBuilder.like("jcr:mixinTypes", "exo:cssFile", LOGICAL.NULL);
    queryBuilder.like("jcr:mixinTypes","exo:jsFile",LOGICAL.OR);
    queryBuilder.closeGroup();
  }

  /**
   * Map query types.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   *
   * @throws Exception the exception
   */
  private void mapQueryTypes(final QueryCriteria queryCriteria, final SQLQueryBuilder queryBuilder) throws Exception {
    queryBuilder.selectTypes(null);
    // Searh on nt:base
    queryBuilder.fromNodeTypes(queryCriteria.getNodeTypes());
    ManageableRepository currentRepository = repositoryService.getCurrentRepository();
    NodeTypeManager manager = currentRepository.getNodeTypeManager();
    // Query all documents for specific content types
    String[] contentTypes = queryCriteria.getContentTypes();
    if ((contentTypes != null && contentTypes.length > 0 && queryCriteria.getKeyword() == null)
        || queryCriteria.isSearchWebpage()) {
      mapQuerySpecificNodeTypes(queryCriteria, queryBuilder, manager);
      return;
    }
    List<String> selectedNodeTypes =  
      (contentTypes != null && contentTypes.length > 0) ? Arrays.asList(contentTypes) :
                                                          templateService.getDocumentTemplates();
    queryBuilder.openGroup(LOGICAL.AND);
    if (selectedNodeTypes.contains("nt:file")) {
      queryBuilder.equal("jcr:primaryType", NodetypeConstant.NT_FILE, LOGICAL.NULL);
    } else {
      //searching only document, not file. In this case, search nt:resource with exo:webContentChild mixin
      queryBuilder.openGroup(null);
      queryBuilder.equal(NodetypeConstant.JCR_PRIMARY_TYPE, NodetypeConstant.NT_RESOURCE, LOGICAL.NULL);
      queryBuilder.equal(NodetypeConstant.JCR_MIXIN_TYPES, NodetypeConstant.EXO_WEBCONTENT_CHILD, LOGICAL.AND);
      queryBuilder.closeGroup();
    }
    // query on exo:rss-enable nodetypes for title, summary field
    //queryBuilder.equal("jcr:mixinTypes", "exo:rss-enable", LOGICAL.OR);
    for (String type : selectedNodeTypes) {
      NodeType nodetype = manager.getNodeType(type);
      if (nodetype.isMixin()) {
        if (selectedNodeTypes.contains("nt:file") || 
            !NodetypeConstant.EXO_CSS_FILE.equals(type) &&
            !NodetypeConstant.EXO_JS_FILE.equals(type) &&
            !NodetypeConstant.EXO_HTML_FILE.equals(type)) {
          queryBuilder.like("jcr:mixinTypes", type, LOGICAL.OR);
        } else {
          //searching only document, not file. In this case, search nt:resource with exo:webContentChild mixin
          queryBuilder.openGroup(LOGICAL.OR);
          queryBuilder.equal(NodetypeConstant.JCR_MIXIN_TYPES, type, LOGICAL.NULL);
          queryBuilder.equal(NodetypeConstant.JCR_MIXIN_TYPES, NodetypeConstant.EXO_WEBCONTENT_CHILD, LOGICAL.AND);
          queryBuilder.closeGroup();
        }
      } else {
        if(!type.equals(NodetypeConstant.NT_FILE)) {
          queryBuilder.equal("jcr:primaryType", type, LOGICAL.OR);
        }
      }
    }
    queryBuilder.closeGroup();
    //unwanted document types: exo:cssFile, exo:jsFile
    if(excludeMimeTypes.size()<1) return;
    queryBuilder.openGroup(LOGICAL.AND_NOT);
    String[] mimetypes = excludeMimeTypes.toArray(new String[]{});
    queryBuilder.equal("jcr:mimeType",mimetypes[0],LOGICAL.NULL);
    for(int i=1; i<mimetypes.length; i++) {
      queryBuilder.equal("jcr:mimeType",mimetypes[i],LOGICAL.OR);
    }
    queryBuilder.closeGroup();
    //Unwanted document by mixin nodetypes
    queryBuilder.openGroup(LOGICAL.AND_NOT);
    queryBuilder.like("jcr:mixinTypes", "exo:cssFile", LOGICAL.NULL);
    queryBuilder.like("jcr:mixinTypes","exo:jsFile",LOGICAL.OR);
    queryBuilder.closeGroup();

    queryBuilder.openGroup(LOGICAL.AND_NOT);
    String[] _excludeNodeTypes = excludeNodeTypes.toArray(new String[]{});
    for(int i=0; i < _excludeNodeTypes.length; i++) {
      if(i==0) {
        queryBuilder.equal("jcr:mixinTypes", _excludeNodeTypes[i], LOGICAL.NULL);
      } else {
        queryBuilder.equal("jcr:mixinTypes", _excludeNodeTypes[i], LOGICAL.OR);
      }
    }
    queryBuilder.closeGroup();

  }

  /**
   * Order by.
   *
   * @param queryCriteria the query criteria
   * @param queryBuilder the query builder
   */
  private void orderBy(final QueryCriteria criteria, final SQLQueryBuilder queryBuilder) {
    String sortBy = "jcr:score";
    String orderBy = "desc";
    //sort by
    if (BaseSearchServiceConnector.sortByTitle.equals(criteria.getSortBy())) {
      sortBy = NodetypeConstant.EXO_TITLE;
    } else if (BaseSearchServiceConnector.sortByDate.equals(criteria.getSortBy())) {
      sortBy = NodetypeConstant.EXO_LAST_MODIFIED_DATE;
    }
    if (StringUtils.isNotBlank(criteria.getOrderBy())) {
      orderBy = criteria.getOrderBy();
    }
    queryBuilder.orderBy(sortBy, "desc".equals(orderBy) ? ORDERBY.DESC : ORDERBY.ASC);
  }
  
  public static class NodeFilter implements NodeSearchFilter {

    private boolean isSearchContent;
    private QueryCriteria queryCriteria;
    private TrashService trashService = WCMCoreUtils.getService(TrashService.class);

    public NodeFilter(boolean isSearchContent, QueryCriteria queryCriteria) {
      this.isSearchContent = isSearchContent;
      this.queryCriteria = queryCriteria;
    }
    
    @Override
    public Node filterNodeToDisplay(Node node) {
      try {
        if (node == null || node.getPath().contains("/jcr:system/")) return null;
        if(trashService.isInTrash(node)) return null;
        Node displayNode = getNodeToCheckState(node);
        if(displayNode == null) return null;
        if (isSearchContent) return displayNode;
        NodeLocation nodeLocation = NodeLocation.getNodeLocationByNode(displayNode);
        WCMComposer wcmComposer = WCMCoreUtils.getService(WCMComposer.class);
        HashMap<String, String> filters = new HashMap<>();
        filters.put(WCMComposer.FILTER_MODE, queryCriteria.isLiveMode() ? WCMComposer.MODE_LIVE
                                                                       : WCMComposer.MODE_EDIT);
        return wcmComposer.getContent(nodeLocation.getWorkspace(),
                                                                  nodeLocation.getPath(),
                                                                  filters,
                                                                  WCMCoreUtils.getSystemSessionProvider());
      } catch (Exception e) {
        return null;
      }
    }
    
    protected Node getNodeToCheckState(Node node)throws Exception{
      Node displayNode = node;
        if (displayNode.isNodeType("nt:resource")) {
          displayNode = node.getParent();
        }
        //return exo:webContent when exo:htmlFile found
        if (displayNode.isNodeType("exo:htmlFile")) {
          Node parent = displayNode.getParent();
          if (parent.isNodeType("exo:webContent")) {
            displayNode = parent;
          }
        }
        String[] contentTypes = queryCriteria.getContentTypes();
        if(contentTypes != null && contentTypes.length > 0) {
          for (String contentType : contentTypes) {
            if (displayNode.isNodeType(contentType)) {
              return displayNode;
            }
          }
        } else {
          return displayNode;
        }
        return null;
    }
    
  }
  
  /**
   * 
   * @author ha_dangviet
   *
   */
  public static class PageNodeFilter implements NodeSearchFilter {

    @Override
    public Node filterNodeToDisplay(Node node) {
      try {
        if (!node.isNodeType("mop:navigation") && !node.isNodeType("mop:pagelink")
            && !node.isNodeType("gtn:language")) {
          return null;
        } else {
          return node;
        }
      } catch (RepositoryException e) {
        return null;
      }
    }

  }
  
  public static class DataCreator implements SearchDataCreator<ResultNode> {

    @Override
    public ResultNode createData(Node node, Row row, SearchResult searchResult) {
      try {
        if(row == null && searchResult != null) {
          return new ResultNode(node, searchResult.getRelevancy(), searchResult.getExcerpt());
        } else {
          return new ResultNode(node, row);
        }
      } catch (Exception e) {
        return null;
      }
    }
    
  }
  
  public static class PageDataCreator implements SearchDataCreator<ResultNode> {

    private static HashSet<String> userNavigationUriList = new HashSet<>();
    
    @Override
    public ResultNode createData(Node node, Row row, SearchResult searchResult) {
      try {
        if (node.isNodeType("mop:pagelink")) {
          node = node.getParent();
        }
        
        if (node.isNodeType("gtn:language")) {
          node = node.getParent().getParent();
        }
        String userNaviUri = "/" + WCMCoreUtils.getPortalName() + "/" + PageDataCreator.getUserNavigationURI(node).toString();
        if (userNavigationUriList.contains(userNaviUri)) {
          return null;
        }
        userNavigationUriList.add(userNaviUri);
        return new ResultNode(node, row, userNaviUri);
      } catch (Exception e) {
        return null;
      }
    }
    
    /**
     * Get user navigation URI from equivalent mop node in portal-system workspace
     * @param node the mop node in portal-system workspace
     * @return 
     * @throws RepositoryException
     */
    public static StringBuilder getUserNavigationURI(Node node) throws RepositoryException {
      if (!node.isNodeType("mop:portalsites")) {
        StringBuilder builder = getUserNavigationURI(node.getParent());
        String name = node.getName();
        if ("mop:children".equals(name) || "mop:default".equals(name)
            || "mop:rootnavigation".equals(name)) {
          return builder.append("");
        } else {
          if (builder.length() > 0) {
            builder.append("/");
          }
          return builder.append(node.getName().replaceFirst("mop:", ""));
        }
      } else {
        return new StringBuilder();
      }

    } 
    
  }  
  
  /**
   * 
   * @author ha_dangviet
   *
   */
  public static class PageTitleDataCreator implements SearchDataCreator<String> {

    @Override
    public String createData(Node node, Row row, SearchResult searchResult) {
      try {
        UserACL userACL = WCMCoreUtils.getService(UserACL.class);
        if (node.hasProperty("gtn:access-permissions")) {
          for (Value v : node.getProperty("gtn:access-permissions").getValues()) {
            if (userACL.hasPermission(v.getString())) {
              return node.getPath();
            }
          }
          return null;
        } else {
          return node.getPath();
        }
      } catch (RepositoryException e) {
        return null;
      }
    }
    
  }
  
}
