io.nextop.Message Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-common Show documentation
Show all versions of java-common Show documentation
Platform-agnostic Java core for Nextop
The newest version!
package io.nextop;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;
import io.nextop.org.apache.http.*;
import io.nextop.org.apache.http.client.methods.*;
import io.nextop.org.apache.http.client.utils.URIBuilder;
import io.nextop.org.apache.http.entity.ByteArrayEntity;
import io.nextop.org.apache.http.entity.StringEntity;
import io.nextop.util.NoCopyByteArrayOutputStream;
import rx.functions.Func1;
import javax.annotation.Nullable;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.*;
import static io.nextop.Route.Method;
import static io.nextop.Route.Target;
// goals: fast to parse, minimal object creation on parse or each operation
// FIXME implement hashCode, equals
public class Message {
public static final WireValue P_CONTENT = WireValue.of("$content");
public static final WireValue P_FRAGMENT = WireValue.of("$fragment");
public static final WireValue P_CODE = WireValue.of("$code");
public static final WireValue P_REASON = WireValue.of("$reason");
/** if the message is redirected, this holds the route (list[string])
* that the message was redirected from. */
public static final WireValue H_REDIRECT = WireValue.of("$redirect");
/** @see #isIdempotent */
public static final WireValue H_IDEMPOTENT = WireValue.of("$idempotent");
/** @see #isNullipotent */
public static final WireValue H_NULLIPOTENT = WireValue.of("$nullopotent");
/** @see #isYieldable */
public static final WireValue H_YIELDABLE = WireValue.of("$yieldable");
// PASSIVE MODES
// FIXME(0.2) important for battery - support this in all nodes
/* passive options control the scheduling of the message.
* by default (no passive) messages are sent with best effort.
* Passive options can be used to limit battery usage in apps
* that send a continuous stream of data, like logs.
* Passing options are:
* - {@link #V_PASSIVE_ACTIVE_RADIO} */
public static final WireValue H_PASSIVE = WireValue.of("$passive");
/** send the message be sent when the radio is active,
* but do not activate the radio to send it.
* Implementations can use a heuristic for an active radio:
* ≤30s after a best effort message is sent. */
public static final int V_PASSIVE_HOLD_FOR_ACTIVE_RADIO = 1;
public static final Id DEFAULT_GROUP_ID = Id.create(0L, 0L, 0L, 0L);
public static final int DEFAULT_GROUP_PRIORITY = 10;
public static final Id LOG_GROUP_ID = Id.create(0L, 0L, 0L, 1L);
public static final int LOG_GROUP_PRIORITY = 0;
/////// ROUTES ///////
private static final Path P_MESSAGE_PREFIX = Path.valueOf("/m");
/** @see #getLocalId */
public static Route outboxRoute(Id id) {
return Route.local(Target.create(Method.PUT, P_MESSAGE_PREFIX.append(id.toString())));
}
/** @see #getLocalId */
public static Route inboxRoute(Id id) {
return Route.local(Target.create(Method.POST, P_MESSAGE_PREFIX.append(id.toString())));
}
/** @see #getLocalId */
public static Route echoRoute(Id id) {
return Route.local(Target.create(Method.GET, P_MESSAGE_PREFIX.append("/" + id)));
}
/** @see #getLocalId */
public static Route echoHeadRoute(Id id) {
return Route.local(Target.create(Method.HEAD, P_MESSAGE_PREFIX.append("/" + id)));
}
public static Route logRoute() {
return Route.local(Target.create(Method.POST, Path.valueOf("/log")));
}
public static boolean isLocal(Route route) {
return route.via.isLocal();
}
@Nullable
public static Id getLocalId(Route route) {
if (isLocal(route) && route.target.path.isFixed()
&& route.target.path.startsWith(P_MESSAGE_PREFIX) && 2 <= route.target.path.segments.size()) {
Path.Segment first = route.target.path.segments.get(0);
assert Path.Segment.Type.FIXED.equals(first.type);
try {
return Id.valueOf(first.value);
} catch (IllegalArgumentException e) {
// FIXME log, strange ... could be a client-generated bad value
return null;
}
}
return null;
}
/////// CONTROL PROPERTIES ///////
/** Test if the message can be safely received multiple times. */
public static boolean isIdempotent(Message message) {
@Nullable WireValue idempotentValue = message.headers.get(H_IDEMPOTENT);
if (null != idempotentValue) {
return idempotentValue.asBoolean();
}
if (isNullipotent(message)) {
return true;
}
// not enough info to infer
return false;
}
/** Tests if the message does not have side effects. */
public static boolean isNullipotent(Message message) {
@Nullable WireValue nullipotentValue = message.headers.get(H_NULLIPOTENT);
if (null != nullipotentValue) {
return nullipotentValue.asBoolean();
}
// not explicitly set; infer
switch (message.route.target.method) {
case GET:
case HEAD:
return true;
default:
return false;
}
}
/** Tests if the message can be moved to the end of the queue on failure.
* This is one way to solve head of line blocking issues, with failing endpoints,
* where yieldable requests hitting the failing endpoints will "get in the back of the line"
* and try again.
*
* The default value assumes all nullipotent ({@link #isNullipotent}) requests can
* yield. This may break some cases where the client needs to get an exact state
* (e.g. after a certain case), but this can be fixed by either marking the request
* explicitly yieldable=false, or bundling the request into the response of the certain case. */
public static boolean isYieldable(Message message) {
@Nullable WireValue yieldableValue = message.headers.get(H_YIELDABLE);
if (null != yieldableValue) {
return yieldableValue.asBoolean();
}
// not explicitly set; infer
return isNullipotent(message);
}
public static Message valueOf(Route.Method method, URL url) {
try {
return valueOf(method, url.toURI());
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
public static Message valueOf(Route.Method method, URI uri) {
// FIXME parse query and fragment into parameters
return newBuilder().setRoute(Route.valueOf(String.format("%s %s", method, uri))).build();
}
public final Id id;
// priority sets the priority of the group relative to other groups
// groups are equal based on *id only*
// on the send/receive side, when multiplexing, priority of a group should be the max of all queued messages
public final Id groupId;
public final int groupPriority;
public final Route route;
public final Map headers;
public final Map parameters;
Message(Id id, Id groupId, int groupPriority, Route route,
Map headers,
Map parameters) {
if (null == id) {
throw new IllegalArgumentException();
}
if (null == groupId) {
throw new IllegalArgumentException();
}
if (null == route) {
throw new IllegalArgumentException();
}
if (null == headers) {
throw new IllegalArgumentException();
}
if (null == parameters) {
throw new IllegalArgumentException();
}
this.id = id;
this.groupId = groupId;
this.groupPriority = groupPriority;
this.route = route;
this.headers = headers;
this.parameters = parameters;
}
// ROUTES
public Route outboxRoute() {
return outboxRoute(id);
}
public Route inboxRoute() {
return inboxRoute(id);
}
public Route echoRoute() {
return echoRoute(id);
}
public Route echoHeadRoute() {
return echoHeadRoute(id);
}
// public Route statusRoute() {
// return statusRoute(id);
// }
@Nullable
public WireValue getContent() {
return parameters.get(P_CONTENT);
}
public int getCode() {
@Nullable WireValue codeValue = parameters.get(P_CODE);
return null != codeValue ? codeValue.asInt() : -1;
}
@Nullable
public String getReason() {
WireValue reasonValue = parameters.get(P_REASON);
return null != reasonValue ? reasonValue.asString() : null;
}
public URI toUri() throws URISyntaxException {
return toUri(this);
}
public String toUriString() {
try {
return toUri(this).toString();
} catch (URISyntaxException e) {
throw new IllegalStateException();
}
}
public Builder buildOn() {
return toBuilder(false);
}
public Builder toBuilder() {
return toBuilder(true);
}
private Builder toBuilder(boolean newId) {
Builder b = (newId ? newBuilder() : newBuilder(id))
.setGroupId(groupId)
.setGroupPriority(groupPriority)
.setRoute(route);
for (Map.Entry e : headers.entrySet()) {
b = b.setHeader(e.getKey(), e.getValue());
}
for (Map.Entry e : parameters.entrySet()) {
b = b.set(e.getKey(), e.getValue());
}
return b;
}
// FIXME this needs to set parameters that are part of the path
// public Message toSpec() {
// return newBuilder()
// .setRoute(route)
// .build();
// }
// public Message newId() {
// return toBuilder().build();
// }
//
// public Message parallel() {
// return buildOn().setGroupId(Id.create()).build();
// }
//
// public Message serial(Id serialGroupId) {
// return buildOn().setGroupId(serialGroupId).build();
// }
public static Builder newBuilder() {
return newBuilder(Id.create());
}
public static Builder newBuilder(Id id) {
return new Builder(id);
}
@Override
public String toString() {
try {
return String.format("%s", toUri());
} catch (URISyntaxException e) {
return String.format("<%s>", route);
}
}
@Override
public int hashCode() {
int c = id.hashCode();
c = 31 * c + groupId.hashCode();
c = 31 * c + groupPriority;
c = 31 * c + route.hashCode();
c = 31 * c + headers.hashCode();
c = 31 * c + parameters.hashCode();
return c;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Message)) {
return false;
}
Message b = (Message) obj;
return id.equals(b.id)
&& groupId.equals(b.groupId) && groupPriority == b.groupPriority
&& route.equals(b.route)
&& headers.equals(b.headers)
&& parameters.equals(b.parameters);
// if (!id.equals(b.id)) {
// return false;
// }
// if (!groupId.equals(b.groupId)) {
// return false;
// }
// if (groupPriority != b.groupPriority) {
// return false;
// }
// if (!route.equals(b.route)) {
// return false;
// }
// if (!headers.equals(b.headers)) {
// return false;
// }
// if (!parameters.equals(b.parameters)) {
// return false;
// }
// return true;
}
public static final class Builder {
private final Id id;
private Id groupId = DEFAULT_GROUP_ID;
private int groupPriority = DEFAULT_GROUP_PRIORITY;
@Nullable
private Route.Target target = null;
@Nullable
private Route.Via via = null;
private Map headers = new HashMap(8);
private Map parameters = new HashMap(8);
private Builder(Id id) {
this.id = id;
}
public Builder setGroupId(Id groupId) {
this.groupId = groupId;
return this;
}
public Builder setGroupPriority(int groupPriority) {
this.groupPriority = groupPriority;
return this;
}
public Builder setTarget(@Nullable Route.Target target) {
this.target = target;
return this;
}
public Builder setTarget(@Nullable String target) {
if (null != target) {
this.target = Route.Target.valueOf(target);
} else {
this.target = null;
}
return this;
}
public Builder setVia(@Nullable Route.Via via) {
this.via = via;
return this;
}
public Builder setVia(@Nullable String via) {
if (null != via) {
this.via = Route.Via.valueOf(via);
} else {
this.via = null;
}
return this;
}
public Builder setRoute(@Nullable Route route) {
if (null != route) {
this.target = route.target;
this.via = route.via;
} else {
this.target = null;
this.via = null;
}
return this;
}
public Builder setRoute(@Nullable String s) {
if (null != s) {
Route route = Route.valueOf(s);
target = route.target;
via = route.via;
} else {
target = null;
via = null;
}
return this;
}
public Builder setHeader(Object name, @Nullable Object value) {
if (null != value) {
headers.put(WireValue.of(name), WireValue.of(value));
} else {
headers.remove(WireValue.of(name));
}
return this;
}
public Builder setContent(@Nullable Object value) {
return set(P_CONTENT, value);
}
public Builder setCode(@Nullable Integer code) {
return set(P_CODE, code);
}
public Builder setReason(@Nullable String reason) {
return set(P_REASON, reason);
}
public Builder set(Object name, @Nullable Object value) {
if (null != value) {
parameters.put(WireValue.of(name), WireValue.of(value));
} else {
parameters.remove(WireValue.of(name));
}
return this;
}
public Message build() {
if (null == target) {
throw new IllegalStateException();
}
Route route;
if (null != via) {
route = Route.create(target, via);
} else {
route = Route.local(target);
}
return new Message(id, groupId, groupPriority, route,
ImmutableMap.copyOf(headers),
ImmutableMap.copyOf(parameters));
}
}
/////// URI CONVERSIONS ///////
/** @throws java.net.URISyntaxException if variables in the path cannot be fully substituted. */
public static URI toUri(final Message message) throws URISyntaxException {
// variables in path get substituted to path;
// if not all variables can be substituted, throw a URISyntaxException
// parameters except P_CONTENT get added as query params (via toString)
Path fixedPath = message.route.target.path.fix(new Func1() {
@Override
public Object call(String s) {
return message.parameters.get(WireValue.of(s));
}
});
Set pathVariableNames = new HashSet(4);
for (Path.Segment segment : message.route.target.path.segments) {
if (Path.Segment.Type.VARIABLE.equals(segment.type)) {
pathVariableNames.add(segment.value);
}
}
URIBuilder builder = new URIBuilder();
builder.setScheme(message.route.via.scheme.toString().toLowerCase());
if (null != message.route.via.authority.getHost()) {
builder.setHost(message.route.via.authority.getHost());
if (0 < message.route.via.authority.port) {
builder.setPort(message.route.via.authority.port);
}
}
builder.setPath(fixedPath.toString());
for (Map.Entry e : message.parameters.entrySet()) {
WireValue key = e.getKey();
WireValue value = e.getValue();
if (!P_CONTENT.equals(key) && !pathVariableNames.contains(key.toText())) {
builder.addParameter(key.toText(), value.toText());
}
}
return builder.build();
}
/////// HTTPCLIENT CONVERSIONS ///////
private static final String H_PRAGMA_ID_PREFIX = "nextop-id";
// FIXME serialID
private static final String H_PRAGMA_PREFIX = "nextop-header";
// FIXME use something standard - figure out what
private static final String H_PRAGMA_IMAGE_SIZE_PREFIX = "image-size";
// IMAGE
// FIXME maxTransferWidth, maxTransferHeight
public static HttpHost toHttpHost(Message message) throws URISyntaxException {
switch (message.route.via.scheme) {
case HTTP:
case HTTPS:
break;
default:
throw new URISyntaxException(message.route.toString(), "Bad scheme");
}
String host = message.route.via.authority.getHost();
if (null == host) {
throw new URISyntaxException(message.route.toString(), "Bad host");
}
int port = message.route.via.authority.port;
if (port <= 0) {
// apply default
switch (message.route.via.scheme) {
case HTTP:
port = 80;
break;
case HTTPS:
port = 443;
break;
default:
// should never reach here; see check above
throw new IllegalStateException();
}
}
return new HttpHost(host, port,
message.route.via.scheme.toString().toLowerCase());
}
public static HttpUriRequest toHttpRequest(Message message) throws URISyntaxException {
switch (message.route.target.method) {
case GET: {
HttpGet get = new HttpGet();
get.setURI(message.toUri());
attachHeaders(message, get);
return get;
}
case POST: {
HttpPost post = new HttpPost();
post.setURI(message.toUri());
attachHeaders(message, post);
attachContent(message, post);
return post;
}
case PUT: {
HttpPut put = new HttpPut();
put.setURI(message.toUri());
attachHeaders(message, put);
attachContent(message, put);
return put;
}
default:
throw new IllegalArgumentException();
}
}
// requires a route set into the builder to build
public static Builder fromHttpResponse(HttpResponse response) {
Message.Builder builder = Message.newBuilder();
builder.setCode(response.getStatusLine().getStatusCode());
builder.setReason(response.getStatusLine().getReasonPhrase());
attachHeaders(response, builder);
attachContent(response, builder);
return builder;
}
/** inverse of {@link #toHttpRequest} */
public static Message fromHttpRequest(HttpUriRequest request) {
// FIXME 0.1.1
return null;
}
/** inverse of {@link #fromHttpResponse} */
public static HttpResponse toHttpResponse(Message message) {
// FIXME 0.1.1
return null;
}
private static void attachHeaders(Message message, HttpRequest request) {
// add the id
request.addHeader(HttpHeaders.PRAGMA, String.format("%s %s",
H_PRAGMA_ID_PREFIX, message.id));
for (Map.Entry e : message.headers.entrySet()) {
WireValue key = e.getKey();
WireValue value = e.getValue();
String keyString = key.toString();
if (ALL_HTTP_HEADERS.contains(keyString)) {
request.setHeader(keyString, value.toText());
} else {
// send non-standard headers as Pragma headers:
// $H_PRAGMA_PREFIX $json([$key, $value])
request.addHeader(HttpHeaders.PRAGMA, String.format("%s %s",
H_PRAGMA_PREFIX, WireValue.of(Arrays.asList(key, value)).toJson()));
}
}
}
private static void attachContent(Message message, HttpEntityEnclosingRequestBase request) {
WireValue content = message.parameters.get(Message.P_CONTENT);
if (null != content) {
HttpEntity entity;
MediaType contentType = getContentType(message, content);
// attach the content type header if missing
if (!request.containsHeader(HttpHeaders.CONTENT_TYPE)) {
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType.toString());
}
if (contentType.is(MediaType.JSON_UTF_8)) {
try {
entity = new StringEntity(content.toJson());
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
} else if (contentType.is(MediaType.ANY_TEXT_TYPE)) {
try {
entity = new StringEntity(content.toText());
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
} else if (contentType.is(MediaType.ANY_IMAGE_TYPE)) {
switch (content.getType()) {
case IMAGE:
EncodedImage image = content.asImage();
// FIXME 0.1.1
// entity = new ByteArrayEntity(image.bytes, image.offset, image.length);
entity = new ByteArrayEntity(Arrays.copyOfRange(image.bytes, image.offset, image.length));
if (0 < image.width || 0 < image.height) {
// attach headers for image width and height
request.addHeader(HttpHeaders.PRAGMA, String.format("%s %d %d",
H_PRAGMA_IMAGE_SIZE_PREFIX, image.width, image.height));
}
break;
default:
ByteBuffer bb = content.asBlob();
byte[] bytes = new byte[bb.remaining()];
bb.get(bytes);
entity = new ByteArrayEntity(bytes);
break;
}
} else {
// send as binary
ByteBuffer bb = content.asBlob();
byte[] bytes = new byte[bb.remaining()];
bb.get(bytes);
entity = new ByteArrayEntity(bytes);
}
// do not set the Content-Length header - the http exec chain does that
// request.setHeader(HttpHeaders.CONTENT_LENGTH, "" + entity.getContentLength());
request.setEntity(entity);
// TODO
// if ("http".equalsIgnoreCase(request.getURI().getScheme())
// && !message.headers.containsKey(WireValue.of(HttpHeaders.CONTENT_MD5))) {
// request.setHeader(HttpHeaders.CONTENT_MD5, WireValue.of(contentMd5(entity)).toString());
// } // else https already checks integrity of message
}
}
private static MediaType getContentType(Message message, WireValue content) {
@Nullable WireValue contentTypeValue = message.headers.get(WireValue.of(HttpHeaders.CONTENT_TYPE));
if (null != contentTypeValue) {
return MediaType.parse(contentTypeValue.asString());
}
// else use a default content type
switch (content.getType()) {
case IMAGE:
switch (content.asImage().format) {
case WEBP:
return MediaType.WEBP;
case JPEG:
return MediaType.JPEG;
case PNG:
return MediaType.PNG;
default:
throw new IllegalArgumentException();
}
case BLOB:
return MediaType.APPLICATION_BINARY;
case UTF8:
return MediaType.PLAIN_TEXT_UTF_8;
default:
return MediaType.JSON_UTF_8;
}
}
private static void attachHeaders(HttpResponse response, Message.Builder builder) {
for (Header header : response.getAllHeaders()) {
builder.setHeader(WireValue.of(header.getName()), WireValue.of(header.getValue()));
}
}
private static void attachContent(HttpResponse response, Message.Builder builder) {
@Nullable HttpEntity entity = response.getEntity();
if (null != entity) {
builder.setContent(createContent(response, entity));
}
}
private static WireValue createContent(HttpResponse response, HttpEntity entity) {
// 1. if there is a Content-MD5 header, check the MD5
// 2. parse based on the content type
// TODO this is a transmission error then, but there is no way to retransmit ...
// TODO this should be surfaced as a receive error and the receiver() can do the logical thing ...
// TODO maybe have a header H_IDEMPOTENT or something, where receive errors can be regenerated by re-sending
// for (Header header : response.getHeaders(HttpHeaders.CONTENT_MD5)) {
// byte[] headerMd5 = Base64.decodeBase64(header.getValue());
// byte[] contentMd5 = contentMd5(entity);
// if (!Arrays.equals(headerMd5, contentMd5)) {
// throw new IllegalArgumentException(String.format("%s %s does not match %s", HttpHeaders.CONTENT_MD5,
// HexBytes.toString(headerMd5), HexBytes.toString(contentMd5)));
// }
// }
@Nullable Header contentTypeHeader = entity.getContentType();
if (null == contentTypeHeader) {
contentTypeHeader = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
}
WireValue value;
MediaType contentType;
if (null != contentTypeHeader) {
contentType = MediaType.parse(contentTypeHeader.getValue());
} else {
contentType = MediaType.APPLICATION_BINARY;
}
if (contentType.is(MediaType.JSON_UTF_8)) {
RepetableEntity re = RepetableEntity.create(entity);
try {
value = WireValue.valueOfJson(new InputStreamReader(re.entity.getContent(), Charsets.UTF_8));
} catch (IOException e) {
// return as text
try {
Reader r = new InputStreamReader(re.entity.getContent(), Charsets.UTF_8);
try {
return WireValue.of(CharStreams.toString(r));
} finally {
r.close();
}
} catch (IOException e2) {
throw new IllegalArgumentException(e2);
}
}
} else if (contentType.is(MediaType.ANY_TEXT_TYPE)) {
try {
Reader r = new InputStreamReader(entity.getContent(), Charsets.UTF_8);
try {
value = WireValue.of(CharStreams.toString(r));
} finally {
r.close();
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
} else if (contentType.is(MediaType.ANY_IMAGE_TYPE)) {
// receive as image if supported (TODO or can be transcoded)
// otherwise receive as binary
RepetableEntity re = RepetableEntity.create(entity);
re.setBytes();
EncodedImage.Orientation orientation = EncodedImage.Orientation.REAR_FACING;
EncodedImage.Format format;
if (contentType.is(MediaType.JPEG)) {
format = EncodedImage.Format.JPEG;
} else if (contentType.is(MediaType.WEBP)) {
format = EncodedImage.Format.WEBP;
} else if (contentType.is(MediaType.PNG)) {
format = EncodedImage.Format.PNG;
} else {
// the image is not supported
// TODO consider doing a transcoding here if possible
// TODO for now, just surface as binary
return WireValue.of(re.bytes, re.offset, re.length);
}
// FIXME accept image size headers (still need to figure out what's standard)
int width = EncodedImage.UNKNOWN_WIDTH;
int height = EncodedImage.UNKNOWN_HEIGHT;
return WireValue.of(EncodedImage.create(format, orientation, width, height, re.bytes, re.offset, re.length));
} else {
// receive as binary
RepetableEntity re = RepetableEntity.create(entity);
re.setBytes();
return WireValue.of(re.bytes, re.offset, re.length);
}
return value;
}
static final class RepetableEntity {
static RepetableEntity create(HttpEntity entity) {
// create a repeatable entity
// this is needed because the type will fall back if parsing fails
HttpEntity repeatableEntity;
@Nullable byte[] bytes = null;
int offset = 0;
int length = 0;
if (entity.isRepeatable()) {
repeatableEntity = entity;
} else {
int contentLength = (int) entity.getContentLength();
if (contentLength < 0) {
contentLength = 1024;
}
NoCopyByteArrayOutputStream out = new NoCopyByteArrayOutputStream(contentLength);
try {
try {
InputStream is = entity.getContent();
try {
ByteStreams.copy(is, out);
} finally {
is.close();
}
} finally {
out.close();
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
bytes = out.getBytes();
offset = out.getOffset();
length = out.getLength();
repeatableEntity = new ByteArrayEntity(bytes, offset, length);
}
if (!repeatableEntity.isRepeatable()) {
throw new IllegalStateException();
}
return new RepetableEntity(repeatableEntity, bytes, offset, length);
}
final HttpEntity entity;
@Nullable byte[] bytes;
int offset;
int length;
RepetableEntity(HttpEntity entity, @Nullable byte[] bytes, int offset, int length) {
this.entity = entity;
this.bytes = bytes;
this.offset = offset;
this.length = length;
}
void setBytes() {
if (null == bytes) {
// TODO this could be improved by extracting the buffer from the repeatable entity
try {
InputStream is = entity.getContent();
try {
bytes = ByteStreams.toByteArray(is);
offset = 0;
length = bytes.length;
} finally {
is.close();
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
}
private static final Set ALL_HTTP_HEADERS;
static {
// TODO fix this to not use reflection (be explicit; easier to be cross platform)
Set httpHeaders = new HashSet(256);
for (Field f : HttpHeaders.class.getFields()) {
int m = f.getModifiers();
try {
if (Modifier.isPublic(m) && Modifier.isStatic(m)) {
httpHeaders.add(f.get(null).toString());
}
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
ALL_HTTP_HEADERS = ImmutableSet.copyOf(httpHeaders);
}
// FIXME in general, be able to set the response group and priority for any message
/////// LAYERS ///////
public static final class LayerInfo {
public static enum Quality {
HIGH,
LOW
}
// the default quality of an image is HIGH
// TODO
// if LOW and no scale, this will force a recode to a smaller, lower quality image
public final Quality quality;
public final EncodedImage.Format format;
// size: <= 0 means "original" or "aspect-aware scaled" if the other dim is pinned
public final int width;
public final int height;
// TODO can attach other transform params here: scale type, crop, etc
public final @Nullable Id groupId;
public final int groupPriority;
public LayerInfo(Quality quality, EncodedImage.Format format, int width, int height,
@Nullable Id groupId, int groupPriority) {
this.quality = quality;
this.format = format;
this.width = width;
this.height = height;
this.groupId = groupId;
this.groupPriority = groupPriority;
}
// scale or quality change
public boolean isTransform() {
return 0 < width || 0 < height
|| !Quality.HIGH.equals(quality);
}
public int scaleWidth(int w, int h) {
if (0 < width) {
// pinned
return width;
}
if (0 < height) {
// aspect scale
return w * height / h ;
}
// track
return w;
}
public int scaleHeight(int w, int h) {
if (0 < height) {
// pinned
return height;
}
if (0 < width) {
// aspect scale
return h * width / w;
}
// track
return h;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(128);
sb.append(quality.toString().toLowerCase())
.append(" ")
.append(format.toString().toLowerCase())
.append(" ")
.append(0 < width ? width : 0)
.append(":")
.append(0 < height ? height : 0);
if (null != groupId) {
sb.append(" ")
.append(groupId)
.append(":")
.append(groupPriority);
}
return sb.toString();
}
public static LayerInfo valueOf(String s) {
String[] parts = s.split(" ");
if (parts.length < 3) {
throw new IllegalArgumentException();
}
Quality quality = Quality.valueOf(parts[0].toUpperCase());
EncodedImage.Format format = EncodedImage.Format.valueOf(parts[1].toUpperCase());
String[] sizeParts = parts[2].split(":");
if (2 != sizeParts.length) {
throw new IllegalArgumentException();
}
int width = Integer.parseInt(sizeParts[0]);
int height = Integer.parseInt(sizeParts[1]);
@Nullable Id groupId;
int groupPriority;
if (4 <= parts.length) {
// group
String[] groupParts = parts[3].split(":");
if (2 != groupParts.length) {
throw new IllegalArgumentException();
}
groupId = Id.valueOf(groupParts[0]);
groupPriority = Integer.parseInt(groupParts[1]);
} else {
groupId = null;
groupPriority = 0;
}
return new LayerInfo(quality, format, width, height,
groupId, groupPriority);
}
}
//
public static final WireValue H_LAYERS = WireValue.of("$layers");
@Nullable
public static LayerInfo[] getLayers(Message message) {
@Nullable WireValue layersValue = message.headers.get(H_LAYERS);
if (null == layersValue) {
return null;
}
switch (layersValue.getType()) {
case UTF8:
String s = layersValue.asString();
String[] parts = s.split(";");
int n = parts.length;
LayerInfo[] layers = new LayerInfo[n];
for (int i = 0; i < n; ++i) {
layers[i] = LayerInfo.valueOf(parts[i]);
}
return layers;
default:
throw new IllegalArgumentException();
}
}
public static void setLayers(Message.Builder builder, LayerInfo ... layers) {
int n = layers.length;
if (n <= 0) {
builder.setHeader(H_LAYERS, null);
} else {
StringBuilder sb = new StringBuilder(128);
sb.append(layers[0].toString());
for (int i = 1; i < n; ++i) {
sb.append(";").append(layers[i].toString());
}
builder.setHeader(H_LAYERS, WireValue.of(sb.toString()));
}
}
}