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