/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.common.stream;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.flow.flowlet.StreamEvent;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
/**
* A GSon {@link TypeAdapter} for serializing/deserializing {@link StreamEvent} to/from JSON. It serializes
*
* StreamEvent into
* <p>
* {@code {"timestamp": ... , "headers": { ... }, "body": ... }}
* </p><p>
* where the body is encoded as a string binary using Bytes.toStringBinary.
* </p>
*/
public class StreamEventTypeAdapter extends TypeAdapter<StreamEvent> {
private static final TypeToken<Map<String, String>> HEADERS_TYPE = new TypeToken<Map<String, String>>() { };
private final TypeAdapter<Map<String, String>> mapTypeAdapter;
/**
* Register an instance of the {@link StreamEventTypeAdapter} to the given {@link GsonBuilder}.
* @param gsonBuilder The build to register to
* @return The same {@link GsonBuilder} instance in the argument
*/
public static GsonBuilder register(GsonBuilder gsonBuilder) {
return gsonBuilder.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (StreamEvent.class.isAssignableFrom(type.getRawType())) {
return (TypeAdapter<T>) new StreamEventTypeAdapter(gson.getAdapter(HEADERS_TYPE));
}
return null;
}
});
}
private StreamEventTypeAdapter(TypeAdapter<Map<String, String>> mapTypeAdapter) {
this.mapTypeAdapter = mapTypeAdapter;
}
@Override
public void write(JsonWriter out, StreamEvent event) throws IOException {
out.beginObject()
.name("timestamp").value(event.getTimestamp())
.name("headers");
mapTypeAdapter.write(out, event.getHeaders());
out.name("body");
ByteBuffer body = event.getBody();
if (body.hasArray()) {
// Need to use ByteBuffer.slice() to make sure position starts at 0, which the toStringBinary requires.
out.value(Bytes.toStringBinary(body.slice()));
} else {
// Slow path, if the byte buffer doesn't have array
byte[] bytes = new byte[body.remaining()];
body.mark();
body.get(bytes);
body.reset();
out.value(Bytes.toStringBinary(bytes));
}
out.endObject();
}
@Override
public StreamEvent read(JsonReader in) throws IOException {
long timestamp = -1;
Map<String, String> headers = null;
ByteBuffer body = null;
in.beginObject();
while (in.peek() == JsonToken.NAME) {
String key = in.nextName();
if ("timestamp".equals(key)) {
timestamp = in.nextLong();
} else if ("headers".equals(key)) {
headers = mapTypeAdapter.read(in);
} else if ("body".equals(key)) {
body = ByteBuffer.wrap(Bytes.toBytesBinary(in.nextString()));
} else {
in.skipValue();
}
}
if (timestamp >= 0 && headers != null && body != null) {
in.endObject();
return new StreamEvent(headers, body, timestamp);
}
throw new IOException(String.format("Failed to read StreamEvent. Timestamp: %d, headers: %s, body: %s",
timestamp, headers, body));
}
}