org.gvnix.web.theme.roo.addon.ThemeOperationsImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.gvnix.web.theme.roo.addon.ThemeOperationsImpl.java

Source

/*
 * gvNIX. Spring Roo based RAD tool for Conselleria d'Infraestructures i
 * Transport - Generalitat Valenciana Copyright (C) 2010, 2011 CIT - Generalitat
 * Valenciana
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */
package org.gvnix.web.theme.roo.addon;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.xml.transform.Transformer;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.gvnix.web.theme.roo.addon.util.FileUtils;
import org.gvnix.web.theme.roo.addon.util.I18nUtils;
import org.gvnix.web.theme.roo.addon.util.XmlUtils;
import org.springframework.roo.addon.propfiles.PropFileOperations;
import org.springframework.roo.addon.web.mvc.jsp.JspOperationsImpl;
import org.springframework.roo.addon.web.mvc.jsp.i18n.I18n;
import org.springframework.roo.classpath.operations.AbstractOperations;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.process.manager.MutableFile;
import org.springframework.roo.project.Execution;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.Plugin;
import org.springframework.roo.project.ProjectMetadata;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.support.osgi.OSGiUtils;
import org.springframework.roo.support.util.DomUtils;
import org.springframework.roo.support.util.XmlElementBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.osgi.service.component.ComponentContext;

/**
 * Theme management operations for Spring MVC web layer:
 * <ul>
 * <li>Find theme repositories: bundles and local directories can act as theme
 * repository.
 * <ul>
 * <li>bundles let us to distribute themes via OBR.</li>
 * <li>local repository let us to share themes in one organization.</li>
 * </ul>
 * </li>
 * <li>Copy themes from repositories to project resources directory.</li>
 * <li>Set the project active theme</li>
 * </ul>
 * A theme is a directory that contains a set of MVC artefacts and having the
 * same directory structure than the structure created by Roo MVC add-ons
 * because a theme will overwrite default Roo MVC artefacts to set the desired
 * style.
 * <p>
 * A theme must contain the theme descriptor file,
 * [THEME_DIR]/WEB-INF/views/theme.xml. Only those directories that contains the
 * file /WEB-INF/views/theme.xml will be considered as valid themes.
 * 
 * @author Ricardo Garca at <a href="http://www.disid.com">DiSiD Technologies
 *         S.L.</a> made for <a href="http://www.cit.gva.es">Conselleria
 *         d'Infraestructures i Transport</a>
 * @author Enrique Ruiz (eruiz at disid dot com) at <a
 *         href="http://www.disid.com">DiSiD Technologies S.L.</a> made for <a
 *         href="http://www.cit.gva.es">Conselleria d'Infraestructures i
 *         Transport</a>
 * @author Oscar Rovira (orovira at disid dot com) at <a
 *         href="http://www.disid.com">DiSiD Technologies S.L.</a> made for <a
 *         href="http://www.cit.gva.es">Conselleria d'Infraestructures i
 *         Transport</a>
 * @since 0.6
 */
@Component
// use these Apache Felix annotations to register your commands class in the Roo
// container
@Service
public class ThemeOperationsImpl extends AbstractOperations implements ThemeOperations {

    private static final String SPRING_URL = "spring:url";
    private static final String FF3_AND_OPERA = " required for FF3 and Opera ";
    private static final String SCRIPT = "script";
    private static final String SRC = "src";
    private static final String TEXT_JSCRPT = "text/javascript";

    private static final String WEB_INF_THEME_FILE = "WEB-INF/views/theme.xml";

    private static final String HREF_ATTRIBUTE = "href";

    private static final String TYPE_ATTRIBUTE = "type";

    private static final String VAR_ATTRIBUTE = "var";

    private static final String VALUE_ATTRIBUTE = "value";

    private ComponentContext cContext;

    protected void activate(final ComponentContext componentContext) {
        cContext = componentContext;
        context = cContext.getBundleContext();
    }

    /**
     * MetadataService offers access to Roo's metadata model, use it to retrieve
     * any available metadate by its MID
     */
    @Reference
    private MetadataService metadataService;

    /**
     * Use ProjectOperations to install new dependencies, plugins, properties,
     * etc into the project configuration
     */
    @Reference
    private ProjectOperations projectOperations;

    /**
     * Use PropFileOperations for property file configuration operations.
     */
    @Reference
    private PropFileOperations propFileOperations;

    /** Themes installation path */
    private File themesPath = null;

    /** Themes repository path */
    private File themesRepositoryPath = null;

    private boolean isThemesAvailable = false;

    private boolean isSpringMvcTilesProject = false;

    // Public operations -----

    /** {@inheritDoc} */
    public boolean isProjectAvailable() {
        // Check if a project has been created
        return projectOperations.isProjectAvailable(projectOperations.getFocusedModuleName());
    }

    /**
     * {@inheritDoc}
     * <p>
     * Themes can be installed from bundles and local repository, so this method
     * checks if there are themes in OSGi bundles or in local repository
     * 
     * @return true if there are themes available in bundles or repository,
     *         otherwise returns false
     */
    public boolean isThemesAvailable() {
        if (isThemesAvailable) {
            return true;
        }
        isThemesAvailable = !findThemeDescriptors().isEmpty();
        return isThemesAvailable;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Do not permit installation unless they have a web project with Spring MVC
     * Tiles.
     */
    public boolean isSpringMvcTilesProject() {
        if (isSpringMvcTilesProject) {
            return true;
        }
        isSpringMvcTilesProject = fileManager.exists(getMvcConfigFile())
                && fileManager.exists(getTilesLayoutsFile());
        return isSpringMvcTilesProject;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method will copy the given theme located in a bundle or located in a
     * local themes repository to {@code Path.SRC_MAIN_RESOURCES/themes}. It
     * creates the directory if it doesn't exist.
     * <p>
     * Note that installation will overwrite installed themes. Useful to reset
     * installed themes.
     */
    public void installThemeArtefacts(String id) {
        StringUtils.isNotBlank(id);

        // Iterate over available themes and install the matching theme
        List<Theme> themes = getAvailableThemes();

        for (Theme theme : themes) {
            if (theme.getId().equals(id)) {

                // destination = [PROJECT-THEMES-PATH]/THEME-ID/
                File destination = new File(getThemesPath(), id);

                // get source URI from which get Theme artefacts
                URI sourceURI = theme.getRootURI();

                // it shouldn't occur
                Validate.notNull(sourceURI,
                        "Could not determine schema for resources for Theme '".concat(id).concat("'"));

                // copy overwriting installed themes
                copyRecursivelyURL(sourceURI, destination, true);

                // not needed to continue the iteration
                break;
            }
        }

    }

    /**
     * {@inheritDoc}
     * <p>
     * This activate method will copy the given theme from project's theme
     * folder {@link #SRC_MAIN_THEMES} to {@link Path#SRC_MAIN_WEBAPP} directory
     * and will update the theme.xml file.
     * <p>
     * {@link #updateProperties(Theme)} copy theme i18n messages to project
     * properties files.
     * <p>
     * Finally, {@link #updateSpringWebCtx(String)} updates
     * <em>webmvc-config.xml</em>.
     */
    public void setActive(String id) {

        List<Theme> themes = getInstalledThemes();

        Theme source = null;
        for (Theme theme : themes) {
            if (theme.getId().equals(id)) {
                source = theme;
            }
        }

        Validate.notNull(source, "Theme '".concat(id).concat("' isn't installed."));

        // destination = [PROJECT-THEMES-PATH]/THEME-ID/
        String destinationPath = getPathResolver().getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                "");
        File destination = new File(destinationPath);

        // before overwrite current web artifacts, load active languages. The
        // goal is to copy same languages to theme footer.jspx (if theme
        // contains
        // it)
        Set<I18n> languages = getInstalledI18n();

        // copy theme from project installation dir to src/main/webapp
        copyRecursivelyURI(source.getRootURI(), destination, true);

        // install application.css in styles folder
        installApplicationStyle();

        // modify load-scripts.tagx
        modifyLoadScriptsTagx();

        // install theme properties files
        updateProperties(source);

        // update webmvc-config.xml to set theme as default and add theme
        // message bundles locations
        updateSpringWebCtx(id);

        // when setting theme-cit modify pom.xml with required elements
        if (id.equalsIgnoreCase("cit")) {
            installApplicationVersionProperties();
            updatePomFile();
        }

        // update theme anchor to change to this theme
        updateThemeLinks(source);

        // update footer to add the active languages
        setInstalledI18n(languages);

    }

    /**
     * Install applicationversion.properties file in destination project
     * <p>
     * This file is used as resource bundle of application version number
     * message
     */
    private void installApplicationVersionProperties() {
        PathResolver pathResolver = projectOperations.getPathResolver();
        InputStream appVersionPropIS = org.springframework.roo.support.util.FileUtils.getInputStream(getClass(),
                "applicationversion-template.properties");
        String appVersionPropTemplate;
        try {
            appVersionPropTemplate = IOUtils.toString(new InputStreamReader(appVersionPropIS));
        } catch (IOException ioe) {
            throw new IllegalStateException("Unable load applicationversion-template.properties", ioe);
        } finally {
            try {
                appVersionPropIS.close();
            } catch (IOException e) {
                throw new IllegalStateException("Error creating jasperreports_extension.properties in project", e);
            }
        }

        String appVersionProp = pathResolver.getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, ""),
                "applicationversion.properties");
        fileManager.createOrUpdateTextFileIfRequired(appVersionProp, appVersionPropTemplate, false);
    }

    /**
     * Update pom.xml adding a plugin maven-resources-plugin or, if it exists,
     * just adding executions component
     * <p>
     * TODO
     * <p>
     * This source is so because Roo doesn't support Execution with
     * Configuration. When it support the thing we could change this code with
     * something like:
     * <code>projectOperations.updateBuildPlugin(pluginWithExecutionConfigured)</code>
     * This issue is solved in 1.2.0.M1 as ROO-2658 says
     */
    private void updatePomFile() {

        Element configuration = org.springframework.roo.support.util.XmlUtils.getConfiguration(getClass());
        Element pluginElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("/configuration/plugin", configuration);

        Plugin plugin = new Plugin(pluginElement);

        Element executionElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("executions/execution", pluginElement);
        Execution pluginExecution = createExecutionFromElement(executionElement);

        String pom = projectOperations.getPathResolver().getIdentifier(LogicalPath.getInstance(Path.ROOT, ""),
                "pom.xml");
        Document pomDoc = org.springframework.roo.support.util.XmlUtils.readXml(fileManager.getInputStream(pom));
        Element root = pomDoc.getDocumentElement();

        // Plugins section: find or create if not exists
        Element build = org.springframework.roo.support.util.XmlUtils.findFirstElement("/project/build", root);
        Element plugins = null;
        if (build != null) {
            plugins = org.springframework.roo.support.util.XmlUtils.findFirstElement("plugins", build);
            if (plugins != null) {
                String pluginXPath = "plugin[groupId='".concat(plugin.getGroupId()).concat("' and artifactId='")
                        .concat(plugin.getArtifactId()).concat("' and version='").concat(plugin.getVersion())
                        .concat("']");
                Element existingPluginElement = org.springframework.roo.support.util.XmlUtils
                        .findFirstElement(pluginXPath, plugins);
                if (existingPluginElement != null) {
                    String pluginExecutionXPath = "executions/execution[id='".concat(pluginExecution.getId())
                            .concat("' and phase='").concat(pluginExecution.getPhase()).concat("' and goals[goal='")
                            .concat(pluginExecution.getGoals().get(0)).concat("']]");
                    Element existingPluginExecutionElement = org.springframework.roo.support.util.XmlUtils
                            .findFirstElement(pluginExecutionXPath, existingPluginElement);
                    if (existingPluginExecutionElement == null) {
                        Element pluginExecutionsElement = org.springframework.roo.support.util.XmlUtils
                                .findFirstElement("executions", existingPluginElement);
                        if (pluginExecutionsElement == null) {
                            pluginExecutionsElement = new XmlElementBuilder("executions", pomDoc).build();
                        }
                        Node importedExecutionPlugin = pomDoc.importNode(executionElement, true);

                        pluginExecutionsElement.appendChild(importedExecutionPlugin);
                        existingPluginElement.appendChild(pluginExecutionsElement);
                    }
                } else {
                    // create maven-resources-plugin
                    Node importedPlugin = pomDoc.importNode(pluginElement, true);
                    plugins.appendChild(importedPlugin);
                }
            } else {
                // create plugins with maven-resources-plugin
                plugins = pomDoc.createElement("plugins");
                Node importedPlugin = pomDoc.importNode(pluginElement, true);
                plugins.appendChild(importedPlugin);
                build.appendChild(plugins);
            }
        } else {
            // create build and plugins with maven-resources-plugin
            build = pomDoc.createElement("build");
            plugins = pomDoc.createElement("plugins");
            Node importedPlugin = pomDoc.importNode(pluginElement, true);
            plugins.appendChild(importedPlugin);
            build.appendChild(plugins);
            root.appendChild(build);
        }

        fileManager.createOrUpdateTextFileIfRequired(pom,
                org.springframework.roo.support.util.XmlUtils.nodeToString(pomDoc), true);

    }

    /**
     * Returns the @org.springframework.roo.project.Execution instance from XML
     * Element representing it
     * 
     * @param executionElement
     * @return
     */
    private Execution createExecutionFromElement(Element executionElement) {
        String executionId = DomUtils.findFirstElementByName("id", executionElement).getTextContent();
        String executionPhase = DomUtils.findFirstElementByName("phase", executionElement).getTextContent();
        String executionGoal = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("goals/goal", executionElement).getTextContent();
        return new Execution(executionId, executionPhase, executionGoal);
    }

    /**
     * Installs application.css file in project styles folder
     */
    private void installApplicationStyle() {
        PathResolver pathResolver = projectOperations.getPathResolver();
        copyDirectoryContents("styles/*.css",
                pathResolver.getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "/styles"), false);
    }

    /**
     * Updates load-scripts.tagx adding in the right position some elements:
     * <ul>
     * <li><code>spring:url</code> elements for JS and CSS</li>
     * <li><code>link</code> element for CSS</li>
     * <li><code>script</code> element for JS</li>
     * </ul>
     */
    private void modifyLoadScriptsTagx() {
        PathResolver pathResolver = projectOperations.getPathResolver();
        String loadScriptsTagx = pathResolver.getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                "WEB-INF/tags/util/load-scripts.tagx");

        if (!fileManager.exists(loadScriptsTagx)) {
            // load-scripts.tagx doesn't exist, so nothing to do
            return;
        }

        InputStream loadScriptsIs = fileManager.getInputStream(loadScriptsTagx);

        Document loadScriptsXml;
        try {
            loadScriptsXml = org.springframework.roo.support.util.XmlUtils.getDocumentBuilder()
                    .parse(loadScriptsIs);
        } catch (Exception ex) {
            throw new IllegalStateException("Could not open load-scripts.tagx file", ex);
        }

        Element lsRoot = loadScriptsXml.getDocumentElement();
        // Add new tag namesapces
        Element jspRoot = org.springframework.roo.support.util.XmlUtils.findFirstElement("/root", lsRoot);
        jspRoot.setAttribute("xmlns:util", "urn:jsptagdir:/WEB-INF/tags/util");

        Node nextSibiling;

        // spring:url elements
        Element testElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("/root/url[@var='roo_css-ie_url']", lsRoot);
        if (testElement == null) {
            Element urlCitIECss = new XmlElementBuilder(SPRING_URL, loadScriptsXml)
                    .addAttribute(VALUE_ATTRIBUTE, "/resources/styles/cit-IE.css")
                    .addAttribute(VAR_ATTRIBUTE, "roo_css-ie_url").build();
            Element urlApplicationCss = new XmlElementBuilder(SPRING_URL, loadScriptsXml)
                    .addAttribute(VALUE_ATTRIBUTE, "/resources/styles/application.css")
                    .addAttribute(VAR_ATTRIBUTE, "application_css_url").build();
            Element urlYuiEventJs = new XmlElementBuilder(SPRING_URL, loadScriptsXml)
                    .addAttribute(VALUE_ATTRIBUTE, "/resources/scripts/yui/yahoo-dom-event.js")
                    .addAttribute(VAR_ATTRIBUTE, "yui_event").build();
            Element urlYuiCoreJs = new XmlElementBuilder(SPRING_URL, loadScriptsXml)
                    .addAttribute(VALUE_ATTRIBUTE, "/resources/scripts/yui/container_core-min.js")
                    .addAttribute(VAR_ATTRIBUTE, "yui_core").build();
            Element urlYoiMenuJs = new XmlElementBuilder(SPRING_URL, loadScriptsXml)
                    .addAttribute(VALUE_ATTRIBUTE, "/resources/scripts/yui/menu-min.js")
                    .addAttribute(VAR_ATTRIBUTE, "yui_menu").build();
            Element urlCitJs = new XmlElementBuilder(SPRING_URL, loadScriptsXml)
                    .addAttribute(VALUE_ATTRIBUTE, "/resources/scripts/utils.js")
                    .addAttribute(VAR_ATTRIBUTE, "cit_js_url").build();
            List<Element> springUrlElements = org.springframework.roo.support.util.XmlUtils
                    .findElements("/root/url", lsRoot);
            // Element lastSpringUrl = null;
            if (!springUrlElements.isEmpty()) {
                Element lastSpringUrl = springUrlElements.get(springUrlElements.size() - 1);
                if (lastSpringUrl != null) {
                    nextSibiling = lastSpringUrl.getNextSibling().getNextSibling();
                    lsRoot.insertBefore(urlCitIECss, nextSibiling);
                    lsRoot.insertBefore(urlApplicationCss, nextSibiling);
                    lsRoot.insertBefore(urlYuiEventJs, nextSibiling);
                    lsRoot.insertBefore(urlYuiCoreJs, nextSibiling);
                    lsRoot.insertBefore(urlYoiMenuJs, nextSibiling);
                    lsRoot.insertBefore(urlCitJs, nextSibiling);
                }
            } else {
                // Add at the end of the document
                lsRoot.appendChild(urlCitIECss);
                lsRoot.appendChild(urlApplicationCss);
                lsRoot.appendChild(urlYuiEventJs);
                lsRoot.appendChild(urlYuiCoreJs);
                lsRoot.appendChild(urlYoiMenuJs);
                lsRoot.appendChild(urlCitJs);
            }
        }

        Element setUserLocale = org.springframework.roo.support.util.XmlUtils.findFirstElement("/root/set/out",
                lsRoot);
        setUserLocale.setAttribute("default", "es");

        // cit-IE.css stylesheet element
        testElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("/root/iecondition/link[@href='${roo_css-ie_url}']", lsRoot);
        if (testElement == null) {
            Element ifIE = new XmlElementBuilder("util:iecondition", loadScriptsXml).build();

            Element linkCitIECss = new XmlElementBuilder("link", loadScriptsXml).addAttribute("rel", "stylesheet")
                    .addAttribute(TYPE_ATTRIBUTE, "text/css").addAttribute(HREF_ATTRIBUTE, "${roo_css-ie_url}")
                    .build();
            ifIE.appendChild(linkCitIECss);
            Node linkFaviconNode = org.springframework.roo.support.util.XmlUtils
                    .findFirstElement("/root/link[@href='${favicon}']", lsRoot);
            if (linkFaviconNode != null) {
                nextSibiling = linkFaviconNode.getNextSibling().getNextSibling();
                lsRoot.insertBefore(ifIE, linkFaviconNode);
            } else {
                // Add ass last link element
                // Element lastLink = null;
                List<Element> linkElements = org.springframework.roo.support.util.XmlUtils
                        .findElements("/root/link", lsRoot);
                if (!linkElements.isEmpty()) {
                    Element lastLink = linkElements.get(linkElements.size() - 1);
                    if (lastLink != null) {
                        nextSibiling = lastLink.getNextSibling().getNextSibling();
                        lsRoot.insertBefore(ifIE, nextSibiling);
                    }
                } else {
                    // Add at the end of document
                    lsRoot.appendChild(ifIE);
                }
            }
        }

        // pattern.css stylesheet element
        testElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("/root/link[@href='${application_css_url}']", lsRoot);
        if (testElement == null) {
            Element linkApplicationCss = new XmlElementBuilder("link", loadScriptsXml)
                    .addAttribute("rel", "stylesheet").addAttribute(TYPE_ATTRIBUTE, "text/css")
                    .addAttribute("media", "screen").addAttribute(HREF_ATTRIBUTE, "${application_css_url}").build();
            linkApplicationCss.appendChild(loadScriptsXml.createComment(FF3_AND_OPERA));
            Node linkFaviconNode = org.springframework.roo.support.util.XmlUtils
                    .findFirstElement("/root/link[@href='${favicon}']", lsRoot);
            if (linkFaviconNode != null) {
                nextSibiling = linkFaviconNode.getNextSibling().getNextSibling();
                lsRoot.insertBefore(linkApplicationCss, linkFaviconNode);
            } else {
                // Add ass last link element
                // Element lastLink = null;
                List<Element> linkElements = org.springframework.roo.support.util.XmlUtils
                        .findElements("/root/link", lsRoot);
                if (!linkElements.isEmpty()) {
                    Element lastLink = linkElements.get(linkElements.size() - 1);
                    if (lastLink != null) {
                        nextSibiling = lastLink.getNextSibling().getNextSibling();
                        lsRoot.insertBefore(linkApplicationCss, nextSibiling);
                    }
                } else {
                    // Add at the end of document
                    lsRoot.appendChild(linkApplicationCss);
                }
            }
        }

        // utils.js script element
        testElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("/root/script[@src='${cit_js_url}']", lsRoot);
        if (testElement == null) {
            Element scriptYuiEventJs = new XmlElementBuilder(SCRIPT, loadScriptsXml)
                    .addAttribute(SRC, "${yui_event}").addAttribute(TYPE_ATTRIBUTE, TEXT_JSCRPT).build();
            scriptYuiEventJs.appendChild(loadScriptsXml.createComment(FF3_AND_OPERA));
            Element scriptYuiCoreJs = new XmlElementBuilder(SCRIPT, loadScriptsXml).addAttribute(SRC, "${yui_core}")
                    .addAttribute(TYPE_ATTRIBUTE, TEXT_JSCRPT).build();
            scriptYuiCoreJs.appendChild(loadScriptsXml.createComment(FF3_AND_OPERA));
            Element scriptYoiMenuJs = new XmlElementBuilder(SCRIPT, loadScriptsXml).addAttribute(SRC, "${yui_menu}")
                    .addAttribute(TYPE_ATTRIBUTE, TEXT_JSCRPT).build();
            scriptYoiMenuJs.appendChild(loadScriptsXml.createComment(FF3_AND_OPERA));
            Element scriptCitJs = new XmlElementBuilder(SCRIPT, loadScriptsXml).addAttribute(SRC, "${cit_js_url}")
                    .addAttribute(TYPE_ATTRIBUTE, TEXT_JSCRPT).build();
            scriptCitJs.appendChild(loadScriptsXml.createComment(FF3_AND_OPERA));
            List<Element> scrtiptElements = org.springframework.roo.support.util.XmlUtils
                    .findElements("/root/script", lsRoot);
            // Element lastScript = null;
            if (!scrtiptElements.isEmpty()) {
                Element lastScript = scrtiptElements.get(scrtiptElements.size() - 1);
                if (lastScript != null) {
                    nextSibiling = lastScript.getNextSibling().getNextSibling();
                    lsRoot.insertBefore(scriptYuiEventJs, nextSibiling);
                    lsRoot.insertBefore(scriptYuiCoreJs, nextSibiling);
                    lsRoot.insertBefore(scriptYoiMenuJs, nextSibiling);
                    lsRoot.insertBefore(scriptCitJs, nextSibiling);
                }
            } else {
                // Add at the end of document
                lsRoot.appendChild(scriptYuiEventJs);
                lsRoot.appendChild(scriptYuiCoreJs);
                lsRoot.appendChild(scriptYoiMenuJs);
                lsRoot.appendChild(scriptCitJs);
            }
        }

        writeToDiskIfNecessary(loadScriptsTagx, loadScriptsXml.getDocumentElement());

    }

    /**
     * Decides if write to disk is needed (ie updated or created)<br/>
     * Used for TAGx files
     * 
     * @param filePath
     * @param body
     * @return
     */
    private boolean writeToDiskIfNecessary(String filePath, Element body) {
        // Build a string representation of the JSP
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Transformer transformer = org.springframework.roo.support.util.XmlUtils.createIndentingTransformer();
        org.springframework.roo.support.util.XmlUtils.writeXml(transformer, byteArrayOutputStream,
                body.getOwnerDocument());
        String viewContent = byteArrayOutputStream.toString();

        // If mutableFile becomes non-null, it means we need to use it to write
        // out the contents of jspContent to the file
        MutableFile mutableFile = null;
        if (fileManager.exists(filePath)) {
            // First verify if the file has even changed
            File newFile = new File(filePath);
            String existing = null;
            try {
                existing = IOUtils.toString(new FileReader(newFile));
            } catch (IOException ignoreAndJustOverwriteIt) {
                LOGGER.finest("Problems reading file".concat(filePath));
            }

            if (!viewContent.equals(existing)) {
                mutableFile = fileManager.updateFile(filePath);
            }
        } else {
            mutableFile = fileManager.createFile(filePath);
            Validate.notNull(mutableFile, "Could not create '" + filePath + "'");
        }

        if (mutableFile != null) {
            try {
                // We need to write the file out (it's a new file, or the
                // existing file has different contents)
                OutputStreamWriter outputStream = null;
                InputStream inputStream = null;
                try {
                    outputStream = new OutputStreamWriter(mutableFile.getOutputStream());
                    inputStream = IOUtils.toInputStream(viewContent);
                    IOUtils.copy(inputStream, outputStream);
                } finally {
                    IOUtils.closeQuietly(outputStream);
                    IOUtils.closeQuietly(inputStream);
                }

                // Return and indicate we wrote out the file
                return true;
            } catch (IOException ioe) {
                throw new IllegalStateException("Could not output '" + mutableFile.getCanonicalPath() + "'", ioe);
            }
        }

        // A file existed, but it contained the same content, so we return false
        return false;
    }

    /** {@inheritDoc} */
    public Theme getActiveTheme() {

        String activeThemePath = getPathResolver().getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                WEB_INF_THEME_FILE);
        File descriptor = new File(activeThemePath);
        Theme active = null;

        // check if there is an active theme
        if (descriptor.exists()) {
            active = Theme.parseTheme(descriptor.toURI());
            active.setActive(true);
        }

        return active;
    }

    /**
     * {@inheritDoc}
     * <p>
     * A theme must have the same directory structure than the structure created
     * by Roo MVC add-ons because a theme will overwrite default Roo MVC
     * artefacts to set the desired style.
     * <p>
     * A theme must contain the theme descriptor file,
     * [THEME_DIR]/WEB-INF/views/theme.xml. Only those directories that contains
     * it will be considered as valid themes.
     * <p>
     * The themes in local repository will have precedence over themes in OSGi
     * bundles. The reason is local repositories could contain custom themes.
     * 
     * @return Theme list
     */
    public List<Theme> getAvailableThemes() {
        List<Theme> themes = new ArrayList<Theme>();

        // find themes in bundles and local repository
        List<URL> urls = findThemeDescriptors();

        for (URL url : urls) {
            // load the theme
            Theme theme = Theme.parseTheme(url);
            theme.setAvailable(true);
            themes.add(theme);
        }

        return themes;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method includes the <em>Active Theme</em>:
     * <ul>
     * <li>If the active theme ID match with one of installed themes no new
     * Theme object will be added, the installed theme will be set as active.</li>
     * <li>If the developer customized the active theme, including the theme ID,
     * the active Theme is a new theme and it will be included in result List.
     * Note that in that case the active theme have the
     * {@link Theme#isInstalled()} method returns false</li>
     * </ul>
     */
    public List<Theme> getInstalledThemes() {
        List<Theme> themes = new ArrayList<Theme>();

        // 1st get the active theme
        Theme active = getActiveTheme();

        // 2nd iterate over project themes
        Set<URI> uris = findFileThemeDescriptorsURIs(getThemesPath());

        for (URI uri : uris) {
            // load the theme
            Theme theme = Theme.parseTheme(uri);
            theme.setInstalled(true);

            // if this installed theme is the active theme, just set it as
            // active
            if (active != null && theme.equals(active)) {
                theme.setActive(true);
                active.setInstalled(true); // mark as installed to avoid to add
                                           // again
            }

            themes.add(theme);
        }

        // finally if the active theme isn't in the set of installed themes
        // add it to List. This case occurs when the developer customizes the
        // activated theme, including the theme ID
        if (active != null && !active.isInstalled()) {
            themes.add(active);
        }

        return Collections.unmodifiableList(themes);
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method will iterate over installed themes checking each theme ID to
     * know if it is an available theme too. In that case, the available theme
     * won't be added to info, we will set availability flag of installed theme
     * to true.
     * <p>
     * Themes that are available and there aren't installed will be added to
     * info too.
     */
    public String getThemesInfo() {
        List<Theme> availableThemes = getAvailableThemes();
        List<Theme> installedThemes = getInstalledThemes();

        // Message builder.
        StringBuilder stringBuilder = new StringBuilder();

        // line of 72 chars
        stringBuilder.append("     ID          Avail  Instal  Active          Description\n");
        stringBuilder.append("---------------  -----  ------  ------  --------------------------------\n");

        // Iterate over installed themes
        for (Theme theme : installedThemes) {

            // remove the installed theme from available themes list and set
            // availability flag to true (it will avoid to show duplicate
            // themes)
            if (availableThemes.contains(theme)) {
                availableThemes.remove(theme);
                theme.setAvailable(true);
            }

            stringBuilder.append(getThemeInfo(theme));
        }

        // Iterate over remain available themes. Note the List doesn't have
        // the available themes that were installed
        for (Theme theme : availableThemes) {
            stringBuilder.append(getThemeInfo(theme));
        }

        return stringBuilder.toString();
    }

    // Private operations and utils -----

    /**
     * Utility method to get a formated string that shows complete Theme info.
     * This method is designed to be used from {@link #getThemesInfo()}. Theme
     * info:
     * <ul>
     * <li>Theme ID</li>
     * <li>Theme available to be installed in the project</li>
     * <li>Installed theme</li>
     * <li>Active theme</li>
     * </ul>
     * Note that the resulting String will have the same length, 72 chars.
     * 
     * @return Theme info
     */
    private String getThemeInfo(Theme theme) {
        // ID must be 15 chars length
        StringBuilder idStrBuilder = new StringBuilder("               ");

        String id = theme.getId();
        if (id.length() > 15) {
            idStrBuilder.replace(0, 15, id.substring(0, 15));
        } else {
            idStrBuilder.replace(0, id.length(), id);
        }

        // description could be null
        String description = theme.getDescription();
        if (description == null) {
            description = "";
        }

        // Message builder.
        StringBuilder stringBuilder = new StringBuilder();

        stringBuilder.append(idStrBuilder);
        stringBuilder.append("  ");
        stringBuilder.append(theme.isAvailable() ? " Yes " : " No  ");
        stringBuilder.append("  ");
        stringBuilder.append(theme.isInstalled() ? "  Yes " : "  No  ");
        stringBuilder.append("  ");
        stringBuilder.append(theme.isActive() ? "  Yes " : "  No  ");
        stringBuilder.append("  ");

        // tail description to 30 chars length
        stringBuilder.append(description.length() > 30 ? description.substring(0, 30) : description);
        stringBuilder.append("\n");

        return stringBuilder.toString();
    }

    /**
     * Find theme descriptors in OSGi bundles and local repository.
     * <p>
     * The themes in local repository will have precedence over themes in OSGi
     * bundles because local repositories could contain custom themes. That is,
     * if there are two themes with the same ID, one in the local repository and
     * the other in a bundle, the result Set will contain the theme descriptor
     * in the local repository.
     * 
     * @return UIRs to available theme descriptors "WEB-INF/views/theme.xml"
     */
    private List<URL> findThemeDescriptors() {

        // URLs to repository theme descriptors
        List<URL> urls = findFileThemeDescriptorsURLs(getThemesRepositoryPath());

        // URLs to theme descriptors in OSGi bundles
        urls.addAll(findBundleThemeDescriptors());

        return urls;
    }

    /**
     * Find theme descriptors in OSGi bundles.
     * 
     * @return URLs to theme descriptors "WEB-INF/views/theme.xml"
     */
    private Collection<URL> findBundleThemeDescriptors() {

        // URLs to theme descriptors in OSGi bundles
        return OSGiUtils.findEntriesByPattern(context, "/**/WEB-INF/views/theme.xml");
    }

    /**
     * Find theme descriptors in the given path. Use this utility to find themes
     * in the local repository or themes installed in the project, just give the
     * search path.
     * 
     * TODO Duplicated code When finding bundle use URL and when finding local
     * files use URI Avoid error "unknown protocol: bundle" on commands
     * 
     * @return URLs to theme descriptors "WEB-INF/views/theme.xml"
     */
    private List<URL> findFileThemeDescriptorsURLs(File path) {
        List<URL> urls = new ArrayList<URL>();

        // find themes in the local theme repository (if it exists)
        if (path == null) {
            // there isn't a local theme repository, return theme descriptors in
            // bundles
            return urls;
        }

        // get the list of theme dirs in the repository
        File[] themeDirs = path.listFiles();
        if (themeDirs == null) {
            // if null there isn't any installed theme : return empty set
            return urls;
        }

        // iterate over the set of theme directories in the repository
        for (File themeDir : themeDirs) {
            File descriptor = new File(themeDir.getAbsolutePath(), WEB_INF_THEME_FILE);
            if (themeDir.isDirectory() && descriptor.exists()) {
                try {
                    urls.add(new URL(descriptor.getAbsolutePath()));
                } catch (MalformedURLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
        return FileUtils.removeDuplicates(urls);
    }

    /**
     * Find theme descriptors in the given path. Use this utility to find themes
     * in the local repository or themes installed in the project, just give the
     * search path.
     * 
     * TODO Duplicated code When finding bundle use URL and when finding local
     * files use URI Avoid error "unknown protocol: bundle" on commands
     * 
     * @return URLs to theme descriptors "WEB-INF/views/theme.xml"
     */
    private Set<URI> findFileThemeDescriptorsURIs(File path) {
        Set<URI> uris = new HashSet<URI>();

        // find themes in the local theme repository (if it exists)
        if (path == null) {
            // there isn't a local theme repository, return theme descriptors in
            // bundles
            return uris;
        }

        // get the list of theme dirs in the repository
        File[] themeDirs = path.listFiles();
        if (themeDirs == null) {
            // if null there isn't any installed theme : return empty set
            return uris;
        }

        // iterate over the set of theme directories in the repository
        for (File themeDir : themeDirs) {
            File descriptor = new File(themeDir.getAbsolutePath(), WEB_INF_THEME_FILE);
            if (themeDir.isDirectory() && descriptor.exists()) {
                uris.add(descriptor.toURI());
            }
        }
        return uris;
    }

    /**
     * Utility to get {@link PathResolver} from {@link ProjectMetadata}.
     * <p>
     * This method will thrown if unavailable project metadata or unavailable
     * project path resolver.
     * 
     * @return PathResolver
     */
    private PathResolver getPathResolver() {
        ProjectMetadata projectMetadata = (ProjectMetadata) metadataService
                .get(ProjectMetadata.getProjectIdentifier(projectOperations.getFocusedModuleName()));
        Validate.notNull(projectMetadata, "Unable to obtain project metadata");

        // Use PathResolver to resolve between {@link File}, {@link Path} and
        // canonical path {@link String}s.
        // See {@link MavenPathResolver} to know location values
        PathResolver pathResolver = projectOperations.getPathResolver();
        Validate.notNull(projectMetadata, "Unable to obtain path resolver");

        return pathResolver;
    }

    /**
     * Returns the local repository that contains install themes. Search order:
     * <ol>
     * <li>System property 'theme.repository' (java -Dtheme.repository).</li>
     * <li>Environment variable 'THEME_REPOSITORY'.</li>
     * </ol>
     */
    private String getLocalRepositoryPath() {

        String path = null;

        // theme.repository
        path = System.getProperty("theme.repository");

        if (path != null) {
            if (!path.endsWith("/")) {
                path += "/";
            }
            return path;
        }

        // THEME_REPOSITORY
        path = System.getenv("THEME_REPOSITORY");

        if (path != null) {
            if (!path.endsWith("/")) {
                path += "/";
            }
            return path;
        }
        return path;
    }

    /**
     * This method will copy the contents of a bundle to a local directory if
     * the resource does not already exist in the target directory
     * 
     * TODO Duplicated code When finding bundle use URL and when finding local
     * files use URI Avoid error "unknown protocol: bundle" on commands
     * 
     * @param sourceDirectory source directory. URI syntax:
     *        [scheme:][//authority][path][?query][#fragment]
     * @param targetDirectory target directory
     * @param overwrite if true copy to target dir overwriting destination file
     * @see JspOperationsImpl#copyDirectoryContents(String, String)
     */
    @SuppressWarnings("unchecked")
    private void copyRecursivelyURL(URI sourceDirectory, File targetDirectory, boolean overwrite) {
        Validate.notNull(sourceDirectory, "Source URI required");
        Validate.notNull(targetDirectory, "Target directory required");

        // if source and target are the same dir, do nothing
        if (targetDirectory.toURI().equals(sourceDirectory)) {
            return;
        }

        if (!targetDirectory.exists()) {
            fileManager.createDirectory(targetDirectory.getAbsolutePath());
        }

        // Set of resource URLs to be copied to target dir
        List<URL> urls = new ArrayList<URL>();

        // if source URI schema is file:// , source files are in a local
        // repository
        if ("file".equals(sourceDirectory.getScheme())) {
            urls = FileUtils.findFilesURL(new File(sourceDirectory));
        }

        // if source URI schema is bundle:// , we can access to that bundle
        // (note the authority contains the bundle ID) and copy Theme
        // artefacts. URI example
        // bundle://8.0:0/org/gvnix/web/theme/roo/addon/themes/theme-cit/
        else if ("bundle".equals(sourceDirectory.getScheme())) {
            String uriAuthority = sourceDirectory.getAuthority();
            long bundleId = Long.parseLong(uriAuthority.substring(0, uriAuthority.indexOf(".")));

            // iterate over bundle entries in the given URI path and add them
            // to URLs to be copied to target dir
            Enumeration<URL> entries = context.getBundle(bundleId).findEntries(sourceDirectory.getPath(), "*.*",
                    true);
            while (entries.hasMoreElements()) {
                urls.add(entries.nextElement());
            }
        }
        // it shouldn't occur
        else {
            throw new IllegalArgumentException("Could not determine schema for resources for source dir '"
                    .concat(sourceDirectory.toString()).concat("'"));
        }

        Validate.notNull(urls, "No resources found to copy in '".concat(sourceDirectory.toString()).concat("'"));

        // remove duplicates on urls
        urls = FileUtils.removeDuplicates(urls);

        // iterate over Theme resources and copy them with same dir layout
        for (URL url : urls) {
            // Remove source directory prefix from absolute url: relative file
            // path
            String filePath = url.toString().substring(sourceDirectory.toString().length());
            if (isVersionControlSystemFile(filePath)) {
                // nothing to do if the URL is of a file from a Version Control
                // System
                continue;
            }
            File targetFile = new File(targetDirectory, filePath);

            try {
                // only copy files and if target file doesn't exist or overwrite
                // flag is true
                if (!targetFile.exists()) {

                    // create file using FileManager to fire creation events
                    InputStream inputStream = null;
                    OutputStream outputStream = null;
                    try {
                        inputStream = url.openStream();
                        outputStream = fileManager.createFile(targetFile.getAbsolutePath()).getOutputStream();
                        IOUtils.copy(inputStream, outputStream);
                    } finally {
                        IOUtils.closeQuietly(inputStream);
                        IOUtils.closeQuietly(outputStream);
                    }
                }
                // if file exists and overwrite is true, update the file
                else if (overwrite) {
                    InputStream inputStream = null;
                    OutputStream outputStream = null;
                    try {
                        inputStream = url.openStream();
                        outputStream = fileManager.updateFile(targetFile.getAbsolutePath()).getOutputStream();
                        IOUtils.copy(inputStream, outputStream);
                    } finally {
                        IOUtils.closeQuietly(inputStream);
                        IOUtils.closeQuietly(outputStream);
                    }
                }
            } catch (IOException e) {
                throw new IllegalStateException(
                        "Encountered an error during copying of resources for MVC Theme addon.", e);
            }
        }
    }

    /**
     * This method will copy the contents of a bundle to a local directory if
     * the resource does not already exist in the target directory
     * 
     * TODO Duplicated code When finding bundle use URL and when finding local
     * files use URI Avoid error "unknown protocol: bundle" on commands
     * 
     * @param sourceDirectory source directory. URI syntax:
     *        [scheme:][//authority][path][?query][#fragment]
     * @param targetDirectory target directory
     * @param overwrite if true copy to target dir overwriting destination file
     * @see JspOperationsImpl#copyDirectoryContents(String, String)
     */
    @SuppressWarnings("unchecked")
    private void copyRecursivelyURI(URI sourceDirectory, File targetDirectory, boolean overwrite) {
        Validate.notNull(sourceDirectory, "Source URI required");
        Validate.notNull(targetDirectory, "Target directory required");

        // if source and target are the same dir, do nothing
        if (targetDirectory.toURI().equals(sourceDirectory)) {
            return;
        }

        if (!targetDirectory.exists()) {
            fileManager.createDirectory(targetDirectory.getAbsolutePath());
        }

        // Set of resource URLs to be copied to target dir
        Set<URI> uris = new HashSet<URI>();

        // if source URI schema is file:// , source files are in a local
        // repository
        if ("file".equals(sourceDirectory.getScheme())) {
            uris = FileUtils.findFilesURI(new File(sourceDirectory));
        }

        // if source URI schema is bundle:// , we can access to that bundle
        // (note the authority contains the bundle ID) and copy Theme
        // artefacts. URI example
        // bundle://8.0:0/org/gvnix/web/theme/roo/addon/themes/theme-cit/
        else if ("bundle".equals(sourceDirectory.getScheme())) {
            String uriAuthority = sourceDirectory.getAuthority();
            long bundleId = Long.parseLong(uriAuthority.substring(0, uriAuthority.indexOf(".")));

            // iterate over bundle entries in the given URI path and add them
            // to URLs to be copied to target dir
            Enumeration<URL> entries = context.getBundle(bundleId).findEntries(sourceDirectory.getPath(), "*.*",
                    true);
            while (entries.hasMoreElements()) {
                try {
                    uris.add(entries.nextElement().toURI());
                } catch (URISyntaxException e) {
                    throw new IllegalStateException(
                            "Encountered an error during copying of resources for MVC Theme addon.", e);
                }
            }
        }
        // it shouldn't occur
        else {
            throw new IllegalArgumentException("Could not determine schema for resources for source dir '"
                    .concat(sourceDirectory.toString()).concat("'"));
        }

        Validate.notNull(uris, "No resources found to copy in '".concat(sourceDirectory.toString()).concat("'"));

        // iterate over Theme resources and copy them with same dir layout
        for (URI uri : uris) {
            // Remove source directory prefix from absolute url: relative file
            // path
            String filePath = uri.toString().substring(sourceDirectory.toString().length());
            if (isVersionControlSystemFile(filePath)) {
                // nothing to do if the URL is of a file from a Version Control
                // System
                continue;
            }
            File targetFile = new File(targetDirectory, filePath);

            try {
                // only copy files and if target file doesn't exist or overwrite
                // flag is true
                if (!targetFile.exists()) {

                    // create file using FileManager to fire creation events
                    InputStream inputStream = null;
                    OutputStream outputStream = null;
                    try {
                        inputStream = uri.toURL().openStream();
                        outputStream = fileManager.createFile(targetFile.getAbsolutePath()).getOutputStream();
                        IOUtils.copy(inputStream, outputStream);
                    } finally {
                        IOUtils.closeQuietly(inputStream);
                        IOUtils.closeQuietly(outputStream);
                    }
                }
                // if file exists and overwrite is true, update the file
                else if (overwrite) {
                    InputStream inputStream = null;
                    OutputStream outputStream = null;
                    try {
                        inputStream = uri.toURL().openStream();
                        outputStream = fileManager.updateFile(targetFile.getAbsolutePath()).getOutputStream();
                        IOUtils.copy(inputStream, outputStream);
                    } finally {
                        IOUtils.closeQuietly(inputStream);
                        IOUtils.closeQuietly(outputStream);
                    }
                }
            } catch (IOException e) {
                throw new IllegalStateException(
                        "Encountered an error during copying of resources for MVC Theme addon.", e);
            }
        }
    }

    /**
     * Says if a file path is the path of a Version Control System control file
     * <p>
     * Currently, we only check for SVN, GIT and CVS, but we could include these
     * other VCS file patterns:<br/>
     * <code>.hg .bzr MT _MTN .cdv .arch-ids .arch-inventory _darcs RCS</code>
     * 
     * @param filePath
     * @return
     */
    private boolean isVersionControlSystemFile(String filePath) {
        return filePath.contains(".svn") || filePath.contains(".git")
                || filePath.contains(File.separator.concat("CVS").concat(File.separator));
    }

    /**
     * Adds/Replaces the contents of the given Theme properties to the given
     * Theme properties file.
     * <p>
     * Note this method updates existing properties files only, it won't create
     * the file if it doesn't exist to avoid future addons cannot create their
     * files. Also note that {@link MessagesFileEventListener} will monitor that
     * creation in order to update properties if needed.
     * 
     * @param theme
     */
    public void updateProperties(Theme theme) {
        HashMap<String, Map<String, String>> bundles = theme.getPropertyBundles();

        for (Entry<String, Map<String, String>> entry : bundles.entrySet()) {

            // get the PATH_ID from key
            int index = entry.getKey().indexOf(":");
            LogicalPath path = LogicalPath.getInstance(entry.getKey().substring(0, index));

            // get filename from key
            String filename = entry.getKey().substring(index);

            // check file exists
            String filePath = getPathResolver().getIdentifier(path, filename);
            if (!fileManager.exists(filePath)) {
                continue;
            }

            // update properties file
            Map<String, String> properties = entry.getValue();
            propFileOperations.addProperties(path, filename, properties, true, true);
        }
    }

    /**
     * Update WEB-INF/spring/webmvc-config.xml:
     * <ul>
     * <li>Update bean 'themeResolver' to set the default theme to given theme
     * Id.</li>
     * <li>Update bean 'messageSource' to add theme localized messages basename.
     * </li>
     * <p>
     * Note this method uses {@link FileManager} for safe update.
     * <p>
     * TODO: Comprobar THEME-ID.properties existe
     * 
     * @param id
     */
    private void updateSpringWebCtx(String themeId) {

        String webMvc = getMvcConfigFile();
        Validate.isTrue(fileManager.exists(webMvc), "webmvc-config.xml not found; cannot continue");

        MutableFile mutableConfigXml = null;
        Document webConfigDoc;

        try {
            if (fileManager.exists(webMvc)) {
                mutableConfigXml = fileManager.updateFile(webMvc);
                webConfigDoc = org.springframework.roo.support.util.XmlUtils.getDocumentBuilder()
                        .parse(mutableConfigXml.getInputStream());
            } else {
                throw new IllegalStateException("Could not acquire ".concat(webMvc));
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        // Get themeResolver bean to change default theme
        Element resolverElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("//*[@id='themeResolver']", (Element) webConfigDoc.getFirstChild());

        // throw exception if themeResolver doesn't exist
        Validate.notNull(resolverElement, "Could not find bean 'themeResolver' in ".concat(webMvc));

        resolverElement.setAttribute("p:defaultThemeName", themeId);

        // Get messageSource to add theme messages basename
        Element msgSourceElement = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("//*[@id='messageSource']", (Element) webConfigDoc.getFirstChild());

        // throw exception if msgSourceElement doesn't exist
        Validate.notNull(msgSourceElement, "Could not find bean 'messageSource' in ".concat(webMvc));

        // The associated resource bundles will be checked sequentially when
        // resolving a message code.
        // We place theme basenames before the default ones to override ones in
        // a later bundle, due to the sequential lookup.
        String msgSourceBasenames = msgSourceElement.getAttribute("p:basenames");
        String sourceApplicationversion = "WEB-INF/i18n/theme/applicationversion";
        String finalMsgSourceBasenames = msgSourceBasenames;
        if (themeId.equalsIgnoreCase("cit") && !msgSourceBasenames.contains(sourceApplicationversion)) {
            // When setting theme-cit we want to show application version
            // number as a message
            finalMsgSourceBasenames = sourceApplicationversion.concat(",").concat(msgSourceBasenames);
        }
        String basenames = "WEB-INF/i18n/theme/messages,WEB-INF/i18n/theme/application";
        if (!msgSourceBasenames.contains(basenames)) {
            finalMsgSourceBasenames = basenames.concat(",").concat(finalMsgSourceBasenames);
        }
        msgSourceElement.setAttribute("p:basenames", finalMsgSourceBasenames);

        org.springframework.roo.support.util.XmlUtils.writeXml(mutableConfigXml.getOutputStream(), webConfigDoc);
    }

    /**
     * Add XML below to setup theme definition in theme.tagx:
     * 
     * <pre>
     *       <c:out value=" | " />
     *       <spring:url var="url_theme_gvnix" value="">
     *         <spring:param name="theme" value="THEME_ID" />
     *         <c:if test="${not empty param.page}">
     *           <spring:param name="page" value="${param.page}" />
     *         </c:if>
     *         <c:if test="${not empty param.size}">
     *           <spring:param name="size" value="${param.size}" />
     *         </c:if>
     *       </spring:url>
     *       <spring:message text="THEME_NAME" var="theme_THEME_ID" htmlEscape="false" />
     *       <a href="${url_theme_gvnix}"
     *         title="${fn:escapeXml(theme_THEME_ID)}">${fn:escapeXml(theme_THEME_ID)}</a>
     * </pre>
     */
    private void updateThemeLinks(Theme theme) {
        String tagxFileLocation = projectOperations.getPathResolver()
                .getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "/WEB-INF/tags/util/theme.tagx");
        MutableFile tagxFile = null;

        // load theme.tagx as XML Document
        Document tagxDocument = null;
        try {
            if (fileManager.exists(tagxFileLocation)) {
                tagxFile = fileManager.updateFile(tagxFileLocation);
                tagxDocument = XmlUtils.parseFile(tagxFile.getInputStream());
            } else {
                // do nothing, some web technologies (i.e. GWT) doesn't use
                // theme.tagx
                return;
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        Element root = tagxDocument.getDocumentElement();

        // locate Element that renders anchor to change to gvNIX theme
        Element themeEl = org.springframework.roo.support.util.XmlUtils
                .findFirstElement("//span/spring:url[@var='url_theme_gvnix']", root);

        // if not found, create the anchor for gvNIX theme
        if (null == themeEl) {
            Element span = org.springframework.roo.support.util.XmlUtils.findRequiredElement("//span", root);
            span.appendChild(
                    new XmlElementBuilder("c:out", tagxDocument).addAttribute(VALUE_ATTRIBUTE, " | ").build());

            // create gvNIX spring:url and add theme parameter
            Element gvnixUrlEl = new XmlElementBuilder(SPRING_URL, tagxDocument)
                    .addAttribute(VAR_ATTRIBUTE, "url_theme_gvnix").addAttribute(VALUE_ATTRIBUTE, "").build();
            gvnixUrlEl.appendChild(new XmlElementBuilder("spring:param", tagxDocument).addAttribute("name", "theme")
                    .addAttribute(VALUE_ATTRIBUTE, theme.getId()).build());

            // create conditional page parameter to add to gvNIX spring:url
            Element ifPageEl = new XmlElementBuilder("c:if", tagxDocument)
                    .addAttribute("test", "${not empty param.page}").build();
            Element paramPageEl = new XmlElementBuilder("spring:param", tagxDocument).addAttribute("name", "page")
                    .addAttribute(VALUE_ATTRIBUTE, "${param.page}").build();
            ifPageEl.appendChild(paramPageEl);

            gvnixUrlEl.appendChild(ifPageEl);

            // create conditional size parameter to add to gvNIX spring:url
            Element ifSizeEl = new XmlElementBuilder("c:if", tagxDocument)
                    .addAttribute("test", "${not empty param.size}").build();
            Element paramSizeEl = new XmlElementBuilder("spring:param", tagxDocument).addAttribute("name", "size")
                    .addAttribute(VALUE_ATTRIBUTE, "${param.size}").build();
            ifSizeEl.appendChild(paramSizeEl);

            gvnixUrlEl.appendChild(ifSizeEl);

            // add gvNIX spring:url to main <span> Element
            span.appendChild(gvnixUrlEl);

            // create the anchor to change to gvNIX Theme
            Element gvnixMsgEl = new XmlElementBuilder("spring:message", tagxDocument)
                    .addAttribute("text", theme.getName()).addAttribute(VAR_ATTRIBUTE, "theme_gvnix")
                    .addAttribute("htmlEscape", "false").build();
            span.appendChild(gvnixMsgEl);

            Element anchorEl = new XmlElementBuilder("a", tagxDocument)
                    .addAttribute(HREF_ATTRIBUTE, "${url_theme_gvnix}")
                    .addAttribute("title", "${fn:escapeXml(theme_gvnix)}").setText("${fn:escapeXml(theme_gvnix)}")
                    .build();
            span.appendChild(anchorEl);
        }
        // if found, update the parameter with given gvNIX theme ID
        else {
            themeEl.setAttribute("theme", theme.getId());

            // locate Element that sets the anchor message text
            Element gvnixMsgEl = org.springframework.roo.support.util.XmlUtils
                    .findFirstElement("//span/spring:message[@var='theme_gvnix']", root);
            gvnixMsgEl.setAttribute("text", theme.getName());
        }

        // update tagx
        org.springframework.roo.support.util.XmlUtils.writeXml(tagxFile.getOutputStream(), tagxDocument);
    }

    /**
     * Utility method to get project themes path.
     * <p>
     * The method initializes the {@link #themesPath} attribute if it is null.
     * 
     * @return
     */
    private File getThemesPath() {

        if (themesPath == null) {
            // resolve project installation directory for themes
            String root = getPathResolver()
                    .getRoot(LogicalPath.getInstance(Path.ROOT, projectOperations.getFocusedModuleName()));

            themesPath = new File(root, "src/main/themes");
        }

        return themesPath;
    }

    /**
     * Utility method to get local themes repository path.
     * 
     * @return
     */
    private File getThemesRepositoryPath() {

        if (themesRepositoryPath == null) {
            // load local themes repository path, it won't change in single
            // shell
            // execution. Note it is optional to have local repository
            String repoPath = getLocalRepositoryPath();
            if (StringUtils.isNotBlank(repoPath)) {
                themesRepositoryPath = new File(repoPath);
            }
        }
        return themesRepositoryPath;
    }

    /**
     * Get the absolute path for {@code webmvc-config.xml}.
     * 
     * @return the absolute path to file (never null)
     */
    private String getMvcConfigFile() {

        // resolve absolute path for menu.jspx if it hasn't been resolved yet
        return getPathResolver().getIdentifier(
                LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, projectOperations.getFocusedModuleName()),
                "/WEB-INF/spring/webmvc-config.xml");
    }

    /**
     * Get the absolute path for {@code layouts.xml}.
     * <p>
     * Note that this file is required for any Tiles project.
     * 
     * @return the absolute path to file (never null)
     */
    private String getTilesLayoutsFile() {

        // resolve absolute path for menu.jspx if it hasn't been resolved yet
        return getPathResolver().getIdentifier(
                LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, projectOperations.getFocusedModuleName()),
                "/WEB-INF/layouts/layouts.xml");
    }

    /**
     * Gets active languages in footer.jspx
     * 
     * @return
     */
    private Set<I18n> getInstalledI18n() {
        String targetDirectory = projectOperations.getPathResolver()
                .getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "");
        // Language definition
        String footerFileLocation = targetDirectory + "WEB-INF" + File.separator + "views" + File.separator
                + "footer.jspx";
        Document footer = null;
        try {
            if (fileManager.exists(footerFileLocation)) {
                footer = XmlUtils.parseFile(fileManager.getInputStream(footerFileLocation));
            } else {
                throw new IllegalStateException("Could not aquire the footer.jspx file");
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        // Iterate over language elements and create a Set of I18n objects that
        // contains current info at footer.jspx
        HashSet<I18n> result = new HashSet<I18n>();

        // get <span> and not <util:language> directly because there is
        // some problem with namespaces that cause cannot locate language
        // elements
        Element span = org.springframework.roo.support.util.XmlUtils.findFirstElement("//span[@id='language']",
                footer.getDocumentElement());

        // <span> contains language elements
        NodeList nodes = span.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);

            // if it is the element language, create the I18n instance to
            // contain
            // its info
            if (node.getNodeName().equals("util:language")) {
                Element langEl = (Element) node;
                I18n i18n = I18nUtils.createI18n(langEl.getAttribute("locale"), langEl.getAttribute("label"));
                result.add(i18n);
            }
        }
        return result;
    }

    /**
     * Add language definitions in footer.jspx
     * 
     * @param languages
     */
    private void setInstalledI18n(Set<I18n> languages) {
        if (languages.isEmpty()) {
            // nothing to do
            return;
        }
        String targetDirectory = projectOperations.getPathResolver()
                .getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "");

        // Language definition
        String footerFileLocation = targetDirectory + "/WEB-INF/views/footer.jspx";
        MutableFile footerFile = null;
        Document footer = null;
        try {
            if (fileManager.exists(footerFileLocation)) {
                footerFile = fileManager.updateFile(footerFileLocation);
                footer = org.springframework.roo.support.util.XmlUtils.getDocumentBuilder()
                        .parse(footerFile.getInputStream());
            } else {
                throw new IllegalStateException("Could not aquire the footer.jspx file");
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        // Iterate given languages to add in footer.jspx
        for (I18n i18n : languages) {

            // if language doesn't exist in footer.jspx, add it. Otherwise do
            // nothing
            if (null == org.springframework.roo.support.util.XmlUtils.findFirstElement(
                    "//span[@id='language']/language[@locale='".concat(i18n.getLocale().getLanguage()).concat("']"),
                    footer.getDocumentElement())) {

                Element span = org.springframework.roo.support.util.XmlUtils
                        .findRequiredElement("//span[@id='language']", footer.getDocumentElement());
                span.appendChild(new XmlElementBuilder("util:language", footer)
                        .addAttribute("locale", i18n.getLocale().getLanguage())
                        .addAttribute("label", i18n.getLanguage()).build());
            }
        }
        org.springframework.roo.support.util.XmlUtils.writeXml(footerFile.getOutputStream(), footer);
    }
}