001package org.ametys.web.frontoffice.search.fast; 002/* 003 * Copyright 2022 Anyware Services 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018import java.util.ArrayList; 019import java.util.List; 020import java.util.Map; 021 022import org.apache.avalon.framework.parameters.Parameters; 023import org.apache.avalon.framework.service.ServiceException; 024import org.apache.avalon.framework.service.ServiceManager; 025import org.apache.cocoon.ProcessingException; 026import org.apache.cocoon.acting.ServiceableAction; 027import org.apache.cocoon.environment.ObjectModelHelper; 028import org.apache.cocoon.environment.Redirector; 029import org.apache.cocoon.environment.Request; 030import org.apache.cocoon.environment.SourceResolver; 031import org.apache.commons.lang.StringUtils; 032 033import org.ametys.cms.search.Sort; 034import org.ametys.cms.search.Sort.Order; 035import org.ametys.cms.search.query.DocumentTypeQuery; 036import org.ametys.cms.search.query.OrQuery; 037import org.ametys.cms.search.query.Query.Operator; 038import org.ametys.cms.search.query.QueryHelper; 039import org.ametys.cms.search.query.StringQuery; 040import org.ametys.cms.search.solr.SearcherFactory; 041import org.ametys.cms.search.solr.SearcherFactory.Searcher; 042import org.ametys.cms.transformation.xslt.ResolveURIComponent; 043import org.ametys.core.cocoon.JSonReader; 044import org.ametys.plugins.repository.AmetysObjectIterable; 045import org.ametys.web.WebHelper; 046import org.ametys.web.indexing.solr.SolrWebFieldNames; 047import org.ametys.web.repository.page.Page; 048import org.ametys.web.repository.page.SitemapElement; 049 050/** 051 * Get propositions for autocompletion on title field while beginning a search on frontoffice 052 */ 053public class FastSearchOnPageTitleAction extends ServiceableAction 054{ 055 private static final int __MAX_NUMBER = 25; 056 private static final int __DEFAULT_NUMBER = 12; 057 058 private static final String __REQUEST_PARAM_LANG = "lang"; 059 private static final String __REQUEST_PARAM_QUERY = "q"; 060 private static final String __REQUEST_PARAM_SORT = "sort"; 061 062 private SearcherFactory _searcherFactory; 063 064 065 @Override 066 public void service(ServiceManager serviceManager) throws ServiceException 067 { 068 super.service(serviceManager); 069 _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE); 070 } 071 072 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 073 { 074 Request request = ObjectModelHelper.getRequest(objectModel); 075 076 List<Map<String, String>> jsonResults = new ArrayList<>(); 077 request.setAttribute(JSonReader.OBJECT_TO_READ, jsonResults); 078 079 try 080 { 081 String siteName = _getSiteName(request); 082 String lang = _getLang(parameters); 083 String query = _getQuery(request); 084 int limit = _getLimit(request); 085 Sort[] sorts = _getSorts(request); 086 087 if (StringUtils.isNotBlank(query)) 088 { 089 AmetysObjectIterable<Page> results = _queryPages(siteName, lang, query, limit, sorts); 090 091 for (Page page : results) 092 { 093 jsonResults.add(Map.of("title", page.getTitle(), 094 "long-title", page.getLongTitle(), 095 "id", page.getId(), 096 "url", ResolveURIComponent.resolve("page", page.getId()), 097 "path", String.join(" > ", _getParentsTitle(page)))); 098 } 099 } 100 101 102 } 103 catch (Exception e) 104 { 105 throw new ProcessingException("Error while autocompleting", e); 106 } 107 108 return EMPTY_MAP; 109 } 110 111 private String _getQuery(Request request) 112 { 113 return _escapeQuery(request.getParameter(__REQUEST_PARAM_QUERY)); 114 } 115 116 private AmetysObjectIterable<Page> _queryPages(String siteName, String lang, String query, int limit, Sort[] sorts) throws Exception 117 { 118 OrQuery titleQuery = new OrQuery(List.of(new StringQuery(SolrWebFieldNames.PAGE_TITLE, Operator.SEARCH, query, lang, true), 119 new StringQuery(SolrWebFieldNames.PAGE_LONG_TITLE, Operator.SEARCH, query, lang, true))); 120 121 Searcher searcher = _searcherFactory.create() 122 .withQuery(titleQuery) 123 .addFilterQuery(new DocumentTypeQuery(SolrWebFieldNames.TYPE_PAGE)) 124 .addFilterQueryString(SolrWebFieldNames.SITE_NAME + ":" + siteName) 125 .addFilterQueryString(SolrWebFieldNames.SITEMAP_NAME + ":" + lang) 126 .withLimits(0, limit) 127 .withSort(sorts) 128 .setCheckRights(true); 129 130 return searcher.search(); 131 } 132 133 private String _escapeQuery(String text) 134 { 135 if (text != null) 136 { 137 String trimText = StringUtils.strip(text.trim(), "*"); 138 return "*" 139 + QueryHelper.escapeQueryCharsExceptStarsAndWhitespaces(trimText) 140 + "*"; 141 } 142 return null; 143 } 144 145 private List<String> _getParentsTitle(Page page) 146 { 147 List<String> parents = new ArrayList<>(); 148 149 SitemapElement cursor = page.getParent(); 150 while (cursor instanceof Page) 151 { 152 parents.add(0, ((Page) cursor).getTitle()); // insert at index 0, so the older parent is the first displayed 153 cursor = cursor.getParent(); 154 } 155 156 return parents; 157 } 158 159 private String _getLang(Parameters parameters) 160 { 161 String lang = parameters.getParameter(__REQUEST_PARAM_LANG, null); 162 if (StringUtils.isBlank(lang)) 163 { 164 throw new IllegalArgumentException("A language must be specified for auto completion"); 165 } 166 return lang; 167 } 168 169 private String _getSiteName(Request request) 170 { 171 String siteName = WebHelper.getSiteName(request); 172 if (StringUtils.isBlank(siteName)) 173 { 174 throw new IllegalArgumentException("A site must be specified for auto completion"); 175 } 176 return siteName; 177 } 178 179 private Sort[] _getSorts(Request request) 180 { 181 String sortParam = request.getParameter(__REQUEST_PARAM_SORT); 182 if (StringUtils.isNotBlank(sortParam) 183 && ("true".equals(sortParam) || "title".equals(sortParam))) 184 { 185 return new Sort[] {new Sort("title_s_sort", Order.ASC)}; 186 } 187 else 188 { 189 return new Sort[0]; 190 } 191 } 192 193 private int _getLimit(Request request) 194 { 195 int limit = __DEFAULT_NUMBER; 196 String limitArg = request.getParameter("limit"); 197 if (StringUtils.isNotBlank(limitArg)) 198 { 199 limit = Integer.parseInt(limitArg); 200 } 201 limit = Math.min(__MAX_NUMBER, limit); 202 return limit; 203 } 204}