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