001/* 002 * Copyright 2024 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.frontoffice.search.fast; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023 024import org.apache.avalon.framework.parameters.Parameters; 025import org.apache.avalon.framework.service.ServiceException; 026import org.apache.avalon.framework.service.ServiceManager; 027import org.apache.cocoon.ProcessingException; 028import org.apache.cocoon.acting.ServiceableAction; 029import org.apache.cocoon.environment.ObjectModelHelper; 030import org.apache.cocoon.environment.Redirector; 031import org.apache.cocoon.environment.Request; 032import org.apache.cocoon.environment.SourceResolver; 033import org.apache.commons.lang.StringUtils; 034 035import org.ametys.cms.content.indexing.solr.SolrFieldNames; 036import org.ametys.cms.search.query.AndQuery; 037import org.ametys.cms.search.query.Query; 038import org.ametys.cms.search.query.Query.Operator; 039import org.ametys.cms.search.query.QueryHelper; 040import org.ametys.cms.search.query.StringQuery; 041import org.ametys.cms.search.solr.SearcherFactory; 042import org.ametys.cms.search.solr.SearcherFactory.Searcher; 043import org.ametys.core.cocoon.JSonReader; 044import org.ametys.plugins.repository.AmetysObject; 045import org.ametys.plugins.repository.AmetysObjectIterable; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 048import org.ametys.web.URIPrefixHandler; 049import org.ametys.web.frontoffice.search.instance.SearchServiceInstance; 050import org.ametys.web.frontoffice.search.instance.SearchServiceInstanceManager; 051import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode; 052import org.ametys.web.frontoffice.search.requesttime.impl.SearchComponentHelper; 053import org.ametys.web.renderingcontext.RenderingContextHandler; 054import org.ametys.web.repository.page.SitemapElement; 055import org.ametys.web.repository.page.ZoneItem; 056import org.ametys.web.repository.site.Site; 057import org.ametys.web.repository.site.SiteManager; 058 059/** 060 * Abstract to get the proposed ametys object while beginning a search from a given search service 061 */ 062public abstract class AbstractAutocompletionSearchServiceAction extends ServiceableAction 063{ 064 /** The default max number of results */ 065 protected static final int NB_MAX_RESULTS = 10; 066 067 /** The search factory */ 068 protected SearcherFactory _searcherFactory; 069 070 /** Component for search service */ 071 protected SearchServiceInstanceManager _searchServiceInstanceManager; 072 073 /** The search component helper */ 074 protected SearchComponentHelper _searchComponentHelper; 075 076 /** The ametys object resolver */ 077 protected AmetysObjectResolver _resolver; 078 079 /** The site manager */ 080 protected SiteManager _siteManager; 081 082 /** The prefix handler */ 083 protected URIPrefixHandler _prefixHandler; 084 085 /** The rendering context handler */ 086 protected RenderingContextHandler _renderingContextHandler; 087 088 @Override 089 public void service(ServiceManager serviceManager) throws ServiceException 090 { 091 super.service(serviceManager); 092 _searcherFactory = (SearcherFactory) serviceManager.lookup(SearcherFactory.ROLE); 093 _searchServiceInstanceManager = (SearchServiceInstanceManager) serviceManager.lookup(SearchServiceInstanceManager.ROLE); 094 _prefixHandler = (URIPrefixHandler) serviceManager.lookup(URIPrefixHandler.ROLE); 095 _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE); 096 _searchComponentHelper = (SearchComponentHelper) serviceManager.lookup(SearchComponentHelper.ROLE); 097 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 098 _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE); 099 } 100 101 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 102 { 103 Request request = ObjectModelHelper.getRequest(objectModel); 104 105 Site site = _getSite(request); 106 String lang = _getLang(request); 107 String zoneItemId = _getZoneItemId(request); 108 109 RightCheckingMode rightCheckingMode = getRightCheckingMode(zoneItemId); 110 int limit = request.getParameter("limit") != null ? Integer.valueOf(request.getParameter("limit")) : NB_MAX_RESULTS; 111 112 // Retrieve current workspace 113 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 114 try 115 { 116 String query = request.getParameter("q"); 117 String escapedQuery = _escapeQuery(query); 118 119 Map<String, Object> results = _searchAmetysObject(site, zoneItemId, lang, escapedQuery, limit, rightCheckingMode); 120 121 request.setAttribute(JSonReader.OBJECT_TO_READ, results); 122 return EMPTY_MAP; 123 } 124 catch (Exception e) 125 { 126 getLogger().error("Error getting auto-complete list.", e); 127 throw new ProcessingException("Error getting auto-complete list.", e); 128 } 129 finally 130 { 131 // Restore context 132 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 133 } 134 } 135 136 /** 137 * Search ametys object and set the results 138 * @param site the site 139 * @param zoneItemId the zone item id 140 * @param lang the lang 141 * @param escapedQuery the escaped query 142 * @param limit the limit 143 * @param rightCheckingMode the right checking mode 144 * @return the search results 145 * @throws Exception if an error occurred during search 146 */ 147 protected Map<String, Object> _searchAmetysObject(Site site, String zoneItemId, String lang, String escapedQuery, int limit, RightCheckingMode rightCheckingMode) throws Exception 148 { 149 Map<String, Object> results = new HashMap<>(); 150 151 // Search on content's title with the given content type 152 AmetysObjectIterable<AmetysObject> searchHits = _search(site, zoneItemId, lang, escapedQuery, limit, rightCheckingMode); 153 154 setHitsInResults(results, searchHits, site, zoneItemId, lang); 155 156 return results; 157 } 158 159 /** 160 * Set the hits in the results 161 * @param results the results 162 * @param searchHits the search hits 163 * @param site the site 164 * @param zoneItemId the zone item it 165 * @param lang the lang 166 */ 167 protected abstract void setHitsInResults(Map<String, Object> results, AmetysObjectIterable<AmetysObject> searchHits, Site site, String zoneItemId, String lang); 168 169 /** 170 * Get the ametys object matching the query 171 * @param site the site 172 * @param zoneItemId the zone item it 173 * @param lang the language 174 * @param escapedQuery the query 175 * @param limit the max number of results 176 * @param rightCheckingMode the mode for checking rights 177 * @return the matching ametys object 178 * @throws Exception if an error occurred during search 179 */ 180 protected AmetysObjectIterable<AmetysObject> _search(Site site, String zoneItemId, String lang, String escapedQuery, int limit, RightCheckingMode rightCheckingMode) throws Exception 181 { 182 SearchServiceInstance searchService = _searchServiceInstanceManager.get(zoneItemId); 183 SitemapElement page = _getSitemapElement(zoneItemId); 184 185 List<Query> queries = new ArrayList<>(); 186 queries.add(new StringQuery(SolrFieldNames.TITLE, Operator.SEARCH, escapedQuery, lang, true)); 187 queries.add(_searchComponentHelper.getCriterionTreeQuery(searchService, Map.of(), site, lang)); 188 189 Searcher searcher = _searcherFactory.create() 190 .withQuery(new AndQuery(queries)) 191 .addFilterQuery(_searchComponentHelper.getFilterQuery(searchService, site, page, lang)) 192 .withLimits(0, limit); 193 194 _setRightCheckingMode(searcher, rightCheckingMode); 195 196 return searcher.search(); 197 } 198 199 /** 200 * Set whether to check rights when searching, 201 * @param searcher the searcher 202 * @param rightCheckingMode the the mode for checking rights 203 */ 204 protected void _setRightCheckingMode(Searcher searcher, RightCheckingMode rightCheckingMode) 205 { 206 switch (rightCheckingMode) 207 { 208 case EXACT: 209 case FAST: 210 // FAST will be force to EXACT because of user inputs 211 searcher.setCheckRights(true); 212 break; 213 case NONE: 214 searcher.setCheckRights(false); 215 break; 216 default: 217 throw new IllegalStateException("Unhandled right checking mode: " + rightCheckingMode); 218 } 219 } 220 221 private SitemapElement _getSitemapElement(String zoneItemId) 222 { 223 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 224 AmetysObject page = zoneItem.getParent(); 225 while(!(page instanceof SitemapElement sitemapElement)) 226 { 227 page = page.getParent(); 228 } 229 230 return (SitemapElement) page; 231 } 232 233 private String _getZoneItemId(Request request) 234 { 235 String zoneItemId = request.getParameter("zoneItemId"); 236 if (StringUtils.isEmpty(zoneItemId)) 237 { 238 throw new IllegalArgumentException("A zone item identifier must be specified for auto completion"); 239 } 240 return zoneItemId; 241 } 242 243 private String _getLang(Request request) 244 { 245 String lang = request.getParameter("lang"); 246 if (StringUtils.isEmpty(lang)) 247 { 248 throw new IllegalArgumentException("A language must be specified for auto completion"); 249 } 250 return lang; 251 } 252 253 private Site _getSite(Request request) 254 { 255 String siteName = request.getParameter("siteName"); 256 if (StringUtils.isEmpty(siteName)) 257 { 258 throw new IllegalArgumentException("A site must be specified for auto completion"); 259 } 260 return _siteManager.getSite(siteName); 261 } 262 263 /** 264 * Get the mode for checking rights 265 * @param zoneItemId the id of zone item 266 * @return the mode for checking rights 267 */ 268 protected RightCheckingMode getRightCheckingMode(String zoneItemId) 269 { 270 return Optional.ofNullable(zoneItemId) 271 .filter(StringUtils::isNotEmpty) 272 .map(s -> _searchServiceInstanceManager.get(zoneItemId)) 273 .map(SearchServiceInstance::getRightCheckingMode) 274 .orElse(RightCheckingMode.EXACT); 275 } 276 277 private String _escapeQuery(String text) 278 { 279 String trimText = StringUtils.strip(text.trim(), "*"); 280 return "*" + QueryHelper.escapeQueryCharsExceptStarsAndWhitespaces(trimText) + "*"; 281 } 282}