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
;
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):
34 * import static com.google.appengine.api.taskqueue.TaskOptions.Builder.*;
37 * {@link QueueFactory#getDefaultQueue()}.add(header("X-HEADER", "value"));
41 public final class TaskOptions
implements Serializable
{
42 private static final long serialVersionUID
= -2702019046191004750L;
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.
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),
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() {
69 boolean supportsBody() {
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");
89 public int hashCode() {
90 return name
.hashCode();
93 protected static String
encodeURLAsUtf8(String url
) throws UnsupportedEncodingException
{
94 return URLEncoder
.encode(url
, "UTF-8");
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
) {
116 throw new IllegalArgumentException("value must not be null");
122 public String
toString() {
123 return "StringParam(" + name
+ "=" + value
+ ")";
127 public boolean equals(Object o
) {
132 if (!(o
instanceof StringValueParam
)) {
136 StringValueParam that
= (StringValueParam
) o
;
138 return value
.equals(that
.value
) && name
.equals(that
.name
);
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
) {
156 throw new IllegalArgumentException("value must not be null");
162 public String
toString() {
163 return "ByteArrayParam(" + name
+ ": " + value
.length
+ " bytes)";
167 public boolean equals(Object o
) {
172 if (!(o
instanceof ByteArrayValueParam
)) {
176 ByteArrayValueParam that
= (ByteArrayValueParam
) o
;
178 return Arrays
.equals(value
, that
.value
) && name
.equals(that
.name
);
183 * @throws UnsupportedEncodingException
186 public String
getURLEncodedValue() throws UnsupportedEncodingException
{
187 StringBuilder result
= new StringBuilder();
188 for (int i
= 0; i
< value
.length
; i
++) {
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
;
205 private Long countdownMillis
;
206 private Long etaMillis
;
207 private RetryOptions retryOptions
;
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
;
223 countdownMillis
= options
.countdownMillis
;
224 etaMillis
= options
.etaMillis
;
226 if (options
.retryOptions
!= null) {
227 retryOptions
= new RetryOptions(options
.retryOptions
);
231 if (options
.getPayload() != null) {
232 payload(options
.getPayload());
236 initializeHeaders(options
.getHeaders());
237 initializeParams(options
.getParams());
240 private void initializeHeaders(Map
<String
, List
<String
>> headers
) {
241 this.headers
= new LinkedHashMap
<String
, List
<String
>>();
243 for (Map
.Entry
<String
, List
<String
>> entry
: headers
.entrySet()) {
244 this.headers
.put(entry
.getKey(), new ArrayList
<String
>(entry
.getValue()));
248 private void initializeParams(List
<Param
> params
) {
249 this.params
= new LinkedList
<Param
>(params
);
257 * Sets the task name.
259 * @throws IllegalArgumentException The provided name is null, empty or doesn't match the regular
260 * expression {@link QueueConstants#TASK_NAME_REGEX}
262 public TaskOptions
taskName(String taskName
) {
263 if (taskName
!= null && taskName
.length() != 0) {
264 TaskHandle
.validateTaskName(taskName
);
266 this.taskName
= taskName
;
270 String
getTaskName() {
274 byte[] getPayload() {
279 * Sets the payload directly without specifying the content-type. If this
280 * task is added to a push queue, the content-type will be set to
281 * 'application/octet-stream' by default.
282 * @param payload The bytes representing the paylaod.
283 * @return TaskOptions object for chaining.
285 public TaskOptions
payload(byte[] payload
) {
286 this.payload
= payload
.clone();
291 * Sets the payload to the serialized form of the deferredTask object.
292 * The payload will be generated by serializing the deferredTask object using
293 * {@link ObjectOutputStream#writeObject(Object)}. If the deferredTask's
294 * {@link Method} is not PULL, the content type will be set to
295 * {@link DeferredTaskContext#RUNNABLE_TASK_CONTENT_TYPE}, the method
296 * will be forced to {@link Method#POST} and if otherwise not specified, the
297 * url will be set to {@link DeferredTaskContext#DEFAULT_DEFERRED_URL}; the
298 * {@link DeferredTask} servlet is, by default, mapped to this url.
300 * <p>Note: While this may be a convenient API, it requires careful control of the
301 * serialization compatibility of objects passed into {@link #payload(DeferredTask)}
302 * method as objects placed in the task queue will survive revision updates
303 * of the application and hence may fail deserialization when the task is decoded
304 * with new revisions of the application. In particular, Java anonymous classes
305 * are convenient but may be particularly difficult to control or test for
306 * serialization compatibility.
308 * @param deferredTask The object to serialize into the payload.
309 * @throws DeferredTaskCreationException if there was an IOException serializing object.
311 public TaskOptions
payload(DeferredTask deferredTask
) {
312 ByteArrayOutputStream stream
= new ByteArrayOutputStream(1024);
313 ObjectOutputStream objectStream
= null;
315 objectStream
= new ObjectOutputStream(stream
);
316 objectStream
.writeObject(deferredTask
);
317 } catch (IOException e
) {
318 throw new DeferredTaskCreationException(e
);
320 payload
= stream
.toByteArray();
321 if (getMethod() != Method
.PULL
) {
322 header("content-type", DeferredTaskContext
.RUNNABLE_TASK_CONTENT_TYPE
);
324 if (getUrl() == null) {
325 url(DeferredTaskContext
.DEFAULT_DEFERRED_URL
);
332 * Sets the payload from a {@link String} given a specific character set.
333 * @throws UnsupportedTranslationException
335 public TaskOptions
payload(String payload
, String charset
) {
337 return payload(payload
.getBytes(charset
), "text/plain; charset=" + charset
);
338 } catch (UnsupportedEncodingException exception
) {
339 throw new UnsupportedTranslationException(
340 "Unsupported charset '" + charset
+ "' requested.", exception
);
345 * Set the payload with the given content type.
346 * @param payload The bytes representing the paylaod.
347 * @param contentType The content-type of the bytes.
348 * @return TaskOptions object for chaining.
350 public TaskOptions
payload(byte[] payload
, String contentType
) {
351 return payload(payload
).
352 header("content-type", contentType
);
356 * Set the payload by {@link String}. The charset to convert the
357 * String to will be UTF-8 unless the method is PULL, in which case the
358 * String's bytes will be used directly.
359 * @param payload The String to be used.
360 * @return TaskOptions object for chaining.
362 public TaskOptions
payload(String payload
) {
363 if (getMethod() == Method
.PULL
) {
364 return payload(payload
.getBytes());
366 return payload(payload
, "UTF-8");
369 HashMap
<String
, List
<String
>> getHeaders() {
374 * Replaces the existing headers with the provided header {@code name/value} mapping.
375 * @param headers The headers to copy.
376 * @return TaskOptions object for chaining.
378 public TaskOptions
headers(Map
<String
, String
> headers
) {
379 this.headers
= new LinkedHashMap
<String
, List
<String
>>();
381 for (Map
.Entry
<String
, String
> entry
: headers
.entrySet()) {
382 List
<String
> values
= new ArrayList
<String
>(1);
383 values
.add(entry
.getValue());
384 this.headers
.put(entry
.getKey(), values
);
390 * Adds a header {@code name/value} pair.
392 * @throws IllegalArgumentException
394 public TaskOptions
header(String headerName
, String value
) {
395 if (headerName
== null || headerName
.length() == 0) {
396 throw new IllegalArgumentException("headerName must not be null or empty.");
399 throw new IllegalArgumentException("header(name, <null>) is not allowed.");
402 if (!headers
.containsKey(headerName
)) {
403 headers
.put(headerName
, new ArrayList
<String
>());
405 headers
.get(headerName
).add(value
);
410 * Remove all headers with the given name.
412 public TaskOptions
removeHeader(String headerName
) {
413 if (headerName
== null || headerName
.length() == 0) {
414 throw new IllegalArgumentException("headerName must not be null or empty.");
416 headers
.remove(headerName
);
421 * Set the method used for this task. Defaults to {@link Method#POST}.
423 public TaskOptions
method(Method method
) {
424 this.method
= method
;
428 List
<Param
> getParams() {
432 TaskOptions
param(Param param
) {
438 * Clears the parameters.
440 public TaskOptions
clearParams() {
446 * Add a named {@link String} parameter.
447 * @param name Name of the parameter. Must not be null or empty.
448 * @param value The value of the parameter will undergo a "UTF-8"
449 * character encoding transformation upon being added to the queue.
450 * {@code value} must not be {@code null}.
451 * @throws IllegalArgumentException
453 public TaskOptions
param(String name
, String value
) {
454 return param(new StringValueParam(name
, value
));
458 * Add a named {@code byte} array parameter.
459 * @param name Name of the parameter. Must not be null or empty.
460 * @param value A byte array and encoded as-is
461 * (i.e. without character encoding transformations).
462 * {@code value} must not be {@code null}.
463 * @throws IllegalArgumentException
465 public TaskOptions
param(String name
, byte[] value
) {
466 return param(new ByteArrayValueParam(name
, value
));
470 * Remove all parameters with the given name.
471 * @param paramName Name of the parameter. Must not be null or empty.
473 public TaskOptions
removeParam(String paramName
) {
474 if (paramName
== null || paramName
.length() == 0) {
475 throw new IllegalArgumentException("paramName must not be null or empty.");
478 Iterator
<Param
> paramsIter
= params
.iterator();
479 while (paramsIter
.hasNext()) {
480 if (paramsIter
.next().name
== paramName
) {
488 public String
getUrl() {
494 * <p>Default value is {@code null}.
495 * @param url String containing URL.
497 public TaskOptions
url(String url
) {
499 throw new IllegalArgumentException("null url is not allowed.");
506 * Delay to apply to the submitted time. May be {@code null}.
508 Long
getCountdownMillis() {
509 return countdownMillis
;
513 * Set the number of milliseconds delay before execution of the task.
515 public TaskOptions
countdownMillis(long countdownMillis
) {
516 this.countdownMillis
= countdownMillis
;
521 * Returns the specified ETA for a task. May be {@code null} if not specified.
523 public Long
getEtaMillis() {
528 * Sets the approximate absolute time to execute. (i.e. etaMillis is comparable with
529 * {@link System#currentTimeMillis()}).
531 public TaskOptions
etaMillis(long etaMillis
) {
532 this.etaMillis
= etaMillis
;
537 * Returns the retry options for a task. May be {@code null} if not specified.
539 RetryOptions
getRetryOptions() {
544 * Sets retry options for this task. Retry Options must be built with
545 * {@code RetryOptions.Builder}.
547 public TaskOptions
retryOptions(RetryOptions retryOptions
) {
548 this.retryOptions
= retryOptions
;
553 * Returns the tag for a task. May be {@code null} if tag is not specified.
555 public byte[] getTagAsBytes() {
560 * Returns the tag for a task. May be {@code null} if tag is not specified.
561 * @throws UnsupportedEncodingException
563 public String
getTag() throws UnsupportedEncodingException
{
565 return new String(tag
, "UTF-8");
571 * Sets the tag for a task. Ignores null or zero-length tags.
573 public TaskOptions
tag(byte[] tag
) {
574 if (tag
!= null && tag
.length
> 0) {
575 this.tag
= tag
.clone();
583 * Sets the tag for a task. Ignores null or empty tags.
585 public TaskOptions
tag(String tag
) {
587 return tag(tag
.getBytes());
593 public int hashCode() {
594 final int prime
= 31;
596 result
= prime
* result
+ ((countdownMillis
== null) ?
0 : countdownMillis
.hashCode());
597 result
= prime
* result
+ ((etaMillis
== null) ?
0 : etaMillis
.hashCode());
598 result
= prime
* result
+ ((headers
== null) ?
0 : headers
.hashCode());
599 result
= prime
* result
+ ((method
== null) ?
0 : method
.hashCode());
600 result
= prime
* result
+ ((params
== null) ?
0 : params
.hashCode());
601 result
= prime
* result
+ Arrays
.hashCode(payload
);
602 result
= prime
* result
+ ((taskName
== null) ?
0 : taskName
.hashCode());
603 result
= prime
* result
+ ((url
== null) ?
0 : url
.hashCode());
604 result
= prime
* result
+ ((retryOptions
== null) ?
0 : retryOptions
.hashCode());
605 result
= prime
* result
+ Arrays
.hashCode(tag
);
610 public boolean equals(Object obj
) {
617 if (getClass() != obj
.getClass()) {
620 TaskOptions other
= (TaskOptions
) obj
;
621 if (countdownMillis
== null) {
622 if (other
.countdownMillis
!= null) {
625 } else if (!countdownMillis
.equals(other
.countdownMillis
)) {
628 if (etaMillis
== null) {
629 if (other
.etaMillis
!= null) {
632 } else if (!etaMillis
.equals(other
.etaMillis
)) {
635 if (headers
== null) {
636 if (other
.headers
!= null) {
639 } else if (!headers
.equals(other
.headers
)) {
642 if (method
== null) {
643 if (other
.method
!= null) {
646 } else if (!method
.equals(other
.method
)) {
649 if (params
== null) {
650 if (other
.params
!= null) {
653 } else if (!params
.equals(other
.params
)) {
656 if (!Arrays
.equals(payload
, other
.payload
)) {
659 if (taskName
== null) {
660 if (other
.taskName
!= null) {
663 } else if (!taskName
.equals(other
.taskName
)) {
667 if (other
.url
!= null) {
670 } else if (!url
.equals(other
.url
)) {
673 if (retryOptions
== null) {
674 if (other
.retryOptions
!= null) {
677 } else if (!retryOptions
.equals(other
.retryOptions
)) {
680 if (!Arrays
.equals(tag
, other
.tag
)) {
687 public String
toString() {
688 String tagString
= null;
690 tagString
= getTag();
691 } catch (UnsupportedEncodingException e
) {
692 tagString
= "not a utf-8 String";
694 return "TaskOptions[taskName=" + taskName
+ ", headers=" + headers
695 + ", method=" + method
+ ", params=" + params
+ ", url=" + url
696 + ", countdownMillis=" + countdownMillis
+ ", etaMillis=" + etaMillis
697 + ", retryOptions=" + retryOptions
+ ", tag=" + tagString
+ "]";
701 * Provides static creation methods for {@link TaskOptions}.
703 public static final class Builder
{
705 * Returns default {@link TaskOptions} and calls {@link TaskOptions#taskName(String)}.
707 public static TaskOptions
withTaskName(String taskName
) {
708 return withDefaults().taskName(taskName
);
712 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(byte[])}.
714 static TaskOptions
withPayload(byte[] payload
) {
715 return withDefaults().payload(payload
);
719 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(String, String)}.
721 public static TaskOptions
withPayload(String payload
, String charset
) {
722 return withDefaults().payload(payload
, charset
);
726 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(DeferredTask)}.
728 public static TaskOptions
withPayload(DeferredTask deferredTask
) {
729 return withDefaults().payload(deferredTask
);
733 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(byte[], String)}.
735 public static TaskOptions
withPayload(byte[] payload
, String contentType
) {
736 return withDefaults().payload(payload
, contentType
);
740 * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(String)}.
742 public static TaskOptions
withPayload(String payload
) {
743 return withDefaults().payload(payload
);
747 * Returns default {@link TaskOptions} and calls {@link TaskOptions#headers(Map)}.
749 public static TaskOptions
withHeaders(Map
<String
, String
> headers
) {
750 return withDefaults().headers(headers
);
754 * Returns default {@link TaskOptions} and calls {@link TaskOptions#header(String, String)}.
756 public static TaskOptions
withHeader(String headerName
, String value
) {
757 return withDefaults().header(headerName
, value
);
761 * Returns default {@link TaskOptions} and calls {@link TaskOptions#method(Method)}.
763 public static TaskOptions
withMethod(Method method
) {
764 return withDefaults().method(method
);
768 * Returns default {@link TaskOptions} and calls {@link TaskOptions#param(String, String)}.
770 public static TaskOptions
withParam(String paramName
, String value
) {
771 return withDefaults().param(paramName
, value
);
775 * Returns default {@link TaskOptions} and calls {@link TaskOptions#param(String, byte[])}.
777 public static TaskOptions
withParam(String paramName
, byte[] value
) {
778 return withDefaults().param(paramName
, value
);
782 * Returns default {@link TaskOptions} and calls {@link TaskOptions#url(String)}.
784 public static TaskOptions
withUrl(String url
) {
785 return withDefaults().url(url
);
789 * Returns default {@link TaskOptions} and calls {@link TaskOptions#countdownMillis(long)}.
791 public static TaskOptions
withCountdownMillis(long countdownMillis
) {
792 return withDefaults().countdownMillis(countdownMillis
);
796 * Returns default {@link TaskOptions} and calls {@link TaskOptions#etaMillis(long)}.
798 public static TaskOptions
withEtaMillis(long etaMillis
) {
799 return withDefaults().etaMillis(etaMillis
);
803 * Returns default {@link TaskOptions} and calls
804 * {@link TaskOptions#retryOptions(RetryOptions)}.
806 public static TaskOptions
withRetryOptions(RetryOptions retryOptions
) {
807 return withDefaults().retryOptions(retryOptions
);
811 * Returns default {@link TaskOptions} and calls {@link TaskOptions#tag(byte[])}.
813 public static TaskOptions
withTag(byte[] tag
) {
814 return withDefaults().tag(tag
);
818 * Returns default {@link TaskOptions} and calls {@link TaskOptions#tag(String)}.
820 public static TaskOptions
withTag(String tag
) {
821 return withDefaults().tag(tag
);
825 * Returns default {@link TaskOptions} with default values.
827 public static TaskOptions
withDefaults() {
828 return new TaskOptions();