package org.springframework.roo.addon.web.mvc.controller.addon.finder; 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.addon.layers.repository.jpa.addon.RepositoryJpaLocator; import org.springframework.roo.addon.web.mvc.controller.addon.responses.ControllerMVCResponseService; import org.springframework.roo.classpath.ModuleFeatureName; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails; import org.springframework.roo.classpath.details.annotations.AnnotationMetadata; 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 org.springframework.roo.support.osgi.ServiceInstaceManager; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; /** * Commands which provide finder functionality through Spring MVC controllers. * * @author Stefan Schmidt * @author Paula Navarro * @author Sergio Clares * @since 1.2.0 */ @Component @Service public class WebFinderCommands implements CommandMarker { private static Logger LOGGER = HandlerUtils.getLogger(WebFinderCommands.class); // ------------ OSGi component attributes ---------------- private BundleContext context; @Reference private WebFinderOperations webFinderOperations; @Reference private RepositoryJpaLocator repositoryJpaLocator; private Map<String, ControllerMVCResponseService> responseTypes = new HashMap<String, ControllerMVCResponseService>(); private Converter<JavaType> javaTypeConverter; private ServiceInstaceManager serviceInstaceManager = new ServiceInstaceManager(); protected void activate(final ComponentContext context) { this.context = context.getBundleContext(); serviceInstaceManager.activate(this.context); } @CliAvailabilityIndicator({"web mvc finder"}) public boolean isCommandAvailable() { return webFinderOperations.isWebFinderInstallationPossible(); } /** * This indicator says if --entity parameter should be visible or not * * If --all parameter has been specified, --entity parameter will not be visible * to prevent conflicts. * * @return */ @CliOptionVisibilityIndicator(params = "entity", command = "web mvc finder", help = "--entity parameter is not visible if --all parameter has been specified before.") public boolean isEntityParameterVisible(ShellContext context) { if (context.getParameters().containsKey("all")) { return false; } return true; } @CliOptionAutocompleteIndicator(param = "entity", command = "web mvc finder", help = "--entity parameter should be completed with classes annotated with @RooJpaEntity.") public List<String> getEntityValues(ShellContext context) { // Get current value of class String currentText = context.getParameters().get("entity"); // Create results to return List<String> results = new ArrayList<String>(); for (JavaType entity : getTypeLocationService().findTypesWithAnnotation( RooJavaType.ROO_JPA_ENTITY)) { ClassOrInterfaceTypeDetails repository = repositoryJpaLocator.getFirstRepository(entity); if (repository == null) { continue; } AnnotationMetadata repositoryAnnotation = repository.getAnnotation(RooJavaType.ROO_REPOSITORY_JPA); if (repositoryAnnotation.getAttribute("finders") == null) { continue; } String name = replaceTopLevelPackageString(entity, currentText); if (!results.contains(name)) { results.add(name); } } if (results.isEmpty()) { results.add(""); } return results; } /** * 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. * * @return */ @CliOptionVisibilityIndicator(params = "all", command = "web mvc finder", 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 --queryMethod parameter should be visible or not * * If --entity parameter has been specified, --queryMethod parameter will be visible. * * @return */ @CliOptionVisibilityIndicator( params = "queryMethod", command = "web mvc finder", help = "--queryMethod parameter is not visible if --entity parameter hasn't been specified before.") public boolean isQueryMethodParameterVisible(ShellContext context) { if (context.getParameters().containsKey("entity")) { return true; } return false; } @CliOptionAutocompleteIndicator(param = "queryMethod", command = "web mvc finder", help = "--queryMethod parameter should be completed with related repository finders.") public List<String> getAllQueryMethodValues(ShellContext context) { List<String> finders = new ArrayList<String>(); if (context.getParameters().containsKey("entity")) { // Extract entity String providedEntity = context.getParameters().get("entity"); // Getting the JavaType converter if (getJavaTypeConverter() != null && StringUtils.isNotBlank(providedEntity)) { JavaType entity = getJavaTypeConverter().convertFromText(providedEntity, JavaType.class, ""); // Get finders finders = getFinders(entity, null); } } if (finders.isEmpty()) { finders.add(""); } return finders; } /** * This indicator says if --responseType parameter should be visible or not * * If --all or --finder parameter have not been specified, --responseType parameter will not be visible * to preserve order. * * @return */ @CliOptionVisibilityIndicator( params = "package", command = "web mvc finder", help = "--package parameter is not be visible if --all or --entity parameters have not been specified before.") public boolean isPackageParameterVisible(ShellContext context) { if (context.getParameters().containsKey("entity") || context.getParameters().containsKey("all")) { return true; } return false; } /** * This indicator says if --package parameter should be visible or not. If project has more * than one 'application' modules (which contain one @SpringBootApplication), package will * be mandatory. * * @param shellContext * @return */ @CliOptionMandatoryIndicator(params = "package", command = "web mvc finder") public boolean isPackageRequired(ShellContext shellContext) { if (getTypeLocationService().getModuleNames(ModuleFeatureName.APPLICATION).size() <= 1) { return false; } return true; } /** * This indicator says if --responseType parameter should be visible or not * * If --all or --entity parameter have not been specified, --responseType parameter will not be visible * to preserve order. * * @return */ @CliOptionVisibilityIndicator( params = "responseType", command = "web mvc finder", help = "--responseType parameter is not be visible if --all or --entity parameters have not been specified before.") public boolean isResponseTypeParameterVisible(ShellContext context) { if (context.getParameters().containsKey("entity") || context.getParameters().containsKey("all")) { return true; } return false; } /** * This indicator returns all possible values for --responseType parameter. * * Depends of the specified --controller, responseTypes will be filtered to provide only that * responseTypes that exists on current controller. Also, only installed response types * will be provided. * * @param context * @return */ @CliOptionAutocompleteIndicator(param = "responseType", command = "web mvc finder", help = "--responseType parameter should be completed with the provided response types.") public List<String> getAllResponseTypeValues(ShellContext context) { // Generating all possible values List<String> responseTypes = new ArrayList<String>(); // Getting all installed services that implements ControllerMVCResponseService Map<String, ControllerMVCResponseService> installedResponseTypes = getInstalledControllerMVCResponseTypes(); for (Entry<String, ControllerMVCResponseService> responseType : installedResponseTypes .entrySet()) { // Add installed response type responseTypes.add(responseType.getKey()); } if (responseTypes.isEmpty()) { responseTypes.add(""); } return responseTypes; } /** * This indicator says if --pathPrefix parameter should be visible or not * * If --all or --entity parameter have not been specified, --pathPrefix parameter will not be visible * to preserve order. * * @return */ @CliOptionVisibilityIndicator( params = "pathPrefix", command = "web mvc finder", help = "--pathPrefix parameter is not be visible if --all or --entity parameters have not been specified before.") public boolean isPathPrefixParameterVisible(ShellContext context) { if (context.getParameters().containsKey("entity") || context.getParameters().containsKey("all")) { return true; } return false; } /** * This method provides the Command definition to be able to add * new finder on controllers. * * @param controller * @param all * @param finder * @param responseType */ @CliCommand( value = "web mvc finder", help = "Publishes existing finders to web layer, generating controllers and additional views for " + "them. It adds `@RooWebFinder` annotation to MVC controller type.") public void addController( @CliOption( key = "entity", mandatory = false, help = "The entity owning the finders that should be published. 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.") JavaType entity, @CliOption( key = "all", mandatory = false, specifiedDefaultValue = "true", unspecifiedDefaultValue = "false", help = "Indicates if developer wants to publish in web layer all finders from all entities in " + "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 = "queryMethod", mandatory = false, help = "Indicates the name of the finder to add to web layer. " + "Possible values are: any of the finder names created for the entity, included in " + "`@RooJpaRepository` of the `--entity` associated repository. " + "This option is available only when `--entity` has been specified.") String queryMethod, @CliOption( key = "responseType", mandatory = false, help = "Indicates the responseType to be used by generated finder controllers. Depending on " + "the selected responseType, generated methods and views will vary. " + "Possible values are: `JSON` plus any response type installed with `web mvc view setup` " + "command. " + "This option is only available if `--all` or `--entity` parameters have been specified. " + "Default: `JSON`.") String responseType, @CliOption( key = "package", mandatory = true, unspecifiedDefaultValue = "~.web", help = "Indicates the Java package where the finder controllers should be generated. In" + " multi-module project you should specify the module name before the package name. " + "Ex.: `--package application:org.springframework.roo.web` but, if module name is not " + "present, the Roo Shell focused module will be used. " + "This option is available only if `--all` or `--entity` option has been specified. " + "Default value if not present: `~.web` package, or 'application:~.web' if multi-module " + "project.") JavaPackage controllerPackage, @CliOption( key = "pathPrefix", mandatory = false, unspecifiedDefaultValue = "", specifiedDefaultValue = "", help = "Indicates the default path value for accesing finder resources in controller, used for " + "this controller `@RequestMapping` excluding first '/'. " + "This option is available only if `--all` or `--entity` option has been specified.") String pathPrefix) { // Getting --responseType service Map<String, ControllerMVCResponseService> responseTypeServices = getInstalledControllerMVCResponseTypes(); // Validate that provided responseType is a valid provided ControllerMVCResponseService controllerResponseType = null; if (responseType != null) { if (!responseTypeServices.containsKey(responseType)) { LOGGER.log(Level.SEVERE, "ERROR: Provided responseType is not valid. Use autocomplete feature " + "to obtain valid responseTypes."); return; } else { controllerResponseType = responseTypeServices.get(responseType); } } else { controllerResponseType = responseTypeServices.get("JSON"); } // Execute finder operation if (!all) { // Create queryMethods list List<String> queryMethods = new ArrayList<String>(); if (queryMethod != null) { queryMethods.add(queryMethod); } else { queryMethods = getFinders(entity, controllerResponseType); } webFinderOperations.createOrUpdateSearchControllerForEntity(entity, queryMethods, controllerResponseType, controllerPackage, pathPrefix); } else { webFinderOperations.createOrUpdateSearchControllerForAllEntities(controllerResponseType, controllerPackage, pathPrefix); } } /** * 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(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(getProjectOperations().getFocusedModuleName())) { // Target module is not focused javaTypeString = type.getModule().concat(LogicalPath.MODULE_PATH_SEPARATOR); topLevelPackageString = getProjectOperations().getTopLevelPackage(type.getModule()) .getFullyQualifiedPackageName(); } else if (StringUtils.isNotBlank(type.getModule()) && type.getModule().equals(getProjectOperations().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 = getProjectOperations().getTopLevelPackage(type.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; } /** * Get all finder names associated to an entity * * @param entity the JavaType representing the entity whose finder names should * be returned. * @param controllerResponseType (can be null) * @return a List<String> with the finder names. */ public List<String> getFinders(JavaType entity, ControllerMVCResponseService controllerResponseType) { return webFinderOperations.getFindersWhichCanBePublish(entity, controllerResponseType); } /** * This method gets all implementations of ControllerMVCResponseService interface to be able * to locate all installed ControllerMVCResponseService * * @return Map with responseTypes identifier and the ControllerMVCResponseService implementation */ public Map<String, ControllerMVCResponseService> getInstalledControllerMVCResponseTypes() { if (responseTypes.isEmpty()) { try { ServiceReference<?>[] references = this.context .getAllServiceReferences(ControllerMVCResponseService.class.getName(), null); for (ServiceReference<?> ref : references) { ControllerMVCResponseService responseTypeService = (ControllerMVCResponseService) this.context.getService(ref); boolean isInstalled = false; for (Pom module : getProjectOperations().getPoms()) { if (responseTypeService.isInstalledInModule(module.getModuleName())) { isInstalled = true; break; } } if (isInstalled) { responseTypes.put(responseTypeService.getResponseType(), responseTypeService); } } return responseTypes; } catch (InvalidSyntaxException e) { LOGGER.warning("Cannot load ControllerMVCResponseService on ControllerCommands."); return null; } } else { return responseTypes; } } public ProjectOperations getProjectOperations() { return serviceInstaceManager.getServiceInstance(this, ProjectOperations.class); } /** * This method obtains JavaType converter to be able to obtain JavaType * from strings * * @return */ @SuppressWarnings("unchecked") public Converter<JavaType> getJavaTypeConverter() { if (javaTypeConverter == null) { List<Converter> javaTypeConverters = serviceInstaceManager.getServiceInstance(this, Converter.class, new ServiceInstaceManager.Matcher<Converter>() { @Override public boolean match(Converter service) { return service.supports(JavaType.class, ""); } }); if (!javaTypeConverters.isEmpty()) { javaTypeConverter = javaTypeConverters.get(0); } return javaTypeConverter; } return javaTypeConverter; } public TypeLocationService getTypeLocationService() { return serviceInstaceManager.getServiceInstance(this, TypeLocationService.class); } }