package org.springframework.roo.addon.webflow;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.Properties;
import java.util.Set;
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.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.javabean.annotations.RooJavaBean;
import org.springframework.roo.addon.web.mvc.controller.addon.responses.ControllerMVCResponseService;
import org.springframework.roo.addon.web.mvc.i18n.components.I18n;
import org.springframework.roo.addon.web.mvc.i18n.components.I18nSupport;
import org.springframework.roo.addon.web.mvc.i18n.languages.EnglishLanguage;
import org.springframework.roo.addon.web.mvc.thymeleaf.addon.ThymeleafMVCViewResponseService;
import org.springframework.roo.addon.web.mvc.thymeleaf.addon.ThymeleafMetadata;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.operations.AbstractOperations;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.RooJavaType;
import org.springframework.roo.project.Dependency;
import org.springframework.roo.project.FeatureNames;
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.propfiles.manager.PropFilesManagerService;
import org.springframework.roo.support.ant.AntPathMatcher;
import org.springframework.roo.support.osgi.OSGiUtils;
import org.springframework.roo.support.osgi.ServiceInstaceManager;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Provides Web Flow configuration operations.
*
* @author Stefan Schmidt
* @author Rossen Stoyanchev
* @author Sergio Clares
*
* @since 1.0
*/
@Component
@Service
public class WebFlowOperationsImpl extends AbstractOperations implements WebFlowOperations {
private static final Dependency SPRINGLETS_WEBFLOW_STARTER = new Dependency("io.springlets",
"springlets-boot-starter-webflow", "${springlets.version}");
/**
* Get a reference to ProjectOperations to be able to manage generated project.
*/
@Reference
private ProjectOperations projectOperations;
/**
* Get a reference to PathResolver to be able to resolve resources directory.
*/
@Reference
private PathResolver pathResolver;
/**
* Get a reference to I18nSupport to be able to get Locale codes.
*/
@Reference
private I18nSupport i18nSupport;
/**
* Get a reference to PropFilesManagerService to be able to manage locale message bundles.
*/
@Reference
private PropFilesManagerService propFilesManagerService;
/**
* Instantiate ServiceInstanceManager for an easy acces to Spring Roo services.
* It should be activated from context.
*/
private ServiceInstaceManager serviceManager = new ServiceInstaceManager();
private String flowName = "";
/**
* The activate method for this OSGi component, this will be called by the OSGi
* container upon bundle activation (result of the 'addon install' command).
*
* @param context the component context can be used to get access to the OSGi
* container (ie find out if certain bundles are active).
*/
protected void activate(ComponentContext context) {
super.activate(context);
this.serviceManager.activate(this.context);
}
/**
* See {@link WebFlowOperations#installWebFlow(String, String)}.
*/
@Override
public void installWebFlow(final String flowName, final String moduleName, JavaType klass) {
this.flowName = flowName.toLowerCase();
// Add WebFlow project configuration
installWebFlowConfiguration(moduleName);
String targetDirectory =
pathResolver.getIdentifier(moduleName, Path.SRC_MAIN_RESOURCES,
"/templates/".concat(this.flowName));
if (fileManager.exists(targetDirectory)) {
throw new IllegalStateException("Flow directory already exists: " + targetDirectory);
}
// Copy Web Flow template views and *-flow.xml to project
Map<String, String> replacements = new HashMap<String, String>();
replacements.put("__WEBFLOW-ID__", this.flowName);
copyDirectoryContents("*.html", targetDirectory, replacements, klass);
createWebFlowFromTemplate(targetDirectory, replacements, klass);
// Add localized messages for Web Flow labels
addLocalizedMessages(moduleName);
// Getting all thymeleaf controllers
Set<ClassOrInterfaceTypeDetails> thymeleafControllers =
getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(
RooJavaType.ROO_THYMELEAF);
if (thymeleafControllers.isEmpty()) {
LOGGER
.log(Level.INFO,
"WARNING: Menu view has not been updated because doesn't exists any Thymeleaf controller.");
return;
}
// Update menu calling to the thymeleaf Metadata of the annotated @RooThymeleaf
Iterator<ClassOrInterfaceTypeDetails> it = thymeleafControllers.iterator();
ClassOrInterfaceTypeDetails thymeleafController = it.next();
String controllerMetadataKey = ThymeleafMetadata.createIdentifier(thymeleafController);
getMetadataService().evictAndGet(controllerMetadataKey);
}
/**
* Add this add-on localized messages from its message bundles to the project's
* message bundles, for each installed language, plus English. Existing messages
* will be replaced.
*
* @param moduleName the module name where the message bundles are.
* @param flowName the name/id of the flow to prefix the messages.
*/
private void addLocalizedMessages(String moduleName) {
// Install localized messages for each installed language
for (I18n i18n : i18nSupport.getSupportedLanguages()) {
if (i18n.getLanguage().equals(new EnglishLanguage().getLanguage())) {
continue;
}
// Get theme specific messages
InputStream themeMessagesInputStream = null;
try {
themeMessagesInputStream =
FileUtils.getInputStream(getClass(),
String.format("messages_%s.properties", i18n.getLocale()));
} catch (NullPointerException ex) {
LOGGER
.warning(String
.format(
"There aren't translations for %1$s language. Adding english messages to messages_%1$s.properties instead.",
i18n.getLocale()));
themeMessagesInputStream =
FileUtils.getInputStream(getClass(),
String.format("messages.properties", i18n.getLocale()));
}
final Properties loadedProperties =
propFilesManagerService.loadProperties(themeMessagesInputStream);
// Add theme messages to localized message bundle
final LogicalPath resourcesPath =
LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, moduleName);
final String targetDirectory = pathResolver.getIdentifier(resourcesPath, "");
String bundlePath =
String.format("%s%smessages_%s.properties", targetDirectory,
AntPathMatcher.DEFAULT_PATH_SEPARATOR, i18n.getLocale());
if (fileManager.exists(bundlePath)) {
Map<String, String> newProperties = new HashMap<String, String>();
for (Entry<Object, Object> entry : loadedProperties.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
// Prefix with flow name
key = String.format("%s_%s", this.flowName.toLowerCase(), key);
newProperties.put(key, value);
newProperties.put("label_".concat(this.flowName.toLowerCase()),
StringUtils.capitalize(this.flowName));
}
propFilesManagerService.addProperties(resourcesPath,
String.format("messages_%s.properties", i18n.getLocale()), newProperties, true, true);
}
// Close InputStream
try {
themeMessagesInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// Always install english messages
InputStream themeMessagesInputStream =
FileUtils.getInputStream(getClass(), "messages.properties");
EnglishLanguage english = new EnglishLanguage();
final Properties loadedProperties =
propFilesManagerService.loadProperties(themeMessagesInputStream);
// Add theme messages to localized message bundle
final LogicalPath resourcesPath = LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, moduleName);
final String targetDirectory = pathResolver.getIdentifier(resourcesPath, "");
String bundlePath =
String.format("%s%smessages.properties", targetDirectory,
AntPathMatcher.DEFAULT_PATH_SEPARATOR, english.getLocale());
if (fileManager.exists(bundlePath)) {
Map<String, String> newProperties = new HashMap<String, String>();
for (Entry<Object, Object> entry : loadedProperties.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
// Prefix with flow name
key = String.format("%s_%s", this.flowName.toLowerCase(), key);
newProperties.put(key, value);
newProperties.put("label_".concat(this.flowName.toLowerCase()),
StringUtils.capitalize(this.flowName));
}
propFilesManagerService.addProperties(resourcesPath, "messages.properties", newProperties,
true, true);
}
// Close InputStream
try {
themeMessagesInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Add model object to each flow view, using flowScope and "model" attribute
*
* @param inputStream the InputStream the file whose contents should be modified
* @param modelObject the object used to create the element's attribute values
*
* @return the Document representing the file to write
*/
private Document addModelObjectToFlow(InputStream inputStream, JavaType modelObject) {
Document document = XmlUtils.readXml(inputStream);
Element documentElement = document.getDocumentElement();
// Find first view state
Element firstViewState = XmlUtils.findFirstElement("//view-state", documentElement);
// Set model object name to use
String modelObjectName = StringUtils.uncapitalize(modelObject.getSimpleTypeName());
// Create 'on-start' element
Element onStartElement = document.createElement("on-start");
Element setElement = document.createElement("set");
setElement.setAttribute("name", String.format("flowScope.%s", modelObjectName));
setElement.setAttribute("value",
String.format("new %s()", modelObject.getFullyQualifiedTypeName()));
onStartElement.appendChild(setElement);
// Insert the element
documentElement.insertBefore(onStartElement, firstViewState.getPreviousSibling()
.getPreviousSibling());
// Add model attribute to views
List<Element> viewElements = XmlUtils.findElements("//view-state", documentElement);
for (Element element : viewElements) {
element.setAttribute("model", modelObjectName);
}
return document;
}
/**
* Creates a new *-flow.xml for a given flow name and target directory, following
* the default template.
*
* @param targetDirectory the directory path where create the file.
* @param replacements the map with String replacements to do in the template
*/
private void createWebFlowFromTemplate(String targetDirectory, Map<String, String> replacements,
JavaType modelObject) {
String fileIdentifier = targetDirectory.concat(String.format("/%s-flow.xml", this.flowName));
InputStream inputStream = FileUtils.getInputStream(this.getClass(), "flow-template.xml");
// Create new file in project with specific name
OutputStream outputStream = fileManager.createFile(fileIdentifier).getOutputStream();
try {
String contents = IOUtils.toString(inputStream);
// Do replacements if needed
if (!replacements.isEmpty()) {
for (Entry<String, String> entry : replacements.entrySet()) {
contents = contents.replace(entry.getKey(), entry.getValue());
}
inputStream = IOUtils.toInputStream(contents);
}
if (modelObject != null) {
// Add model object (if any) to flow
XmlUtils.writeXml(outputStream, addModelObjectToFlow(inputStream, modelObject));
} else {
// Copy input to output file
IOUtils.copy(inputStream, outputStream);
}
} catch (IOException e) {
throw new IllegalStateException(String.format("Unable to create '%s'", fileIdentifier), e);
}
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
}
/**
* Add Springlets Web Flow dependency to project, which manages WebFlow
* configuration of the project.
*
* @param moduleName the module name where dependency should be added.
*/
private void installWebFlowConfiguration(String moduleName) {
projectOperations.addDependency(moduleName, SPRINGLETS_WEBFLOW_STARTER);
}
/**
* This method will copy the contents of a directory to another if the
* resource does not already exist in the target directory. Also, it makes
* replacements of strings which could exist with the provided Map.
*
* @param sourceAntPath the source path
* @param targetDirectory the target directory
* @param replacements the Map with replacements to do in the content
*/
public void copyDirectoryContents(final String sourceAntPath, String targetDirectory,
Map<String, String> replacements, JavaType modelObject) {
Validate.notBlank(sourceAntPath, "Source path required");
Validate.notBlank(targetDirectory, "Target directory required");
if (!targetDirectory.endsWith("/")) {
targetDirectory += "/";
}
if (!fileManager.exists(targetDirectory)) {
fileManager.createDirectory(targetDirectory);
}
// Check if should do replacements
boolean doReplacements = false;
if (!replacements.isEmpty()) {
doReplacements = true;
}
final String path = FileUtils.getPath(getClass(), sourceAntPath);
final Iterable<URL> urls = OSGiUtils.findEntriesByPattern(context, path);
Validate.notNull(urls, "Could not search bundles for resources for Ant Path '%s'", path);
for (final URL url : urls) {
final String fileName = url.getPath().substring(url.getPath().lastIndexOf("/") + 1);
try {
String contents = IOUtils.toString(url);
// Add model object to view form
if (modelObject != null && contents.contains("data-th-action=\"${flowExecutionUrl}\"")) {
contents =
StringUtils.replace(contents, "data-th-action=\"${flowExecutionUrl}\"", String
.format("data-th-action=\"${flowExecutionUrl}\" data-th-object=\"${%s}\"",
StringUtils.uncapitalize(modelObject.getSimpleTypeName())));
}
// Do replacements if necessary
if (doReplacements) {
for (Entry<String, String> entry : replacements.entrySet()) {
contents = contents.replace(entry.getKey(), entry.getValue());
}
}
fileManager.createOrUpdateTextFileIfRequired(targetDirectory + fileName, contents, false);
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
}
@Override
public boolean isWebFlowInstallationPossible() {
if (getThymeleafViewResponseService() == null) {
return false;
}
// Check if Thymeleaf view support is installed in any module
boolean thymeleafInstalled = false;
List<ControllerMVCResponseService> responseServices = getThymeleafViewResponseService();
for (String moduleName : projectOperations.getModuleNames()) {
for (ControllerMVCResponseService responseService : responseServices) {
if (responseService.isInstalledInModule(moduleName)) {
thymeleafInstalled = true;
break;
}
}
}
return projectOperations.isFeatureInstalled(FeatureNames.MVC) && thymeleafInstalled;
}
/**
* Returns {@link ThymeleafMVCViewResponseService} if available.
*
* @return a list with {@link ControllerMVCResponseService} that match with
* ThymeleafMVCViewResponseService (usually one).
*/
public List<ControllerMVCResponseService> getThymeleafViewResponseService() {
return this.serviceManager.getServiceInstance(this, ControllerMVCResponseService.class,
new ServiceInstaceManager.Matcher<ControllerMVCResponseService>() {
@Override
public boolean match(ControllerMVCResponseService service) {
if (service instanceof ThymeleafMVCViewResponseService) {
return true;
}
return false;
}
});
}
public MetadataService getMetadataService() {
return this.serviceManager.getServiceInstance(this, MetadataService.class);
}
public TypeLocationService getTypeLocationService() {
return this.serviceManager.getServiceInstance(this, TypeLocationService.class);
}
}