001/*
002 *  Copyright 2017 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.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.List;
022import java.util.function.Predicate;
023import java.util.stream.Collector;
024import java.util.stream.Collectors;
025
026import org.apache.commons.lang3.StringUtils;
027
028import org.ametys.core.util.LambdaUtils;
029
030/**
031 * Represents a search {@link Query} corresponding to the logical "and" between several other queries.
032 */
033public class AndQuery implements Query
034{
035    
036    /** The list of queries. The queries on this list are distinct. */
037    protected List<Query> _queries;
038    /** If a query is empty should just ignore it, or return an empty AndQuery */
039    protected boolean _ignoreEmptyQueries;
040    
041    /**
042     * Build an AndQuery object.
043     * @param queries the queries.
044     */
045    public AndQuery(Query... queries)
046    {
047        this(true, queries);
048    }
049    
050    /**
051     * Build an AndQuery object.
052     * @param ignoreEmptyQueries If a query is empty should just ignore it, or return an empty AndQuery
053     * @param queries the queries.
054     */
055    public AndQuery(boolean ignoreEmptyQueries, Query... queries)
056    {
057        this(ignoreEmptyQueries, Arrays.asList(queries));
058    }
059    
060    /**
061     * Build an AndQuery object.
062     * @param queries the queries as a Collection.
063     */
064    public AndQuery(Collection<Query> queries)
065    {
066        this(true, queries);
067    }
068    
069    /**
070     * Build an AndQuery object.
071     * @param ignoreEmptyQueries If a query is empty should just ignore it, or return an empty AndQuery
072     * @param queries the queries as a Collection.
073     */
074    public AndQuery(boolean ignoreEmptyQueries, Collection<Query> queries)
075    {
076        _queries = queries.stream().distinct().collect(Collectors.toList());
077        _ignoreEmptyQueries = ignoreEmptyQueries;
078    }
079    
080    /**
081     * Returns a {@link Collector} wih collects {@link Query Queries} into an AND query
082     * @return a {@link Collector} wih collects {@link Query Queries} into an AND query
083     */
084    public static Collector<Query, ?, AndQuery> collector()
085    {
086        return LambdaUtils.Collectors.withListAccumulation(AndQuery::new);
087    }
088    
089    /**
090     * Get the list of queries in this "and".
091     * @return the list of queries.
092     */
093    public List<Query> getQueries()
094    {
095        return Collections.unmodifiableList(_queries);
096    }
097    
098    @Override
099    public String build() throws QuerySyntaxException
100    {
101        Predicate<Query> isMatchAll = MatchAllQuery.class::isInstance;
102        List<Query> queriesWithoutMatchAll = _queries
103                .stream()
104                .filter(isMatchAll.negate())
105                .collect(Collectors.toList());
106        if (!_queries.isEmpty() && queriesWithoutMatchAll.isEmpty())
107        {
108            // special case where all are MatchAll
109            return new MatchAllQuery().build();
110        }
111        
112        boolean isFirst = true;
113        StringBuilder sb = new StringBuilder();
114        
115        for (Query subQuery : queriesWithoutMatchAll)
116        {
117            if (subQuery instanceof MatchNoneQuery)
118            {
119                // short-circuit => no doc will match
120                return new MatchNoneQuery().build();
121            }
122            else if (subQuery != null)
123            {
124                String exprAsString = subQuery.build();
125                if (StringUtils.isNotBlank(exprAsString))
126                {
127                    if (!isFirst)
128                    {
129                        sb.append(" AND ");
130                    }
131                    sb.append("(").append(exprAsString).append(")");
132                    isFirst = false;
133                }    
134                else if (!_ignoreEmptyQueries)
135                {
136                    return "";
137                }
138            }
139        }
140        
141        if (isFirst)
142        {
143            return "";
144        }
145        else
146        {
147            return sb.toString();
148        }
149    }
150    
151    @Override
152    public String toString(int indent)
153    {
154        final String andLineIndent = StringUtils.repeat(' ', indent);
155        final int subIndent = indent + 2;
156        final String subQueries = _queries
157                .stream()
158                .map(sq -> sq.toString(subIndent))
159                .collect(Collectors.joining("\n"));
160        return andLineIndent + "[AND]\n" + subQueries + "\n" + andLineIndent + "[/AND]";
161    }
162    
163    @Override
164    public int hashCode()
165    {
166        final int prime = 31;
167        int result = 1;
168        result = prime * result + (_ignoreEmptyQueries ? 1231 : 1237);
169        result = prime * result + ((_queries == null) ? 0 : _queries.hashCode());
170        return result;
171    }
172
173    @Override
174    public boolean equals(Object obj)
175    {
176        if (this == obj)
177        {
178            return true;
179        }
180        if (obj == null)
181        {
182            return false;
183        }
184        if (getClass() != obj.getClass())
185        {
186            return false;
187        }
188        AndQuery other = (AndQuery) obj;
189        if (_ignoreEmptyQueries != other._ignoreEmptyQueries)
190        {
191            return false;
192        }
193        if (_queries == null)
194        {
195            if (other._queries != null)
196            {
197                return false;
198            }
199        }
200        else if (!_queries.equals(other._queries))
201        {
202            return false;
203        }
204        return true;
205    }
206}