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