package org.springframework.roo.addon.pushin;
import static org.springframework.roo.shell.OptionContexts.PROJECT;
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.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.classpath.scanner.MemberDetailsScanner;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.model.JavaType;
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.CliOptionVisibilityIndicator;
import org.springframework.roo.shell.CommandMarker;
import org.springframework.roo.shell.Converter;
import org.springframework.roo.shell.ShellContext;
import org.springframework.roo.support.logging.HandlerUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Commands for the 'push-in' add-on to be used by the ROO shell.
*
* This command marker will provide necessary operations to make push-in of all
* methods, fields and annotations declared on ITDs.
*
* @author Juan Carlos GarcĂa
* @since 2.0
*/
@Component
@Service
public class PushInCommands implements CommandMarker {
private static final Logger LOGGER = HandlerUtils.getLogger(PushInCommands.class);
//------------ OSGi component attributes ----------------
private BundleContext context;
@Reference
private PushInOperations pushInOperations;
@Reference
private TypeLocationService typeLocationService;
@Reference
private MemberDetailsScanner memberDetailsScanner;
private Converter<JavaType> javaTypeConverter;
protected void activate(final ComponentContext context) {
this.context = context.getBundleContext();
}
/**
* Method that checks if push-in operation is available or not.
*
* "push-in" command will be available only if some project was generated.
*
* @return true if some project was created on focused directory.
*/
@CliAvailabilityIndicator("push-in")
public boolean isPushInCommandAvailable() {
return pushInOperations.isPushInCommandAvailable();
}
/**
* Method that checks visibility of --all parameter.
*
* This parameter will be available only if --package, --class or --method
* parameter has not been specified before.
*
* @param context
* ShellContext used to obtain specified parameters
* @return true if --all parameter is visible, false if not.
*/
@CliOptionVisibilityIndicator(
command = "push-in",
params = "all",
help = "--all parameter is not available if --package, --class or --method parameter has been specified.")
public boolean isAllParameterVisible(ShellContext context) {
Map<String, String> specifiedParameters = context.getParameters();
if (specifiedParameters.containsKey("package") || specifiedParameters.containsKey("class")
|| specifiedParameters.containsKey("method")) {
return false;
}
return true;
}
/**
* Method that checks visibility of --package, --class and --method parameters.
*
* This parameter will be available only if --all parameter
* has not been specified before.
*
* @param context
* ShellContext used to obtain specified parameters
* @return true if parameters are visible, false if not.
*/
@CliOptionVisibilityIndicator(
command = "push-in",
params = {"package", "class", "method"},
help = "--package, --class and --method parameters are not available if --all parameter has been specified.")
public boolean isOtherParametersVisible(ShellContext context) {
Map<String, String> specifiedParameters = context.getParameters();
if (specifiedParameters.containsKey("all")) {
return false;
}
return true;
}
/**
* Method that returns all defined methods for provided class on --class parameter.
*
* @param context context
* ShellContext used to obtain specified parameters
* @return List with available methods. Empty List if class has not been specified.
*/
@CliOptionAutocompleteIndicator(command = "push-in", param = "method", validate = false,
help = "Provides possible methods names if some class parameter has been specified")
public List<String> getAllPossibleMethods(ShellContext context) {
List<String> allPossibleMethods = new ArrayList<String>();
// Getting all introduces parameters
Map<String, String> specifiedParameters = context.getParameters();
String specifiedClass = specifiedParameters.get("class");
// Check if class parameter has been specified
if (StringUtils.isNotEmpty(specifiedClass)) {
JavaType klass =
getJavaTypeConverter().convertFromText(specifiedClass, JavaType.class, PROJECT);
// TODO: Class details should be cached to prevent load MemberDetails everytime.
// The problem is that if some element is cached, and then, new method is added
// to .aj file, this parameter will not autocomplete it.
MemberDetails klassDetails =
memberDetailsScanner.getMemberDetails(getClass().getName(),
typeLocationService.getTypeDetails(klass));
if (klassDetails != null) {
List<MethodMetadata> definedMethods = klassDetails.getMethods();
for (MethodMetadata method : definedMethods) {
// Check if method has been defined on current class and check
// if current method has been pushed before.
String declaredByMetadataID = method.getDeclaredByMetadataId();
if (StringUtils.isNotBlank(declaredByMetadataID)
&& declaredByMetadataID.split("\\?").length > 1
&& declaredByMetadataID.split("\\#").length > 0
&& !declaredByMetadataID.split("\\#")[0]
.equals("MID:org.springframework.roo.classpath.PhysicalTypeIdentifier")
&& declaredByMetadataID.split("\\?")[1].equals(klass.getFullyQualifiedTypeName())) {
String methodName = method.getMethodName().getSymbolName();
List<AnnotatedJavaType> parameterTypes = method.getParameterTypes();
methodName = methodName.concat("(");
for (int i = 0; i < parameterTypes.size(); i++) {
String paramType = parameterTypes.get(i).getJavaType().getSimpleTypeName();
methodName = methodName.concat(paramType).concat(",");
}
if (!parameterTypes.isEmpty()) {
methodName = methodName.substring(0, methodName.length() - 1).concat(")");
} else {
methodName = methodName.concat(")");
}
allPossibleMethods.add(methodName);
}
}
}
}
return allPossibleMethods;
}
/**
* Method that register "push-in" command on Spring Roo Shell.
*
* Push-in all methods, fields, annotations, imports, extends, etc.. declared on
* ITDs to its .java files. You could specify --all parameter to apply push-in on every
* component of generated project, or you could define package, class or method where wants
* to apply push-in.
*
* @param all
* String that indicates if push-in process should be applied to entire project. All specified
* values will be ignored.
* @param package
* JavaPackage with the specified package where developers wants to make
* push-in
* @param klass
* JavaType with the specified class where developer wants to
* make push-in
* @param method
* String with the specified name of the method that
* developer wants to push-in
* @param shellContext
* ShellContext used to know if --force parameter has been used by developer
*
*/
@CliCommand(
value = "push-in",
help = "Allows to push-in elements declared in the ITDs to its .java files. You could specify "
+ "`--all` option to apply push-in on every component of generated project, or you could "
+ "define any package, class or method to apply push-in, combining them.")
public void pushIn(
@CliOption(
key = "all",
mandatory = false,
specifiedDefaultValue = "",
help = "Option that indicates if push-in process should be applied to entire project. "
+ "This option is mandatory if none of `--package`, `--class` or `--method` are specified. "
+ "Otherwise, using `--package`, `--class` or `--method` will cause the parameter `--all` "
+ "won't be available.") String all,
@CliOption(
key = "package",
mandatory = false,
help = "JavaPackage with the specified package where developer wants to make push-in. In "
+ "multi-module project you should specify the module name before the package name. "
+ "Ex.: `--package model:org.springframework.roo` but, if module name is not present, "
+ "the Roo Shell focused module will be used. "
+ "This option is not avalaible if `--all` parameter has been already specified.") JavaPackage specifiedPackage,
@CliOption(
key = "class",
mandatory = false,
help = "JavaType with the specified class where developer wants to make push-in. When working "
+ "on a single module project, simply specify the name of the class. If you consider it "
+ "necessary, you can also specify the package. Ex.: `--class ~.domain.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:~.domain.MyClass`. If the "
+ "module is not specified, it is assumed that the class is in the module which has the "
+ "focus. "
+ "This option is not avalaible if `--all` parameter has been already specified.") final JavaType klass,
@CliOption(
key = "method",
mandatory = false,
help = "String with the specified name of the method which developer wants to push-in. "
+ "You could use a Regular Expression to make push-in of more than one method on the same "
+ "execution. "
+ "This option is not avalaible if `--all` parameter has been already specified.") String method,
ShellContext shellContext) {
// Developer must specify at least one parameter
if (all == null && specifiedPackage == null && klass == null && method == null) {
LOGGER.log(Level.WARNING, "ERROR: You must specify at least one parameter. ");
return;
}
// Check if all parameter contains value
if (all != null && StringUtils.isNotEmpty(all)) {
LOGGER.log(Level.WARNING, "ERROR: --all parameter doesn't allow any value.");
return;
}
if (method == null && shellContext.getParameters().get("method") != null) {
LOGGER
.log(Level.WARNING,
"ERROR: You must provide a valid value (method name or Regular Expression) on --method parameter.");
return;
}
// Check if developer wants to apply push-in on every component of generated project
if (all != null) {
pushInOperations.pushInAll(true, shellContext.isForce());
} else {
pushInOperations.pushIn(specifiedPackage, klass, method, true);
}
}
public Converter<JavaType> getJavaTypeConverter() {
if (javaTypeConverter == null) {
// Get all Services implement JavaTypeConverter interface
try {
ServiceReference<?>[] references =
this.context.getAllServiceReferences(Converter.class.getName(), null);
for (ServiceReference<?> ref : references) {
Converter<?> converter = (Converter<?>) this.context.getService(ref);
if (converter.supports(JavaType.class, PROJECT)) {
javaTypeConverter = (Converter<JavaType>) converter;
return javaTypeConverter;
}
}
return null;
} catch (InvalidSyntaxException e) {
LOGGER.warning("ERROR: Cannot load JavaTypeConverter on FinderCommands.");
return null;
}
} else {
return javaTypeConverter;
}
}
}