Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
*
* 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 http://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.vmware.xenon.common;
import static java.lang.String.format;
import java.net.HttpURLConnection;
import java.net.URI;
import java.security.Principal;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.Consumer;
import javax.security.cert.X509Certificate;
import com.vmware.xenon.common.Service.Action;
import com.vmware.xenon.common.ServiceDocumentDescription.Builder;
import com.vmware.xenon.common.ServiceErrorResponse.ErrorDetail;
import com.vmware.xenon.common.ServiceHost.ServiceNotFoundException;
import com.vmware.xenon.common.serialization.KryoSerializers;
import com.vmware.xenon.services.common.GuestUserService;
import com.vmware.xenon.services.common.QueryFilter;
import com.vmware.xenon.services.common.QueryTask.Query;
import com.vmware.xenon.services.common.SystemUserService;
/**
* Service operation container. Encapsulates the request / response pattern of client to service and
* service to service asynchronous communication
*/
public class Operation implements Cloneable {
/**
* Portion of serialized JSON body string to include in {@code toString}
*/
private static final int TO_STRING_SERIALIZED_BODY_LIMIT = 256;
@FunctionalInterface
public interface CompletionHandler {
void handle(Operation completedOp, Throwable failure);
}
public static class SocketContext {
private long lastUseTimeMicros;
public long getLastUseTimeMicros() {
return this.lastUseTimeMicros;
}
public void updateLastUseTime() {
this.lastUseTimeMicros = Utils.getSystemNowMicrosUtc();
}
public void writeHttpRequest(Object request) {
throw new IllegalStateException();
}
public void close() {
throw new IllegalStateException();
}
}
static class InstrumentationContext {
long handleInvokeTimeMicros;
long enqueueTimeMicros;
long documentStoreCompletionTimeMicros;
long handlerCompletionTimeMicros;
long operationCompletionTimeMicros;
}
/**
* Operation metadata being sent to the transaction coordinator.
*/
public static class TransactionContext {
/**
* Action the service received
*/
public Action action;
/**
* Set of pending transactions on the same service
*/
public Set coordinatorLinks;
/**
* Notify whether the service completed (true) or failed (false) the operation
*/
public boolean isSuccessful;
}
static class RemoteContext {
SocketContext socketCtx;
Map requestHeaders;
Map responseHeaders;
Principal peerPrincipal;
X509Certificate[] peerCertificateChain;
String connectionTag;
Map cookies;
}
/**
* An operation's authorization context.
*
* The {@link Claims} in this context was originally set by an authentication
* if the operation originated from a remote client and the claims were encoded
* in a token. If the operation is an internal derivative of such an operation,
* the authorization context is inherited so that the claims object doesn't
* need to be deserialized multiple times.
*/
public static final class AuthorizationContext {
/**
* Set of claims for this authorization context.
*/
private Claims claims;
/**
* Token representation for this set of claims.
*
* This field is only kept in this field so that we don't have to recreate the token
* when this context is inherited and used for a request to a peer node.
*/
private String token;
/**
* Whether this context should propagate to a client or not.
*
* If it is, the transport layer will propagate the context back to the client.
* In the case of netty/http, it will add a Set-Cookie header for the token.
*/
private boolean propagateToClient = false;
/**
* The resource query is a composite query constructed by grouping all
* resource group queries that apply to this user's authorization context.
*/
private Map resourceQueryMap = null;
/**
* The resource query filter is a query filter of the composite query
* constructed by grouping all resource group queries that apply to
* this user's authorization context.
*/
private Map resourceQueryFiltersMap = null;
public Claims getClaims() {
return this.claims;
}
public String getToken() {
return this.token;
}
public boolean shouldPropagateToClient() {
return this.propagateToClient;
}
public Query getResourceQuery(Action action) {
if (this.resourceQueryMap == null) {
return null;
}
return Utils.clone(this.resourceQueryMap.get(action));
}
public QueryFilter getResourceQueryFilter(Action action) {
if (this.resourceQueryFiltersMap == null) {
return null;
}
return this.resourceQueryFiltersMap.get(action);
}
public boolean isSystemUser() {
return isUserSubject(SystemUserService.SELF_LINK);
}
public boolean isGuestUser() {
return isUserSubject(GuestUserService.SELF_LINK);
}
private boolean isUserSubject(String userLink) {
Claims claims = getClaims();
if (claims == null) {
return false;
}
String subject = claims.getSubject();
if (subject == null) {
return false;
}
return subject.equals(userLink);
}
public static class Builder {
private AuthorizationContext authorizationContext;
public static Builder create() {
return new Builder();
}
private Builder() {
initialize();
}
protected void initialize() {
this.authorizationContext = new AuthorizationContext();
}
public AuthorizationContext getResult() {
AuthorizationContext result = this.authorizationContext;
initialize();
return result;
}
public Builder setClaims(Claims claims) {
this.authorizationContext.claims = claims;
return this;
}
public Builder setToken(String token) {
this.authorizationContext.token = token;
return this;
}
public Builder setPropagateToClient(boolean propagateToClient) {
this.authorizationContext.propagateToClient = propagateToClient;
return this;
}
public Builder setResourceQueryMap(Map resourceQueryMap) {
this.authorizationContext.resourceQueryMap = resourceQueryMap;
return this;
}
public Builder setResourceQueryFilterMap(
Map resourceQueryFiltersMap) {
this.authorizationContext.resourceQueryFiltersMap = resourceQueryFiltersMap;
return this;
}
}
}
public enum OperationOption {
/**
* Set to request underlying support for overlapping operations on the same connection.
* For example, if set, and the service client is HTTP/2 aware, the operation will use the
* same connection as many others, pending, operations
*/
CONNECTION_SHARING,
/**
* Set by the client to both request a long lived connection on out-bound requests,
* or indicate the operation was received on a long lived connection, for in-bound requests
*/
KEEP_ALIVE,
/**
* Set on both out-bound and in-bound replicated updates
*/
REPLICATED,
/**
* Set on both out-bound and in-bound forwarded requests
*/
FORWARDED,
/**
* Set to prevent replication
*/
REPLICATION_DISABLED,
/**
* Set by request listener to prevent cloning of the body during
* {@link Operation#setBody(Object)}
*/
CLONING_DISABLED,
/**
* Set to prevent notifications being sent after the service handler completes the
* operation
*/
NOTIFICATION_DISABLED,
/**
* Set if the target service is replicated
*/
REPLICATED_TARGET,
/**
* Set by client to disable default logging of operation failures
*/
FAILURE_LOGGING_DISABLED,
/**
* Set by a local {@code ServiceRequestListener} instance indicating the operation
* originated outside the process / host
*/
REMOTE,
/**
* The operation exceeded the rate limit associated with it logical context
* (authorization subject by default)
*/
RATE_LIMITED,
/**
* Infrastructure use only
*
* Set by transport/client to indicate the operation has an active socket
* channel associated with it.
*/
SOCKET_ACTIVE
}
public static class SerializedOperation extends ServiceDocument {
public Action action;
public String host;
public int port;
public String path;
public String query;
public Long id;
public URI referer;
public String jsonBody;
public int statusCode;
public EnumSet options;
public String contextId;
public String transactionId;
public String userInfo;
public static final ServiceDocumentDescription DESCRIPTION = Operation.SerializedOperation
.buildDescription();
public static final String KIND = Utils.buildKind(Operation.SerializedOperation.class);
public static SerializedOperation create(Operation op) {
SerializedOperation ctx = new SerializedOperation();
ctx.contextId = op.getContextId();
ctx.action = op.action;
ctx.referer = op.getReferer();
ctx.id = op.id;
ctx.statusCode = op.statusCode;
ctx.options = op.options.clone();
ctx.transactionId = op.getTransactionId();
if (op.uri != null) {
ctx.host = op.uri.getHost();
ctx.port = op.uri.getPort();
ctx.path = op.uri.getPath();
ctx.query = op.uri.getQuery();
ctx.userInfo = op.uri.getUserInfo();
}
Object body = op.getBodyRaw();
if (body instanceof String) {
ctx.jsonBody = (String) body;
} else {
ctx.jsonBody = Utils.toJson(body);
}
ctx.documentKind = KIND;
ctx.documentExpirationTimeMicros = op.expirationMicrosUtc;
return ctx;
}
public static boolean isValid(SerializedOperation sop) {
if (sop.action == null || sop.id == null || sop.jsonBody == null || sop.path == null
|| sop.referer == null) {
return false;
}
return true;
}
public static ServiceDocumentDescription buildDescription() {
EnumSet options = EnumSet.of(Service.ServiceOption.PERSISTENCE);
return Builder.create().buildDescription(SerializedOperation.class, options);
}
}
public static void fail(Operation request, int statusCode, int errorCode, Throwable e) {
request.setStatusCode(statusCode);
ServiceErrorResponse r = Utils.toServiceErrorResponse(e);
r.statusCode = statusCode;
r.errorCode = errorCode;
if (e instanceof ServiceNotFoundException) {
r.stackTrace = null;
}
request.setContentType(Operation.MEDIA_TYPE_APPLICATION_JSON).fail(e, r);
}
static void failOwnerMismatch(Operation op, String id, ServiceDocument body) {
String owner = body != null ? body.documentOwner : "";
op.setStatusCode(Operation.STATUS_CODE_CONFLICT);
Throwable e = new IllegalStateException(format(
"Owner in body: %s, computed locally: %s",
owner, id));
ServiceErrorResponse rsp = ServiceErrorResponse.create(e, op.getStatusCode(),
EnumSet.of(ErrorDetail.SHOULD_RETRY));
rsp.setInternalErrorCode(ServiceErrorResponse.ERROR_CODE_OWNER_MISMATCH);
op.fail(e, rsp);
}
public static void failActionNotSupported(Operation request) {
request.setStatusCode(Operation.STATUS_CODE_BAD_METHOD).fail(
new IllegalStateException("Action not supported: " + request.getAction()));
}
public static void failLimitExceeded(Operation request, int errorCode, String queueDescription) {
// Add a header indicating retry should be attempted after some interval.
// Currently set to just one second, subject to change in the future
request.addResponseHeader(Operation.RETRY_AFTER_HEADER, "1");
fail(request, Operation.STATUS_CODE_UNAVAILABLE,
errorCode,
new CancellationException(format("queue limit exceeded (%s)", queueDescription)));
}
public static void failForwardedRequest(Operation op, Operation fo, Throwable fe) {
op.setStatusCode(fo.getStatusCode());
op.setBodyNoCloning(fo.getBodyRaw()).fail(fe);
}
public static void failServiceNotFound(Operation inboundOp) {
failServiceNotFound(inboundOp,
ServiceErrorResponse.ERROR_CODE_INTERNAL_MASK);
}
public static void failServiceNotFound(Operation inboundOp, int errorCode,
String errorMsg) {
fail(inboundOp, Operation.STATUS_CODE_NOT_FOUND,
errorCode,
new ServiceNotFoundException(inboundOp.getUri().toString(), errorMsg));
}
public static void failServiceNotFound(Operation inboundOp, int errorCode) {
fail(inboundOp, Operation.STATUS_CODE_NOT_FOUND,
errorCode,
new ServiceNotFoundException(inboundOp.getUri().toString()));
}
static void failServiceMarkedDeleted(String documentSelfLink,
Operation serviceStartPost) {
fail(serviceStartPost, Operation.STATUS_CODE_CONFLICT,
ServiceErrorResponse.ERROR_CODE_STATE_MARKED_DELETED,
new IllegalStateException("Service marked deleted: "
+ documentSelfLink));
}
// HTTP Header definitions
public static final String REFERER_HEADER = "referer";
public static final String CONNECTION_HEADER = "connection";
public static final String CONTENT_TYPE_HEADER = "content-type";
public static final String CONTENT_ENCODING_HEADER = "content-encoding";
public static final String CONTENT_LENGTH_HEADER = "content-length";
public static final String CONTENT_RANGE_HEADER = "content-range";
public static final String RANGE_HEADER = "range";
public static final String RETRY_AFTER_HEADER = "retry-after";
public static final String PRAGMA_HEADER = "pragma";
public static final String SET_COOKIE_HEADER = "set-cookie";
public static final String COOKIE_HEADER = "cookie";
public static final String LOCATION_HEADER = "location";
public static final String USER_AGENT_HEADER = "user-agent";
public static final String HOST_HEADER = "host";
public static final String ACCEPT_HEADER = "accept";
public static final String ACCEPT_ENCODING_HEADER = "accept-encoding";
public static final String AUTHORIZATION_HEADER = "authorization";
public static final String ACCEPT_LANGUAGE_HEADER = "accept-language";
public static final String LAST_EVENT_ID_HEADER = "last-event-id";
// HTTP2 Header definitions
public static final String STREAM_ID_HEADER = "x-http2-stream-id";
public static final String STREAM_WEIGHT_HEADER = "x-http2-stream-weight";
public static final String HTTP2_SCHEME_HEADER = "x-http2-scheme";
// Proprietary header definitions
public static final String HEADER_NAME_PREFIX = "x-xenon-";
public static final String CONTEXT_ID_HEADER = HEADER_NAME_PREFIX + "ctx-id";
public static final String REQUEST_AUTH_TOKEN_HEADER = HEADER_NAME_PREFIX
+ "auth-token";
public static final String REPLICATION_PHASE_HEADER = HEADER_NAME_PREFIX
+ "rpl-phase";
public static final String REPLICATION_QUORUM_HEADER = HEADER_NAME_PREFIX
+ "rpl-quorum";
public static final String REPLICATION_PARENT_HEADER = HEADER_NAME_PREFIX + "rpl-parent";
public static final String REPLICATION_QUORUM_HEADER_VALUE_ALL = HEADER_NAME_PREFIX
+ "all";
public static final String TRANSACTION_HEADER = HEADER_NAME_PREFIX
+ "tx-phase";
public static final String TRANSACTION_ID_HEADER = HEADER_NAME_PREFIX + "tx-id";
public static final String TRANSACTION_REFLINK_HEADER = HEADER_NAME_PREFIX + "tx-reflink";
/**
* Infrastructure use only. Set when a service is first created due to a client request. Since
* service start can be invoked by the runtime during node synchronization, restart, this
* directive is the only way to distinguish original creation of arbitrary services (without relying
* on state version heuristics)
*/
public static final String PRAGMA_DIRECTIVE_CREATED = "xn-created";
/**
* Infrastructure use only. Set when the runtime, as part of its consensus protocol,
* forwards a client request from one node, to a node deemed as the owner of the service
*/
public static final String PRAGMA_DIRECTIVE_FORWARDED = "xn-fwd";
/**
* Infrastructure use only. Set when the consensus protocol replicates an update, on the owner
* node service instance, to the peer replica instances
*/
public static final String PRAGMA_DIRECTIVE_REPLICATED = "xn-rpl";
/**
* Infrastructure use only. Set when the Synchronization task makes a POST request
* to trigger synchronization of a service instance. This post request is processed
* by the OWNER of the service instance.
*/
public static final String PRAGMA_DIRECTIVE_SYNCH_OWNER = "xn-synch-owner";
/**
* Infrastructure use only. Set when the owner node of a service instance
* computes the best state as part of synchronization and broadcasts the
* best state to all peer nodes.
*/
public static final String PRAGMA_DIRECTIVE_SYNCH_PEER = "xn-synch-peer";
/**
* Advanced use. Instructs the runtime to queue a request, for a service to become available.
*/
public static final String PRAGMA_DIRECTIVE_QUEUE_FOR_SERVICE_AVAILABILITY = "xn-queue";
/**
* Infrastructure use only. Instructs the runtime that this request should be processed on the node
* it arrived on. It should not be forwarded regardless of owner selection and load balancing decisions.
*/
public static final String PRAGMA_DIRECTIVE_NO_FORWARDING = "xn-no-fwd";
/**
* Infrastructure use only. Set on notifications only.
*/
public static final String PRAGMA_DIRECTIVE_NOTIFICATION = "xn-nt";
/**
* Infrastructure use only. Set by the runtime to inform a subscriber that they might have missed notifications
* due to state updates occurring when the subscriber was not available.
*/
public static final String PRAGMA_DIRECTIVE_SKIPPED_NOTIFICATIONS = "xn-nt-skipped";
/**
* Infrastructure use only. Does a strict update version check and if the service exists or
* the service has been previously deleted the request will fail.
*
* Overridden by: PRAGMA_DIRECTIVE_FORCE_INDEX_UPDATE
*/
public static final String PRAGMA_DIRECTIVE_VERSION_CHECK = "xn-check-version";
/**
* Infrastructure use only. Used for on demand load of services. Checks the index if a service
* exists.
*/
public static final String PRAGMA_DIRECTIVE_INDEX_CHECK = "xn-check-index";
/**
* Instructs a persisted service to force the update, overriding version checks. It should be used
* for conflict resolution only.
*
* Overrides: PRAGMA_DIRECTIVE_VERSION_CHECK
*/
public static final String PRAGMA_DIRECTIVE_FORCE_INDEX_UPDATE = "xn-force-index-update";
/**
* Infrastructure use only. Instructs a persisted service to complete the operation but skip any index
* updates.
*/
public static final String PRAGMA_DIRECTIVE_NO_INDEX_UPDATE = "xn-no-index-update";
/**
* Infrastructure use only. Instructs AuthorizationContextService to treat this as a request to
* clear the authz cache
*/
public static final String PRAGMA_DIRECTIVE_CLEAR_AUTH_CACHE = "xn-clear-auth-cache";
/**
* Infrastructure use only. Debugging only. Indicates this operation was converted from POST to PUT
* due to {@link com.vmware.xenon.common.Service.ServiceOption#IDEMPOTENT_POST}
*/
public static final String PRAGMA_DIRECTIVE_POST_TO_PUT = "xn-post-to-put";
/**
* Infrastructure use only. Instructs AuthenticationService to treat this as a request to
* authenticate and retrieve the auth token
*/
public static final String PRAGMA_DIRECTIVE_AUTHENTICATE = "xn-authn";
/**
* Infrastructure use only. Instructs AuthenticationService to treat this as a request to
* verify the auth token
*/
public static final String PRAGMA_DIRECTIVE_VERIFY_TOKEN = "xn-verify-token";
/**
* Infrastructure use only. Instructs AuthenticationService to treat this as a request to
* logout and invalidate the auth token
*/
public static final String PRAGMA_DIRECTIVE_AUTHN_INVALIDATE = "xn-authn-invalidate";
/**
* Set when a MigrationTaskService invokes operations against the destination cluster.
*/
public static final String PRAGMA_DIRECTIVE_FROM_MIGRATION_TASK = "xn-from-migration";
/**
* Infrastructure use only. Indicate document state was not modified by the update request.
* When this pragma is set in handler methods, rest of the pipeline will not update the
* indexed/cached state.
*/
public static final String PRAGMA_DIRECTIVE_STATE_NOT_MODIFIED = "xn-state-not-modified";
public static final String TX_ENSURE_COMMIT = "ensure-commit";
public static final String TX_COMMIT = "commit";
public static final String TX_ABORT = "abort";
public static final String REPLICATION_PHASE_COMMIT = "commit";
public static final String MEDIA_TYPE_APPLICATION_JSON = "application/json";
public static final String MEDIA_TYPE_TEXT_YAML = "text/x-yaml";
public static final String MEDIA_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream";
public static final String MEDIA_TYPE_APPLICATION_KRYO_OCTET_STREAM = "application/kryo-octet-stream";
public static final String MEDIA_TYPE_APPLICATION_X_WWW_FORM_ENCODED = "application/x-www-form-urlencoded";
public static final String MEDIA_TYPE_TEXT_HTML = "text/html";
public static final String MEDIA_TYPE_TEXT_PLAIN = "text/plain";
public static final String MEDIA_TYPE_TEXT_CSS = "text/css";
public static final String MEDIA_TYPE_APPLICATION_JAVASCRIPT = "application/javascript";
public static final String MEDIA_TYPE_IMAGE_SVG_XML = "image/svg+xml";
public static final String MEDIA_TYPE_APPLICATION_FONT_WOFF2 = "application/font-woff2";
public static final String MEDIA_TYPE_TEXT_EVENT_STREAM = "text/event-stream";
public static final String CONTENT_ENCODING_GZIP = "gzip";
public static final int STATUS_CODE_SERVER_FAILURE_THRESHOLD = HttpURLConnection.HTTP_INTERNAL_ERROR;
public static final int STATUS_CODE_FAILURE_THRESHOLD = HttpURLConnection.HTTP_BAD_REQUEST;
public static final int STATUS_CODE_UNAUTHORIZED = HttpURLConnection.HTTP_UNAUTHORIZED;
public static final int STATUS_CODE_UNAVAILABLE = HttpURLConnection.HTTP_UNAVAILABLE;
public static final int STATUS_CODE_FORBIDDEN = HttpURLConnection.HTTP_FORBIDDEN;
public static final int STATUS_CODE_TIMEOUT = HttpURLConnection.HTTP_CLIENT_TIMEOUT;
public static final int STATUS_CODE_CONFLICT = HttpURLConnection.HTTP_CONFLICT;
public static final int STATUS_CODE_NOT_MODIFIED = HttpURLConnection.HTTP_NOT_MODIFIED;
public static final int STATUS_CODE_NOT_FOUND = HttpURLConnection.HTTP_NOT_FOUND;
public static final int STATUS_CODE_MOVED_PERM = HttpURLConnection.HTTP_MOVED_PERM;
public static final int STATUS_CODE_MOVED_TEMP = HttpURLConnection.HTTP_MOVED_TEMP;
public static final int STATUS_CODE_OK = HttpURLConnection.HTTP_OK;
public static final int STATUS_CODE_CREATED = HttpURLConnection.HTTP_CREATED;
public static final int STATUS_CODE_ACCEPTED = HttpURLConnection.HTTP_ACCEPTED;
public static final int STATUS_CODE_BAD_REQUEST = HttpURLConnection.HTTP_BAD_REQUEST;
public static final int STATUS_CODE_BAD_METHOD = HttpURLConnection.HTTP_BAD_METHOD;
public static final int STATUS_CODE_INTERNAL_ERROR = HttpURLConnection.HTTP_INTERNAL_ERROR;
public static final String MEDIA_TYPE_EVERYTHING_WILDCARDS = "*/*";
public static final String EMPTY_JSON_BODY = "{}";
public static final String HEADER_FIELD_VALUE_SEPARATOR = ":";
public static final String CR_LF = "\r\n";
private static final char DIRECTIVE_PRAGMA_VALUE_SEPARATOR_CHAR_CONST = ';';
private static final char HEADER_FIELD_VALUE_SEPARATOR_CHAR_CONST = ':';
private static final AtomicLong idCounter = new AtomicLong();
private static final AtomicReferenceFieldUpdater completionUpdater = AtomicReferenceFieldUpdater
.newUpdater(Operation.class, CompletionHandler.class, "completion");
private URI uri;
private Object referer;
private final long id = idCounter.incrementAndGet();
private int statusCode = Operation.STATUS_CODE_OK;
private Action action;
private ServiceDocument linkedState;
private byte[] linkedSerializedState;
private volatile CompletionHandler completion;
private String contextId;
private String transactionId;
private long expirationMicrosUtc;
private Object body;
private Object serializedBody;
private String contentType = MEDIA_TYPE_APPLICATION_JSON;
private long contentLength;
private RemoteContext remoteCtx;
private AuthorizationContext authorizationCtx;
private InstrumentationContext instrumentationCtx;
private short retryCount;
private short retriesRemaining;
private EnumSet options = EnumSet.of(OperationOption.KEEP_ALIVE);
private volatile Consumer serverSentEventHandler;
private volatile Consumer headersReceivedHandler;
public static Operation create(SerializedOperation ctx, ServiceHost host) {
Operation op = new Operation();
op.action = ctx.action;
op.body = ctx.jsonBody;
op.expirationMicrosUtc = ctx.documentExpirationTimeMicros;
op.setContextId(ctx.id.toString());
op.referer = ctx.referer;
op.uri = UriUtils.buildUri(host, ctx.path, ctx.query, ctx.userInfo);
op.transactionId = ctx.transactionId;
return op;
}
public Operation() {
// Set operation context from thread local.
// The thread local is populated by the service host when it handles an operation,
// which means that derivative operations will automatically inherit this context.
// It is set as early as possible since there is a possibility that it is
// overridden by the service implementation (i.e. when it impersonates).
OperationContext opCtx = OperationContext.getOperationContextNoCloning();
this.authorizationCtx = opCtx.authContext;
this.transactionId = opCtx.transactionId;
this.contextId = opCtx.contextId;
}
static Operation createOperation(Action action, URI uri) {
Operation op = new Operation();
op.uri = uri;
op.action = action;
return op;
}
public static Operation createPost(Service sender, String targetPath) {
return createPost(sender.getHost(), targetPath);
}
public static Operation createPost(ServiceHost sender, String targetPath) {
return createPost(UriUtils.buildUri(sender, targetPath));
}
public static Operation createPost(URI uri) {
return createOperation(Action.POST, uri);
}
public static Operation createPatch(Service sender, String targetPath) {
return createPatch(sender.getHost(), targetPath);
}
public static Operation createPatch(ServiceHost sender, String targetPath) {
return createPatch(UriUtils.buildUri(sender, targetPath));
}
public static Operation createPatch(URI uri) {
return createOperation(Action.PATCH, uri);
}
public static Operation createPut(Service sender, String targetPath) {
return createPut(UriUtils.buildUri(sender.getHost(), targetPath));
}
public static Operation createPut(ServiceHost sender, String targetPath) {
return createPut(UriUtils.buildUri(sender, targetPath));
}
public static Operation createPut(URI uri) {
return createOperation(Action.PUT, uri);
}
public static Operation createOptions(Service sender, String targetPath) {
return createOptions(UriUtils.buildUri(sender.getHost(), targetPath));
}
public static Operation createOptions(ServiceHost sender, String targetPath) {
return createOptions(UriUtils.buildUri(sender, targetPath));
}
public static Operation createOptions(URI uri) {
return createOperation(Action.OPTIONS, uri);
}
public static Operation createDelete(Service sender, String targetPath) {
return createDelete(UriUtils.buildUri(sender.getHost(), targetPath));
}
public static Operation createDelete(ServiceHost sender, String targetPath) {
return createDelete(UriUtils.buildUri(sender, targetPath));
}
public static Operation createDelete(URI uri) {
return createOperation(Action.DELETE, uri);
}
public static Operation createGet(Service sender, String targetPath) {
return createGet(UriUtils.buildUri(sender.getHost(), targetPath));
}
public static Operation createGet(ServiceHost sender, String targetPath) {
return createGet(UriUtils.buildUri(sender, targetPath));
}
public static Operation createGet(URI uri) {
return createOperation(Action.GET, uri);
}
public void sendWith(ServiceRequestSender sender) {
sender.sendRequest(this);
}
@Override
public String toString() {
SerializedOperation sop = SerializedOperation.create(this);
if (sop.jsonBody != null && sop.jsonBody.length() > TO_STRING_SERIALIZED_BODY_LIMIT) {
// Avoiding logging the entire body, which could be huge, and overwhelm the logs.
// Keep just an arbitrary prefix, serving as a hint
sop.jsonBody = sop.jsonBody.substring(0, TO_STRING_SERIALIZED_BODY_LIMIT);
}
return Utils.toJsonHtml(sop);
}
/**
* Returns a string summary of the operation appropriate for logging
*/
public String toLogString() {
StringBuilder sb = Utils.getBuilder();
sb.append(this.action.toString()).append(" ")
.append(this.getUri()).append(" ")
.append(this.id).append(" ")
.append(this.getRefererAsString()).append(" ");
if (this.contextId != null) {
sb.append("[ctxId] ").append(this.contextId);
}
if (this.transactionId != null) {
sb.append("[txId] ").append(this.transactionId);
}
if (this.authorizationCtx != null && this.authorizationCtx.claims != null) {
sb.append("[subject] ").append(this.authorizationCtx.claims.getSubject());
}
return sb.toString();
}
@Override
public Operation clone() {
Operation clone;
try {
clone = (Operation) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
// Clone mutable fields
// body is always cloned on set, so no need to re-clone
clone.options = EnumSet.copyOf(this.options);
if (this.remoteCtx != null) {
clone.remoteCtx = new RemoteContext();
if (this.remoteCtx.cookies != null) {
clone.remoteCtx.cookies = new HashMap<>(this.remoteCtx.cookies);
}
// do not clone socket context
clone.remoteCtx.socketCtx = null;
if (this.remoteCtx.requestHeaders != null && !this.remoteCtx.requestHeaders.isEmpty()) {
clone.remoteCtx.requestHeaders = new HashMap<>(this.remoteCtx.requestHeaders);
}
clone.remoteCtx.peerPrincipal = this.remoteCtx.peerPrincipal;
if (this.remoteCtx.peerCertificateChain != null) {
clone.remoteCtx.peerCertificateChain = Arrays.copyOf(
this.remoteCtx.peerCertificateChain,
this.remoteCtx.peerCertificateChain.length);
}
clone.remoteCtx.connectionTag = this.remoteCtx.connectionTag;
}
return clone;
}
private void allocateRemoteContext() {
if (this.remoteCtx == null) {
this.remoteCtx = new RemoteContext();
}
}
private void allocateRequestHeaders() {
if (this.remoteCtx.requestHeaders == null) {
this.remoteCtx.requestHeaders = new HashMap<>();
}
}
private void allocateResponseHeaders() {
if (this.remoteCtx.responseHeaders == null) {
this.remoteCtx.responseHeaders = new HashMap<>();
}
}
public boolean isRemote() {
return this.options.contains(OperationOption.REMOTE) ||
(this.remoteCtx != null && this.remoteCtx.socketCtx != null);
}
public Operation forceRemote() {
return toggleOption(OperationOption.REMOTE, true);
}
public AuthorizationContext getAuthorizationContext() {
return this.authorizationCtx;
}
/**
* Sets (overwrites) the authorization context of this operation.
*
* Infrastructure use only.
*/
public Operation setAuthorizationContext(AuthorizationContext ctx) {
this.authorizationCtx = ctx;
return this;
}
public String getTransactionId() {
return this.transactionId;
}
public Operation setTransactionId(String transactionId) {
this.transactionId = transactionId;
return this;
}
public boolean isWithinTransaction() {
return this.transactionId != null;
}
public String getContextId() {
return this.contextId;
}
public Operation setContextId(String id) {
this.contextId = id;
return this;
}
public Operation setBody(Object body) {
if (body != null) {
if (hasOption(OperationOption.CLONING_DISABLED)) {
this.body = body;
} else {
this.body = Utils.clone(body);
}
} else {
this.body = null;
}
return this;
}
public Operation setStatusCode(int code) {
this.statusCode = code;
return this;
}
/**
* Infrastructure use only
*
* @param body
* @return
*/
public Operation setBodyNoCloning(Object body) {
this.body = body;
return this;
}
public ServiceErrorResponse getErrorResponseBody() {
if (!hasBody()) {
return null;
}
ServiceErrorResponse rsp = getBody(ServiceErrorResponse.class);
if (rsp.message == null && rsp.statusCode == 0) {
// very likely not a error response body
return null;
}
return rsp;
}
/**
* Deserializes the body associated with the operation, given the type.
*
* Note: This method is *not* idempotent. It will modify the body contents
* so subsequent calls will not have access to the original instance. This
* occurs only for local operations, not operations that have a serialized
* body already attached (in the form of a JSON string).
*
* If idempotent behavior is desired, use {@link #getBodyRaw}
*/
@SuppressWarnings("unchecked")
public T getBody(Class type) {
if (this.body != null && this.body.getClass() == type) {
return (T) this.body;
}
if (this.body != null && !(this.body instanceof String)) {
if (this.contentType != null && Utils.isContentTypeKryoBinary(this.contentType)
&& this.body instanceof byte[]) {
byte[] bytes = (byte[])this.body;
this.body = KryoSerializers.deserializeDocument(bytes, 0, bytes.length);
this.serializedBody = Utils.toJson(this.body);
return (T) this.body;
}
if (this.contentType == null
|| !this.contentType.contains(MEDIA_TYPE_APPLICATION_JSON)) {
throw new IllegalStateException("content type is not JSON: " + this.contentType);
}
if (this.serializedBody != null) {
this.body = this.serializedBody;
} else {
String json = Utils.toJson(this.body);
return Utils.fromJson(json, type);
}
}
if (this.body != null) {
if (this.body instanceof String) {
this.serializedBody = this.body;
}
// Request must specify a Content-Type we understand
if (this.contentType != null
&& this.contentType.contains(MEDIA_TYPE_APPLICATION_JSON)) {
try {
this.body = Utils.fromJson(this.body, type);
} catch (com.google.gson.JsonSyntaxException e) {
throw new IllegalArgumentException("Unparseable JSON body: " + e.getMessage(),
e);
}
} else {
throw new IllegalArgumentException(
"Unrecognized Content-Type for parsing request body: " +
this.contentType);
}
return (T) this.body;
}
throw new IllegalStateException();
}
public Object getBodyRaw() {
return this.body;
}
public long getContentLength() {
return this.contentLength;
}
public Operation setContentLength(long l) {
this.contentLength = l;
return this;
}
public String getContentType() {
return this.contentType;
}
public Operation setContentType(String type) {
this.contentType = type;
return this;
}
public Operation setCookies(Map cookies) {
allocateRemoteContext();
this.remoteCtx.cookies = cookies;
return this;
}
public Map getCookies() {
if (this.remoteCtx == null) {
return null;
}
return this.remoteCtx.cookies;
}
public int getRetriesRemaining() {
return this.retriesRemaining;
}
public int getRetryCount() {
return this.retryCount;
}
public int incrementRetryCount() {
return ++this.retryCount;
}
public Operation setRetryCount(int retryCount) {
if (retryCount < 0) {
throw new IllegalArgumentException("retryCount must be positive");
}
if (retryCount > Short.MAX_VALUE) {
throw new IllegalArgumentException("retryCount must be less than " + Short.MAX_VALUE);
}
this.retryCount = (short) retryCount;
this.retriesRemaining = (short) retryCount;
return this;
}
public Operation setCompletion(CompletionHandler completion) {
this.completion = completion;
return this;
}
/**
* Takes two discrete callback handlers for completion.
*
* For completion that does not share code between success and failure paths, this should be the
* preferred alternative.
*
* @param successHandler called at successful operation completion
* @param failureHandler called at failure operation completion
* @return operation
*/
public Operation setCompletion(Consumer successHandler,
CompletionHandler failureHandler) {
this.completion = (op, e) -> {
if (e != null) {
failureHandler.handle(op, e);
return;
}
successHandler.accept(op);
};
return this;
}
/**
* Sets the handler to be invoked upon receiving {@link ServerSentEvent}.
* @param serverSentEventHandler called when {@link ServerSentEvent} is received
* @return operation
*/
public Operation setServerSentEventHandler(Consumer serverSentEventHandler) {
this.serverSentEventHandler = serverSentEventHandler;
return this;
}
/**
* Inserts a handler in LIFO style.
* @see #nestHeadersReceivedHandler(Consumer)
* @see #nestCompletion(CompletionHandler)
* @param serverSentEventHandler
* @return
*/
public Operation nestServerSentEventHandler(Consumer serverSentEventHandler) {
if (this.serverSentEventHandler != null) {
serverSentEventHandler = serverSentEventHandler.andThen(this.serverSentEventHandler);
}
return this.setServerSentEventHandler(serverSentEventHandler);
}
/**
* Sets the handler to be invoked upon receiving the headers.
* @param handler called after the headers are received.
* @return operation
*/
public Operation setHeadersReceivedHandler(Consumer handler) {
this.headersReceivedHandler = handler;
return this;
}
/**
* Inserts a handler in LIFO style.
* @see #nestServerSentEventHandler(Consumer)
* @see #nestCompletion(CompletionHandler)
* @param handler
* @return
*/
public Operation nestHeadersReceivedHandler(Consumer handler) {
if (this.headersReceivedHandler != null) {
handler = handler.andThen(this.headersReceivedHandler);
}
return this.setHeadersReceivedHandler(handler);
}
public CompletionHandler getCompletion() {
return this.completion;
}
public Operation setUri(URI uri) {
this.uri = uri;
return this;
}
public URI getUri() {
return this.uri;
}
Operation linkState(ServiceDocument serviceDoc) {
if (serviceDoc != null && this.linkedState != null
&& this.linkedState.documentKind != null) {
serviceDoc.documentKind = this.linkedState.documentKind;
}
// we do not clone here because the service will clone before the next
// request is processed
this.linkedState = serviceDoc;
return this;
}
ServiceDocument getLinkedState() {
return this.linkedState;
}
/**
* Infrastructure use only
*/
byte[] getLinkedSerializedState() {
return this.linkedSerializedState;
}
boolean hasLinkedSerializedState() {
return this.linkedSerializedState != null;
}
public Operation setReferer(URI uri) {
this.referer = uri;
return this;
}
public Operation setReferer(String uri) {
this.referer = uri;
return this;
}
public Operation transferRefererFrom(Operation op) {
this.referer = op.referer;
return this;
}
public boolean hasReferer() {
return this.referer != null;
}
public URI getReferer() {
if (this.referer instanceof String) {
try {
this.referer = new URI((String) this.referer);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return (URI) this.referer;
}
public String getRefererAsString() {
if (this.referer == null) {
return null;
}
return this.referer.toString();
}
public Operation setAction(Action action) {
this.action = action;
return this;
}
public Action getAction() {
return this.action;
}
public long getId() {
return this.id;
}
public Operation setSocketContext(SocketContext socketContext) {
allocateRemoteContext();
this.remoteCtx.socketCtx = socketContext;
return this;
}
public SocketContext getSocketContext() {
return this.remoteCtx == null ? null : this.remoteCtx.socketCtx;
}
public long getExpirationMicrosUtc() {
return this.expirationMicrosUtc;
}
/**
* Sets the expiration using the supplied absolute time value, that should be in microseconds
* since epoch
*/
public Operation setExpiration(long futureMicrosUtc) {
this.expirationMicrosUtc = futureMicrosUtc;
return this;
}
public int getStatusCode() {
return this.statusCode;
}
public void complete() {
completeOrFail(null);
}
public void fail(Throwable e) {
fail(e, null);
}
/**
* Sends a "server sent event". See SSE specification}
* @param event The event to send.
*/
public void sendServerSentEvent(ServerSentEvent event) {
if (this.serverSentEventHandler == null) {
return;
}
// Keep track of current operation context
OperationContext originalContext = OperationContext.getOperationContext();
try {
OperationContext.setFrom(this);
this.serverSentEventHandler.accept(event);
} catch (Exception outer) {
Utils.logWarning("Uncaught failure inside serverSentEventHandler: %s", Utils.toString(outer));
} finally {
// Restore original context
OperationContext.setFrom(originalContext);
}
}
/**
* Sends the headers to the channel.
* This effectively enables streaming.
*/
public void sendHeaders() {
if (this.headersReceivedHandler == null) {
return;
}
// Keep track of current operation context
OperationContext originalContext = OperationContext.getOperationContext();
try {
OperationContext.setFrom(this);
this.headersReceivedHandler.accept(this);
} catch (Exception outer) {
Utils.logWarning("Uncaught failure inside headersReceivedHandler: %s", Utils.toString(outer));
} finally {
// Restore original context
OperationContext.setFrom(originalContext);
}
}
/**
* Send the appropriate headers and prepare the connection for streaming.
*/
public void startEventStream() {
this.setContentType(MEDIA_TYPE_TEXT_EVENT_STREAM);
this.sendHeaders();
}
public void fail(int statusCode) {
setStatusCode(statusCode);
switch (statusCode) {
case STATUS_CODE_FORBIDDEN:
fail(new IllegalAccessError("forbidden"));
break;
case STATUS_CODE_TIMEOUT:
fail(new TimeoutException());
break;
default:
fail(new Exception("request failed with " + statusCode + ", no additional details provided"));
break;
}
}
public void fail(int statusCode, Throwable e, Object failureBody) {
this.statusCode = statusCode;
fail(e, failureBody);
}
public void fail(Throwable e, Object failureBody) {
if (this.statusCode < STATUS_CODE_FAILURE_THRESHOLD) {
this.statusCode = STATUS_CODE_SERVER_FAILURE_THRESHOLD;
}
if (e instanceof TimeoutException) {
this.statusCode = Operation.STATUS_CODE_TIMEOUT;
}
if (failureBody != null) {
setBodyNoCloning(failureBody);
}
boolean hasErrorResponseBody = false;
if (this.body != null && this.body instanceof String) {
if (Operation.MEDIA_TYPE_APPLICATION_JSON.equals(this.contentType)) {
try {
ServiceErrorResponse rsp = Utils.fromJson(this.body,
ServiceErrorResponse.class);
if (rsp.message != null) {
hasErrorResponseBody = true;
}
} catch (Exception ex) {
// the body is not JSON, ignore
}
} else {
// the response body is text but not JSON, we will leave as is
hasErrorResponseBody = true;
}
}
if (this.body != null && this.body instanceof byte[]) {
// the response body is binary or unknown text encoding, leave as is
hasErrorResponseBody = true;
}
if (this.body == null
|| ((!hasErrorResponseBody) && !(this.body instanceof ServiceErrorResponse))) {
ServiceErrorResponse rsp;
if (Utils.isValidationError(e)) {
this.statusCode = STATUS_CODE_BAD_REQUEST;
rsp = Utils.toValidationErrorResponse(e, this);
} else {
rsp = Utils.toServiceErrorResponse(e);
}
rsp.statusCode = this.statusCode;
setBodyNoCloning(rsp).setContentType(Operation.MEDIA_TYPE_APPLICATION_JSON);
}
completeOrFail(e);
}
private void completeOrFail(Throwable e) {
CompletionHandler c = this.completion;
if (c == null) {
return;
}
if (!completionUpdater.compareAndSet(this, c, null)) {
Utils.logWarning("%s:%s",
Utils.toString(new IllegalStateException("double completion")),
toString());
return;
}
// Keep track of current operation context so that code AFTER "op.complete()"
// or "op.fail()" retains its operation context, and is not overwritten by
// the one associated with "op" (which might be different).
OperationContext originalContext = OperationContext.getOperationContext();
try {
OperationContext.setFrom(this);
c.handle(this, e);
} catch (Exception outer) {
Utils.logWarning("Uncaught failure inside completion: %s", Utils.toString(outer));
}
// Restore original context
OperationContext.setFrom(originalContext);
}
public boolean hasBody() {
return this.body != null;
}
/**
* Add CompletionHandler in LIFO style.
*
* This is symmetric to {@link #appendCompletion(CompletionHandler)}.
*
* {@code
* op.setCompletion(ORG);
* op.nestCompletion(A);
* op.nestCompletion(B);
* // complete() will trigger: B -> A -> ORG
* }
*