package org.springframework.roo.addon.layers.repository.jpa.addon.finder; import static org.springframework.roo.shell.OptionContexts.PROJECT; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; 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.service.component.ComponentContext; import org.springframework.roo.addon.layers.repository.jpa.addon.RepositoryJpaLocator; import org.springframework.roo.addon.layers.repository.jpa.addon.RepositoryJpaMetadata; import org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser.FinderAutocomplete; import org.springframework.roo.addon.layers.repository.jpa.addon.finder.parser.PartTree; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.TypeManagementService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; import org.springframework.roo.classpath.scanner.MemberDetails; import org.springframework.roo.classpath.scanner.MemberDetailsScanner; import org.springframework.roo.converters.JavaTypeConverter; import org.springframework.roo.converters.LastUsed; import org.springframework.roo.model.JavaSymbolName; 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.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 org.springframework.roo.support.osgi.ServiceInstaceManager; /** * Commands for the 'finder' add-on to be used by the ROO shell. * * These commands allow developers to generate Spring Data finders * on existing repositories. * * @author Stefan Schmidt * @author Paula Navarro * @author Juan Carlos GarcĂ­a * @author Sergio Clares * @since 1.0 */ @Component @Service public class FinderCommands implements CommandMarker, FinderAutocomplete { private static final Logger LOGGER = HandlerUtils.getLogger(FinderCommands.class); // ------------ OSGi component attributes ---------------- private BundleContext context; private ServiceInstaceManager serviceInstaceManager = new ServiceInstaceManager(); @Reference private FinderOperations finderOperations; @Reference private LastUsed lastUsed; // Map where entity details will be cached private Map<JavaType, MemberDetails> entitiesDetails; protected void activate(final ComponentContext context) { this.context = context.getBundleContext(); this.entitiesDetails = new HashMap<JavaType, MemberDetails>(); serviceInstaceManager.activate(this.context); } @CliAvailabilityIndicator({"finder add"}) public boolean isFinderCommandAvailable() { return finderOperations.isFinderInstallationPossible(); } @CliOptionVisibilityIndicator(command = "finder add", params = {"name"}, help = "You must define --entity to be able to define --name parameter.") public boolean isNameVisible(ShellContext shellContext) { // Getting all defined parameters on autocompleted command Map<String, String> params = shellContext.getParameters(); // If mandatory parameter entity is not defined, name parameter should not // be visible String entity = params.get("entity"); if (StringUtils.isBlank(entity)) { return false; } // Get current entity member details to check if is a valid Spring Roo entity MemberDetails entityDetails = getEntityDetails(entity); // If not entity details, is not a valid entity, so --name parameter is not visible if (entityDetails == null) { return false; } return true; } /** * This indicator says if --returnType parameter should be visible or not. * * @param context ShellContext * @return false if domain entity specified in --entity parameter has no associated Projections. */ @CliOptionVisibilityIndicator(params = "returnType", command = "finder add", help = "--returnType parameter is not visible if --entity parameter hasn't " + "been specified before or if there aren't exist any Projection class associated " + "to the current entity.") public boolean isReturnTypeParameterVisible(ShellContext shellContext) { // Get current value of 'entity' JavaType entity = getTypeFromEntityParam(shellContext); if (entity == null) { return false; } Set<ClassOrInterfaceTypeDetails> projectionsInProject = getTypeLocationService().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)) { return true; } } return false; } @CliOptionAutocompleteIndicator(command = "finder add", includeSpaceOnFinish = false, param = "name", help = "--name parameter must follow Spring Data nomenclature. Please, write a " + "valid value using autocomplete feature (TAB or CTRL + Space)") public List<String> returnOptions(ShellContext shellContext) { List<String> allPossibleValues = new ArrayList<String>(); // Getting all defined parameters on autocompleted command Map<String, String> contextParameters = shellContext.getParameters(); // Getting current name value String name = contextParameters.get("name"); try { // Use PartTree class to obtain all possible values PartTree part = new PartTree(name, getEntityDetails(contextParameters.get("entity")), this); // Check if part has value if (part != null) { allPossibleValues = part.getOptions(); } return allPossibleValues; } catch (Exception e) { LOGGER.warning(e.getLocalizedMessage()); return allPossibleValues; } } @CliOptionAutocompleteIndicator(command = "finder add", includeSpaceOnFinish = false, param = "entity", help = "--entity option should be an entity.") public List<String> getClassPossibleResults(ShellContext shellContext) { // ROO-3763: Clear current cache during --entity autocompletion. // With that, Spring Roo will maintain cache during --name autocompletion // but if --entity is autocomplete, cache should be refreshed to obtain // last changes on entities entitiesDetails = new HashMap<JavaType, MemberDetails>(); // Get current value of entity String currentText = shellContext.getParameters().get("entity"); List<String> allPossibleValues = new ArrayList<String>(); Set<ClassOrInterfaceTypeDetails> entities = getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation( RooJavaType.ROO_JPA_ENTITY); for (ClassOrInterfaceTypeDetails entity : entities) { String name = replaceTopLevelPackageString(entity, currentText); if (!allPossibleValues.contains(name)) { allPossibleValues.add(name); } } return allPossibleValues; } @CliOptionAutocompleteIndicator( command = "finder add", includeSpaceOnFinish = false, param = "returnType", help = "--returnType option could be a Projection class related with the specified entity or the same entity. " + "By default, uses the value of the 'defaultReturnType' parameter specified during the Jpa Repository creation.") public List<String> getReturnTypePossibleResults(ShellContext shellContext) { // Get current value of returnType String currentText = shellContext.getParameters().get("returnType"); List<String> allPossibleValues = new ArrayList<String>(); // Get current value of 'entity' JavaType entity = getTypeFromEntityParam(shellContext); Set<ClassOrInterfaceTypeDetails> entityProjections = getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation( RooJavaType.ROO_ENTITY_PROJECTION); for (ClassOrInterfaceTypeDetails projection : entityProjections) { if (entity != null && projection.getAnnotation(RooJavaType.ROO_ENTITY_PROJECTION).getAttribute("entity") .getValue().equals(entity)) { String name = replaceTopLevelPackageString(projection, currentText); if (!allPossibleValues.contains(name)) { allPossibleValues.add(name); } } } // Always include the entity name, because if the defaultReturnType of the repository // is a projection, the developer should be able to specify an entity as return type. String entityName = replaceTopLevelPackageString(getTypeLocationService().getTypeDetails(entity), currentText); if (!allPossibleValues.contains(entityName)) { allPossibleValues.add(entityName); } return allPossibleValues; } @CliOptionAutocompleteIndicator(command = "finder add", includeSpaceOnFinish = false, param = "formBean", help = "--formBean option should be a DTO.") public List<String> getFormBeanPossibleResults(ShellContext shellContext) { // Get current value of entity String currentText = shellContext.getParameters().get("formBean"); List<String> allPossibleValues = new ArrayList<String>(); Set<ClassOrInterfaceTypeDetails> dtosInProject = getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_DTO); for (ClassOrInterfaceTypeDetails dto : dtosInProject) { String name = replaceTopLevelPackageString(dto, currentText); if (!allPossibleValues.contains(name)) { allPossibleValues.add(name); } } return allPossibleValues; } /** * Indicates if 'formBean' option should be visible. It checks the existence of * DTO's in project. * * @param shellContext * @return true if project contains at least one DTO, false otherwise. */ @CliOptionVisibilityIndicator(command = "finder add", help = "--formBean parameter is not visible if --entity parameter hasn't been specified " + "before or if there aren't exist any DTO in generated project", params = {"formBean"}) public boolean isFormBeanVisible(ShellContext shellContext) { Set<ClassOrInterfaceTypeDetails> dtosInProject = getTypeLocationService().findClassesOrInterfaceDetailsWithAnnotation(RooJavaType.ROO_DTO); if (dtosInProject.isEmpty()) { return false; } return true; } @CliOptionMandatoryIndicator(command = "finder add", params = {"formBean"}) public boolean isFormBeanMandatory(ShellContext shellContext) { // Get current value of 'entity' JavaType entity = getTypeFromEntityParam(shellContext); if (entity == null) { return false; } // Get current value of returnType String returnType = shellContext.getParameters().get("returnType"); // This parameter is not mandatory if returnType has not been specified if (StringUtils.isBlank(returnType)) { return false; } // If returnType has been specified, but it's an entity, this parameter is not // mandatory JavaTypeConverter converter = (JavaTypeConverter) getJavaTypeConverter().get(0); JavaType type = converter.convertFromText(returnType, JavaType.class, PROJECT); if (type == null) { return false; } ClassOrInterfaceTypeDetails details = getTypeLocationService().getTypeDetails(type); if (details == null) { return false; } AnnotationMetadata entityAnnotation = details.getAnnotation(RooJavaType.ROO_JPA_ENTITY); if (entityAnnotation != null) { return false; } return true; } @CliCommand( value = "finder add", help = "Installs a finder in the given target (must be an entity). This command needs an existing " + "repository for the target entity, you can create it with `repository jpa` command. The " + "finder will be added to targeted entity associated repository and associated service if " + "exists or when it will be created.") public void installFinders( @CliOption( key = "entity", mandatory = true, unspecifiedDefaultValue = "*", optionContext = PROJECT, help = "The entity for which the finders are generated. When working on a mono 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 class 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.") final JavaType entity, @CliOption( key = "name", mandatory = true, help = "The finder string defined as a Spring Data query. Use Spring Data JPA nomenclature. " + "Possible values are: any finder name following Spring Data nomenclature. " + "This option will not be available until `--entity` is specified.") final JavaSymbolName finderName, @CliOption( key = "formBean", mandatory = true, help = "The finder's search parameter. Should be a DTO and it must have at least same fields " + "(name and type) as those included in the finder `--name`, which can be target entity" + " fields or related entity fields. " + "Possible values are: any of the DTO's in the project. " + "This option is mandatory if `--returnType` is specified and its a projection. " + "This option is not available if `--entity` parameter has not been specified before or " + "if it does not exist any DTO in generated project. " + "Default if option not present: the entity specified in `--entity` option.") final JavaType formBean, @CliOption( key = "returnType", mandatory = false, optionContext = PROJECT, help = "The finder's results return type. " + "Possible values are: Projection classes annotated with `@RooEntityProjection` and " + "related to the entity specified in `--entity` option (use `entity projection` command), " + "or the same entity. " + "This option is not available if `--entity` parameter has not been specified before or " + "if it does not exist any Projection class associated to the targeted entity. " + "Default if not present: the default return type of the repository related to the entity, " + "which can be specified with `--defaultReturnType` parameter in `repository jpa` command.") JavaType returnType) { // Check if specified finderName follows Spring Data nomenclature PartTree partTree = new PartTree(finderName.getSymbolName(), getEntityDetails(entity), this); // If generated partTree is not valid, shows an exception Validate .isTrue( partTree.isValid(), "--name parameter must follow Spring Data nomenclature. Please, write a valid value using autocomplete feature (TAB or CTRL + Space)"); // If returnType has not been specified, use the specified "defaultReturnType" from the // related repository if (returnType == null) { // Obtain the related repository metadata RepositoryJpaMetadata repositoryMetadata = getRepositoryJpaLocator().getFirstRepositoryMetadata(entity); Validate .notNull(repositoryMetadata, "ERROR: You must create a repository related with this entity before to generate a finder"); // Use the repository metadata to obtain the default return type returnType = repositoryMetadata.getDefaultReturnType(); } Validate.notNull(returnType, "ERROR: The new finder must define a returnType"); // Check if the returnType is an entity. If is is not an entity validate // that the formBean is not null ClassOrInterfaceTypeDetails type = getTypeLocationService().getTypeDetails(returnType); if (type.getAnnotation(RooJavaType.ROO_JPA_ENTITY) == null) { Validate.notNull(formBean, "--formBean is requied when --returnType parameter is a projection."); } finderOperations.installFinder(entity, finderName, formBean, returnType); } /** * 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(getProjectOperations().getFocusedModuleName())) { // Target module is not focused javaTypeString = cid.getType().getModule().concat(LogicalPath.MODULE_PATH_SEPARATOR); topLevelPackageString = getProjectOperations().getTopLevelPackage(cid.getType().getModule()) .getFullyQualifiedPackageName(); } else if (StringUtils.isNotBlank(cid.getType().getModule()) && cid.getType().getModule().equals(getProjectOperations().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 = getProjectOperations().getTopLevelPackage(cid.getType().getModule()) .getFullyQualifiedPackageName(); } else { // Not multimodule project topLevelPackageString = getProjectOperations().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 'entity' from ShellContext String typeString = shellContext.getParameters().get("entity"); JavaType type = null; if (typeString != null) { JavaTypeConverter converter = (JavaTypeConverter) getJavaTypeConverter().get(0); type = converter.convertFromText(typeString, JavaType.class, PROJECT); } else { type = lastUsed.getJavaType(); } return type; } /** * Method that obtains entity details from entity name * * @param entityName * @return MemberDetails */ public MemberDetails getEntityDetails(String entityName) { String moduleName = getProjectOperations().getFocusedModuleName(); if (entityName.contains(":")) { moduleName = StringUtils.split(entityName, ":")[0]; entityName = StringUtils.split(entityName, ":")[1]; } // Getting JavaType for entityName // Check first if contains base package (~) if (entityName.contains("~")) { entityName = entityName.replace("~", getProjectOperations().getTopLevelPackage(moduleName) .getFullyQualifiedPackageName()); } JavaType entityType = new JavaType(entityName, moduleName); return getEntityDetails(entityType); } @Override public MemberDetails getEntityDetails(JavaType entity) { Validate.notNull(entity, "ERROR: Entity should be provided"); if (entitiesDetails.containsKey(entity)) { return entitiesDetails.get(entity); } // We know the file exists, as there's already entity metadata for it final ClassOrInterfaceTypeDetails cid = getTypeLocationService().getTypeDetails(entity); if (cid == null) { return null; } if (cid.getAnnotation(RooJavaType.ROO_JPA_ENTITY) == null) { LOGGER.warning("Unable to find the entity annotation on '" + entity + "'"); return null; } entitiesDetails.put(entity, getMemberDetailsScanner().getMemberDetails(getClass().getName(), cid)); return entitiesDetails.get(entity); } public ProjectOperations getProjectOperations() { return serviceInstaceManager.getServiceInstance(this, ProjectOperations.class); } public TypeLocationService getTypeLocationService() { return serviceInstaceManager.getServiceInstance(this, TypeLocationService.class); } public TypeManagementService getTypeManagementService() { return serviceInstaceManager.getServiceInstance(this, TypeManagementService.class); } public MemberDetailsScanner getMemberDetailsScanner() { return serviceInstaceManager.getServiceInstance(this, MemberDetailsScanner.class); } public RepositoryJpaLocator getRepositoryJpaLocator() { return serviceInstaceManager.getServiceInstance(this, RepositoryJpaLocator.class); } @SuppressWarnings("unchecked") public List<Converter> getJavaTypeConverter() { return serviceInstaceManager.getServiceInstance(this, Converter.class, new ServiceInstaceManager.Matcher<Converter>() { @Override public boolean match(Converter service) { if (service instanceof JavaTypeConverter) { return true; } return false; } }); } }