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