/***************************************************************************** * * Copyright (C) Zenoss, Inc. 2010-2011, 2014 all rights reserved. * * This content is made available according to terms specified in * License.zenoss under the directory where your Zenoss product is installed. * ****************************************************************************/ package org.zenoss.zep.dao.impl; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Message; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.simple.SimpleJdbcOperations; import org.zenoss.protobufs.JsonFormat; import org.zenoss.protobufs.zep.Zep.ZepConfig; import org.zenoss.protobufs.zep.Zep.ZepConfig.Builder; import org.zenoss.zep.Messages; import org.zenoss.zep.ZepException; import org.zenoss.zep.annotations.TransactionalReadOnly; import org.zenoss.zep.annotations.TransactionalRollbackAllExceptions; import org.zenoss.zep.dao.ConfigDao; import org.zenoss.zep.dao.impl.compat.NestedTransactionService; import org.zenoss.zep.dao.impl.SimpleJdbcTemplateProxy; import java.lang.reflect.Proxy; import javax.sql.DataSource; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static org.zenoss.zep.dao.impl.EventConstants.*; public class ConfigDaoImpl implements ConfigDao { private static final Logger logger = LoggerFactory.getLogger(ConfigDao.class); private final SimpleJdbcOperations template; private Messages messages; private static final int MAX_PARTITIONS = 1000; private final int maxEventArchivePurgeIntervalDays; private int maxEventArchiveIntervalMinutes = 30 * 24 * 60; private static final long MIN_EVENT_SIZE_MAX_BYTES = 8 * 1024; private static final long MAX_EVENT_SIZE_MAX_BYTES = 100 * 1024; private NestedTransactionService nestedTransactionService; private static final String COLUMN_CONFIG_NAME = "config_name"; private static final String COLUMN_CONFIG_VALUE = "config_value"; public ConfigDaoImpl(DataSource ds, PartitionConfig partitionConfig) { this.template = (SimpleJdbcOperations) Proxy.newProxyInstance(SimpleJdbcOperations.class.getClassLoader(), new Class<?>[] {SimpleJdbcOperations.class}, new SimpleJdbcTemplateProxy(ds)); this.maxEventArchivePurgeIntervalDays = calculateMaximumDays(partitionConfig.getConfig(TABLE_EVENT_ARCHIVE)); logger.info("Maximum archive days: {}", maxEventArchivePurgeIntervalDays); } public void setNestedTransactionService(NestedTransactionService nestedTransactionService) { this.nestedTransactionService = nestedTransactionService; } private static int calculateMaximumDays(PartitionTableConfig config) { long partitionRange = config.getPartitionUnit().toMinutes(config.getPartitionDuration()) * MAX_PARTITIONS; return (int) TimeUnit.DAYS.convert(partitionRange, TimeUnit.MINUTES); } @Autowired public void setMessages(Messages messages) { this.messages = messages; } public void setMaxEventArchiveIntervalMinutes(int maxEventArchiveIntervalDays) { this.maxEventArchiveIntervalMinutes = maxEventArchiveIntervalDays; } @Override @TransactionalReadOnly public ZepConfig getConfig() throws ZepException { final String sql = "SELECT * FROM config"; ZepConfig config = template.getJdbcOperations().query(sql, new ZepConfigExtractor()); validateConfig(config); return config; } @Override @TransactionalRollbackAllExceptions public void setConfig(ZepConfig config) throws ZepException { validateConfig(config); final List<Map<String,String>> records = configToRecords(config); for (Map<String,String> record : records) { setConfigValueInternal(record); } } private void setConfigValueInternal(final Map<String,String> fields) throws ZepException { final String insertSql = "INSERT INTO config (config_name,config_value) VALUES(:config_name,:config_value)"; final String updateSql = "UPDATE config SET config_value=:config_value WHERE config_name=:config_name"; DaoUtils.insertOrUpdate(nestedTransactionService, template, insertSql, updateSql, fields); } @Override @TransactionalRollbackAllExceptions public int removeConfigValue(String name) throws ZepException { final String sql = "DELETE FROM config WHERE config_name=:config_name"; return this.template.update(sql, Collections.singletonMap(COLUMN_CONFIG_NAME, name)); } @Override @TransactionalRollbackAllExceptions public void setConfigValue(String name, ZepConfig config) throws ZepException { validateConfig(config); FieldDescriptor field = ZepConfig.getDescriptor().findFieldByName(name); if (field == null) { throw new ZepException("Invalid field name: " + name); } Object value = config.getField(field); if (value == null) { removeConfigValue(name); } final Map<String,String> fields = new HashMap<String,String>(2); fields.put(COLUMN_CONFIG_NAME, name); fields.put(COLUMN_CONFIG_VALUE, valueToString(field, value)); setConfigValueInternal(fields); } private void validateConfig(ZepConfig config) throws ZepException { int ageIntervalMinutes = config.getEventAgeIntervalMinutes(); if (ageIntervalMinutes < 0) { throw new ZepException(messages.getMessage("invalid_event_age_interval")); } long agingIntervalMilliseconds = config.getAgingIntervalMilliseconds(); if (agingIntervalMilliseconds < 1L) { throw new ZepException(messages.getMessage("invalid_aging_interval_milliseconds")); } int agingLimit = config.getAgingLimit(); if (agingLimit < 1) { throw new ZepException(messages.getMessage("invalid_aging_limit")); } int eventArchivePurgeIntervalDays = config.getEventArchivePurgeIntervalDays(); if (eventArchivePurgeIntervalDays < 1 || eventArchivePurgeIntervalDays > maxEventArchivePurgeIntervalDays) { throw new ZepException(messages.getMessage("invalid_event_archive_purge_interval", 1, maxEventArchivePurgeIntervalDays)); } int eventArchiveIntervalMinutes = config.getEventArchiveIntervalMinutes(); if (eventArchiveIntervalMinutes < 1 || eventArchiveIntervalMinutes > maxEventArchiveIntervalMinutes) { throw new ZepException(messages.getMessage("invalid_event_archive_interval", 1, maxEventArchiveIntervalMinutes)); } long archiveIntervalMilliseconds = config.getArchiveIntervalMilliseconds(); if (archiveIntervalMilliseconds < 1L) { throw new ZepException(messages.getMessage("invalid_archive_interval_milliseconds")); } int archiveLimit = config.getArchiveLimit(); if (archiveLimit < 1) { throw new ZepException(messages.getMessage("invalid_archive_limit")); } long eventMaxSizeBytes = config.getEventMaxSizeBytes(); if (eventMaxSizeBytes < MIN_EVENT_SIZE_MAX_BYTES || eventMaxSizeBytes > MAX_EVENT_SIZE_MAX_BYTES) { throw new ZepException(messages.getMessage("invalid_event_max_size_bytes", MIN_EVENT_SIZE_MAX_BYTES, MAX_EVENT_SIZE_MAX_BYTES)); } long indexSummaryIntervalMilliseconds = config.getIndexSummaryIntervalMilliseconds(); if (indexSummaryIntervalMilliseconds < 1L) { throw new ZepException(messages.getMessage("invalid_index_summary_interval_milliseconds")); } long indexArchiveIntervalMilliseconds = config.getIndexArchiveIntervalMilliseconds(); if (indexArchiveIntervalMilliseconds < 1L) { throw new ZepException(messages.getMessage("invalid_index_archive_interval_milliseconds")); } int indexLimit = config.getIndexLimit(); if (indexLimit < 1) { throw new ZepException(messages.getMessage("invalid_index_limit")); } int eventTimePurgeIntervalDays = config.getEventTimePurgeIntervalDays(); if (eventTimePurgeIntervalDays < 1) { throw new ZepException(messages.getMessage("invalid_event_time_purge_interval_days")); } } private static List<Map<String,String>> configToRecords(ZepConfig config) throws ZepException { final List<Map<String,String>> records = new ArrayList<Map<String,String>>(); final Descriptor descriptor = config.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { final Object value = config.getField(field); if (value != null) { Map<String,String> record = new HashMap<String, String>(2); record.put(COLUMN_CONFIG_NAME, field.getName()); record.put(COLUMN_CONFIG_VALUE, valueToString(field, value)); records.add(record); } } return records; } private static String valueToString(FieldDescriptor field, Object value) throws ZepException { if (field.isRepeated()) { throw new ZepException("Repeated field not supported"); } switch (field.getJavaType()) { case BOOLEAN: return Boolean.toString((Boolean)value); case BYTE_STRING: return new String(Base64.encodeBase64(((ByteString)value).toByteArray()), Charset.forName("US-ASCII")); case DOUBLE: return Double.toString((Double) value); case ENUM: return Integer.toString(((EnumValueDescriptor) value).getNumber()); case FLOAT: return Float.toString((Float) value); case INT: return Integer.toString((Integer) value); case LONG: return Long.toString((Long) value); case MESSAGE: try { return JsonFormat.writeAsString((Message) value); } catch (IOException e) { throw new ZepException(e.getLocalizedMessage(), e); } case STRING: return (String) value; default: throw new ZepException("Unsupported type: " + field.getType()); } } private static Object valueFromString(FieldDescriptor field, Builder builder, String strValue) throws ZepException { if (field.isRepeated()) { throw new ZepException("Repeated field not supported"); } switch (field.getJavaType()) { case BOOLEAN: return Boolean.valueOf(strValue); case BYTE_STRING: try { return ByteString.copyFrom(Base64.decodeBase64(strValue.getBytes("US-ASCII"))); } catch (UnsupportedEncodingException e) { // This exception should never happen - US-ASCII always exists in JVM throw new RuntimeException(e.getLocalizedMessage(), e); } case DOUBLE: return Double.valueOf(strValue); case ENUM: return field.getEnumType().findValueByNumber(Integer.valueOf(strValue)); case FLOAT: return Float.valueOf(strValue); case INT: return Integer.valueOf(strValue); case LONG: return Long.valueOf(strValue); case MESSAGE: try { return JsonFormat.merge(strValue, builder.newBuilderForField(field)); } catch (IOException e) { throw new ZepException(e.getLocalizedMessage(), e); } case STRING: return strValue; default: throw new ZepException("Unsupported type: " + field.getType()); } } private static final class ZepConfigExtractor implements ResultSetExtractor<ZepConfig> { @Override public ZepConfig extractData(ResultSet rs) throws SQLException, DataAccessException { Descriptor descriptor = ZepConfig.getDescriptor(); Builder configBuilder = ZepConfig.newBuilder(); while (rs.next()) { final String fieldName = rs.getString(COLUMN_CONFIG_NAME); final String fieldValueStr = rs.getString(COLUMN_CONFIG_VALUE); FieldDescriptor field = descriptor.findFieldByName(fieldName); if (field == null) { logger.warn("Unrecognized field: {}", fieldName); } else { try { Object fieldValue = valueFromString(field, configBuilder, fieldValueStr); configBuilder.setField(field, fieldValue); } catch (ZepException e) { throw new SQLException(e.getLocalizedMessage(), e); } } } return configBuilder.build(); } } }