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}