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