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.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Set; 026import java.util.function.Predicate; 027import java.util.stream.Collector; 028import java.util.stream.Collectors; 029 030import org.apache.commons.collections4.map.HashedMap; 031import org.apache.commons.lang3.StringUtils; 032 033import org.ametys.core.util.LambdaUtils; 034 035/** 036 * Represents a search {@link Query} corresponding to the logical "and" between several other queries. 037 */ 038public class AndQuery implements Query 039{ 040 private static final Predicate<Object> __NOT_EMPTY_QUERY = q -> !(q instanceof String s) || !StringUtils.isBlank(s); 041 042 /** The list of queries. The queries on this list are distinct. */ 043 protected List<Query> _queries; 044 045 /** 046 * Build an AndQuery object. 047 * @param queries the queries. 048 */ 049 public AndQuery(Query... queries) 050 { 051 this(Arrays.asList(queries)); 052 } 053 054 /** 055 * Build an AndQuery object. 056 * @param queries the queries as a Collection. 057 */ 058 public AndQuery(Collection<Query> queries) 059 { 060 _queries = queries.stream().distinct().toList(); 061 } 062 063 /** 064 * Returns a {@link Collector} which collects {@link Query Queries} into an AND query 065 * @return a {@link Collector} which collects {@link Query Queries} into an AND query 066 */ 067 public static Collector<Query, ?, AndQuery> collector() 068 { 069 return LambdaUtils.Collectors.withListAccumulation(AndQuery::new); 070 } 071 072 /** 073 * Get the list of queries in this "and". 074 * @return the list of queries. 075 */ 076 public List<Query> getQueries() 077 { 078 return Collections.unmodifiableList(_queries); 079 } 080 081 @Override 082 public String build() throws QuerySyntaxException 083 { 084 Predicate<Query> isMatchAll = MatchAllQuery.class::isInstance; 085 List<Query> queriesWithoutMatchAll = _queries 086 .stream() 087 .filter(isMatchAll.negate()) 088 .collect(Collectors.toList()); 089 if (!_queries.isEmpty() && queriesWithoutMatchAll.isEmpty()) 090 { 091 // special case where all are MatchAll 092 return new MatchAllQuery().build(); 093 } 094 095 boolean isFirst = true; 096 StringBuilder sb = new StringBuilder(); 097 098 for (Query subQuery : queriesWithoutMatchAll) 099 { 100 if (subQuery instanceof MatchNoneQuery) 101 { 102 // short-circuit => no doc will match 103 return new MatchNoneQuery().build(); 104 } 105 else if (subQuery != null) 106 { 107 String exprAsString = subQuery.build(); 108 if (StringUtils.isNotBlank(exprAsString)) 109 { 110 if (!isFirst) 111 { 112 sb.append(" AND "); 113 } 114 sb.append("(").append(exprAsString).append(")"); 115 isFirst = false; 116 } 117 } 118 } 119 120 if (isFirst) 121 { 122 return ""; 123 } 124 else 125 { 126 return sb.toString(); 127 } 128 } 129 130 public Object buildAsJson() throws QuerySyntaxException 131 { 132 Query rewrittenQuery = rewrite(); 133 134 if (!(rewrittenQuery instanceof AndQuery andQuery)) 135 { 136 return rewrittenQuery.buildAsJson(); 137 } 138 139 List<Query> rewrittenQueries = andQuery.getQueries(); 140 141 Set<Query> mustQueries = new HashSet<>(); 142 Set<Query> filterQueries = new HashSet<>(); 143 Set<Query> mustNotQueries = new HashSet<>(); 144 145 _dispatchClauses(rewrittenQueries, mustQueries, filterQueries, mustNotQueries); 146 147 boolean matchNone = mustQueries.stream().anyMatch(MatchNoneQuery.class::isInstance) 148 || filterQueries.stream().anyMatch(MatchNoneQuery.class::isInstance) 149 || mustNotQueries.stream().anyMatch(MatchAllQuery.class::isInstance); 150 151 if (matchNone) 152 { 153 return new MatchNoneQuery().buildAsJson(); 154 } 155 156 // filter out empty queries 157 List<Object> builtMustQueries = mustQueries.stream() 158 .map(LambdaUtils.wrap(Query::buildAsJson)) 159 .filter(__NOT_EMPTY_QUERY) 160 .toList(); 161 162 List<Object> builtFilterQueries = filterQueries.stream() 163 .map(LambdaUtils.wrap(Query::buildAsJson)) 164 .filter(__NOT_EMPTY_QUERY) 165 .toList(); 166 167 List<Object> builtMustNotQueries = mustNotQueries.stream() 168 .map(LambdaUtils.wrap(Query::buildAsJson)) 169 .filter(__NOT_EMPTY_QUERY) 170 .toList(); 171 172 boolean noPositiveQueries = builtMustQueries.isEmpty() && builtFilterQueries.isEmpty(); 173 174 if (noPositiveQueries && builtMustNotQueries.isEmpty()) 175 { 176 return new MatchAllQuery().buildAsJson(); 177 } 178 179 if (builtMustQueries.size() == 1 && builtFilterQueries.isEmpty() && builtMustNotQueries.isEmpty()) 180 { 181 return builtMustQueries.get(0); 182 } 183 184 Map<String, Object> clauses = new HashedMap<>(); 185 if (!builtMustQueries.isEmpty()) 186 { 187 clauses.put(Query.BOOL_MUST, builtMustQueries); 188 } 189 190 if (!builtFilterQueries.isEmpty()) 191 { 192 clauses.put(Query.BOOL_FILTER, builtFilterQueries); 193 } 194 195 if (!builtMustNotQueries.isEmpty()) 196 { 197 if (noPositiveQueries) 198 { 199 clauses.put(Query.BOOL_MUST, "*:*"); 200 } 201 202 clauses.put(Query.BOOL_MUST_NOT, builtMustNotQueries); 203 } 204 205 return Map.of("bool", clauses); 206 } 207 208 private void _dispatchClauses(List<Query> queries, Set<Query> mustQueries, Set<Query> filterQueries, Set<Query> mustNotQueries) 209 { 210 // dispatch queries by nature, while discarding useless clauses 211 for (Query q : queries) 212 { 213 if (q instanceof ConstantNilScoreQuery filterQuery) 214 { 215 Query filteredQuery = filterQuery.getSubQuery(); 216 if (!(filteredQuery instanceof MatchAllQuery)) 217 { 218 filterQueries.add(filteredQuery); 219 } 220 } 221 else if (q instanceof NotQuery notQuery) 222 { 223 Query negatedQuery = notQuery.getSubQuery(); 224 if (negatedQuery instanceof OrQuery orQuery) 225 { 226 mustNotQueries.addAll(orQuery.getQueries()); 227 } 228 else if (!(negatedQuery instanceof MatchNoneQuery)) 229 { 230 mustNotQueries.add(negatedQuery); 231 } 232 } 233 else if (!(q instanceof MatchAllQuery)) 234 { 235 mustQueries.add(q); 236 } 237 } 238 } 239 240 public Query rewrite() 241 { 242 List<Query> rewrittenQueries = _queries.stream().map(Query::rewrite).toList(); 243 244 Set<Query> queries = new HashSet<>(); 245 for (Query q : rewrittenQueries) 246 { 247 // do not use instanceof here, as we don't want to match AndQuery subclasses 248 if (q.getClass().getName().equals(AndQuery.class.getName())) 249 { 250 queries.addAll(((AndQuery) q).getQueries()); 251 } 252 else if (q instanceof MatchNoneQuery) 253 { 254 return new MatchNoneQuery(); 255 } 256 else if (!(q instanceof MatchAllQuery)) 257 { 258 queries.add(q); 259 } 260 } 261 262 if (queries.isEmpty()) 263 { 264 return new MatchAllQuery(); 265 } 266 267 if (queries.size() == 1) 268 { 269 return queries.iterator().next(); 270 } 271 272 return new AndQuery(queries); 273 } 274 275 @Override 276 public String toString(int indent) 277 { 278 final String andLineIndent = StringUtils.repeat(' ', indent); 279 final int subIndent = indent + 2; 280 final String subQueries = _queries 281 .stream() 282 .filter(Objects::nonNull) 283 .map(sq -> sq.toString(subIndent)) 284 .collect(Collectors.joining("\n")); 285 return andLineIndent + "[AND]\n" + subQueries + "\n" + andLineIndent + "[/AND]"; 286 } 287 288 @Override 289 public int hashCode() 290 { 291 return _queries.hashCode(); 292 } 293 294 @Override 295 public boolean equals(Object obj) 296 { 297 if (this == obj) 298 { 299 return true; 300 } 301 302 if (obj == null || getClass() != obj.getClass()) 303 { 304 return false; 305 } 306 307 AndQuery other = (AndQuery) obj; 308 return Objects.equals(_queries, other._queries); 309 } 310}