org.gvnix.web.menu.roo.addon.MenuEntryOperationsImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.gvnix.web.menu.roo.addon.MenuEntryOperationsImpl.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.menu.roo.addon;

import java.io.ByteArrayOutputStream;
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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.gvnix.support.WebProjectUtils;
import org.gvnix.web.menu.roo.addon.listeners.MenuDependencyListener;
import org.gvnix.web.menu.roo.addon.util.FilenameUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.propfiles.PropFileOperations;
import org.springframework.roo.addon.web.mvc.jsp.i18n.I18n;
import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.process.manager.MutableFile;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.Property;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.XmlElementBuilder;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Implementation of operations this add-on offers
 * 
 * @author Jose Manuel Viv (jmvivo 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 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>
 * @since 0.6
 */
@Component
// use these Apache Felix annotations to register your commands class in the Roo
// container
@Service
public class MenuEntryOperationsImpl implements MenuEntryOperations {

    private static Logger logger = HandlerUtils.getLogger(MenuEntryOperationsImpl.class);

    private static final String ID_MENU_EXP = "//*[@id='_menu']";

    private static final String GVNIX_MENU = "gvnix-menu";

    private static final String INVALID_XML = "menu.xml hasn't valid XML structure.";

    private static final String ID_EXP = "//*[@id='";

    private static final String MENU_ITEM = "menu-item";

    private static final String LABEL_CODE = "labelCode";

    private static final String MESSAGE_CODE = "messageCode";

    private static final String URL = "url";

    private static final String HIDDEN = "hidden";

    private static final String NOT_FOUND = "' not found. [No changes done]";

    private static final String PAGE = "Page '";

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

    /**
     * Use FileManager to modify the underlying disk storage.
     */
    @Reference
    private FileManager fileManager;

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

    /** menu.jspx file path */
    private String menuFile;

    /** gvnixsubcategory */

    private String menuBootstrapFile;

    /** menu.xml file path */
    private String menuConfigFile;

    /**
     * Is OSGI Roo menu component already disabled ? In other words, is OSGI
     * gvNIX menu component already activated ?
     */
    private static boolean isRooMenuDisabled = false;

    /**
     * Uses to ensure that dependencyListener will be loaded
     */
    @Reference
    private MenuDependencyListener dependencyListener;

    private static final Logger LOGGER = HandlerUtils.getLogger(MenuEntryOperationsImpl.class);

    // ------------ OSGi component attributes ----------------
    private BundleContext context;

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

    // Public operations -----

    /**
     * Inform if roo menu operations must be disabled
     * 
     * @return
     */
    public static boolean isRooMenuDisabled() {
        return isRooMenuDisabled;
    }

    /**
     * Set {@link #isRooMenuDisabled} to true
     */
    private static void setRooMenuDisabled() {
        isRooMenuDisabled = true;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Note the project isn't available when you start a new project (emtpy
     * project dir) because the project metadata doesn't exist yet.
     */
    public boolean isProjectAvailable() {
        // Check if a project has been created
        return getProjectOperations().isProjectAvailable(getProjectOperations().getFocusedModuleName());
    }

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

    /** {@inheritDoc} */
    public boolean isGvNixMenuAvailable() {
        return fileManager.exists(getMenuConfigFile());
    }

    // Check if bootstrap is installed
    @Override
    public boolean isGvNixMenuBootstrapAvailable() {
        return fileManager.exists(getMenuBootstrapConfigFile());
    }

    /** {@inheritDoc} */
    public boolean isSpringSecurityInstalled() {

        if (!isProjectAvailable()) {
            // no project available yet, we cannot check for SS
            return false;
        }

        // create Spring Security dependency entity
        Dependency dep = new Dependency("org.springframework.security", "spring-security-core", "3.0.5.RELEASE");

        // locate Spring Security dependency
        Set<Dependency> dependencies = getProjectOperations().getFocusedModule()
                .getDependenciesExcludingVersion(dep);

        // if didn't find, Spring Security is not installed
        if (dependencies.isEmpty()) {
            return false;
        }
        return true;
    }

    /** {@inheritDoc} */
    public boolean isSpringSecurityInstalledOnMenuTag() {

        if (!isGvNixMenuAvailable()) {
            return false;
        }

        // Getting gvnixitem.tagx
        String targetPath = getPathResolver().getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                "/WEB-INF/tags/menu/gvnixitem.tagx");
        InputStream file = fileManager.getInputStream(targetPath);

        final Document tagxFile = XmlUtils.readXml(file);
        final Element root = tagxFile.getDocumentElement();

        if (StringUtils.isNotBlank(root.getAttribute("xmlns:sec"))) {
            return true;
        }

        return false;

    }

    /** {@inheritDoc} */
    public void setup() {
        // Parse the configuration.xml file
        Element configuration = XmlUtils.getConfiguration(getClass());

        // Add POM properties
        updatePomProperties(configuration);

        // Add dependencies to POM
        updateDependencies(configuration);

        // disable Roo MenuOperations to receive requests from clients
        // note we disable Roo MenuOp before start reading menu.jspx to avoid
        // clients create page whereas we are reading menu.jspx
        disableRooMenuOperations();

        // populate menu.xml from Roo menu.jspx
        createMenu();

        // create project menu entity model
        createEntityModel("~.web.menu");

        // create web layer artifacts
        createWebArtifacts("~.web.menu");
    }

    /** {@inheritDoc} */
    public void updateTags() {
        // Update web layer artfiacts
        updateWebArtifacts("~.web.menu");
    }

    @Override
    public void setupBootstrapMenu() {
        if (isSpringSecurityInstalled()) {
            installResourceIfNeeded("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem-bootstrap-sec.tagx", null,
                    new String[] { "<menu:gvnixitem", " xmlns:spring=", " xmlns:sec=" });
        } else {
            installResourceIfNeeded("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem-bootstrap.tagx", null,
                    new String[] { "<menu:gvnixitem" });
        }

        // Adding gvnixsubcategory
        installResourceIfNeeded("/WEB-INF/tags/menu/gvnixsubcategory.tagx", "gvnixsubcategory.tagx", null, null);

        // Adding subcategory.tagx
        installResourceIfNeeded("/WEB-INF/tags/menu/subcategory.tagx", "subcategory.tagx", null, null);

        // Adding gvNIX Menu styles with Bootstrap
        installResourceIfNeeded("/styles/menu/dropdown-submenu.css", "dropdown-submenu.css", null, null);

        // Adding new css to load scripts
        addStylesToLoadScriptBootstrap();
    }

    /**
     * Method to update bootstrap menu
     */
    public void updateBootstrapMenu() {
        if (isSpringSecurityInstalled()) {
            updateResource("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem-bootstrap-sec.tagx", null,
                    new String[] { "<menu:gvnixitem", " xmlns:spring=", " xmlns:sec=" });
        } else {
            updateResource("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem-bootstrap.tagx", null,
                    new String[] { "<menu:gvnixitem" });
        }

        // Adding gvnixsubcategory
        updateResource("/WEB-INF/tags/menu/gvnixsubcategory.tagx", "gvnixsubcategory.tagx", null, null);

        // Adding subcategory.tagx
        updateResource("/WEB-INF/tags/menu/subcategory.tagx", "subcategory.tagx", null, null);

        // Adding gvNIX Menu styles with Bootstrap
        updateResource("/styles/menu/dropdown-submenu.css", "dropdown-submenu.css", null, null);

        // Adding new css to load scripts
        addStylesToLoadScriptBootstrap();
    }

    /** {@inheritDoc} */
    public void addMenuItem(JavaSymbolName menuCategoryName, JavaSymbolName menuItemId, String globalMessageCode,
            String link, String idPrefix) {
        addMenuItem(menuCategoryName, menuItemId, "", globalMessageCode, link, idPrefix, null, false, false);
    }

    /** {@inheritDoc} */
    public void addMenuItem(JavaSymbolName menuCategoryName, JavaSymbolName menuItemId, String menuItemLabel,
            String globalMessageCode, String link, String idPrefix) {
        addMenuItem(menuCategoryName, menuItemId, menuItemLabel, globalMessageCode, link, idPrefix, null, false,
                true);
    }

    /** {@inheritDoc} */
    public String addMenuItem(JavaSymbolName menuCategoryName, JavaSymbolName menuItemId, String menuItemLabel,
            String globalMessageCode, String link, String idPrefix, String roles, boolean hide,
            boolean writeProps) {

        Validate.notNull(menuCategoryName, "Menu category name required");
        Validate.notNull(menuItemId, "Menu item name required");

        // Properties to be written
        Map<String, String> properties = new HashMap<String, String>();

        if (idPrefix == null || idPrefix.length() == 0) {
            idPrefix = MenuOperations.DEFAULT_MENU_ITEM_PREFIX;
        }

        Document document = getMenuDocument();

        // make the root element of the menu the one with the menu identifier
        // allowing for different decorations of menu
        Element rootElement = XmlUtils.findFirstElement(ID_MENU_EXP, (Element) document.getFirstChild());

        if (!rootElement.getNodeName().equals(GVNIX_MENU)) {
            throw new IllegalArgumentException(INVALID_XML);
        }

        // build category name and Id:
        // - menuCategoryName is a name if it doesn't start with c_: create the
        // id
        // - menuCategoryName is an identifier if it starts with c_: create the
        // name
        String categoryName = menuCategoryName.getSymbolName();
        StringBuilder categoryId = new StringBuilder();
        Element category = null;

        // check for existence of menu category by looking for the
        // identifier
        // provided

        // build category name and Id:
        // - menuCategoryName is a name if it doesn't start with c_: create
        // the
        // id
        // - menuCategoryName is an identifier if it starts with c_: create
        // the name
        // don't create any categoryId if there is already an id prefix
        if (!categoryName.startsWith(MenuEntryOperations.CATEGORY_MENU_ITEM_PREFIX)
                && !categoryName.startsWith(idPrefix)) {

            // create categoryId using the category name
            categoryId.append(MenuEntryOperations.CATEGORY_MENU_ITEM_PREFIX).append(categoryName.toLowerCase());
        } else {
            categoryId.append(categoryName.toLowerCase());
            // create category name using the category Id
            categoryName = StringUtils.capitalize(categoryName.substring(2));
        }

        List<Element> givenCategory = XmlUtils.findElements(ID_EXP.concat(categoryId.toString()).concat("']"),
                rootElement);

        // if given category not exists, create new one
        if (givenCategory.isEmpty()) {

            String categoryLabelCode = "menu_category_".concat(categoryName.toLowerCase()).concat("_label");

            category = (Element) rootElement.appendChild(new XmlElementBuilder(MENU_ITEM, document)
                    .addAttribute("id", categoryId.toString()).addAttribute("name", categoryName)
                    .addAttribute(LABEL_CODE, categoryLabelCode).build());

            properties.put(categoryLabelCode, menuCategoryName.getReadableSymbolName());
        } else {
            category = givenCategory.get(0);
        }

        // build menu item Id:
        // - if menu item id starts with 'i_', it is a valid ID but we remove
        // 'i_' for convenience
        // - otherwise, have to compose the ID
        StringBuilder itemId = new StringBuilder();

        if (menuItemId.getSymbolName().toLowerCase().startsWith(idPrefix)) {
            itemId.append(menuItemId.getSymbolName().toLowerCase());
            itemId.delete(0, idPrefix.length());
        } else {
            itemId.append(categoryName.toLowerCase()).append("_").append(menuItemId.getSymbolName().toLowerCase());
        }

        // check for existence of menu item by looking for the identifier
        // provided
        // Note that in view files, menu item ID has idPrefix_, but it doesn't
        // have
        // at application.properties, so we have to add idPrefix_ to look for
        // the given menu item but we have to add without idPrefix_ to
        // application.properties
        List<Element> menuList = XmlUtils
                .findElements(ID_EXP.concat(idPrefix).concat(itemId.toString()).concat("']"), rootElement);

        String itemLabelCode = "menu_item_".concat(itemId.toString()).concat("_label");

        if (menuList.isEmpty()) {
            Element menuItem = new XmlElementBuilder(MENU_ITEM, document)
                    .addAttribute("id", idPrefix.concat(itemId.toString())).addAttribute(LABEL_CODE, itemLabelCode)
                    .addAttribute(MESSAGE_CODE, StringUtils.isNotBlank(globalMessageCode) ? globalMessageCode : "")
                    .addAttribute(URL, StringUtils.isNotBlank(link) ? link : "")
                    .addAttribute(HIDDEN, Boolean.toString(hide))
                    .addAttribute("roles", StringUtils.isNotBlank(roles) ? roles : "").build();

            // TODO: gvnix*.tagx uses destination in spite of url, change to url
            category.appendChild(menuItem);
        }

        if (StringUtils.isNotBlank(menuItemLabel)) {
            properties.put(itemLabelCode, menuItemLabel);
        }

        if (writeProps) {
            propFileOperations.addProperties(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                    "/WEB-INF/i18n/application.properties", properties, true, false);
        }

        writeXMLConfigIfNeeded(document);

        // return the ID assigned to new entry
        return idPrefix.concat(itemId.toString());
    }

    /** {@inheritDoc} */
    public Document getMenuDocument() {
        Document menuDocument = null;

        // it could be menu.xml has to be installed
        if (!fileManager.exists(getMenuConfigFile())) {
            installResourceIfNeeded("/WEB-INF/views/menu.xml", "menu.xml", null, null);
        }

        InputStream menuIs = fileManager.getInputStream(getMenuConfigFile());
        menuDocument = org.gvnix.web.menu.roo.addon.util.XmlUtils.parseFile(menuIs);

        return menuDocument;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method uses MutableFile in combination with FileManager to take
     * advantage of Roo transactional file handling which offers automatic
     * rollback if an exception occurs.
     */
    public void writeXMLConfigIfNeeded(Document doc) {

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        XmlUtils.writeXml(XmlUtils.createIndentingTransformer(), byteArrayOutputStream, doc);

        // new content
        String proposed = 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(getMenuConfigFile())) {
            String original = null;

            try {
                // Current content to rollback to if an exception occur
                original = IOUtils.toString(new FileReader(getMenuConfigFile()));
            } catch (Exception e) {
                throw new IllegalStateException("Could not load file: ".concat(getMenuConfigFile()));
            }

            if (!proposed.equals(original)) {
                mutableFile = fileManager.updateFile(getMenuConfigFile());
            } else {
                // contents are equal, nothing to do
                return;
            }
        } else {
            mutableFile = fileManager.createFile(getMenuConfigFile());
            Validate.notNull(mutableFile, "Could not create file '".concat(getMenuConfigFile()).concat("'"));
        }

        try {
            if (mutableFile != null) {
                InputStream inputStream = null;
                OutputStreamWriter outputStream = null;
                try {
                    inputStream = IOUtils.toInputStream(proposed);
                    outputStream = new OutputStreamWriter(mutableFile.getOutputStream());
                    IOUtils.copy(inputStream, outputStream);
                } finally {
                    IOUtils.closeQuietly(inputStream);
                    IOUtils.closeQuietly(outputStream);
                }
            }
        } catch (IOException ioe) {
            throw new IllegalStateException("Could not output '".concat(mutableFile.getCanonicalPath()).concat("'"),
                    ioe);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * Update the entry ID could change entry type because category entry starts
     * with 'c_' prefix, item entry starts with 'i_' prefix, so to change a
     * category entry to item entry you have to set a new ID that starts with
     * 'i_'.
     */
    public void updateEntry(JavaSymbolName pageId, JavaSymbolName nid, String label, String messageCode,
            String destination, String roles, Boolean hidden, boolean writeProps) {
        Document document = getMenuDocument();

        // Properties to be writen
        Map<String, String> properties = new HashMap<String, String>();

        // make the root element of the menu the one with the menu identifier
        // allowing for different decorations of menu
        Element rootElement = XmlUtils.findFirstElement(ID_MENU_EXP, (Element) document.getFirstChild());

        if (!rootElement.getNodeName().equals(GVNIX_MENU)) {
            throw new IllegalArgumentException(INVALID_XML);
        }

        // check for existence of menu category by looking for the identifier
        // provided
        Element pageElement = XmlUtils.findFirstElement(ID_EXP.concat(pageId.getSymbolName()).concat("']"),
                rootElement);

        // exit if menu entry doesn't exist
        Validate.notNull(pageElement, "Menu entry '".concat(pageId.getSymbolName()).concat(NOT_FOUND));

        if (nid != null) {
            pageElement.setAttribute("id", nid.getSymbolName());

            // TODO: if Element has children, children IDs should be
            // recalculated too
            // TODO: label code should change too (as addMenuItem does)
        }

        if (StringUtils.isNotBlank(label)) {
            String itemLabelCode = pageElement.getAttribute(LABEL_CODE);
            properties.put(itemLabelCode, label);
        }

        if (writeProps) {
            propFileOperations.addProperties(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                    "/WEB-INF/i18n/application.properties", properties, true, true);
        }

        if (StringUtils.isNotBlank(messageCode)) {
            pageElement.setAttribute(MESSAGE_CODE, messageCode);
        }

        if (StringUtils.isNotBlank(destination)) {
            pageElement.setAttribute(URL, destination);
        }

        if (StringUtils.isNotBlank(roles)) {
            pageElement.setAttribute("roles", roles);
        }

        if (hidden != null) {
            pageElement.setAttribute(HIDDEN, hidden.toString());
        }

        writeXMLConfigIfNeeded(document);
    }

    /** {@inheritDoc} */
    public String getFormatedInfo(JavaSymbolName pageId, I18n lang) {
        Validate.notNull(pageId, "Menu entry ID required");
        return getFormatedInfo(pageId, true, true, true, lang);
    }

    /** {@inheritDoc} */
    public String getCompactInfo(JavaSymbolName pageId) {
        Element rootElement = getMenuRootElement();

        // if no entry selected, show the info of all 1st level Elements
        if (pageId == null) {
            return getCompactInfo(rootElement.getChildNodes(), 0);
        }

        // check for existence of menu category by looking for the identifier
        // provided
        Element pageElement = XmlUtils.findFirstElement(ID_EXP.concat(pageId.getSymbolName()).concat("']"),
                rootElement);

        // if selected entry doesn't exist, error
        Validate.notNull(pageElement, PAGE.concat(pageId.getSymbolName()).concat("' not found [No info found]"));

        // show the info of selected menu entry
        return getCompactInfo(pageElement, 0);
    }

    /**
     * Iterates over a list of menu entry Nodes and call
     * {@link #getCompactInfo(Element, int)} to get the info of all the menu
     * entry Nodes in the given list.
     * 
     * @param nodes
     * @param tabSize
     * @return
     */
    private String getCompactInfo(NodeList nodes, int tabSize) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);

            // filter nodes that aren't menuItems
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            String nodeName = node.getNodeName();
            if (!nodeName.equals(MENU_ITEM)) {
                continue;
            }

            builder.append(getCompactInfo((Element) node, tabSize));
        }
        return builder.toString();
    }

    /**
     * Shows menu info in compact mode: <br/>
     * <code>
     * &nbsp;&nbsp;/tribunales  [c_tribunales, visible]<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;/tribunales?form  [i_tribunales_new, visible]<br/>
     * &nbsp;&nbsp;&nbsp;&nbsp;/tribunales?page=1&size=${empty param.size ? 10 : param.size}  [i_tribunales_list, hidden]<br/>
     * </code>
     * 
     * @param element
     * @param tabSize
     * @return
     */
    private String getCompactInfo(Element element, int tabSize) {
        StringBuilder builder = new StringBuilder();
        StringBuilder indent = new StringBuilder();

        // tab string to align children
        for (int i = 0; i < tabSize; i++) {
            indent.append(" ");
        }

        String url = element.getAttribute(URL);
        if (StringUtils.isNotBlank(url)) {
            builder.append(indent).append(url).append("  ");
        }

        // string containing "[ID, visibility]: "
        StringBuilder idVisibility = new StringBuilder();
        idVisibility.append("[").append(element.getAttribute("id"));
        String hidden = element.getAttribute(HIDDEN);
        if (!StringUtils.isNotBlank(hidden)) {
            hidden = "false"; // visible by default
        }
        if (Boolean.valueOf(hidden)) {
            idVisibility.append(", hidden");
        } else {
            idVisibility.append(", visible");
        }
        if (!StringUtils.isNotBlank(url)) {
            idVisibility.append(", no-URL");
        }
        idVisibility.append("]");

        // build Element info
        builder.append(idVisibility);

        // get children info
        if (element.hasChildNodes()) {
            builder.append("\n").append(getCompactInfo(element.getChildNodes(), tabSize + 10)).append("\n");
        } else {
            builder.append("\n"); // empty line
        }
        return builder.toString();
    }

    /** {@inheritDoc} */
    public String getFormatedInfo(JavaSymbolName pageId, boolean label, boolean messageCode, boolean roles,
            I18n lang) {
        Element rootElement = getMenuRootElement();

        // if no entry selected, show the info of all 1st level Elements
        if (pageId == null) {
            return getFormatedInfo(rootElement.getChildNodes(), label, messageCode, roles, lang, 0);
        }

        // check for existence of menu category by looking for the identifier
        // provided
        Element pageElement = XmlUtils.findFirstElement(ID_EXP.concat(pageId.getSymbolName()).concat("']"),
                rootElement);

        // if selected entry doesn't exist, error
        Validate.notNull(pageElement, PAGE.concat(pageId.getSymbolName()).concat("' not found [No info found]"));

        // show the info of selected menu entry
        return getFormatedInfo(pageElement, label, messageCode, roles, lang, 0);
    }

    /**
     * Returns the Root element of the menu.xml file
     * 
     * @return
     */
    private Element getMenuRootElement() {
        Document document = getMenuDocument();

        // make the root element of the menu the one with the menu identifier
        // allowing for different decorations of menu
        Element rootElement = XmlUtils.findFirstElement(ID_MENU_EXP, (Element) document.getFirstChild());

        if (!rootElement.getNodeName().equals(GVNIX_MENU)) {
            throw new IllegalArgumentException(INVALID_XML);
        }

        return rootElement;
    }

    /** {@inheritDoc} */
    public void moveBefore(JavaSymbolName pageId, JavaSymbolName beforeId) {
        Document document = getMenuDocument();

        // make the root element of the menu the one with the menu identifier
        // allowing for different decorations of menu
        Element rootElement = XmlUtils.findFirstElement(ID_MENU_EXP, (Element) document.getFirstChild());

        if (!rootElement.getNodeName().equals(GVNIX_MENU)) {
            throw new IllegalArgumentException(INVALID_XML);
        }

        // check for existence of menu category by looking for the identifier
        // provided
        Element pageElement = XmlUtils.findFirstElement(ID_EXP.concat(pageId.getSymbolName()).concat("']"),
                rootElement);

        // exit if menu entry doesn't exist
        Validate.notNull(pageElement, PAGE.concat(pageId.getSymbolName()).concat(NOT_FOUND));

        Element beforeElement = XmlUtils.findFirstElement(ID_EXP.concat(beforeId.getSymbolName()).concat("']"),
                rootElement);

        // exit if menu entry doesn't exist
        Validate.notNull(beforeElement, PAGE.concat(beforeId.getSymbolName()).concat(NOT_FOUND));

        // page parent element where remove menu entry element
        Element pageParentEl = (Element) pageElement.getParentNode();
        pageParentEl.removeChild(pageElement);

        // before parent element where execute insert before
        Element beforeParentEl = (Element) beforeElement.getParentNode();
        beforeParentEl.insertBefore(pageElement, beforeElement);

        writeXMLConfigIfNeeded(document);
    }

    /** {@inheritDoc} */
    public void moveInto(JavaSymbolName pageId, JavaSymbolName intoId) {
        Document document = getMenuDocument();

        // make the root element of the menu the one with the menu identifier
        // allowing for different decorations of menu
        Element rootElement = XmlUtils.findFirstElement(ID_MENU_EXP, (Element) document.getFirstChild());

        if (!rootElement.getNodeName().equals(GVNIX_MENU)) {
            throw new IllegalArgumentException(INVALID_XML);
        }

        // check for existence of menu category by looking for the identifier
        // provided
        Element pageElement = XmlUtils.findFirstElement(ID_EXP.concat(pageId.getSymbolName()).concat("']"),
                rootElement);

        // exit if menu entry doesn't exist
        Validate.notNull(pageElement, PAGE.concat(pageId.getSymbolName()).concat(NOT_FOUND));

        Element intoElement = XmlUtils.findFirstElement(ID_EXP.concat(intoId.getSymbolName()).concat("']"),
                rootElement);

        // exit if menu entry doesn't exist
        Validate.notNull(intoElement, PAGE.concat(intoId.getSymbolName()).concat(NOT_FOUND));

        // parent element where remove menu entry element
        Element parent = (Element) pageElement.getParentNode();
        parent.removeChild(pageElement);

        // insert
        intoElement.appendChild(pageElement);

        writeXMLConfigIfNeeded(document);
    }

    /** {@inheritDoc} */
    public String getMenuConfigFile() {

        // resolve path for menu.xml if it hasn't been resolved yet
        if (menuConfigFile == null) {
            menuConfigFile = getPathResolver().getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                    "/WEB-INF/views/menu.xml");
        }
        return menuConfigFile;
    }

    /** {@inheritDoc} */
    public String getMenuBootstrapConfigFile() {

        // resolve path for gvnixsubcategory.tagx if it hasn't been resolved yet
        if (menuBootstrapFile == null) {
            menuBootstrapFile = getPathResolver().getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                    "/WEB-INF/tags/menu/gvnixsubcategory.tagx");
        }
        return menuBootstrapFile;
    }

    /**
     * Get and initialize the absolute path for the {@code menu.jspx}.
     * 
     * @return the absolute path to the file (never null)
     */
    public String getMenuFile() {

        // resolve absolute path for menu.jspx if it hasn't been resolved yet
        if (menuFile == null) {
            menuFile = getPathResolver().getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                    "/WEB-INF/views/menu.jspx");
        }
        return menuFile;
    }

    // Private operations and utils -----

    /**
     * Utility to get {@link PathResolver}.
     * 
     * @return PathResolver or null if project isn't available yet
     */
    private PathResolver getPathResolver() {

        // Use PathResolver to resolve between {@link File}, {@link Path} and
        // canonical path {@link String}s.
        // See {@link MavenPathResolver} to know location values
        PathResolver pathResolver = getProjectOperations().getPathResolver();

        return pathResolver;
    }

    /**
     * Create Menu model that lets the project works with the underlying menu
     * structure representation, i.e, menu.xml
     * 
     * @param targetPackage Java entities will be created inside this package
     */
    protected void createEntityModel(String targetPackage) {

        installIfNeeded("MenuItem.java", targetPackage);
        installIfNeeded("Menu.java", targetPackage);
        installIfNeeded("MenuLoader.java", targetPackage);
        installIfNeeded("ContextMenuStrategy.java", targetPackage);
        installIfNeeded("BaseURLContextMenuStrategy.java", targetPackage);
        installIfNeeded("URLBrothersContextMenuStrategy.java", targetPackage);
        installIfNeeded("URLChildrenContextMenuStrategy.java", targetPackage);
    }

    /** {@inheritDoc} */
    public void createWebArtifacts(String classesPackage) {

        // parameters Map to replace variables in file templates
        Map<String, String> params = new HashMap<String, String>();

        // resolve given classes package
        String javaPackage = getFullyQualifiedPackageName(classesPackage);

        // Put variable values in parameters Map
        params.put("__TOP_LEVEL_PACKAGE__", getProjectOperations()
                .getTopLevelPackage(getProjectOperations().getFocusedModuleName()).getFullyQualifiedPackageName());
        params.put("__MENU_MODEL_CLASS__", javaPackage.concat(".Menu"));

        // install tags
        installResourceIfNeeded("/WEB-INF/tags/menu/gvnixmenu.tagx", "gvnixmenu.tagx", params,
                new String[] { "<menu:gvnixitem" });

        // Check if bootstrap is installed
        if (getProjectOperations().isFeatureInstalledInFocusedModule("gvnix-bootstrap")) {

            if (!isGvNixMenuBootstrapAvailable()) {
                // Installing gvnixitem and gvnixitem-sec with Bootstrap
                setupBootstrapMenu();
            }
        } else {

            // Installing gvnixitem and gvnixitem-sec without Bootstrap
            if (isSpringSecurityInstalled()) {
                installResourceIfNeeded("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem-sec.tagx", null,
                        new String[] { "<menu:gvnixitem", " xmlns:spring=", " xmlns:sec=" });
            } else {
                installResourceIfNeeded("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem.tagx", null,
                        new String[] { "<menu:gvnixitem" });
            }
        }

        // change menu.jspx to use gvnix menu tag that will render menu.xml
        installResourceIfNeeded("/WEB-INF/views/menu.jspx", "menu.jspx", null, new String[] { "menu:gvnixmenu" });
    }

    /** {@inheritDoc} */
    public void updateWebArtifacts(String classesPackage) {

        // parameters Map to replace variables in file templates
        Map<String, String> params = new HashMap<String, String>();

        // resolve given classes package
        String javaPackage = getFullyQualifiedPackageName(classesPackage);

        // Put variable values in parameters Map
        params.put("__TOP_LEVEL_PACKAGE__", getProjectOperations()
                .getTopLevelPackage(getProjectOperations().getFocusedModuleName()).getFullyQualifiedPackageName());
        params.put("__MENU_MODEL_CLASS__", javaPackage.concat(".Menu"));

        // update tags
        updateResource("/WEB-INF/tags/menu/gvnixmenu.tagx", "gvnixmenu.tagx", params,
                new String[] { "<menu:gvnixitem" });

        // Check if bootstrap is installed
        if (getProjectOperations().isFeatureInstalledInFocusedModule("gvnix-bootstrap")) {
            // Updating gvnixitem and gvnixitem-sec with Bootstrap
            updateBootstrapMenu();
        } else {

            // Installing gvnixitem and gvnixitem-sec without Bootstrap
            if (isSpringSecurityInstalled()) {
                updateResource("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem-sec.tagx", null,
                        new String[] { "<menu:gvnixitem", " xmlns:spring=", " xmlns:sec=" });
            } else {
                updateResource("/WEB-INF/tags/menu/gvnixitem.tagx", "gvnixitem.tagx", null,
                        new String[] { "<menu:gvnixitem" });
            }
        }

        // change menu.jspx to use gvnix menu tag that will render menu.xml
        updateResource("/WEB-INF/views/menu.jspx", "menu.jspx", null, new String[] { "menu:gvnixmenu" });
    }

    /**
     * Install a file template if it doesn't exist
     * <p>
     * This method has been copied from Maven addon, maybe it could be
     * refactored to utility class.
     * 
     * @param targetFilename File to create. Note this method will create the
     *        file by locating a file template with the same name as target file
     *        but prefixing "-template" to file extension. For example, given
     *        {@code Menu.java} will locate {@code Menu-template.java}
     */
    private void installIfNeeded(String targetFilename, String targetPackage) {

        String destinationFile = getAbsolutePath(targetPackage, targetFilename);

        if (!fileManager.exists(destinationFile)) {
            try {
                // Read template and insert the user's package
                String fileName = FilenameUtils.removeExtension(targetFilename);
                String fileExt = FilenameUtils.getExtension(targetFilename);

                InputStream templateInputStream = FileUtils.getInputStream(getClass(),
                        fileName.concat("-template").concat(".").concat(fileExt));
                String input = IOUtils.toString(new InputStreamReader(templateInputStream));

                // replace template variables
                input = input.replace("__TOP_LEVEL_PACKAGE__",
                        getProjectOperations().getTopLevelPackage(getProjectOperations().getFocusedModuleName())
                                .getFullyQualifiedPackageName());

                // Output the file for the user
                // use MutableFile in combination with FileManager to take
                // advantage of
                // Roos transactional file handling which offers automatic
                // rollback if an
                // exception occurs
                MutableFile mutableFile = fileManager.createFile(destinationFile);
                InputStream inputStream = null;
                OutputStream outputStream = null;
                try {
                    inputStream = IOUtils.toInputStream(input);
                    outputStream = mutableFile.getOutputStream();
                    IOUtils.copy(inputStream, outputStream);
                } finally {
                    IOUtils.closeQuietly(inputStream);
                    IOUtils.closeQuietly(outputStream);
                }
            } catch (IOException ioe) {
                throw new IllegalStateException("Unable to create '".concat(targetFilename).concat("'"), ioe);
            }
        }
    }

    /**
     * Creates or updates the contents for one resource represented by the given
     * target file path and relative source path.
     * 
     * @param relativePath path relative to {@link Path.SRC_MAIN_WEBAPP} of
     *        target file
     * @param resourceName path relative to classpath of file to be copied
     *        (cannot be null)
     * @param toReplace
     * @param containsStrings
     */
    private void installResourceIfNeeded(String relativePath, String resourceName, Map<String, String> toReplace,
            String[] containsStrings) {
        PathResolver pathResolver = getPathResolver();

        String targetPath = pathResolver.getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                relativePath);

        InputStream resource = getClass().getResourceAsStream(resourceName);

        String sourceContents;
        String targetContents = null;

        // load resource to copy
        try {
            sourceContents = IOUtils.toString(new InputStreamReader(resource));
            // Replace params
            if (toReplace != null) {
                for (Entry<String, String> entry : toReplace.entrySet()) {
                    sourceContents = StringUtils.replace(sourceContents, entry.getKey(), entry.getValue());
                }
            }
        } catch (IOException e) {
            throw new IllegalStateException("Unable to load file to be copied '".concat(resourceName).concat("'"),
                    e);
        } finally {
            try {
                resource.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // load target contents if exists
        if (fileManager.exists(targetPath)) {
            FileReader reader = null;
            try {
                reader = new FileReader(targetPath);
                targetContents = IOUtils.toString(reader);
            } catch (Exception e) {
                throw new IllegalStateException("Error reading '".concat(targetPath).concat("'"), e);
            } finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // prepare mutable file
        // use MutableFile in combination with FileManager to take advantage of
        // Roos transactional file handling which offers automatic rollback if
        // an
        // exception occurs
        MutableFile target = null;
        if (targetContents == null) {
            target = fileManager.createFile(targetPath);
        } else {
            // decide if need to replace target
            if (containsStrings == null || containsStrings.length == 0) {
                // No checks to do
                target = fileManager.updateFile(targetPath);
            } else {
                for (String contains : containsStrings) {
                    if (!targetContents.contains(contains)) {
                        target = fileManager.updateFile(targetPath);
                        break;
                    }
                }
            }
        }

        if (target == null) {
            return;
        }

        try {
            InputStream inputStream = null;
            OutputStreamWriter outputStream = null;
            try {
                inputStream = IOUtils.toInputStream(sourceContents);
                outputStream = new OutputStreamWriter(target.getOutputStream());
                IOUtils.copy(inputStream, outputStream);
            } finally {
                IOUtils.closeQuietly(inputStream);
                IOUtils.closeQuietly(outputStream);
            }
        } catch (IOException e) {
            throw new IllegalStateException("Unable to create/update '".concat(targetPath).concat("'"), e);
        }
    }

    /**
     * Updates the contents for one resource represented by the given target
     * file path and relative source path.
     * 
     * @param relativePath path relative to {@link Path.SRC_MAIN_WEBAPP} of
     *        target file
     * @param resourceName path relative to classpath of file to be copied
     *        (cannot be null)
     * @param toReplace
     * @param containsStrings
     */
    private void updateResource(String relativePath, String resourceName, Map<String, String> toReplace,
            String[] containsStrings) {
        PathResolver pathResolver = getPathResolver();

        String targetPath = pathResolver.getIdentifier(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                relativePath);

        InputStream resource = getClass().getResourceAsStream(resourceName);

        String sourceContents;
        String targetContents = null;

        // load resource to copy
        try {
            sourceContents = IOUtils.toString(new InputStreamReader(resource));
            // Replace params
            if (toReplace != null) {
                for (Entry<String, String> entry : toReplace.entrySet()) {
                    sourceContents = StringUtils.replace(sourceContents, entry.getKey(), entry.getValue());
                }
            }
        } catch (IOException e) {
            throw new IllegalStateException("Unable to load file to be copied '".concat(resourceName).concat("'"),
                    e);
        } finally {
            try {
                resource.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // load target contents if exists
        if (fileManager.exists(targetPath)) {
            FileReader reader = null;
            try {
                reader = new FileReader(targetPath);
                targetContents = IOUtils.toString(reader);
            } catch (Exception e) {
                throw new IllegalStateException("Error reading '".concat(targetPath).concat("'"), e);
            } finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // prepare mutable file
        // use MutableFile in combination with FileManager to take advantage of
        // Roos transactional file handling which offers automatic rollback if
        // an
        // exception occurs
        MutableFile target = fileManager.updateFile(targetPath);

        if (target == null) {
            return;
        }

        try {
            InputStream inputStream = null;
            OutputStreamWriter outputStream = null;
            try {
                inputStream = IOUtils.toInputStream(sourceContents);
                outputStream = new OutputStreamWriter(target.getOutputStream());
                IOUtils.copy(inputStream, outputStream);
            } finally {
                IOUtils.closeQuietly(inputStream);
                IOUtils.closeQuietly(outputStream);
            }
        } catch (IOException e) {
            throw new IllegalStateException("Unable to create/update '".concat(targetPath).concat("'"), e);
        }
    }

    /**
     * Get the absolute path to a file name in given package name.
     * 
     * @param packageName fully qualified package name
     * @param fileName file to get its absolute path
     * @return Path.SRC_MAIN_JAVA + packagePath + fileName
     */
    private String getAbsolutePath(String packageName, String fileName) {
        String fullyQualifPackage = getFullyQualifiedPackageName(packageName);

        // default package
        String packagePath = fullyQualifPackage.replace('.', '/');

        return getProjectOperations().getPathResolver().getIdentifier(
                LogicalPath.getInstance(Path.SRC_MAIN_JAVA, ""), packagePath.concat("/").concat(fileName));
    }

    /**
     * Convert a package name to a fully qualified package name with full
     * support for using "~" as denoting the user's top-level package.
     * 
     * @param packageName
     * @param prjMetadata
     * @return
     */
    private String getFullyQualifiedPackageName(String packageName) {
        if (packageName == null || "".equals(packageName)) {
            return "";
        }

        // by default return the given packageName
        String newPackage = packageName.toLowerCase();

        // resolve "~" as denoting the user's top-level package
        if (packageName.startsWith("~")) {
            String topLevelPath = "";
            topLevelPath = getProjectOperations().getTopLevelPackage(getProjectOperations().getFocusedModuleName())
                    .getFullyQualifiedPackageName();
            // analyze char after ~, if it is . do nothing, else concat .
            // between
            // topLevelPath and given package name and remove ~ at start
            if (packageName.length() > 1) {
                newPackage = (!(packageName.charAt(1) == '.') ? topLevelPath.concat(".") : topLevelPath)
                        .concat(packageName.substring(1));
            }
            // when given packageName is ~
            else {
                newPackage = topLevelPath;
            }
        }

        // normalize
        if (newPackage.endsWith(".")) {
            newPackage = newPackage.substring(0, newPackage.length() - 1);
        }
        return newPackage;
    }

    /**
     * Create menu.xml from menu.jspx.
     * <p>
     * Iterates over menu.jspx elements and delegates menu.xml creation to
     * {@link #addMenuItem(JavaSymbolName, JavaSymbolName, String, String, String, String)}
     * , the same method used to create menu entries from shell.
     */
    private void createMenu() {

        InputStream rooMenuInput = fileManager.getInputStream(getMenuFile());

        try {
            Document rooMenuDoc = XmlUtils.getDocumentBuilder().parse(rooMenuInput);
            Element root = rooMenuDoc.getDocumentElement();
            Element menu = XmlUtils.findFirstElement("/div/menu", root);
            if (menu == null) {
                // Roo menu exists but is still empty: there is no menu element
                // into div element already
                // Avoid error when execute "web mvc setup" and next
                // "menu setup"
                return;
            }

            // root categories and items
            NodeList menuElements = menu.getChildNodes();
            for (int i = 0; i < menuElements.getLength(); i++) {
                Node tmpNode = menuElements.item(i);
                if (tmpNode.getNodeType() != Node.ELEMENT_NODE) {
                    continue;
                }
                String nodeName = tmpNode.getNodeName();

                // process root category elements
                if (nodeName.equals("menu:category")) {
                    Element menuCategory = (Element) tmpNode;

                    // We have to recover original categoryName to success
                    // addMenuItem
                    // execution
                    JavaSymbolName categoryId = new JavaSymbolName(menuCategory.getAttribute("id"));
                    JavaSymbolName categoryName = new JavaSymbolName(
                            StringUtils.capitalize(categoryId.getSymbolName().substring(2)));

                    NodeList menuItems = menuCategory.getChildNodes();

                    // process category inner item elements
                    for (int j = 0; j < menuItems.getLength(); j++) {
                        tmpNode = menuItems.item(j);
                        if (tmpNode.getNodeType() != Node.ELEMENT_NODE) {
                            continue;
                        }
                        Element menuItem = (Element) tmpNode;

                        // - At menu.jspx there isn't label, no prob because it
                        // was added to
                        // application.properties
                        JavaSymbolName menuItemId = new JavaSymbolName(menuItem.getAttribute("id"));
                        String menuItemPrefix = getMenuItemPrefix(menuItemId.getSymbolName());

                        addMenuItem(categoryName, menuItemId, null, menuItem.getAttribute(MESSAGE_CODE),
                                menuItem.getAttribute(URL), menuItemPrefix);
                    }
                }
                // item elements must be inside a category
                else if (nodeName.equals("menu:item")) {
                    Element menuItem = (Element) tmpNode;
                    JavaSymbolName menuItemId = new JavaSymbolName(menuItem.getAttribute("id"));
                    logger.log(Level.SEVERE, "Found menu:item '".concat(menuItemId.getSymbolName())
                            .concat("' in menu:menu tag. It must be in ".concat("menu:category tag [Ignored]")));
                } else {
                    logger.warning(
                            "Found unknow element in menu:menu tag: '".concat(nodeName).concat("' [Ignored]"));
                }

            }
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void disableRooMenuOperations() {
        logger.fine("Disable Roo MenuOperationsImpl");
        setRooMenuDisabled();
    }

    /**
     * Iterates over a list of menu entry Nodes and call
     * {@link #getFormatedInfo(Element, boolean, boolean, boolean, boolean, int)}
     * to get the info of all the menu entry Nodes in the given list.
     * 
     * @param nodes
     * @param label
     * @param messageCode
     * @param roles
     * @param tabSize
     * @return
     */
    private String getFormatedInfo(NodeList nodes, boolean label, boolean messageCode, boolean roles, I18n lang,
            int tabSize) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);

            // filter nodes that aren't menuItems
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            String nodeName = node.getNodeName();
            if (!nodeName.equals(MENU_ITEM)) {
                continue;
            }

            builder.append(getFormatedInfo((Element) node, label, messageCode, roles, lang, tabSize));
        }
        return builder.toString();
    }

    /**
     * Gets the selected info about the given menu entry Element.
     * <p>
     * TODO: I think compact info better. See section "List menu structure" at
     * "docs/pd-addon-web-menu.rst". Note, it means we should refactor this
     * method in 2 methods: on for list command and other for info command
     * 
     * @param element
     * @param label
     * @param messageCode
     * @param destination
     * @param roles
     * @param tabSize
     * @return
     */
    private String getFormatedInfo(Element element, boolean label, boolean message, boolean roles, I18n lang,
            int tabSize) {
        StringBuilder builder = new StringBuilder();
        StringBuilder indent = new StringBuilder();

        // tab string to align children
        for (int i = 0; i < tabSize; i++) {
            indent.append(" ");
        }

        // string containing "[ID]: "
        StringBuilder idInfo = new StringBuilder();
        idInfo.append("[").append(element.getAttribute("id")).append("]");

        // build Element info
        builder.append(indent).append(idInfo).append("\n");

        String url = element.getAttribute(URL);
        if (!StringUtils.isNotBlank(url)) {
            url = "No";
        }
        builder.append(indent).append("URL          : ").append(url).append("\n");

        if (label) {
            String labelCode = element.getAttribute(LABEL_CODE);
            String labelValue = null;

            // get label text from application.properties
            if (StringUtils.isNotBlank(labelCode)) {
                labelValue = propFileOperations.getProperty(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                        "/WEB-INF/i18n/application.properties", labelCode);
            }
            builder.append(indent).append("Label Code   : ").append(labelCode).append("\n");
            builder.append(indent).append("Label        : ").append(labelValue != null ? labelValue : "")
                    .append("\n");
        }
        if (message) {
            String messageCode = element.getAttribute(MESSAGE_CODE);
            String messageValue = null;

            // get message text from messages.properties
            if (StringUtils.isNotBlank(messageCode)) {

                if (lang != null) {
                    String messageBundle = "/WEB-INF/i18n/messages_".concat(lang.getLocale().toString())
                            .concat(".properties");
                    messageValue = propFileOperations.getProperty(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                            messageBundle, messageCode);
                }

                // if no value for given lang, try default lang
                if (!StringUtils.isNotBlank(messageValue)) {
                    String messageBundle = "/WEB-INF/i18n/messages.properties";
                    messageValue = propFileOperations.getProperty(LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""),
                            messageBundle, messageCode);
                }
            }
            builder.append(indent).append("Message Code : ").append(element.getAttribute(MESSAGE_CODE))
                    .append("\n");
            builder.append(indent).append("Message      : ").append(messageValue != null ? messageValue : "")
                    .append("\n");
        }
        if (roles) {
            builder.append(indent).append("Roles        : ").append(element.getAttribute("roles")).append("\n");
        }

        String hidden = element.getAttribute(HIDDEN);
        if (!StringUtils.isNotBlank(hidden)) {
            hidden = "false"; // visible by default
        }
        builder.append(indent).append("Hidden       : ").append(hidden).append("\n");

        // get children info
        if (element.hasChildNodes()) {
            builder.append(indent).append("Children     : ").append("\n");
            builder.append(getFormatedInfo(element.getChildNodes(), label, message, roles, lang, tabSize + 15)); // indent to the right
                                                                                                                 // of
                                                                                                                 // "Children     : "
                                                                                                                 // (length 15 chars)
        } else {
            builder.append("\n"); // empty line
        }
        return builder.toString();
    }

    /**
     * Install properties defined in external XML file
     * 
     * @param configuration
     */
    private void updatePomProperties(Element configuration) {
        List<Element> addonProperties = XmlUtils.findElements("/configuration/gvnix/menu/properties/*",
                configuration);
        for (Element property : addonProperties) {
            getProjectOperations().addProperty(getProjectOperations().getFocusedModuleName(),
                    new Property(property));
        }
    }

    /**
     * Install dependencies defined in external XML file
     * 
     * @param configuration
     */
    private void updateDependencies(Element configuration) {
        List<Dependency> dependencies = new ArrayList<Dependency>();
        List<Element> securityDependencies = XmlUtils
                .findElements("/configuration/gvnix/menu/dependencies/dependency", configuration);
        for (Element dependencyElement : securityDependencies) {
            dependencies.add(new Dependency(dependencyElement));
        }
        getProjectOperations().addDependencies(getProjectOperations().getFocusedModuleName(), dependencies);
    }

    /**
     * Gets menu item prefix: {@link MenuOperations#DEFAULT_MENU_ITEM_PREFIX} or
     * {@link MenuOperations#FINDER_MENU_ITEM_PREFIX}
     * 
     * @param itemId
     * @return MenuOperations.FINDER_MENU_ITEM_PREFIX if given itemId starts
     *         with "fi_", otherwise return
     *         MenuOperations.DEFAULT_MENU_ITEM_PREFIX
     */
    private String getMenuItemPrefix(String itemId) {
        if (itemId.startsWith(MenuOperations.FINDER_MENU_ITEM_PREFIX)) {
            return MenuOperations.FINDER_MENU_ITEM_PREFIX;
        } else if (itemId.startsWith("si_")) {
            return "si_";
        }
        return MenuOperations.DEFAULT_MENU_ITEM_PREFIX;
    }

    /**
     * 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, ""),
                "/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, ""),
                "/WEB-INF/layouts/layouts.xml");
    }

    /**
     * Method to add menu styles to load-scripts-bootstrap.tagx
     */
    private void addStylesToLoadScriptBootstrap() {

        List<Pair<String, String>> cssList = new ArrayList<Pair<String, String>>();
        List<Pair<String, String>> jsList = new ArrayList<Pair<String, String>>();

        // Add jquery.datatables.css url resolution
        cssList.add(new ImmutablePair<String, String>("css_bootstrap_menu_url",
                "/resources/styles/menu/dropdown-submenu.css"));

        WebProjectUtils.addJsAndCssToLoadScriptsTag(cssList, jsList, getProjectOperations(), fileManager);

    }

    public ProjectOperations getProjectOperations() {
        if (projectOperations == null) {
            // Get all Services implement ProjectOperations interface
            try {
                ServiceReference[] references = this.context
                        .getAllServiceReferences(ProjectOperations.class.getName(), null);

                for (ServiceReference ref : references) {
                    projectOperations = (ProjectOperations) this.context.getService(ref);
                    return projectOperations;
                }

                return null;

            } catch (InvalidSyntaxException e) {
                LOGGER.warning("Cannot load ProjectOperations on MenuEntryOperationsImpl.");
                return null;
            }
        } else {
            return projectOperations;
        }
    }
}