/* * Copyright 2008-2014 the original author or authors * * 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 org.kaleidofoundry.core.context; import static org.kaleidofoundry.core.i18n.InternalBundleHelper.ContextMessageBundle; import java.io.Serializable; import java.lang.reflect.Field; import java.util.LinkedHashSet; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.kaleidofoundry.core.config.Configuration; import org.kaleidofoundry.core.config.ConfigurationChangeEvent; import org.kaleidofoundry.core.config.ConfigurationChangeHandler; import org.kaleidofoundry.core.config.ConfigurationFactory; import org.kaleidofoundry.core.config.ConfigurationRegistry; import org.kaleidofoundry.core.i18n.InternalBundleHelper; import org.kaleidofoundry.core.lang.annotation.Immutable; import org.kaleidofoundry.core.lang.annotation.NotNull; import org.kaleidofoundry.core.lang.annotation.Nullable; import org.kaleidofoundry.core.lang.annotation.Task; import org.kaleidofoundry.core.lang.annotation.TaskLabel; import org.kaleidofoundry.core.lang.annotation.Tasks; import org.kaleidofoundry.core.lang.annotation.ThreadSafe; import org.kaleidofoundry.core.plugin.PluginHelper; import org.kaleidofoundry.core.plugin.model.Plugin; import org.kaleidofoundry.core.util.AbstractPropertyAccessor; import org.kaleidofoundry.core.util.StringHelper; /** * A runtime context represents a subset a {@link Configuration} runtime / environment properties, specific to the * generic class argument T. It can be used by an instance of class T, which need to access and handle configuration properties changes<br/> * <ul> * <li>it encapsulates access to runtime configuration or environment informations, * <li>it is bind to one or more {@link Configuration}, so configurations changes can be managed at runtime, * <li>it can be a subset of this {@link Configuration} (the prefix is done for that), * <li>it has read only access to the {@link Configuration} * </ul> * <p> * <b>To categorize your properties :</b> you can used a prefix : {@link #getPrefix()} <br/> * <br/> * This prefix can be manually set, using constructors : * <ul> * <li> {@link RuntimeContext#RuntimeContext(String, Configuration...)} * <li> {@link RuntimeContext#RuntimeContext(String, String, Configuration...)}</li> * <li> {@link RuntimeContext#RuntimeContext(String, String, ConcurrentMap, Configuration...)}</li> * </ul> * <br/> * Another way to set this prefix is to bind it to a type, using constructors : * <ul> * <li> {@link RuntimeContext#RuntimeContext(Class)}</li> * <li> {@link RuntimeContext#RuntimeContext(Class, Configuration...)}</li> * <li> {@link RuntimeContext#RuntimeContext(String, Class, Configuration...)}</li> * <li> {@link RuntimeContext#RuntimeContext(String, Class, ConcurrentMap, Configuration...)}</li> * </ul> * The last way to build it using {@link Context} annotation, with static constructors : * <ul> * <li>{@link #createFromField(Field)}</li> * <li>{@link #createFrom(Context, String, Class)}</li> * </ul> * </p> * <p> * <b>This class is thread safe</b>: it use internally {@link Configuration}, to access context informations. * </p> * <p> * Given a file <code>jndi-local.properties</code> contains following : * * <pre> * * # Tomcat Intial Context Information [new RuntimeContext("tomcat", configuration, "jndi.context")] * jndi.context.tomcat.java.naming.env.prefix=java:comp/env * jndi.context.tomcat.java.naming.provider.url= * jndi.context.tomcat.java.naming.factory.initial=org.apache.naming.java.javaURLContextFactory * jndi.context.tomcat.java.naming.factory.url.pkgs= * jndi.context.tomcat.java.naming.dns.url= * * # Jboss Intial Context Information [new RuntimeContext("jboss", configuration, "jndi.context")] * jndi.context.jboss.java.naming.env.prefix= * jndi.context.jboss.java.naming.provider.url=localhost:1099 * jndi.context.jboss.java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory * jndi.context.jboss.java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces * jndi.context.jboss.java.naming.dns.url= * * # so : * # runtimeContext prefix is: 'jndi.context' * # runtimeContext name are: 'tomcat' or 'jboss' * * </pre> * * </p> * Sample java code : * <p> * * <pre> * try { * // create & load configuration & a extract runtime context * Configuration jndiProperties = ConfigurationFactory.provideConfiguration("myJndiConf", "classpath:/jndi-local.properties"); * RuntimeContext tomcatContext = new RuntimeContext("tomcat", "jndi.context", jndiProperties); * * // print properties value * System.out.println(tomcatContext.getProperty("java.naming.env.prefix")); * System.out.println(tomcatContext.getProperty("java.naming.factory.initial")); * * // extract current properties to another file * Properties tomcatJndiProperties = tomcatContext.toProperties(); * tomcatJndiProperties.store(new FileOutputStream("jndi-local-tomcat.properties"), "extraction of tomcat jndi properties"); * } catch (final ResourceException stoe) { * ... * } catch (final IOException ioe) { * ... * } * </pre> * * </p> * Will result in a <code>jndi-local-tomcat.properties</code> * <p> * * <pre> * # extraction of tomcat jndi properties * jndi.context.tomcat.java.naming.env.prefix=java:comp/env * jndi.context.tomcat.java.naming.provider.url= * jndi.context.tomcat.java.naming.factory.initial=org.apache.naming.java.javaURLContextFactory * jndi.context.tomcat.java.naming.factory.url.pkgs= * jndi.context.tomcat.java.naming.dns.url= * </pre> * * </p> * <br/> * <p> * <b>I would preferred introspection to get generic type T, without giving type in constructor argument...</b> <br/> * But generic type are erased one compiled (java 4 compatibility)... <br/> * <br/> * Very interesting topics on the subject : * <ul> * <li>http://download.oracle.com/docs/cd/E17409_01/javase/tutorial/java/generics/erasure.html * <li>http://gafter.blogspot.com/2006/12/super-type-tokens.html * </ul> * So, I keep simple solution of passing the Class of the generic in constructor. The abstract solution does not satisfy me. * </p> * * @param <T> service interface / implementation class to use with this context * @author jraduget * @see Context inject {@link RuntimeContext} to a class field, method argument... */ @Immutable(comment = "instance which have been injected using @Context are immutable after injection") @ThreadSafe @Tasks(tasks = { @Task(comment = "comment and review interaction between configuration and runtime context... use case, creation, event handling... have to be thread safe...", labels = TaskLabel.Review), @Task(comment = "implements Serializeable, check Configuration[] fields transiant.", labels = TaskLabel.Enhancement) }) public class RuntimeContext<T> extends AbstractPropertyAccessor { /** Context property name, used to enable the component listening of configuration parameters changes */ public static final String DynamicsParameter = "dynamics"; /** Context property name, used to define the {@link Scope} of the configured context. If not defined, the default is {@link Scope#singleton} */ public static final String ScopeParameter = "scope"; /* * Even if the field are not final, the class stay immutable. * a class instance which have been injected with @Context, can't be modifiable * -> see #copyFrom(...) methods */ private String name; private String prefix; private final Class<T> type; private Configuration[] configurations; // used only by {@link AbstractRuntimeContextBuilder}, for static injection boolean hasBeenInjectedByAnnotationProcessing; // does context has been build by runtime context builder ? if yes, no more updates is possible) boolean hasBeenBuildByContextBuilder; // static parameters (@Contex( parameters = {...}) // there are injected manually by the developer coding using annotation, or using builder final ConcurrentMap<String, Serializable> parameters; // an optional configuration change handler ConfigurationChangeHandler configurationChangesHandler; /** * create <b>unnamed</b> {@link RuntimeContext} name, <b>without prefix</b> * * @param configurations configuration instances where to find properties, configurations have to be load before */ public RuntimeContext(final Configuration... configurations) { this(null, (String) null, configurations); } /** * create <b>unnamed</b> {@link RuntimeContext} name, with static parameters, <b>without prefix</b> * * @param parameters Optional static parameters * @param configurations */ public RuntimeContext(final ConcurrentMap<String, Serializable> parameters, final Configuration... configurations) { this(null, (String) null, parameters, configurations); } /** * create <b>named</b> {@link RuntimeContext} name, <b>without prefix</b> * * @param name context name * @param configurations configuration instances where to find properties, configurations have to be load before */ public RuntimeContext(final String name, final Configuration... configurations) { this(name, (String) null, configurations); } /** * create unnamed {@link RuntimeContext}, with the given type * * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package */ public RuntimeContext(final Class<T> type) { this(null, type, null, new Configuration[0]); } /** * create unnamed {@link RuntimeContext}, with the given plugin prefix & static parameters * * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param parameters */ public RuntimeContext(final Class<T> type, final ConcurrentMap<String, Serializable> parameters) { this(null, type, parameters, new Configuration[0]); } /** * create unnamed {@link RuntimeContext}, with the given plugin prefix & configurations * * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param configurations configuration instances where to find properties, configurations have to be load before */ public RuntimeContext(final Class<T> type, final Configuration... configurations) { this(null, type, configurations); } /** * create unnamed {@link RuntimeContext}, with the given plugin prefix & static parameters & configurations * * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param parameters optional static parameters * @param configurations */ public RuntimeContext(final Class<T> type, final ConcurrentMap<String, Serializable> parameters, final Configuration... configurations) { this(null, type, parameters, configurations); } /** * create named {@link RuntimeContext}, with the given plugin prefix & configurations * * @param name context name * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param configurations configuration instances where to find properties, configurations have to be load before */ public RuntimeContext(final String name, final Class<T> type, final Configuration... configurations) { this(name, type, false, null, configurations); } /** * create named {@link RuntimeContext}, with the given prefix & configurations * * @param name context name * @param prefix an optional prefix for the properties name (it can be used to categorized your own RuntimeContext) * @param configurations configuration instances where to find properties, configurations have to be load before */ public RuntimeContext(final String name, final String prefix, final Configuration... configurations) { this(name, prefix, false, null, configurations); } /** * create named {@link RuntimeContext}, with the given type prefix & static parameters & configurations * * @param name context name * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param parameters optional static parameters * @param configurations */ public RuntimeContext(final String name, final Class<T> type, final ConcurrentMap<String, Serializable> parameters, final Configuration... configurations) { this(name, type, false, parameters, configurations); } /** * create named {@link RuntimeContext}, with the given type prefix & static parameters & configurations * * @param name context name * @param prefix an optional prefix for the properties name (it can be used to categorized your own RuntimeContext) * @param parameters optional static parameters * @param configurations */ public RuntimeContext(final String name, final String prefix, final ConcurrentMap<String, Serializable> parameters, final Configuration... configurations) { this(name, prefix, false, parameters, configurations); } /** * create named {@link RuntimeContext}, with the given type prefix and using given runtime context configurations * * @param name context name * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param context */ public RuntimeContext(final String name, final Class<T> type, @NotNull final RuntimeContext<?> context) { this(name, type, false, null, context.getConfigurations()); } /** * create named {@link RuntimeContext}, with the given type prefix and using given runtime context configurations * * @param name context name * @param type if type is annotated {@link Plugin}, the current prefix will be the plugin code, otherwise the type package * @param parameters optional static parameters * @param context */ public RuntimeContext(final String name, final Class<T> type, final ConcurrentMap<String, Serializable> parameters, @NotNull final RuntimeContext<?> context) { this(name, type, false, parameters, context.getConfigurations()); } /** * @param name context name * @param prefix prefix an optional prefix for the properties name (it can be used to categorized your own * RuntimeContext) * @param hasBeenInjectedByAnnotationProcessing does context injection have been done (immutable after injection) * @param parameters optional static parameters * @param configurations configuration instances where to find properties, configurations have to be load before */ RuntimeContext(final String name, final String prefix, final boolean hasBeenInjectedByAnnotationProcessing, final ConcurrentMap<String, Serializable> parameters, final Configuration... configurations) { this.name = name; this.prefix = prefix; this.type = null; this.configurations = configurations; // keep original instance, to handle property modification. don't re-copy it this.hasBeenInjectedByAnnotationProcessing = hasBeenInjectedByAnnotationProcessing; this.parameters = parameters == null ? new ConcurrentHashMap<String, Serializable>() : parameters; } /** * @param name context name * @param type * @param hasBeenInjectedByAnnotationProcessing does context injection have been done (immutable after injection) * @param parameters optional static parameters * @param configurations configuration instances where to find properties, configurations have to be load before */ RuntimeContext(final String name, final Class<T> type, final boolean hasBeenInjectedByAnnotationProcessing, final ConcurrentMap<String, Serializable> parameters, final Configuration... configurations) { final String pluginPrefix = PluginHelper.getPluginName(type); this.name = name; this.prefix = pluginPrefix != null ? pluginPrefix : null; this.type = type; this.configurations = configurations; // keep original instance, to handle property modification. don't re-copy it this.hasBeenInjectedByAnnotationProcessing = hasBeenInjectedByAnnotationProcessing; this.parameters = parameters == null ? new ConcurrentHashMap<String, Serializable>() : parameters; } /** * If context annotation contain a name, the created {@link RuntimeContext} will use it, otherwise it will use the defaultName argument * coming from field or argument... * * @param <T> * @param context context annotation * @param defaultName default context name if {@link Context} annotation provide no one (field name, argument name, ...) * @param type * @return new runtime context instance, build from given annotation * @throws IllegalContextParameterException if one of {@link Context#configurations()} is not registered */ public static <T> RuntimeContext<T> createFrom(@NotNull final Context context, @Nullable final String defaultName, final Class<T> type) { // name of the context final String contextName = !StringHelper.isEmpty(context.value()) ? context.value() : defaultName; if (StringHelper.isEmpty(contextName)) { throw new IllegalContextParameterException("context.annotation.value.empty"); } final RuntimeContext<T> rc; // handle configurations to used in the runtime context final String[] configIds = context.configurations(); final Configuration[] configs = new Configuration[configIds != null ? configIds.length : 0]; for (int i = 0; i < configs.length; i++) { configs[i] = ConfigurationFactory.getRegistry().get(configIds[i]); if (configs[i] == null) { throw new IllegalContextParameterException("context.annotation.illegalconfig.simple", context.value(), configIds[i]); } } // copy static annotation parameters final ConcurrentMap<String, Serializable> parameters = new ConcurrentHashMap<String, Serializable>(); // fixed the dynamics status of the context parameters.put(DynamicsParameter, String.valueOf(context.dynamics())); for (final Parameter p : context.parameters()) { parameters.put(p.name(), p.value()); } // create runtimeContext instance if (type != null) { rc = new RuntimeContext<T>(contextName, type, true, parameters, configs); } else { rc = new RuntimeContext<T>(contextName, (String) null, true, parameters, configs); } return rc; } /** * @param annotatedField * @return new runtime context instance, build from given field * @throws IllegalArgumentException is the given field is not annotated {@link Context} * @throws ContextException if one of {@link Context#configurations()} is not registered */ public static RuntimeContext<?> createFromField(@NotNull final Field annotatedField) { final Context context = annotatedField.getAnnotation(Context.class); if (context == null) { throw new IllegalArgumentException(ContextMessageBundle.getMessage("context.annotation.missing", annotatedField.getName())); } final Plugin<?> plugin = PluginHelper.getInterfacePlugin(annotatedField.getDeclaringClass()); return createFrom(context, annotatedField.getName(), plugin != null ? plugin.getAnnotatedClass() : null); } /** * feed given context, with annotation meta-data * * @param <T> * @param context * @param defaultName default context name if {@link Context} annotation provide no one (field name, argument name, ...) * @param contextTarget */ public static <T> void createFrom(@NotNull final Context context, @Nullable final String defaultName, @NotNull final RuntimeContext<T> contextTarget) { final RuntimeContext<T> newOne = createFrom(context, defaultName, contextTarget.getType()); copyFrom(newOne, contextTarget); contextTarget.hasBeenInjectedByAnnotationProcessing = true; } /** * Internal helper for re-copy a runtime context, * which have not been yet processing by annotation processor injector. * It is used for final field, which have been manually instantiate * * @param origin * @param target * @throws IllegalStateException is current context have already been injected */ public static void copyFrom(final RuntimeContext<?> origin, final RuntimeContext<?> target) { if (!target.hasBeenInjectedByAnnotationProcessing) { if (StringHelper.isEmpty(target.name)) { target.name = origin.name; } target.prefix = origin.prefix; target.configurations = origin.configurations; target.hasBeenInjectedByAnnotationProcessing = false; target.hasBeenBuildByContextBuilder = false; for (final String key : origin.parameters.keySet()) { // if parameter is already set, we keep original value if (target.parameters.get(key) == null) { target.parameters.put(key, origin.parameters.get(key)); } } } else { // RuntimeContext have already been injected throw new IllegalStateException(InternalBundleHelper.ContextMessageBundle.getMessage("context.annotation.illegalinject", target.name)); } } /** * @return context name identifier */ public String getName() { return name; } /** * @return an optional prefix for the properties name (it can be used to categorized your own RuntimeContext) <br/> * <br/> * For example in kaleidofoundry, it could be : * <ul> * <li>store.file * <li>jndi.context * <li>messaging.consumer * <li>messaging.transport * <li>mail.session * <li>... * </ul> */ public String getPrefix() { return prefix; } /** * @return if {@link RuntimeContext} has been create giving an type, return it, otherwise return null */ public Class<T> getType() { return type; } /** * Does context allow dynamics properties changes ? * * @return <code>true / false</code>, if not set return <code>true</code> */ public boolean isDynamics() { return getBoolean(DynamicsParameter, true); } /** * Scope defined in the context configuration * * @return if not defined, {@link Scope#singleton} will be used */ public Scope getScope() { return Scope.valueOf(getString(ScopeParameter, Scope.singleton.name())); } /** * @param property local property name (without prefix and name) * @return value of the given property <br/> * For a property which is defined both in static parameters, and in configurations: the first parameter occurrence will be * return <br/> * For a property which is defined both in multiple configurations: the first occurrence will be return (configuration array * declaration order) <br/> */ @Override public Serializable getProperty(final String property) { // first, search in local static parameters Serializable result = parameters.get(property); if (result != null) { return result; } // if not found, search in configurations for (final Configuration config : getConfigurations()) { result = config.getProperty(getFullPropertyName(property)); if (result != null) { return result; } } return null; } /** * @return does the current instance have been injected via @{@link Context} aop processing */ public boolean hasBeenInjectedByAnnotationProcessing() { return hasBeenInjectedByAnnotationProcessing; } /** * @return does the current instance have been build via an @{@link AbstractRuntimeContextBuilder} */ public boolean hasBeenBuildByContextBuilder() { return hasBeenBuildByContextBuilder; } /** * @param separator print separator * @return string representation of the runtime context */ public String toString(final String separator) { final StringBuilder str = new StringBuilder(); str.append("{context name").append("=").append(name).append(" "); str.append("prefix").append("=").append(prefix).append("}").append(separator); final Set<String> keys = keySet(); if (keys != null) { for (final String key : keys) { str.append(key).append("=").append(getProperty(key)).append(separator); } } return str.toString(); } /** * @return string representation with ; as separator */ @Override public String toString() { return toString(" , "); } /** * @return a clone copy of the properties set names (property name without prefix) */ public Set<String> keySet() { final String prefix = getFullPrefix().toString(); final Set<String> keys = new LinkedHashSet<String>(); final Set<String> result = new LinkedHashSet<String>(); if (parameters != null && !parameters.isEmpty()) { keys.addAll(parameters.keySet()); } for (final Configuration config : getConfigurations()) { keys.addAll(config.toProperties().stringPropertyNames()); } for (final String prop : keys) { if (prop.startsWith(prefix)) { result.add(prop.substring(prefix.length() <= 0 ? prefix.length() : prefix.length() + 1)); } } return result; } /** * @return clone properties of the context<br/> * For a property which is defined both in static parameters, and in configurations: the first parameter occurrence will be * return <br/> * For a property which is defined both in multiple configurations: the first occurrence will be return (configuration array * declaration order) <br/> */ public Properties toProperties() { final Properties props = new Properties(); for (final String prop : keySet()) { if (props.get(prop) == null) { props.setProperty(prop, getProperty(prop).toString()); } } return props; } // *************************************************************************** // -> Configuration changes events methods // *************************************************************************** /** * set here your configuration changes handler * * @param handler */ public void setConfigurationChangeHandler(final ConfigurationChangeHandler handler) { this.configurationChangesHandler = handler; } /** * if current context is dynamics ({@link #isDynamics()}), and if configuration change handler is defined with * {@link #setConfigurationChangeHandler(ConfigurationChangeHandler)}, * it will triggers the configuration changes given in argument, to the current handler<br/> * <br/> * * @param events */ final void triggerConfigurationChangeEvents(final LinkedHashSet<ConfigurationChangeEvent> events) { if (configurationChangesHandler != null && isDynamics()) { configurationChangesHandler.onConfigurationChanges(events); } } // *************************************************************************** // * PROTECTED // *************************************************************************** @NotNull protected Configuration[] getConfigurations() { if (configurations == null || configurations.length <= 0) { final ConfigurationRegistry registry = ConfigurationFactory.getRegistry(); return registry.values().toArray(new Configuration[registry.size()]); } else { return configurations; } } /** * @return static parameters set by developer, which will overrides configuration items * @see Context#parameters() */ protected ConcurrentMap<String, Serializable> getParameters() { return parameters; } /** * @param property simple name of the property * @return full property name of the property */ protected String getFullPropertyName(final String property) { if (property == null) { return null; } final StringBuilder prefix = getFullPrefix(); if (prefix.length() > 0) { return prefix + "." + property; } else { return property; } } /** * @return Returns the eventual prefix (prefix + name) of property names */ protected StringBuilder getFullPrefix() { final StringBuilder fullprefix = new StringBuilder(); if (!StringHelper.isEmpty(prefix)) { fullprefix.append(prefix); } if (!StringHelper.isEmpty(getName()) && prefix != null && prefix.length() > 0) { fullprefix.append(".").append(name); } if (!StringHelper.isEmpty(getName()) && (prefix == null || prefix.length() <= 0)) { fullprefix.append(name); } return fullprefix; } }