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