All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.taskqueue.TaskOptions Maven / Gradle / Ivy

Go to download

API for Google App Engine standard environment with some of the dependencies shaded (repackaged)

There is a newer version: 2.0.27
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.appengine.api.taskqueue;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.appengine.api.taskqueue.TaskQueuePb.TaskQueueAddRequest.RequestMethod;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Contains various options for a task following the builder pattern. Calls to {@link TaskOptions}
 * methods may be chained to specify multiple options in the one {@link TaskOptions} object.
 *
 * 

taskOptions can have either {@link TaskOptions.Method} PULL or a PUSH-related method, e.g. * POST, GET, ... Tasks with PULL method can only be added into a PULL queue and PUSH tasks can only * be added into a PUSH queue. * *

Notes on usage:
* The recommended way to instantiate a {@link TaskOptions} object is to statically import {@link * Builder}.* and invoke a static creation method followed by an instance mutator (if needed): * *

 * import static com.google.appengine.api.taskqueue.TaskOptions.Builder.*;
 *
 * ...
 * {@link QueueFactory#getDefaultQueue()}.add(withUrl(url).etaMillis(eta));
 * 
* */ public final class TaskOptions implements Serializable { private static final long serialVersionUID = -2702019046191004750L; // The limits on dispatch deadline are consistent with Cloud Tasks API. // https://cloud.google.com/tasks/docs/reference/rpc/google.cloud.tasks.v2 private static final Duration MAX_DISPATCH_DEADLINE = Duration.ofHours(24); private static final Duration MIN_DISPATCH_DEADLINE = Duration.ofSeconds(15); /** * Methods supported by {@link Queue}. Tasks added to pull queues must have the {@link #Method} * PULL. All other methods are appropriate for push queues. See {@link Queue} for more detail on * pull and push mode queues. */ public enum Method { DELETE(RequestMethod.DELETE, false), GET(RequestMethod.GET, false), HEAD(RequestMethod.HEAD, false), POST(RequestMethod.POST, true), PUT(RequestMethod.PUT, true), // Sets PbMethod to null for PULL Method, since there's no corresponding // PbMethod for such Method. Tasks with PULL method will be translated into // an add request with pull mode. PULL(null, true); private final RequestMethod pbMethod; private final boolean isBodyMethod; Method(RequestMethod pbMethod, boolean isBodyMethod) { this.pbMethod = pbMethod; this.isBodyMethod = isBodyMethod; } RequestMethod getPbMethod() { return pbMethod; } boolean supportsBody() { return isBodyMethod; } } /** * Params are currently immutable and need to remain that way to avoid the need to clone them in * the TaskOptions copy constructor. */ abstract static class Param implements Serializable { private static final long serialVersionUID = -4677920038530554173L; final String name; Param(String name) { if (name == null || name.length() == 0) { throw new IllegalArgumentException("name must not be null or empty"); } this.name = name; } @Override public int hashCode() { return name.hashCode(); } static String encodeURLAsUtf8(String url) throws UnsupportedEncodingException { return URLEncoder.encode(url, "UTF-8"); } // Force concrete subclasses to implement equals. This is required because this class defines // hashCode(). @Override public abstract boolean equals(Object o); String getURLEncodedName() throws UnsupportedEncodingException { return encodeURLAsUtf8(name); } abstract String getURLEncodedValue() throws UnsupportedEncodingException; } static class StringValueParam extends Param { private static final long serialVersionUID = -2306561754387422446L; final String value; StringValueParam(String name, String value) { super(name); if (value == null) { throw new IllegalArgumentException("value must not be null"); } this.value = value; } @Override public String toString() { return "StringParam(" + name + "=" + value + ")"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof StringValueParam)) { return false; } StringValueParam that = (StringValueParam) o; return value.equals(that.value) && name.equals(that.name); } @Override public String getURLEncodedValue() throws UnsupportedEncodingException { return encodeURLAsUtf8(value); } } static class ByteArrayValueParam extends Param { private static final long serialVersionUID = 1420600427192025644L; protected final byte[] value; ByteArrayValueParam(String name, byte[] value) { super(name); if (value == null) { throw new IllegalArgumentException("value must not be null"); } this.value = value; } @Override public String toString() { return "ByteArrayParam(" + name + ": " + value.length + " bytes)"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ByteArrayValueParam)) { return false; } ByteArrayValueParam that = (ByteArrayValueParam) o; return Arrays.equals(value, that.value) && name.equals(that.name); } /** * {@inheritDoc} * * @throws UnsupportedEncodingException */ @Override public String getURLEncodedValue() throws UnsupportedEncodingException { // See RFC 2396, Section 2.4.1. for a description of escape encoding. // See RFC 2616, Section 3.2.3. for a description of the semantic equivalence of escape and // non-escape encoded characters. StringBuilder result = new StringBuilder(); for (int i = 0; i < value.length; i++) { // TODO - could optimize this to produce smaller payload. result.append("%"); char character = Character.toUpperCase(Character.forDigit((value[i] >> 4) & 0xF, 16)); result.append(character); character = Character.toUpperCase(Character.forDigit(value[i] & 0xF, 16)); result.append(character); } return result.toString(); } } private String taskName; private byte[] payload; private HashMap> headers; private Method method; private List params; private String url; private Long countdownMillis; private Long etaMillis; private RetryOptions retryOptions; private byte[] tag; private @Nullable Duration dispatchDeadline; private TaskOptions() { this.method = Method.POST; this.headers = new LinkedHashMap>(); this.params = new ArrayList(); } /** A copy constructor for {@link TaskOptions}. */ public TaskOptions(TaskOptions options) { taskName = options.taskName; method = options.method; url = options.url; countdownMillis = options.countdownMillis; etaMillis = options.etaMillis; tag(options.tag); this.dispatchDeadline = options.dispatchDeadline; if (options.retryOptions != null) { retryOptions = new RetryOptions(options.retryOptions); } else { retryOptions = null; } if (options.getPayload() != null) { payload(options.getPayload()); } else { payload = null; } this.headers = copyOfHeaders(options.getHeaders()); this.params = new ArrayList<>(options.getParams()); } private static LinkedHashMap> copyOfHeaders( Map> headers) { LinkedHashMap> copy = new LinkedHashMap<>(); for (Map.Entry> entry : headers.entrySet()) { copy.put(entry.getKey(), new ArrayList(entry.getValue())); } return copy; } /** * Sets the task name. * * @throws IllegalArgumentException The provided name is null, empty or doesn't match the regular * expression {@link QueueConstants#TASK_NAME_REGEX} */ public TaskOptions taskName(String taskName) { if (taskName != null && taskName.length() != 0) { TaskHandle.validateTaskName(taskName); } this.taskName = taskName; return this; } /** Returns the task name. */ public String getTaskName() { return taskName; } /** Returns the live payload for the task, not a copy. May be {@code null}. */ public byte[] getPayload() { return payload; } /** * Sets the payload directly without specifying the content-type. If this task is added to a push * queue, the content-type will be set to 'application/octet-stream' by default. * * @param payload The bytes representing the paylaod. * @return TaskOptions object for chaining. */ public TaskOptions payload(byte[] payload) { // A header for content type set to "application/octet-stream" will be added in // taskqueue_service.cc for push tasks if content type is not set in API. this.payload = payload.clone(); return this; } /** * Sets the payload to the serialized form of the deferredTask object. The payload will be * generated by serializing the deferredTask object using {@link * ObjectOutputStream#writeObject(Object)}. If the deferredTask's {@link Method} is not PULL, the * content type will be set to {@link DeferredTaskContext#RUNNABLE_TASK_CONTENT_TYPE}, the method * will be forced to {@link Method#POST} and if otherwise not specified, the url will be set to * {@link DeferredTaskContext#DEFAULT_DEFERRED_URL}; the {@link DeferredTask} servlet is, by * default, mapped to this url. * *

Note: While this may be a convenient API, it requires careful control of the serialization * compatibility of objects passed into {@link #payload(DeferredTask)} method as objects placed in * the task queue will survive revision updates of the application and hence may fail * deserialization when the task is decoded with new revisions of the application. In particular, * Java anonymous classes are convenient but may be particularly difficult to control or test for * serialization compatibility. * * @param deferredTask The object to serialize into the payload. * @throws DeferredTaskCreationException if there was an IOException serializing object. */ public TaskOptions payload(DeferredTask deferredTask) { ByteArrayOutputStream stream = new ByteArrayOutputStream(1024); ObjectOutputStream objectStream = null; try { objectStream = new ObjectOutputStream(stream); objectStream.writeObject(deferredTask); } catch (IOException e) { throw new DeferredTaskCreationException(e); } payload = stream.toByteArray(); // Don't add header or url for pull tasks. if (getMethod() != Method.PULL) { header("content-type", DeferredTaskContext.RUNNABLE_TASK_CONTENT_TYPE); method(Method.POST); if (getUrl() == null) { url(DeferredTaskContext.DEFAULT_DEFERRED_URL); } } return this; } /** * Sets the payload from a {@link String} given a specific character set. * * @throws UnsupportedTranslationException */ public TaskOptions payload(String payload, String charset) { try { return payload(payload.getBytes(charset), "text/plain; charset=" + charset); } catch (UnsupportedEncodingException exception) { throw new UnsupportedTranslationException( "Unsupported charset '" + charset + "' requested.", exception); } } /** * Set the payload with the given content type. * * @param payload The bytes representing the paylaod. * @param contentType The content-type of the bytes. * @return TaskOptions object for chaining. */ public TaskOptions payload(byte[] payload, String contentType) { return payload(payload).header("content-type", contentType); } /** * Set the payload by {@link String}. The charset to convert the String to will be UTF-8 unless * the method is PULL, in which case the String's bytes will be used directly. * * @param payload The String to be used. * @return TaskOptions object for chaining. */ public TaskOptions payload(String payload) { if (getMethod() == Method.PULL) { return payload(payload.getBytes(Charset.defaultCharset())); } return payload(payload, "UTF-8"); } /** * Returns a copy of the task's headers as a map from each header name to a list of values for * that header name. */ public Map> getHeaders() { return copyOfHeaders(headers); } /** * Replaces the existing headers with the provided header {@code name/value} mapping. * * @param headers The headers to copy. * @return TaskOptions object for chaining. */ public TaskOptions headers(Map headers) { this.headers = new LinkedHashMap>(); for (Map.Entry entry : headers.entrySet()) { List values = new ArrayList<>(1); values.add(entry.getValue()); this.headers.put(entry.getKey(), values); } return this; } /** * Adds a header {@code name/value} pair. * * @throws IllegalArgumentException */ public TaskOptions header(String headerName, String value) { if (headerName == null || headerName.length() == 0) { throw new IllegalArgumentException("headerName must not be null or empty."); } if (value == null) { throw new IllegalArgumentException("header(name, ) is not allowed."); } headers.computeIfAbsent(headerName, unused -> new ArrayList<>()).add(value); return this; } /** Remove all headers with the given name. */ public TaskOptions removeHeader(String headerName) { if (headerName == null || headerName.length() == 0) { throw new IllegalArgumentException("headerName must not be null or empty."); } headers.remove(headerName); return this; } /** Returns the method used for this task. */ public Method getMethod() { return method; } /** Set the method used for this task. Defaults to {@link Method#POST}. */ public TaskOptions method(Method method) { this.method = method; return this; } /** * Returns a copy of the task's string-valued parameters as a map from each parameter name to a * list of values for that name. */ public Map> getStringParams() { LinkedHashMap> stringParams = new LinkedHashMap<>(); for (Param param : params) { if (param instanceof StringValueParam) { if (!stringParams.containsKey(param.name)) { stringParams.put(param.name, new ArrayList()); } stringParams.get(param.name).add(((StringValueParam) param).value); } } return stringParams; } /** * Returns a copy of the task's byte-array-valued parameters as a map from each parameter name to * a list of values for that name. */ public Map> getByteArrayParams() { LinkedHashMap> byteArrayParams = new LinkedHashMap<>(); for (Param param : params) { if (param instanceof ByteArrayValueParam) { if (!byteArrayParams.containsKey(param.name)) { byteArrayParams.put(param.name, new ArrayList()); } byte[] value = ((ByteArrayValueParam) param).value; byteArrayParams.get(param.name).add(Arrays.copyOf(value, value.length)); } } return byteArrayParams; } List getParams() { return params; } TaskOptions param(Param param) { params.add(param); return this; } /** Clears the parameters. */ public TaskOptions clearParams() { params.clear(); return this; } /** * Add a named {@link String} parameter. * * @param name Name of the parameter. Must not be null or empty. * @param value The value of the parameter will undergo a "UTF-8" character encoding * transformation upon being added to the queue. {@code value} must not be {@code null}. * @throws IllegalArgumentException */ public TaskOptions param(String name, String value) { return param(new StringValueParam(name, value)); } /** * Add a named {@code byte} array parameter. * * @param name Name of the parameter. Must not be null or empty. * @param value A byte array and encoded as-is (i.e. without character encoding transformations). * {@code value} must not be {@code null}. * @throws IllegalArgumentException */ public TaskOptions param(String name, byte[] value) { return param(new ByteArrayValueParam(name, value)); } /** * Remove all parameters with the given name. * * @param paramName Name of the parameter. Must not be null or empty. */ public TaskOptions removeParam(String paramName) { if (paramName == null || paramName.length() == 0) { throw new IllegalArgumentException("paramName must not be null or empty."); } Iterator paramsIter = params.iterator(); while (paramsIter.hasNext()) { if (paramName.equals(paramsIter.next().name)) { paramsIter.remove(); } } return this; } public String getUrl() { return url; } /** * Set the URL. * *

Default value is {@code null}. * * @param url String containing URL. */ public TaskOptions url(String url) { if (url == null) { throw new IllegalArgumentException("null url is not allowed."); } this.url = url; return this; } /** Returns the delay to apply to the submitted time. May be {@code null}. */ public Long getCountdownMillis() { return countdownMillis; } /** Set the number of milliseconds delay before execution of the task. */ public TaskOptions countdownMillis(long countdownMillis) { this.countdownMillis = countdownMillis; return this; } /** Returns the specified ETA for a task. May be {@code null} if not specified. */ public Long getEtaMillis() { return etaMillis; } /** * Sets the approximate absolute time to execute. (i.e. etaMillis is comparable with {@link * System#currentTimeMillis()}). */ public TaskOptions etaMillis(long etaMillis) { this.etaMillis = etaMillis; return this; } /** Returns a copy of the retry options for a task. May be {@code null} if not specified. */ public RetryOptions getRetryOptions() { return retryOptions == null ? null : new RetryOptions(retryOptions); } /** * Sets retry options for this task. Retry Options must be built with {@code * RetryOptions.Builder}. */ public TaskOptions retryOptions(RetryOptions retryOptions) { this.retryOptions = retryOptions; return this; } /** * Returns the live tag bytes for a task, not a copy. May be {@code null} if tag is not specified. */ public byte[] getTagAsBytes() { return tag; } /** * Returns the tag for a task. May be {@code null} if tag is not specified. * * @throws UnsupportedEncodingException */ public String getTag() throws UnsupportedEncodingException { if (tag != null) { return new String(tag, UTF_8); } return null; } /** * Returns the dispatch deadline for a task. The dispatch deadline determines how long the of a * task should take. If a task exceeds its deadline, it will be canceled and retried based on * retry configurations of the task and/or queue. */ public @Nullable Duration getDispatchDeadline() { return dispatchDeadline; } /** Sets the tag for a task. Ignores null or zero-length tags. */ public TaskOptions tag(byte[] tag) { if (tag != null && tag.length > 0) { this.tag = tag.clone(); } else { this.tag = null; } return this; } /** Sets the tag for a task. Ignores null or empty tags. */ public TaskOptions tag(String tag) { if (tag != null) { return tag(tag.getBytes(UTF_8)); } return this; } /** * Sets the dispatch deadline for a task. The dispatch deadline should be no smaller than * TaskOptions#MIN_DISPATCH_DEADLINE and no larger than TaskOptions#MAX_DISPATCH_DEADLINE. */ public TaskOptions dispatchDeadline(Duration dispatchDeadline) { checkNotNull(dispatchDeadline); checkArgument( dispatchDeadline.compareTo(MIN_DISPATCH_DEADLINE) >= 0, "Dispatch deadline %s must be at least %s.", dispatchDeadline, MIN_DISPATCH_DEADLINE); checkArgument( dispatchDeadline.compareTo(MAX_DISPATCH_DEADLINE) <= 0, "Dispatch deadline %s must be at most %s.", dispatchDeadline, MAX_DISPATCH_DEADLINE); this.dispatchDeadline = dispatchDeadline; return this; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((countdownMillis == null) ? 0 : countdownMillis.hashCode()); result = prime * result + ((etaMillis == null) ? 0 : etaMillis.hashCode()); result = prime * result + ((headers == null) ? 0 : headers.hashCode()); result = prime * result + ((method == null) ? 0 : method.hashCode()); result = prime * result + ((params == null) ? 0 : params.hashCode()); result = prime * result + Arrays.hashCode(payload); result = prime * result + ((taskName == null) ? 0 : taskName.hashCode()); result = prime * result + ((url == null) ? 0 : url.hashCode()); result = prime * result + ((retryOptions == null) ? 0 : retryOptions.hashCode()); result = prime * result + Arrays.hashCode(tag); result = prime * result + ((dispatchDeadline == null) ? 0 : dispatchDeadline.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } TaskOptions other = (TaskOptions) obj; if (countdownMillis == null) { if (other.countdownMillis != null) { return false; } } else if (!countdownMillis.equals(other.countdownMillis)) { return false; } if (etaMillis == null) { if (other.etaMillis != null) { return false; } } else if (!etaMillis.equals(other.etaMillis)) { return false; } if (headers == null) { if (other.headers != null) { return false; } } else if (!headers.equals(other.headers)) { return false; } if (method == null) { if (other.method != null) { return false; } } else if (!method.equals(other.method)) { return false; } if (params == null) { if (other.params != null) { return false; } } else if (!params.equals(other.params)) { return false; } if (!Arrays.equals(payload, other.payload)) { return false; } if (taskName == null) { if (other.taskName != null) { return false; } } else if (!taskName.equals(other.taskName)) { return false; } if (url == null) { if (other.url != null) { return false; } } else if (!url.equals(other.url)) { return false; } if (retryOptions == null) { if (other.retryOptions != null) { return false; } } else if (!retryOptions.equals(other.retryOptions)) { return false; } if (!Arrays.equals(tag, other.tag)) { return false; } return Objects.equals(dispatchDeadline, other.dispatchDeadline); } @Override public String toString() { String tagString = null; try { tagString = getTag(); } catch (UnsupportedEncodingException e) { tagString = "not a utf-8 String"; } return "TaskOptions[taskName=" + taskName + ", headers=" + headers + ", method=" + method + ", params=" + params + ", url=" + url + ", countdownMillis=" + countdownMillis + ", etaMillis=" + etaMillis + ", retryOptions=" + retryOptions + ", tag=" + tagString + ", dispatchDeadline=" + dispatchDeadline + "]"; } /** Provides static creation methods for {@link TaskOptions}. */ public static final class Builder { /** Returns default {@link TaskOptions} and calls {@link TaskOptions#taskName(String)}. */ public static TaskOptions withTaskName(String taskName) { return withDefaults().taskName(taskName); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(byte[])}. */ static TaskOptions withPayload(byte[] payload) { return withDefaults().payload(payload); } /** * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(String, String)}. */ public static TaskOptions withPayload(String payload, String charset) { return withDefaults().payload(payload, charset); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(DeferredTask)}. */ public static TaskOptions withPayload(DeferredTask deferredTask) { return withDefaults().payload(deferredTask); } /** * Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(byte[], String)}. */ public static TaskOptions withPayload(byte[] payload, String contentType) { return withDefaults().payload(payload, contentType); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#payload(String)}. */ public static TaskOptions withPayload(String payload) { return withDefaults().payload(payload); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#headers(Map)}. */ public static TaskOptions withHeaders(Map headers) { return withDefaults().headers(headers); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#header(String, String)}. */ public static TaskOptions withHeader(String headerName, String value) { return withDefaults().header(headerName, value); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#method(Method)}. */ public static TaskOptions withMethod(Method method) { return withDefaults().method(method); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#param(String, String)}. */ public static TaskOptions withParam(String paramName, String value) { return withDefaults().param(paramName, value); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#param(String, byte[])}. */ public static TaskOptions withParam(String paramName, byte[] value) { return withDefaults().param(paramName, value); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#url(String)}. */ public static TaskOptions withUrl(String url) { return withDefaults().url(url); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#countdownMillis(long)}. */ public static TaskOptions withCountdownMillis(long countdownMillis) { return withDefaults().countdownMillis(countdownMillis); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#etaMillis(long)}. */ public static TaskOptions withEtaMillis(long etaMillis) { return withDefaults().etaMillis(etaMillis); } /** * Returns default {@link TaskOptions} and calls {@link TaskOptions#retryOptions(RetryOptions)}. */ public static TaskOptions withRetryOptions(RetryOptions retryOptions) { return withDefaults().retryOptions(retryOptions); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#tag(byte[])}. */ public static TaskOptions withTag(byte[] tag) { return withDefaults().tag(tag); } /** Returns default {@link TaskOptions} and calls {@link TaskOptions#tag(String)}. */ public static TaskOptions withTag(String tag) { return withDefaults().tag(tag); } /** Returns default {@link TaskOptions} with default values. */ public static TaskOptions withDefaults() { return new TaskOptions(); } private Builder() {} } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy