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)
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 (_value instanceof Map<?, ?>)
140        {
141            Map<String, Object> mapValues = (Map<String, Object>) this._value;
142            Object values = mapValues.get("value");
143            boolean autoposting = (boolean) mapValues.get("autoposting");
144            if (autoposting)
145            {
146                if (_resolveChildren)
147                {
148                    Query query = null;
149                    if (values instanceof Collection<?>)
150                    {
151                        query = _getChildrenOrSelfQuery((Collection<String>) values, _operator);
152                    }
153                    else
154                    {
155                        query = _getChildrenOrSelfQuery(Collections.singletonList((String) values), _operator);
156                    }
157                    
158                    return query.build();
159                }
160                else
161                {
162                    Query subQuery = null;
163                    if (values instanceof Collection<?>)
164                    {
165                        subQuery = _createRefParentsQuery((Collection<String>) values, _operator);
166                    }
167                    else
168                    {
169                        subQuery = _createRefParentsQuery(Collections.singletonList((String) values), _operator);
170                    }
171                    
172                    Query joinQuery = new JoinQuery(subQuery, _fieldPath);
173                    return joinQuery.build();
174                }
175                
176            }
177            else
178            {
179                Query query = _createQueryString(values, _operator);
180                return query.build();
181            }
182        }
183        else 
184        {
185            Query query = _createQueryString(_value, _operator);
186            return query.build();
187        }
188    }
189    
190    private Query _getChildrenOrSelfQuery(Collection<String> values, Operator operator)
191    {
192        List<Query> queries = new ArrayList<>();
193        
194        if (_isAndMultipleOperand)
195        {
196            for (String value: values)
197            {
198                Set<String> childrenAndSelf = _getChildrenOrSelf(value);
199                
200                List<Query> subQueries = new ArrayList<>();
201                for (String id : childrenAndSelf)
202                {
203                    subQueries.add(_createQueryString(id, operator));
204                }
205                
206                queries.add(new OrQuery(subQueries));
207            }
208            
209            return new AndQuery(queries);
210        }
211        else
212        {
213            Set<String> allChildrenAndSelf = new HashSet<>();
214            for (String value: values)
215            {
216                allChildrenAndSelf.addAll(_getChildrenOrSelf(value));
217            }
218            
219            List<Query> subQueries = new ArrayList<>();
220            for (String id : allChildrenAndSelf)
221            {
222                subQueries.add(_createQueryString(id, operator));
223            }
224            
225            return new OrQuery(subQueries);
226        }
227    }
228    
229    private Set<String> _getChildrenOrSelf(String id)
230    {
231        Content content = _resolver.resolveById(id);
232        
233        Set<String> childrenOrSelf = new HashSet<>();
234        childrenOrSelf.add(id);
235        if (_contentHelper.isReferenceTable(content) && _refTableHelper.isHierarchical(content))
236        {
237            childrenOrSelf.addAll(_refTableHelper.getAllChildren(content));
238        }
239        
240        return childrenOrSelf;
241    }
242    
243    private Query _createQueryString(Object value, Operator operator)
244    {
245        String fieldPath = getFieldPath();
246        
247        if (value instanceof Collection<?>)
248        {
249            @SuppressWarnings("unchecked")
250            List<String> stringValues = (List<String>) value;
251            
252            List<Query> queries = new ArrayList<>();
253            for (String val : stringValues)
254            {
255                queries.add(new StringQuery(fieldPath, operator, val, null));
256            }
257  
258            return _isAndMultipleOperand ? new AndQuery(queries) : new OrQuery(queries);
259        }
260        else
261        {
262            return new StringQuery(fieldPath, operator, (String) value, null);
263        }
264    }
265    
266    /**
267     * Create a subQuery in order to create the join query
268     * @param values the values to add in the query
269     * @param operator the operator
270     * @return The non-null query 
271     */
272    private Query _createRefParentsQuery(Collection<String> values, Operator operator)
273    {
274        List<Query> queries = new ArrayList<>();
275        for (String val: values)
276        {
277            queries.add(() -> 
278            {
279                StringBuilder q = new StringBuilder();
280                if (Operator.NE == operator)
281                {
282                    NotQuery.appendNegation(q).append('(');
283                }
284                q.append(ParentContentSearchField.NAME).append(":").append(ClientUtils.escapeQueryChars(val));
285                if (Operator.NE == operator)
286                {
287                    q.append(')');
288                }
289                return q.toString();
290            });
291        }
292        
293        if (queries.size() == 1)
294        {
295            return queries.get(0);
296        }
297        else
298        {
299            return _isAndMultipleOperand ? new AndQuery(queries) : new OrQuery(queries);
300        }
301    }
302
303    @Override
304    public int hashCode()
305    {
306        final int prime = 31;
307        int result = super.hashCode();
308        result = prime * result + Objects.hash(_isAndMultipleOperand, _operator, _resolveChildren, _value);
309        return result;
310    }
311
312    @Override
313    public boolean equals(Object obj)
314    {
315        if (this == obj)
316        {
317            return true;
318        }
319        if (!super.equals(obj))
320        {
321            return false;
322        }
323        if (getClass() != obj.getClass())
324        {
325            return false;
326        }
327        ContentQuery other = (ContentQuery) obj;
328        return _isAndMultipleOperand == other._isAndMultipleOperand && _operator == other._operator && _resolveChildren == other._resolveChildren && Objects.equals(_value, other._value);
329    }
330}