Java tutorial
/** * Copyright (C) 2013 jLDMud Developers. * This file is free software under the MIT License - see the file LICENSE for details. */ package org.ldmud.jldmud; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; /** * The Mud's configuration settings.<p/> * * The properties are primarily read from a configuration file, but the * class allows for the manual override from a {@code Properties} instance.<p/> * * In addition to storing the values, the class also attempts to generalize * the way the settings are defined, to simplify the definition itself, * and the generation of help texts.<p/> * * Due to the global nature of configuration settings, instances of this class * (ok, the one instance) are not passed to the modules needing the values. Instead, * either the individual values are passed by the Main module explicitly, or * the modules can define their own 'configuration settings' classes, which this global * class can then generate as needed. That way, the business modules aren't tempted to * look at other modules parameters. And all the memory used for this instance can be * released once initialization is complete. */ public class GameConfiguration { /** * Default configuration filename. */ public final static String DEFAULT_SETTINGS_FILE = "mud.properties"; /* * The settings themselves. */ private final SettingProperty mudDirectory = new SettingProperty("mud.dir", "The root directory of the mud lib, which is also the working directory of the driver process.", true); /* * This list tracks all settings as they are defined. */ List<SettingBase<?>> allSettings; /** * Base class describing a setting.<p/> * * The class allows options to create an effective value based * upon the given value - to this end the retrieval of the * final effective value is done through the method {@link #getEffectiveValue()}, * which just happens in its default implementation to return the * given values. * * @param <T> The target type of the setting value. */ abstract class SettingBase<T> { // The name of the setting, e.g. 'mud.directory' protected String name; // The description of the setting, e.g. "The mud directory" // The help printer will prepend '#' and possibly 'Optional: ', and // take care of the wrapping. Embedded line feeds will be preserved. protected String description; // Flag whether the setting is required protected boolean required; // Flag set to true if this setting was ever explicitly set. protected boolean wasSet = false; // The parsed value protected T value; // The default value (optional) protected T defaultValue; public SettingBase(String name, String description, boolean required) { super(); this.name = name; this.description = description; this.required = required; registerThis(); } /** * Register this Setting instance with the allSettings list in the GameConfiguration class instance. * The list is created if necessary. */ private void registerThis() { if (allSettings == null) { allSettings = new ArrayList<>(); } allSettings.add(this); } public SettingBase(String name, String description, T defaultValue) { super(); this.name = name; this.description = description; this.required = false; this.defaultValue = defaultValue; this.value = defaultValue; registerThis(); } /** * Create the self-description string suitable for a properties template file. * * @return The multi-line self description string, with a trailing line break. */ public String describe() { return wrap("# " + (required ? "" : "Optional: ") + description + System.lineSeparator() + name + "=" + (defaultValue != null ? defaultValue : "") + System.lineSeparator()); } /** * Print the given and effective setting of the setting, inclusive description. * * @return The multi-line string describing the setting setting, with a trailing line break. */ public String effective() { StringBuilder sb = new StringBuilder("# "); if (!required) { sb.append("Optional: "); } sb.append(description).append(System.lineSeparator()); if (!wasSet) { if (value == null) { sb.append("# Using default value (unset).").append(System.lineSeparator()); } else { sb.append("# Using default value: ").append(value).append(System.lineSeparator()); } } else if (value == null && getEffectiveValue() != null) { sb.append("# Configured value unset.").append(System.lineSeparator()); } else if (value != null && !value.equals(getEffectiveValue())) { sb.append("# Configured value: ").append(value).append(System.lineSeparator()); } if (getEffectiveValue() == null) { sb.append("# ").append(name).append("="); } else { sb.append(name).append("=").append(getEffectiveValue()); } sb.append(System.lineSeparator()); return wrap(sb.toString()); } /** * Parse the given string for the desired value, and set the {@link #value} member from it. * The actual work of the parsing is done by {@link #parseValueImpl(String)}, this method * just does some housekeeping. * * @param v The setting value string to parse * @return An error message, or {@code null} if the value could be set. */ public String parseValue(String v) { String rc = parseValueImpl(v); if (rc == null) { wasSet = true; } return rc; } /** * Parse the given string for the desired value, and set the {@link #value} member from it. * This method is called as part of the {@link #parseValue(String)} processing. * * @param v The property string to parse * @return An error message, or {@code null} if the value could be set. */ protected abstract String parseValueImpl(String v); /** * @return the effective value of this property. */ public T getEffectiveValue() { return value; } } /** * A setting holding an directory, which must exist. * The effective value will be the absolute canonical file path. */ class SettingProperty extends SettingBase<File> { File effectiveValue; public SettingProperty(String name, String description, File defaultValue) { super(name, description, defaultValue); } public SettingProperty(String name, String description, boolean required) { super(name, description, required); } @Override public String parseValueImpl(String v) { if (!StringUtils.isEmpty(v)) { File f = new File(v); if (!f.isDirectory()) { return "'" + v + "' doesn't exist, or is not a directory."; } value = f; try { effectiveValue = f.getAbsoluteFile().getCanonicalFile(); } catch (IOException e) { return "'" + v + "' can't be resolved to a canonical path."; } } return null; } @Override public File getEffectiveValue() { return effectiveValue; } } /** * Load the settings from the given input source and/or the override properties, and validate them. * If the validation fails, an error message is printed to stderr. * * @param propertyFileName The name of the settings file in properties format. * @param overrideProperties A manually created set of properties, overriding those in the settings file. If this set * is not empty, the {@code propertyFileName} need not exist. * @return {@code true} if the file was successfully loaded. */ public boolean loadProperties(String propertyFileName, Properties overrideProperties) { Properties properties = new Properties(); try (InputStream in = new FileInputStream(propertyFileName)) { properties.load(in); } catch (IOException ioe) { final String message = (ioe instanceof FileNotFoundException) ? "File not found" : ioe.toString(); if (overrideProperties.isEmpty()) { System.err.println("Error: Problem loading ".concat(propertyFileName).concat(": ").concat(message)); return false; } System.err.println("Warning: Problem loading ".concat(propertyFileName).concat(": ").concat(message)); } List<String> errors = loadProperties(properties, overrideProperties, allSettings); if (!errors.isEmpty()) { System.err.println( "Error: Property validation problems loading the configuration '" + propertyFileName + "':"); for (String entry : errors) { System.err.println(" " + entry); } } return errors.isEmpty(); } /** * Load the given properties and validate them. * * @param properties The properties file read from the input source. * @param overrideProperties A manually created set of properties, overriding those in the properties file. * @param propertyList The list of property instances to load the values into. * @return A list of errors, if any property value failed to validate. */ List<String> loadProperties(Properties properties, Properties overrideProperties, List<SettingBase<?>> propertyList) { List<String> errors = new ArrayList<>(); for (SettingBase<?> entry : propertyList) { String value = overrideProperties.containsKey(entry.name) ? overrideProperties.getProperty(entry.name) : properties.getProperty(entry.name); String error = null; if (value != null) { error = entry.parseValue(StringUtils.strip(value)); } if (StringUtils.isEmpty(error) && entry.value == null && entry.required) { error = "Setting is required."; } if (!StringUtils.isEmpty(error)) { errors.add("Setting '" + entry.name + "': " + error); } } return errors; } /** * Wrap the given text to a line length of 70, while preserving hard line * breaks. Wrapped lines will be prepended with '# '. * * @param lines The text to wrap, each String representing a line. * @return The wrapped text, joined into one string, with a trailing line break. */ static String wrap(String[] lines) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < lines.length; ++i) { sb.append(WordUtils.wrap(lines[i], 70, System.lineSeparator() + "# ", false)) .append(System.lineSeparator()); } return sb.toString(); } /** * Wrap the given text to a line length of 70, while preserving hard line * breaks. Wrapped lines will be prepended with '# '. * * @param in The string to wrap. * @return The wrapped string, with a trailing line break. */ static String wrap(String in) { return wrap(StringUtils.split(in, System.lineSeparator())); } /** * Using the registered options, print an an example template. */ public static void printTemplate() { System.out.print(wrap(new String[] { "# This is a template Mud settings file.", "#", "# Settings with defaults will have their default value printed as example value.", "#", "# All settings can also be specified as arguments on the commandline; in that case, any commandline value overrides a corresponding properties file value." })); System.out.println(); for (SettingBase<?> entry : new GameConfiguration().allSettings) { System.out.print(entry.describe()); System.out.println(); } } /** * Using the registered options, print the currently effective settings. */ public void printEffectiveSettings() { System.out.print(wrap(new String[] { "# -- Effective Mud configuration settings --", "# If the effective setting is different from the originally provided configuration value, that original value will be printed as comment." })); System.out.println(); for (SettingBase<?> entry : allSettings) { System.out.print(entry.effective()); System.out.println(); } System.out.println("# -- END of effective Mud configuration properties --"); } /* --------------------- Configuration Property Accessors ------------------------------ */ /** * The configured mud directory may have been specified relative to the initial working * directory, so its absolute path may no longer be correct once the startup is complete. Instead, create * paths relative to the value of {@link GameConfiguration#getMudRoot() getMudRoot()}. * TODO: Needed? * * @return The configured mud directory (may be relative to the initial working directory). */ public File getMudDirectory() { return mudDirectory.value; } /** * @return The effective absolute root directory of the mud. */ public File getMudRoot() { return mudDirectory.getEffectiveValue(); } }