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}