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}