package org.springframework.roo.addon.layers.repository.jpa.addon; import static org.springframework.roo.shell.OptionContexts.PROJECT; 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.field.addon.FieldCommands; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.converters.JavaPackageConverter; import org.springframework.roo.converters.LastUsed; import org.springframework.roo.model.JavaPackage; import org.springframework.roo.model.JavaType; import org.springframework.roo.model.RooJavaType; 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.Converter; import org.springframework.roo.shell.ShellContext; import org.springframework.roo.support.logging.HandlerUtils; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.logging.Logger; /** * Commands for the JPA repository add-on. * * @author Stefan Schmidt * @author Juan Carlos GarcĂ­a * @author Sergio Clares * @since 1.2.0 */ @Component @Service public class RepositoryJpaCommands implements CommandMarker { protected final static Logger LOGGER = HandlerUtils.getLogger(FieldCommands.class); //------------ OSGi component attributes ---------------- private BundleContext context; @Reference private RepositoryJpaOperations repositoryJpaOperations; @Reference private ProjectOperations projectOperations; @Reference private TypeLocationService typeLocationService; @Reference private LastUsed lastUsed; private Converter<JavaType> javaTypeConverter; protected void activate(final ComponentContext context) { this.context = context.getBundleContext(); } protected void deactivate(final ComponentContext context) { this.context = null; } @CliAvailabilityIndicator({"repository jpa"}) public boolean isRepositoryCommandAvailable() { return repositoryJpaOperations.isRepositoryInstallationPossible(); } @CliOptionAutocompleteIndicator(command = "repository jpa", param = "entity", help = "--entity option should be an entity.") public List<String> getEntityPossibleResults(ShellContext shellContext) { // Get current value of class String currentText = shellContext.getParameters().get("entity"); List<String> allPossibleValues = new ArrayList<String>(); // Getting all existing entities Set<ClassOrInterfaceTypeDetails> entitiesInProject = typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_JPA_ENTITY); for (ClassOrInterfaceTypeDetails entity : entitiesInProject) { String name = replaceTopLevelPackageString(entity, currentText); if (!allPossibleValues.contains(name)) { allPossibleValues.add(name); } } return allPossibleValues; } @CliOptionAutocompleteIndicator(command = "repository jpa", param = "interface", help = "--interface option should be a new type.", validate = false, includeSpaceOnFinish = false) public List<String> getInterfacePossibleResults(ShellContext shellContext) { List<String> allPossibleValues = new ArrayList<String>(); // Add all modules to completions list Collection<String> modules = projectOperations.getModuleNames(); for (String module : modules) { if (StringUtils.isNotBlank(module) && !module.equals(projectOperations.getFocusedModule().getModuleName())) { allPossibleValues.add(module.concat(LogicalPath.MODULE_PATH_SEPARATOR) .concat(JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL).concat(".")); } else if (!projectOperations.isMultimoduleProject()) { // Add only JavaPackage completion allPossibleValues.add(String.format("%s.repository.", JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL)); } } // Always add base package allPossibleValues.add(JavaPackageConverter.TOP_LEVEL_PACKAGE_SYMBOL.concat(".")); return allPossibleValues; } /** * This indicator return all Projection classes associated to an entity specified * in the 'entity' parameter. * * @param shellContext the Roo ShellContext. * @return List<String> with fullyQualifiedNames of each associated Projection. */ @CliOptionAutocompleteIndicator( command = "repository jpa", param = "defaultReturnType", help = "--defaultReturnType option should be a Projection class associated to the entity specified in --entity.") public List<String> getAssociatedProjectionResults(ShellContext shellContext) { List<String> allPossibleValues = new ArrayList<String>(); // Get current value of 'defaultReturnType' String currentText = shellContext.getParameters().get("defaultReturnType"); // Get current value of 'entity' JavaType entity = getTypeFromEntityParam(shellContext); Set<ClassOrInterfaceTypeDetails> projectionsInProject = typeLocationService .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_ENTITY_PROJECTION); for (ClassOrInterfaceTypeDetails projection : projectionsInProject) { // Add only projections associated to the entity specified in the command if (projection.getAnnotation(RooJavaType.ROO_ENTITY_PROJECTION).getAttribute("entity") .getValue().equals(entity)) { String name = replaceTopLevelPackageString(projection, currentText); if (!allPossibleValues.contains(name)) { allPossibleValues.add(name); } } } return allPossibleValues; } /** * This indicator says if --defaultReturnType parameter should be visible or not. * * @param context ShellContext * @return false if domain entity specified in --entity parameter has no associated Projections. */ @CliOptionVisibilityIndicator( params = "defaultReturnType", command = "repository jpa", help = "--defaultReturnType parameter is not visible if domain entity specified in --entity parameter has no associated Projections.") public boolean isDefaultReturnTypeParameterVisible(ShellContext shellContext) { // Get current value of 'entity' JavaType entity = getTypeFromEntityParam(shellContext); if (entity == null) { return false; } Set<ClassOrInterfaceTypeDetails> projectionsInProject = typeLocationService .findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_ENTITY_PROJECTION); boolean visible = false; for (ClassOrInterfaceTypeDetails projection : projectionsInProject) { // Add only projections associated to the entity specified in the command if (projection.getAnnotation(RooJavaType.ROO_ENTITY_PROJECTION).getAttribute("entity") .getValue().equals(entity)) { visible = true; break; } } return visible; } /** * This indicator says if --interface parameter should be mandatory or not * * If --entity parameter has been specified and we are working under multimodule * project, --interface parameter will be mandatory. * * @param context ShellContext * @return */ @CliOptionMandatoryIndicator(params = "interface", command = "repository jpa") public boolean isInterfaceParameterMandatory(ShellContext context) { if (context.getParameters().containsKey("entity") && projectOperations.isMultimoduleProject()) { return true; } return false; } /** * This indicator says if --package parameter should be visible or not * * If --all parameter has not been specified, --package parameter will not be visible * to prevent conflicts. * * @param context ShellContext * @return */ @CliOptionVisibilityIndicator(params = "package", command = "repository jpa", help = "--package parameter is not visible if --all parameter has not been specified before.") public boolean isPackageParameterVisible(ShellContext context) { if (context.getParameters().containsKey("all")) { return true; } return false; } /** * This indicator says if --all parameter should be visible or not * * If --entity parameter has been specified, --all parameter will not be visible * to prevent conflicts. * * @param context ShellContext * @return */ @CliOptionVisibilityIndicator(params = "all", command = "repository jpa", help = "--all parameter is not be visible if --entity parameter has been specified before.") public boolean isAllParameterVisible(ShellContext context) { if (context.getParameters().containsKey("entity")) { return false; } return true; } /** * This indicator says if --interface and --defaultSearchResult parameter are visible. * * If --entity is specified, --interface and --defaultSearchResult will be visible * * @param context ShellContext * @return */ @CliOptionVisibilityIndicator( params = {"interface", "defaultSearchResult"}, command = "repository jpa", help = "--interface or --defaultSearchResult parameters are not be visible if --entity parameter hasn't been specified before.") public boolean areInterfaceAndSearchResultVisible(ShellContext context) { if (context.getParameters().containsKey("entity")) { return true; } return false; } /** * This indicator says if --entity parameter is visible. * * If --all is specified, --entity won't be visible * * @param context ShellContext * @return */ @CliOptionVisibilityIndicator(params = "entity", command = "repository jpa", help = "--entity parameter is not be visible --all parameter has been specified before.") public boolean isEntityParameterVisible(ShellContext context) { if (context.getParameters().containsKey("all")) { return false; } return true; } @CliCommand( value = "repository jpa", help = "Generates new Spring Data repository for specified entity or for all entities in generated " + "project.") public void repository( @CliOption( key = "all", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Indicates if developer wants to generate repositories for every entity of current " + "project. " + "This option is mandatory if `--entity` is not specified. Otherwise, using `--entity` " + "will cause the parameter `--all` won't be available. " + "Default if option present: `true`; default if option not present: `false`.") boolean all, @CliOption( key = "entity", mandatory = false, optionContext = PROJECT, help = "The domain entity this repository should manage. When working on a single module " + "project, simply specify the name of the entity. If you consider it necessary, you can " + "also specify the package. Ex.: `--class ~.domain.MyEntity` (where `~` is the base package). " + "When working with multiple modules, you should specify the name of the entity and the " + "module where it is. Ex.: `--class model:~.domain.MyEntity`. If the module is not specified, " + "it is assumed that the entity is in the module which has the focus. " + "Possible values are: any of the entities in the project. " + "This option is mandatory if `--all` is not specified. Otherwise, using `--all` " + "will cause the parameter `--entity` won't be available.") final JavaType domainType, @CliOption( key = "interface", mandatory = true, help = "The java Spring Data repository to generate. 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 mandatory if `--entity` has been already specified and the project is " + "multi-module. " + "This option is available only when `--entity` has been specified.") final JavaType interfaceType, @CliOption( key = "defaultReturnType", mandatory = false, help = "The default return type which this repository will have for all finders, including those" + "created by default. The default return type should be a Projection class associated to " + "the entity specified in `--entity` parameter. " + "Possible values are: any of the projections associated to the entity in `--entity` option. " + "This option is not available if domain entity specified in `--entity` parameter has no " + "associated Projections. " + "Default: the entity specified in the `entity` option.") JavaType defaultReturnType, @CliOption( key = "package", mandatory = false, help = "The package where repositories will be generated. 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 available if `--all` option has not been specified. " + "Default value if not present: `~.repository` package, or 'repository:~.' if multi-module " + "project.") JavaPackage repositoriesPackage) { if (all) { // If user didn't specified some JavaPackage, use default repository package if (repositoriesPackage == null) { if (projectOperations.isMultimoduleProject()) { // Build default JavaPackage with module for (String moduleName : projectOperations.getModuleNames()) { if (moduleName.equals("repository")) { Pom module = projectOperations.getPomFromModuleName(moduleName); repositoriesPackage = new JavaPackage(typeLocationService.getTopLevelPackageForModule(module), moduleName); break; } } // Check if repository found Validate.notNull(repositoriesPackage, "Couldn't find in project a default 'repository' " + "module. Please, use 'package' option to specify module and package."); } else { // Build default JavaPackage for single module repositoriesPackage = new JavaPackage(projectOperations.getFocusedTopLevelPackage() .getFullyQualifiedPackageName().concat(".repository"), projectOperations.getFocusedModuleName()); } } repositoryJpaOperations.generateAllRepositories(repositoriesPackage); } else { repositoryJpaOperations.addRepository(interfaceType, domainType, defaultReturnType, true); } } /** * Replaces a JavaType fullyQualifiedName for a shorter name using '~' for TopLevelPackage * * @param cid 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(ClassOrInterfaceTypeDetails cid, String currentText) { String javaTypeFullyQualilfiedName = cid.getType().getFullyQualifiedTypeName(); String javaTypeString = ""; String topLevelPackageString = ""; // Add module value to topLevelPackage when necessary if (StringUtils.isNotBlank(cid.getType().getModule()) && !cid.getType().getModule().equals(projectOperations.getFocusedModuleName())) { // Target module is not focused javaTypeString = cid.getType().getModule().concat(LogicalPath.MODULE_PATH_SEPARATOR); topLevelPackageString = projectOperations.getTopLevelPackage(cid.getType().getModule()) .getFullyQualifiedPackageName(); } else if (StringUtils.isNotBlank(cid.getType().getModule()) && cid.getType().getModule().equals(projectOperations.getFocusedModuleName()) && (currentText.startsWith(cid.getType().getModule()) || cid.getType().getModule() .startsWith(currentText)) && StringUtils.isNotBlank(currentText)) { // Target module is focused but user wrote it javaTypeString = cid.getType().getModule().concat(LogicalPath.MODULE_PATH_SEPARATOR); topLevelPackageString = projectOperations.getTopLevelPackage(cid.getType().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; } /** * Tries to obtain JavaType indicated in command or which has the focus * in the Shell * * @param shellContext the Roo Shell context * @return JavaType or null if no class has the focus or no class is * specified in the command */ private JavaType getTypeFromEntityParam(ShellContext shellContext) { // Try to get 'class' from ShellContext String typeString = shellContext.getParameters().get("entity"); JavaType type = null; if (typeString != null) { type = getJavaTypeConverter().convertFromText(typeString, JavaType.class, PROJECT); } else { type = lastUsed.getJavaType(); } return type; } @SuppressWarnings("unchecked") 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 FieldCommands."); return null; } } else { return javaTypeConverter; } } }