001/*
002 *  Copyright 2018 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.web.frontoffice.search.instance.model;
017
018import java.lang.reflect.Constructor;
019import java.util.List;
020import java.util.stream.Collectors;
021import java.util.stream.Stream;
022
023import org.apache.commons.lang3.NotImplementedException;
024import org.apache.commons.lang3.tuple.Pair;
025
026import org.ametys.cms.search.query.AndQuery;
027import org.ametys.cms.search.query.MatchAllQuery;
028import org.ametys.cms.search.query.MatchNoneQuery;
029import org.ametys.cms.search.query.NotQuery;
030import org.ametys.cms.search.query.OrQuery;
031import org.ametys.cms.search.query.Query;
032import org.ametys.cms.search.query.Query.LogicalOperator;
033import org.ametys.cms.search.query.Query.Operator;
034import org.ametys.cms.search.query.TagQuery;
035import org.ametys.web.frontoffice.search.metamodel.Returnable;
036import org.ametys.web.frontoffice.search.metamodel.context.ContextQueriesWrapper;
037import org.ametys.web.repository.page.Page;
038import org.ametys.web.repository.site.Site;
039import org.ametys.web.search.query.ChildPageQuery;
040import org.ametys.web.search.query.DescendantPageQuery;
041import org.ametys.web.search.query.SiteQuery;
042
043/**
044 * A search context
045 */
046public class SearchContext
047{
048    private SiteContext _sites;
049    private SitemapContext _sitemap;
050    private ContextLang _langs;
051    // keep id instead of real tag objects and then re-resolve them to avoid RepositoryException about session closed
052    private List<String> _tagIds;
053    private boolean _tagAutoposting;
054
055    /**
056     * Creates a SearchContext
057     * @param sites The site context
058     * @param sitemap The sitemap context
059     * @param langs The lang of the context
060     * @param tags The tags of the context
061     * @param tagAutoposting <code>true</code> if search on tags should be with autoposting
062     */
063    public SearchContext(SiteContext sites,
064            SitemapContext sitemap,
065            ContextLang langs,
066            List<String> tags,
067            boolean tagAutoposting)
068    {
069        _sites = sites;
070        _sitemap = sitemap;
071        _langs = langs;
072        _tagIds = tags;
073        _tagAutoposting = tagAutoposting;
074    }
075    
076    /**
077     * Gets the site context
078     * @return the site context
079     */
080    public SiteContext siteContext()
081    {
082        return _sites;
083    }
084    
085    /**
086     * Gets the sitemap context
087     * @return the sitemap context
088     */
089    public SitemapContext sitemapContext()
090    {
091        return _sitemap;
092    }
093    
094    /**
095     * Gets the lang context
096     * @return the lang context
097     */
098    public ContextLang langContext()
099    {
100        return _langs;
101    }
102    
103    /**
104     * Gets the tag context
105     * @return the tag context
106     */
107    public Pair<List<String>, Boolean> tagContext()
108    {
109        return Pair.of(_tagIds, _tagAutoposting);
110    }
111    
112    /**
113     * Gets the query corresponding to the site part of the context
114     * @param currentSite the current site
115     * @return the query corresponding to the site part of the context
116     */
117    public Query getSiteQuery(Site currentSite)
118    {
119        String currentSiteName = currentSite.getName();
120        switch (_sites.getType())
121        {
122            case CURRENT:
123                // test is current
124                return new SiteQuery(Operator.EQ, currentSiteName);
125                
126            case AMONG:
127                // test is one of sites of SiteContext
128                List<String> siteNames = _sites.getSites()
129                    .get()
130                    .stream()
131                    .map(Site::getName)
132                    .collect(Collectors.toList());
133                return new SiteQuery(Operator.EQ, siteNames);
134                
135            case ALL:
136                // test existence
137                return new SiteQuery();
138                
139            case OTHERS:
140                // test existence and is not current
141                return new AndQuery(
142                        new SiteQuery(),
143                        new SiteQuery(Operator.NE, currentSiteName));
144                
145            default:
146                throw new NotImplementedException("This SiteContextType is not handled: " + _sites.getType());
147        }
148    }
149    
150    /**
151     * Gets the query corresponding to the sitemap part of the context
152     * @param currentPage the current page
153     * @return the query corresponding to the sitemap part of the context
154     */
155    public Query getSitemapQuery(Page currentPage)
156    {
157        Stream<Query> childPageQueriesStream;
158        switch (_sitemap.getType())
159        {
160            case CURRENT_SITE:
161                // Do nothing more
162                return null;
163                
164            case CHILD_PAGES:
165                return new DescendantPageQuery(currentPage.getId());
166            case CHILD_PAGES_OF:
167                childPageQueriesStream = _sitemap.getPages().get()
168                        .stream()
169                        .map(Page::getId)
170                        .map(ancestorPageId -> new DescendantPageQuery(ancestorPageId));
171                break;
172            case DIRECT_CHILD_PAGES:
173                return new ChildPageQuery(currentPage.getId());
174            case DIRECT_CHILD_PAGES_OF:
175                childPageQueriesStream = _sitemap.getPages().get()
176                        .stream()
177                        .map(Page::getId)
178                        .map(parentPageId -> new ChildPageQuery(parentPageId));
179                break;
180            default:
181                throw new NotImplementedException("This SitemapContextType is not handled: " + _sitemap.getType());
182        }
183        
184        // If no selected page, we do not want any result => in this special case, return a MatchNoneQuery
185        List<Query> childPageQueries = childPageQueriesStream.collect(Collectors.toList());
186        return childPageQueries.isEmpty() ? new MatchNoneQuery() : new OrQuery(childPageQueries);
187    }
188    
189    /**
190     * Gets the {@link ContextLang} and the current lang
191     * @param currentLang The current lang
192     * @return the wrapper of the {@link ContextLang} and the current lang
193     */
194    public ContextLangAndCurrentLang getContextLang(String currentLang)
195    {
196        return new ContextLangAndCurrentLang(_langs, currentLang);
197    }
198    
199    /**
200     * Class wrapping a {@link ContextLang} and the current lang.
201     * <br>Needed because at the time of the {@link ContextLangAndCurrentLang} constructor call, the Query cannot be created yet, even though the current lang is known (the {@link Returnable}s are responsible for creating the query via {@link LangQueryProducer})
202     * @see LangQueryProducer
203     * @see Returnable#filterReturnedDocumentQuery
204     * @see ContextQueriesWrapper#getQuery
205     */
206    public static final class ContextLangAndCurrentLang
207    {
208        ContextLang _contextLangs;
209        String _currentLang;
210
211        ContextLangAndCurrentLang(ContextLang contextLangs, String currentLang)
212        {
213            _contextLangs = contextLangs;
214            _currentLang = currentLang;
215        }
216    }
217    
218    /**
219     * Class wrapping an implementation of {@link Query} in order to {@link #produce} the final query executed for limiting the context language for a given {@link Returnable}.
220     * <br>Instances of this class should be created only in {@link Returnable#filterReturnedDocumentQuery} methods, and then passed to {@link ContextQueriesWrapper#getQuery}
221     * @see Returnable#filterReturnedDocumentQuery
222     * @see ContextQueriesWrapper#getQuery
223     */
224    public static final class LangQueryProducer
225    {
226        private Constructor< ? extends Query> _noArgConstructor;
227        private Constructor< ? extends Query> _opAndStrConstructor;
228        private boolean _acceptDocWithNolang;
229        
230        /**
231         * Constructs a {@link LangQueryProducer}
232         * <br>The given class must have at least two constructors:
233         * <ul>
234         * <li>one with no arguments in order to test {@link Operator#EXISTS  existence};</li>
235         * <li>one with an {@link Operator} and an array of {@link String} values in order to test {@link Operator#EQ equality} of the current language.</li>
236         * </ul>
237         * @param languageQueryClass The implementation of {@link Query} for testing the lang
238         * @param acceptDocWithNolang <code>true</code> to accept documents with no lang
239         */
240        public LangQueryProducer(Class<? extends Query> languageQueryClass, boolean acceptDocWithNolang)
241        {
242            try
243            {
244                _noArgConstructor = languageQueryClass.getConstructor();
245                _opAndStrConstructor = languageQueryClass.getConstructor(Operator.class, String[].class);
246            }
247            catch (NoSuchMethodException | SecurityException e)
248            {
249                throw new IllegalArgumentException("The provided class is not valid. It should have a no arg constructor, and a constructor with an Operator and String...", e);
250            }
251            _acceptDocWithNolang = acceptDocWithNolang;
252        }
253        
254        /**
255         * Produces the Query for testing the context language
256         * @param contextLangAndCurrentLang The wrapper of the {@link ContextLang} and the real current language
257         * @return a {@link Query}
258         * @throws Exception if an error occurs
259         */
260        public final Query produce(ContextLangAndCurrentLang contextLangAndCurrentLang) throws Exception
261        {
262            String currentLang = contextLangAndCurrentLang._currentLang;
263            ContextLang contextLang = contextLangAndCurrentLang._contextLangs;
264            
265            Query equalityQuery = _opAndStrConstructor.newInstance(Operator.EQ, new String[] {currentLang});
266            Query nonEqualityQuery = _opAndStrConstructor.newInstance(Operator.NE, new String[] {currentLang});
267            Query existenceQuery = _noArgConstructor.newInstance();
268            Query nonExistenceQuery = new NotQuery(existenceQuery);
269            
270            switch (contextLang)
271            {
272                case CURRENT:
273                    if (_acceptDocWithNolang)
274                    {
275                        // test is current, or not present
276                        return new OrQuery(equalityQuery, nonExistenceQuery);
277                    }
278                    else
279                    {
280                        // test is current
281                        return equalityQuery;
282                    }
283                    
284                case OTHERS:
285                    if (_acceptDocWithNolang)
286                    {
287                        // is not current (we do not care about existence)
288                        return nonEqualityQuery;
289                    }
290                    else
291                    {
292                        // test existence and is not current
293                        return new AndQuery(nonEqualityQuery, existenceQuery);
294                    }
295                    
296                case ALL:
297                    if (_acceptDocWithNolang)
298                    {
299                        // nothing to test, whether the doc has or not the lang field, it is accepted
300                        return new MatchAllQuery();
301                    }
302                    else
303                    {
304                        // test existence 
305                        return existenceQuery;
306                    }
307                    
308                default:
309                    throw new NotImplementedException("This ContextLang is not handled: " + contextLang);
310            }
311        }
312    }
313    
314    /**
315     * Gets the query corresponding to the tag part of the context
316     * @return the query corresponding to the tag part of the context
317     */
318    public Query getTagQuery()
319    {
320        return _tagIds.isEmpty() ? null : new TagQuery(Operator.EQ, _tagAutoposting, LogicalOperator.AND, _tagIds.toArray(new String[_tagIds.size()]));
321    }
322
323    @Override
324    public int hashCode()
325    {
326        final int prime = 31;
327        int result = 1;
328        result = prime * result + ((_langs == null) ? 0 : _langs.hashCode());
329        result = prime * result + ((_sitemap == null) ? 0 : _sitemap.hashCode());
330        result = prime * result + ((_sites == null) ? 0 : _sites.hashCode());
331        result = prime * result + (_tagAutoposting ? 1231 : 1237);
332        result = prime * result + ((_tagIds == null) ? 0 : _tagIds.hashCode());
333        return result;
334    }
335
336    @Override
337    public boolean equals(Object obj)
338    {
339        if (this == obj)
340        {
341            return true;
342        }
343        if (obj == null)
344        {
345            return false;
346        }
347        if (!(obj instanceof SearchContext))
348        {
349            return false;
350        }
351        SearchContext other = (SearchContext) obj;
352        if (_langs != other._langs)
353        {
354            return false;
355        }
356        if (_sitemap == null)
357        {
358            if (other._sitemap != null)
359            {
360                return false;
361            }
362        }
363        else if (!_sitemap.equals(other._sitemap))
364        {
365            return false;
366        }
367        if (_sites == null)
368        {
369            if (other._sites != null)
370            {
371                return false;
372            }
373        }
374        else if (!_sites.equals(other._sites))
375        {
376            return false;
377        }
378        if (_tagAutoposting != other._tagAutoposting)
379        {
380            return false;
381        }
382        if (_tagIds == null)
383        {
384            if (other._tagIds != null)
385            {
386                return false;
387            }
388        }
389        else if (!_tagIds.equals(other._tagIds))
390        {
391            return false;
392        }
393        return true;
394    }
395}