001/* 002 * (C) Copyright 2018-2019 Nuxeo (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Kevin Leturc <kleturc@nuxeo.com> 018 */ 019package org.nuxeo.ecm.core.bulk.message; 020 021import static org.apache.commons.lang3.StringUtils.isEmpty; 022 023import java.io.Serializable; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.Map; 027import java.util.UUID; 028 029import org.apache.avro.reflect.AvroDefault; 030import org.apache.avro.reflect.AvroEncode; 031import org.apache.avro.reflect.Nullable; 032import org.apache.commons.lang3.BooleanUtils; 033import org.apache.commons.lang3.builder.EqualsBuilder; 034import org.apache.commons.lang3.builder.HashCodeBuilder; 035import org.apache.commons.lang3.builder.ToStringBuilder; 036 037/** 038 * A message representing a bulk command 039 * 040 * @since 10.2 041 */ 042public class BulkCommand implements Serializable { 043 044 private static final long serialVersionUID = 20200904L; 045 046 protected String id; 047 048 protected String action; 049 050 protected String query; 051 052 // @scince 11.4 053 @Nullable 054 protected Long queryLimit; 055 056 protected String username; 057 058 @Nullable 059 protected String repository; 060 061 protected int bucketSize; 062 063 protected int batchSize; 064 065 // @since 11.1 066 @Nullable 067 protected String scroller; 068 069 // @since 11.1 070 @AvroDefault("false") 071 protected boolean genericScroller; 072 073 // @since 11.3 074 @AvroDefault("false") 075 protected boolean externalScroller; 076 077 @AvroEncode(using = MapAsJsonAsStringEncoding.class) 078 protected Map<String, Serializable> params; 079 080 protected BulkCommand() { 081 // Empty constructor for Avro decoder 082 } 083 084 public BulkCommand(Builder builder) { 085 this.id = UUID.randomUUID().toString(); 086 this.username = builder.username; 087 this.repository = builder.repository; 088 this.query = builder.query; 089 this.queryLimit = builder.queryLimit; 090 this.action = builder.action; 091 this.bucketSize = builder.bucketSize; 092 this.batchSize = builder.batchSize; 093 this.params = builder.params; 094 this.scroller = builder.scroller; 095 this.genericScroller = BooleanUtils.toBoolean(builder.genericScroller); 096 this.externalScroller = BooleanUtils.toBoolean(builder.externalScroller); 097 } 098 099 public String getUsername() { 100 return username; 101 } 102 103 public String getRepository() { 104 return repository; 105 } 106 107 public String getQuery() { 108 return query; 109 } 110 111 public String getAction() { 112 return action; 113 } 114 115 public String getScroller() { 116 return scroller; 117 } 118 119 /** 120 * True if the command uses a generic scroller. 121 * 122 * @since 11.1 123 */ 124 public boolean useGenericScroller() { 125 return genericScroller; 126 } 127 128 /** 129 * True if the command uses an external scroller. 130 * 131 * @since 11.3 132 */ 133 public boolean useExternalScroller() { 134 return externalScroller; 135 } 136 137 public Map<String, Serializable> getParams() { 138 return Collections.unmodifiableMap(params); 139 } 140 141 @SuppressWarnings("unchecked") 142 public <T> T getParam(String key) { 143 return (T) params.get(key); 144 } 145 146 public String getId() { 147 return id; 148 } 149 150 public int getBucketSize() { 151 return bucketSize; 152 } 153 154 public int getBatchSize() { 155 return batchSize; 156 } 157 158 /** 159 * When greater than 0, the limit applied to the query results 160 * 161 * @since 11.4 162 */ 163 public Long getQueryLimit() { 164 return queryLimit; 165 } 166 167 @Override 168 public int hashCode() { 169 return HashCodeBuilder.reflectionHashCode(this); 170 } 171 172 @Override 173 public boolean equals(Object o) { 174 return EqualsBuilder.reflectionEquals(this, o); 175 } 176 177 @Override 178 public String toString() { 179 return ToStringBuilder.reflectionToString(this); 180 } 181 182 public void setQueryLimit(Long limit) { 183 this.queryLimit = limit; 184 } 185 186 public void setBatchSize(int batchSize) { 187 this.batchSize = batchSize; 188 } 189 190 public void setBucketSize(int bucketSize) { 191 this.bucketSize = bucketSize; 192 } 193 194 public void setRepository(String repository) { 195 this.repository = repository; 196 } 197 198 public void setScroller(String scrollerName) { 199 this.scroller = scrollerName; 200 } 201 202 public static class Builder { 203 protected final String action; 204 205 protected final String query; 206 207 protected Long queryLimit; 208 209 protected String repository; 210 211 protected String username; 212 213 protected int bucketSize; 214 215 protected int batchSize; 216 217 protected String scroller; 218 219 protected Boolean genericScroller; 220 221 protected Boolean externalScroller; 222 223 protected Map<String, Serializable> params = new HashMap<>(); 224 225 /** 226 * BulkCommand builder 227 * 228 * @param action the registered bulk action name 229 * @param query by default an NXQL query that represents the document set to apply the action. When using a 230 * generic scroller the query syntax is a convention with the scroller implementation. When using an 231 * external scroller the field is null. 232 * @param username the user with whose rights the computation will be executed 233 * @since 11.1 234 */ 235 public Builder(String action, String query, String username) { 236 if (isEmpty(action)) { 237 throw new IllegalArgumentException("Action cannot be empty"); 238 } 239 this.action = action; 240 if (isEmpty(query)) { 241 throw new IllegalArgumentException("Query cannot be empty"); 242 } 243 this.query = query; 244 if (isEmpty(username)) { 245 throw new IllegalArgumentException("Username cannot be empty"); 246 } 247 this.username = username; 248 } 249 250 /** 251 * BulkCommand builder 252 * 253 * @param action the registered bulk action name 254 * @param nxqlQuery the query that represent the document set to apply the action 255 * @deprecated since 11.1, use {@link #Builder(String, String, String)} constructor with username instead 256 */ 257 @Deprecated 258 public Builder(String action, String nxqlQuery) { 259 if (isEmpty(action)) { 260 throw new IllegalArgumentException("Action cannot be empty"); 261 } 262 this.action = action; 263 if (isEmpty(nxqlQuery)) { 264 throw new IllegalArgumentException("Query cannot be empty"); 265 } 266 this.query = nxqlQuery; 267 } 268 269 /** 270 * Use a non default document repository 271 */ 272 public Builder repository(String name) { 273 this.repository = name; 274 return this; 275 } 276 277 /** 278 * Limits the query result. 279 * 280 * @since 11.4 281 */ 282 public Builder queryLimit(long limit) { 283 if (limit <= 0) { 284 throw new IllegalArgumentException(String.format("Invalid limit: %d, must be > 0", limit)); 285 } 286 this.queryLimit = limit; 287 return this; 288 } 289 290 /** 291 * Unlimited query results, this will override the action defaultQueryLimit. 292 * 293 * @since 11.4 294 */ 295 public Builder queryUnlimited() { 296 this.queryLimit = 0L; 297 return this; 298 } 299 300 /** 301 * User running the bulk action 302 * 303 * @deprecated since 11.1, use {@link #Builder(String, String, String)} constructor with username instead 304 */ 305 @Deprecated 306 public Builder user(String name) { 307 this.username = name; 308 return this; 309 } 310 311 /** 312 * The size of a bucket of documents id that fits into a record 313 */ 314 public Builder bucket(int size) { 315 if (size <= 0) { 316 throw new IllegalArgumentException("Invalid bucket size must > 0"); 317 } 318 if (batchSize > size) { 319 throw new IllegalArgumentException( 320 String.format("Bucket size: %d must be greater or equals to batch size: %d", size, batchSize)); 321 } 322 this.bucketSize = size; 323 return this; 324 } 325 326 /** 327 * The number of documents processed by action within a transaction 328 */ 329 public Builder batch(int size) { 330 if (size <= 0) { 331 throw new IllegalArgumentException("Invalid batch size must > 0"); 332 } 333 if (bucketSize > 0 && size > bucketSize) { 334 throw new IllegalArgumentException( 335 String.format("Bucket size: %d must be greater or equals to batch size: %d", size, batchSize)); 336 } 337 this.batchSize = size; 338 return this; 339 } 340 341 /** 342 * Add an action parameter 343 */ 344 public Builder param(String key, Serializable value) { 345 if (isEmpty(key)) { 346 throw new IllegalArgumentException("Param key cannot be null"); 347 } 348 params.put(key, value); 349 return this; 350 } 351 352 /** 353 * Set all action parameters 354 */ 355 public Builder params(Map<String, Serializable> params) { 356 if (params != null && !params.isEmpty()) { 357 if (params.containsKey(null)) { 358 throw new IllegalArgumentException("Param key cannot be null"); 359 } 360 this.params = params; 361 } 362 return this; 363 } 364 365 /** 366 * Sets scroller name used to materialized the document set 367 */ 368 public Builder scroller(String scrollerName) { 369 this.scroller = scrollerName; 370 return this; 371 } 372 373 /** 374 * Uses a generic scroller, the query syntax depends on scroller implementation. 375 * 376 * @since 11.1 377 */ 378 public Builder useGenericScroller() { 379 checkScrollerType(); 380 this.genericScroller = true; 381 return this; 382 } 383 384 /** 385 * Uses a document scroller, the query must be a valid NXQL query. This is the default. 386 * 387 * @since 11.1 388 */ 389 public Builder useDocumentScroller() { 390 checkScrollerType(); 391 this.genericScroller = false; 392 return this; 393 } 394 395 /** 396 * Uses an external scroller. 397 * 398 * @since 11.3 399 */ 400 public Builder useExternalScroller() { 401 checkScrollerType(); 402 this.externalScroller = true; 403 return this; 404 } 405 406 protected void checkScrollerType() { 407 if (this.genericScroller != null || this.externalScroller != null) { 408 throw new IllegalArgumentException("Only one useScroller method should be called"); 409 } 410 } 411 412 public BulkCommand build() { 413 return new BulkCommand(this); 414 } 415 416 } 417}