package com.pinterest.secor.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.protobuf.Message;
import com.pinterest.secor.common.SecorConfig;
/**
* Various utilities for working with protocol buffer encoded messages. This
* utility will look for protobuf class in the configuration. It can be either
* per Kafka topic configuration, for example:
*
* <code>secor.protobuf.message.class.<topic>=<protobuf class name></code>
*
* or, it can be global configuration for all topics (in case all the topics
* transfer the same message type):
*
* <code>secor.protobuf.message.class.*=<protobuf class name></code>
*
* @author Michael Spector (spektom@gmail.com)
*/
public class ProtobufUtil {
private static final Logger LOG = LoggerFactory.getLogger(ProtobufUtil.class);
private boolean allTopics;
private Map<String, Class<? extends Message>> messageClassByTopic = new HashMap<String, Class<? extends Message>>();
private Map<String, Method> messageParseMethodByTopic = new HashMap<String, Method>();
private Class<? extends Message> messageClassForAll;
private Method messageParseMethodForAll;
/**
* Creates new instance of {@link ProtobufUtil}
*
* @param config
* Secor configuration instance
* @throws RuntimeException
* when configuration option
* <code>secor.protobuf.message.class</code> is invalid.
*/
@SuppressWarnings("unchecked")
public ProtobufUtil(SecorConfig config) {
Map<String, String> messageClassPerTopic = config.getProtobufMessageClassPerTopic();
for (Entry<String, String> entry : messageClassPerTopic.entrySet()) {
try {
String topic = entry.getKey();
Class<? extends Message> messageClass = (Class<? extends Message>) Class.forName(entry.getValue());
Method messageParseMethod = messageClass.getDeclaredMethod("parseFrom",
new Class<?>[] { byte[].class });
allTopics = "*".equals(topic);
if (allTopics) {
messageClassForAll = messageClass;
messageParseMethodForAll = messageParseMethod;
LOG.info("Using protobuf message class: {} for all Kafka topics", messageClass.getName());
} else {
messageClassByTopic.put(topic, messageClass);
messageParseMethodByTopic.put(topic, messageParseMethod);
LOG.info("Using protobuf message class: {} for Kafka topic: {}", messageClass.getName(), topic);
}
} catch (ClassNotFoundException e) {
LOG.error("Unable to load protobuf message class", e);
} catch (NoSuchMethodException e) {
LOG.error("Unable to find parseFrom() method in protobuf message class", e);
} catch (SecurityException e) {
LOG.error("Unable to use parseFrom() method from protobuf message class", e);
}
}
}
/**
* Returns whether there was a protobuf class configuration
*/
public boolean isConfigured() {
return allTopics || !messageClassByTopic.isEmpty();
}
/**
* Returns configured protobuf message class for the given Kafka topic
*
* @param topic
* Kafka topic
* @return protobuf message class used by this utility instance, or
* <code>null</code> in case valid class couldn't be found in the
* configuration.
*/
public Class<? extends Message> getMessageClass(String topic) {
return allTopics ? messageClassForAll : messageClassByTopic.get(topic);
}
/**
* Decodes protobuf message
*
* @param topic
* Kafka topic name
* @param payload
* Byte array containing encoded protobuf message
* @return protobuf message instance
* @throws RuntimeException
* when there's problem decoding protobuf message
*/
public Message decodeMessage(String topic, byte[] payload) {
try {
Method parseMethod = allTopics ? messageParseMethodForAll : messageParseMethodByTopic.get(topic);
return (Message) parseMethod.invoke(null, payload);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Can't parse protobuf message, since parseMethod() is not callable. "
+ "Please check your protobuf version (this code works with protobuf >= 2.6.1)", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Can't parse protobuf message, since parseMethod() is not accessible. "
+ "Please check your protobuf version (this code works with protobuf >= 2.6.1)", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Error parsing protobuf message", e);
}
}
}