Java tutorial
/* * The Fascinator - JSON Simple Config * Copyright (C) 2011 University of Southern Queensland * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package com.googlecode.fascinator.common; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.json.simple.JSONArray; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * <p> * An extension of the JsonSimple class specifically to access configuration. * </p> * * <p> * Aside from offering a constructor that takes care of finding and accessing * the system configuration file, this class also offers a selection of methods * for management of the system configuration, such as backup and version * testing. * </p> * * <p> * Finally, whatever configuration is provided to this class, it will be backed * by the full system configuration file. Nodes not found in the provided config * will also be checked in the System config. * </p> * * @author Greg Pendlebury */ @Component(value = "fascinatorConfig") public class JsonSimpleConfig extends JsonSimple { /** Logging */ private static Logger log = LoggerFactory.getLogger(JsonSimpleConfig.class); /** Default configuration directory */ private static final String CONFIG_DIR = FascinatorHome.getPath(); /** Default system configuration file name */ private static final String SYSTEM_CONFIG_FILE = "system-config.json"; /** Fallback to system configuration file */ private JsonSimple systemConfig; private static final String INCLUDE_DIR_KEY = "includeConfigDir"; private static final String INCLUDE_DIR_KEY_EXT = "includeConfigExt"; /** * Creates JSON Configuration object from the system config file * * @throws IOException if there was an error during creation */ public JsonSimpleConfig() throws IOException { super(JsonSimpleConfig.getSystemFile()); systemConfig = new JsonSimple(JsonSimpleConfig.getSystemFile()); loadIncludeDir(); } /** * Creates JSON Configuration object from the provided config file * * @param jsonFile : The file containing JSON * @throws IOException if there was an error during creation */ public JsonSimpleConfig(File jsonFile) throws IOException { super(jsonFile); systemConfig = new JsonSimple(JsonSimpleConfig.getSystemFile()); loadIncludeDir(); } /** * Creates JSON Configuration object from the provided input stream * * @param jsonIn : The input stream to read * @throws IOException if there was an error during creation */ public JsonSimpleConfig(InputStream jsonIn) throws IOException { super(jsonIn); systemConfig = new JsonSimple(JsonSimpleConfig.getSystemFile()); loadIncludeDir(); } /** * Creates JSON Configuration object from the provided config string * * @param jsonString : The JSON in string form * @throws IOException if there was an error during creation */ public JsonSimpleConfig(String jsonString) throws IOException { super(jsonString); systemConfig = new JsonSimple(JsonSimpleConfig.getSystemFile()); loadIncludeDir(); } /** * Performs a backup on the system-wide configuration file from the default * config dir if it exists. Returns a reference to the backed up file. * * @return the backed up system JSON file * @throws IOException if there was an error reading or writing either file */ public static File backupSystemFile() throws IOException { File configFile = new File(CONFIG_DIR, SYSTEM_CONFIG_FILE); File backupFile = new File(CONFIG_DIR, SYSTEM_CONFIG_FILE + ".old"); if (!configFile.exists()) { throw new IOException("System file does not exist! '" + configFile.getAbsolutePath() + "'"); } else { if (backupFile.exists()) { backupFile.delete(); } OutputStream out = new FileOutputStream(backupFile); InputStream in = new FileInputStream(configFile); IOUtils.copy(in, out); in.close(); out.close(); log.info("Configuration copied to '{}'", backupFile); } return backupFile; } /** * Gets the system-wide configuration file from the default config dir. If * the file doesn't exist, a default is copied to the config dir. * * @return the system JSON file * @throws IOException if there was an error reading or writing the system * configuration file */ public static File getSystemFile() throws IOException { File configFile = new File(CONFIG_DIR, SYSTEM_CONFIG_FILE); if (!configFile.exists()) { configFile.getParentFile().mkdirs(); OutputStream out = new FileOutputStream(configFile); IOUtils.copy(JsonSimpleConfig.class.getResourceAsStream("/" + SYSTEM_CONFIG_FILE), out); out.close(); log.info("Default configuration copied to '{}'", configFile); } return configFile; } /** * Tests whether or not the system-config has been properly configured. * * @return <code>true</code> if configured, <code>false</code> if still * using defaults */ public boolean isConfigured() { return getBoolean(false, "configured"); } /** * To check if configuration file is outdated * * @return <code>true</code> if outdated, <code>false</code> otherwise */ public boolean isOutdated() { boolean outdated = false; String systemVersion = getString(null, "version"); if (systemVersion == null) { return true; } try { JsonSimple compiledConfig = new JsonSimple(getClass().getResourceAsStream("/" + SYSTEM_CONFIG_FILE)); String compiledVersion = compiledConfig.getString(null, "version"); outdated = !systemVersion.equals(compiledVersion); if (compiledVersion == null) { return false; } if (outdated) { log.debug("Configuration versions do not match! '{}' != '{}'", systemVersion, compiledVersion); } } catch (IOException ioe) { log.error("Failed to parse compiled configuration!", ioe); } return outdated; } /** * Walk down the JSON nodes specified by the path and retrieve the target * JSONArray. * * @param path : Variable length array of path segments * @return JSONArray : The target node, or NULL if path invalid or not an * array */ @Override public JSONArray getArray(Object... path) { JSONArray array = super.getArray(path); if (array == null && systemConfig != null) { return systemConfig.getArray(path); } return array; } /** * Walk down the JSON nodes specified by the path and retrieve the target * JsonObject. * * @param path : Variable length array of path segments * @return JsonObject : The target node, or NULL if path invalid or not an * object */ @Override public JsonObject getObject(Object... path) { JsonObject object = super.getObject(path); if (object == null && systemConfig != null) { return systemConfig.getObject(path); } return object; } /** * Walk down the JSON nodes specified by the path and retrieve the target. * * @param path : Variable length array of path segments * @return Object : The target node, or NULL if invalid */ @Override public Object getPath(Object... path) { Object object = super.getPath(path); if (object == null && systemConfig != null) { return systemConfig.getPath(path); } return object; } /** * Retrieve the Boolean value on the given path. * * <strong>IMPORTANT:</strong> The default value only applies if the path is * not found. If a string on the path is found it will be considered * <b>false</b> unless the value is 'true' (ignoring case). This is the * default behaviour of the Boolean.parseBoolean() method. * * @param defaultValue : The fallback value to use if the path is invalid or * not found * @param path : An array of indeterminate length to use as the path * @return Boolean : The Boolean value found on the given path, or null if * no default provided */ @Override public Boolean getBoolean(Boolean defaultValue, Object... path) { Boolean bool = super.getBoolean(null, path); if (bool == null) { if (systemConfig != null) { return systemConfig.getBoolean(defaultValue, path); } return defaultValue; } return bool; } /** * Retrieve the Integer value on the given path. * * @param defaultValue : The fallback value to use if the path is invalid or * not found * @param path : An array of indeterminate length to use as the path * @return Integer : The Integer value found on the given path, or null if * no default provided */ @Override public Integer getInteger(Integer defaultValue, Object... path) { Integer integer = super.getInteger(null, path); if (integer == null) { if (systemConfig != null) { return systemConfig.getInteger(defaultValue, path); } return defaultValue; } return integer; } /** * Retrieve the String value on the given path. * * @param defaultValue : The fallback value to use if the path is invalid or * not found * @param path : An array of indeterminate length to use as the path * @return String : The String value found on the given path, or null if no * default provided */ @Override public String getString(String defaultValue, Object... path) { String string = super.getString(null, path); if (string == null) { if (systemConfig != null) { return systemConfig.getString(defaultValue, path); } return defaultValue; } return string; } /** * <p> * Retrieve a list of Strings found on the given path. Note that this is a * utility function, and not designed for data traversal. It <b>will</b> * only retrieve Strings found on the provided node, and the node must be a * JSONArray. * </p> * * @param path : An array of indeterminate length to use as the path * @return List<String> : A list of Strings, null if the node is not found */ @Override public List<String> getStringList(Object... path) { List<String> list = super.getStringList(path); if (list == null && systemConfig != null) { return systemConfig.getStringList(path); } return list; } /** * <p> * Retrieve a list of JsonSimple objects found on the given path. Note that * this is a utility function, and not designed for data traversal. It * <b>will</b> only retrieve valid JsonObjects found on the provided node, * and wrap them in JsonSimple objects. * </p> * * <p> * Other objects found on that path will be ignored, and if the path itself * is not a JSONArray or not found, the function will return NULL. * </p> * * @param path : An array of indeterminate length to use as the path * @return List<JsonSimple> : A list of JSONSimple objects, or null */ @Override public List<JsonSimple> getJsonSimpleList(Object... path) { List<JsonSimple> list = super.getJsonSimpleList(path); if (list == null && systemConfig != null) { return systemConfig.getJsonSimpleList(path); } return list; } /** * <p> * Retrieve a map of JsonSimple objects found on the given path. Note that * this is a utility function, and not designed for data traversal. It * <b>will</b> only retrieve valid JsonObjects found on the provided node, * and wrap them in JsonSimple objects. * </p> * * <p> * Other objects found on that path will be ignored, and if the path itself * is not a JsonObject or not found, the function will return NULL. * </p> * * @param path : An array of indeterminate length to use as the path * @return Map<String, JsonSimple> : A map of JSONSimple objects, or null */ @Override public Map<String, JsonSimple> getJsonSimpleMap(Object... path) { Map<String, JsonSimple> map = super.getJsonSimpleMap(path); if (map == null && systemConfig != null) { return systemConfig.getJsonSimpleMap(path); } return map; } /** * <p> * Search through the JSON for any nodes (at any depth) matching the * requested name and return them. The returned List will be of type Object * and require type interrogation for detailed use, but will be implemented * as a LinkedList to preserve order. * </p> * * @param node : The node name we are looking for * @return List<Object> : A list of matching Objects from the data */ @Override public List<Object> search(String node) { List<Object> results = super.search(node); if ((results == null || results.isEmpty()) && systemConfig != null) { return systemConfig.search(node); } return results; } /** * <p> * Returns a reference to the underlying system configuration object. This * is meant to be used on conjunction with storeSystemConfig() to make * changes to the config file on disk. * </p> * * <p> * Normal modifications to this objects JSON are not written to disk unless * they they are made via writableSystemConfig(). * </p> * * @return JsonObject : A reference to the system configuration JSON object */ public JsonObject writableSystemConfig() { return systemConfig.getJsonObject(); } /** * <p> * Store the underlying system configuration on disk in the appropriate * location. * </p> * * <p> * Normal modifications to this objects JSON are not written to disk unless * they they are made via writableSystemConfig(). * </p> * * @return JsonObject : A reference to the system configuration JSON object */ public void storeSystemConfig() throws IOException { FileWriter writer = new FileWriter(JsonSimpleConfig.getSystemFile()); writer.write(systemConfig.toString(true)); writer.close(); } /** * Loads all the config files found in INCLUDE_DIR_KEY property entry, that * have extensions in INCLUDE_DIR_KEY_EXT config array. The included * directory may contain subdirectories, and these are searched as well. * * The entries are merged, with the last file included overwriting all * previous values. These includes Map and List entries, with the exception * of List of Maps, which is appended by default and not examined further. * Therefore, the sort order is important. * * If the base property is of different type from the included property, the * included property will overwrite the base property. * * For details, please look at JsonSimpleConfigTest. * */ private void loadIncludeDir() { boolean hasIncludedDir = getJsonObject().containsKey(INCLUDE_DIR_KEY); boolean systemHasIncludedDir = systemConfig.getJsonObject().containsKey(INCLUDE_DIR_KEY); if (hasIncludedDir) { log.trace("Loading main included dir..."); loadIncludedDir(this); } else { log.trace("Main config has no included dir, trying system config..."); } if (systemHasIncludedDir) { log.trace("Loading system config included dir..."); loadIncludedDir(systemConfig); } else { log.trace("System config has no included dir, moving on..."); } } @SuppressWarnings(value = { "unchecked" }) private void loadIncludedDir(JsonSimple config) { List<String> extList = config.getStringList(INCLUDE_DIR_KEY_EXT); log.trace("Inclusion directory found:'" + INCLUDE_DIR_KEY + "', merging all files in '" + config.getString(null, INCLUDE_DIR_KEY) + "' ending with: {}", extList); List<File> configFiles = new ArrayList( FileUtils.listFiles(new File(config.getString(null, INCLUDE_DIR_KEY)), extList.toArray(new String[extList.size()]), true)); final Comparator<File> ALPHABETICAL_ORDER = new Comparator<File>() { public int compare(File file1, File file2) { int res = String.CASE_INSENSITIVE_ORDER.compare(file1.getAbsolutePath(), file2.getAbsolutePath()); if (res == 0) { res = file1.getAbsolutePath().compareTo(file2.getAbsolutePath()); } return res; } }; Collections.sort(configFiles, ALPHABETICAL_ORDER); for (File configFile : configFiles) { try { // log.debug("Merging included config file: {}", // configFile); JsonSimple jsonConfig = new JsonSimple(configFile); mergeConfig(config.getJsonObject(), jsonConfig.getJsonObject()); } catch (IOException e) { log.error("Failed to load file: {}", configFile); e.printStackTrace(); } } } @SuppressWarnings(value = { "unchecked" }) private void mergeConfig(Map targetMap, Map srcMap) { for (Object key : srcMap.keySet()) { Object src = srcMap.get(key); Object target = targetMap.get(key); if (target == null) { targetMap.put(key, src); } else { if (src instanceof Map && target instanceof Map) { mergeConfig((Map) target, (Map) src); } else if (src instanceof JSONArray && target instanceof JSONArray) { JSONArray srcArray = (JSONArray) src; JSONArray targetArray = (JSONArray) target; targetArray.addAll(srcArray); } else { targetMap.put(key, src); } } } } }