package org.springframework.roo.converters;
import static org.springframework.roo.converters.JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL;
import static org.springframework.roo.project.LogicalPath.MODULE_PATH_SEPARATOR;
import static org.springframework.roo.shell.OptionContexts.ENUMERATION;
import static org.springframework.roo.shell.OptionContexts.INTERFACE;
import static org.springframework.roo.shell.OptionContexts.PROJECT;
import static org.springframework.roo.shell.OptionContexts.SUPERCLASS;
import static org.springframework.roo.shell.OptionContexts.UPDATE;
import static org.springframework.roo.shell.OptionContexts.UPDATELAST;
import static org.springframework.roo.support.util.AnsiEscapeCode.FG_CYAN;
import static org.springframework.roo.support.util.AnsiEscapeCode.decorate;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
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.classpath.PhysicalTypeCategory;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.maven.Pom;
import org.springframework.roo.shell.Completion;
import org.springframework.roo.shell.Converter;
import org.springframework.roo.shell.MethodTarget;
/**
* Provides conversion to and from {@link JavaType}, with full support for using
* {@value JavaPackageConverter#TOP_LEVEL_PACKAGE_SYMBOL} as denoting the user's
* top-level package.
*
* @author Ben Alex
* @since 1.0
*/
@Component
@Service
public class JavaTypeConverter implements Converter<JavaType> {
/**
* The value that converts to the most recently used {@link JavaType}.
*/
static final String LAST_USED_INDICATOR = "*";
private static final List<String> NUMBER_PRIMITIVES = Arrays.asList("byte", "short", "int",
"long", "float", "double");
@Reference
FileManager fileManager;
@Reference
LastUsed lastUsed;
@Reference
ProjectOperations projectOperations;
@Reference
TypeLocationService typeLocationService;
public JavaType convertFromText(String value, final Class<?> requiredType,
final String optionContext) {
if (StringUtils.isBlank(value)) {
return null;
}
// Check for number primitives
if (NUMBER_PRIMITIVES.contains(value)) {
return getNumberPrimitiveType(value);
}
if (LAST_USED_INDICATOR.equals(value)) {
final JavaType result = lastUsed.getJavaType();
if (result == null) {
throw new IllegalStateException(
"Unknown type; please indicate the type as a command option (ie --xxxx)");
}
return result;
}
String topLevelPath;
Pom module = projectOperations.getFocusedModule();
if (value.contains(MODULE_PATH_SEPARATOR)) {
final String moduleName = value.substring(0, value.indexOf(MODULE_PATH_SEPARATOR));
module = projectOperations.getPomFromModuleName(moduleName);
topLevelPath = typeLocationService.getTopLevelPackageForModule(module);
value = value.substring(value.indexOf(MODULE_PATH_SEPARATOR) + 1, value.length()).trim();
if (StringUtils.contains(optionContext, UPDATE)) {
projectOperations.setModule(module);
}
} else {
topLevelPath =
typeLocationService.getTopLevelPackageForModule(projectOperations.getFocusedModule());
}
if (value.equals(topLevelPath)) {
return null;
}
String newValue = locateExisting(value, topLevelPath);
if (newValue == null) {
newValue = locateNew(value, topLevelPath);
}
if (StringUtils.isNotBlank(newValue)) {
final String physicalTypeIdentifier =
typeLocationService.getPhysicalTypeIdentifier(new JavaType(newValue));
if (StringUtils.isNotBlank(physicalTypeIdentifier)) {
module =
projectOperations.getPomFromModuleName(PhysicalTypeIdentifier.getPath(
physicalTypeIdentifier).getModule());
}
}
// If the user did not provide a java type name containing a dot, it's
// taken as relative to the current package directory
if (!newValue.contains(".")) {
newValue =
(lastUsed.getJavaPackage() == null ? lastUsed.getTopLevelPackage()
.getFullyQualifiedPackageName() : lastUsed.getJavaPackage()
.getFullyQualifiedPackageName())
+ "." + newValue;
}
// Automatically capitalise the first letter of the last name segment
// (i.e. capitalise the type name, but not the package)
final int index = newValue.lastIndexOf(".");
if (index > -1 && !newValue.endsWith(".")) {
String typeName = newValue.substring(index + 1);
typeName = StringUtils.capitalize(typeName);
newValue = newValue.substring(0, index).toLowerCase() + "." + typeName;
}
final JavaType result = new JavaType(newValue, module.getModuleName());
// ROO-3581: On this time we don't know if current result
// exists as type on generated project. We need to save as
// not verified
if (StringUtils.contains(optionContext, UPDATE)
|| StringUtils.contains(optionContext, UPDATELAST)) {
if (StringUtils.contains(optionContext, INTERFACE)) {
lastUsed.setTypeNotVerified(null, module);
} else {
lastUsed.setTypeNotVerified(result, module);
}
}
return result;
}
public boolean getAllPossibleValues(final List<Completion> completions,
final Class<?> requiredType, String existingData, final String optionContext,
final MethodTarget target) {
existingData = StringUtils.stripToEmpty(existingData);
if (StringUtils.isBlank(optionContext) || optionContext.contains(PROJECT)
|| optionContext.contains(SUPERCLASS) || optionContext.contains(INTERFACE)
|| optionContext.contains(ENUMERATION)) {
completeProjectSpecificPaths(completions, existingData, optionContext);
}
if (StringUtils.contains(optionContext, "java")) {
completeJavaSpecificPaths(completions, existingData, optionContext);
}
return false;
}
public boolean supports(final Class<?> requiredType, final String optionContext) {
return JavaType.class.isAssignableFrom(requiredType);
}
private void addCompletionsForOtherModuleNames(final List<Completion> completions,
final Pom targetModule) {
for (final String moduleName : projectOperations.getModuleNames()) {
if (StringUtils.isNotBlank(moduleName) && !moduleName.equals(targetModule.getModuleName())) {
completions.add(new Completion(moduleName + MODULE_PATH_SEPARATOR, decorate(moduleName
+ MODULE_PATH_SEPARATOR, FG_CYAN), "Modules", 0));
}
}
}
private void addCompletionsForTypesInTargetModule(final List<Completion> completions,
final String optionContext, final Pom targetModule, final String heading,
final String prefix, final String formattedPrefix, final String topLevelPackage,
final String basePackage) {
final Collection<JavaType> typesInModule = getTypesForModule(optionContext, targetModule);
completions.add(new Completion(prefix + topLevelPackage, formattedPrefix + topLevelPackage,
heading, 1));
for (final JavaType javaType : typesInModule) {
String type = javaType.getFullyQualifiedTypeName();
if (type.startsWith(basePackage)) {
type = StringUtils.replace(type, topLevelPackage, TOP_LEVEL_PACKAGE_SYMBOL, 1);
completions.add(new Completion(prefix + type, formattedPrefix + type, heading, 1));
}
}
}
private Collection<JavaType> getTypesForModule(final String optionContext, final Pom targetModule) {
final Collection<JavaType> typesForModule = typeLocationService.getTypesForModule(targetModule);
if (!(optionContext.contains(SUPERCLASS) || optionContext.contains(INTERFACE) || optionContext
.contains(ENUMERATION))) {
return typesForModule;
}
final Collection<JavaType> types = new ArrayList<JavaType>();
for (final JavaType javaType : typesForModule) {
final ClassOrInterfaceTypeDetails typeDetails = typeLocationService.getTypeDetails(javaType);
if ((optionContext.contains(SUPERCLASS) && (Modifier.isFinal(typeDetails.getModifier()) || typeDetails
.getPhysicalTypeCategory() == PhysicalTypeCategory.INTERFACE))
|| (optionContext.contains(INTERFACE) && typeDetails.getPhysicalTypeCategory() != PhysicalTypeCategory.INTERFACE)
|| (optionContext.contains(ENUMERATION) && typeDetails.getPhysicalTypeCategory() != PhysicalTypeCategory.ENUMERATION)) {
continue;
}
types.add(javaType);
}
return types;
}
/**
* Adds common "java." types to the completions. For now we just provide
* them statically.
*/
private void completeJavaSpecificPaths(final List<Completion> completions,
final String existingData, String optionContext) {
final SortedSet<String> types = new TreeSet<String>();
if (StringUtils.isBlank(optionContext)) {
optionContext = "java-all";
}
if (optionContext.contains("java-all") || optionContext.contains("java-lang")) {
// lang - other
types.add(Boolean.class.getName());
types.add(String.class.getName());
}
if (optionContext.contains("java-all") || optionContext.contains("java-lang")
|| optionContext.contains("java-number")) {
// lang - numeric
types.add(Number.class.getName());
types.add(Short.class.getName());
types.add(Byte.class.getName());
types.add(Integer.class.getName());
types.add(Long.class.getName());
types.add(Float.class.getName());
types.add(Double.class.getName());
types.add(Byte.TYPE.getName());
types.add(Short.TYPE.getName());
types.add(Integer.TYPE.getName());
types.add(Long.TYPE.getName());
types.add(Float.TYPE.getName());
types.add(Double.TYPE.getName());
}
if (optionContext.contains("java-all") || optionContext.contains("java-number")) {
// misc
types.add(BigDecimal.class.getName());
types.add(BigInteger.class.getName());
}
if (optionContext.contains("java-all") || optionContext.contains("java-util")
|| optionContext.contains("java-collections")) {
// util
types.add(Collection.class.getName());
types.add(List.class.getName());
types.add(Queue.class.getName());
types.add(Set.class.getName());
types.add(SortedSet.class.getName());
types.add(Map.class.getName());
}
if (optionContext.contains("java-all") || optionContext.contains("java-util")
|| optionContext.contains("java-date")) {
// util
types.add(Date.class.getName());
types.add(Calendar.class.getName());
}
for (final String type : types) {
if (type.startsWith(existingData) || existingData.startsWith(type)) {
completions.add(new Completion(type));
}
}
}
private void completeProjectSpecificPaths(final List<Completion> completions,
final String existingData, final String optionContext) {
if (!projectOperations.isFocusedProjectAvailable()) {
return;
}
final Pom targetModule;
final String heading;
final String prefix;
final String formattedPrefix;
final String typeName;
if (existingData.contains(MODULE_PATH_SEPARATOR)) {
// Looking for a type in another module
final String targetModuleName =
existingData.substring(0, existingData.indexOf(MODULE_PATH_SEPARATOR));
targetModule = projectOperations.getPomFromModuleName(targetModuleName);
heading = "";
prefix = targetModuleName + MODULE_PATH_SEPARATOR;
formattedPrefix = decorate(targetModuleName + MODULE_PATH_SEPARATOR, FG_CYAN);
typeName = StringUtils.substringAfterLast(existingData, MODULE_PATH_SEPARATOR);
} else {
// Looking for a type in the currently focused module
targetModule = projectOperations.getFocusedModule();
heading = targetModule.getModuleName();
prefix = "";
formattedPrefix = "";
typeName = existingData;
}
final String topLevelPackage = typeLocationService.getTopLevelPackageForModule(targetModule);
final String basePackage = resolveTopLevelPackageSymbol(typeName, topLevelPackage);
addCompletionsForOtherModuleNames(completions, targetModule);
if (!"pom".equals(targetModule.getPackaging())) {
addCompletionsForTypesInTargetModule(completions, optionContext, targetModule, heading,
prefix, formattedPrefix, topLevelPackage, basePackage);
}
}
private JavaType getNumberPrimitiveType(final String value) {
if ("byte".equals(value)) {
return JavaType.BYTE_PRIMITIVE;
} else if ("short".equals(value)) {
return JavaType.SHORT_PRIMITIVE;
} else if ("int".equals(value)) {
return JavaType.INT_PRIMITIVE;
} else if ("long".equals(value)) {
return JavaType.LONG_PRIMITIVE;
} else if ("float".equals(value)) {
return JavaType.FLOAT_PRIMITIVE;
} else if ("double".equals(value)) {
return JavaType.DOUBLE_PRIMITIVE;
} else {
return null;
}
}
private String locateExisting(final String value, String topLevelPath) {
String newValue = value;
if (value.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) {
boolean found = false;
while (!found) {
if (value.length() > 1) {
newValue =
(value.charAt(1) == '.' ? topLevelPath : topLevelPath + ".") + value.substring(1);
} else {
newValue = topLevelPath + ".";
}
final String physicalTypeIdentifier =
typeLocationService.getPhysicalTypeIdentifier(new JavaType(newValue));
if (physicalTypeIdentifier != null) {
topLevelPath =
typeLocationService.getTopLevelPackageForModule(projectOperations
.getPomFromModuleName(PhysicalTypeIdentifier.getPath(physicalTypeIdentifier)
.getModule()));
found = true;
} else {
final int index = topLevelPath.lastIndexOf('.');
if (index == -1) {
break;
}
topLevelPath = topLevelPath.substring(0, topLevelPath.lastIndexOf('.'));
}
}
if (!found) {
return null;
}
}
lastUsed.setTopLevelPackage(new JavaPackage(topLevelPath));
return newValue;
}
private String locateNew(final String value, final String topLevelPath) {
String newValue = value;
if (value.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) {
if (value.length() > 1) {
newValue =
(value.charAt(1) == '.' ? topLevelPath : topLevelPath + ".") + value.substring(1);
} else {
newValue = topLevelPath + ".";
}
}
lastUsed.setTopLevelPackage(new JavaPackage(topLevelPath));
return newValue;
}
private String resolveTopLevelPackageSymbol(final String existingData,
final String topLevelPackage) {
if (TOP_LEVEL_PACKAGE_SYMBOL.equals(existingData)) {
// existing data = "~" => "com.foo."
return topLevelPackage + ".";
}
if (existingData.startsWith(TOP_LEVEL_PACKAGE_SYMBOL)) {
// e.g. turn "~.blah" or "~blah" into "com.foo.blah"
return topLevelPackage + (existingData.charAt(1) == '.' ? "" : ".")
+ existingData.substring(1);
}
return existingData;
}
}