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.Optional;
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.search.SearchResults;
038import org.ametys.cms.search.query.AndQuery;
039import org.ametys.cms.search.query.ConstantNilScoreQuery;
040import org.ametys.cms.search.query.ContentTypeQuery;
041import org.ametys.cms.search.query.DocumentTypeQuery;
042import org.ametys.cms.search.query.JoinQuery;
043import org.ametys.cms.search.query.MatchAllQuery;
044import org.ametys.cms.search.query.MaxScoreOrQuery;
045import org.ametys.cms.search.query.OrQuery;
046import org.ametys.cms.search.query.Query;
047import org.ametys.cms.search.query.StringQuery;
048import org.ametys.cms.search.solr.SearcherFactory.Searcher;
049import org.ametys.odf.ProgramItem;
050import org.ametys.odf.program.AbstractProgram;
051import org.ametys.odf.program.ProgramFactory;
052import org.ametys.odf.program.ProgramPart;
053import org.ametys.odf.program.SubProgram;
054import org.ametys.odf.program.SubProgramFactory;
055import org.ametys.odf.program.TraversableProgramPart;
056import org.ametys.odf.skill.ProgramSkillsIndexingField;
057import org.ametys.plugins.odfweb.repository.OdfPageResolver;
058import org.ametys.plugins.odfweb.repository.ProgramPage;
059import org.ametys.plugins.repository.AmetysObject;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.runtime.model.View;
062import org.ametys.web.frontoffice.SearchGenerator;
063import org.ametys.web.indexing.solr.SolrWebFieldNames;
064import org.ametys.web.repository.page.Page;
065import org.ametys.web.search.query.PageContentQuery;
066
067/**
068 * ODF search results
069 */
070public class FrontODFSearch extends SearchGenerator
071{
072    /**
073     * Enumeration for display subprogram mode
074     */
075    public enum DisplaySubprogramMode
076    {
077        /** Display no subprogram */
078        NONE,
079        /** Display all subprograms */
080        ALL,
081        /** Display all subprograms with highlighting those which match the search criteria*/
082        ALL_WITH_HIGHLIGHT,
083        /** Display matching subprograms only */
084        MATCHING_SEARCH_ONLY
085    }
086
087    /** The matching subprograms */
088    protected List<String> _matchingSubProgramIds;
089    
090    private DisplaySubprogramMode _displaySubprogramMode;
091    private OdfPageResolver _odfPageResolver;
092    
093    @Override
094    public void service(ServiceManager smanager) throws ServiceException
095    {
096        super.service(smanager);
097        _odfPageResolver = (OdfPageResolver) smanager.lookup(OdfPageResolver.ROLE);
098    }
099    
100    @Override
101    protected Collection<String> getContentTypes(Request request)
102    {
103        return Arrays.asList(parameters.getParameter("contentType", ProgramFactory.PROGRAM_CONTENT_TYPE));
104    }
105    
106    @Override
107    public void generate() throws IOException, SAXException, ProcessingException
108    {
109        _displaySubprogramMode = null;
110        _matchingSubProgramIds = new ArrayList<>();
111        
112        super.generate();
113    }
114    
115    @Override
116    protected List<Query> getContentQueries(Request request, Collection<String> siteNames, String language)
117    {
118        List<Query> contentQueries = super.getContentQueries(request, siteNames, language);
119        
120        // Add catalog query
121        String catalog = parameters.getParameter("catalog", request.getParameter("catalog"));
122        contentQueries.add(new ConstantNilScoreQuery(new StringQuery(ProgramItem.CATALOG, catalog)));
123        
124        // Add query on acquired skill's id if present
125        String skillId = request.getParameter("skillId");
126        if (StringUtils.isNotBlank(skillId))
127        {
128            contentQueries.add(new StringQuery(ProgramSkillsIndexingField.PROGRAM_SKILLS_INDEXING_FIELD, skillId));
129        }
130        
131        return contentQueries;
132    }
133    
134    @Override
135    protected Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
136    {
137        List<Query> finalQueries = new ArrayList<>();
138        
139        List<Query> wordingQueries = getWordingQueries(request, siteNames, language);
140        Query wordingQuery = wordingQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(wordingQueries);
141        
142        // Query to execute on pages
143        List<Query> pagesQueries = new ArrayList<>();  
144        pagesQueries.addAll(getPageQueries(request, siteNames, language)); // add specific queries to pages
145        
146        if (!pagesQueries.isEmpty())
147        {
148            pagesQueries.add(wordingQuery); // add wording queries
149            finalQueries.add(new AndQuery(pagesQueries));
150        }
151        
152        // Query to execute on joined contents
153        List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries
154        contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents
155        Query contentQuery = new AndQuery(contentQueries);
156        
157        List<Query> contentOrResourcesQueries = new ArrayList<>();
158        contentOrResourcesQueries.add(contentQuery);
159        contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(wordingQuery)); // add queries on join content's resources
160        
161        Query programContentQuery = new OrQuery(contentOrResourcesQueries);
162        Query programPageQuery = new PageContentQuery(programContentQuery);
163        
164        finalQueries.add(programPageQuery);
165
166        Query subProgramPageQuery = null;
167        if (_searchOnSubPrograms())
168        {
169            subProgramPageQuery = getSubProgramPageQuery(programContentQuery); // add query on joined subprograms
170        }
171        
172        finalQueries.add(subProgramPageQuery);
173        
174        return finalQueries.isEmpty() ? new MatchAllQuery() : new MaxScoreOrQuery(finalQueries);
175    }
176    
177    @Override
178    protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults, boolean saxResults)
179            throws Exception
180    {
181        _matchingSubProgramIds = new ArrayList<>();
182        if (saxResults)
183        {
184            DisplaySubprogramMode displaySubProgramMode = getDisplaySubProgramMode();
185            if (displaySubProgramMode.equals(DisplaySubprogramMode.ALL_WITH_HIGHLIGHT) || displaySubProgramMode.equals(DisplaySubprogramMode.MATCHING_SEARCH_ONLY))
186            {
187                _matchingSubProgramIds = getSubProgramsMatchingSearch(request, siteNames, language);
188            }
189        }
190        
191        return super.search(request, siteNames, language, pageIndex, start, maxResults, saxResults);
192    }
193    
194    /**
195     * Get the ids of subprograms matching the current search
196     * @param request The request
197     * @param siteNames The site names
198     * @param language The languages
199     * @return the ids of matching subprograms
200     * @throws Exception if failed to execute search
201     */
202    protected List<String> getSubProgramsMatchingSearch(Request request, Collection<String> siteNames, String language) throws Exception
203    {
204        List<Query> wordingQueries = getWordingQueries(request, siteNames, language);
205        Query wordingQuery = wordingQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(wordingQueries);
206        
207        // Query to execute on joined contents
208        List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries
209        contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents
210        contentQueries.add(new ContentTypeQuery(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE));
211        Query contentQuery = new AndQuery(contentQueries);
212        
213        List<Query> contentOrResourcesQueries = new ArrayList<>();
214        contentOrResourcesQueries.add(contentQuery);
215        contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(wordingQuery)); // add queries on join content's resources
216        
217        Searcher searcher = _searcherFactory.create()
218                .withQuery(new OrQuery(contentOrResourcesQueries))
219                .addFilterQuery(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
220                .withLimits(0, Integer.MAX_VALUE)
221                .setCheckRights(true);
222        
223        AmetysObjectIterable<AmetysObject> subPrograms = searcher.search();
224        return subPrograms.stream().map(ao -> ao.getId()).collect(Collectors.toList());
225    }
226    
227    /**
228     * Get the page query to execute for subprogram's pages
229     * @param contentQuery the initial content query
230     * @return the page query for subprogram
231     */
232    protected Query getSubProgramPageQuery(Query contentQuery)
233    {
234        Query subProgramTypeQuery = new ConstantNilScoreQuery(new ContentTypeQuery(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE));
235        Query subProgramContentQuery = new AndQuery(subProgramTypeQuery, contentQuery);
236        return new SubProgramPageContentQuery(subProgramContentQuery);
237    }
238    
239    @Override
240    protected void saxAdditionalInfosOnPageHit(Page page) throws SAXException
241    {
242        super.saxAdditionalInfosOnPageHit(page);
243        
244        DisplaySubprogramMode displaySubProgramMode = getDisplaySubProgramMode();
245        
246        if (displaySubProgramMode != DisplaySubprogramMode.NONE && page instanceof ProgramPage)
247        {
248            ContentType contentType = _cTypeExtPt.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
249            View view = Optional.ofNullable(contentType).map(cType -> cType.getView("index")).orElse(null);
250            
251            if (page instanceof ProgramPage)
252            {
253                String programPath = page.getPathInSitemap();
254                AbstractProgram<ProgramFactory> program = ((ProgramPage) page).getProgram();
255                for (ProgramPart childProgramPart : program.getProgramPartChildren())
256                {
257                    if (childProgramPart instanceof SubProgram)
258                    {
259                        SubProgram subProgram = (SubProgram) childProgramPart;
260                        
261                        boolean matchSearch = _matchingSubProgramIds.contains(subProgram.getId());
262                        if (!_displaySubprogramMode.equals(DisplaySubprogramMode.MATCHING_SEARCH_ONLY) || matchSearch)
263                        {
264                            AttributesImpl attrs = new AttributesImpl();
265                            Page subProgramPage = _odfPageResolver.getSubProgramPage(subProgram, program, page.getSiteName());
266                            if (subProgramPage != null)
267                            {
268                                attrs.addCDATAAttribute("path", StringUtils.substringAfterLast(subProgramPage.getPathInSitemap(), programPath));
269                            }
270                            else
271                            {
272                                getLogger().warn("The subprogram '" + subProgram.getId() + "' was returned from the search but its virtual page could not be resolved");
273                            }
274                            attrs.addCDATAAttribute("title", subProgram.getTitle());
275                            if (_displaySubprogramMode == DisplaySubprogramMode.ALL_WITH_HIGHLIGHT)
276                            {
277                                attrs.addCDATAAttribute("highlight", String.valueOf(matchSearch));
278                            }
279                            XMLUtils.startElement(contentHandler, "subprogram", attrs);
280
281                            try
282                            {
283                                subProgram.dataToSAX(contentHandler, view);
284                            }
285                            catch (Exception e)
286                            {
287                                getLogger().error("An error occurred during saxing subprogram '" + subProgram.getId() + "' metadata", e);
288                            }
289                            
290                            XMLUtils.endElement(contentHandler, "subprogram");
291                        }
292                    }
293                }
294            }
295        }
296    }
297    
298    /**
299     * Get the display mode for subprograms
300     * @return the display mode
301     */
302    protected DisplaySubprogramMode getDisplaySubProgramMode()
303    {
304        if (_displaySubprogramMode == null)
305        {
306            String displaySubprogramsParam = parameters.getParameter("display-subprograms", "none");
307            _displaySubprogramMode = DisplaySubprogramMode.valueOf(displaySubprogramsParam.toUpperCase());
308        }
309        return _displaySubprogramMode;
310        
311    }
312    /**
313     * Determines the search should be executed on subprograms
314     * @return true to execute search also on subprograms
315     */
316    protected boolean _searchOnSubPrograms()
317    {
318        return parameters.getParameterAsBoolean("subprogram-search", false);
319    }
320    
321    @Override
322    protected void addContentTypeQuery(Collection<Query> queries, Request request)
323    {
324        Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
325        queries.add(new PageContentQuery(new ContentTypeQuery(cTypes)));
326    }
327    
328    class SubProgramPageContentQuery extends JoinQuery
329    {
330        public SubProgramPageContentQuery(Query subQuery)
331        {
332            super(subQuery, SolrWebFieldNames.CONTENT_IDS, TraversableProgramPart.CHILD_PROGRAM_PARTS);
333        }
334    }
335}