package protobuf.codec;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.Message.Builder;
import com.google.protobuf.UnknownFieldSet;
/**
* Codec base class, provides for common validations and functionality
* @author sijuv
*
*/
//TODO Check Handle charset handling
public abstract class AbstractCodec implements Codec {
public static final String DEFAULT_CHARSET = "UTF-8";
private Map<Feature, Object> featureMap = new HashMap<Feature, Object>();
protected AbstractCodec() {
featureMap.put(Feature.SUPPORT_UNKNOWN_FIELDS, Boolean.TRUE);
featureMap.put(Feature.UNKNOWN_FIELD_ELEM_NAME, Codec.DEFAULT_UNKNOWN_FIELD_ELEM_NAME);
featureMap.put(Feature.EXTENSION_FIELD_NAME_PREFIX, Codec.DEFAULT_EXTENSION_NAME_PREFIX);
featureMap.put(Feature.CLOSE_STREAM, true);
}
@Override
public void fromMessage(Message message, OutputStream os)
throws IOException {
Writer writer = new BufferedWriter(new OutputStreamWriter(os, DEFAULT_CHARSET));
fromMessage(message, writer);
}
@Override
public void fromMessage(Message message, Writer writer) throws IOException {
if (!message.isInitialized()) {
throw new IllegalArgumentException(
"Provided protobuf message is not initialized, call build() on the Message");
}
writeToStream(message, writer);
closeStreams(writer);
}
@Override
@SuppressWarnings("unchecked")
public <T extends Message> T toMessage(Class<T> messageType,
Reader reader, ExtensionRegistry extnRegistry) throws IOException {
Builder builder = null;
try {
Method builderMethod = messageType.getMethod("newBuilder");
builder = (Builder) builderMethod.invoke(messageType);
return (T) readFromStream(builder, reader, extnRegistry);
} catch (IOException ioe) {
throw ioe;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
closeStreams(reader);
}
}
@Override
public <T extends Message> T toMessage(Class<T> messageType, InputStream in) throws IOException {
Reader reader = new BufferedReader(new InputStreamReader(in, DEFAULT_CHARSET));
return toMessage(messageType, reader);
}
@Override
public <T extends Message> T toMessage(Class<T> messageType, Reader reader) throws IOException {
return toMessage(messageType, reader, ExtensionRegistry.getEmptyRegistry());
}
@Override
public <T extends Message> T toMessage(Class<T> messageType, InputStream in,
ExtensionRegistry extnRegistry) throws IOException {
Reader reader = new BufferedReader(new InputStreamReader(in, DEFAULT_CHARSET));
return toMessage(messageType, reader, extnRegistry);
}
/**
* Close streams
* @param stream
* @throws IOException
*/
protected void closeStreams(Closeable stream) throws IOException {
if (isFeatureSet(Feature.CLOSE_STREAM) && Boolean.TRUE.equals(Feature.CLOSE_STREAM)) {
stream.close();
}
}
protected abstract void writeToStream(Message message, Writer writer) throws IOException;
protected abstract Message readFromStream(Builder builder, Reader reader, ExtensionRegistry extnRegistry) throws IOException;
protected abstract void validateAndSetFeature(Feature feature, Object value);
//TODO Rewrite
@Override
public void setFeature(Feature feature, Object value) {
if (Feature.SUPPORT_UNKNOWN_FIELDS.equals(feature)) {
if (Boolean.TRUE.equals(value) || Boolean.FALSE.equals(value)) {
featureMap.put(feature, value);
} else {
throw new IllegalArgumentException(String.format("Feature [%s] does not support [%s] value", feature, value));
}
}
if (Feature.UNKNOWN_FIELD_ELEM_NAME.equals(feature)) {
if (value == null) {
throw new IllegalArgumentException(String.format("Feature [%s] does not support [%s] value", feature, value));
} else {
featureMap.put(feature, value);
}
}
if (Feature.STRIP_FIELD_NAME_UNDERSCORES.equals(feature)) {
if (!(Boolean.TRUE.equals(value) || Boolean.FALSE.equals(value))) {
throw new IllegalArgumentException(String.format("Unsupported value [%s] for feature [%s]", value, feature));
}
}
if (Feature.FIELD_NAME_READ_SUBSTITUTES.equals(feature) || Feature.FIELD_NAME_WRITE_SUBSTITUTES.equals(feature)) {
if (value == null || !(Map.class).isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException(String.format("Feature [%s] expected to be a non null Map<String,String>", feature));
}
}
validateAndSetFeature(feature, value);
featureMap.put(feature, value);
}
@Override
public boolean isFeatureSet(Feature feature) {
return featureMap.containsKey(feature);
}
@Override
public Object getFeature(Feature feature) {
return featureMap.get(feature);
}
/**
* Encodes the {@link UnknownFieldSet} to a base64 string
* @param unknownFields
* @return
*/
public static String encodeUnknownFieldsToString(UnknownFieldSet unknownFields) {
return new String(Base64.encodeBase64(unknownFields.toByteArray()));
}
/**
* Merges the unknownfield set, which was encoded as a base64 string to provided builder
* using the provided {@link ExtensionRegistry}
* @param builder
* @param extnReg
* @param unknownFieldText
* @return
* @throws InvalidProtocolBufferException
*/
public static Builder mergeUnknownFieldsFromString(Builder builder,
ExtensionRegistry extnReg, String unknownFieldText) throws InvalidProtocolBufferException {
byte[] unknownFields = Base64.decodeBase64(unknownFieldText.getBytes());
return builder.mergeFrom(unknownFields, extnReg);
}
@Override
public Map<Feature, Object> getAllFeaturesSet() {
return Collections.unmodifiableMap(featureMap);
}
public static boolean stripFieldNameUnderscores(Map<Feature, Object> featureMap) {
return Boolean.TRUE.equals(featureMap.get(Feature.STRIP_FIELD_NAME_UNDERSCORES));
}
public static String stripFieldName(String fieldName, Map<Feature, Object> featureMap) {
if (stripFieldNameUnderscores(featureMap) && fieldName.endsWith("_")) {
String processed = fieldName;
boolean modified = false;
if (processed.startsWith("_")) {
processed = processed.substring(1);
modified = true;
}
if (processed.endsWith("_")) {
processed = processed.substring(0, processed.length() - 1);
modified = true;
}
if (modified) {
return stripFieldName(processed, featureMap);
} else {
return processed;
}
}
return fieldName;
}
public static String substituteFieldNameForWriting(String fieldName, Map<Feature, Object> featureMap) {
return substituteFieldName(fieldName, false, featureMap);
}
public static String substituteFieldNameForReading(String fieldName, Map<Feature, Object> featureMap) {
return substituteFieldName(fieldName, true, featureMap);
}
private static String substituteFieldName(String fieldName, boolean read, Map<Feature, Object> featureMap) {
Map<String, String> aliases = Collections.EMPTY_MAP;
if (read && featureMap.containsKey(Feature.FIELD_NAME_READ_SUBSTITUTES)) {
aliases = (Map<String, String>) featureMap.get(Feature.FIELD_NAME_READ_SUBSTITUTES);
} else if (featureMap.containsKey(Feature.FIELD_NAME_WRITE_SUBSTITUTES)) {
aliases = (Map<String, String>) featureMap.get(Feature.FIELD_NAME_WRITE_SUBSTITUTES);
}
if (aliases.containsKey(fieldName)) {
return aliases.get(fieldName);
} else {
return fieldName;
}
}
/**
* Whether the provied field name follows the naming scheme for extn fields.
* default pattern is "extension-<field_name>". {@link Feature#EXTENSION_FIELD_NAME_PREFIX} can dictate the prefix
* @param fieldName the field name
* @return <code>true</code> if the field name follows the naming scheme for extn fields, <code>false</code> otherwise
*
*/
public static boolean isExtensionFieldName(String fieldName, Map<Feature, Object> featureMap) {
return fieldName.startsWith(((String) featureMap.get(Feature.EXTENSION_FIELD_NAME_PREFIX)) + "-");
}
/**
* Returns the field protobuf extn field name for the provided field name.
* @param fieldName the provided field name
* @return the protobuf field name = <field_name> for "extension_field_name>"
* @throws IllegalArgumentException if {@link #isExtensionFieldName(String, java.util.Map)} returns false
*/
public static String parseExtensionFieldName(String fieldName, Map<Feature, Object> featureMap) {
if (!isExtensionFieldName(fieldName, featureMap)) {
throw new IllegalArgumentException(String.format("Field [%s] does not follow the extn field naming scheme"));
}
StringBuilder strb = new StringBuilder(fieldName);
return strb.substring((((String) featureMap.get(Feature.EXTENSION_FIELD_NAME_PREFIX)) + "-").length(), fieldName.length());
}
/**
* The name this extn field needs to be written as
* @param fieldName
* @return {@link Feature#EXTENSION_FIELD_NAME_PREFIX}"_<fieldName>"
*/
public static String getExtensionFieldName(String fieldName, Map<Feature, Object> featureMap) {
StringBuilder strb = new StringBuilder((String) featureMap.get(Feature.EXTENSION_FIELD_NAME_PREFIX));
strb.append("-");
strb.append(fieldName);
return strb.toString();
}
/**
* Should I support unknown fields
* @param featureMap
* @return
* @see Feature#SUPPORT_UNKNOWN_FIELDS
*/
public static boolean supportUnknownFields(Map<Feature, Object> featureMap) {
return Boolean.TRUE.equals(featureMap.get(Feature.SUPPORT_UNKNOWN_FIELDS));
}
/**
* Get the name for the element which should contain the unknown fields
* @param featureMap
* @return
* @see Feature#UNKNOWN_FIELD_ELEM_NAME
*/
public static String getUnknownFieldElementName(Map<Feature, Object> featureMap) {
return (String) featureMap.get(Feature.UNKNOWN_FIELD_ELEM_NAME);
}
/**
* Should I pretty print?
* @param featureMap
* @return
* @see Feature#PRETTY_PRINT
*/
public static boolean prettyPrint(Map<Feature, Object> featureMap) {
return featureMap.containsKey(Feature.PRETTY_PRINT) && Boolean.TRUE.equals(featureMap.get(Feature.PRETTY_PRINT));
}
/**
* Should I close the underlying stream
* @param featureMap
* @return
* @see Feature#CLOSE_STREAM
*/
public static boolean closeStream(Map<Feature, Object> featureMap) {
return featureMap.containsKey(Feature.CLOSE_STREAM) && Boolean.TRUE.equals(featureMap.get(Feature.CLOSE_STREAM));
}
/**
* Is the provided field name an unknown field ?
* @param fieldName
* @param featureMap
* @return
*/
public static boolean isFieldNameUnknownField(String fieldName, Map<Feature, Object> featureMap) {
return featureMap.get(Feature.UNKNOWN_FIELD_ELEM_NAME).equals(fieldName);
}
}