/* * Copyright (c) 2016 Network New Technologies Inc. * * 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.networknt.config; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import org.owasp.encoder.Encode; import org.slf4j.ext.XLogger; import org.slf4j.ext.XLoggerFactory; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.yaml.snakeyaml.Yaml; /** * A injectable singleton config that has default implementation * based on FileSystem json files. It can be extended to * other sources (database, distributed cache etc.) by providing * another jar in the classpath to replace the default implementation. * * Config files are loaded in the following sequence: * 1. resources/config folder for the default * 2. externalized directory specified by light-4j-config-dir * * In docker, the config files should be in volume and any update will * be picked up the next day morning. * * */ public abstract class Config { public static final String LIGHT_4J_CONFIG_DIR = "light-4j-config-dir"; protected Config() { } // abstract methods that need be implemented by all implementations public abstract Map<String, Object> getJsonMapConfig(String configName); public abstract Map<String, Object> getJsonMapConfigNoCache(String configName); //public abstract JsonNode getJsonNodeConfig(String configName); public abstract Object getJsonObjectConfig(String configName, Class clazz); public abstract String getStringFromFile(String filename); public abstract InputStream getInputStreamFromFile(String filename); public abstract ObjectMapper getMapper(); public abstract Yaml getYaml(); public abstract void clear(); public static Config getInstance() { return FileConfigImpl.DEFAULT; } private static final class FileConfigImpl extends Config { static final String CONFIG_EXT_JSON = ".json"; static final String CONFIG_EXT_YAML = ".yaml"; static final String CONFIG_EXT_YML = ".yml"; static final XLogger logger = XLoggerFactory.getXLogger(Config.class); public final String EXTERNALIZED_PROPERTY_DIR = System.getProperty(LIGHT_4J_CONFIG_DIR, ""); private long cacheExpirationTime = 0L; private static final Config DEFAULT = initialize(); // Memory cache of all the configuration object. Each config will be loaded on the first time is is accessed. final Map<String, Object> configCache = new ConcurrentHashMap<>(10, 0.9f, 1); // An instance of Jackson ObjectMapper that can be used anywhere else for Json. final ObjectMapper mapper = new ObjectMapper(); final Yaml yaml = new Yaml(); private static Config initialize() { Iterator<Config> it; it = ServiceLoader.load(Config.class).iterator(); return it.hasNext() ? it.next() : new FileConfigImpl(); } // Return instance of Jackson Object Mapper @Override public ObjectMapper getMapper() { return mapper; } @Override public Yaml getYaml() { return yaml; } @Override public void clear() { configCache.clear(); } @Override public String getStringFromFile(String filename) { checkCacheExpiration(); String content = (String)configCache.get(filename); if(content == null) { synchronized (FileConfigImpl.class) { content = (String)configCache.get(filename); if(content == null) { content = loadStringFromFile(filename); if(content != null) configCache.put(filename, content); } } } return content; } @Override public InputStream getInputStreamFromFile(String filename) { return getConfigStream(filename); } @Override public Object getJsonObjectConfig(String configName, Class clazz) { checkCacheExpiration(); Object config = configCache.get(configName); if(config == null) { synchronized (FileConfigImpl.class) { config = configCache.get(configName); if(config == null) { config = loadObjectConfig(configName, clazz); if(config != null) configCache.put(configName, config); } } } return config; } @Override public Map<String, Object> getJsonMapConfig(String configName) { checkCacheExpiration(); Map<String, Object> config = (Map<String, Object>)configCache.get(configName); if(config == null) { synchronized (FileConfigImpl.class) { config = (Map<String, Object>)configCache.get(configName); if(config == null) { config = loadMapConfig(configName); if(config != null) configCache.put(configName, config); } } } return config; } @Override public Map<String, Object> getJsonMapConfigNoCache(String configName) { return loadMapConfig(configName); } private String loadStringFromFile(String filename) { String content = null; InputStream inStream = null; try { inStream = getConfigStream(filename); if(inStream != null) { content = convertStreamToString(inStream); } } catch (Exception ioe) { logger.catching(ioe); } finally { if(inStream != null) { try { inStream.close(); } catch(IOException ioe) { logger.catching(ioe); } } } return content; } private Object loadObjectConfig(String configName, Class clazz) { Object config = null; String ymlFilename = configName + CONFIG_EXT_YML; try (InputStream inStream = getConfigStream(ymlFilename)) { if(inStream != null) { config = yaml.loadAs(inStream, clazz); } } catch (IOException ioe) { logger.error("IOException", ioe); } if(config != null) return config; String yamlFilename = configName + CONFIG_EXT_YAML; try (InputStream inStream = getConfigStream(yamlFilename)) { if(inStream != null) { config = yaml.loadAs(inStream, clazz); } } catch (IOException ioe) { logger.error("IOException", ioe); } if(config != null) return config; String jsonFilename = configName + CONFIG_EXT_JSON; try (InputStream inStream = getConfigStream(jsonFilename)) { if(inStream != null) { config = mapper.readValue(inStream, clazz); } } catch (IOException ioe) { logger.error("IOException", ioe); } return config; } private Map<String, Object> loadMapConfig(String configName) { Map<String, Object> config = null; String ymlFilename = configName + CONFIG_EXT_YML; try (InputStream inStream = getConfigStream(ymlFilename)) { if(inStream != null) { config = (Map<String, Object>)yaml.load(inStream); } } catch (IOException ioe) { logger.error("IOException", ioe); } if(config != null) return config; String yamlFilename = configName + CONFIG_EXT_YAML; try (InputStream inStream = getConfigStream(yamlFilename)) { if(inStream != null) { config = (Map<String, Object>)yaml.load(inStream); } } catch (IOException ioe) { logger.error("IOException", ioe); } if(config != null) return config; String configFilename = configName + CONFIG_EXT_JSON; try (InputStream inStream = getConfigStream(configFilename)){ if(inStream != null) { config = mapper.readValue(inStream, new TypeReference<HashMap<String, Object>>() {}); } } catch (IOException ioe) { logger.error("IOException", ioe); } return config; } private InputStream getConfigStream(String configFilename) { InputStream inStream = null; try{ inStream = new FileInputStream(EXTERNALIZED_PROPERTY_DIR + "/" + configFilename); } catch (FileNotFoundException ex){ if(logger.isInfoEnabled()) { logger.info("Unable to load config from externalized folder for " + Encode.forJava(configFilename + " in " + EXTERNALIZED_PROPERTY_DIR)); } } if(inStream != null) { if(logger.isInfoEnabled()) { logger.info("Config loaded from externalized folder for " + Encode.forJava(configFilename + " in " + EXTERNALIZED_PROPERTY_DIR)); } return inStream; } if(logger.isInfoEnabled()) { logger.info("Trying to load config from classpath directory for file " + Encode.forJava(configFilename)); } inStream = getClass().getClassLoader().getResourceAsStream(configFilename); if(inStream != null) { if(logger.isInfoEnabled()) { logger.info("config loaded from classpath for " + Encode.forJava(configFilename)); } return inStream; } inStream = getClass().getClassLoader().getResourceAsStream("config/" + configFilename); if(inStream != null) { if(logger.isInfoEnabled()) { logger.info("Config loaded from default folder for " + Encode.forJava(configFilename)); } return inStream; } if(logger.isInfoEnabled()) { logger.error("Unable to load config " + Encode.forJava(configFilename)); } return null; } private static long getNextMidNightTime() { Calendar cal = new GregorianCalendar(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); cal.add(Calendar.DAY_OF_MONTH, 1); return cal.getTimeInMillis(); } private void checkCacheExpiration() { if(System.currentTimeMillis() > cacheExpirationTime) { clear(); logger.info("daily config cache refresh"); cacheExpirationTime = getNextMidNightTime(); } } } static String convertStreamToString(java.io.InputStream is) { java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } }