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