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}