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