ModelProcessor.java
package org.andromda.core.engine;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.andromda.core.ModelValidationException;
import org.andromda.core.cartridge.Cartridge;
import org.andromda.core.common.AndroMDALogger;
import org.andromda.core.common.BuildInformation;
import org.andromda.core.common.ComponentContainer;
import org.andromda.core.common.ExceptionRecorder;
import org.andromda.core.common.Introspector;
import org.andromda.core.common.ResourceWriter;
import org.andromda.core.common.XmlObjectFactory;
import org.andromda.core.configuration.Configuration;
import org.andromda.core.configuration.Filters;
import org.andromda.core.configuration.Model;
import org.andromda.core.configuration.Namespace;
import org.andromda.core.configuration.Namespaces;
import org.andromda.core.configuration.Property;
import org.andromda.core.configuration.Repository;
import org.andromda.core.metafacade.MetafacadeFactory;
import org.andromda.core.metafacade.ModelAccessFacade;
import org.andromda.core.metafacade.ModelValidationMessage;
import org.andromda.core.namespace.NamespaceComponents;
import org.andromda.core.repository.Repositories;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.comparators.ComparatorChain;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
/**
* <p>
* Handles the processing of models. Facilitates Model Driven
* Architecture by enabling the generation of source code, configuration files, and other such artifacts from a single
* or multiple models. </p>
*
* @author Chad Brandon
* @author Bob Fields
* @author Michail Plushnikov
*/
public class ModelProcessor
{
/**
* The logger instance.
*/
private static final Logger logger = Logger.getLogger(ModelProcessor.class);
/**
* Creates a new instance the ModelProcessor.
*
* @return the shared ModelProcessor instance.
*/
public static ModelProcessor newInstance()
{
return new ModelProcessor();
}
private ModelProcessor()
{
// - do not allow instantiation
}
/**
* Re-configures this model processor from the given <code>configuration</code>
* instance (if different from that of the one passed in during the call to
* {@link #initialize(Configuration)}), and runs the model processor.
*
* @param configuration the configuration from which to configure this model
* processor instance.
* @return any model validation messages collected during model processing (if
* model validation is enabled).
*/
public ModelValidationMessage[] process(final Configuration configuration)
{
this.configure(configuration);
final List<ModelValidationMessage> messages = this.process(configuration.getRepositories());
return messages != null ? messages.toArray(new ModelValidationMessage[messages.size()])
: new ModelValidationMessage[0];
}
/**
* Configures (or re-configures) the model processor if configuration
* is required (the configuration has changed since the previous, or has
* yet to be used).
*
* @param configuration the AndroMDA configuration instance.
*/
private void configure(final Configuration configuration)
{
if (this.requiresConfiguration(configuration))
{
configuration.initialize();
this.reset();
final Property[] properties = configuration.getProperties();
final Introspector introspector = Introspector.instance();
for (Property property : properties)
{
try
{
introspector.setProperty(
this,
property.getName(),
property.getValue());
}
catch (final Throwable throwable)
{
AndroMDALogger.warn(
"Could not set model processor property '" + property.getName() + "' with a value of '" +
property.getValue() + '\'');
}
}
this.currentConfiguration = configuration;
}
}
/**
* Processes all models contained within the <code>repositories</code>
* with the discovered cartridges.
*
* @return any model validation messages that may have been collected during model loading/validation.
*/
private List<ModelValidationMessage> process(final Repository[] repositories)
{
List<ModelValidationMessage> messages = null;
final long startTime = System.currentTimeMillis();
for (Repository repository : repositories)
{
if (repository != null)
{
final String repositoryName = repository.getName();
// - filter out any invalid models (ones that don't have any uris defined)
final Model[] models = this.filterInvalidModels(repository.getModels());
if (models.length > 0)
{
messages = this.processModels(
repositoryName,
models);
AndroMDALogger.info(
"completed model processing --> TIME: " + this.getDurationInSeconds(startTime) +
"[s], RESOURCES WRITTEN: " + ResourceWriter.instance().getWrittenCount());
}
else
{
AndroMDALogger.warn("No model(s) found to process for repository '" + repositoryName + '\'');
}
}
}
if(messages == null)
{
messages = Collections.emptyList();
}
return messages;
}
/**
* The shared metafacade factory instance.
*/
private final MetafacadeFactory factory = MetafacadeFactory.getInstance();
/**
* The shared namespaces instance.
*/
private final Namespaces namespaces = Namespaces.instance();
/**
* The shared repositories instance.
*/
private final Repositories repositories = Repositories.instance();
/**
* Check if files have not been modified since last model modification date.
* Do not generate from model if no modifications made.
*/
private boolean lastModifiedCheck = true;
/**
* Location where model generation file list is written, normally /target.
*/
private String historyDir = null;
/**
* @param lastModifiedCheck the lastModifiedCheck to set
*/
public void setLastModifiedCheck(boolean lastModifiedCheck)
{
this.lastModifiedCheck = lastModifiedCheck;
}
/**
* @param historyDir the historyDir to set
*/
public void setHistoryDir(String historyDir)
{
this.historyDir = historyDir;
}
/**
* Processes multiple <code>models</code>.
*
* @param repositoryName the name of the repository that loads/reads the model.
* @param models the Model(s) to process.
* @return any model validation messages that may have been collected during validation/loading of
* the <code>models</code>.
*/
private List<ModelValidationMessage> processModels(
final String repositoryName,
final Model[] models)
{
List<ModelValidationMessage> messages = null;
String cartridgeName = null;
try
{
// If lastModifiedCheck = true, always check for modification times
long lastModified = 0;
final ResourceWriter writer = ResourceWriter.instance();
writer.setHistoryStorage(historyDir);
// - get the time from the model that has the latest modified time
for (Model model : models)
{
writer.resetHistory(model.getUris()[0]);
// lastModifiedCheck from andromda.xml can override the global lastModifiedCheck from maven if true
this.lastModifiedCheck = model.isLastModifiedCheck() || this.lastModifiedCheck;
// - we go off the model that was most recently modified.
if (model.getLastModified() > lastModified)
{
lastModified = model.getLastModified();
}
}
if (!this.lastModifiedCheck || writer.isHistoryBefore(lastModified))
{
final Collection<Cartridge> cartridges = ComponentContainer.instance().findComponentsOfType(Cartridge.class);
if (cartridges.isEmpty())
{
AndroMDALogger.warn("WARNING! No cartridges found, check your classpath!");
}
final Map<String, Cartridge> cartridgesByNamespace = this.loadCartridgesByNamespace(cartridges);
// - we want to process by namespace so that the order within the configuration is kept
final Collection<Namespace> namespaces = this.namespaces.getNamespaces();
// - pre-load the models
messages = this.loadIfNecessary(models);
for (Namespace namespace : namespaces)
{
final Cartridge cartridge = cartridgesByNamespace.get(namespace.getName());
if (cartridge != null)
{
cartridgeName = cartridge.getNamespace();
if (this.shouldProcess(cartridgeName))
{
// - set the active namespace on the shared factory and profile instances
this.factory.setNamespace(cartridgeName);
cartridge.initialize();
// - process each model with the cartridge
for (Model model : models)
{
AndroMDALogger.info("Processing cartridge " + cartridge.getNamespace() + " on model " + model);
// - set the namespace on the metafacades instance so we know the
// correct facades to use
this.factory.setModel(
this.repositories.getImplementation(repositoryName).getModel(),
model.getType());
cartridge.processModelElements(this.factory);
writer.writeHistory();
}
cartridge.shutdown();
}
}
}
}
else
{
AndroMDALogger.info("Files are up-to-date, skipping AndroMDA execution");
}
}
catch (final ModelValidationException exception)
{
// - we don't want to record model validation exceptions
throw exception;
}
catch (final Throwable throwable)
{
final String messsage =
"Error performing ModelProcessor.process with model(s) --> '" + StringUtils.join(
models,
",") + '\'';
logger.error(messsage);
ExceptionRecorder.instance().record(
messsage,
throwable,
cartridgeName);
throw new ModelProcessorException(messsage, throwable);
}
if(messages == null)
{
messages = Collections.emptyList();
}
return messages;
}
/**
* Loads the given list of <code>cartridges</code> into a map keyed by namespace.
*
* @param cartridges the cartridges loaded.
* @return the loaded cartridge map.
*/
private Map<String, Cartridge> loadCartridgesByNamespace(final Collection<Cartridge> cartridges)
{
final Map<String, Cartridge> cartridgesByNamespace = new LinkedHashMap<String, Cartridge>();
for (Cartridge cartridge : cartridges)
{
cartridgesByNamespace.put(cartridge.getNamespace(), cartridge);
}
return cartridgesByNamespace;
}
/**
* Initializes this model processor instance with the given
* configuration. This configuration information is overridden (if changed)
* when calling {@link #process(Configuration)}
*
* @param configuration the configuration instance by which to initialize this
* model processor instance.
*/
public void initialize(final Configuration configuration)
{
final long startTime = System.currentTimeMillis();
// - first, print the AndroMDA header
this.printConsoleHeader();
// - second, configure this model processor
// - the ordering of this step is important: it needs to occur
// before everything else in the framework is initialized so that
// we have all configuration information available (such as the
// namespace properties)
this.configure(configuration);
// - the logger configuration may have changed - re-init the logger.
AndroMDALogger.initialize();
// - discover all namespace components
NamespaceComponents.instance().discover();
// - find and initialize any repositories
repositories.initialize();
// - finally initialize the metafacade factory
this.factory.initialize();
this.printWorkCompleteMessage(
"core initialization",
startTime);
}
/**
* Loads the model into the repository only when necessary (the model has a timestamp
* later than the last timestamp of the loaded model).
*
* @param model the model to be loaded.
* @return List validation messages
*/
protected final List<ModelValidationMessage> loadModelIfNecessary(final Model model)
{
final List<ModelValidationMessage> validationMessages = new ArrayList<ModelValidationMessage>();
final long startTime = System.currentTimeMillis();
if (this.repositories.loadModel(model))
{
this.printWorkCompleteMessage(
"loading",
startTime);
// - validate the model since loading has successfully occurred
final Repository repository = model.getRepository();
final String repositoryName = repository != null ? repository.getName() : null;
validationMessages.addAll(this.validateModel(
repositoryName,
model));
}
return validationMessages;
}
/**
* Validates the entire model with each cartridge namespace,
* and returns any validation messages that occurred during validation
* (also logs any validation failures).
*
* @param repositoryName the name of the repository storing the model to validate.
* @param model the model to validate
* @return any {@link ModelValidationMessage} instances that may have been collected
* during validation.
*/
private List<ModelValidationMessage> validateModel(
final String repositoryName,
final Model model)
{
final Filters constraints = model != null ? model.getConstraints() : null;
final List<ModelValidationMessage> validationMessages = new ArrayList<ModelValidationMessage>();
if (ModelProcessor.modelValidation && model != null)
{
final long startTime = System.currentTimeMillis();
AndroMDALogger.info("- validating model -");
final Collection<Cartridge> cartridges = ComponentContainer.instance().findComponentsOfType(Cartridge.class);
final ModelAccessFacade modelAccessFacade =
this.repositories.getImplementation(repositoryName).getModel();
// - clear out the factory's caches (such as any previous validation messages, etc.)
this.factory.clearCaches();
this.factory.setModel(
modelAccessFacade,
model.getType());
for (Cartridge cartridge : cartridges)
{
final String cartridgeName = cartridge.getNamespace();
if (this.shouldProcess(cartridgeName))
{
// - set the active namespace on the shared factory and profile instances
this.factory.setNamespace(cartridgeName);
this.factory.validateAllMetafacades();
}
}
final List<ModelValidationMessage> messages = this.factory.getValidationMessages();
this.filterAndSortValidationMessages(
messages,
constraints);
this.printValidationMessages(messages);
this.printWorkCompleteMessage(
"validation",
startTime);
if (messages != null && !messages.isEmpty())
{
validationMessages.addAll(messages);
}
}
return validationMessages;
}
/**
* Prints a work complete message using the type of <code>unitOfWork</code> and
* <code>startTime</code> as input.
* @param unitOfWork the type of unit of work that was completed
* @param startTime the time the unit of work was started.
*/
private void printWorkCompleteMessage(
final String unitOfWork,
final long startTime)
{
AndroMDALogger.info("- " + unitOfWork + " complete: " + this.getDurationInSeconds(startTime) + "[s] -");
}
/**
* Calculates the duration in seconds between the
* given <code>startTime</code> and the current time.
* @param startTime the time to compare against.
* @return the duration of time in seconds.
*/
private double getDurationInSeconds(final long startTime)
{
return ((System.currentTimeMillis() - startTime) / 1000.0);
}
/**
* Prints any model validation errors stored within the <code>factory</code>.
*/
private void printValidationMessages(final List<ModelValidationMessage> messages)
{
// - log all error messages
if (messages != null && !messages.isEmpty())
{
final StringBuilder header =
new StringBuilder("Model Validation Failed - " + messages.size() + " VALIDATION ERROR");
if (messages.size() > 1)
{
header.append('S');
}
AndroMDALogger.error(header);
int ctr = 1;
for (ModelValidationMessage message : messages)
{
AndroMDALogger.error(ctr + ") " + message);
ctr++;
}
AndroMDALogger.reset();
if (this.failOnValidationErrors)
{
throw new ModelValidationException("Model validation failed!");
}
}
}
/**
* The current configuration of this model processor.
*/
private Configuration currentConfiguration = null;
/**
* Determines whether or not this model processor needs to be reconfigured.
* This is based on whether or not the new configuration is different
* than the <code>currentConfiguration</code>. We determine this checking
* if their contents are equal or not, if not equal this method will
* return true, otherwise false.
*
* @param configuration the configuration to compare to the lastConfiguration.
* @return true/false
*/
private boolean requiresConfiguration(final Configuration configuration)
{
boolean requiresConfiguration =
this.currentConfiguration == null || this.currentConfiguration.getContents() == null ||
configuration.getContents() == null;
if (!requiresConfiguration)
{
requiresConfiguration = !this.currentConfiguration.getContents().equals(configuration.getContents());
}
return requiresConfiguration;
}
/**
* Checks to see if <em>any</em> of the repositories contain models
* that need to be reloaded, and if so, re-loads them.
*
* @param repositories the repositories from which to load the model(s).
* @return messages
*/
final List<ModelValidationMessage> loadIfNecessary(final org.andromda.core.configuration.Repository[] repositories)
{
final List<ModelValidationMessage> messages = new ArrayList<ModelValidationMessage>();
if (repositories != null)
{
for (Repository repository : repositories)
{
if (repository != null)
{
messages.addAll(this.loadIfNecessary(repository.getModels()));
}
}
}
return messages;
}
/**
* Checks to see if <em>any</em> of the models need to be reloaded, and if so, re-loads them.
*
* @param models that will be loaded (if necessary).
* @return any validation messages collected during loading.
*/
private List<ModelValidationMessage> loadIfNecessary(final Model[] models)
{
final List<ModelValidationMessage> messages = new ArrayList<ModelValidationMessage>();
if (models != null)
{
for (Model model : models)
{
messages.addAll(this.loadModelIfNecessary(model));
}
}
return messages;
}
/**
* Stores the current version of AndroMDA.
*/
private static final String VERSION = BuildInformation.instance().getBuildVersion();
/**
* Prints the console header.
*/
protected void printConsoleHeader()
{
AndroMDALogger.info("");
AndroMDALogger.info("A n d r o M D A - " + VERSION);
AndroMDALogger.info("");
}
/**
* Whether or not model validation should be performed.
*/
private static boolean modelValidation = true;
/**
* Sets whether or not model validation should occur. This is useful for
* performance reasons (i.e. if you have a large model it can significantly decrease the amount of time it takes for
* AndroMDA to process a model). By default this is set to <code>true</code>.
*
* @param modelValidationIn true/false on whether model validation should be performed or not.
*/
public void setModelValidation(final boolean modelValidationIn)
{
ModelProcessor.modelValidation = modelValidationIn;
}
/**
* Gets whether or not model validation should occur. This is useful for
* performance reasons (i.e. if you have a large model it can significantly decrease the amount of time it takes for
* AndroMDA to process a model). By default this is set to <code>true</code>.
*
* @return modelValidation true/false on whether model validation should be performed or not.
*/
public static boolean getModelValidation()
{
return ModelProcessor.modelValidation;
}
/**
* A flag indicating whether or not failure should occur
* when model validation errors are present.
*/
private boolean failOnValidationErrors = true;
/**
* Sets whether or not processing should fail when validation errors occur, default is <code>true</code>.
*
* @param failOnValidationErrors whether or not processing should fail if any validation errors are present.
*/
public void setFailOnValidationErrors(final boolean failOnValidationErrors)
{
this.failOnValidationErrors = failOnValidationErrors;
}
/**
* Stores the cartridge filter.
*/
private List cartridgeFilter = null;
/**
* Denotes whether or not the complement of filtered cartridges should be processed
*/
private boolean negateCartridgeFilter = false;
/**
* Indicates whether or not the <code>namespace</code> should be processed. This is determined in conjunction with
* {@link #setCartridgeFilter(String)}. If the <code>cartridgeFilter</code> is not defined and the namespace is
* present within the configuration, then this method will <strong>ALWAYS </strong> return true.
*
* @param namespace the name of the namespace to check whether or not it should be processed.
* @return true/false on whether or not it should be processed.
*/
protected boolean shouldProcess(final String namespace)
{
boolean shouldProcess = this.namespaces.namespacePresent(namespace);
if (shouldProcess)
{
shouldProcess = this.cartridgeFilter == null || this.cartridgeFilter.isEmpty();
if (!shouldProcess)
{
shouldProcess =
this.negateCartridgeFilter ^ this.cartridgeFilter.contains(StringUtils.trimToEmpty(namespace));
}
}
return shouldProcess;
}
/**
* The prefix used for cartridge filter negation.
*/
private static final String CARTRIDGE_FILTER_NEGATOR = "~";
/**
* <p>
* Sets the current cartridge filter. This is a comma separated list of namespaces (matching cartridges names) that
* should be processed. </p>
* <p>
* If this filter is defined, then any cartridge names found in this list <strong>will be processed </strong>, while
* any other discovered cartridges <strong>will not be processed </strong>. </p>
*
* @param namespaces a comma separated list of the cartridge namespaces to be processed.
*/
public void setCartridgeFilter(String namespaces)
{
if (namespaces != null)
{
namespaces = StringUtils.deleteWhitespace(namespaces);
if (namespaces.startsWith(CARTRIDGE_FILTER_NEGATOR))
{
this.negateCartridgeFilter = true;
namespaces = namespaces.substring(1);
}
else
{
this.negateCartridgeFilter = false;
}
if (StringUtils.isNotBlank(namespaces))
{
this.cartridgeFilter = Arrays.asList(namespaces.split(","));
}
}
}
/**
* Sets the encoding (UTF-8, ISO-8859-1, etc) for all output
* produced during model processing.
*
* @param outputEncoding the encoding.
*/
public void setOutputEncoding(final String outputEncoding)
{
ResourceWriter.instance().setEncoding(outputEncoding);
}
/**
* Sets <code>xmlValidation</code> to be true/false. This defines whether XML resources loaded by AndroMDA (such as
* plugin descriptors) should be validated. Sometimes underlying parsers don't support XML Schema validation and in
* that case, we want to be able to turn it off.
*
* @param xmlValidation true/false on whether we should validate XML resources used by AndroMDA
*/
public void setXmlValidation(final boolean xmlValidation)
{
XmlObjectFactory.setDefaultValidating(xmlValidation);
}
/**
* <p>
* Sets the <code>loggingConfigurationUri</code> for AndroMDA. This is the URI to an external logging configuration
* file. This is useful when you want to override the default logging configuration of AndroMDA. </p>
* <p>
* You can retrieve the default log4j.xml contained within the {@link org.andromda.core.common}package, customize
* it, and then specify the location of this logging file with this operation. </p>
*
* @param loggingConfigurationUri the URI to the external logging configuration file.
*/
public void setLoggingConfigurationUri(final String loggingConfigurationUri)
{
AndroMDALogger.setLoggingConfigurationUri(loggingConfigurationUri);
}
/**
* Filters out any <em>invalid</em> models. This means models that either are null within the specified
* <code>models</code> array or those that don't have URLs set.
*
* @param models the models to filter.
* @return the array of valid models
*/
private Model[] filterInvalidModels(final Model[] models)
{
final Collection<Model> validModels = new ArrayList<Model>(Arrays.asList(models));
CollectionUtils.filter(validModels, new Predicate() {
public boolean evaluate(Object o) {
final Model model = (Model)o;
return model != null && model.getUris() != null && model.getUris().length > 0;
}
});
return validModels.toArray(new Model[validModels.size()]);
}
/**
* Shuts down the model processor (reclaims any
* resources).
*/
public void shutdown()
{
// - shutdown the metafacade factory instance
this.factory.shutdown();
// - shutdown the configuration namespaces instance
this.namespaces.clear();
// - shutdown the container instance
ComponentContainer.instance().shutdown();
// - shutdown the namespace components registry
NamespaceComponents.instance().shutdown();
// - shutdown the introspector
Introspector.instance().shutdown();
// - clear out any caches used by the configuration
Configuration.clearCaches();
// - clear out any repositories
this.repositories.clear();
}
/**
* Reinitializes the model processor's resources.
*/
private void reset()
{
this.factory.reset();
this.cartridgeFilter = null;
this.setXmlValidation(true);
this.setOutputEncoding(null);
this.setModelValidation(true);
this.setFailOnValidationErrors(true);
}
/**
* Filters out any messages that should not be applied according to the AndroMDA configuration's
* constraints and sorts the resulting <code>messages</code> first by type (i.e. the metafacade class)
* and then by the <code>name</code> of the model element to which the validation message applies.
*
* @param messages the collection of messages to sort.
* @param constraints any constraint filters to apply to the validation messages.
*/
protected void filterAndSortValidationMessages(
final List<ModelValidationMessage> messages,
final Filters constraints)
{
if (constraints != null)
{
// - perform constraint filtering (if any applies)
CollectionUtils.filter(messages, new Predicate() {
public boolean evaluate(Object o) {
ModelValidationMessage message = (ModelValidationMessage)o;
return constraints.isApply(message.getName());
}
});
}
if (messages != null && !messages.isEmpty())
{
final ComparatorChain chain = new ComparatorChain();
chain.addComparator(new ValidationMessageTypeComparator());
chain.addComparator(new ValidationMessageNameComparator());
Collections.sort(
messages,
chain);
}
}
/**
* Used to sort validation messages by <code>metafacadeClass</code>.
*/
private static final class ValidationMessageTypeComparator
implements Comparator<ModelValidationMessage>
{
private final Collator collator = Collator.getInstance();
ValidationMessageTypeComparator()
{
collator.setStrength(Collator.PRIMARY);
}
public int compare(
final ModelValidationMessage objectA,
final ModelValidationMessage objectB)
{
return collator.compare(
objectA.getMetafacadeClass().getName(),
objectB.getMetafacadeClass().getName());
}
}
/**
* Used to sort validation messages by <code>modelElementName</code>.
*/
private static final class ValidationMessageNameComparator
implements Comparator<ModelValidationMessage>
{
private final Collator collator = Collator.getInstance();
ValidationMessageNameComparator()
{
collator.setStrength(Collator.PRIMARY);
}
public int compare(
final ModelValidationMessage objectA,
final ModelValidationMessage objectB)
{
return collator.compare(
StringUtils.trimToEmpty(objectA.getMetafacadeName()),
StringUtils.trimToEmpty(objectB.getMetafacadeName()));
}
}
}