001/* 002 * Copyright 2018 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.ArrayList; 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; 026 027import org.apache.solr.client.solrj.util.ClientUtils; 028 029import org.ametys.cms.content.ContentHelper; 030import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 031import org.ametys.cms.content.referencetable.search.ParentContentSearchField; 032import org.ametys.cms.repository.Content; 033import org.ametys.plugins.repository.AmetysObjectResolver; 034 035/** 036 * Represents a {@link Query} testing a content field. 037 */ 038public class ContentQuery extends AbstractOperatorQuery<Object> 039{ 040 /** The resolver to resolve the results. */ 041 protected AmetysObjectResolver _resolver; 042 /** The extension point. */ 043 protected ContentHelper _contentHelper; 044 /** <code>true</code> if the query should be executed with an AndQuery. */ 045 protected boolean _isAndMultipleOperand; 046 /** The helper to resolve the reference tables. */ 047 protected HierarchicalReferenceTablesHelper _refTableHelper; 048 /** <code>true</code> if the query should be executed by resolving the children. */ 049 protected boolean _resolveChildren; 050 051 /** 052 * Build a content query. 053 * @param fieldPath the field's path 054 * @param operator the operator 055 * @param value the value 056 * @param resolver the resolver 057 * @param contentHelper the content helper 058 */ 059 public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper) 060 { 061 this(fieldPath, operator, value, resolver, contentHelper, false); 062 } 063 064 /** 065 * Build a content query by testing the operand. 066 * @param fieldPath the field's path 067 * @param operator the operator 068 * @param value the value 069 * @param resolver the resolver 070 * @param contentHelper the content helper 071 * @param isAndMultipleOperand 'true' to execute an AndQuery, OrQuery otherwise 072 */ 073 public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper, boolean isAndMultipleOperand) 074 { 075 this(fieldPath, operator, value, resolver, contentHelper, null, false, isAndMultipleOperand); 076 } 077 078 /** 079 * Build a content query by testing if the ancestors should be resolved. 080 * @param fieldPath the field's path 081 * @param operator the operator 082 * @param value the value 083 * @param resolver the resolver 084 * @param contentHelper the content helper 085 * @param refTableHelper The helper to resolve the ancestors 086 * @param resolveChildren 'true' to execute an OrQuery on the ancestors and itself, 'false' will execute a join request 087 */ 088 public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper, HierarchicalReferenceTablesHelper refTableHelper, boolean resolveChildren) 089 { 090 this(fieldPath, operator, value, resolver, contentHelper, refTableHelper, resolveChildren, false); 091 } 092 093 /** 094 * Build a content query by testing the operand and if the ancestors should be resolved. 095 * @param fieldPath the field's path 096 * @param operator The operator. Only {@link Query.Operator#EQ} and {@link Query.Operator#NE} are allowed. 097 * @param value the value 098 * @param resolver the resolver 099 * @param contentHelper the content helper 100 * @param refTableHelper The helper to resolve the ancestors 101 * @param resolveChildren 'true' to execute an OrQuery on the ancestors and itself, 'false' will execute a join request 102 * @param isAndMultipleOperand 'true' to execute an AndQuery, OrQuery otherwise 103 */ 104 public ContentQuery(String fieldPath, Operator operator, Object value, AmetysObjectResolver resolver, ContentHelper contentHelper, HierarchicalReferenceTablesHelper refTableHelper, boolean resolveChildren, boolean isAndMultipleOperand) 105 { 106 super(fieldPath, operator, value); 107 if (Operator.EQ != operator && Operator.NE != operator && Operator.EXISTS != operator) 108 { 109 throw new IllegalArgumentException("Test operator '" + operator + "' can't be used for a content query."); 110 } 111 112 this._resolver = resolver; 113 this._contentHelper = contentHelper; 114 this._refTableHelper = refTableHelper; 115 this._resolveChildren = resolveChildren; 116 this._isAndMultipleOperand = isAndMultipleOperand; 117 } 118 119 @Override 120 @SuppressWarnings("unchecked") 121 public String build() throws QuerySyntaxException 122 { 123 Operator operator = getOperator(); 124 Object value = getValue(); 125 126 if (operator == Operator.EXISTS) 127 { 128 StringBuilder query = new StringBuilder(); 129 query.append(getFieldName()).append("_s:").append(QueryHelper.EXISTS_VALUE); 130 return query.toString(); 131 } 132 133 if (value instanceof Map) 134 { 135 Map<String, Object> mapValues = (Map<String, Object>) value; 136 137 Object values = mapValues.get("value"); 138 boolean autoposting = (boolean) mapValues.get("autoposting"); 139 if (autoposting) 140 { 141 if (_resolveChildren) 142 { 143 Query query = null; 144 if (values instanceof Collection<?>) 145 { 146 query = _getChildrenOrSelfQuery((Collection<String>) values, operator); 147 } 148 else 149 { 150 query = _getChildrenOrSelfQuery(Collections.singletonList((String) values), operator); 151 } 152 153 return query.build(); 154 } 155 else 156 { 157 Query subQuery = null; 158 if (values instanceof Collection<?>) 159 { 160 subQuery = _createRefParentsQuery((Collection<String>) values, operator); 161 } 162 else 163 { 164 subQuery = _createRefParentsQuery(Collections.singletonList((String) values), operator); 165 } 166 167 Query joinQuery = new JoinQuery(subQuery, getFieldName()); 168 return joinQuery.build(); 169 } 170 171 } 172 else 173 { 174 Query query = _createQueryString(values, operator); 175 return query.build(); 176 } 177 } 178 else 179 { 180 Query query = _createQueryString(value, operator); 181 return query.build(); 182 } 183 } 184 185 private Query _getChildrenOrSelfQuery(Collection<String> values, Operator operator) 186 { 187 List<Query> queries = new ArrayList<>(); 188 189 if (_isAndMultipleOperand) 190 { 191 for (String value: values) 192 { 193 Set<String> childrenAndSelf = _getChildrenOrSelf(value); 194 195 List<Query> subQueries = new ArrayList<>(); 196 for (String id : childrenAndSelf) 197 { 198 subQueries.add(_createQueryString(id, operator)); 199 } 200 201 queries.add(new OrQuery(subQueries)); 202 } 203 204 return new AndQuery(queries); 205 } 206 else 207 { 208 Set<String> allChildrenAndSelf = new HashSet<>(); 209 for (String value: values) 210 { 211 allChildrenAndSelf.addAll(_getChildrenOrSelf(value)); 212 } 213 214 List<Query> subQueries = new ArrayList<>(); 215 for (String id : allChildrenAndSelf) 216 { 217 subQueries.add(_createQueryString(id, operator)); 218 } 219 220 return new OrQuery(subQueries); 221 } 222 } 223 224 private Set<String> _getChildrenOrSelf(String id) 225 { 226 Content content = _resolver.resolveById(id); 227 228 Set<String> childrenOrSelf = new HashSet<>(); 229 childrenOrSelf.add(id); 230 if (_contentHelper.isReferenceTable(content) && _refTableHelper.isHierarchical(content)) 231 { 232 childrenOrSelf.addAll(_refTableHelper.getAllChildren(content)); 233 } 234 235 return childrenOrSelf; 236 } 237 238 private Query _createQueryString(Object value, Operator operator) 239 { 240 String fieldPath = getFieldName(); 241 242 if (value instanceof Collection<?>) 243 { 244 @SuppressWarnings("unchecked") 245 List<String> stringValues = (List<String>) value; 246 247 List<Query> queries = new ArrayList<>(); 248 for (String val : stringValues) 249 { 250 queries.add(new StringQuery(fieldPath, operator, val, null)); 251 } 252 253 return _isAndMultipleOperand ? new AndQuery(queries) : new OrQuery(queries); 254 } 255 else 256 { 257 return new StringQuery(fieldPath, operator, (String) value, null); 258 } 259 } 260 261 /** 262 * Create a subQuery in order to create the join query 263 * @param values the values to add in the query 264 * @param operator the operator 265 * @return The non-null query 266 */ 267 private Query _createRefParentsQuery(Collection<String> values, Operator operator) 268 { 269 List<Query> queries = new ArrayList<>(); 270 for (String val: values) 271 { 272 queries.add(() -> 273 { 274 StringBuilder q = new StringBuilder(); 275 if (Operator.NE == operator) 276 { 277 NotQuery.appendNegation(q).append('('); 278 } 279 q.append(ParentContentSearchField.NAME).append(":").append(ClientUtils.escapeQueryChars(val)); 280 if (Operator.NE == operator) 281 { 282 q.append(')'); 283 } 284 return q.toString(); 285 }); 286 } 287 288 if (queries.size() == 1) 289 { 290 return queries.get(0); 291 } 292 else 293 { 294 return _isAndMultipleOperand ? new AndQuery(queries) : new OrQuery(queries); 295 } 296 } 297 298 @Override 299 public int hashCode() 300 { 301 final int prime = 31; 302 int result = super.hashCode(); 303 result = prime * result + Objects.hash(_isAndMultipleOperand, _resolveChildren); 304 return result; 305 } 306 307 @Override 308 public boolean equals(Object obj) 309 { 310 if (!super.equals(obj)) 311 { 312 return false; 313 } 314 315 ContentQuery other = (ContentQuery) obj; 316 return _isAndMultipleOperand == other._isAndMultipleOperand && _resolveChildren == other._resolveChildren; 317 } 318}