package org.springframework.roo.addon.test;
import static org.springframework.roo.shell.OptionContexts.APPLICATION_FEATURE_INCLUDE_CURRENT_MODULE;
import static org.springframework.roo.shell.OptionContexts.UPDATE_PROJECT;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
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.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.test.providers.TestCreatorProvider;
import org.springframework.roo.classpath.ModuleFeatureName;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.BeanInfoUtils;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.converters.LastUsed;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.ReservedWords;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.maven.Pom;
import org.springframework.roo.shell.CliAvailabilityIndicator;
import org.springframework.roo.shell.CliCommand;
import org.springframework.roo.shell.CliOption;
import org.springframework.roo.shell.CliOptionAutocompleteIndicator;
import org.springframework.roo.shell.CliOptionMandatoryIndicator;
import org.springframework.roo.shell.CliOptionVisibilityIndicator;
import org.springframework.roo.shell.CommandMarker;
import org.springframework.roo.shell.ShellContext;
import org.springframework.roo.support.logging.HandlerUtils;
/**
* Shell commands for {@link UnitTestOperationsImpl}.
*
* @author Sergio Clares
* @since 2.0
*/
@Component
@Service
public class TestCommands implements CommandMarker {
protected final static Logger LOGGER = HandlerUtils.getLogger(TestCommands.class);
private BundleContext context;
@Reference
private TestOperations testOperations;
@Reference
private TypeLocationService typeLocationService;
@Reference
private ProjectOperations projectOperations;
@Reference
private LastUsed lastUsed;
// TestCreatorProvider implementations
private List<TestCreatorProvider> testCreators = new ArrayList<TestCreatorProvider>();
protected void activate(final ComponentContext context) {
this.context = context.getBundleContext();
}
protected void deactivate(final ComponentContext context) {
this.context = null;
}
@CliAvailabilityIndicator({"test unit"})
public boolean isTestUnitCommandAvailable() {
JavaType type = lastUsed.getJavaType();
if (type != null) {
for (TestCreatorProvider provider : getValidTestCreatorsForType(type)) {
if (provider.isUnitTestCreationAvailable()) {
return true;
}
}
}
return getUnitTestCreationAvailable();
}
@CliOptionAutocompleteIndicator(command = "test unit", help = "Option `--class` must "
+ "be a non-abstract valid type. Please, use auto-complete feature to select it.",
param = "class")
public List<String> getClassPosibleValues(ShellContext shellContext) {
// Get current value of class
String currentText = shellContext.getParameters().get("class");
// Create results to return
List<String> results = new ArrayList<String>();
// Look for all valid types for all available test creators
for (TestCreatorProvider creator : getAllTestCreators()) {
if (creator.isUnitTestCreationAvailable()) {
for (JavaType annotationType : creator.getValidTypes()) {
// Look for types with this annotation type
Set<ClassOrInterfaceTypeDetails> types =
typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(annotationType);
for (ClassOrInterfaceTypeDetails typeCid : types) {
String name = replaceTopLevelPackageString(typeCid.getType(), currentText);
if (!results.contains(name) && !typeCid.isAbstract()) {
results.add(name);
}
}
}
}
}
return results;
}
@CliCommand(value = "test unit",
help = "Creates a unit test class with a basic structure and with the "
+ "necessary testing components, for the specified class.")
public void newMockTest(
@CliOption(
key = "class",
mandatory = true,
optionContext = UPDATE_PROJECT,
help = "The name of the project class which this unit test class is targeting. If you consider "
+ "it necessary, you can also specify the package. Ex.: `--class ~.model.MyClass` (where "
+ "`~` is the base package). When working with multiple modules, you should specify the name"
+ " of the class and the module where it is. Ex.: `--class model:~.MyClass`. If the module "
+ "is not specified, it is assumed that the class is in the module which has the focus. ") final JavaType type,
@CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Indicates whether reserved words are ignored by Roo. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean permitReservedWords) {
if (!permitReservedWords) {
ReservedWords.verifyReservedWordsNotPresent(type);
}
Validate
.isTrue(
BeanInfoUtils.isEntityReasonablyNamed(type),
"Cannot create an integration test for an entity named 'Test' or 'TestCase' under any circumstances");
testOperations.createUnitTest(type);
}
@CliAvailabilityIndicator({"test integration"})
public boolean isTestIntegrationCommandAvailable() {
JavaType type = lastUsed.getJavaType();
if (type != null) {
for (TestCreatorProvider provider : getValidTestCreatorsForType(type)) {
if (provider.isIntegrationTestCreationAvailable()) {
return true;
}
}
}
return getIntegrationTestCreationAvailable();
}
@CliOptionVisibilityIndicator(command = "test integration", params = {"module"},
help = "Module parameter is not available if there is only one application module")
public boolean isModuleVisible(ShellContext shellContext) {
if (typeLocationService.getModuleNames(ModuleFeatureName.APPLICATION).size() > 1) {
return true;
}
return false;
}
@CliOptionMandatoryIndicator(command = "test integration", params = {"module"})
public boolean isModuleRequired(ShellContext shellContext) {
Pom module = projectOperations.getFocusedModule();
if (!isModuleVisible(shellContext)
|| typeLocationService.hasModuleFeature(module, ModuleFeatureName.APPLICATION)) {
return false;
}
return true;
}
@CliOptionAutocompleteIndicator(command = "test integration", param = "class",
help = "Option `--class` must "
+ "be a non-abstract valid type. Please, use auto-complete feature to select it.")
public List<String> getAllEntities(ShellContext shellContext) {
// Get current value of class
String currentText = shellContext.getParameters().get("class");
// Create results to return
List<String> results = new ArrayList<String>();
// Look for all valid types for all available test creators
for (TestCreatorProvider creator : getAllTestCreators()) {
if (creator.isIntegrationTestCreationAvailable()) {
for (JavaType annotationType : creator.getValidTypes()) {
// Look for types with this annotation type
Set<ClassOrInterfaceTypeDetails> types =
typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(annotationType);
for (ClassOrInterfaceTypeDetails typeCid : types) {
String name = replaceTopLevelPackageString(typeCid.getType(), currentText);
if (!results.contains(name)) {
results.add(name);
}
}
}
}
}
return results;
}
@CliCommand(value = "test integration",
help = "Creates a new integration test class for the specified class. The generated test "
+ "class will contain a basic structure and the necessary testing components.")
public void newIntegrationTest(
@CliOption(
key = "class",
mandatory = false,
unspecifiedDefaultValue = "*",
help = "The name of the class to create an integration test. If you consider it necessary, you can "
+ "also specify the package. Ex.: `--class ~.package.MyClass` (where `~` is the "
+ "base package). When working with multiple modules, you should specify the name of the "
+ "class and the module where it is. Ex.: `--class module:~.MyClass`. If the "
+ "module is not specified, it is assumed that the class is in the module which has the "
+ "focus. "
+ "Possible values are: any of the valid classes in the project which support "
+ "automatically integration test creation.") final JavaType klass,
@CliOption(
key = "module",
mandatory = true,
help = "The application module where generate the integration test. "
+ "This option is mandatory if the focus is not set in an 'application' module and there "
+ "are more than one 'application' modules, that is, a module containing an "
+ "`@SpringBootApplication` class. "
+ "This option is available only if there are more than one application module and none of"
+ " them is focused. "
+ "Default if option not present: the unique 'application' module, or focused 'application'"
+ " module.", unspecifiedDefaultValue = ".",
optionContext = APPLICATION_FEATURE_INCLUDE_CURRENT_MODULE) Pom module,
@CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Indicates whether reserved words are ignored by Roo. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean permitReservedWords) {
if (!permitReservedWords) {
ReservedWords.verifyReservedWordsNotPresent(klass);
}
Validate
.isTrue(
BeanInfoUtils.isEntityReasonablyNamed(klass),
"Cannot create an integration test for an entity named 'Test' or 'TestCase' under any circumstances");
testOperations.createIntegrationTest(klass, module);
}
/**
* Replaces a JavaType fullyQualifiedName for a shorter name using '~' for
* TopLevelPackage
*
* @param type ClassOrInterfaceTypeDetails of a JavaType
* @param currentText String current text for option value
* @return the String representing a JavaType with its name shortened
*/
private String replaceTopLevelPackageString(JavaType type, String currentText) {
String javaTypeFullyQualilfiedName = type.getFullyQualifiedTypeName();
String javaTypeString = "";
String topLevelPackageString = "";
// Add module value to topLevelPackage when necessary
if (StringUtils.isNotBlank(type.getModule())
&& !type.getModule().equals(projectOperations.getFocusedModuleName())) {
// Target module is not focused
javaTypeString = type.getModule().concat(LogicalPath.MODULE_PATH_SEPARATOR);
topLevelPackageString =
projectOperations.getTopLevelPackage(type.getModule()).getFullyQualifiedPackageName();
} else if (StringUtils.isNotBlank(type.getModule())
&& type.getModule().equals(projectOperations.getFocusedModuleName())
&& (currentText.startsWith(type.getModule()) || type.getModule().startsWith(currentText))
&& StringUtils.isNotBlank(currentText)) {
// Target module is focused but user wrote it
javaTypeString = type.getModule().concat(LogicalPath.MODULE_PATH_SEPARATOR);
topLevelPackageString =
projectOperations.getTopLevelPackage(type.getModule()).getFullyQualifiedPackageName();
} else {
// Not multimodule project
topLevelPackageString =
projectOperations.getFocusedTopLevelPackage().getFullyQualifiedPackageName();
}
// Autocomplete with abbreviate or full qualified mode
String auxString =
javaTypeString.concat(StringUtils.replace(javaTypeFullyQualilfiedName,
topLevelPackageString, "~"));
if ((StringUtils.isBlank(currentText) || auxString.startsWith(currentText))
&& StringUtils.contains(javaTypeFullyQualilfiedName, topLevelPackageString)) {
// Value is for autocomplete only or user wrote abbreviate value
javaTypeString = auxString;
} else {
// Value could be for autocomplete or for validation
javaTypeString = String.format("%s%s", javaTypeString, javaTypeFullyQualilfiedName);
}
return javaTypeString;
}
/**
* Gets all the valid implementations of TestCreatorProvider for a JavaType.
*
* @param type the JavaType to get the valid implementations.
* @return a `List` with the {@link TestCreatorProvider} valid
* implementations. Never `null`.
*/
public List<TestCreatorProvider> getValidTestCreatorsForType(JavaType type) {
// Get all Services implement TestCreatorProvider interface
if (this.testCreators.isEmpty()) {
try {
ServiceReference<?>[] references =
this.context.getAllServiceReferences(TestCreatorProvider.class.getName(), null);
for (ServiceReference<?> ref : references) {
TestCreatorProvider testCreatorProvider =
(TestCreatorProvider) this.context.getService(ref);
this.testCreators.add(testCreatorProvider);
}
} catch (InvalidSyntaxException e) {
LOGGER.warning("Cannot load TestCreatorProvider on TestCommands.");
return null;
}
}
List<TestCreatorProvider> validTestCreators = new ArrayList<TestCreatorProvider>();
for (TestCreatorProvider provider : this.testCreators) {
if (provider.isValid(type)) {
validTestCreators.add(provider);
}
}
return validTestCreators;
}
/**
* Gets all the implementations of TestCreatorProvider
*
* @param type the JavaType to get the valid implementations.
* @return a `List` with the {@link TestCreatorProvider} valid
* implementations. Never `null`.
*/
public List<TestCreatorProvider> getAllTestCreators() {
// Get all Services implement TestCreatorProvider interface
if (this.testCreators.isEmpty()) {
try {
ServiceReference<?>[] references =
this.context.getAllServiceReferences(TestCreatorProvider.class.getName(), null);
for (ServiceReference<?> ref : references) {
TestCreatorProvider testCreatorProvider =
(TestCreatorProvider) this.context.getService(ref);
this.testCreators.add(testCreatorProvider);
}
} catch (InvalidSyntaxException e) {
LOGGER.warning("Cannot load TestCreatorProvider on TestCommands.");
return null;
}
}
return this.testCreators;
}
/**
* Checks all {@link TestCreatorProvider} implementations looking for any
* available for 'test unit' command.
*
* @return `true` if any of the implementations is available or
* `false` if none of the implementations are available.
*/
private boolean getUnitTestCreationAvailable() {
// Get all Services implement TestCreatorProvider interface
if (this.testCreators.isEmpty()) {
try {
ServiceReference<?>[] references =
this.context.getAllServiceReferences(TestCreatorProvider.class.getName(), null);
for (ServiceReference<?> ref : references) {
TestCreatorProvider testCreatorProvider =
(TestCreatorProvider) this.context.getService(ref);
this.testCreators.add(testCreatorProvider);
}
} catch (InvalidSyntaxException e) {
LOGGER.warning("Cannot load TestCreatorProvider on TestCommands.");
return false;
}
}
for (TestCreatorProvider provider : this.testCreators) {
if (provider.isUnitTestCreationAvailable()) {
return true;
}
}
return false;
}
/**
* Checks all {@link TestCreatorProvider} implementations looking for any
* available for 'test integration' command.
*
* @return `true` if any of the implementations is available or
* `false` if none of the implementations are available.
*/
private boolean getIntegrationTestCreationAvailable() {
// Get all Services implement TestCreatorProvider interface
if (this.testCreators.isEmpty()) {
try {
ServiceReference<?>[] references =
this.context.getAllServiceReferences(TestCreatorProvider.class.getName(), null);
for (ServiceReference<?> ref : references) {
TestCreatorProvider testCreatorProvider =
(TestCreatorProvider) this.context.getService(ref);
this.testCreators.add(testCreatorProvider);
}
} catch (InvalidSyntaxException e) {
LOGGER.warning("Cannot load TestCreatorProvider on UnitTestCommands.");
return false;
}
}
for (TestCreatorProvider provider : this.testCreators) {
if (provider.isIntegrationTestCreationAvailable()) {
return true;
}
}
return false;
}
}