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