package org.springframework.roo.addon.web.selenium; import static org.springframework.roo.model.JavaType.LONG_OBJECT; import static org.springframework.roo.model.JdkJavaType.BIG_DECIMAL; import static org.springframework.roo.model.Jsr303JavaType.FUTURE; import static org.springframework.roo.model.Jsr303JavaType.MIN; import static org.springframework.roo.model.Jsr303JavaType.PAST; import static org.springframework.roo.model.SpringJavaType.DATE_TIME_FORMAT; import java.io.InputStream; import java.math.BigDecimal; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.logging.Logger; 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.addon.web.mvc.controller.addon.details.WebMetadataService; 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.classpath.PhysicalTypeIdentifier; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.FieldMetadata; import org.springframework.roo.classpath.details.FieldMetadataBuilder; import org.springframework.roo.classpath.details.MemberFindingUtils; import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; import org.springframework.roo.classpath.operations.DateTime; import org.springframework.roo.classpath.persistence.PersistenceMemberLocator; import org.springframework.roo.classpath.scanner.MemberDetails; import org.springframework.roo.classpath.scanner.MemberDetailsScanner; import org.springframework.roo.metadata.MetadataService; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.SpringJavaType; import org.springframework.roo.process.manager.FileManager; 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.Plugin; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.support.logging.HandlerUtils; 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.w3c.dom.Node; /** * Implementation of {@link SeleniumOperations}. * * @author Stefan Schmidt * @author Juan Carlos GarcĂ­a * @since 1.0 */ @Component @Service public class SeleniumOperationsImpl implements SeleniumOperations { private static final Logger LOGGER = HandlerUtils.getLogger(SeleniumOperationsImpl.class); @Reference private FileManager fileManager; @Reference private MemberDetailsScanner memberDetailsScanner; @Reference private MenuOperations menuOperations; @Reference private MetadataService metadataService; @Reference private PathResolver pathResolver; @Reference private PersistenceMemberLocator persistenceMemberLocator; @Reference private ProjectOperations projectOperations; @Reference private TypeLocationService typeLocationService; @Reference private WebMetadataService webMetadataService; private Node clickAndWaitCommand(final Document document, final String linkTarget) { final Node tr = document.createElement("tr"); final Node td1 = tr.appendChild(document.createElement("td")); td1.setTextContent("clickAndWait"); final Node td2 = tr.appendChild(document.createElement("td")); td2.setTextContent(linkTarget); final Node td3 = tr.appendChild(document.createElement("td")); td3.setTextContent(" "); return tr; } private String convertToInitializer(final FieldMetadata field) { String initializer = " "; short index = 1; final AnnotationMetadata min = MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), MIN); if (min != null) { final AnnotationAttributeValue<?> value = min.getAttribute(new JavaSymbolName("value")); if (value != null) { index = new Short(value.getValue().toString()); } } final JavaType fieldType = field.getFieldType(); if (field.getFieldName().getSymbolName().contains("email") || field.getFieldName().getSymbolName().contains("Email")) { initializer = "some@email.com"; } else if (fieldType.equals(JavaType.STRING)) { initializer = "some" + field.getFieldName().getSymbolNameCapitalisedFirstLetter() + index; } else if (fieldType.equals(new JavaType(Date.class.getName())) || fieldType.equals(new JavaType(Calendar.class.getName()))) { final Calendar cal = Calendar.getInstance(); AnnotationMetadata dateTimeFormat = null; String style = null; if ((dateTimeFormat = MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), DATE_TIME_FORMAT)) != null) { final AnnotationAttributeValue<?> value = dateTimeFormat.getAttribute(new JavaSymbolName("style")); if (value != null) { style = value.getValue().toString(); } } if (MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), PAST) != null) { cal.add(Calendar.YEAR, -1); cal.add(Calendar.MONTH, -1); cal.add(Calendar.DAY_OF_MONTH, -1); } else if (MemberFindingUtils.getAnnotationOfType(field.getAnnotations(), FUTURE) != null) { cal.add(Calendar.YEAR, 1); cal.add(Calendar.MONTH, 1); cal.add(Calendar.DAY_OF_MONTH, 1); } if (style != null) { if (style.startsWith("-")) { initializer = ((SimpleDateFormat) DateFormat.getTimeInstance( DateTime.parseDateFormat(style.charAt(1)), Locale.getDefault())).format(cal .getTime()); } else if (style.endsWith("-")) { initializer = ((SimpleDateFormat) DateFormat.getDateInstance( DateTime.parseDateFormat(style.charAt(0)), Locale.getDefault())).format(cal .getTime()); } else { initializer = ((SimpleDateFormat) DateFormat.getDateTimeInstance( DateTime.parseDateFormat(style.charAt(0)), DateTime.parseDateFormat(style.charAt(1)), Locale.getDefault())).format(cal .getTime()); } } else { initializer = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())) .format(cal.getTime()); } } else if (fieldType.equals(JavaType.BOOLEAN_OBJECT) || fieldType.equals(JavaType.BOOLEAN_PRIMITIVE)) { initializer = Boolean.valueOf(false).toString(); } else if (fieldType.equals(JavaType.INT_OBJECT) || fieldType.equals(JavaType.INT_PRIMITIVE)) { initializer = Integer.valueOf(index).toString(); } else if (fieldType.equals(JavaType.DOUBLE_OBJECT) || fieldType.equals(JavaType.DOUBLE_PRIMITIVE)) { initializer = Double.toString(index); } else if (fieldType.equals(JavaType.FLOAT_OBJECT) || fieldType.equals(JavaType.FLOAT_PRIMITIVE)) { initializer = Float.toString(index); } else if (fieldType.equals(LONG_OBJECT) || fieldType.equals(JavaType.LONG_PRIMITIVE)) { initializer = Long.valueOf(index).toString(); } else if (fieldType.equals(JavaType.SHORT_OBJECT) || fieldType.equals(JavaType.SHORT_PRIMITIVE)) { initializer = Short.valueOf(index).toString(); } else if (fieldType.equals(BIG_DECIMAL)) { initializer = new BigDecimal(index).toString(); } return initializer; } /** * Creates Selenium testcase for all registered controllers * * @param serverURL the URL of the Selenium server (optional) */ @Override public void generateAll(String serverURL) { // Getting all registered controllers Set<JavaType> registeredControllers = typeLocationService.findTypesWithAnnotation(SpringJavaType.CONTROLLER); Iterator<JavaType> it = registeredControllers.iterator(); // Iterate all registered controllers while (it.hasNext()) { JavaType controller = it.next(); // Delegating on generateTest function generateTest(controller, "", serverURL); } } /** * Creates a new Selenium testcase * * @param controller the JavaType of the controller under test (required) * @param name the name of the test case (optional) * @param serverURL the URL of the Selenium server (optional) */ @Override public void generateTest(final JavaType controller, String name, String serverURL) { Validate.notNull(controller, "Controller type required"); final ClassOrInterfaceTypeDetails controllerTypeDetails = typeLocationService.getTypeDetails(controller); Validate.notNull(controllerTypeDetails, "Class or interface type details for type '%s' could not be resolved", controller); final LogicalPath path = PhysicalTypeIdentifier.getPath(controllerTypeDetails.getDeclaredByMetadataId()); final String webScaffoldMetadataIdentifier = WebScaffoldMetadata.createIdentifier(controller, path); final WebScaffoldMetadata webScaffoldMetadata = (WebScaffoldMetadata) metadataService.get(webScaffoldMetadataIdentifier); Validate.notNull(webScaffoldMetadata, "Web controller '%s' does not appear to be an automatic, scaffolded controller", controller.getFullyQualifiedTypeName()); // We abort the creation of a selenium test if the controller does not // allow the creation of new instances for the form backing object if (!webScaffoldMetadata.getAnnotationValues().isCreate()) { LOGGER .warning("The controller you specified does not allow the creation of new instances of the form backing object. No Selenium tests created."); return; } if (!serverURL.endsWith("/")) { serverURL = serverURL + "/"; } final JavaType formBackingType = webScaffoldMetadata.getAnnotationValues().getFormBackingObject(); final String relativeTestFilePath = "selenium/test-" + formBackingType.getSimpleTypeName().toLowerCase() + ".xhtml"; final String seleniumPath = pathResolver.getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, relativeTestFilePath); final InputStream templateInputStream = FileUtils.getInputStream(getClass(), "selenium-template.xhtml"); Validate.notNull(templateInputStream, "Could not acquire selenium.xhtml template"); final Document document = XmlUtils.readXml(templateInputStream); final Element root = (Element) document.getLastChild(); if (root == null || !"html".equals(root.getNodeName())) { throw new IllegalArgumentException("Could not parse selenium test case template file!"); } name = name != null ? name : "Selenium test for " + controller.getSimpleTypeName(); XmlUtils.findRequiredElement("/html/head/title", root).setTextContent(name); XmlUtils.findRequiredElement("/html/body/table/thead/tr/td", root).setTextContent(name); final Element tbody = XmlUtils.findRequiredElement("/html/body/table/tbody", root); tbody.appendChild(openCommand(document, serverURL + projectOperations.getProjectName(projectOperations.getFocusedModuleName()) + "/" + webScaffoldMetadata.getAnnotationValues().getPath() + "?form")); final ClassOrInterfaceTypeDetails formBackingTypeDetails = typeLocationService.getTypeDetails(formBackingType); Validate.notNull(formBackingType, "Class or interface type details for type '%s' could not be resolved", formBackingType); final MemberDetails memberDetails = memberDetailsScanner.getMemberDetails(getClass().getName(), formBackingTypeDetails); // Add composite PK identifier fields if needed for (final FieldMetadata field : persistenceMemberLocator .getEmbeddedIdentifierFields(formBackingType)) { final JavaType fieldType = field.getFieldType(); if (!fieldType.isCommonCollectionType() && !isSpecialType(fieldType)) { final FieldMetadataBuilder fieldBuilder = new FieldMetadataBuilder(field); final String fieldName = field.getFieldName().getSymbolName(); fieldBuilder.setFieldName(new JavaSymbolName(fieldName + "." + fieldName)); tbody.appendChild(typeCommand(document, fieldBuilder.build())); } } // Add all other fields final List<FieldMetadata> fields = webMetadataService.getScaffoldEligibleFieldMetadata(formBackingType, memberDetails, null); for (final FieldMetadata field : fields) { final JavaType fieldType = field.getFieldType(); if (!fieldType.isCommonCollectionType() && !isSpecialType(fieldType)) { tbody.appendChild(typeCommand(document, field)); } } tbody.appendChild(clickAndWaitCommand(document, "//input[@id = 'proceed']")); // Add verifications for all other fields for (final FieldMetadata field : fields) { final JavaType fieldType = field.getFieldType(); if (!fieldType.isCommonCollectionType() && !isSpecialType(fieldType)) { tbody.appendChild(verifyTextCommand(document, formBackingType, field)); } } fileManager.createOrUpdateTextFileIfRequired(seleniumPath, XmlUtils.nodeToString(document), false); manageTestSuite(relativeTestFilePath, name, serverURL); // Install selenium-maven-plugin installMavenPlugin(); } private void installMavenPlugin() { // Stop if the plugin is already installed for (final Plugin plugin : projectOperations.getFocusedModule().getBuildPlugins()) { if (plugin.getArtifactId().equals("selenium-maven-plugin")) { return; } } final Element configuration = XmlUtils.getConfiguration(getClass()); final Element plugin = XmlUtils.findFirstElement("/configuration/selenium/plugin", configuration); // Now install the plugin itself if (plugin != null) { projectOperations .addBuildPlugin(projectOperations.getFocusedModuleName(), new Plugin(plugin)); } } public boolean isSeleniumInstallationPossible() { return projectOperations.isFocusedProjectAvailable() && projectOperations.isFeatureInstalled(FeatureNames.MVC); } private boolean isSpecialType(final JavaType javaType) { return typeLocationService.isInProject(javaType); } private void manageTestSuite(final String testPath, final String name, final String serverURL) { final String relativeTestFilePath = "selenium/test-suite.xhtml"; final String seleniumPath = pathResolver.getFocusedIdentifier(Path.SRC_MAIN_WEBAPP, relativeTestFilePath); final InputStream inputStream; if (fileManager.exists(seleniumPath)) { inputStream = fileManager.getInputStream(seleniumPath); } else { inputStream = FileUtils.getInputStream(getClass(), "selenium-test-suite-template.xhtml"); Validate.notNull(inputStream, "Could not acquire selenium test suite template"); } final Document suite = XmlUtils.readXml(inputStream); final Element root = (Element) suite.getLastChild(); XmlUtils.findRequiredElement("/html/head/title", root).setTextContent( "Test suite for " + projectOperations.getProjectName(projectOperations.getFocusedModuleName()) + "project"); final Element tr = suite.createElement("tr"); final Element td = suite.createElement("td"); tr.appendChild(td); final Element a = suite.createElement("a"); a.setAttribute("href", serverURL + projectOperations.getProjectName(projectOperations.getFocusedModuleName()) + "/resources/" + testPath); a.setTextContent(name); td.appendChild(a); XmlUtils.findRequiredElement("/html/body/table", root).appendChild(tr); fileManager.createOrUpdateTextFileIfRequired(seleniumPath, XmlUtils.nodeToString(suite), false); menuOperations.addMenuItem(new JavaSymbolName("SeleniumTests"), new JavaSymbolName("Test"), "Test", "selenium_menu_test_suite", "/resources/" + relativeTestFilePath, "si_", pathResolver.getFocusedPath(Path.SRC_MAIN_WEBAPP)); } private Node openCommand(final Document document, final String linkTarget) { final Node tr = document.createElement("tr"); final Node td1 = tr.appendChild(document.createElement("td")); td1.setTextContent("open"); final Node td2 = tr.appendChild(document.createElement("td")); td2.setTextContent(linkTarget + (linkTarget.contains("?") ? "&" : "?") + "lang=" + Locale.getDefault()); final Node td3 = tr.appendChild(document.createElement("td")); td3.setTextContent(" "); return tr; } private Node typeCommand(final Document document, final FieldMetadata field) { final Node tr = document.createElement("tr"); final Node td1 = tr.appendChild(document.createElement("td")); td1.setTextContent("type"); final Node td2 = tr.appendChild(document.createElement("td")); td2.setTextContent("_" + field.getFieldName().getSymbolName() + "_id"); final Node td3 = tr.appendChild(document.createElement("td")); td3.setTextContent(convertToInitializer(field)); return tr; } private Node verifyTextCommand(final Document document, final JavaType formBackingType, final FieldMetadata field) { final Node tr = document.createElement("tr"); final Node td1 = tr.appendChild(document.createElement("td")); td1.setTextContent("verifyText"); final Node td2 = tr.appendChild(document.createElement("td")); td2.setTextContent(XmlUtils.convertId("_s_" + formBackingType.getFullyQualifiedTypeName() + "_" + field.getFieldName().getSymbolName() + "_" + field.getFieldName().getSymbolName() + "_id")); final Node td3 = tr.appendChild(document.createElement("td")); td3.setTextContent(convertToInitializer(field)); return tr; } }