Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / taskqueue / TaskOptions.java
blobb2e7dd8d3d14e1c54943f7038c89a2a07a06de28
1 // Copyright 2010 Google Inc. All rights reserved.
2 package com.google.appengine.api.taskqueue;
4 import com.google.appengine.api.taskqueue.TaskQueuePb.TaskQueueAddRequest.RequestMethod;
6 import java.io.ByteArrayOutputStream;
7 import java.io.IOException;
8 import java.io.ObjectOutputStream;
9 import java.io.Serializable;
10 import java.io.UnsupportedEncodingException;
11 import java.net.URLEncoder;
12 import java.util.ArrayList;
13 import java.util.Arrays;
14 import java.util.HashMap;
15 import java.util.Iterator;
16 import java.util.LinkedHashMap;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.Map;
21 /**
22 * Contains various options for a task following the builder pattern.
23 * Calls to {@link TaskOptions} methods may be chained to specify
24 * multiple options in the one {@link TaskOptions} object.
25 * <p>taskOptions can have either {@link TaskOptions.Method} PULL or a
26 * PUSH-related method, e.g. POST, GET, ... Tasks with PULL method can
27 * only be added into a PULL queue and PUSH tasks can only be added
28 * into a PUSH queue. <p>Notes on usage:<br> The recommended way to
29 * instantiate a {@link TaskOptions} object is to statically import
30 * {@link Builder}.* and invoke a static creation method followed by
31 * an instance mutator (if needed):
33 * <pre>
34 * import static com.google.appengine.api.taskqueue.TaskOptions.Builder.*;
36 * ...
37 * {@link QueueFactory#getDefaultQueue()}.add(withUrl(url).etaMillis(eta));
38 * </pre>
41 public final class TaskOptions implements Serializable {
42 private static final long serialVersionUID = -2702019046191004750L;
43 /**
44 * Methods supported by {@link Queue}.
45 * Tasks added to pull queues must have the {@link #Method} PULL.
46 * All other methods are appropriate for push queues.
47 * See {@link Queue} for more detail on pull and push mode queues.
49 public enum Method {
50 DELETE(RequestMethod.DELETE, false),
51 GET(RequestMethod.GET, false),
52 HEAD(RequestMethod.HEAD, false),
53 POST(RequestMethod.POST, true),
54 PUT(RequestMethod.PUT, true),
55 PULL(null, true);
57 private RequestMethod pbMethod;
58 private boolean isBodyMethod;
60 Method(RequestMethod pbMethod, boolean isBodyMethod) {
61 this.pbMethod = pbMethod;
62 this.isBodyMethod = isBodyMethod;
65 RequestMethod getPbMethod() {
66 return pbMethod;
69 boolean supportsBody() {
70 return isBodyMethod;
74 /**
75 * Params are currently immutable and need to remain that way to avoid the need to clone them
76 * in the TaskOptions copy constructor.
78 abstract static class Param implements Serializable {
79 protected final String name;
81 public Param(String name) {
82 if (name == null || name.length() == 0) {
83 throw new IllegalArgumentException("name must not be null or empty");
85 this.name = name;
88 @Override
89 public int hashCode() {
90 return name.hashCode();
93 protected static String encodeURLAsUtf8(String url) throws UnsupportedEncodingException {
94 return URLEncoder.encode(url, "UTF-8");
97 @Override
98 public abstract boolean equals(Object o);
100 public String getURLEncodedName() throws UnsupportedEncodingException {
101 return encodeURLAsUtf8(name);
104 abstract String getURLEncodedValue() throws UnsupportedEncodingException;
107 static class StringValueParam extends Param {
108 private static final long serialVersionUID = -2306561754387422446L;
110 protected final String value;
112 StringValueParam(String name, String value) {
113 super(name);
115 if (value == null) {
116 throw new IllegalArgumentException("value must not be null");
118 this.value = value;
121 @Override
122 public String toString() {
123 return "StringParam(" + name + "=" + value + ")";
126 @Override
127 public boolean equals(Object o) {
128 if (this == o) {
129 return true;
132 if (!(o instanceof StringValueParam)) {
133 return false;
136 StringValueParam that = (StringValueParam) o;
138 return value.equals(that.value) && name.equals(that.name);
141 @Override
142 public String getURLEncodedValue() throws UnsupportedEncodingException {
143 return encodeURLAsUtf8(value);
147 static class ByteArrayValueParam extends Param {
148 private static final long serialVersionUID = 1420600427192025644L;
150 protected final byte[] value;
152 ByteArrayValueParam(String name, byte[] value) {
153 super(name);
155 if (value == null) {
156 throw new IllegalArgumentException("value must not be null");
158 this.value = value;
161 @Override
162 public String toString() {
163 return "ByteArrayParam(" + name + ": " + value.length + " bytes)";
166 @Override
167 public boolean equals(Object o) {
168 if (this == o) {
169 return true;
172 if (!(o instanceof ByteArrayValueParam)) {
173 return false;
176 ByteArrayValueParam that = (ByteArrayValueParam) o;
178 return Arrays.equals(value, that.value) && name.equals(that.name);
182 * {@inheritDoc}
183 * @throws UnsupportedEncodingException
185 @Override
186 public String getURLEncodedValue() throws UnsupportedEncodingException {
187 StringBuilder result = new StringBuilder();
188 for (int i = 0; i < value.length; i++) {
189 result.append("%");
190 char character = Character.toUpperCase(Character.forDigit((value[i] >> 4) & 0xF, 16));
191 result.append(character);
192 character = Character.toUpperCase(Character.forDigit(value[i] & 0xF, 16));
193 result.append(character);
195 return result.toString();
199 private String taskName;
200 private byte[] payload;
201 private HashMap<String, List<String>> headers;
202 private Method method;
203 private List<Param> params;
204 private String url;
205 private Long countdownMillis;
206 private Long etaMillis;
207 private RetryOptions retryOptions;
208 private byte[] tag;
210 private TaskOptions() {
211 this.method = Method.POST;
212 this.headers = new LinkedHashMap<String, List<String>>();
213 this.params = new LinkedList<Param>();
217 * A copy constructor for {@link TaskOptions}.
219 public TaskOptions(TaskOptions options) {
220 taskName = options.taskName;
221 method = options.method;
222 url = options.url;
223 countdownMillis = options.countdownMillis;
224 etaMillis = options.etaMillis;
225 tag(options.tag);
226 if (options.retryOptions != null) {
227 retryOptions = new RetryOptions(options.retryOptions);
228 } else {
229 retryOptions = null;
231 if (options.getPayload() != null) {
232 payload(options.getPayload());
233 } else {
234 payload = null;
236 this.headers = copyOfHeaders(options.getHeaders());
237 this.params = new LinkedList<Param>(options.getParams());
240 private static LinkedHashMap<String, List<String>> copyOfHeaders(
241 Map<String, List<String>> headers) {
242 LinkedHashMap<String, List<String>> copy = new LinkedHashMap<String, List<String>>();
243 for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
244 copy.put(entry.getKey(), new ArrayList<String>(entry.getValue()));
246 return copy;
250 * Sets the task name.
252 * @throws IllegalArgumentException The provided name is null, empty or doesn't match the regular
253 * expression {@link QueueConstants#TASK_NAME_REGEX}
255 public TaskOptions taskName(String taskName) {
256 if (taskName != null && taskName.length() != 0) {
257 TaskHandle.validateTaskName(taskName);
259 this.taskName = taskName;
260 return this;
264 * Returns the task name.
266 public String getTaskName() {
267 return taskName;
271 * Returns the live payload for the task, not a copy. May be {@code null}.
273 public byte[] getPayload() {
274 return payload;
278 * Sets the payload directly without specifying the content-type. If this
279 * task is added to a push queue, the content-type will be set to
280 * 'application/octet-stream' by default.
281 * @param payload The bytes representing the paylaod.
282 * @return TaskOptions object for chaining.
284 public TaskOptions payload(byte[] payload) {
285 this.payload = payload.clone();
286 return this;
290 * Sets the payload to the serialized form of the deferredTask object.
291 * The payload will be generated by serializing the deferredTask object using
292 * {@link ObjectOutputStream#writeObject(Object)}. If the deferredTask's
293 * {@link Method} is not PULL, the content type will be set to
294 * {@link DeferredTaskContext#RUNNABLE_TASK_CONTENT_TYPE}, the method
295 * will be forced to {@link Method#POST} and if otherwise not specified, the
296 * url will be set to {@link DeferredTaskContext#DEFAULT_DEFERRED_URL}; the
297 * {@link DeferredTask} servlet is, by default, mapped to this url.
299 * <p>Note: While this may be a convenient API, it requires careful control of the
300 * serialization compatibility of objects passed into {@link #payload(DeferredTask)}
301 * method as objects placed in the task queue will survive revision updates
302 * of the application and hence may fail deserialization when the task is decoded
303 * with new revisions of the application. In particular, Java anonymous classes
304 * are convenient but may be particularly difficult to control or test for
305 * serialization compatibility.
307 * @param deferredTask The object to serialize into the payload.
308 * @throws DeferredTaskCreationException if there was an IOException serializing object.
310 public TaskOptions payload(DeferredTask deferredTask) {
311 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024);
312 ObjectOutputStream objectStream = null;
313 try {
314 objectStream = new ObjectOutputStream(stream);
315 objectStream.writeObject(deferredTask);
316 } catch (IOException e) {
317 throw new DeferredTaskCreationException(e);
319 payload = stream.toByteArray();
320 if (getMethod() != Method.PULL) {
321 header("content-type", DeferredTaskContext.RUNNABLE_TASK_CONTENT_TYPE);
322 method(Method.POST);
323 if (getUrl() == null) {
324 url(DeferredTaskContext.DEFAULT_DEFERRED_URL);
327 return this;
331 * Sets the payload from a {@link String} given a specific character set.
332 * @throws UnsupportedTranslationException
334 public TaskOptions payload(String payload, String charset) {
335 try {
336 return payload(payload.getBytes(charset), "text/plain; charset=" + charset);
337 } catch (UnsupportedEncodingException exception) {
338 throw new UnsupportedTranslationException(
339 "Unsupported charset '" + charset + "' requested.", exception);
344 * Set the payload with the given content type.
345 * @param payload The bytes representing the paylaod.
346 * @param contentType The content-type of the bytes.
347 * @return TaskOptions object for chaining.
349 public TaskOptions payload(byte[] payload, String contentType) {
350 return payload(payload).
351 header("content-type", contentType);
355 * Set the payload by {@link String}. The charset to convert the
356 * String to will be UTF-8 unless the method is PULL, in which case the
357 * String's bytes will be used directly.
358 * @param payload The String to be used.
359 * @return TaskOptions object for chaining.
361 public TaskOptions payload(String payload) {
362 if (getMethod() == Method.PULL) {
363 return payload(payload.getBytes());
365 return payload(payload, "UTF-8");
369 * Returns a copy of the task's headers as a map from each header name to a list of values for
370 * that header name.
372 public Map<String, List<String>> getHeaders() {
373 return copyOfHeaders(headers);
377 * Replaces the existing headers with the provided header {@code name/value} mapping.
378 * @param headers The headers to copy.
379 * @return TaskOptions object for chaining.
381 public TaskOptions headers(Map<String, String> headers) {
382 this.headers = new LinkedHashMap<String, List<String>>();
383 for (Map.Entry<String, String> entry : headers.entrySet()) {
384 List<String> values = new ArrayList<String>(1);
385 values.add(entry.getValue());
386 this.headers.put(entry.getKey(), values);
388 return this;
392 * Adds a header {@code name/value} pair.
394 * @throws IllegalArgumentException
396 public TaskOptions header(String headerName, String value) {
397 if (headerName == null || headerName.length() == 0) {
398 throw new IllegalArgumentException("headerName must not be null or empty.");
400 if (value == null) {
401 throw new IllegalArgumentException("header(name, <null>) is not allowed.");
404 if (!headers.containsKey(headerName)) {
405 headers.put(headerName, new ArrayList<String>());
407 headers.get(headerName).add(value);
408 return this;
412 * Remove all headers with the given name.
414 public TaskOptions removeHeader(String headerName) {
415 if (headerName == null || headerName.length() == 0) {
416 throw new IllegalArgumentException("headerName must not be null or empty.");
418 headers.remove(headerName);
419 return this;
423 * Returns the method used for this task.
425 public Method getMethod() {
426 return method;
430 * Set the method used for this task. Defaults to {@link Method#POST}.
432 public TaskOptions method(Method method) {
433 this.method = method;
434 return this;
438 * Returns a copy of the task's string-valued parameters as a map from each parameter name to a
439 * list of values for that name.
441 public Map<String, List<String>> getStringParams() {
442 LinkedHashMap<String, List<String>> stringParams = new LinkedHashMap<>();
443 for (Param param : params) {
444 if (param instanceof StringValueParam) {
445 if (!stringParams.containsKey(param.name)) {
446 stringParams.put(param.name, new ArrayList<String>());
448 stringParams.get(param.name).add(((StringValueParam) param).value);
451 return stringParams;
455 * Returns a copy of the task's byte-array-valued parameters as a map from each parameter name to
456 * a list of values for that name.
458 public Map<String, List<byte[]>> getByteArrayParams() {
459 LinkedHashMap<String, List<byte[]>> byteArrayParams = new LinkedHashMap<>();
460 for (Param param : params) {
461 if (param instanceof ByteArrayValueParam) {
462 if (!byteArrayParams.containsKey(param.name)) {
463 byteArrayParams.put(param.name, new ArrayList<byte[]>());
465 byte[] value = ((ByteArrayValueParam) param).value;
466 byteArrayParams.get(param.name).add(Arrays.copyOf(value, value.length));
469 return byteArrayParams;
472 List<Param> getParams() {
473 return params;
476 TaskOptions param(Param param) {
477 params.add(param);
478 return this;
482 * Clears the parameters.
484 public TaskOptions clearParams() {
485 params.clear();
486 return this;
490 * Add a named {@link String} parameter.
491 * @param name Name of the parameter. Must not be null or empty.
492 * @param value The value of the parameter will undergo a "UTF-8"
493 * character encoding transformation upon being added to the queue.
494 * {@code value} must not be {@code null}.
495 * @throws IllegalArgumentException
497 public TaskOptions param(String name, String value) {
498 return param(new StringValueParam(name, value));
502 * Add a named {@code byte} array parameter.
503 * @param name Name of the parameter. Must not be null or empty.
504 * @param value A byte array and encoded as-is
505 * (i.e. without character encoding transformations).
506 * {@code value} must not be {@code null}.
507 * @throws IllegalArgumentException
509 public TaskOptions param(String name, byte[] value) {
510 return param(new ByteArrayValueParam(name, value));
514 * Remove all parameters with the given name.
515 * @param paramName Name of the parameter. Must not be null or empty.
517 public TaskOptions removeParam(String paramName) {
518 if (paramName == null || paramName.length() == 0) {
519 throw new IllegalArgumentException("paramName must not be null or empty.");
522 Iterator<Param> paramsIter = params.iterator();
523 while (paramsIter.hasNext()) {
524 if (paramName.equals(paramsIter.next().name)) {
525 paramsIter.remove();
529 return this;
532 public String getUrl() {
533 return url;
537 * Set the URL.
538 * <p>Default value is {@code null}.
539 * @param url String containing URL.
541 public TaskOptions url(String url) {
542 if (url == null) {
543 throw new IllegalArgumentException("null url is not allowed.");
545 this.url = url;
546 return this;
550 * Returns the delay to apply to the submitted time. May be {@code null}.
552 public Long getCountdownMillis() {
553 return countdownMillis;
557 * Set the number of milliseconds delay before execution of the task.
559 public TaskOptions countdownMillis(long countdownMillis) {
560 this.countdownMillis = countdownMillis;
561 return this;
565 * Returns the specified ETA for a task. May be {@code null} if not specified.
567 public Long getEtaMillis() {
568 return etaMillis;
572 * Sets the approximate absolute time to execute. (i.e. etaMillis is comparable with
573 * {@link System#currentTimeMillis()}).
575 public TaskOptions etaMillis(long etaMillis) {
576 this.etaMillis = etaMillis;
577 return this;
581 * Returns a copy of the retry options for a task. May be {@code null} if not specified.
583 public RetryOptions getRetryOptions() {
584 return retryOptions == null ? null : new RetryOptions(retryOptions);
588 * Sets retry options for this task. Retry Options must be built with
589 * {@code RetryOptions.Builder}.
591 public TaskOptions retryOptions(RetryOptions retryOptions) {
592 this.retryOptions = retryOptions;
593 return this;
597 * Returns the live tag bytes for a task, not a copy. May be {@code null} if tag is not specified.
599 public byte[] getTagAsBytes() {
600 return tag;
604 * Returns the tag for a task. May be {@code null} if tag is not specified.
605 * @throws UnsupportedEncodingException
607 public String getTag() throws UnsupportedEncodingException {
608 if (tag != null) {
609 return new String(tag, "UTF-8");
611 return null;
615 * Sets the tag for a task. Ignores null or zero-length tags.
617 public TaskOptions tag(byte[] tag) {
618 if (tag != null && tag.length > 0) {
619 this.tag = tag.clone();
620 } else {
621 this.tag = null;
623 return this;
627 * Sets the tag for a task. Ignores null or empty tags.
629 public TaskOptions tag(String tag) {
630 if (tag != null) {
631 return tag(tag.getBytes());
633 return this;
636 @Override
637 public int hashCode() {
638 final int prime = 31;
639 int result = 1;
640 result = prime * result + ((countdownMillis == null) ? 0 : countdownMillis.hashCode());
641 result = prime * result + ((etaMillis == null) ? 0 : etaMillis.hashCode());
642 result = prime * result + ((headers == null) ? 0 : headers.hashCode());
643 result = prime * result + ((method == null) ? 0 : method.hashCode());
644 result = prime * result + ((params == null) ? 0 : params.hashCode());
645 result = prime * result + Arrays.hashCode(payload);
646 result = prime * result + ((taskName == null) ? 0 : taskName.hashCode());
647 result = prime * result + ((url == null) ? 0 : url.hashCode());
648 result = prime * result + ((retryOptions == null) ? 0 : retryOptions.hashCode());
649 result = prime * result + Arrays.hashCode(tag);
650 return result;
653 @Override
654 public boolean equals(Object obj) {
655 if (this == obj) {
656 return true;
658 if (obj == null) {
659 return false;
661 if (getClass() != obj.getClass()) {
662 return false;
664 TaskOptions other = (TaskOptions) obj;
665 if (countdownMillis == null) {
666 if (other.countdownMillis != null) {
667 return false;
669 } else if (!countdownMillis.equals(other.countdownMillis)) {
670 return false;
672 if (etaMillis == null) {
673 if (other.etaMillis != null) {
674 return false;
676 } else if (!etaMillis.equals(other.etaMillis)) {
677 return false;
679 if (headers == null) {
680 if (other.headers != null) {
681 return false;
683 } else if (!headers.equals(other.headers)) {
684 return false;
686 if (method == null) {
687 if (other.method != null) {
688 return false;
690 } else if (!method.equals(other.method)) {
691 return false;
693 if (params == null) {
694 if (other.params != null) {
695 return false;
697 } else if (!params.equals(other.params)) {
698 return false;
700 if (!Arrays.equals(payload, other.payload)) {
701 return false;
703 if (taskName == null) {
704 if (other.taskName != null) {
705 return false;
707 } else if (!taskName.equals(other.taskName)) {
708 return false;
710 if (url == null) {
711 if (other.url != null) {
712 return false;
714 } else if (!url.equals(other.url)) {
715 return false;
717 if (retryOptions == null) {
718 if (other.retryOptions != null) {
719 return false;
721 } else if (!retryOptions.equals(other.retryOptions)) {
722 return false;
724 if (!Arrays.equals(tag, other.tag)) {
725 return false;
727 return true;
730 @Override
731 public String toString() {
732 String tagString = null;
733 try {
734 tagString = getTag();
735 } catch (UnsupportedEncodingException e) {
736 tagString = "not a utf-8 String";
738 return "TaskOptions[taskName=" + taskName + ", headers=" + headers
739 + ", method=" + method + ", params=" + params + ", url=" + url
740 + ", countdownMillis=" + countdownMillis + ", etaMillis=" + etaMillis
741 + ", retryOptions=" + retryOptions + ", tag=" + tagString + "]";
745 * Provides static creation methods for {@link TaskOptions}.
747 public static final class Builder {
749 * Returns default {@link TaskOptions} and calls {@link TaskOptions#taskName(String)}.
751 public static TaskOptions withTaskName(String taskName) {
752 return withDefaults().taskName(taskName);
756 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(byte[])}.
758 static TaskOptions withPayload(byte[] payload) {
759 return withDefaults().payload(payload);
763 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(String, String)}.
765 public static TaskOptions withPayload(String payload, String charset) {
766 return withDefaults().payload(payload, charset);
770 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(DeferredTask)}.
772 public static TaskOptions withPayload(DeferredTask deferredTask) {
773 return withDefaults().payload(deferredTask);
777 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(byte[], String)}.
779 public static TaskOptions withPayload(byte[] payload, String contentType) {
780 return withDefaults().payload(payload, contentType);
784 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(String)}.
786 public static TaskOptions withPayload(String payload) {
787 return withDefaults().payload(payload);
791 * Returns default {@link TaskOptions} and calls {@link TaskOptions#headers(Map)}.
793 public static TaskOptions withHeaders(Map<String, String> headers) {
794 return withDefaults().headers(headers);
798 * Returns default {@link TaskOptions} and calls {@link TaskOptions#header(String, String)}.
800 public static TaskOptions withHeader(String headerName, String value) {
801 return withDefaults().header(headerName, value);
805 * Returns default {@link TaskOptions} and calls {@link TaskOptions#method(Method)}.
807 public static TaskOptions withMethod(Method method) {
808 return withDefaults().method(method);
812 * Returns default {@link TaskOptions} and calls {@link TaskOptions#param(String, String)}.
814 public static TaskOptions withParam(String paramName, String value) {
815 return withDefaults().param(paramName, value);
819 * Returns default {@link TaskOptions} and calls {@link TaskOptions#param(String, byte[])}.
821 public static TaskOptions withParam(String paramName, byte[] value) {
822 return withDefaults().param(paramName, value);
826 * Returns default {@link TaskOptions} and calls {@link TaskOptions#url(String)}.
828 public static TaskOptions withUrl(String url) {
829 return withDefaults().url(url);
833 * Returns default {@link TaskOptions} and calls {@link TaskOptions#countdownMillis(long)}.
835 public static TaskOptions withCountdownMillis(long countdownMillis) {
836 return withDefaults().countdownMillis(countdownMillis);
840 * Returns default {@link TaskOptions} and calls {@link TaskOptions#etaMillis(long)}.
842 public static TaskOptions withEtaMillis(long etaMillis) {
843 return withDefaults().etaMillis(etaMillis);
847 * Returns default {@link TaskOptions} and calls
848 * {@link TaskOptions#retryOptions(RetryOptions)}.
850 public static TaskOptions withRetryOptions(RetryOptions retryOptions) {
851 return withDefaults().retryOptions(retryOptions);
855 * Returns default {@link TaskOptions} and calls {@link TaskOptions#tag(byte[])}.
857 public static TaskOptions withTag(byte[] tag) {
858 return withDefaults().tag(tag);
862 * Returns default {@link TaskOptions} and calls {@link TaskOptions#tag(String)}.
864 public static TaskOptions withTag(String tag) {
865 return withDefaults().tag(tag);
869 * Returns default {@link TaskOptions} with default values.
871 public static TaskOptions withDefaults() {
872 return new TaskOptions();
875 private Builder() {