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.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 Predicate<Query> isMatchNone = MatchNoneQuery.class::isInstance; 082 List<Query> queriesWithoutMatchNone = _queries 083 .stream() 084 .filter(isMatchNone.negate()) 085 .collect(Collectors.toList()); 086 if (!_queries.isEmpty() && queriesWithoutMatchNone.isEmpty()) 087 { 088 // special case where all are MatchNone 089 return new MatchNoneQuery().build(); 090 } 091 092 boolean isFirst = true; 093 StringBuilder sb = new StringBuilder(); 094 095 for (Query subQuery : queriesWithoutMatchNone) 096 { 097 if (subQuery instanceof MatchAllQuery) 098 { 099 // short-circuit => all docs will match 100 return new MatchAllQuery().build(); 101 } 102 else if (subQuery != null) 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 117 if (isFirst) 118 { 119 return ""; 120 } 121 else 122 { 123 return sb.toString(); 124 } 125 } 126 127 public Object buildAsJson() throws QuerySyntaxException 128 { 129 Query rewrittenQuery = rewrite(); 130 131 if (!(rewrittenQuery instanceof OrQuery orQuery)) 132 { 133 return rewrittenQuery.buildAsJson(); 134 } 135 136 List<Query> rewrittenQueries = orQuery.getQueries(); 137 138 // filter out empty queries 139 List<Object> builtQueries = rewrittenQueries.stream() 140 .map(LambdaUtils.wrap(Query::buildAsJson)) 141 .filter(q -> !(q instanceof String s) || !StringUtils.isBlank(s)) 142 .toList(); 143 144 if (builtQueries.isEmpty()) 145 { 146 return new MatchAllQuery().buildAsJson(); 147 } 148 149 if (builtQueries.size() == 1) 150 { 151 return builtQueries.get(0); 152 } 153 154 return Map.of("bool", Map.of(Query.BOOL_SHOULD, builtQueries)); 155 } 156 157 public Query rewrite() 158 { 159 List<Query> rewrittenQueries = _queries.stream() 160 .filter(Objects::nonNull) 161 .map(Query::rewrite).toList(); 162 163 Set<Query> queries = new HashSet<>(); 164 for (Query q : rewrittenQueries) 165 { 166 // do not use instanceof here, as we don't want to match OrQuery subclasses 167 if (q.getClass().getName().equals(OrQuery.class.getName())) 168 { 169 queries.addAll(((OrQuery) q).getQueries()); 170 } 171 else if (q instanceof MatchAllQuery) 172 { 173 return new MatchAllQuery(); 174 } 175 else if (!(q instanceof MatchNoneQuery)) 176 { 177 queries.add(q); 178 } 179 } 180 181 if (queries.isEmpty()) 182 { 183 return new MatchAllQuery(); 184 } 185 186 if (queries.size() == 1) 187 { 188 return queries.iterator().next(); 189 } 190 191 return new OrQuery(queries); 192 } 193 194 @Override 195 public String toString(int indent) 196 { 197 String tagName = _tagNameForToString(); 198 final String orLineIndent = StringUtils.repeat(' ', indent); 199 final int subIndent = indent + 2; 200 final String subQueries = _queries 201 .stream() 202 .filter(Objects::nonNull) 203 .map(sq -> sq.toString(subIndent)) 204 .collect(Collectors.joining("\n")); 205 return orLineIndent + "[" + tagName + "]\n" + subQueries + "\n" + orLineIndent + "[/" + tagName + "]"; 206 } 207 208 /** 209 * The tag name for {@link #toString(int)} debug method. 210 * @return The tag name for {@link #toString(int)} debug method. 211 */ 212 protected String _tagNameForToString() 213 { 214 return "OR"; 215 } 216 217 @Override 218 public int hashCode() 219 { 220 return _queries.hashCode(); 221 } 222 223 @Override 224 public boolean equals(Object obj) 225 { 226 if (this == obj) 227 { 228 return true; 229 } 230 231 if (obj == null || getClass() != obj.getClass()) 232 { 233 return false; 234 } 235 236 OrQuery other = (OrQuery) obj; 237 return Objects.equals(_queries, other._queries); 238 } 239}