Cartridge.java

package org.andromda.core.cartridge;

import java.io.File;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import org.andromda.core.cartridge.template.ModelElement;
import org.andromda.core.cartridge.template.ModelElements;
import org.andromda.core.cartridge.template.Template;
import org.andromda.core.cartridge.template.Type;
import org.andromda.core.common.AndroMDALogger;
import org.andromda.core.common.BasePlugin;
import org.andromda.core.common.ExceptionUtils;
import org.andromda.core.common.Introspector;
import org.andromda.core.common.PathMatcher;
import org.andromda.core.common.PostProcessor;
import org.andromda.core.common.ResourceUtils;
import org.andromda.core.common.ResourceWriter;
import org.andromda.core.common.TemplateObject;
import org.andromda.core.configuration.Namespaces;
import org.andromda.core.metafacade.MetafacadeBase;
import org.andromda.core.metafacade.MetafacadeFactory;
import org.andromda.core.metafacade.ModelAccessFacade;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * The AndroMDA Cartridge implementation of the Plugin. Cartridge instances are configured from
 * <code>META-INF/andromda/cartridge.xml</code> files discovered on the classpath.
 *
 * @author <a href="http://www.mbohlen.de">Matthias Bohlen </a>
 * @author Chad Brandon
 * @author Bob Fields
 * @author Michail Plushnikov
 */
public class Cartridge
    extends BasePlugin
{
    /** The logger instance. */
    private static final Logger LOGGER = Logger.getLogger(Cartridge.class);

    /**
     * Processes all model elements with relevant stereotypes by retrieving the model elements from the model facade
     * contained within the context.
     *
     * @param factory the metafacade factory (which is used to manage the lifecycle of metafacades).
     */
    public void processModelElements(final MetafacadeFactory factory)
    {
        ExceptionUtils.checkNull(
            "factory",
            factory);
        final Collection<Resource> resources = this.getResources();
        if (resources != null && !resources.isEmpty())
        {
            for (Resource resource : resources)
            {
                try
                {
                    if (resource instanceof Template)
                    {
                        this.processTemplate(
                            factory,
                            (Template)resource);
                    }
                    else
                    {
                        this.processResource(resource);
                    }
                }
                catch (Exception e)
                {
                    // Don't kill the entire code generation if one output fails
                    LOGGER.error("Error processing resource " + resource.toString(), e);
                }
            }
        }
    }

    /**
     * Processes the given <code>template</code>.
     *
     * @param factory the metafacade factory instance.
     * @param template the Template instance to process.
     */
    protected void processTemplate(
        final MetafacadeFactory factory,
        final Template template)
    {
        ExceptionUtils.checkNull(
            "template",
            template);
        final ModelElements templateModelElements = template.getSupportedModeElements();

        // - handle the templates WITH model elements
        if (templateModelElements != null && !templateModelElements.isEmpty())
        {
            for (ModelElement templateModelElement : templateModelElements.getModelElements())
            {
                // - if the template model element has a stereotype
                //   defined, then we filter the metafacades based
                //   on that stereotype, otherwise we get all metafacades
                //   and let the templateModelElement perform filtering on the
                //   metafacades by type and properties
                if (templateModelElement.hasStereotype())
                {
                    templateModelElement.setMetafacades(
                        factory.getMetafacadesByStereotype(templateModelElement.getStereotype()));
                }
                else if (templateModelElement.hasTypes())
                {
                    templateModelElement.setMetafacades(factory.getAllMetafacades());
                }
            }
            this.processTemplateWithMetafacades(
                factory,
                template);
        }
        else
        {
            // - handle any templates WITHOUT metafacades.
            this.processTemplateWithoutMetafacades(template);
        }
    }

    /**
     * Processes all <code>modelElements</code> for this template.
     *
     * @param factory the metafacade factory
     * @param template the template to process
     */
    protected void processTemplateWithMetafacades(
        final MetafacadeFactory factory,
        final Template template)
    {
        ExceptionUtils.checkNull(
            "template",
            template);
        final ModelElements modelElements = template.getSupportedModeElements();
        if (modelElements != null && !modelElements.isEmpty())
        {
            try
            {
                final Collection<MetafacadeBase> allMetafacades = modelElements.getAllMetafacades();
                // Tell us which template is processed against how many metafacade elements
                LOGGER.info("Processing " + template.getPath() + " with " + allMetafacades.size() + " metafacades from " + modelElements.getModelElements().size() + " model elements");

                // - if outputToSingleFile is true AND outputOnEmptyElements
                //   is true or we have at least one metafacade in the
                //   allMetafacades collection, then we collect the template
                //   model elements and place them into the template context
                //   by their variable names.
                if (template.isOutputToSingleFile() &&
                    (template.isOutputOnEmptyElements() || !allMetafacades.isEmpty()))
                {
                    final Map<String, Object> templateContext = new LinkedHashMap<String, Object>();

                    // - first place all relevant model elements by the
                    //   <modelElements/> variable name. If the variable
                    //   isn't defined (which is possible), ignore.
                    final String modelElementsVariable = modelElements.getVariable();
                    if (StringUtils.isNotBlank(modelElementsVariable))
                    {
                        templateContext.put(
                            modelElementsVariable,
                            allMetafacades);
                    }

                    // - now place the collections of elements by the given variable names.
                    //   (skip if the variable is NOT defined)
                    for (final ModelElement modelElement : modelElements.getModelElements())
                    {
                        final String modelElementVariable = modelElement.getVariable();
                        if (StringUtils.isNotBlank(modelElementVariable))
                        {
                            // - if a modelElement has the same variable defined
                            //   more than one time, then get the existing
                            //   model elements added from the last iteration
                            //   and add the new ones to that collection
                            Collection<MetafacadeBase> metafacades = (Collection<MetafacadeBase>)templateContext.get(modelElementVariable);
                            if (metafacades != null)
                            {
                                metafacades.addAll(modelElement.getMetafacades());
                            }
                            else
                            {
                                metafacades = modelElement.getMetafacades();
                                templateContext.put(
                                    modelElementVariable,
                                    new LinkedHashSet<MetafacadeBase>(metafacades));
                            }
                        }
                    }
                    this.processWithTemplate(
                        template,
                        templateContext,
                        null,
                        null);
                }
                else
                {
                    // - if outputToSingleFile isn't true, then
                    //   we just place the model element with the default
                    //   variable defined on the <modelElements/> into the
                    //   template.
                    for (Object metafacade : allMetafacades)
                    {
                        final Map<String, Object> templateContext = new LinkedHashMap<String, Object>();
                        final ModelAccessFacade model = factory.getModel();
                        for (final ModelElement modelElement : modelElements.getModelElements())
                        {
                            String variable = modelElement.getVariable();

                            // - if the variable isn't defined on the <modelElement/>, try
                            //   the <modelElements/>
                            if (StringUtils.isBlank(variable))
                            {
                                variable = modelElements.getVariable();
                            }

                            // - only add the metafacade to the template context if the variable
                            //   is defined (which is possible)
                            if (StringUtils.isNotBlank(variable))
                            {
                                templateContext.put(
                                    variable,
                                    metafacade);
                            }

                            // - now we process any property templates (if any 'variable' attributes are defined on one or
                            //   more type's given properties), otherwise we process the single metafacade as usual
                            if (!this.processPropertyTemplates(
                                    template,
                                    metafacade,
                                    templateContext,
                                    modelElement))
                            {
                                this.processWithTemplate(
                                    template,
                                    templateContext,
                                    model.getName(metafacade),
                                    model.getPackageName(metafacade));
                            }
                        }
                    }
                }
            }
            catch (final Throwable throwable)
            {
                LOGGER.error("Error Processing " + template.getPath(), throwable);
                throw new CartridgeException(throwable);
            }
        }
    }

    /**
     * Determines if any property templates need to be processed (that is templates
     * that are processed given related <em>properties</em> of a metafacade).
     *
     * @param template the template to use for processing.
     * @param metafacade the metafacade instance (the property value is retrieved from this).
     * @param templateContext the template context containing the instance to pass to the template.
     * @param modelElement the model element from which we retrieve the corresponding types and then
     *        properties to determine if any properties have been mapped for template processing.
     * @return true if any property templates have been evaluated (false otherwise).
     */
    private boolean processPropertyTemplates(
        final Template template,
        final Object metafacade,
        final Map<String, Object> templateContext,
        final ModelElement modelElement)
    {
        boolean propertyTemplatesEvaluated = false;
        for (final Type type : modelElement.getTypes())
        {
            for (final Type.Property property : type.getProperties())
            {
                final String variable = property.getVariable();
                propertyTemplatesEvaluated = StringUtils.isNotBlank(variable);
                if (propertyTemplatesEvaluated)
                {
                    final Object value = Introspector.instance().getProperty(
                            metafacade,
                            property.getName());
                    if (value instanceof Collection)
                    {
                        for (Object entry : (Collection) value)
                        {
                            templateContext.put(
                                variable,
                                entry);
                            this.processWithTemplate(
                                template,
                                templateContext,
                                null,
                                null);
                        }
                    }
                    else
                    {
                        templateContext.put(
                            variable,
                            value);
                        this.processWithTemplate(
                            template,
                            templateContext,
                            null,
                            null);
                    }
                }
            }
        }
        return propertyTemplatesEvaluated;
    }

    /**
     * Processes the <code>template</code> without metafacades. This is useful if you need to generate something that
     * is part of your cartridge, however you only need to use a property passed in from a namespace or a template
     * object defined in your cartridge descriptor.
     *
     * @param template the template to process.
     */
    protected void processTemplateWithoutMetafacades(final Template template)
    {
        ExceptionUtils.checkNull(
            "template",
            template);
        final Map<String, Object> templateContext = new LinkedHashMap<String, Object>();
        this.processWithTemplate(
            template,
            templateContext,
            null,
            null);
    }

    /**
     * <p>
     * Perform processing with the <code>template</code>.
     * </p>
     *
     * @param template the Template containing the template path to process.
     * @param templateContext the context to which variables are added and made
     *        available to the template engine for processing. This will contain
     *        any model elements being made available to the template(s) as well
     *        as properties/template objects.
     * @param metafacadeName the name of the model element (if we are
     *        processing a single model element, otherwise this will be
     *        ignored).
     * @param metafacadePackage the name of the package (if we are processing
     *        a single model element, otherwise this will be ignored).
     */
    private void processWithTemplate(
        final Template template,
        final Map<String, Object> templateContext,
        final String metafacadeName,
        final String metafacadePackage)
    {
        ExceptionUtils.checkNull(
            "template",
            template);
        ExceptionUtils.checkNull(
            "templateContext",
            templateContext);

        File outputFile = null;
        try
        {
            // - populate the template context with cartridge descriptor
            //   properties and template objects
            this.populateTemplateContext(templateContext);

            final StringWriter output = new StringWriter();

            // - process the template with the set TemplateEngine
            this.getTemplateEngine().processTemplate(
                template.getPath(),
                templateContext,
                output);

            // - if we have an outputCondition defined make sure it evaluates to true before continuing
            if (this.isValidOutputCondition(template.getOutputCondition(), templateContext))
            {
                // - get the location and at the same time evaluate the outlet as a template engine variable (in case
                //   its defined as that).
                final String location =
                    Namespaces.instance().getPropertyValue(
                        this.getNamespace(),
                        this.getTemplateEngine().getEvaluatedExpression(
                            template.getOutlet(),
                            templateContext));
                if (location == null)
                {
                    LOGGER.warn("Template outlet location '" + template.getOutlet() + "' is not defined in namespace " + this.getNamespace());
                }
                else
                {
                    outputFile =
                        template.getOutputLocation(
                            metafacadeName,
                            metafacadePackage,
                            new File(location),
                            this.getTemplateEngine().getEvaluatedExpression(
                                template.getOutputPattern(),
                                templateContext));
                    if (outputFile == null)
                    {
                        LOGGER.warn("Template outputFile is null for location " + location);
                    }
                    else
                    {
                        // - only write files that do NOT exist, and
                        //   those that have overwrite set to 'true'
                        if (!outputFile.exists() || template.isOverwrite())
                        {
                            String outputString = output.toString();
                            AndroMDALogger.setSuffix(this.getNamespace());

                            // - check to see if generateEmptyFiles is true and if
                            //   outString is not blank
                            if (StringUtils.isNotBlank(outputString) ||
                                template.isGenerateEmptyFiles())
                            {
                                for (PostProcessor postProcessor : getTemplatePostProcessor())
                                {
                                    if(postProcessor.acceptFile(outputFile))
                                    {
                                        try
                                        {
                                            String lResult = postProcessor.postProcess(outputString, null);
                                            if(StringUtils.isNotBlank(lResult))
                                            {
                                                outputString = lResult;
                                            }
                                            else
                                            {
                                                LOGGER.warn("Error PostProcessing " + outputFile.toURI());
                                            }
                                        }
                                        catch (Exception exc)
                                        {
                                            LOGGER.warn("Error PostProcessing " + outputFile.toURI() + ": " + exc.getMessage());
                                        }
                                    }
                                }

                                ResourceWriter.instance().writeStringToFile(
                                    outputString,
                                    outputFile,
                                    this.getNamespace());
                                AndroMDALogger.info("Output: '" + outputFile.toURI() + '\'');
                            }
                            else
                            {
                                if (this.getLogger().isDebugEnabled())
                                {
                                    this.getLogger().debug("Empty Output: '" + outputFile.toURI() + "' --> not writing");
                                }
                            }
                            AndroMDALogger.reset();
                        }
                    }
                }
            }
        }
        catch (final Throwable throwable)
        {
            if (FileUtils.deleteQuietly(outputFile))
            {
                this.getLogger().info("Removed: '" + outputFile + '\'');
            }
            final String message =
                "Error processing template '" + template.getPath() + "' with template context '" + templateContext +
                "' using cartridge '" + this.getNamespace() + '\'';
            LOGGER.error(message, throwable);
            //throw new CartridgeException(message, throwable);
        }
    }

    /**
     * Processes the given <code>resource</code>
     *
     * @param resource the resource to process.
     */
    protected void processResource(final Resource resource)
    {
        ExceptionUtils.checkNull(
            "resource",
            resource);

        URL resourceUrl = ResourceUtils.getResource(
                resource.getPath(),
                this.getMergeLocation());
        if (resourceUrl == null)
        {
            // - if the resourceUrl is null, the path is probably a regular
            //   outputCondition pattern so we'll see if we can match it against
            //   the contents of the plugin and write any contents that do match
            final List<String> contents = this.getContents();
            if (contents != null)
            {
                AndroMDALogger.setSuffix(this.getNamespace());
                for (final String content : contents)
                {
                    if (StringUtils.isNotBlank(content))
                    {
                        if (PathMatcher.wildcardMatch(
                                content,
                                resource.getPath()))
                        {
                            resourceUrl = ResourceUtils.getResource(
                                    content,
                                    this.getMergeLocation());
                            // - don't attempt to write the directories within the resource
                            if (!resourceUrl.toString().endsWith(FORWARD_SLASH))
                            {
                                this.writeResource(
                                    resource,
                                    resourceUrl);
                            }
                        }
                    }
                }
                AndroMDALogger.reset();
            }
        }
        else
        {
            this.writeResource(
                resource,
                resourceUrl);
        }
    }

    /**
     * The forward slash constant.
     */
    private static final String FORWARD_SLASH = "/";

    private static final String PATH_PATTERN = "\\*.*";

    /**
     * Writes the contents of <code>resourceUrl</code> to the outlet specified by <code>resource</code>.
     *
     * @param resource contains the outlet where the resource is written.
     * @param resourceUrl the URL contents to write.
     */
    private void writeResource(
        final Resource resource,
        final URL resourceUrl)
    {
        File outputFile = null;
        try
        {
            // - make sure we don't have any back slashes
            final String resourceUri = ResourceUtils.normalizePath(resourceUrl.toString());
            String uriSuffix = resource.getPath().replaceAll(PATH_PATTERN, "");
            if (resourceUri.contains(uriSuffix))
            {
                uriSuffix = resourceUri.substring(resourceUri.indexOf(uriSuffix) + uriSuffix.length(), resourceUri.length());
            }
            else
            {
                uriSuffix =
                    resourceUri.substring(
                        resourceUri.lastIndexOf(FORWARD_SLASH),
                        resourceUri.length());
            }

            final Map<String, Object> templateContext = new LinkedHashMap<String, Object>();
            this.populateTemplateContext(templateContext);

            // - if we have an outputCondition defined make sure it evaluates to true before continuing
            if (this.isValidOutputCondition(resource.getOutputCondition(), templateContext))
            {
                // - get the location and at the same time evaluate the outlet as a template engine variable (in case
                //   its defined as that).
                final String location =
                    Namespaces.instance().getPropertyValue(
                        this.getNamespace(),
                        this.getTemplateEngine().getEvaluatedExpression(
                            resource.getOutlet(),
                            templateContext));

                if (location != null)
                {
                    outputFile =
                        resource.getOutputLocation(
                            new String[] {uriSuffix},
                            new File(location),
                            this.getTemplateEngine().getEvaluatedExpression(
                                resource.getOutputPattern(),
                                templateContext));

                    final boolean lastModifiedCheck = resource.isLastModifiedCheck();
                    // - if we have the last modified check set, then make sure the last modified time is greater than the outputFile
                    if (!lastModifiedCheck || (lastModifiedCheck && ResourceUtils.getLastModifiedTime(resourceUrl) > outputFile.lastModified()))
                    {
                        // - only write files that do NOT exist, and
                        //   those that have overwrite set to 'true'
                        if (!outputFile.exists() || resource.isOverwrite())
                        {
                            ResourceWriter.instance().writeUrlToFile(
                                resourceUrl,
                                outputFile.toString());
                            AndroMDALogger.info("Output: '" + outputFile.toURI() + '\'');
                        }
                    }
                }
            }
        }
        catch (final Throwable throwable)
        {
            if (outputFile != null)
            {
                outputFile.delete();
                this.getLogger().info("Removed: '" + outputFile + '\'');
            }
            LOGGER.error("Error writing resource " + resource.getOutlet() + " to URL " + resourceUrl.toString(), throwable);
            //throw new CartridgeException(throwable);
        }
    }

    /**
     * Stores the loaded resources to be processed by this cartridge instance.
     */
    private final List<Resource> resources = new ArrayList<Resource>();

    /**
     * Returns the list of templates configured in this cartridge.
     *
     * @return List the template list.
     */
    public List<Resource> getResources()
    {
        return this.resources;
    }

    /**
     * Adds a resource to the list of defined resources.
     *
     * @param resource the new resource to add
     */
    public void addResource(final Resource resource)
    {
        ExceptionUtils.checkNull(
            "resource",
            resource);
        resource.setCartridge(this);
        resources.add(resource);
    }

    /**
     * Populates the <code>templateContext</code> with the properties and template objects defined in the
     * <code>plugin</code>'s descriptor. If the <code>templateContext</code> is null, a new Map instance will be created
     * before populating the context.
     *
     * @param templateContext the context of the template to populate.
     */
    protected void populateTemplateContext(Map<String, Object> templateContext)
    {
        super.populateTemplateContext(templateContext);
        templateContext.putAll(this.getEvaluatedConditions(templateContext));
    }

    /**
     * Stores the global conditions from cartridge.xml condition expressions
     */
    private final Map<String, String> conditions = new LinkedHashMap<String, String>();

    /**
     * Stores the evaluated conditions from cartridge.xml condition expressions
     */
    private final Map<String, Boolean> evaluatedConditions = new LinkedHashMap<String, Boolean>();

    /**
     * Stores the postProcessor from cartridge.xml configuration
     */
    private final Collection<PostProcessor> templatePostProcessor = new ArrayList<PostProcessor>();

    /**
     * Adds the outputCondition given the <code>name</code> and <code>value</code>
     * to the outputConditions map.
     *
     * @param name the name of the outputCondition.
     * @param value the value of the outputCondition.
     */
    public void addCondition(final String name, final String value)
    {
        this.conditions.put(name, StringUtils.trimToEmpty(value));
    }

    /**
     * Gets the current outputConditions defined within this cartridge
     * @return this.conditions
     */
    public Map<String, String> getConditions()
    {
        return this.conditions;
    }

    /**
     * Indicates whether or not the global outputConditions have been evaluated.
     */
    private boolean conditionsEvaluated = false;

    /**
     * Evaluates all conditions and stores the results in the <code>evaluatedConditions</code>
     * and returns that Map
     *
     * @param templateContext the template context used to evaluate the conditions.
     * @return the map containing the evaluated conditions.
     */
    private Map<String, Boolean> getEvaluatedConditions(final Map<String, Object> templateContext)
    {
        if (!this.conditionsEvaluated)
        {
            for (final Map.Entry<String, String> entry : conditions.entrySet())
            {
                final String value = entry.getValue();
                if (StringUtils.isNotBlank(value))
                {
                    final String evaluationResult = this.getTemplateEngine().getEvaluatedExpression(
                        value,
                        templateContext);
                    final String name = entry.getKey();
                    this.evaluatedConditions.put(name, BooleanUtils.toBoolean(evaluationResult));
                }
            }
            this.conditionsEvaluated = true;
        }
        return this.evaluatedConditions;
    }

    /**
     * Gets the evaluated outputCondition result of a global outputCondition.
     *
     * @param outputCondition the outputCondition to evaluate.
     * @param templateContext the current template context to pass the template engine if
     *        evaluation has yet to occur.
     * @return the evaluated outputCondition results.
     */
    private Boolean getGlobalConditionResult(final String outputCondition, final Map<String, Object> templateContext)
    {
        return this.getEvaluatedConditions(templateContext).get(outputCondition);
    }

    /**
     * Indicates whether or not the given <code>outputCondition</code> is a valid
     * outputCondition, that is, whether or not it returns true.
     *
     * @param outputCondition the outputCondition to evaluate.
     * @param templateContext the template context containing the variables to use.
     * @return true/false
     */
    private boolean isValidOutputCondition(final String outputCondition, final Map<String, Object> templateContext)
    {
        boolean validOutputCondition = true;
        if (StringUtils.isNotBlank(outputCondition))
        {
            Boolean result = this.getGlobalConditionResult(outputCondition, templateContext);
            if (result == null)
            {
                final String outputConditionResult = this.getTemplateEngine().getEvaluatedExpression(
                    outputCondition,
                    templateContext);
                result = outputConditionResult != null ? BooleanUtils.toBoolean(outputConditionResult.trim()) : null;
            }
            validOutputCondition = BooleanUtils.toBoolean(result);
        }
        return validOutputCondition;
    }

    /**
     * Gets the current postProcessors defined within this cartridge
     * @return this.postProcessors
     */
    public Collection<PostProcessor> getTemplatePostProcessor()
    {
        return this.templatePostProcessor;
    }

    /**
     * Adds new postProcessor to the cartridge
     * @param postProcessor new postProcessor
     */
    public void addTemplatePostProcessor(final TemplateObject postProcessor)
    {
        final PostProcessor lPostProcessor = (PostProcessor) postProcessor.getObject();
        this.templatePostProcessor.add(lPostProcessor);
    }

    /**
     * Override to provide cartridge specific shutdown (
     *
     * @see org.andromda.core.common.Plugin#shutdown()
     */
    public void shutdown()
    {
        super.shutdown();
        this.conditions.clear();
        this.evaluatedConditions.clear();
        this.templatePostProcessor.clear();
    }

    /**
     * @see Object#toString()
     */
    @Override
    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        builder.append(super.toString());
        //builder.append(" [resources=").append(this.resources); // StackOverflow
        builder.append(" [conditions=").append(this.conditions);
        builder.append(", evaluatedConditions=").append(this.evaluatedConditions);
        builder.append(", templatePostProcessor=").append(this.templatePostProcessor);
        builder.append(", conditionsEvaluated=").append(this.conditionsEvaluated).append("]");
        return builder.toString();
    }
}