package org.pentaho.di.trans.kafka.consumer; import kafka.consumer.ConsumerConfig; import org.pentaho.di.core.CheckResult; import org.pentaho.di.core.CheckResultInterface; import org.pentaho.di.core.Const; import org.pentaho.di.core.annotations.Step; import org.pentaho.di.core.database.DatabaseMeta; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettlePluginException; import org.pentaho.di.core.exception.KettleStepException; import org.pentaho.di.core.exception.KettleXMLException; import org.pentaho.di.core.row.RowMetaInterface; import org.pentaho.di.core.row.ValueMetaInterface; import org.pentaho.di.core.row.value.ValueMetaFactory; import org.pentaho.di.core.variables.VariableSpace; import org.pentaho.di.core.xml.XMLHandler; import org.pentaho.di.repository.ObjectId; import org.pentaho.di.repository.Repository; import org.pentaho.di.trans.Trans; import org.pentaho.di.trans.TransMeta; import org.pentaho.di.trans.step.*; import org.pentaho.metastore.api.IMetaStore; import org.w3c.dom.Node; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; /** * Kafka Consumer step definitions and serializer to/from XML and to/from Kettle * repository. * * @author Michael Spector */ @Step( id = "KafkaConsumer", image = "org/pentaho/di/trans/kafka/consumer/resources/kafka_consumer.png", i18nPackageName = "org.pentaho.di.trans.kafka.consumer", name = "KafkaConsumerDialog.Shell.Title", description = "KafkaConsumerDialog.Shell.Tooltip", documentationUrl = "KafkaConsumerDialog.Shell.DocumentationURL", casesUrl = "KafkaConsumerDialog.Shell.CasesURL", categoryDescription = "i18n:org.pentaho.di.trans.step:BaseStep.Category.Input") public class KafkaConsumerMeta extends BaseStepMeta implements StepMetaInterface { @SuppressWarnings("WeakerAccess") protected static final String[] KAFKA_PROPERTIES_NAMES = new String[]{"zookeeper.connect", "group.id", "consumer.id", "socket.timeout.ms", "socket.receive.buffer.bytes", "fetch.message.max.bytes", "auto.commit.interval.ms", "queued.max.message.chunks", "rebalance.max.retries", "fetch.min.bytes", "fetch.wait.max.ms", "rebalance.backoff.ms", "refresh.leader.backoff.ms", "auto.commit.enable", "auto.offset.reset", "consumer.timeout.ms", "client.id", "zookeeper.session.timeout.ms", "zookeeper.connection.timeout.ms", "zookeeper.sync.time.ms"}; @SuppressWarnings("WeakerAccess") protected static final Map<String, String> KAFKA_PROPERTIES_DEFAULTS = new HashMap<String, String>(); private static final String ATTR_TOPIC = "TOPIC"; private static final String ATTR_FIELD = "FIELD"; private static final String ATTR_KEY_FIELD = "KEY_FIELD"; private static final String ATTR_LIMIT = "LIMIT"; private static final String ATTR_TIMEOUT = "TIMEOUT"; private static final String ATTR_STOP_ON_EMPTY_TOPIC = "STOP_ON_EMPTY_TOPIC"; private static final String ATTR_KAFKA = "KAFKA"; static { KAFKA_PROPERTIES_DEFAULTS.put("zookeeper.connect", "localhost:2181"); KAFKA_PROPERTIES_DEFAULTS.put("group.id", "group"); } private Properties kafkaProperties = new Properties(); private String topic; private String field; private String keyField; private String limit; private String timeout; private boolean stopOnEmptyTopic; public static String[] getKafkaPropertiesNames() { return KAFKA_PROPERTIES_NAMES; } public static Map<String, String> getKafkaPropertiesDefaults() { return KAFKA_PROPERTIES_DEFAULTS; } public KafkaConsumerMeta() { super(); } public Properties getKafkaProperties() { return kafkaProperties; } @SuppressWarnings("unused") public Map getKafkaPropertiesMap() { return getKafkaProperties(); } public void setKafkaProperties(Properties kafkaProperties) { this.kafkaProperties = kafkaProperties; } @SuppressWarnings("unused") public void setKafkaPropertiesMap(Map<String, String> propertiesMap) { Properties props = new Properties(); props.putAll(propertiesMap); setKafkaProperties(props); } /** * @return Kafka topic name */ public String getTopic() { return topic; } /** * @param topic Kafka topic name */ public void setTopic(String topic) { this.topic = topic; } /** * @return Target field name in Kettle stream */ public String getField() { return field; } /** * @param field Target field name in Kettle stream */ public void setField(String field) { this.field = field; } /** * @return Target key field name in Kettle stream */ public String getKeyField() { return keyField; } /** * @param keyField Target key field name in Kettle stream */ public void setKeyField(String keyField) { this.keyField = keyField; } /** * @return Limit number of entries to read from Kafka queue */ public String getLimit() { return limit; } /** * @param limit Limit number of entries to read from Kafka queue */ public void setLimit(String limit) { this.limit = limit; } /** * @return Time limit for reading entries from Kafka queue (in ms) */ public String getTimeout() { return timeout; } /** * @param timeout Time limit for reading entries from Kafka queue (in ms) */ public void setTimeout(String timeout) { this.timeout = timeout; } /** * @return 'true' if the consumer should stop when no more messages are * available */ public boolean isStopOnEmptyTopic() { return stopOnEmptyTopic; } /** * @param stopOnEmptyTopic If 'true', stop the consumer when no more messages are * available on the topic */ public void setStopOnEmptyTopic(boolean stopOnEmptyTopic) { this.stopOnEmptyTopic = stopOnEmptyTopic; } public void check(List<CheckResultInterface> remarks, TransMeta transMeta, StepMeta stepMeta, RowMetaInterface prev, String[] input, String[] output, RowMetaInterface info, VariableSpace space, Repository repository, IMetaStore metaStore) { if (topic == null) { remarks.add(new CheckResult(CheckResultInterface.TYPE_RESULT_ERROR, Messages.getString("KafkaConsumerMeta.Check.InvalidTopic"), stepMeta)); } if (field == null) { remarks.add(new CheckResult(CheckResultInterface.TYPE_RESULT_ERROR, Messages.getString("KafkaConsumerMeta.Check.InvalidField"), stepMeta)); } if (keyField == null) { remarks.add(new CheckResult(CheckResultInterface.TYPE_RESULT_ERROR, Messages.getString("KafkaConsumerMeta.Check.InvalidKeyField"), stepMeta)); } try { new ConsumerConfig(kafkaProperties); } catch (IllegalArgumentException e) { remarks.add(new CheckResult(CheckResultInterface.TYPE_RESULT_ERROR, e.getMessage(), stepMeta)); } } public StepInterface getStep(StepMeta stepMeta, StepDataInterface stepDataInterface, int cnr, TransMeta transMeta, Trans trans) { return new KafkaConsumer(stepMeta, stepDataInterface, cnr, transMeta, trans); } public StepDataInterface getStepData() { return new KafkaConsumerData(); } @Override public void loadXML(Node stepnode, List<DatabaseMeta> databases, IMetaStore metaStore) throws KettleXMLException { try { topic = XMLHandler.getTagValue(stepnode, ATTR_TOPIC); field = XMLHandler.getTagValue(stepnode, ATTR_FIELD); keyField = XMLHandler.getTagValue(stepnode, ATTR_KEY_FIELD); limit = XMLHandler.getTagValue(stepnode, ATTR_LIMIT); timeout = XMLHandler.getTagValue(stepnode, ATTR_TIMEOUT); // This tag only exists if the value is "true", so we can directly // populate the field stopOnEmptyTopic = XMLHandler.getTagValue(stepnode, ATTR_STOP_ON_EMPTY_TOPIC) != null; Node kafkaNode = XMLHandler.getSubNode(stepnode, ATTR_KAFKA); String[] kafkaElements = XMLHandler.getNodeElements(kafkaNode); if (kafkaElements != null) { for (String propName : kafkaElements) { String value = XMLHandler.getTagValue(kafkaNode, propName); if (value != null) { kafkaProperties.put(propName, value); } } } } catch (Exception e) { throw new KettleXMLException(Messages.getString("KafkaConsumerMeta.Exception.loadXml"), e); } } @Override public String getXML() throws KettleException { StringBuilder retval = new StringBuilder(); if (topic != null) { retval.append(" ").append(XMLHandler.addTagValue(ATTR_TOPIC, topic)); } if (field != null) { retval.append(" ").append(XMLHandler.addTagValue(ATTR_FIELD, field)); } if (keyField != null) { retval.append(" ").append(XMLHandler.addTagValue(ATTR_KEY_FIELD, keyField)); } if (limit != null) { retval.append(" ").append(XMLHandler.addTagValue(ATTR_LIMIT, limit)); } if (timeout != null) { retval.append(" ").append(XMLHandler.addTagValue(ATTR_TIMEOUT, timeout)); } if (stopOnEmptyTopic) { retval.append(" ").append(XMLHandler.addTagValue(ATTR_STOP_ON_EMPTY_TOPIC, "true")); } retval.append(" ").append(XMLHandler.openTag(ATTR_KAFKA)).append(Const.CR); for (String name : kafkaProperties.stringPropertyNames()) { String value = kafkaProperties.getProperty(name); if (value != null) { retval.append(" ").append(XMLHandler.addTagValue(name, value)); } } retval.append(" ").append(XMLHandler.closeTag(ATTR_KAFKA)).append(Const.CR); return retval.toString(); } @Override public void readRep(Repository rep, IMetaStore metaStore, ObjectId stepId, List<DatabaseMeta> databases) throws KettleException { try { topic = rep.getStepAttributeString(stepId, ATTR_TOPIC); field = rep.getStepAttributeString(stepId, ATTR_FIELD); keyField = rep.getStepAttributeString(stepId, ATTR_KEY_FIELD); limit = rep.getStepAttributeString(stepId, ATTR_LIMIT); timeout = rep.getStepAttributeString(stepId, ATTR_TIMEOUT); stopOnEmptyTopic = rep.getStepAttributeBoolean(stepId, ATTR_STOP_ON_EMPTY_TOPIC); String kafkaPropsXML = rep.getStepAttributeString(stepId, ATTR_KAFKA); if (kafkaPropsXML != null) { kafkaProperties.loadFromXML(new ByteArrayInputStream(kafkaPropsXML.getBytes())); } // Support old versions: for (String name : KAFKA_PROPERTIES_NAMES) { String value = rep.getStepAttributeString(stepId, name); if (value != null) { kafkaProperties.put(name, value); } } } catch (Exception e) { throw new KettleException("KafkaConsumerMeta.Exception.loadRep", e); } } @Override public void saveRep(Repository rep, IMetaStore metaStore, ObjectId transformationId, ObjectId stepId) throws KettleException { try { if (topic != null) { rep.saveStepAttribute(transformationId, stepId, ATTR_TOPIC, topic); } if (field != null) { rep.saveStepAttribute(transformationId, stepId, ATTR_FIELD, field); } if (keyField != null) { rep.saveStepAttribute(transformationId, stepId, ATTR_KEY_FIELD, keyField); } if (limit != null) { rep.saveStepAttribute(transformationId, stepId, ATTR_LIMIT, limit); } if (timeout != null) { rep.saveStepAttribute(transformationId, stepId, ATTR_TIMEOUT, timeout); } rep.saveStepAttribute(transformationId, stepId, ATTR_STOP_ON_EMPTY_TOPIC, stopOnEmptyTopic); ByteArrayOutputStream buf = new ByteArrayOutputStream(); kafkaProperties.storeToXML(buf, null); rep.saveStepAttribute(transformationId, stepId, ATTR_KAFKA, buf.toString()); } catch (Exception e) { throw new KettleException("KafkaConsumerMeta.Exception.saveRep", e); } } /** * Set default values to the transformation */ public void setDefault() { setTopic(""); } public void getFields(RowMetaInterface rowMeta, String origin, RowMetaInterface[] info, StepMeta nextStep, VariableSpace space, Repository repository, IMetaStore metaStore) throws KettleStepException { try { ValueMetaInterface fieldValueMeta = ValueMetaFactory.createValueMeta(getField(), ValueMetaInterface.TYPE_BINARY); fieldValueMeta.setOrigin(origin); rowMeta.addValueMeta(fieldValueMeta); ValueMetaInterface keyFieldValueMeta = ValueMetaFactory.createValueMeta(getKeyField(), ValueMetaInterface.TYPE_BINARY); keyFieldValueMeta.setOrigin(origin); rowMeta.addValueMeta(keyFieldValueMeta); } catch (KettlePluginException e) { throw new KettleStepException("KafkaConsumerMeta.Exception.getFields", e); } } public static boolean isEmpty(String str) { return str == null || str.length() == 0; } }