/* * gvNIX is an open source tool for rapid application development (RAD). * Copyright (C) 2010 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.report.roo.addon.addon; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilder; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Service; 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.jpa.addon.activerecord.JpaActiveRecordMetadata; import org.springframework.roo.addon.propfiles.PropFileOperations; import org.springframework.roo.addon.web.mvc.controller.addon.scaffold.WebScaffoldMetadata; import org.springframework.roo.addon.web.mvc.jsp.menu.MenuOperations; import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperations; import org.springframework.roo.addon.web.mvc.jsp.tiles.TilesOperationsImpl; import org.springframework.roo.metadata.MetadataDependencyRegistry; import org.springframework.roo.metadata.MetadataIdentificationUtils; import org.springframework.roo.metadata.MetadataItem; import org.springframework.roo.metadata.MetadataNotificationListener; import org.springframework.roo.metadata.MetadataProvider; import org.springframework.roo.metadata.MetadataService; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; 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.Path; import org.springframework.roo.project.PathResolver; import org.springframework.roo.support.logging.HandlerUtils; import org.springframework.roo.support.util.XmlElementBuilder; import org.springframework.roo.support.util.XmlRoundTripUtils; import org.springframework.roo.support.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * Implementation of Metadata listener. * * @author <a href="http://www.disid.com">DISID Corporation S.L.</a> made for <a * href="http://www.dgti.gva.es">General Directorate for Information * Technologies (DGTI)</a> * @since 0.6 */ @Component @Service public final class ReportJspMetadataListener implements MetadataProvider, MetadataNotificationListener { private static final Logger LOGGER = HandlerUtils .getLogger(ReportJspMetadataListener.class); // ------------ OSGi component attributes ---------------- private BundleContext context; private MetadataDependencyRegistry metadataDependencyRegistry; private MetadataService metadataService; private FileManager fileManager; private TilesOperations tilesOperations; private MenuOperations menuOperations; private PathResolver pathResolver; private PropFileOperations propFileOperations; private WebScaffoldMetadata webScaffoldMetadata; private JpaActiveRecordMetadata entityMetadata; private JavaType javaType; private JavaType formbackingObject; protected void activate(ComponentContext cContext) { context = cContext.getBundleContext(); getMetadataDependencyRegistry().registerDependency( ReportMetadata.getMetadataIdentiferType(), getProvidesType()); } public MetadataItem get(String metadataIdentificationString) { javaType = ReportJspMetadata.getJavaType(metadataIdentificationString); LogicalPath path = ReportJspMetadata .getPath(metadataIdentificationString); String reportMetadataKey = ReportMetadata.createIdentifier(javaType, path); ReportMetadata reportMetadata = (ReportMetadata) getMetadataService() .get(reportMetadataKey); if (reportMetadata == null || !reportMetadata.isValid()) { return null; } webScaffoldMetadata = reportMetadata.getWebScaffoldMetadata(); Validate.notNull(webScaffoldMetadata, "Web scaffold metadata required"); formbackingObject = webScaffoldMetadata.getAnnotationValues() .getFormBackingObject(); entityMetadata = (JpaActiveRecordMetadata) getMetadataService().get( JpaActiveRecordMetadata.createIdentifier(formbackingObject, path)); Validate.notNull(entityMetadata, "Could not determine entity metadata for type: " + formbackingObject.getFullyQualifiedTypeName()); for (String installedReport : reportMetadata.getInstalledReports()) { installMvcArtifacts(installedReport); } return new ReportJspMetadata(metadataIdentificationString, reportMetadata); } /** * Given a reportFormat it generates/updates the JSPX showing the form with * the generate report submit button. * * @param report */ public void installMvcArtifacts(String report) { String[] reportNameFormat = ReportMetadata .stripGvNixReportValue(report); String controllerPath = webScaffoldMetadata.getAnnotationValues() .getPath(); // Make the holding directory for this controller String destinationDirectory = getPathResolver().getIdentifier( LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "WEB-INF/views/" + controllerPath); if (!getFileManager().exists(destinationDirectory)) { getFileManager().createDirectory(destinationDirectory); } else { File file = new File(destinationDirectory); Validate.isTrue(file.isDirectory(), destinationDirectory + " is a file, when a directory was expected"); } Document document = getReportFormJsp(reportNameFormat[0], controllerPath); writeToDiskIfNecessary( getPathResolver().getIdentifier( LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "WEB-INF/views/" + controllerPath + "/" + reportNameFormat[0] + ".jspx"), document); Map<String, String> properties = new HashMap<String, String>(); getTilesOperations().addViewDefinition( controllerPath, LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), controllerPath + "/" + reportNameFormat[0], TilesOperationsImpl.DEFAULT_TEMPLATE, "/WEB-INF/views/" + controllerPath + "/" + reportNameFormat[0] + ".jspx"); getMenuOperations().addMenuItem( new JavaSymbolName(formbackingObject.getSimpleTypeName()), new JavaSymbolName(reportNameFormat[0] + "_report"), "menu_" + formbackingObject.getSimpleTypeName().toLowerCase() + "_" + reportNameFormat[0] + "_report", "/" + controllerPath + "/reports/" + reportNameFormat[0] + "?form", MenuOperations.DEFAULT_MENU_ITEM_PREFIX, LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, "")); properties.put("menu_" + formbackingObject.getSimpleTypeName().toLowerCase() + "_" + reportNameFormat[0] + "_report", new JavaSymbolName( formbackingObject.getSimpleTypeName()).getReadableSymbolName() + " " + reportNameFormat[0] + " Report"); properties.put("menu_item_" + formbackingObject.getSimpleTypeName().toLowerCase() + "_" + reportNameFormat[0] + "_report_label", new JavaSymbolName( formbackingObject.getSimpleTypeName()).getReadableSymbolName() + " " + reportNameFormat[0] + " Report"); // Add the message error to the application.properties getPropFileOperations() .addProperties( LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "/WEB-INF/i18n/application.properties", properties, true, false); } /** * Generates a JSPX with a form requesting the report. The form has as many * radio buttons as formats has set the report. * * @param reportName * @param formats * @param controllerPath * @return */ private Document getReportFormJsp(String reportName, String controllerPath) { DocumentBuilder builder = XmlUtils.getDocumentBuilder(); Document document = builder.newDocument(); Map<String, String> properties = new HashMap<String, String>(); // Add document namespaces Element div = (Element) document.appendChild(new XmlElementBuilder( "div", document) .addAttribute("xmlns:c", "http://java.sun.com/jsp/jstl/core") .addAttribute("xmlns:fn", "http://java.sun.com/jsp/jstl/functions") .addAttribute("xmlns:spring", "http://www.springframework.org/tags") .addAttribute("xmlns:jsp", "http://java.sun.com/JSP/Page") .addAttribute("xmlns:form", "http://www.springframework.org/tags/form") .addAttribute("version", "2.0") .addChild( new XmlElementBuilder("jsp:directive.page", document) .addAttribute("contentType", "text/html;charset=UTF-8").build()) .addChild( new XmlElementBuilder("jsp:output", document) .addAttribute("omit-xml-declaration", "yes") .build()).build()); // Add div panel Element divPanel = (Element) new XmlElementBuilder("div", document) .addAttribute("class", "panel panel-default").build(); // Add div header Element divPanelHeader = (Element) new XmlElementBuilder("div", document).addAttribute("class", "panel-heading").build(); // Add h3 title with content Element h3PanelTitle = (Element) new XmlElementBuilder("h3", document) .addAttribute("class", "panel-title") .addChild( new XmlElementBuilder("spring:message", document) .addAttribute( "code", "label_report_" + controllerPath + "_" + reportName) .addAttribute("htmlEscape", "false").build()) .build(); // Adding title panel to panel header divPanelHeader.appendChild(h3PanelTitle); // Adding panel header to div panel divPanel.appendChild(divPanelHeader); // Add div panel body Element divPanelBody = (Element) new XmlElementBuilder("div", document) .addAttribute("class", "panel-body").build(); // Add if not empty error Element ifNotEmptyError = (Element) new XmlElementBuilder("c:if", document).addAttribute("test", "${not empty error}").build(); // Add h3 title with error message Element h3ErrorMessage = (Element) new XmlElementBuilder("h3", document) .addAttribute("class", "panel-title") .addChild( new XmlElementBuilder("spring:message", document) .addAttribute("code", "${error}") .addAttribute("htmlEscape", "false").build()) .build(); // Adding h3 message to if empty error ifNotEmptyError.appendChild(h3ErrorMessage); // Adding if not empty error to divPanelBody divPanelBody.appendChild(ifNotEmptyError); // Add form element Element formElement = (Element) new XmlElementBuilder("form:form", document) .addAttribute("class", "form-horizontal") .addAttribute("role", "form") .addAttribute("action", reportName) .addAttribute( "id", XmlUtils.convertId("fr_" + formbackingObject.getFullyQualifiedTypeName())) .addAttribute("method", "GET").build(); // Add div control group Element divControlGroup = (Element) new XmlElementBuilder("div", document).addAttribute("class", "control-group form-group") .build(); // Add div controls col Element divControlsCol = (Element) new XmlElementBuilder("div", document).addAttribute("class", "controls col-xs-7 col-sm-8 col-md-12 col-lg-12").build(); // Add a drop-down select Element cifSelectFormat = (Element) new XmlElementBuilder("c:if", document).addAttribute("test", "${not empty report_formats}") .build(); Element selectFormat = (Element) new XmlElementBuilder("select", document).addAttribute("class", "form-control input-sm") .addAttribute("id", "_select_format") .addAttribute("name", "format").build(); Element cforEach = (Element) new XmlElementBuilder("c:forEach", document).addAttribute("items", "${report_formats}") .addAttribute("var", "format").build(); Element optionFormat = (Element) new XmlElementBuilder("option", document).addAttribute("id", "option_format_${format}") .addAttribute("value", "${format}").build(); Element coutFormat = (Element) new XmlElementBuilder("c:out", document) .addAttribute("value", "${fn:toUpperCase(format)}").build(); optionFormat.appendChild(coutFormat); cforEach.appendChild(optionFormat); selectFormat.appendChild(cforEach); cifSelectFormat.appendChild(selectFormat); // Add input element Element inputElement = (Element) new XmlElementBuilder("input", document).addAttribute("class", "btn btn-primary btn-block") .addAttribute("type", "submit").build(); // Adding elements to divControlsCol divControlsCol.appendChild(cifSelectFormat); divControlsCol.appendChild(inputElement); // Adding controls col to div control group divControlGroup.appendChild(divControlsCol); // Adding control group to form formElement.appendChild(divControlGroup); // Adding form to Panel Body divPanelBody.appendChild(formElement); // Adding Panel Body to Main Panel divPanel.appendChild(divPanelBody); // Adding Main Panel to general div div.appendChild(divPanel); // Add the message error to the application.properties properties.put("label_report_" + controllerPath + "_" + reportName, "Report " + reportName); getPropFileOperations() .addProperties( LogicalPath.getInstance(Path.SRC_MAIN_WEBAPP, ""), "/WEB-INF/i18n/application.properties", properties, true, false); return document; } public void notify(String upstreamDependency, String downstreamDependency) { if (MetadataIdentificationUtils .isIdentifyingClass(downstreamDependency)) { Validate.isTrue( MetadataIdentificationUtils.getMetadataClass( upstreamDependency).equals( MetadataIdentificationUtils .getMetadataClass(ReportMetadata .getMetadataIdentiferType())), "Expected class-level notifications only for gvNIX Report metadata (not '" + upstreamDependency + "')"); // A physical Java type has changed, and determine what the // corresponding local metadata identification string would have // been JavaType javaType = ReportMetadata.getJavaType(upstreamDependency); LogicalPath path = ReportMetadata.getPath(upstreamDependency); downstreamDependency = ReportJspMetadata.createIdentifier(javaType, path); // We only need to proceed if the downstream dependency relationship // is not already registered // (if it's already registered, the event will be delivered directly // later on) if (getMetadataDependencyRegistry().getDownstream( upstreamDependency).contains(downstreamDependency)) { return; } } // We should now have an instance-specific "downstream dependency" that // can be processed by this class Validate.isTrue( MetadataIdentificationUtils.getMetadataClass( downstreamDependency).equals( MetadataIdentificationUtils .getMetadataClass(getProvidesType())), "Unexpected downstream notification for '" + downstreamDependency + "' to this provider (which uses '" + getProvidesType() + "'"); getMetadataService().evict(downstreamDependency); if (get(downstreamDependency) != null) { getMetadataDependencyRegistry().notifyDownstream( downstreamDependency); } } public String getProvidesType() { return ReportJspMetadata.getMetadataIdentiferType(); } /** return indicates if disk was changed (ie updated or created) */ private boolean writeToDiskIfNecessary(String jspFilename, Document proposed) { Document original = null; // 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 (getFileManager().exists(jspFilename)) { try { original = XmlUtils.getDocumentBuilder().parse( getFileManager().getInputStream(jspFilename)); } catch (Exception e) { throw new IllegalStateException("Could not parse file: " + jspFilename); } Validate.notNull(original, "Unable to parse " + jspFilename); if (XmlRoundTripUtils.compareDocuments(original, proposed)) { mutableFile = getFileManager().updateFile(jspFilename); } } else { original = proposed; mutableFile = getFileManager().createFile(jspFilename); Validate.notNull(mutableFile, "Could not create JSP file '" + jspFilename + "'"); } if (mutableFile != null) { try { // Build a string representation of the JSP ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); XmlUtils.writeXml(XmlUtils.createIndentingTransformer(), byteArrayOutputStream, original); String jspContent = byteArrayOutputStream.toString(); // We need to write the file out (it's a new file, or the // existing file has different contents) InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = IOUtils.toInputStream(jspContent); outputStream = mutableFile.getOutputStream(); IOUtils.copy(inputStream, outputStream); } finally { IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(outputStream); } // 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; } public MetadataDependencyRegistry getMetadataDependencyRegistry() { if (metadataDependencyRegistry == null) { // Get all Services implement MetadataDependencyRegistry interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences( MetadataDependencyRegistry.class.getName(), null); for (ServiceReference<?> ref : references) { return (MetadataDependencyRegistry) this.context .getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load MetadataDependencyRegistry on ReportJspMetadataListener."); return null; } } else { return metadataDependencyRegistry; } } public MetadataService getMetadataService() { if (metadataService == null) { // Get all Services implement MetadataService interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences( MetadataService.class.getName(), null); for (ServiceReference<?> ref : references) { return (MetadataService) this.context.getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load MetadataService on ReportJspMetadataListener."); return null; } } else { return metadataService; } } public FileManager getFileManager() { if (fileManager == null) { // Get all Services implement FileManager interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences(FileManager.class.getName(), null); for (ServiceReference<?> ref : references) { return (FileManager) this.context.getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load FileManager on ReportJspMetadataListener."); return null; } } else { return fileManager; } } public TilesOperations getTilesOperations() { if (tilesOperations == null) { // Get all Services implement TilesOperations interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences( TilesOperations.class.getName(), null); for (ServiceReference<?> ref : references) { return (TilesOperations) this.context.getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load TilesOperations on ReportJspMetadataListener."); return null; } } else { return tilesOperations; } } public MenuOperations getMenuOperations() { if (menuOperations == null) { // Get all Services implement MenuOperations interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences( MenuOperations.class.getName(), null); for (ServiceReference<?> ref : references) { return (MenuOperations) this.context.getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load MenuOperations on ReportJspMetadataListener."); return null; } } else { return menuOperations; } } public PathResolver getPathResolver() { if (pathResolver == null) { // Get all Services implement PathResolver interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences(PathResolver.class.getName(), null); for (ServiceReference<?> ref : references) { return (PathResolver) this.context.getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load PathResolver on ReportJspMetadataListener."); return null; } } else { return pathResolver; } } public PropFileOperations getPropFileOperations() { if (propFileOperations == null) { // Get all Services implement PropFileOperations interface try { ServiceReference<?>[] references = this.context .getAllServiceReferences( PropFileOperations.class.getName(), null); for (ServiceReference<?> ref : references) { return (PropFileOperations) this.context.getService(ref); } return null; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load PropFileOperations on ReportJspMetadataListener."); return null; } } else { return propFileOperations; } } }