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.cms.search.query;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Set;
026
027import org.apache.solr.client.solrj.util.ClientUtils;
028
029import org.ametys.cms.content.ContentHelper;
030import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper;
031import org.ametys.cms.content.referencetable.search.ParentContentSearchField;
032import org.ametys.cms.repository.Content;
033import org.ametys.plugins.repository.AmetysObjectResolver;
034
035/**
036 * Represents a {@link Query} testing a content field.
037 */
038public class ContentQuery extends AbstractOperatorQuery<Object>
039{
040    /** The resolver to resolve the results. */
041    protected AmetysObjectResolver _resolver;
042    /** The extension point. */
043    protected ContentHelper _contentHelper;
044    /** <code>true</code> if the query should be executed with an AndQuery. */
045    protected boolean _isAndMultipleOperand;
046    /** The helper to resolve the reference tables. */
047    protected HierarchicalReferenceTablesHelper _refTableHelper;
048    /** <code>true</code> if the query should be executed by resolving the children. */
049    protected boolean _resolveChildren;
050
051    /**
052     * Build a content query.
053     * @param fieldPath the field's path
054     * @param operator the operator
055     * @param value the value
056     * @param resolver the resolver
057     * @param contentHelper the content helper
058     */
059    public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper)
060    {
061        this(fieldPath, operator, value, resolver, contentHelper, false);
062    }
063    
064    /**
065     * Build a content query by testing the operand.
066     * @param fieldPath the field's path
067     * @param operator the operator
068     * @param value the value
069     * @param resolver the resolver
070     * @param contentHelper the content helper
071     * @param isAndMultipleOperand 'true' to execute an AndQuery, OrQuery otherwise 
072     */
073    public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper, boolean isAndMultipleOperand)
074    {
075        this(fieldPath, operator, value, resolver, contentHelper, null, false, isAndMultipleOperand);
076    }
077    
078    /**
079     * Build a content query by testing if the ancestors should be resolved.
080     * @param fieldPath the field's path
081     * @param operator the operator
082     * @param value the value
083     * @param resolver the resolver
084     * @param contentHelper the content helper
085     * @param refTableHelper The helper to resolve the ancestors
086     * @param resolveChildren 'true' to execute an OrQuery on the ancestors and itself, 'false' will execute a join request
087     */
088    public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper, HierarchicalReferenceTablesHelper refTableHelper, boolean resolveChildren)
089    {
090        this(fieldPath, operator, value, resolver, contentHelper, refTableHelper, resolveChildren, false);
091    }
092    
093    /**
094     * Build a content query by testing the operand and if the ancestors should be resolved.
095     * @param fieldPath the field's path
096     * @param operator The operator. Only {@link Query.Operator#EQ} and {@link Query.Operator#NE} are allowed.
097     * @param value the value
098     * @param resolver the resolver
099     * @param contentHelper the content helper
100     * @param refTableHelper The helper to resolve the ancestors
101     * @param resolveChildren 'true' to execute an OrQuery on the ancestors and itself, 'false' will execute a join request
102     * @param isAndMultipleOperand 'true' to execute an AndQuery, OrQuery otherwise
103     */
104    public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper, HierarchicalReferenceTablesHelper refTableHelper, boolean resolveChildren, boolean isAndMultipleOperand)
105    {
106        super(fieldPath, operator, value);
107        if (Operator.EQ != operator && Operator.NE != operator && Operator.EXISTS != operator)
108        {
109            throw new IllegalArgumentException("Test operator '" + operator + "' can't be used for a content query.");
110        }
111        
112        this._resolver = resolver;
113        this._contentHelper = contentHelper;
114        this._refTableHelper = refTableHelper;
115        this._resolveChildren = resolveChildren;
116        this._isAndMultipleOperand = isAndMultipleOperand;
117    }
118
119    @Override
120    @SuppressWarnings("unchecked")
121    public String build() throws QuerySyntaxException
122    {
123        Operator operator = getOperator();
124        Object value = getValue();
125        
126        if (operator == Operator.EXISTS)
127        {
128            StringBuilder query = new StringBuilder();
129            query.append(getFieldName()).append("_s:").append(QueryHelper.EXISTS_VALUE);
130            return query.toString();
131        }
132        
133        if (value instanceof Map)
134        {
135            Map<String, Object> mapValues = (Map<String, Object>) value;
136            
137            Object values = mapValues.get("value");
138            boolean autoposting = (boolean) mapValues.get("autoposting");
139            if (autoposting)
140            {
141                if (_resolveChildren)
142                {
143                    Query query = null;
144                    if (values instanceof Collection<?>)
145                    {
146                        query = _getChildrenOrSelfQuery((Collection<String>) values, operator);
147                    }
148                    else
149                    {
150                        query = _getChildrenOrSelfQuery(Collections.singletonList((String) values), operator);
151                    }
152                    
153                    return query.build();
154                }
155                else
156                {
157                    Query subQuery = null;
158                    if (values instanceof Collection<?>)
159                    {
160                        subQuery = _createRefParentsQuery((Collection<String>) values, operator);
161                    }
162                    else
163                    {
164                        subQuery = _createRefParentsQuery(Collections.singletonList((String) values), operator);
165                    }
166                    
167                    Query joinQuery = new JoinQuery(subQuery, getFieldName());
168                    return joinQuery.build();
169                }
170                
171            }
172            else
173            {
174                Query query = _createQueryString(values, operator);
175                return query.build();
176            }
177        }
178        else 
179        {
180            Query query = _createQueryString(value, operator);
181            return query.build();
182        }
183    }
184    
185    private Query _getChildrenOrSelfQuery(Collection<String> values, Operator operator)
186    {
187        List<Query> queries = new ArrayList<>();
188        
189        if (_isAndMultipleOperand)
190        {
191            for (String value: values)
192            {
193                Set<String> childrenAndSelf = _getChildrenOrSelf(value);
194                
195                List<Query> subQueries = new ArrayList<>();
196                for (String id : childrenAndSelf)
197                {
198                    subQueries.add(_createQueryString(id, operator));
199                }
200                
201                queries.add(new OrQuery(subQueries));
202            }
203            
204            return new AndQuery(queries);
205        }
206        else
207        {
208            Set<String> allChildrenAndSelf = new HashSet<>();
209            for (String value: values)
210            {
211                allChildrenAndSelf.addAll(_getChildrenOrSelf(value));
212            }
213            
214            List<Query> subQueries = new ArrayList<>();
215            for (String id : allChildrenAndSelf)
216            {
217                subQueries.add(_createQueryString(id, operator));
218            }
219            
220            return new OrQuery(subQueries);
221        }
222    }
223    
224    private Set<String> _getChildrenOrSelf(String id)
225    {
226        Content content = _resolver.resolveById(id);
227        
228        Set<String> childrenOrSelf = new HashSet<>();
229        childrenOrSelf.add(id);
230        if (_contentHelper.isReferenceTable(content) && _refTableHelper.isHierarchical(content))
231        {
232            childrenOrSelf.addAll(_refTableHelper.getAllChildren(content));
233        }
234        
235        return childrenOrSelf;
236    }
237    
238    private Query _createQueryString(Object value, Operator operator)
239    {
240        String fieldPath = getFieldName();
241        
242        if (value instanceof Collection<?>)
243        {
244            @SuppressWarnings("unchecked")
245            List<String> stringValues = (List<String>) value;
246            
247            List<Query> queries = new ArrayList<>();
248            for (String val : stringValues)
249            {
250                queries.add(new StringQuery(fieldPath, operator, val, null));
251            }
252  
253            return _isAndMultipleOperand ? new AndQuery(queries) : new OrQuery(queries);
254        }
255        else
256        {
257            return new StringQuery(fieldPath, operator, (String) value, null);
258        }
259    }
260    
261    /**
262     * Create a subQuery in order to create the join query
263     * @param values the values to add in the query
264     * @param operator the operator
265     * @return The non-null query 
266     */
267    private Query _createRefParentsQuery(Collection<String> values, Operator operator)
268    {
269        List<Query> queries = new ArrayList<>();
270        for (String val: values)
271        {
272            queries.add(() -> 
273            {
274                StringBuilder q = new StringBuilder();
275                if (Operator.NE == operator)
276                {
277                    NotQuery.appendNegation(q).append('(');
278                }
279                q.append(ParentContentSearchField.NAME).append(":").append(ClientUtils.escapeQueryChars(val));
280                if (Operator.NE == operator)
281                {
282                    q.append(')');
283                }
284                return q.toString();
285            });
286        }
287        
288        if (queries.size() == 1)
289        {
290            return queries.get(0);
291        }
292        else
293        {
294            return _isAndMultipleOperand ? new AndQuery(queries) : new OrQuery(queries);
295        }
296    }
297
298    @Override
299    public int hashCode()
300    {
301        final int prime = 31;
302        int result = super.hashCode();
303        result = prime * result + Objects.hash(_isAndMultipleOperand, _resolveChildren);
304        return result;
305    }
306
307    @Override
308    public boolean equals(Object obj)
309    {
310        if (!super.equals(obj))
311        {
312            return false;
313        }
314        
315        ContentQuery other = (ContentQuery) obj;
316        return _isAndMultipleOperand == other._isAndMultipleOperand && _resolveChildren == other._resolveChildren;
317    }
318}