001/* 002 * Copyright 2019 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.plugins.odfweb.service.search; 017 018import java.util.HashMap; 019import java.util.List; 020import java.util.Map; 021import java.util.stream.Collectors; 022 023import org.apache.avalon.framework.parameters.Parameters; 024import org.apache.avalon.framework.service.ServiceException; 025import org.apache.avalon.framework.service.ServiceManager; 026import org.apache.cocoon.ProcessingException; 027import org.apache.cocoon.acting.ServiceableAction; 028import org.apache.cocoon.environment.ObjectModelHelper; 029import org.apache.cocoon.environment.Redirector; 030import org.apache.cocoon.environment.Request; 031import org.apache.cocoon.environment.SourceResolver; 032import org.apache.commons.lang.StringUtils; 033import org.apache.solr.client.solrj.util.ClientUtils; 034 035import org.ametys.cms.content.indexing.solr.SolrFieldNames; 036import org.ametys.cms.repository.Content; 037import org.ametys.cms.search.query.ContentTypeQuery; 038import org.ametys.cms.search.query.DocumentTypeQuery; 039import org.ametys.cms.search.query.Query.Operator; 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.cms.transformation.xslt.ResolveURIComponent; 044import org.ametys.core.cocoon.JSonReader; 045import org.ametys.odf.enumeration.OdfReferenceTableHelper; 046import org.ametys.odf.program.ProgramFactory; 047import org.ametys.plugins.repository.AmetysObjectIterable; 048import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 049import org.ametys.web.indexing.solr.SolrWebFieldNames; 050import org.ametys.web.repository.page.Page; 051import org.ametys.web.search.query.SiteQuery; 052import org.ametys.web.search.query.SitemapQuery; 053 054/** 055 * Get the proposed program's pages and skills for auto-completion while beginning a search 056 */ 057public class AutocompletionSearchAction extends ServiceableAction 058{ 059 /** The default max number of results */ 060 protected static final int NB_MAX_RESULTS = 10; 061 062 /** The search factory */ 063 protected 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 String siteName = request.getParameter("siteName"); 078 if (StringUtils.isEmpty(siteName)) 079 { 080 throw new IllegalArgumentException("A site must be specified for auto completion"); 081 } 082 083 String lang = request.getParameter("lang"); 084 if (StringUtils.isEmpty(lang)) 085 { 086 throw new IllegalArgumentException("A language must be specified for auto completion"); 087 } 088 089 int limit = request.getParameter("limit") != null ? Integer.valueOf(request.getParameter("limit")) : NB_MAX_RESULTS; 090 091 // Retrieve current workspace 092 String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 093 094 String contentType = parameters.getParameter("contentType", ProgramFactory.PROGRAM_CONTENT_TYPE); 095 096 try 097 { 098 String query = request.getParameter("q"); 099 String escapedQuery = _escapeQuery(query); 100 101 Map<String, Object> results = new HashMap<>(); 102 103 // Search on page's title with the given content type 104 AmetysObjectIterable<Page> pageResults = getContentPages(contentType, siteName, lang, escapedQuery, limit); 105 List<Map<String, Object>> pages = pageResults.stream() 106 .map(this::getPageHit) 107 .collect(Collectors.toList()); 108 results.put("pages", pages); 109 110 // Search on skill's title 111 AmetysObjectIterable<Content> skillResults = getSkills(escapedQuery, lang, limit); 112 List<Map<String, Object>> skills = skillResults.stream() 113 .map(this::getContentHit) 114 .collect(Collectors.toList()); 115 116 results.put("skills", skills); 117 118 request.setAttribute(JSonReader.OBJECT_TO_READ, results); 119 return EMPTY_MAP; 120 } 121 catch (Exception e) 122 { 123 getLogger().error("Error getting auto-complete list.", e); 124 throw new ProcessingException("Error getting auto-complete list.", e); 125 } 126 finally 127 { 128 // Restore context 129 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp); 130 } 131 } 132 133 /** 134 * Get the content pages matching the query 135 * @param cType The content type of pages to search 136 * @param siteName the site name 137 * @param lang the language 138 * @param escapedQuery the query 139 * @param limit the max number of results 140 * @return the matching pages 141 * @throws Exception if an error occurred during search 142 */ 143 protected AmetysObjectIterable<Page> getContentPages(String cType, String siteName, String lang, String escapedQuery, int limit) throws Exception 144 { 145 Searcher searcher = _searcherFactory.create() 146 .withQuery(new StringQuery(SolrWebFieldNames.PAGE_TITLE, Operator.LIKE, escapedQuery, null, true)) 147 .addFilterQuery(new DocumentTypeQuery(SolrWebFieldNames.TYPE_PAGE)) 148 .addFilterQuery(new SiteQuery(siteName)) 149 .addFilterQuery(new SitemapQuery(lang)) 150 .addFilterQueryString(SolrWebFieldNames.PAGE_CONTENT_TYPES + ":" + cType) 151 .withLimits(0, limit) 152 .setCheckRights(true); 153 154 return searcher.search(); 155 } 156 157 /** 158 * Get the skills contents matching the query 159 * @param escapedQuery the query 160 * @param lang the language 161 * @param limit the max number of results 162 * @return the matching contents 163 * @throws Exception if an error occurred during search 164 */ 165 protected AmetysObjectIterable<Content> getSkills(String escapedQuery, String lang, int limit) throws Exception 166 { 167 Searcher searcher = _searcherFactory.create() 168 .withQuery(new StringQuery(SolrFieldNames.TITLE, Operator.LIKE, escapedQuery, lang, true)) 169 .addFilterQuery(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)) 170 .addFilterQuery(new ContentTypeQuery(OdfReferenceTableHelper.SKILL)) 171 .withLimits(0, limit) 172 .setCheckRights(true); 173 174 return searcher.search(); 175 } 176 177 private String _escapeQuery(String text) 178 { 179 String trimText = StringUtils.strip(text.trim(), "*"); 180 return "*" + ClientUtils.escapeQueryChars(trimText) + "*"; 181 } 182 183 /** 184 * Get the JSON representation of a page hit 185 * @param page the page 186 * @return the page as json 187 */ 188 protected Map<String, Object> getPageHit(Page page) 189 { 190 Map<String, Object> result = new HashMap<>(); 191 192 result.put("title", page.getTitle()); 193 result.put("id", page.getId()); 194 result.put("url", ResolveURIComponent.resolve("page", page.getId())); 195 196 return result; 197 } 198 199 /** 200 * Get the JSON representation of a content hit 201 * @param content the content 202 * @return the content as json 203 */ 204 protected Map<String, Object> getContentHit(Content content) 205 { 206 Map<String, Object> result = new HashMap<>(); 207 208 result.put("title", content.getTitle()); 209 result.put("id", content.getId()); 210 211 return result; 212 } 213 214}