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.PagesContainer; 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_SITE = "siteName"; 059 private static final String __REQUEST_PARAM_LANG = "lang"; 060 private static final String __REQUEST_PARAM_QUERY = "q"; 061 private static final String __REQUEST_PARAM_SORT = "sort"; 062 063 private SearcherFactory _searcherFactory; 064 065 066 @Override 067 public void service(ServiceManager serviceManager) throws ServiceException 068 { 069 super.service(serviceManager); 070 _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE); 071 } 072 073 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 074 { 075 Request request = ObjectModelHelper.getRequest(objectModel); 076 077 List<Map<String, String>> jsonResults = new ArrayList<>(); 078 request.setAttribute(JSonReader.OBJECT_TO_READ, jsonResults); 079 080 try 081 { 082 String siteName = _getSiteName(request); 083 String lang = _getLang(parameters); 084 String query = _getQuery(request); 085 int limit = _getLimit(request); 086 Sort[] sorts = _getSorts(request); 087 088 if (StringUtils.isNotBlank(query)) 089 { 090 AmetysObjectIterable<Page> results = _queryPages(siteName, lang, query, limit, sorts); 091 092 for (Page page : results) 093 { 094 jsonResults.add(Map.of("title", page.getTitle(), 095 "long-title", page.getLongTitle(), 096 "id", page.getId(), 097 "url", ResolveURIComponent.resolve("page", page.getId()), 098 "path", String.join(" > ", _getParentsTitle(page)))); 099 } 100 } 101 102 103 } 104 catch (Exception e) 105 { 106 throw new ProcessingException("Error while autocompleting", e); 107 } 108 109 return EMPTY_MAP; 110 } 111 112 private String _getQuery(Request request) 113 { 114 return _escapeQuery(request.getParameter(__REQUEST_PARAM_QUERY)); 115 } 116 117 private AmetysObjectIterable<Page> _queryPages(String siteName, String lang, String query, int limit, Sort[] sorts) throws Exception 118 { 119 OrQuery titleQuery = new OrQuery(List.of(new StringQuery(SolrWebFieldNames.PAGE_TITLE, Operator.SEARCH, query, lang, true), 120 new StringQuery(SolrWebFieldNames.PAGE_LONG_TITLE, Operator.SEARCH, query, lang, true))); 121 122 Searcher searcher = _searcherFactory.create() 123 .withQuery(titleQuery) 124 .addFilterQuery(new DocumentTypeQuery(SolrWebFieldNames.TYPE_PAGE)) 125 .addFilterQueryString(SolrWebFieldNames.SITE_NAME + ":" + siteName) 126 .addFilterQueryString(SolrWebFieldNames.SITEMAP_NAME + ":" + lang) 127 .withLimits(0, limit) 128 .withSort(sorts) 129 .setCheckRights(true); 130 131 return searcher.search(); 132 } 133 134 private String _escapeQuery(String text) 135 { 136 if (text != null) 137 { 138 String trimText = StringUtils.strip(text.trim(), "*"); 139 return "*" 140 + QueryHelper.escapeQueryCharsExceptStarsAndWhitespaces(trimText) 141 + "*"; 142 } 143 return null; 144 } 145 146 private List<String> _getParentsTitle(Page page) 147 { 148 List<String> parents = new ArrayList<>(); 149 150 PagesContainer cursor = page.getParent(); 151 while (cursor instanceof Page) 152 { 153 parents.add(0, ((Page) cursor).getTitle()); // insert at index 0, so the older parent is the first displayed 154 cursor = cursor.getParent(); 155 } 156 157 return parents; 158 } 159 160 private String _getLang(Parameters parameters) 161 { 162 String lang = parameters.getParameter(__REQUEST_PARAM_LANG, null); 163 if (StringUtils.isBlank(lang)) 164 { 165 throw new IllegalArgumentException("A language must be specified for auto completion"); 166 } 167 return lang; 168 } 169 170 private String _getSiteName(Request request) 171 { 172 String siteName = WebHelper.getSiteName(request); 173 if (StringUtils.isBlank(siteName)) 174 { 175 throw new IllegalArgumentException("A site must be specified for auto completion"); 176 } 177 return siteName; 178 } 179 180 private Sort[] _getSorts(Request request) 181 { 182 String sortParam = request.getParameter(__REQUEST_PARAM_SORT); 183 if (StringUtils.isNotBlank(sortParam) 184 && ("true".equals(sortParam) || "title".equals(sortParam))) 185 { 186 return new Sort[] {new Sort("title_s_sort", Order.ASC)}; 187 } 188 else 189 { 190 return new Sort[0]; 191 } 192 } 193 194 private int _getLimit(Request request) 195 { 196 int limit = __DEFAULT_NUMBER; 197 String limitArg = request.getParameter("limit"); 198 if (StringUtils.isNotBlank(limitArg)) 199 { 200 limit = Integer.parseInt(limitArg); 201 } 202 limit = Math.min(__MAX_NUMBER, limit); 203 return limit; 204 } 205}