package pt.ist.fenixframework; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import java.util.Properties; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ist.fenixframework.backend.BackEnd; import pt.ist.fenixframework.core.DmlFile; import pt.ist.fenixframework.core.Project; import pt.ist.fenixframework.core.exception.ProjectException; import pt.ist.fenixframework.util.Converter; /** * <p> An instance of the <code>Config</code> class bundles together the initialization parameters * used by the Fenix Framework. * * Therefore, before initializing the framework (via the call to the * {@link FenixFramework#initialize(Config)} method), the programmer should create an instance of * <code>Config</code> with the correct values for each of the parameters.</p> * * <p> No constructor is provided for this class (other than the default constructor), because the * <code>Config</code> class has several parameters, some of which are optional. But, whereas * optional parameters do not need to be specified, the parameters that are required must be * specified by the programmer before calling the {@link FenixFramework#initialize(Config)} method. * * <p> Additional configuration parameters may be added by subclassing this class. Subclasses of * config can override the {@link #init()} method. Typically, their own {@link #init()} should also * call {@link super#init()} if an hierarchy of configs is used. * * <p> To create an instance of this class with the proper values for its parameters, programmers * should generally use code like this (assuming one specific backend as an example): * * <blockquote> * * <pre> * * Config config = new MemConfig() { // any subclass of Config should be ok * { * this.domainModelURLs = resourceToURLArray("path/to/domain.dml"); * this.appName = "MyAppName"; * } * }; * * </pre> * * </blockquote> * * Note that the <code>Config</code> takes an array of URLs for the <code>domainModelURLs</code>. * Utility methods are provided in this class to convert from other typical representations to their * corresponding URL. All of the utility method return the required <code>URL[]</code>. Here are * some guidelines: * * <ul> * * <li>{@link #resourceToURLArray(String)}: looks up the given resource using * <code>Thread.currentThread().getContextClassLoader().getResource(String)</code>;</li> * * <li>{@link #filenameToURLArray(String)}: looks up the given file on the local filesystem; * </li> * * <li>Methods using the plural form (e.g. {@link #resourcesToURLArray(String [])}) are equivalent, * except that they take multiple DML file locations.</li> * * </ul> * * Note the use of the double * * <pre> * { {} } * </pre> * * to delimit an instance initializer block for the anonymous inner class being * created. * * Each of the parameters of the <code>Config</code> class is represented as a * protected class field. Look at the documentation of each field to see what is * it for, whether it is optional or required, and in the former case, what is * its default value. * * @see pt.ist.fenixframework.FenixFramework * @see pt.ist.fenixframework.backend.mem.MemConfig * */ public abstract class Config { private static final Logger logger = LoggerFactory.getLogger(Config.class); protected static final String NO_DOMAIN_MODEL = "Fenix Framework cannot proceed without a domain model!. Either provide \"appName\" (for which there is a project.properties file) or explicitly set \"domainModelURLs\"."; protected static final String PROPERTY_CONFIG_CLASS = "config.class"; // the suffix of the method that sets a property from a String property protected static final String SETTER_FROM_STRING = "FromString"; /** * This <strong>required</strong> parameter specifies the <code>URL[]</code> to each file * containing the DML code that corresponds to the domain model of the application. A non-empty * array must be specified for this parameter. */ protected URL[] domainModelURLs = null; /** * This <strong>optional</strong> parameter specifies a name for the application. When using * configuration by convention, this name (if set) is used to lookup the * <code><appName>/project.properties</code> file. Additionally, this name will be used * by the framework in the statistical logs performed during the application execution. The * default value for this parameter is <code>null</code>. */ protected String appName = null; /** * This <strong>optional</strong> parameter specifies the number of nodes that are expected to * be deployed. This can be used by the backends to perform some setup, e.g. to wait for some * number of nodes to become live in order to complete the initialization process. The default * value for this parameter is <code>1</code>. */ protected int expectedInitialNodes = 1; /** * This <strong>optional</strong> parameter specifies the JGroups configuration file. This * configuration will used to create channels between Fenix Framework nodes. The default value * for this parameter is <code>fenix-framework-udp-jgroups.xml</code>, which is the default * configuration file that ships with the framework. */ protected String jGroupsConfigFile = "fenix-framework-udp-jgroups.xml"; protected void checkRequired(Object obj, String fieldName) { if (obj == null) { missingRequired(fieldName); } } /** * Check if the value of <code>domainModelURLs</code> is already set. */ protected void checkForDomainModelURLs() { if ((domainModelURLs == null) || (domainModelURLs.length == 0)) { logger.error(NO_DOMAIN_MODEL); missingRequired("domainModelURLs"); } } /** * Subclasses of this class can overwrite this method, but they should specifically call * <code>super.checkConfig()</code> to check the superclass's configuration. */ protected void checkConfig() { checkForDomainModelURLs(); } protected static void missingRequired(String fieldName) { throw new ConfigError(ConfigError.MISSING_REQUIRED_FIELD, "'" + fieldName + "'"); } /** * This method is invoked by the {@link FenixFramework#initialize(Config)}. */ protected final void initialize() { checkConfig(); init(); } // set each property via reflection, ignoring the config.class property, which was used to // define which config instance to create protected final void populate(Properties props) { for (String propName : props.stringPropertyNames()) { if (PROPERTY_CONFIG_CLASS.equals(propName)) { continue; } String value = props.getProperty(propName); setProperty(propName, value); } } protected final void setProperty(String propName, String value) { // first check if it really exists Field field = null; try { field = getField(this.getClass(), propName); } catch (ConfigError e) { // we choose to ignore unknown config properties, but we do give a loud warning logger.warn(e.getMessage()); logger.debug(e.getMessage(), e); return; } // note that lazy evaluation is used on purpose! boolean success = attemptSetPropertyUsingMethod(getSetterFor(this.getClass(), propName + SETTER_FROM_STRING), value) || attemptSetPropertyUsingField(field, value); if (! success) { throw new ConfigError(ConfigError.COULD_NOT_SET_PROPERTY, propName); } } private Field getField(Class<? extends Config> clazz, String fieldName) { try { return clazz.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { // climb the hierarchy, but only up to Config class Class<?> superclass = clazz.getSuperclass(); if (Config.class.isAssignableFrom(superclass)) { return getField((Class<? extends Config>)superclass, fieldName); } else { throw new ConfigError(ConfigError.UNKNOWN_PROPERTY + fieldName, e); } } } private boolean attemptSetPropertyUsingMethod(Method setter, String value) { if (setter == null) return false; setter.setAccessible(true); try { setter.invoke(this, value); } catch (Exception e) { throw new ConfigError(e); } return true; } private boolean attemptSetPropertyUsingField(Field field, String value) { field.setAccessible(true); try { field.set(this, value); } catch (Exception e) { return false; } return true; } private Method getSetterFor(Class<? extends Config> clazz, String setterName) { try { return clazz.getDeclaredMethod(setterName, new Class[]{String.class}); } catch (NoSuchMethodException e) { // climb the hierarchy, but only up to Config class Class<?> superclass = clazz.getSuperclass(); if (Config.class.isAssignableFrom(superclass)) { return getSetterFor((Class<? extends Config>)superclass, setterName); } else { return null; } } } private void checkForMultipleDomainModelUrlsDefinition() { if (domainModelURLs != null) { // means that it was already set logger.error(ConfigError.DUPLICATE_DEFINITION_OF_DOMAIN_MODEL_URLS); throw new ConfigError(ConfigError.DUPLICATE_DEFINITION_OF_DOMAIN_MODEL_URLS); } } /** * Note: Either appNameFromString or domainModelURLsFromString should be used, but not both! */ protected void appNameFromString(String value) { this.appName = value; try { List<DmlFile> dmlFiles = Project.fromName(value).getFullDmlSortedList(); if (dmlFiles.size() > 0) { checkForMultipleDomainModelUrlsDefinition(); } domainModelURLs = new URL[dmlFiles.size()]; int counter = 0; for (DmlFile dmlFile : dmlFiles) { domainModelURLs[counter++] = dmlFile.getUrl(); } } catch (IOException e) { logger.warn("failed when setting appNameFromString", e); } catch (ProjectException e) { logger.warn("failed when setting appNameFromString", e); } } /** * Note: Either appNameFromString or domainModelURLsFromString should be used, but not both! */ protected void domainModelURLsFromString(String value) { checkForMultipleDomainModelUrlsDefinition(); String[] tokens = value.split("\\s*,\\s*"); URL[] urls = new URL[tokens.length]; for (int i = 0; i < tokens.length; i++) { // try URL from resource urls[i] = Converter.resourceToURL(tokens[i]); if (urls[i] != null) { continue; } // try URL from filename urls[i] = Converter.filenameToURL(tokens[i]); if (urls[i] != null) { continue; } // failed to get the URL throw new Error("FenixFramework config error: cannot find DML '" + tokens[i] + "'"); } domainModelURLs = urls; } protected void expectedInitialNodesFromString(String value) { try { expectedInitialNodes = Integer.parseInt(value.trim()); } catch (NumberFormatException e) { throw new ConfigError(e); } } protected abstract void init(); /** * Utility method to wait for the number of expected initial nodes to be up. */ public void waitForExpectedInitialNodes(String barrierName) throws Exception { logger.debug("expectedInitialNodes=" + expectedInitialNodes); if (expectedInitialNodes > 1) { logger.debug("Waiting until " + expectedInitialNodes + " nodes are up"); FenixFramework.barrier(barrierName, expectedInitialNodes); logger.debug("All nodes are up."); } } /** * Get the current {@link BackEnd} in use. */ public abstract BackEnd getBackEnd(); public URL[] getDomainModelURLs() { return domainModelURLs; } public int getExpectedInitialNodes() { return expectedInitialNodes; } public String getJGroupsConfigFile() { return jGroupsConfigFile; } public String getAppName() { return appName; } /** * Subclasses of this class can overwrite this method, but they should specifically call * <code>super.shutdown()</code> to orderly shutdown the framework. */ protected void shutdown() { getBackEnd().shutdown(); } /* UTILITY METHODS TO CONVERT DIFFERENT FORMATS TO URL - BEGIN */ /* code linked from pt.ist.fenixframework.util.Converter to make configuration API more straightforward */ // REGARDING RESOURCES public static URL[] resourceToURLArray(String resource) { return Converter.resourceToURLArray(resource); } public static URL[] resourcesToURLArray(String ... resources) { return Converter.resourcesToURLArray(resources); } // REGARDING FILENAMES public static URL[] filenameToURLArray(String filename) { return Converter.filenameToURLArray(filename); } public static URL[] filenamesToURLArray(String ... filenames) { return Converter.filenamesToURLArray(filenames); } /* UTILITY METHODS TO CONVERT DIFFERENT FORMATS TO URL - END */ }