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<>(wordingQueries);  // add wording queries
144        pagesQueries.addAll(getPageQueries(request, siteNames, language)); // add specific queries to pages
145        
146        if (!pagesQueries.isEmpty())
147        {
148            finalQueries.add(new AndQuery(pagesQueries));
149        }
150        
151        // Query to execute on joined contents
152        List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries
153        contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents
154        Query contentQuery = new AndQuery(contentQueries);
155        
156        List<Query> contentOrResourcesQueries = new ArrayList<>();
157        contentOrResourcesQueries.add(contentQuery);
158        contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(wordingQuery)); // add queries on join content's resources
159        
160        Query programContentQuery = new OrQuery(contentOrResourcesQueries);
161        Query programPageQuery = new PageContentQuery(programContentQuery);
162        
163        finalQueries.add(programPageQuery);
164
165        Query subProgramPageQuery = null;
166        if (_searchOnSubPrograms())
167        {
168            subProgramPageQuery = getSubProgramPageQuery(programContentQuery); // add query on joined subprograms
169        }
170        
171        finalQueries.add(subProgramPageQuery);
172        
173        return finalQueries.isEmpty() ? new MatchAllQuery() : new MaxScoreOrQuery(finalQueries);
174    }
175    
176    @Override
177    protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults, boolean saxResults)
178            throws Exception
179    {
180        _matchingSubProgramIds = new ArrayList<>();
181        if (saxResults)
182        {
183            DisplaySubprogramMode displaySubProgramMode = getDisplaySubProgramMode();
184            if (displaySubProgramMode.equals(DisplaySubprogramMode.ALL_WITH_HIGHLIGHT) || displaySubProgramMode.equals(DisplaySubprogramMode.MATCHING_SEARCH_ONLY))
185            {
186                _matchingSubProgramIds = getSubProgramsMatchingSearch(request, siteNames, language);
187            }
188        }
189        
190        return super.search(request, siteNames, language, pageIndex, start, maxResults, saxResults);
191    }
192    
193    /**
194     * Get the ids of subprograms matching the current search
195     * @param request The request
196     * @param siteNames The site names
197     * @param language The languages
198     * @return the ids of matching subprograms
199     * @throws Exception if failed to execute search
200     */
201    protected List<String> getSubProgramsMatchingSearch(Request request, Collection<String> siteNames, String language) throws Exception
202    {
203        List<Query> wordingQueries = getWordingQueries(request, siteNames, language);
204        Query wordingQuery = wordingQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(wordingQueries);
205        
206        // Query to execute on joined contents
207        List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries
208        contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents
209        contentQueries.add(new ContentTypeQuery(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE));
210        Query contentQuery = new AndQuery(contentQueries);
211        
212        List<Query> contentOrResourcesQueries = new ArrayList<>();
213        contentOrResourcesQueries.add(contentQuery);
214        contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(wordingQuery)); // add queries on join content's resources
215        
216        Searcher searcher = _searcherFactory.create()
217                .withQuery(new OrQuery(contentOrResourcesQueries))
218                .addFilterQuery(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT))
219                .withLimits(0, Integer.MAX_VALUE)
220                .setCheckRights(true);
221        
222        AmetysObjectIterable<AmetysObject> subPrograms = searcher.search();
223        return subPrograms.stream().map(ao -> ao.getId()).collect(Collectors.toList());
224    }
225    
226    /**
227     * Get the page query to execute for subprogram's pages
228     * @param contentQuery the initial content query
229     * @return the page query for subprogram
230     */
231    protected Query getSubProgramPageQuery(Query contentQuery)
232    {
233        Query subProgramTypeQuery = new ConstantNilScoreQuery(new ContentTypeQuery(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE));
234        Query subProgramContentQuery = new AndQuery(subProgramTypeQuery, contentQuery);
235        return new SubProgramPageContentQuery(subProgramContentQuery);
236    }
237    
238    @Override
239    protected void saxAdditionalInfosOnPageHit(Page page) throws SAXException
240    {
241        super.saxAdditionalInfosOnPageHit(page);
242        
243        DisplaySubprogramMode displaySubProgramMode = getDisplaySubProgramMode();
244        
245        if (displaySubProgramMode != DisplaySubprogramMode.NONE && page instanceof ProgramPage)
246        {
247            ContentType contentType = _cTypeExtPt.getExtension(SubProgramFactory.SUBPROGRAM_CONTENT_TYPE);
248            View view = Optional.ofNullable(contentType).map(cType -> cType.getView("index")).orElse(null);
249            
250            if (page instanceof ProgramPage)
251            {
252                String programPath = page.getPathInSitemap();
253                AbstractProgram<ProgramFactory> program = ((ProgramPage) page).getProgram();
254                for (ProgramPart childProgramPart : program.getProgramPartChildren())
255                {
256                    if (childProgramPart instanceof SubProgram)
257                    {
258                        SubProgram subProgram = (SubProgram) childProgramPart;
259                        
260                        boolean matchSearch = _matchingSubProgramIds.contains(subProgram.getId());
261                        if (!_displaySubprogramMode.equals(DisplaySubprogramMode.MATCHING_SEARCH_ONLY) || matchSearch)
262                        {
263                            AttributesImpl attrs = new AttributesImpl();
264                            Page subProgramPage = _odfPageResolver.getSubProgramPage(subProgram, program, page.getSiteName());
265                            if (subProgramPage != null)
266                            {
267                                attrs.addCDATAAttribute("path", StringUtils.substringAfterLast(subProgramPage.getPathInSitemap(), programPath));
268                            }
269                            else
270                            {
271                                getLogger().warn("The subprogram '" + subProgram.getId() + "' was returned from the search but its virtual page could not be resolved");
272                            }
273                            attrs.addCDATAAttribute("title", subProgram.getTitle());
274                            if (_displaySubprogramMode == DisplaySubprogramMode.ALL_WITH_HIGHLIGHT)
275                            {
276                                attrs.addCDATAAttribute("highlight", String.valueOf(matchSearch));
277                            }
278                            XMLUtils.startElement(contentHandler, "subprogram", attrs);
279
280                            try
281                            {
282                                subProgram.dataToSAX(contentHandler, view);
283                            }
284                            catch (Exception e)
285                            {
286                                getLogger().error("An error occurred during saxing subprogram '" + subProgram.getId() + "' metadata", e);
287                            }
288                            
289                            XMLUtils.endElement(contentHandler, "subprogram");
290                        }
291                    }
292                }
293            }
294        }
295    }
296    
297    /**
298     * Get the display mode for subprograms
299     * @return the display mode
300     */
301    protected DisplaySubprogramMode getDisplaySubProgramMode()
302    {
303        if (_displaySubprogramMode == null)
304        {
305            String displaySubprogramsParam = parameters.getParameter("display-subprograms", "none");
306            _displaySubprogramMode = DisplaySubprogramMode.valueOf(displaySubprogramsParam.toUpperCase());
307        }
308        return _displaySubprogramMode;
309        
310    }
311    /**
312     * Determines the search should be executed on subprograms
313     * @return true to execute search also on subprograms
314     */
315    protected boolean _searchOnSubPrograms()
316    {
317        return parameters.getParameterAsBoolean("subprogram-search", false);
318    }
319    
320    @Override
321    protected void addContentTypeQuery(Collection<Query> queries, Request request)
322    {
323        Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
324        queries.add(new PageContentQuery(new ContentTypeQuery(cTypes)));
325    }
326    
327    class SubProgramPageContentQuery extends JoinQuery
328    {
329        public SubProgramPageContentQuery(Query subQuery)
330        {
331            super(subQuery, SolrWebFieldNames.CONTENT_IDS, TraversableProgramPart.CHILD_PROGRAM_PARTS);
332        }
333    }
334}