/*
* JETERS – Java Extensible Text Replacement System
* Copyright (C) 2006–2008 Tobias Knerr
*
* 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 St, Fifth Floor, Boston, MA 02110, USA
*/
package net.sf.jeters.componentInterface;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import net.sf.jeters.configuration.Configuration;
import net.sf.jeters.configuration.io.ConfigurationReader;
import net.sf.jeters.configuration.io.ConfigurationStorage;
import net.sf.jeters.core.ComponentClassLoader;
import net.sf.jeters.internationalization.Translatable;
import net.sf.jeters.internationalization.TranslationReader;
/**
* class that manages components. It will make sure configurations and
* translations are applied to components when being initialized.
* For other classes, it provides get and set methods to access components and
* to change which components are currently in use.
*
* Note that neither set nor get methods clone the components, modifying them
* (e.g. by setting configurations) will affect the active component.
*
* @author Tobias Knerr
*/
public class ComponentManager {
/** manifest entry name specifying the component class contained in a jar */
private static final String MANIFEST_CLASS_ENTRY = "JETERS-Class";
/** entry for localized component names in resource bundles */
private static final String LOCALIZED_NAME_ENTRY = "displayName";
/** directory containing the components */
private File componentDirectory;
/** the class loader that will load the components and .properties files */
private ComponentClassLoader classLoader;
/** permanent storage for configurations */
private ConfigurationStorage configurationStorage;
/**
* objects that read and write the configurations of components,
* with increasing priority (conflict => earlier one is overwritten)
*/
private ConfigurationReader[] configurationReaders;
/** reader for translations for components */
private TranslationReader translationReader;
/** the components available to the component manager */
private List<Class<? extends Component>> availableComponentClasses;
/** map storing the current configuration for each component class */
private final Map<Class<? extends Component>, Configuration> configurationMap;
/** map storing the .jar file a component is based on */
private final Map<Class<? extends Component>, File> jarFileMap;
/** collection of weak references to the instances of component classes
* created by this manager */
private final Collection<WeakReference<Component>> instances =
new LinkedList<WeakReference<Component>>();
/** the currently used UI component */
private UIComponent uiComponent;
/** the currently used input component */
private InputComponent<?> inputComponent;
/** the currently used output component */
private OutputComponent<?> outputComponent;
/** the currently used replacer components */
private List<ReplacerComponent<?,?>> replacerComponents =
new ArrayList<ReplacerComponent<?,?>>();
/**
* constructor creating a component manager using a given directory as
* component source
*
* @param componentDirectory directory containing components (classes/jars)
* @param configurationStorage storage for base configurations; may be null
* @param configurationReaders objects reading additional configuration options.
* They will not be used to store configurations,
* but will temporarily overwrite information from
* configurationStorage. Ordered with increasing priority.
* (conflicting values => earlier one is overwritten)
* @param translationReader reader providing language-specific Strings for
* components implementing Translatable
*/
public ComponentManager(File componentDirectory,
ConfigurationStorage configurationStorage,
ConfigurationReader[] configurationReaders,
TranslationReader translationReader) {
this.componentDirectory = componentDirectory;
this.configurationStorage = configurationStorage;
this.configurationReaders = configurationReaders;
this.translationReader = translationReader;
try {
classLoader = new ComponentClassLoader(componentDirectory);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("creation of ComponentClassLoader for directory " +
componentDirectory + " resulted in MalformedURLException", e);
}
this.jarFileMap = new HashMap<Class<? extends Component>, File>();
loadAvailableComponentClasses();
this.configurationMap = new HashMap<Class<? extends Component>, Configuration>();
readAllComponentConfigurations();
}
/**
* returns the current input component
*
* @return current input component
*/
public UIComponent getUIComponent() {
return uiComponent;
}
/**
* returns the current input component
*
* @return current input component
*/
public InputComponent<?> getInputComponent() {
return inputComponent;
}
/**
* returns the current output component
*
* @return current input component
*/
public OutputComponent<?> getOutputComponent() {
return outputComponent;
}
/**
* returns a list of the current replacer components;
* modifying of the list will not affect the component manager, modifying
* the components, however, will modify the originals.
*
* @return list of current replacer components;
* guaranteed to be non-null (can, however, be empty)
*/
public List<ReplacerComponent<?,?>> getReplacerComponents() {
List<ReplacerComponent<?,?>> listClone =
new ArrayList<ReplacerComponent<?,?>>(replacerComponents);
return listClone;
}
/**
* creates a new UI component from the given class
* and sets it as active UI component
*
* @param uiCompClass class that will be instantiated to create the
* component; must implement UIComponent, not null,
* a call to newInstance must not cause an
* InstantiationException or IllegalAccessException
*/
public void setUIComponent(Class<? extends UIComponent> uiCompClass) {
UIComponent component;
try {
component = createComponent(uiCompClass);
} catch (InstantiationException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
}
configureComponent(component);
setTranslation(component);
uiComponent = component;
}
/**
* creates a new input component from the given class
* and sets it as active input component
*
* @param componentClass class that will be instantiated to create the
* component; must implement InputComponent,
* not null, a call to newInstance must not cause an
* InstantiationException or IllegalAccessException
*/
public void setInputComponent(Class<? extends InputComponent<?>> componentClass) {
InputComponent<?> component;
try {
component = createComponent(componentClass);
} catch (InstantiationException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
}
configureComponent(component);
setTranslation(component);
inputComponent = component;
}
/**
* creates a new output component from the given class
* and sets it as active output component
*
* @param componentClass class that will be instantiated to create the
* component; must implement OutputComponent,
* not null, a call to newInstance must not cause an
* InstantiationException or IllegalAccessException
*/
public void setOutputComponent(Class<? extends OutputComponent<?>> componentClass) {
OutputComponent<?> component;
try {
component = createComponent(componentClass);
} catch (InstantiationException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
}
configureComponent(component);
setTranslation(component);
outputComponent = component;
}
/**
* creates new replacer components from the given classes
* and sets it as active replacer components
*
* @param componentClasses collection of classes that will be instantiated
* to create the components; every class must
* implement ReplacerComponent, neither the list
* nor its element may be null, a call to
* newInstance for a class must not cause an
* InstantiationException or IllegalAccessException
*/
public void setReplacerComponent(Collection<Class<? extends ReplacerComponent<?,?>>> componentClasses) {
List<ReplacerComponent<?,?>> newReplacerComponents =
new ArrayList<ReplacerComponent<?,?>>();
for (Class<? extends ReplacerComponent<?,?>> componentClass : componentClasses) {
Component component;
try {
component = createComponent(componentClass);
configureComponent(component);
setTranslation(component);
} catch (InstantiationException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
"instantiation of componentClass failed: " + e);
}
newReplacerComponents.add((ReplacerComponent<?,?>)component);
}
replacerComponents = newReplacerComponents;
}
/**
* returns the available components classes, can be limited to those
* components that are instances of given classes/interfaces.
*
* Examples:
* If you want to get only UI components, call:<br/>
* <code>getAvailableComponentClasses(UIComponent.class)</code><br/>
* For all configurable input components, use:<br/>
* <code>getAvailableComponentClasses(Configurable.class,
* InputComponent.class)</code><br/>
* If you simply want all components, you should call<br/>
* <code>getAvailableComponents()</code>
*
* @param classes classes/interfaces the component classes must be
* subtypes of (a vararg parameter)
* @return list of all avaliable component classes
* that are subtypes of the classes given as parameter
*/
public List<Class<? extends Component>> getAvailableComponentClasses(Class<?>... classes) {
List<Class<? extends Component>> matchingComponentClasses =
new ArrayList<Class<? extends Component>>();
for (Class<? extends Component> componentClass : availableComponentClasses) {
boolean instanceOfClasses = true;
for (Class<?> c : classes) {
if (!c.isAssignableFrom(componentClass)) {
instanceOfClasses = false;
break;
}
}
if (instanceOfClasses) {
matchingComponentClasses.add(componentClass);
}
}
return matchingComponentClasses;
}
/**
* returns the full names of the available components classes, can be
* limited to those components that are instances of given
* classes/interfaces. For details and examples, see
* {@link #getAvailableComponentClasses(Class[])}
*
* @param classes classes/interfaces the component classes must be
* subtypes of (a vararg parameter)
* @return list of the names of all avaliable component classes
* that are subtypes of the classes given as parameter
*/
public List<String> getNamesOfAvailableComponentClasses(Class<?>... classes) {
List<String> componentClassNames = new ArrayList<String>();
List<Class<? extends Component>> componentClasses =
getAvailableComponentClasses(classes);
for (Class<? extends Component> componentClass : componentClasses) {
componentClassNames.add(componentClass.getName());
}
return componentClassNames;
}
/**
* returns the component class for the given name
*
* @param name the full name of the component class
* @return a class implementing the Component interface
* or null if no component class with the given name exists
*/
public Class<? extends Component> getComponentClassForName(String name) {
try{
Class<?> loadedClass = classLoader.loadClass(name);
if (isValidComponentClass(loadedClass)) {
@SuppressWarnings("unchecked")
Class<? extends Component> componentClass =
(Class<? extends Component>) loadedClass;
return componentClass;
} else {
return null;
}
} catch (ClassNotFoundException e) {
return null;
} catch (NoClassDefFoundError e) {
return null;
}
}
/**
* reads the configuration for all component classes using the configurationReaders
* and writes the result to the configurationMap.
* Existing instances are reconfigured.
*/
private void readAllComponentConfigurations() {
for (Class<? extends Component> componentClass : availableComponentClasses) {
readComponentConfiguration(componentClass);
}
}
private void readComponentConfiguration(Class <? extends Component> componentClass) {
Configuration classConf = retrieveBaseConfiguration(componentClass);
for (ConfigurationReader configReader : configurationReaders) {
Configuration newClassConf = configReader.read(componentClass);
if (newClassConf != null) {
classConf.setValuesFrom(newClassConf);
}
}
configurationMap.put(componentClass, classConf);
//reconfigure existing instances of componentClass
for (WeakReference<Component> instanceReference : instances) {
Component instance = instanceReference.get();
if (instance != null && componentClass.isInstance(instance)) {
classConf.applyTo(instance);
}
}
}
/**
* reads the configuration from the {@link #configurationStorage};
* uses defaults where necessary
*/
private Configuration retrieveBaseConfiguration(Class <? extends Component> componentClass) {
//get defaults
Configuration baseConf;
try {
baseConf = new Configuration((Class<Component>)componentClass, (Component)componentClass.newInstance());
} catch (InstantiationException e) {
throw new Error(e);
} catch (IllegalAccessException e) {
throw new Error(e);
}
//get values from component storage and overwrite defaults
if (configurationStorage != null) {
Configuration confFromStorage = configurationStorage.read(componentClass);
if (confFromStorage != null) {
baseConf.setValuesFrom(confFromStorage);
} else if (baseConf.getConfigurableFields().size() > 0) {
System.out.println("no configuration found for " + componentClass.getSimpleName());
}
}
return baseConf;
}
/**
* stores the base configurations of all component classes,
* also adds defaults if some fields are not part of the current configuration
* in the configuration storage
*/
public void writeAllComponentBaseConfigurations() {
for (Class<? extends Component> componentClass : availableComponentClasses) {
Configuration baseConf = retrieveBaseConfiguration(componentClass);
writeComponentConfiguration(baseConf);
}
}
/**
* stores a configuration
*
* @param configuration configuration to be stored, must not be null
*/
public void writeComponentConfiguration(Configuration configuration) {
assert configuration != null;
configurationStorage.write(configuration);
}
/**
* "installs" a component by copying its .jar into the component directory
* @param jarFile file to copy to component directory; != null
* @throws IOException
*/
public void installComponent(File jarFile) throws IOException {
File target = new File(componentDirectory + File.separator + jarFile.getName());
System.out.println(target);
InputStream in = new FileInputStream(jarFile);
OutputStream out = new FileOutputStream(target);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0){
out.write(buf, 0, len);
}
in.close();
out.close();
loadAvailableComponentClasses();
}
/**
* "uninstalls" a component by removing its .jar from the component directory
* @param componentClass class whose jar is to be deleted; != null
* @return true if the file was successfully deleted
*/
public boolean uninstallComponent(Class<? extends Component> componentClass) {
File file = jarFileMap.get(componentClass);
if (file != null) {
boolean success = file.delete();
if (success) {
loadAvailableComponentClasses();
}
return success;
} else {
return false;
}
}
/**
* returns true if the file can be removed (it needs to be a .jar file for this)
*/
public boolean canUninstall(Class<? extends Component> componentClass) {
return jarFileMap.containsKey(componentClass);
}
/**
* finds all available component classes
* and creates the availableComponentClasses list
*/
private void loadAvailableComponentClasses() {
availableComponentClasses = new LinkedList<Class<? extends Component>>();
availableComponentClasses.addAll(getComponentClassesFromClassFiles());
availableComponentClasses.addAll(getComponentClassesFromJarFiles());
}
/**
* returns a collection of all valid components classes that exist as
* class files in the component directory.
*
* @return classes as collection, guaranteed to be component classes
*/
private Collection<Class<? extends Component>> getComponentClassesFromClassFiles() {
Collection<Class<? extends Component>> componentClasses =
new LinkedList<Class<? extends Component>>();
//get all filenames from the directory that end with ".class"
String[] fileNames = listFilesEndingWith(componentDirectory, ".class");
if (fileNames != null) {
for (String fileName : fileNames) {
//get the name of the class (remove ".class")
String className = fileName.substring(0,
fileName.length() - ".class".length());
/* get and add the class itself (the method will return
* null if the class cannot be loaded or is not a valid
* component class) */
Class<? extends Component> componentClass = getComponentClassForName(className);
if (componentClass != null) {
componentClasses.add(componentClass);
}
}
}
return componentClasses;
}
/**
* returns a collection of all valid components classes that exist in
* jar files in the component directory. The class for loading is
* determined by the manifest's MANIFEST_CLASS_ENTRY value.
*
* @return classes as collection, guaranteed to be component classes
*/
private Collection<Class<? extends Component>> getComponentClassesFromJarFiles() {
Collection<Class<? extends Component>> componentClasses =
new LinkedList<Class<? extends Component>>();
//get all filenames from the directory that end with ".jar"
String[] fileNames = listFilesEndingWith(componentDirectory, ".jar");
if (fileNames != null) {
for (String fileName : fileNames) {
String className = null;
File file = null;
try{
//create a JarFile-object
file = new File(componentDirectory, fileName);
JarFile jarFile = new JarFile(file);
//find out which class is the component
Manifest m = jarFile.getManifest();
className =
m.getMainAttributes().getValue(MANIFEST_CLASS_ENTRY);
} catch(IOException e){
/* TODO: log "Error accessing the component jar \"" +
fileName + "\": " + ioe.toString() */
}
if (className != null) {
/* get and add the class itself (the method will return
* null if the class cannot be loaded or is not a valid
* component class) */
Class<? extends Component> componentClass =
getComponentClassForName(className);
if (componentClass != null) {
componentClasses.add(componentClass);
jarFileMap.put(componentClass, file);
}
}
}
}
return componentClasses;
}
/**
* instantiates a component class and returns the created component
*
* @param <C> component subtype to create
*
* @param c class to instantiate, must implement component class; not null
* @return true for valid component classes, otherwise false
*
* @throws InstantiationException may be thrown on creating an instance of c
* @throws IllegalAccessException may be thrown on creating an instance of c
*/
private <C extends Component> C createComponent(Class<C> c)
throws InstantiationException, IllegalAccessException {
if (c == null) {
throw new NullPointerException("class must not be null");
}
C componentInstance = c.newInstance();
cleanInstancesCollection();
instances.add(new WeakReference<Component>(componentInstance));
return componentInstance;
}
/**
* removes all weak references to components from {@link #instances}
* that don't exist anymore
*/
private void cleanInstancesCollection() {
for (Iterator<WeakReference<Component>> iterator = instances.iterator(); iterator.hasNext();) {
WeakReference<Component> instanceReference = (WeakReference<Component>) iterator.next();
if (instanceReference.get() == null) {
iterator.remove();
}
}
}
/**
* loads and sets configuration for a component
*
* @param component component to set configurations for, not null; may
* be a component that is not configurable (=> no action)
*/
private void configureComponent(Component component) {
assert component != null;
Configuration configuration = configurationMap.get(component.getClass());
if (configuration != null) {
configuration.applyTo(component);
}
}
/**
* returns the base configuration for a component class.
* This provides the configuration as permanently stored for that class,
* <em>without</em> influences by the temporary configuration readers
* that were also set in the constructor.
*
* @param componentClass class to get configuration for; != null
*/
public Configuration getComponentClassBaseConfiguration(Class<? extends Component> componentClass) {
assert componentClass != null;
return retrieveBaseConfiguration(componentClass);
}
/**
* changes the current base configuration for a component class;
* see also {@link #getComponentClassBaseConfiguration(Class)}
*
* @param componentClass class to set configuration for; != null
* @param configuration new base configuration for componentClass; != null
*/
public void setComponentClassBaseConfiguration(
Class<? extends Component> componentClass,
Configuration configuration) {
assert componentClass != null && configuration != null;
assert configuration.getClass().equals(componentClass);
if (configurationStorage != null) {
configurationStorage.write(configuration);
}
readComponentConfiguration(componentClass);
}
/**
* loads and sets the translation for a component
* using the current translatationReader.
*
* @param component component whose translation is to be set,
* not null; may be an object that is not translatable
* (=> no action)
*/
public void setTranslation(Object component) {
if (component == null) {
throw new NullPointerException("component must not be null");
}
if (component instanceof Translatable) {
ResourceBundle langRB = loadTranslation(component.getClass());
((Translatable)component).setLanguageResourceBundle(langRB);
}
}
private ResourceBundle loadTranslation(Class<?> componentClass) {
assert componentClass != null;
String componentSimpleName = componentClass.getSimpleName();
return translationReader.readTranslation(componentSimpleName,
classLoader);
}
/** reloads translations for all active components */
private void reloadTranslations() {
for (Component component : activeComponents()) {
setTranslation(component);
}
}
/**
* returns the localized component name for a component;
* this name is determined from its resource bundle for the current locale
* (if it exists)
*
* @param componentClass component class to get localized name for, != null
* @return localized name or null (if no localized name exists)
*/
public String getLocalizedComponentName(Class<? extends Component> componentClass) {
if (componentClass == null) {
throw new NullPointerException("componentClass must not be null");
}
if (!Translatable.class.isAssignableFrom(componentClass)) {
return null;
}
ResourceBundle langRB = loadTranslation(componentClass);
if (langRB != null && langRB.containsKey(LOCALIZED_NAME_ENTRY)) {
return langRB.getString(LOCALIZED_NAME_ENTRY);
} else {
return null;
}
}
/**
* returns a list of all components currently active
* (i.e. inputComp., outputComp., uiComp. and the replacerComp.s
* to ease performing operations on all of them
*/
private List<? extends Component> activeComponents() {
List<Component> activeComponents = new LinkedList<Component>();
activeComponents.add(inputComponent);
activeComponents.add(outputComponent);
activeComponents.add(uiComponent);
for (ReplacerComponent<?,?> replacerComponent : replacerComponents) {
activeComponents.add(replacerComponent);
}
return activeComponents;
}
/**
* lists all files in a directory that end with a given string
*
* @param dir directory to list contents of
* @param str String all returned filenames must end with
* @return list of filenames ending with s; null if this abstract
* pathname does not denote a directory, or if an I/O error
* occurs (compare java.io.File's list(FilenameFilter) method)
*/
private static final String[] listFilesEndingWith(File dir,
final String str) {
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(str);
}
};
return dir.list(filter);
}
/**
* checks whether a class is a valid component class, i.e. implementing
* Component, public and not abstract
*
* @param c class to check
* @return true for valid component classes, otherwise false
*/
private static final boolean isValidComponentClass(Class<?> c) {
return Component.class.isAssignableFrom(c)
&& Modifier.isPublic(c.getModifiers())
&& !java.lang.reflect.Modifier.isAbstract(c.getModifiers());
}
}