package org.springframework.roo.addon.web.mvc.jsp.tiles; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.transform.OutputKeys; 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.springframework.roo.process.manager.FileManager; import org.springframework.roo.process.manager.MutableFile; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.PathResolver; import org.springframework.roo.support.util.FileUtils; import org.springframework.roo.support.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * Provides operations to manage tiles view definitions. * * @author Stefan Schmidt * @since 1.0 */ @Component @Service public class TilesOperationsImpl implements TilesOperations { private static class TilesDtdResolver implements EntityResolver { public InputSource resolveEntity(final String publicId, final String systemId) { if (systemId.equals("http://tiles.apache.org/dtds/tiles-config_2_1.dtd")) { return new InputSource(FileUtils.getInputStream(TilesOperationsImpl.class, "tiles-config_2_1.dtd")); } // Use the default behaviour return null; } } @Reference private FileManager fileManager; @Reference private PathResolver pathResolver; public void addViewDefinition(final String folderName, final LogicalPath path, final String tilesViewName, final String tilesTemplateName, final String viewLocation) { Validate.notBlank(tilesViewName, "View name required"); Validate.notBlank(tilesTemplateName, "Template name required"); Validate.notBlank(viewLocation, "View location required"); final String viewsDefinitionFile = getTilesConfigFile(folderName, path); final String unprefixedViewName = StringUtils.removeStart(tilesViewName, "/"); final Element root = getViewsElement(viewsDefinitionFile); final Element existingDefinition = XmlUtils.findFirstElement("/tiles-definitions/definition[@name = '" + unprefixedViewName + "']", root); if (existingDefinition != null) { // A definition with this name does already exist - nothing to do return; } final Element newDefinition = root.getOwnerDocument().createElement("definition"); newDefinition.setAttribute("name", unprefixedViewName); newDefinition.setAttribute("extends", tilesTemplateName); final Element putAttribute = root.getOwnerDocument().createElement("put-attribute"); putAttribute.setAttribute("name", "body"); putAttribute.setAttribute("value", viewLocation); newDefinition.appendChild(putAttribute); root.appendChild(newDefinition); writeToDiskIfNecessary(viewsDefinitionFile, root); } /** * Returns the canonical path of the "views.xml" Tiles configuration file in * the given folder. * * @param folderName can be blank for the main views file; if not, any * leading slash is ignored * @param path * @return a non-<code>null</code> path */ private String getTilesConfigFile(final String folderName, final LogicalPath path) { final String subPath; if (StringUtils.isNotBlank(folderName) && !"/".equals(folderName)) { subPath = "/" + folderName; } else { subPath = ""; } return pathResolver.getIdentifier(path, "WEB-INF/views" + subPath + "/views.xml"); } /** * Returns the root element of the given Tiles configuration file * * @param viewsDefinitionFile the canonical path of the file to load * @return the root of a new XML document if that file does not exist */ private Element getViewsElement(final String viewsDefinitionFile) { final Document tilesView; if (fileManager.exists(viewsDefinitionFile)) { final DocumentBuilder builder = XmlUtils.getDocumentBuilder(); builder.setEntityResolver(new TilesDtdResolver()); try { tilesView = builder.parse(fileManager.getInputStream(viewsDefinitionFile)); } catch (final SAXException se) { throw new IllegalStateException("Unable to parse the tiles " + viewsDefinitionFile + " file", se); } catch (final IOException ioe) { throw new IllegalStateException("Unable to read the tiles " + viewsDefinitionFile + " file (reason: " + ioe.getMessage() + ")", ioe); } } else { tilesView = XmlUtils.getDocumentBuilder().newDocument(); tilesView.appendChild(tilesView.createElement("tiles-definitions")); } return tilesView.getDocumentElement(); } public void removeViewDefinition(final String name, final String folderName, final LogicalPath path) { Validate.notBlank(name, "View name required"); final String viewsDefinitionFile = getTilesConfigFile(folderName, path); final Element root = getViewsElement(viewsDefinitionFile); // Find menu item under this category if exists final Element element = XmlUtils.findFirstElement("/tiles-definitions/definition[@name = '" + name + "']", root); if (element != null) { element.getParentNode().removeChild(element); writeToDiskIfNecessary(viewsDefinitionFile, root); } } /** * @param viewsDefinitionFile the canonical path of the file to update * @param body the element whose parent document is to be written * @return */ private boolean writeToDiskIfNecessary(final String tilesDefinitionFile, final Element body) { // Build a string representation of the Tiles config file final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); final Transformer transformer = XmlUtils.createIndentingTransformer(); transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://tiles.apache.org/dtds/tiles-config_2_1.dtd"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"); XmlUtils.writeXml(transformer, byteArrayOutputStream, body.getOwnerDocument()); final 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(tilesDefinitionFile)) { // First verify if the file has even changed final File file = new File(tilesDefinitionFile); String existing = null; try { existing = org.apache.commons.io.FileUtils.readFileToString(file); } catch (final IOException ignored) { } if (!viewContent.equals(existing)) { mutableFile = fileManager.updateFile(tilesDefinitionFile); } } else { mutableFile = fileManager.createFile(tilesDefinitionFile); Validate.notNull(mutableFile, "Could not create tiles view definition '%s'", tilesDefinitionFile); } if (mutableFile != null) { OutputStream outputStream = null; try { // We need to write the file out (it's a new file, or the // existing file has different contents) outputStream = mutableFile.getOutputStream(); IOUtils.write(viewContent, outputStream); // Return and indicate we wrote out the file return true; } catch (final IOException ioe) { throw new IllegalStateException( "Could not output '" + mutableFile.getCanonicalPath() + "'", ioe); } finally { IOUtils.closeQuietly(outputStream); } } // A file existed, but it contained the same content, so we return false return false; } }