001/* 002 * Copyright 2017 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.program; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.List; 023import java.util.Optional; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.cocoon.ProcessingException; 029import org.apache.cocoon.environment.Request; 030import org.apache.cocoon.xml.AttributesImpl; 031import org.apache.cocoon.xml.XMLUtils; 032import org.apache.commons.lang3.StringUtils; 033import org.xml.sax.SAXException; 034 035import org.ametys.cms.content.indexing.solr.SolrFieldNames; 036import org.ametys.cms.contenttype.ContentType; 037import org.ametys.cms.search.SearchResults; 038import org.ametys.cms.search.query.AndQuery; 039import org.ametys.cms.search.query.ConstantNilScoreQuery; 040import org.ametys.cms.search.query.ContentTypeQuery; 041import org.ametys.cms.search.query.DocumentTypeQuery; 042import org.ametys.cms.search.query.JoinQuery; 043import org.ametys.cms.search.query.MatchAllQuery; 044import org.ametys.cms.search.query.MaxScoreOrQuery; 045import org.ametys.cms.search.query.OrQuery; 046import org.ametys.cms.search.query.Query; 047import org.ametys.cms.search.query.StringQuery; 048import org.ametys.cms.search.solr.SearcherFactory.Searcher; 049import org.ametys.odf.ProgramItem; 050import org.ametys.odf.program.AbstractProgram; 051import org.ametys.odf.program.ProgramFactory; 052import org.ametys.odf.program.ProgramPart; 053import org.ametys.odf.program.SubProgram; 054import org.ametys.odf.program.SubProgramFactory; 055import org.ametys.odf.program.TraversableProgramPart; 056import org.ametys.odf.skill.ProgramSkillsIndexingField; 057import org.ametys.plugins.odfweb.repository.OdfPageResolver; 058import org.ametys.plugins.odfweb.repository.ProgramPage; 059import org.ametys.plugins.repository.AmetysObject; 060import org.ametys.plugins.repository.AmetysObjectIterable; 061import org.ametys.runtime.model.View; 062import org.ametys.web.frontoffice.SearchGenerator; 063import org.ametys.web.indexing.solr.SolrWebFieldNames; 064import org.ametys.web.repository.page.Page; 065import org.ametys.web.search.query.PageContentQuery; 066 067/** 068 * ODF search results 069 */ 070public class FrontODFSearch extends SearchGenerator 071{ 072 /** 073 * Enumeration for display subprogram mode 074 */ 075 public enum DisplaySubprogramMode 076 { 077 /** Display no subprogram */ 078 NONE, 079 /** Display all subprograms */ 080 ALL, 081 /** Display all subprograms with highlighting those which match the search criteria*/ 082 ALL_WITH_HIGHLIGHT, 083 /** Display matching subprograms only */ 084 MATCHING_SEARCH_ONLY 085 } 086 087 /** The matching subprograms */ 088 protected List<String> _matchingSubProgramIds; 089 090 private DisplaySubprogramMode _displaySubprogramMode; 091 private OdfPageResolver _odfPageResolver; 092 093 @Override 094 public void service(ServiceManager smanager) throws ServiceException 095 { 096 super.service(smanager); 097 _odfPageResolver = (OdfPageResolver) smanager.lookup(OdfPageResolver.ROLE); 098 } 099 100 @Override 101 protected Collection<String> getContentTypes(Request request) 102 { 103 return Arrays.asList(parameters.getParameter("contentType", ProgramFactory.PROGRAM_CONTENT_TYPE)); 104 } 105 106 @Override 107 public void generate() throws IOException, SAXException, ProcessingException 108 { 109 _displaySubprogramMode = null; 110 _matchingSubProgramIds = new ArrayList<>(); 111 112 super.generate(); 113 } 114 115 @Override 116 protected List<Query> getContentQueries(Request request, Collection<String> siteNames, String language) 117 { 118 List<Query> contentQueries = super.getContentQueries(request, siteNames, language); 119 120 // Add catalog query 121 String catalog = parameters.getParameter("catalog", request.getParameter("catalog")); 122 contentQueries.add(new ConstantNilScoreQuery(new StringQuery(ProgramItem.CATALOG, catalog))); 123 124 // Add query on acquired skill's id if present 125 String skillId = request.getParameter("skillId"); 126 if (StringUtils.isNotBlank(skillId)) 127 { 128 contentQueries.add(new StringQuery(ProgramSkillsIndexingField.PROGRAM_SKILLS_INDEXING_FIELD, skillId)); 129 } 130 131 return contentQueries; 132 } 133 134 @Override 135 protected Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException 136 { 137 List<Query> finalQueries = new ArrayList<>(); 138 139 List<Query> wordingQueries = getWordingQueries(request, siteNames, language); 140 Query wordingQuery = wordingQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(wordingQueries); 141 142 // Query to execute on pages 143 List<Query> pagesQueries = new ArrayList<>(wordingQueries); // add wording queries 144 pagesQueries.addAll(getPageQueries(request, siteNames, language)); // add specific queries to pages 145 146 if (!pagesQueries.isEmpty()) 147 { 148 finalQueries.add(new AndQuery(pagesQueries)); 149 } 150 151 // Query to execute on joined contents 152 List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries 153 contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents 154 Query contentQuery = new AndQuery(contentQueries); 155 156 List<Query> contentOrResourcesQueries = new ArrayList<>(); 157 contentOrResourcesQueries.add(contentQuery); 158 contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(wordingQuery)); // add queries on join content's resources 159 160 Query programContentQuery = new OrQuery(contentOrResourcesQueries); 161 Query programPageQuery = new PageContentQuery(programContentQuery); 162 163 finalQueries.add(programPageQuery); 164 165 Query subProgramPageQuery = null; 166 if (_searchOnSubPrograms()) 167 { 168 subProgramPageQuery = getSubProgramPageQuery(programContentQuery); // add query on joined subprograms 169 } 170 171 finalQueries.add(subProgramPageQuery); 172 173 return finalQueries.isEmpty() ? new MatchAllQuery() : new MaxScoreOrQuery(finalQueries); 174 } 175 176 @Override 177 protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults, boolean saxResults) 178 throws Exception 179 { 180 _matchingSubProgramIds = new ArrayList<>(); 181 if (saxResults) 182 { 183 DisplaySubprogramMode displaySubProgramMode = getDisplaySubProgramMode(); 184 if (displaySubProgramMode.equals(DisplaySubprogramMode.ALL_WITH_HIGHLIGHT) || displaySubProgramMode.equals(DisplaySubprogramMode.MATCHING_SEARCH_ONLY)) 185 { 186 _matchingSubProgramIds = getSubProgramsMatchingSearch(request, siteNames, language); 187 } 188 } 189 190 return super.search(request, siteNames, language, pageIndex, start, maxResults, saxResults); 191 } 192 193 /** 194 * Get the ids of subprograms matching the current search 195 * @param request The request 196 * @param siteNames The site names 197 * @param language The languages 198 * @return the ids of matching subprograms 199 * @throws Exception if failed to execute search 200 */ 201 protected List<String> getSubProgramsMatchingSearch(Request request, Collection<String> siteNames, String language) throws Exception 202 { 203 List<Query> wordingQueries = getWordingQueries(request, siteNames, language); 204 Query wordingQuery = wordingQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(wordingQueries); 205 206 // Query to execute on joined contents 207 List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries 208 contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents 209 contentQueries.add(new ContentTypeQuery(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE)); 210 Query contentQuery = new AndQuery(contentQueries); 211 212 List<Query> contentOrResourcesQueries = new ArrayList<>(); 213 contentOrResourcesQueries.add(contentQuery); 214 contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(wordingQuery)); // add queries on join content's resources 215 216 Searcher searcher = _searcherFactory.create() 217 .withQuery(new OrQuery(contentOrResourcesQueries)) 218 .addFilterQuery(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)) 219 .withLimits(0, Integer.MAX_VALUE) 220 .setCheckRights(true); 221 222 AmetysObjectIterable<AmetysObject> subPrograms = searcher.search(); 223 return subPrograms.stream().map(ao -> ao.getId()).collect(Collectors.toList()); 224 } 225 226 /** 227 * Get the page query to execute for subprogram's pages 228 * @param contentQuery the initial content query 229 * @return the page query for subprogram 230 */ 231 protected Query getSubProgramPageQuery(Query contentQuery) 232 { 233 Query subProgramTypeQuery = new ConstantNilScoreQuery(new ContentTypeQuery(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE)); 234 Query subProgramContentQuery = new AndQuery(subProgramTypeQuery, contentQuery); 235 return new SubProgramPageContentQuery(subProgramContentQuery); 236 } 237 238 @Override 239 protected void saxAdditionalInfosOnPageHit(Page page) throws SAXException 240 { 241 super.saxAdditionalInfosOnPageHit(page); 242 243 DisplaySubprogramMode displaySubProgramMode = getDisplaySubProgramMode(); 244 245 if (displaySubProgramMode != DisplaySubprogramMode.NONE && page instanceof ProgramPage) 246 { 247 ContentType contentType = _cTypeExtPt.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE); 248 View view = Optional.ofNullable(contentType).map(cType -> cType.getView("index")).orElse(null); 249 250 if (page instanceof ProgramPage) 251 { 252 String programPath = page.getPathInSitemap(); 253 AbstractProgram<ProgramFactory> program = ((ProgramPage) page).getProgram(); 254 for (ProgramPart childProgramPart : program.getProgramPartChildren()) 255 { 256 if (childProgramPart instanceof SubProgram) 257 { 258 SubProgram subProgram = (SubProgram) childProgramPart; 259 260 boolean matchSearch = _matchingSubProgramIds.contains(subProgram.getId()); 261 if (!_displaySubprogramMode.equals(DisplaySubprogramMode.MATCHING_SEARCH_ONLY) || matchSearch) 262 { 263 AttributesImpl attrs = new AttributesImpl(); 264 Page subProgramPage = _odfPageResolver.getSubProgramPage(subProgram, program, page.getSiteName()); 265 if (subProgramPage != null) 266 { 267 attrs.addCDATAAttribute("path", StringUtils.substringAfterLast(subProgramPage.getPathInSitemap(), programPath)); 268 } 269 else 270 { 271 getLogger().warn("The subprogram '" + subProgram.getId() + "' was returned from the search but its virtual page could not be resolved"); 272 } 273 attrs.addCDATAAttribute("title", subProgram.getTitle()); 274 if (_displaySubprogramMode == DisplaySubprogramMode.ALL_WITH_HIGHLIGHT) 275 { 276 attrs.addCDATAAttribute("highlight", String.valueOf(matchSearch)); 277 } 278 XMLUtils.startElement(contentHandler, "subprogram", attrs); 279 280 try 281 { 282 subProgram.dataToSAX(contentHandler, view); 283 } 284 catch (Exception e) 285 { 286 getLogger().error("An error occurred during saxing subprogram '" + subProgram.getId() + "' metadata", e); 287 } 288 289 XMLUtils.endElement(contentHandler, "subprogram"); 290 } 291 } 292 } 293 } 294 } 295 } 296 297 /** 298 * Get the display mode for subprograms 299 * @return the display mode 300 */ 301 protected DisplaySubprogramMode getDisplaySubProgramMode() 302 { 303 if (_displaySubprogramMode == null) 304 { 305 String displaySubprogramsParam = parameters.getParameter("display-subprograms", "none"); 306 _displaySubprogramMode = DisplaySubprogramMode.valueOf(displaySubprogramsParam.toUpperCase()); 307 } 308 return _displaySubprogramMode; 309 310 } 311 /** 312 * Determines the search should be executed on subprograms 313 * @return true to execute search also on subprograms 314 */ 315 protected boolean _searchOnSubPrograms() 316 { 317 return parameters.getParameterAsBoolean("subprogram-search", false); 318 } 319 320 @Override 321 protected void addContentTypeQuery(Collection<Query> queries, Request request) 322 { 323 Collection<String> cTypes = new ArrayList<>(getContentTypes(request)); 324 queries.add(new PageContentQuery(new ContentTypeQuery(cTypes))); 325 } 326 327 class SubProgramPageContentQuery extends JoinQuery 328 { 329 public SubProgramPageContentQuery(Query subQuery) 330 { 331 super(subQuery, SolrWebFieldNames.CONTENT_IDS, TraversableProgramPart.CHILD_PROGRAM_PARTS); 332 } 333 } 334}