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 "or" between several other queries.
032 */
033public class OrQuery implements Query
034{
035    
036    /** The list of queries. The queries on this list are distinct. */
037    protected List<Query> _queries;
038    
039    /**
040     * Build an OrQuery object.
041     * @param queries the queries.
042     */
043    public OrQuery(Query... queries)
044    {
045        this(Arrays.asList(queries));
046    }
047    
048    /**
049     * Build an OrQuery object.
050     * @param queries the queries as a Collection.
051     */
052    public OrQuery(Collection<Query> queries)
053    {
054        _queries = queries.stream().distinct().collect(Collectors.toList());
055    }
056    
057    /**
058     * Returns a {@link Collector} wih collects {@link Query Queries} into an OR query
059     * @return a {@link Collector} wih collects {@link Query Queries} into an OR query
060     */
061    public static Collector<Query, ?, OrQuery> collector()
062    {
063        return LambdaUtils.Collectors.withListAccumulation(OrQuery::new);
064    }
065    
066    /**
067     * Get the list of queries in this "or".
068     * @return the list of queries.
069     */
070    public List<Query> getQueries()
071    {
072        return Collections.unmodifiableList(_queries);
073    }
074    
075    @Override
076    public String build() throws QuerySyntaxException
077    {
078        Predicate<Query> isMatchNone = MatchNoneQuery.class::isInstance;
079        List<Query> queriesWithoutMatchNone = _queries
080                .stream()
081                .filter(isMatchNone.negate())
082                .collect(Collectors.toList());
083        if (!_queries.isEmpty() && queriesWithoutMatchNone.isEmpty())
084        {
085            // special case where all are MatchNone
086            return new MatchNoneQuery().build();
087        }
088        
089        boolean isFirst = true;
090        StringBuilder sb = new StringBuilder();
091        
092        for (Query subQuery : queriesWithoutMatchNone)
093        {
094            if (subQuery instanceof MatchAllQuery)
095            {
096                // short-circuit => all docs will match
097                return new MatchAllQuery().build();
098            }
099            else if (subQuery != null)
100            {
101                String exprAsString = subQuery.build();
102                if (StringUtils.isNotBlank(exprAsString))
103                {
104                    if (!isFirst)
105                    {
106                        sb.append(" OR ");
107                    }
108                    sb.append("(").append(exprAsString).append(")");
109                    isFirst = false;
110                }
111            }
112        }
113        
114        if (isFirst)
115        {
116            return "";
117        }
118        else
119        {
120            return sb.toString();
121        }
122    }
123    
124    @Override
125    public String toString(int indent)
126    {
127        String tagName = _tagNameForToString();
128        final String orLineIndent = StringUtils.repeat(' ', indent);
129        final int subIndent = indent + 2;
130        final String subQueries = _queries
131                .stream()
132                .map(sq -> sq.toString(subIndent))
133                .collect(Collectors.joining("\n"));
134        return orLineIndent + "[" + tagName + "]\n" + subQueries + "\n" + orLineIndent + "[/" + tagName + "]";
135    }
136    
137    /**
138     * The tag name for {@link #toString(int)} debug method.
139     * @return The tag name for {@link #toString(int)} debug method.
140     */
141    protected String _tagNameForToString()
142    {
143        return "OR";
144    }
145
146    @Override
147    public int hashCode()
148    {
149        final int prime = 31;
150        int result = 1;
151        result = prime * result + ((_queries == null) ? 0 : _queries.hashCode());
152        return result;
153    }
154
155    @Override
156    public boolean equals(Object obj)
157    {
158        if (this == obj)
159        {
160            return true;
161        }
162        if (obj == null)
163        {
164            return false;
165        }
166        if (getClass() != obj.getClass())
167        {
168            return false;
169        }
170        OrQuery other = (OrQuery) obj;
171        if (_queries == null)
172        {
173            if (other._queries != null)
174            {
175                return false;
176            }
177        }
178        else if (!_queries.equals(other._queries))
179        {
180            return false;
181        }
182        return true;
183    }
184}