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