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.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import java.util.Optional; 025import java.util.stream.Collectors; 026 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.commons.lang.StringUtils; 030 031import org.ametys.cms.content.indexing.solr.SolrFieldNames; 032import org.ametys.cms.repository.Content; 033import org.ametys.cms.search.advanced.AbstractTreeNode; 034import org.ametys.cms.search.advanced.TreeLeaf; 035import org.ametys.cms.search.query.AndQuery; 036import org.ametys.cms.search.query.BooleanQuery; 037import org.ametys.cms.search.query.ContentTypeQuery; 038import org.ametys.cms.search.query.DocumentTypeQuery; 039import org.ametys.cms.search.query.JoinQuery; 040import org.ametys.cms.search.query.OrQuery; 041import org.ametys.cms.search.query.Query; 042import org.ametys.cms.search.query.Query.Operator; 043import org.ametys.cms.search.query.StringQuery; 044import org.ametys.cms.search.solr.SearcherFactory.Searcher; 045import org.ametys.core.util.URIUtils; 046import org.ametys.odf.course.Course; 047import org.ametys.odf.program.AbstractProgram; 048import org.ametys.odf.program.Program; 049import org.ametys.odf.program.ProgramFactory; 050import org.ametys.odf.program.SubProgram; 051import org.ametys.odf.skill.ODFSkillsHelper; 052import org.ametys.odf.skill.workflow.SkillEditionFunction; 053import org.ametys.plugins.odfweb.repository.OdfPageResolver; 054import org.ametys.plugins.odfweb.restrictions.OdfProgramRestriction; 055import org.ametys.plugins.odfweb.restrictions.OdfProgramRestrictionManager; 056import org.ametys.plugins.repository.AmetysObject; 057import org.ametys.plugins.repository.AmetysObjectIterable; 058import org.ametys.web.frontoffice.search.fast.AbstractAutocompletionSearchServiceAction; 059import org.ametys.web.frontoffice.search.instance.SearchServiceInstance; 060import org.ametys.web.frontoffice.search.instance.model.RightCheckingMode; 061import org.ametys.web.frontoffice.search.instance.model.SearchServiceCriterion; 062import org.ametys.web.frontoffice.search.instance.model.SearchServiceCriterionMode; 063import org.ametys.web.renderingcontext.RenderingContext; 064import org.ametys.web.repository.page.Page; 065import org.ametys.web.repository.site.Site; 066 067/** 068 * Get the proposed program's pages and skills for auto-completion while beginning a search 069 */ 070public class ProgramItemAutocompletionSearchServiceAction extends AbstractAutocompletionSearchServiceAction 071{ 072 /** The ODF page resolver */ 073 protected OdfPageResolver _odfPageResolver; 074 075 /** The ODF program restriction manager */ 076 protected OdfProgramRestrictionManager _odfRestrictionManager; 077 078 @Override 079 public void service(ServiceManager serviceManager) throws ServiceException 080 { 081 super.service(serviceManager); 082 _odfPageResolver = (OdfPageResolver) serviceManager.lookup(OdfPageResolver.ROLE); 083 _odfRestrictionManager = (OdfProgramRestrictionManager) serviceManager.lookup(OdfProgramRestrictionManager.ROLE); 084 } 085 086 @Override 087 protected Map<String, Object> _searchAmetysObject(Site site, String zoneItemId, String lang, String escapedQuery, int limit, RightCheckingMode rightCheckingMode) throws Exception 088 { 089 Map<String, Object> results = super._searchAmetysObject(site, zoneItemId, lang, escapedQuery, limit, rightCheckingMode); 090 091 if (ODFSkillsHelper.isSkillsEnabled()) 092 { 093 List<String> catalogNames = _getCatalogNames(zoneItemId); 094 095 // Search on skill's title 096 AmetysObjectIterable<Content> skillResults = _getSkills(escapedQuery, site, catalogNames, lang, limit, rightCheckingMode); 097 List<Map<String, Object>> skills = skillResults.stream() 098 .map(this::_getContentHit) 099 .collect(Collectors.toList()); 100 101 results.put("skills", skills); 102 } 103 104 return results; 105 } 106 107 @Override 108 protected void setHitsInResults(Map<String, Object> results, AmetysObjectIterable<AmetysObject> searchHits, Site site, String zoneItemId, String lang) 109 { 110 List<String> catalogNames = _getCatalogNames(zoneItemId); 111 112 // Compute common ODF root page if search is configured for one catalog 113 Page odfRootPage = catalogNames.size() == 1 ? _odfPageResolver.getOdfRootPage(site.getName(), lang, catalogNames.get(0)) : null; 114 115 List<Map<String, Object>> pages = searchHits.stream() 116 .filter(Content.class::isInstance) 117 .map(Content.class::cast) 118 .map(c -> _getPageHit(c, site.getName(), odfRootPage)) 119 .filter(Objects::nonNull) 120 .collect(Collectors.toList()); 121 results.put("pages", pages); 122 } 123 124 /** 125 * Get the configured catalog in criterion definitions 126 * @param zoneItemId The id of zone item 127 * @return the catalog's name 128 */ 129 protected List<String> _getCatalogNames(String zoneItemId) 130 { 131 if (StringUtils.isNotEmpty(zoneItemId)) 132 { 133 SearchServiceInstance serviceInstance = _searchServiceInstanceManager.get(zoneItemId); 134 135 List<String> catalogValues = serviceInstance.getCriterionTree() 136 .map(AbstractTreeNode::getFlatLeaves) 137 .orElseGet(Collections::emptyList) 138 .stream() 139 .map(TreeLeaf::getValue) 140 .filter(c -> c.getMode() == SearchServiceCriterionMode.STATIC) 141 .filter(c -> StringUtils.endsWith(c.getCriterionDefinition().getName(), "$org.ametys.plugins.odf.Content.programItem$catalog")) 142 .map(SearchServiceCriterion::getStaticValue) 143 .flatMap(Optional::stream) 144 .map(List.class::cast) 145 .flatMap(List::stream) 146 .map(String.class::cast) 147 .toList(); 148 149 return catalogValues; 150 } 151 else 152 { 153 return List.of(); 154 } 155 156 } 157 158 /** 159 * Get the skills contents matching the query 160 * @param escapedQuery the query 161 * @param site the current site 162 * @param catalogNames The catalog names 163 * @param lang the language 164 * @param limit the max number of results 165 * @param rightCheckingMode the mode for checking rights 166 * @return the matching contents 167 * @throws Exception if an error occurred during search 168 */ 169 protected AmetysObjectIterable<Content> _getSkills(String escapedQuery, Site site, List<String> catalogNames, String lang, int limit, RightCheckingMode rightCheckingMode) throws Exception 170 { 171 // Search for skills in the given catalog(s) 172 Query catalogQuery = catalogNames.stream() 173 .map(c -> new StringQuery("catalog", c)) 174 .collect(OrQuery.collector()); 175 176 // Search for skills that are not orphans (either transversal or linked to a program) 177 Query transversal = new BooleanQuery("transversal", true); 178 Query programQuery = new JoinQuery(_getProgramQuery(site, catalogNames), "parentProgram"); 179 Query skillQuery = new OrQuery(transversal, programQuery); 180 181 Searcher searcher = _searcherFactory.create() 182 .withQuery(new StringQuery(SolrFieldNames.TITLE, Operator.SEARCH, escapedQuery, lang, true)) 183 .addFilterQuery(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT)) 184 .addFilterQuery(new ContentTypeQuery(SkillEditionFunction.MACRO_SKILL_TYPE)) 185 .addFilterQuery(catalogQuery) 186 .addFilterQuery(skillQuery) 187 .withLimits(0, limit); 188 189 _setRightCheckingMode(searcher, rightCheckingMode); 190 191 return searcher.search(); 192 } 193 194 /** 195 * Get the program query to filter skills by programs 196 * @param site The current site 197 * @param catalogNames The filtering catalog names 198 * @return the program query 199 */ 200 protected Query _getProgramQuery(Site site, List<String> catalogNames) 201 { 202 List<Query> programQueries = new ArrayList<>(); 203 204 programQueries.add(new ContentTypeQuery(ProgramFactory.PROGRAM_CONTENT_TYPE)); 205 206 Query catalogQuery = catalogNames.stream() 207 .map(c -> new StringQuery("catalog", c)) 208 .collect(OrQuery.collector()); 209 programQueries.add(catalogQuery); 210 211 OdfProgramRestriction restriction = _odfRestrictionManager.getRestriction(site); 212 Query restrictionQuery = restriction != null ? restriction.getQuery() : null; 213 if (restrictionQuery != null) 214 { 215 programQueries.add(restrictionQuery); 216 } 217 218 return new AndQuery(programQueries); 219 } 220 221 /** 222 * Get the JSON representation of a page hit 223 * @param content the content 224 * @param siteName the current site name 225 * @param odfRootPage the ODF root page. Can be null if search is configured for multiple catalogs 226 * @return the page as json 227 */ 228 protected Map<String, Object> _getPageHit(Content content, String siteName, Page odfRootPage) 229 { 230 Page page = null; 231 if (content instanceof Program) 232 { 233 page = odfRootPage != null ? _odfPageResolver.getProgramPage(odfRootPage, (Program) content) : _odfPageResolver.getProgramPage((Program) content, siteName); 234 } 235 else if (content instanceof SubProgram) 236 { 237 page = odfRootPage != null ? _odfPageResolver.getSubProgramPage(odfRootPage, (SubProgram) content, null) : _odfPageResolver.getSubProgramPage((SubProgram) content, null, siteName); 238 } 239 else if (content instanceof Course) 240 { 241 page = odfRootPage != null ? _odfPageResolver.getCoursePage(odfRootPage, (Course) content, (AbstractProgram) null) : _odfPageResolver.getCoursePage((Course) content, (AbstractProgram) null, siteName); 242 } 243 244 if (page != null) 245 { 246 Map<String, Object> result = new HashMap<>(); 247 result.put("title", page.getTitle()); 248 249 RenderingContext context = _renderingContextHandler.getRenderingContext(); 250 if (!(context == RenderingContext.BACK)) 251 { 252 StringBuilder uri = new StringBuilder(); 253 uri.append(_prefixHandler.getUriPrefix(siteName)); 254 uri.append("/"); 255 uri.append(page.getSitemapName() + "/" + page.getPathInSitemap() + ".html"); 256 257 result.put("url", URIUtils.encodePath(uri.toString())); 258 } 259 else // back 260 { 261 result.put("url", "javascript:(function(){parent.Ametys.tool.ToolsManager.openTool('uitool-page', {id:'" + page.getId() + "'});})()"); 262 } 263 264 return result; 265 } 266 267 return null; 268 } 269 270 /** 271 * Get the JSON representation of a content hit 272 * @param content the content 273 * @return the content as json 274 */ 275 protected Map<String, Object> _getContentHit(Content content) 276 { 277 Map<String, Object> result = new HashMap<>(); 278 279 result.put("title", content.getTitle()); 280 result.put("id", content.getId()); 281 282 return result; 283 } 284 285}