package org.springframework.roo.addon.web.mvc.controller;
import static org.springframework.roo.model.JdkJavaType.EXCEPTION;
import static org.springframework.roo.model.SpringJavaType.CHARACTER_ENCODING_FILTER;
import static org.springframework.roo.model.SpringJavaType.CONTEXT_LOADER_LISTENER;
import static org.springframework.roo.model.SpringJavaType.CONVERSION_SERVICE_EXPOSING_INTERCEPTOR;
import static org.springframework.roo.model.SpringJavaType.DISPATCHER_SERVLET;
import static org.springframework.roo.model.SpringJavaType.FLOW_HANDLER_MAPPING;
import static org.springframework.roo.model.SpringJavaType.HIDDEN_HTTP_METHOD_FILTER;
import static org.springframework.roo.model.SpringJavaType.OPEN_ENTITY_MANAGER_IN_VIEW_FILTER;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
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.model.JavaPackage;
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.Dependency;
import org.springframework.roo.project.DependencyScope;
import org.springframework.roo.project.DependencyType;
import org.springframework.roo.project.FeatureNames;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.ProjectType;
import org.springframework.roo.support.util.DomUtils;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.WebXmlUtils;
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.NodeList;
/**
* Implementation of {@link WebMvcOperations}.
*
* @author Stefan Schmidt
* @author Ben Alex
* @since 1.0
*/
@Component(immediate = true)
@Service
public class WebMvcOperationsImpl implements WebMvcOperations {
private static final String CONVERSION_SERVICE_BEAN_NAME = "applicationConversionService";
private static final String CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME = "conversionServiceExposingInterceptor";
private static final String CONVERSION_SERVICE_SIMPLE_TYPE = "ApplicationConversionServiceFactoryBean";
private static final String WEB_XML = "WEB-INF/web.xml";
private static final String WEBMVC_CONFIG_XML = "WEB-INF/spring/webmvc-config.xml";
@Reference private FileManager fileManager;
@Reference private PathResolver pathResolver;
@Reference private ProjectOperations projectOperations;
private void copyWebXml() {
Validate.isTrue(projectOperations.isFocusedProjectAvailable(),
"Project metadata required");
// Verify the servlet application context already exists
final String servletCtxFilename = WEBMVC_CONFIG_XML;
Validate.isTrue(fileManager.exists(pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, servletCtxFilename)), "'"
+ servletCtxFilename + "' does not exist");
final String webXmlPath = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, WEB_XML);
if (fileManager.exists(webXmlPath)) {
// File exists, so nothing to do
return;
}
final InputStream templateInputStream = FileUtils.getInputStream(
getClass(), "web-template.xml");
Validate.notNull(templateInputStream,
"Could not acquire web.xml template");
final Document document = XmlUtils.readXml(templateInputStream);
final String projectName = projectOperations
.getProjectName(projectOperations.getFocusedModuleName());
WebXmlUtils.setDisplayName(projectName, document, null);
WebXmlUtils.setDescription("Roo generated " + projectName
+ " application", document, null);
fileManager.createOrUpdateTextFileIfRequired(webXmlPath,
XmlUtils.nodeToString(document), true);
}
private void createWebApplicationContext() {
Validate.isTrue(projectOperations.isFocusedProjectAvailable(),
"Project metadata required");
final String webConfigFile = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML);
final Document document = readOrCreateSpringWebConfigFile(webConfigFile);
setBasePackageForComponentScan(document);
fileManager.createOrUpdateTextFileIfRequired(webConfigFile,
XmlUtils.nodeToString(document), true);
}
public void installAllWebMvcArtifacts() {
installMinimalWebArtifacts();
manageWebXml();
updateConfiguration();
}
public void installConversionService(final JavaPackage destinationPackage) {
final String webMvcConfigPath = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML);
Validate.isTrue(fileManager.exists(webMvcConfigPath), "'"
+ webMvcConfigPath + "' does not exist");
final Document document = XmlUtils.readXml(fileManager
.getInputStream(webMvcConfigPath));
final Element root = document.getDocumentElement();
final Element annotationDriven = DomUtils.findFirstElementByName(
"mvc:annotation-driven", root);
if (isConversionServiceConfigured(root, annotationDriven)) {
// Conversion service already defined, moving on.
return;
}
annotationDriven.setAttribute("conversion-service",
CONVERSION_SERVICE_BEAN_NAME);
final Element conversionServiceBean = new XmlElementBuilder("bean",
document)
.addAttribute("id", CONVERSION_SERVICE_BEAN_NAME)
.addAttribute(
"class",
destinationPackage.getFullyQualifiedPackageName() + "."
+ CONVERSION_SERVICE_SIMPLE_TYPE).build();
root.appendChild(conversionServiceBean);
fileManager.createOrUpdateTextFileIfRequired(webMvcConfigPath,
XmlUtils.nodeToString(document), false);
installConversionServiceJavaClass(destinationPackage);
registerWebFlowConversionServiceExposingInterceptor();
}
private void installConversionServiceJavaClass(final JavaPackage thePackage) {
final JavaType javaType = new JavaType(
thePackage.getFullyQualifiedPackageName()
+ ".ApplicationConversionServiceFactoryBean");
final String physicalPath = pathResolver.getFocusedCanonicalPath(
Path.SRC_MAIN_JAVA, javaType);
if (fileManager.exists(physicalPath)) {
return;
}
InputStream inputStream = null;
try {
inputStream = FileUtils
.getInputStream(getClass(),
"converter/ApplicationConversionServiceFactoryBean-template._java");
String input = IOUtils.toString(inputStream);
input = input.replace("__PACKAGE__",
thePackage.getFullyQualifiedPackageName());
fileManager.createOrUpdateTextFileIfRequired(physicalPath, input,
false);
}
catch (final IOException e) {
throw new IllegalStateException("Unable to create '" + physicalPath
+ "'", e);
}
finally {
IOUtils.closeQuietly(inputStream);
}
}
public void installMinimalWebArtifacts() {
// Note that the sequence matters here as some of these artifacts are
// loaded further down the line
createWebApplicationContext();
copyWebXml();
}
private boolean isConversionServiceConfigured() {
final String webMvcConfigPath = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML);
Validate.isTrue(fileManager.exists(webMvcConfigPath), webMvcConfigPath
+ " doesn't exist");
final MutableFile mutableFile = fileManager
.updateFile(webMvcConfigPath);
final Document document = XmlUtils
.readXml(mutableFile.getInputStream());
final Element root = document.getDocumentElement();
final Element annotationDrivenElement = DomUtils
.findFirstElementByName("mvc:annotation-driven", root);
return isConversionServiceConfigured(root, annotationDrivenElement);
}
private boolean isConversionServiceConfigured(final Element root,
final Element annotationDrivenElement) {
final String beanName = annotationDrivenElement
.getAttribute("conversion-service");
if (StringUtils.isBlank(beanName)) {
return false;
}
final Element bean = XmlUtils.findFirstElement("/beans/bean[@id=\""
+ beanName + "\"]", root);
final String classAttribute = bean.getAttribute("class");
final StringBuilder sb = new StringBuilder(
"Found custom ConversionService installed in webmvc-config.xml. ");
sb.append("Remove the conversion-service attribute, let Spring ROO 1.1.1 (or higher), install the new application-wide ");
sb.append("ApplicationConversionServiceFactoryBean and then use that to register your custom converters and formatters.");
Validate.isTrue(
classAttribute.endsWith(CONVERSION_SERVICE_SIMPLE_TYPE),
sb.toString());
return true;
}
private void manageWebXml() {
Validate.isTrue(projectOperations.isFocusedProjectAvailable(),
"Project metadata required");
// Verify that the web.xml already exists
final String webXmlPath = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, WEB_XML);
Validate.isTrue(fileManager.exists(webXmlPath), "'" + webXmlPath
+ "' does not exist");
final Document document = XmlUtils.readXml(fileManager
.getInputStream(webXmlPath));
WebXmlUtils.addContextParam(new WebXmlUtils.WebXmlParam(
"defaultHtmlEscape", "true"), document,
"Enable escaping of form submission contents");
WebXmlUtils.addContextParam(new WebXmlUtils.WebXmlParam(
"contextConfigLocation",
"classpath*:META-INF/spring/applicationContext*.xml"),
document, null);
WebXmlUtils.addFilter(CHARACTER_ENCODING_FILTER_NAME,
CHARACTER_ENCODING_FILTER.getFullyQualifiedTypeName(), "/*",
document, null,
new WebXmlUtils.WebXmlParam("encoding", "UTF-8"),
new WebXmlUtils.WebXmlParam("forceEncoding", "true"));
WebXmlUtils.addFilter(HTTP_METHOD_FILTER_NAME,
HIDDEN_HTTP_METHOD_FILTER.getFullyQualifiedTypeName(), "/*",
document, null);
if (projectOperations.isFeatureInstalled(FeatureNames.JPA)) {
WebXmlUtils.addFilter(OPEN_ENTITYMANAGER_IN_VIEW_FILTER_NAME,
OPEN_ENTITY_MANAGER_IN_VIEW_FILTER
.getFullyQualifiedTypeName(), "/*", document, null);
}
WebXmlUtils
.addListener(
CONTEXT_LOADER_LISTENER.getFullyQualifiedTypeName(),
document,
"Creates the Spring Container shared by all Servlets and Filters");
WebXmlUtils.addServlet(projectOperations.getFocusedProjectName(),
DISPATCHER_SERVLET.getFullyQualifiedTypeName(), "/", 1,
document, "Handles Spring requests",
new WebXmlUtils.WebXmlParam("contextConfigLocation",
WEBMVC_CONFIG_XML));
WebXmlUtils.setSessionTimeout(10, document, null);
WebXmlUtils.addExceptionType(EXCEPTION.getFullyQualifiedTypeName(),
"/uncaughtException", document, null);
WebXmlUtils.addErrorCode(new Integer(404), "/resourceNotFound",
document, null);
fileManager.createOrUpdateTextFileIfRequired(webXmlPath,
XmlUtils.nodeToString(document), false);
}
private Document readOrCreateSpringWebConfigFile(final String webConfigFile) {
final InputStream inputStream;
if (fileManager.exists(webConfigFile)) {
inputStream = fileManager.getInputStream(webConfigFile);
}
else {
inputStream = FileUtils.getInputStream(getClass(),
"webmvc-config.xml");
Validate.notNull(inputStream, "Could not acquire web.xml template");
}
return XmlUtils.readXml(inputStream);
}
public void registerWebFlowConversionServiceExposingInterceptor() {
final String webFlowConfigPath = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, "WEB-INF/spring/webflow-config.xml");
if (!fileManager.exists(webFlowConfigPath)) {
// No web flow configured, moving on.
return;
}
if (!isConversionServiceConfigured()) {
// We only need to install the ConversionServiceExposingInterceptor
// for Web Flow if a custom conversion service is present.
return;
}
final Document document = XmlUtils.readXml(fileManager
.getInputStream(webFlowConfigPath));
final Element root = document.getDocumentElement();
if (XmlUtils.findFirstElement("/beans/bean[@id='"
+ CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME + "']", root) == null) {
final Element conversionServiceExposingInterceptor = new XmlElementBuilder(
"bean", document)
.addAttribute(
"class",
CONVERSION_SERVICE_EXPOSING_INTERCEPTOR
.getFullyQualifiedTypeName())
.addAttribute("id",
CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME)
.addChild(
new XmlElementBuilder("constructor-arg", document)
.addAttribute("ref",
CONVERSION_SERVICE_BEAN_NAME)
.build()).build();
root.appendChild(conversionServiceExposingInterceptor);
}
final Element flowHandlerMapping = XmlUtils.findFirstElement(
"/beans/bean[@class='"
+ FLOW_HANDLER_MAPPING.getFullyQualifiedTypeName()
+ "']", root);
if (flowHandlerMapping != null) {
if (XmlUtils.findFirstElement(
"property[@name='interceptors']/array/ref[@bean='"
+ CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME
+ "']", flowHandlerMapping) == null) {
final Element interceptors = new XmlElementBuilder("property",
document)
.addAttribute("name", "interceptors")
.addChild(
new XmlElementBuilder("array", document)
.addChild(
new XmlElementBuilder("ref",
document)
.addAttribute("bean",
CONVERSION_SERVICE_EXPOSING_INTERCEPTOR_NAME)
.build()).build())
.build();
flowHandlerMapping.appendChild(interceptors);
}
}
fileManager.createOrUpdateTextFileIfRequired(webFlowConfigPath,
XmlUtils.nodeToString(document), false);
}
private void setBasePackageForComponentScan(final Document document) {
final Element componentScanElement = DomUtils.findFirstElementByName(
"context:component-scan", (Element) document.getFirstChild());
final JavaPackage topLevelPackage = projectOperations
.getTopLevelPackage(projectOperations.getFocusedModuleName());
componentScanElement.setAttribute("base-package",
topLevelPackage.getFullyQualifiedPackageName());
}
private void updateConfiguration() {
// Update webmvc-config.xml if needed.
final String webConfigFile = pathResolver.getFocusedIdentifier(
Path.SRC_MAIN_WEBAPP, WEBMVC_CONFIG_XML);
Validate.isTrue(fileManager.exists(webConfigFile),
"Aborting: Unable to find " + webConfigFile);
InputStream webMvcConfigInputStream = null;
try {
webMvcConfigInputStream = fileManager.getInputStream(webConfigFile);
Validate.notNull(webMvcConfigInputStream,
"Aborting: Unable to acquire webmvc-config.xml file");
final Document webMvcConfig = XmlUtils
.readXml(webMvcConfigInputStream);
final Element root = webMvcConfig.getDocumentElement();
if (XmlUtils.findFirstElement("/beans/interceptors", root) == null) {
final InputStream templateInputStream = FileUtils
.getInputStream(getClass(),
"webmvc-config-additions.xml");
Validate.notNull(templateInputStream,
"Could not acquire webmvc-config-additions.xml template");
final Document webMvcConfigAdditions = XmlUtils
.readXml(templateInputStream);
final NodeList nodes = webMvcConfigAdditions
.getDocumentElement().getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
root.appendChild(webMvcConfig.importNode(nodes.item(i),
true));
}
fileManager.createOrUpdateTextFileIfRequired(webConfigFile,
XmlUtils.nodeToString(webMvcConfig), true);
}
}
finally {
IOUtils.closeQuietly(webMvcConfigInputStream);
}
// Add MVC dependencies.
final boolean isGaeEnabled = projectOperations
.isFeatureInstalledInFocusedModule(FeatureNames.GAE);
final Element configuration = XmlUtils.getConfiguration(getClass());
final List<Dependency> dependencies = new ArrayList<Dependency>();
final List<Element> springDependencies = XmlUtils.findElements(
"/configuration/springWebMvc/dependencies/dependency",
configuration);
for (final Element dependencyElement : springDependencies) {
final Dependency dependency = new Dependency(dependencyElement);
if (isGaeEnabled
&& dependency.getGroupId().equals("org.glassfish.web")
&& dependency.getArtifactId().equals("jstl-impl")) {
dependencies.add(new Dependency(dependency.getGroupId(),
dependency.getArtifactId(), dependency.getVersion(),
DependencyType.JAR, DependencyScope.PROVIDED));
}
else {
dependencies.add(dependency);
}
}
projectOperations.addDependencies(
projectOperations.getFocusedModuleName(), dependencies);
projectOperations.updateProjectType(
projectOperations.getFocusedModuleName(), ProjectType.WAR);
}
}