/* * Copyright 2014-2015 JKOOL, LLC. * * 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 com.jkoolcloud.tnt4j.config; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import com.jkoolcloud.tnt4j.dump.DumpSinkFactory; import com.jkoolcloud.tnt4j.source.Source; import com.jkoolcloud.tnt4j.source.SourceFactory; import com.jkoolcloud.tnt4j.core.ActivityListener; import com.jkoolcloud.tnt4j.core.OpLevel; import com.jkoolcloud.tnt4j.format.EventFormatter; import com.jkoolcloud.tnt4j.locator.GeoLocator; import com.jkoolcloud.tnt4j.repository.TokenRepository; import com.jkoolcloud.tnt4j.selector.TrackingSelector; import com.jkoolcloud.tnt4j.sink.DefaultEventSinkFactory; import com.jkoolcloud.tnt4j.sink.EventSink; import com.jkoolcloud.tnt4j.sink.EventSinkFactory; import com.jkoolcloud.tnt4j.sink.SinkErrorListener; import com.jkoolcloud.tnt4j.sink.SinkEventFilter; import com.jkoolcloud.tnt4j.sink.SinkLogEventListener; import com.jkoolcloud.tnt4j.source.SourceType; import com.jkoolcloud.tnt4j.tracker.TrackerFactory; import com.jkoolcloud.tnt4j.utils.Utils; import com.jkoolcloud.tnt4j.uuid.SignFactory; import com.jkoolcloud.tnt4j.uuid.UUIDFactory; /** * <p> * This class consolidates all configuration for {@link TrackerFactory} using a configuration file. * Developers should use this class and override default configuration with user defined elements. Configuration is * loaded from a file specified by {@code tnt4j.config} property which set to {@code tnt4j.properties} by * default. Configuration specifies factories, formatters, token repositories and other elements required by the * framework using JSON like convention. * </p> * <p> * Below is a example of the sample configuration file (tnt4j.properties): * </p> * * <pre> * <code> * ; source: * designates all sources, which is used as default for non matching sources * { * source: * * event.sink.factory: com.jkoolcloud.tnt4j.sink.impl.slf4j.SLF4JEventSinkFactory * event.source.factory: com.jkoolcloud.tnt4j.core.DefaultSourceFactory * event.formatter: com.jkoolcloud.tnt4j.format.DefaultFormatter * token.repository: com.jkoolcloud.tnt4j.repository.FileTokenRepository * tracking.selector: com.jkoolcloud.tnt4j.selector.DefaultTrackingSelector * activity.listener: com.jkoolcloud.tnt4j.examples.MyActivityHandler * } * { * source: com * event.sink.factory: com.jkoolcloud.tnt4j.sink.impl.slf4j.SLF4JEventSinkFactory * event.formatter: com.jkoolcloud.tnt4j.format.DefaultFormatter * token.repository: com.jkoolcloud.tnt4j.repository.FileTokenRepository * tracking.selector: com.jkoolcloud.tnt4j.selector.DefaultTrackingSelector * activity.listener: com.jkoolcloud.tnt4j.examples.MyActivityHandler * } * ;Stanza used for sources that start with com.jkoolcloud * { * source: com.jkoolcloud * tracker.factory: com.jkoolcloud.tnt4j.tracker.DefaultTrackerFactory * dump.sink.factory: com.jkoolcloud.tnt4j.dump.DefaultDumpSinkFactory * event.sink.factory: com.jkoolcloud.tnt4j.sink.impl.SocketEventSinkFactory * event.sink.factory.Host: localhost * event.sink.factory.Port: 6408 * event.formatter: com.jkoolcloud.tnt4j.format.JSONFormatter * tracking.selector: com.jkoolcloud.tnt4j.selector.DefaultTrackingSelector * tracking.selector.Repository: com.jkoolcloud.tnt4j.repository.FileTokenRepository * } * ; define source based on configuration from another source defined above * { * source: org * like: com.jkoolcloud * enabled: true * } * </code> * </pre> * * Below is an example of how to use {@link TrackerConfigStore} when registering with the framework. * * <pre> * {@code * TrackerConfig config = DefaultConfigFactory.getInstance().getConfig(source); * TrackingLogger.register(config.build()); * ... * } * </pre> * * @see TokenRepository * @see TrackingSelector * @see EventFormatter * @see EventSinkFactory * * @version $Revision: 11 $ * */ public class TrackerConfigStore extends TrackerConfig { private static final EventSink logger = DefaultEventSinkFactory.defaultEventSink(TrackerConfigStore.class); public static final String TNT4J_PROPERTIES_KEY = "tnt4j.config"; public static final String TNT4J_PROPERTIES = "tnt4j.properties"; private static final String DEFAULT_SOURCE = "*"; private static final String SOURCE_KEY = "source"; private static final String ENABLED_KEY = "enabled"; private static final String LIKE_KEY = "like"; private String configFile = null; /** * Create an default configuration with a specific source name. Configuration is loaded from a file specified by * {@code tnt4j.config} property. * * @param source * name of the source instance associated with the configuration */ protected TrackerConfigStore(String source) { this(source, SourceType.APPL); } /** * Create an default configuration with a specific source name. Configuration is loaded from a file specified by * {@code tnt4j.config} property. * * @param source * name of the source instance associated with the configuration * @param type * source type */ protected TrackerConfigStore(String source, SourceType type) { this(source, type, (String) null); } /** * Create an default configuration with a specific source name. Configuration is loaded from a file specified by * {@code tnt4j.config} property if fileName is null. * * @param source * name of the source instance associated with the configuration * @param type * type of the source instance * @param fileName * configuration file name */ protected TrackerConfigStore(String source, SourceType type, String fileName) { super(source, type); initConfig(fileName); } /** * Create an default configuration with a specific source name. Configuration is loaded from a * key/Properties map. * * @param source * name of the source instance associated with the configuration * @param type * type of the source instance * @param configMap * configuration map containing source/properties configuration */ protected TrackerConfigStore(String source, SourceType type, Map<String, Properties> configMap) { super(source, type); loadConfigProps(configMap); } /** * Create an default configuration with a specific source name. Configuration is loaded from a * key/Properties map. * * @param source * name of the source instance associated with the configuration * @param configMap * configuration map containing source/properties configuration */ protected TrackerConfigStore(String source, Map<String, Properties> configMap) { super(source, SourceType.APPL); loadConfigProps(configMap); } /** * Create an default configuration with a specific source name and a given file name; * * @param source * source instance associated with the configuration * @param configMap * configuration map containing source/properties configuration */ protected TrackerConfigStore(Source source, Map<String, Properties> configMap) { super(source); loadConfigProps(configMap); } /** * Create an default configuration with a specific source name. Configuration is loaded from a file specified by * {@code tnt4j.config} property. * * @param source * source instance associated with the configuration */ protected TrackerConfigStore(Source source) { this(source, (String) null); } /** * Create an default configuration with a specific source name and a given file name; * * @param source * source instance associated with the configuration * @param fileName * configuration file name */ protected TrackerConfigStore(Source source, String fileName) { super(source); initConfig(fileName); } private void initConfig(String fileName) { configFile = fileName == null ? System.getProperty(TNT4J_PROPERTIES_KEY, TNT4J_PROPERTIES) : fileName; setProperty(TNT4J_PROPERTIES_KEY, configFile); Map<String, Properties> configMap = loadConfiguration(configFile); loadConfigProps(configMap); } private Object createConfigurableObject(String classProp, String prefix) { Properties props = getProperties(); try { return Utils.createConfigurableObject(classProp, prefix, props); } catch (Throwable e) { logger.log(OpLevel.ERROR, "Failed to create configurable instance class={0}, property={1}, prefix={2}", props.get(classProp), classProp, prefix, e); } return null; } private void loadConfigProps(Map<String, Properties> map) { setProperties(loadProperties(map)); applyProperties(); } /** * Applies properties defined configuration. */ public void applyProperties() { if (props != null) { if (logger.isSet(OpLevel.DEBUG)) { logger.log(OpLevel.DEBUG, "Loaded properties source={0}, tid={1}, properties={2}", srcName, Thread.currentThread().getId(), props); } setUUIDFactory((UUIDFactory) createConfigurableObject("uuid.factory", "uuid.factory.")); setSignFactory((SignFactory) createConfigurableObject("sign.factory", "sign.factory.")); setGeoLocator((GeoLocator) createConfigurableObject("geo.locator", "geo.locator.")); setDefaultEventSinkFactory((EventSinkFactory) createConfigurableObject("default.event.sink.factory", "default.event.sink.factory.")); setSourceFactory((SourceFactory) createConfigurableObject("source.factory", "source.factory.")); setTrackerFactory((TrackerFactory) createConfigurableObject("tracker.factory", "tracker.factory.")); setEventSinkFactory((EventSinkFactory) createConfigurableObject("event.sink.factory", "event.sink.factory.")); setEventFormatter((EventFormatter) createConfigurableObject("event.formatter", "event.formatter.")); setTrackingSelector((TrackingSelector) createConfigurableObject("tracking.selector", "tracking.selector.")); setDumpSinkFactory((DumpSinkFactory) createConfigurableObject("dump.sink.factory", "dump.sink.factory.")); setActivityListener((ActivityListener) createConfigurableObject("activity.listener", "activity.listener.")); setSinkLogEventListener((SinkLogEventListener) createConfigurableObject("sink.log.listener", "sink.log.listener.")); setSinkErrorListener((SinkErrorListener) createConfigurableObject("sink.error.listener", "sink.error.listener.")); setSinkEventFilter((SinkEventFilter) createConfigurableObject("sink.event.filter", "sink.event.filter.")); } } private Properties loadProperties(Map<String, Properties> map) { int maxKeyLen = 0; Properties selectedSet = null; if (map == null) return selectedSet; for (Entry<String, Properties> entry : map.entrySet()) { if (entry.getKey().equals(DEFAULT_SOURCE)) { selectedSet = entry.getValue(); continue; } // find the best match (longest string match) String configKey = entry.getKey(); boolean match = this.srcName.indexOf(configKey) >= 0; if (match && configKey.length() > maxKeyLen) { maxKeyLen = configKey.length(); selectedSet = entry.getValue(); } } return selectedSet; } private Map<String, Properties> loadConfiguration(String configFile) { Map<String, Properties> map = null; try { map = loadConfigResource(configFile); logger.log(OpLevel.DEBUG, "Loaded configuration source={0}, file={1}, config.size={2}, tid={3}", srcName, configFile, map.size(), Thread.currentThread().getId()); } catch (Throwable e) { logger.log(OpLevel.ERROR, "Unable to load configuration: source={0}, file={1}", srcName, configFile, e); } return map; } private Map<String, Properties> loadConfigResource(String fileName) throws IOException { LinkedHashMap<String, Properties> map = new LinkedHashMap<String, Properties>(111); BufferedReader reader = null; try { reader = getConfigReader(fileName); Properties config = null; do { config = readStanza(reader); String key = config.getProperty(SOURCE_KEY); String like = config.getProperty(LIKE_KEY); String enabled = config.getProperty(ENABLED_KEY); if (enabled != null && enabled.equalsIgnoreCase("true")) { logger.log(OpLevel.WARNING, "Disabling properties for source={0}, like={1}, enabled={2}", key, like, enabled); continue; } if (like != null) { config = mergeConfig(key, like, config, map); } if (key != null) { map.put(key, config); } } while (!config.isEmpty()); } finally { Utils.close(reader); } return map; } private Properties mergeConfig(String key, String like, Properties config, Map<String, Properties> map) { Properties copyFrom = map.get(like); if (copyFrom == null) { copyFrom = map.get(DEFAULT_SOURCE); logger.log(OpLevel.WARNING, "Properties for source={0}, like={1} not found, assigning default set={2}", key, like, DEFAULT_SOURCE); } // merge properties from "like" model with original Properties merged = new Properties(); merged.putAll(copyFrom); merged.putAll(config); return merged; } private BufferedReader getConfigReader(String fileName) throws IOException { BufferedReader reader = null; IOException exc = null; try { if (fileName != null) { reader = new BufferedReader(new FileReader(fileName)); return reader; } } catch (IOException err) { exc = err; } String tnt4jResource = "/" + TNT4J_PROPERTIES; InputStream ins = getClass().getResourceAsStream(tnt4jResource); if (ins == null) { FileNotFoundException ioe = new FileNotFoundException("Resource '" + tnt4jResource + "' not found"); ioe.initCause(exc); throw ioe; } reader = new BufferedReader(new InputStreamReader(ins)); return reader; } private Properties readStanza(BufferedReader reader) throws IOException { String line; Properties props = new Properties(); do { line = reader.readLine(); if (line != null) { line = line.trim(); if ((line.isEmpty()) || line.startsWith("{") || line.startsWith(";") || line.startsWith("#") || line.startsWith("//") || line.endsWith("}")) { continue; } int sepIndex = line.indexOf(":"); if (sepIndex <= 0) { logger.log(OpLevel.WARNING, "Skipping invalid source={0}, file={1}, entry='{2}'", srcName, configFile, line); continue; } String key = line.substring(0, sepIndex).trim(); String value = line.substring(sepIndex + 1).trim(); props.setProperty(key, Utils.resolve(value, value)); } } while (line != null && !line.endsWith("}")); return props; } }