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.Optional; 026import java.util.Set; 027import java.util.function.Predicate; 028import java.util.stream.Collector; 029import java.util.stream.Collectors; 030 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 "or" between several other queries. 037 */ 038public class OrQuery implements Query 039{ 040 /** The list of queries. The queries on this list are distinct. */ 041 protected List<Query> _queries; 042 043 /** 044 * Build an OrQuery object. 045 * @param queries the queries. 046 */ 047 public OrQuery(Query... queries) 048 { 049 this(Arrays.asList(queries)); 050 } 051 052 /** 053 * Build an OrQuery object. 054 * @param queries the queries as a Collection. 055 */ 056 public OrQuery(Collection<Query> queries) 057 { 058 _queries = queries.stream().distinct().toList(); 059 } 060 061 /** 062 * Returns a {@link Collector} which collects {@link Query Queries} into an OR query 063 * @return a {@link Collector} which collects {@link Query Queries} into an OR query 064 */ 065 public static Collector<Query, ?, OrQuery> collector() 066 { 067 return LambdaUtils.Collectors.withListAccumulation(OrQuery::new); 068 } 069 070 /** 071 * Get the list of queries in this "or". 072 * @return the list of queries. 073 */ 074 public List<Query> getQueries() 075 { 076 return Collections.unmodifiableList(_queries); 077 } 078 079 @Override 080 public String build() throws QuerySyntaxException 081 { 082 Predicate<Query> isMatchNone = MatchNoneQuery.class::isInstance; 083 List<Query> queriesWithoutMatchNone = _queries 084 .stream() 085 .filter(isMatchNone.negate()) 086 .collect(Collectors.toList()); 087 if (!_queries.isEmpty() && queriesWithoutMatchNone.isEmpty()) 088 { 089 // special case where all are MatchNone 090 return new MatchNoneQuery().build(); 091 } 092 093 boolean isFirst = true; 094 StringBuilder sb = new StringBuilder(); 095 096 for (Query subQuery : queriesWithoutMatchNone) 097 { 098 if (subQuery instanceof MatchAllQuery) 099 { 100 // short-circuit => all docs will match 101 return new MatchAllQuery().build(); 102 } 103 else if (subQuery != null) 104 { 105 String exprAsString = subQuery.build(); 106 if (StringUtils.isNotBlank(exprAsString)) 107 { 108 if (!isFirst) 109 { 110 sb.append(" OR "); 111 } 112 sb.append("(").append(exprAsString).append(")"); 113 isFirst = false; 114 } 115 } 116 } 117 118 if (isFirst) 119 { 120 return ""; 121 } 122 else 123 { 124 return sb.toString(); 125 } 126 } 127 128 public Optional<Object> buildAsJson() throws QuerySyntaxException 129 { 130 Optional<Query> rewrittenQueryAsOptional = rewrite(); 131 if (rewrittenQueryAsOptional.isEmpty()) 132 { 133 return Optional.empty(); 134 } 135 136 Query rewrittenQuery = rewrittenQueryAsOptional.get(); 137 if (!(rewrittenQuery instanceof OrQuery orQuery)) 138 { 139 return rewrittenQuery.buildAsJson(); 140 } 141 142 List<Query> rewrittenQueries = orQuery.getQueries(); 143 144 // filter out empty queries 145 List<Object> builtQueries = rewrittenQueries.stream() 146 .map(LambdaUtils.wrap(Query::buildAsJson)) 147 .flatMap(Optional::stream) 148 .filter(q -> !(q instanceof String s) || !StringUtils.isBlank(s)) 149 .toList(); 150 151 if (builtQueries.isEmpty()) 152 { 153 return Optional.empty(); 154 } 155 156 if (builtQueries.size() == 1) 157 { 158 return Optional.of(builtQueries.get(0)); 159 } 160 161 return Optional.of(Map.of("bool", Map.of(Query.BOOL_SHOULD, builtQueries))); 162 } 163 164 public Optional<Query> rewrite() 165 { 166 List<Query> rewrittenQueries = _queries.stream() 167 .filter(Objects::nonNull) 168 .map(Query::rewrite) 169 .flatMap(Optional::stream) 170 .toList(); 171 172 Set<Query> queries = new HashSet<>(); 173 for (Query q : rewrittenQueries) 174 { 175 // do not use instanceof here, as we don't want to match OrQuery subclasses 176 if (q.getClass().getName().equals(OrQuery.class.getName())) 177 { 178 queries.addAll(((OrQuery) q).getQueries()); 179 } 180 else if (q instanceof MatchAllQuery) 181 { 182 return Optional.of(new MatchAllQuery()); 183 } 184 else if (!(q instanceof MatchNoneQuery)) 185 { 186 queries.add(q); 187 } 188 } 189 190 if (queries.isEmpty()) 191 { 192 return Optional.empty(); 193 } 194 195 if (queries.size() == 1) 196 { 197 return Optional.of(queries.iterator().next()); 198 } 199 200 return Optional.of(new OrQuery(queries)); 201 } 202 203 @Override 204 public String toString(int indent) 205 { 206 String tagName = _tagNameForToString(); 207 final String orLineIndent = StringUtils.repeat(' ', indent); 208 final int subIndent = indent + 2; 209 final String subQueries = _queries 210 .stream() 211 .filter(Objects::nonNull) 212 .map(sq -> sq.toString(subIndent)) 213 .collect(Collectors.joining("\n")); 214 return orLineIndent + "[" + tagName + "]\n" + subQueries + "\n" + orLineIndent + "[/" + tagName + "]"; 215 } 216 217 /** 218 * The tag name for {@link #toString(int)} debug method. 219 * @return The tag name for {@link #toString(int)} debug method. 220 */ 221 protected String _tagNameForToString() 222 { 223 return "OR"; 224 } 225 226 @Override 227 public int hashCode() 228 { 229 return _queries.hashCode(); 230 } 231 232 @Override 233 public boolean equals(Object obj) 234 { 235 if (this == obj) 236 { 237 return true; 238 } 239 240 if (obj == null || getClass() != obj.getClass()) 241 { 242 return false; 243 } 244 245 OrQuery other = (OrQuery) obj; 246 return Objects.equals(_queries, other._queries); 247 } 248}