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