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