package org.springframework.roo.addon.jpa.addon;
import static org.springframework.roo.model.RooJavaType.ROO_EQUALS;
import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN;
import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY;
import static org.springframework.roo.model.RooJavaType.ROO_PLURAL;
import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE;
import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING;
import static org.springframework.roo.shell.OptionContexts.APPLICATION_FEATURE_INCLUDE_CURRENT_MODULE;
import static org.springframework.roo.shell.OptionContexts.INTERFACE;
import static org.springframework.roo.shell.OptionContexts.SUPERCLASS;
import static org.springframework.roo.shell.OptionContexts.UPDATELAST_PROJECT;
import static org.springframework.roo.shell.OptionContexts.UPDATE_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.service.component.ComponentContext;
import org.springframework.roo.addon.jpa.addon.entity.IdentifierStrategy;
import org.springframework.roo.addon.propfiles.PropFileOperations;
import org.springframework.roo.classpath.ModuleFeatureName;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.BeanInfoUtils;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.operations.InheritanceType;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.model.JpaJavaType;
import org.springframework.roo.model.ReservedWords;
import org.springframework.roo.model.SpringJavaType;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.maven.Pom;
import org.springframework.roo.settings.project.ProjectSettingsService;
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.ShellContext;
import org.springframework.roo.shell.converters.StaticFieldConverter;
import org.springframework.roo.support.logging.HandlerUtils;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Commands for the JPA add-on to be used by the ROO shell.
*
* @author Stefan Schmidt
* @author Ben Alex
* @author Alan Stewart
* @author Juan Carlos García
* @author Sergio Clares
* @since 1.0
*/
@Component
@Service
public class JpaCommands implements CommandMarker {
private static Logger LOGGER = HandlerUtils.getLogger(JpaCommands.class);
// Project Settings
private static final String SPRING_ROO_JPA_REQUIRE_SCHEMA_OBJECT_NAME =
"spring.roo.jpa.require.schema-object-name";
// Annotations
private static final AnnotationMetadataBuilder ROO_EQUALS_BUILDER =
new AnnotationMetadataBuilder(ROO_EQUALS);
private static final AnnotationMetadataBuilder ROO_SERIALIZABLE_BUILDER =
new AnnotationMetadataBuilder(ROO_SERIALIZABLE);
private static final AnnotationMetadataBuilder ROO_TO_STRING_BUILDER =
new AnnotationMetadataBuilder(ROO_TO_STRING);
private static final String IDENTIFIER_DEFAULT_TYPE = "java.lang.Long";
private static final String VERSION_DEFAULT_TYPE = "java.lang.Integer";
@Reference
private JpaOperations jpaOperations;
@Reference
private ProjectOperations projectOperations;
@Reference
private PropFileOperations propFileOperations;
@Reference
private StaticFieldConverter staticFieldConverter;
@Reference
private TypeLocationService typeLocationService;
@Reference
private ProjectSettingsService projectSettings;
@Reference
private PathResolver pathResolver;
@Reference
private FileManager fileManager;
protected void activate(final ComponentContext context) {
staticFieldConverter.add(JdbcDatabase.class);
staticFieldConverter.add(OrmProvider.class);
}
protected void deactivate(final ComponentContext context) {
staticFieldConverter.remove(JdbcDatabase.class);
staticFieldConverter.remove(OrmProvider.class);
}
@CliAvailabilityIndicator({"jpa setup"})
public boolean isJpaSetupAvailable() {
return jpaOperations.isJpaInstallationPossible();
}
@CliAvailabilityIndicator({"entity jpa", "embeddable"})
public boolean isClassGenerationAvailable() {
return jpaOperations.isJpaInstalled();
}
@CliCommand(
value = "embeddable",
help = "Creates a new Java class source file with the JPA `@Embeddable` annotation in the directory _src/main/java_ of the selected project module (if any).")
public void createEmbeddableClass(
@CliOption(key = "class", optionContext = UPDATE_PROJECT, mandatory = true,
help = "The name of the embeddable class to create. If you consider it "
+ "necessary, you can also specify the package (base package can be "
+ "specified with `~`). Ex.: `--class ~.domain.MyEmbeddableClass`. "
+ "You can specify module as well, if necessary. "
+ "Ex.: `--class model:~.domain.MyEmbeddableClass`. When working "
+ "with a multi-module project, if module is not specified the class "
+ "will be created in the module which has the focus.") final JavaType name,
@CliOption(key = "serializable", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Whether the generated class should implement `java.io.Serializable`. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean serializable,
@CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Indicates whether reserved words are ignored by Roo. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean permitReservedWords) {
if (!permitReservedWords) {
ReservedWords.verifyReservedWordsNotPresent(name);
}
jpaOperations.newEmbeddableClass(name, serializable);
}
@CliOptionVisibilityIndicator(command = "jpa setup", params = {"jndiDataSource"},
help = "`--jndiDataSource` parameter is not available if any of `--database`, "
+ "`--databaseName`, `--hostName`, `--password` or `--userName` are "
+ "present or if you are using an HYPERSONIC database.")
public boolean isJndiVisible(ShellContext shellContext) {
Map<String, String> params = shellContext.getParameters();
// If user define databaseName, hostName, password or username parameters,
// jndiDataSource
// should not be visible.
if (params.containsKey("database") || params.containsKey("databaseName")
|| params.containsKey("hostName") || params.containsKey("password")
|| params.containsKey("userName")) {
return false;
}
return true;
}
@CliOptionVisibilityIndicator(command = "jpa setup", params = {"databaseName", "hostName",
"password", "userName"},
help = "Connection parameters are not available if jndiDatasource is "
+ "specified or if you are using an HYPERSONIC database.")
public boolean areConnectionParamsVisible(ShellContext shellContext) {
Map<String, String> params = shellContext.getParameters();
// If parameter database is not defined, all parameters are not visible
String database = params.get("database");
if (database == null) {
return false;
}
// If uses some memory databases or file databases, jndiDataSource parameter
// should not be visible.
if (database.startsWith("HYPERSONIC") || database.equals("H2_IN_MEMORY")) {
return false;
}
// If user define jndiDatasource parameter, connection parameters should not
// be visible
if (params.containsKey("jndiDataSource")) {
return false;
}
return true;
}
@CliOptionVisibilityIndicator(command = "jpa setup", params = "database",
help = "'--database' option is not available if '--jndiDatasource' " + "is specified.")
public boolean areProviderAndDatabaseVisible(ShellContext shellContext) {
Map<String, String> params = shellContext.getParameters();
// If user define jndiDatasource parameter, database should not be visible
if (params.containsKey("jndiDataSource")) {
return false;
}
return true;
}
@CliOptionVisibilityIndicator(command = "jpa setup", params = {"module"},
help = "Module parameter is not available if there is only one application module")
public boolean isModuleVisible(ShellContext shellContext) {
if (typeLocationService.getModuleNames(ModuleFeatureName.APPLICATION).size() > 1) {
return true;
}
return false;
}
@CliOptionMandatoryIndicator(params = "module", command = "jpa setup")
public boolean isModuleRequired(ShellContext shellContext) {
Pom module = projectOperations.getFocusedModule();
if (!isModuleVisible(shellContext)
|| typeLocationService.hasModuleFeature(module, ModuleFeatureName.APPLICATION)) {
return false;
}
return true;
}
@CliCommand(
value = "jpa setup",
help = "Installs or updates a JPA persistence provider in your project. User can execute this "
+ "command for diferent profiles with different persistence configurations.")
public void installJpa(
@CliOption(key = "provider", mandatory = true,
help = "The persistence ORM provider to support. "
+ "Possible values are: `ECLIPSELINK` and `HIBERNATE`. "
+ "This option is available only if `--jndiDataSource` has not been specified. "
+ "This option is mandatory if `--jndiDataSource` has not been specified.") final OrmProvider ormProvider,
@CliOption(
key = "database",
mandatory = false,
help = "The database type to support."
+ "Possible values are: `DB2_400`, `DB2_EXPRESS_C`, `DERBY_CLIENT`, `DERBY_EMBEDDED`, "
+ "`FIREBIRD`, `H2_IN_MEMORY`, `HYPERSONIC_IN_MEMORY`, `HYPERSONIC_PERSISTENT`, `MSSQL`, "
+ "`MYSQL`, `ORACLE`, `POSTGRES` and `SYBASE`. "
+ "This option is mandatory if `--jndiDataSource` has not been specified. "
+ "This option is available only if `--jndiDataSource` has not been specified.") final JdbcDatabase jdbcDatabase,
@CliOption(
key = "module",
mandatory = true,
help = "The application module where to install the persistence. "
+ "This option is mandatory if the focus is not set in an application module, that is, a "
+ "module containing an `@SpringBootApplication` class. "
+ "This option is available only if there are more than one application module and none "
+ "of them is focused. "
+ "Default if option not present: the unique 'application' module, or focused 'application'"
+ " module.", unspecifiedDefaultValue = ".",
optionContext = APPLICATION_FEATURE_INCLUDE_CURRENT_MODULE) Pom module,
@CliOption(key = "jndiDataSource", mandatory = false, help = "The JNDI datasource to use. "
+ "This option is not available if any of `--provider`, `--database`, `--databaseName`, "
+ "`--hostName`, `--password` or `--userName` options are specified.") final String jndi,
@CliOption(key = "hostName", mandatory = false, help = "The host name to use. "
+ "This option is available if `--database` has already been specified and its value is"
+ " not `HYPERSONIC` or `H2_IN_MEMORY` and `--jndiDatasource` has not been specified.") final String hostName,
@CliOption(key = "databaseName", mandatory = false, help = "The database name to use. "
+ "This option is available if `--database` has already been specified and its value is"
+ " not `HYPERSONIC` or `H2_IN_MEMORY` and `--jndiDatasource` has not been specified.") final String databaseName,
@CliOption(key = "userName", mandatory = false, help = "The username to use. "
+ "This option is available if `--database` has already been specified and its value is"
+ " not `HYPERSONIC` or `H2_IN_MEMORY` and `--jndiDatasource` has not been specified.") final String userName,
@CliOption(key = "password", mandatory = false, help = "The password to use. "
+ "This option is available if `--database` has already been specified and its value is"
+ " not `HYPERSONIC` or `H2_IN_MEMORY` and `--jndiDatasource` has not been specified.") final String password,
ShellContext shellContext) {
if (jdbcDatabase != null && jdbcDatabase == JdbcDatabase.FIREBIRD && !isJdk6OrHigher()) {
LOGGER.warning("JDK must be 1.6 or higher to use Firebird");
return;
}
jpaOperations.configureJpa(ormProvider, jdbcDatabase, module, jndi, hostName, databaseName,
userName, password, shellContext.getProfile(), shellContext.isForce());
}
/**
* Indicator that checks if versionField param has been specified and makes
* its associate params visible
*
* @param shellContext
* @return true if versionField param has been specified.
*/
@CliOptionVisibilityIndicator(
command = "entity jpa",
params = {"versionType", "versionColumn"},
help = "Options --versionType and --versionColumn must be used with the --versionField option.")
public boolean areVersionParamsVisibleForEntityJpa(ShellContext shellContext) {
String versionFieldParam = shellContext.getParameters().get("versionField");
if (versionFieldParam != null) {
return true;
}
return false;
}
/**
* ROO-3709: Indicator that checks if exists some project setting that makes
* each of the following parameters mandatory: sequenceName, identifierColumn,
* identifierStrategy, versionField, versionColumn, versionType and table.
*
* @param shellContext
* @return true if exists property
* {@link #SPRING_ROO_JPA_REQUIRE_SCHEMA_OBJECT_NAME} on project
* settings and its value is "true". If not, return false.
*/
@CliOptionMandatoryIndicator(params = {"sequenceName", "identifierStrategy", "identifierColumn",
"table", "versionField", "versionColumn", "versionType"}, command = "entity jpa")
public boolean areSchemaObjectNamesRequired(ShellContext shellContext) {
// Check if property 'spring.roo.jpa.require.schema-object-name' is defined
// on
// project settings
String requiredSchemaObjectName =
projectSettings.getProperty(SPRING_ROO_JPA_REQUIRE_SCHEMA_OBJECT_NAME);
if (requiredSchemaObjectName != null && requiredSchemaObjectName.equals("true")) {
return true;
}
return false;
}
/**
* Indicator that provides all possible values for --class parameter The
* provided results will not be validate. It will not include space on finish.
*
* @param shellContext
* @return List with all possible values for --class parameter
*/
@CliOptionAutocompleteIndicator(command = "entity jpa", param = "class",
help = "Provided --class option should be a class annotated with @RooJpaEntity.",
validate = false, includeSpaceOnFinish = false)
public List<String> getClassPossibleValues(ShellContext shellContext) {
List<String> allPossibleValues = new ArrayList<String>();
// Add all modules to completions list
if (projectOperations.isMultimoduleProject()) {
Collection<String> modules = projectOperations.getModuleNames();
for (String module : modules) {
// Ignore root module
if (StringUtils.isBlank(module)) {
continue;
}
// Ignore module name if it is the focused module
if (module.equals(projectOperations.getFocusedModule().getModuleName())) {
List<JavaPackage> modulePackages = typeLocationService.getPackagesForModule(module);
// Always add module top level package and project top level package
modulePackages.add(projectOperations.getTopLevelPackage(module));
for (JavaPackage javaPackage : modulePackages) {
// Check if package name contains top level package to shorten it
String currentPackageName =
getPackageStringValue(module, javaPackage.getFullyQualifiedPackageName());
// Add package to possible values
if (!allPossibleValues.contains(currentPackageName.concat("."))) {
allPossibleValues.add(currentPackageName.concat("."));
}
}
} else {
// It is not the focused module
List<JavaPackage> modulePackages = typeLocationService.getPackagesForModule(module);
// Always add module top level package and project top level package
modulePackages.add(projectOperations.getTopLevelPackage(module));
for (JavaPackage javaPackage : modulePackages) {
// Check if package name contains top level package to shorten it
String currentPackageName =
getPackageStringValue(module, javaPackage.getFullyQualifiedPackageName());
// Add package to possible values
String valueToAdd =
String.format("%s%s%s.", module, LogicalPath.MODULE_PATH_SEPARATOR,
currentPackageName);
if (!allPossibleValues.contains(valueToAdd)) {
allPossibleValues.add(valueToAdd);
}
}
}
}
} else {
// Check all JavaPackages in single module project
for (JavaPackage javaPackage : typeLocationService.getPackagesForModule("")) {
// Check if package name contains top level package to shorten it
String currentPackageName =
getPackageStringValue("", javaPackage.getFullyQualifiedPackageName());
// Add package to possible values
if (!allPossibleValues.contains(currentPackageName.concat("."))) {
allPossibleValues.add(currentPackageName.concat("."));
}
}
}
return allPossibleValues;
}
@CliOptionAutocompleteIndicator(
command = "entity jpa",
param = "identifierType",
help = "--identifierType option should be a wrapper of a primitive type or an embeddable class.")
public List<String> getIdentifierTypePossibleValues(ShellContext shellContext) {
String currentText = shellContext.getParameters().get("identifierType");
List<String> allPossibleValues = new ArrayList<String>();
// Add java-lang and java-number classes
allPossibleValues.add(Number.class.getName());
allPossibleValues.add(Short.class.getName());
allPossibleValues.add(Byte.class.getName());
allPossibleValues.add(Integer.class.getName());
allPossibleValues.add(Long.class.getName());
allPossibleValues.add(Float.class.getName());
allPossibleValues.add(Double.class.getName());
allPossibleValues.add(BigDecimal.class.getName());
allPossibleValues.add(BigInteger.class.getName());
// Getting all existing embeddable classes
Set<ClassOrInterfaceTypeDetails> embeddableClassesInProject =
typeLocationService.findClassesOrInterfaceDetailsWithAnnotation(JpaJavaType.EMBEDDABLE);
for (ClassOrInterfaceTypeDetails embeddableClass : embeddableClassesInProject) {
String name = replaceTopLevelPackageString(embeddableClass, currentText);
if (!allPossibleValues.contains(name)) {
allPossibleValues.add(name);
}
}
return allPossibleValues;
}
@CliOptionAutocompleteIndicator(command = "entity jpa", param = "versionType",
help = "--versionType option should be a wrapper of a primitive type.")
public List<String> getVersionTypePossibleValues(ShellContext shellContext) {
List<String> allPossibleValues = new ArrayList<String>();
// Add java-lang and java-number classes
allPossibleValues.add(Number.class.getName());
allPossibleValues.add(Short.class.getName());
allPossibleValues.add(Byte.class.getName());
allPossibleValues.add(Integer.class.getName());
allPossibleValues.add(Long.class.getName());
allPossibleValues.add(Float.class.getName());
allPossibleValues.add(Double.class.getName());
allPossibleValues.add(BigDecimal.class.getName());
allPossibleValues.add(BigInteger.class.getName());
return allPossibleValues;
}
@CliCommand(
value = "entity jpa",
help = "Creates a new JPA persistent entity in the directory _src/main/java_ of the selected project module (if any) with `@RooEntity` annotation.")
public void newPersistenceClassJpa(
@CliOption(
key = "class",
optionContext = UPDATELAST_PROJECT,
mandatory = true,
help = "The name of the entity to create. If you consider it necessary, you can also "
+ "specify the package (base package can be specified with `~`). "
+ "Ex.: `--class ~.domain.MyEntity`. You can specify module as well, if necessary. "
+ "Ex.: `--class model:~.domain.MyEntity`. When working with a multi-module project, "
+ "if module is not specified the entity will be created in the module which has the focus.") final JavaType name,
@CliOption(
key = "table",
mandatory = true,
help = "The JPA table name to use for this entity. "
+ "This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`.") final String table,
@CliOption(
key = "identifierColumn",
mandatory = true,
help = "The JPA identifier field column to use for this entity. "
+ "This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`.") final String identifierColumn,
@CliOption(
key = "versionField",
mandatory = true,
help = "The JPA version field name to use for this entity. "
+ "This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`.") final String versionField,
@CliOption(
key = "versionColumn",
mandatory = true,
help = "The JPA version field column to use for this entity. "
+ "This option is available if 'versionField' option is set."
+ " This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`.") final String versionColumn,
@CliOption(
key = "versionType",
mandatory = true,
optionContext = "java-lang,project",
unspecifiedDefaultValue = VERSION_DEFAULT_TYPE,
help = "The data type that will be used for the JPA version field. "
+ "This option is available if 'versionField' option is set."
+ " This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`.") final JavaType versionType,
@CliOption(
key = "sequenceName",
mandatory = true,
help = "The name of the sequence for incrementing sequence-driven primary keys."
+ " This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`.") final String sequenceName,
@CliOption(
key = "identifierStrategy",
mandatory = true,
specifiedDefaultValue = "AUTO",
help = "The generation value strategy to be used."
+ " This option is mandatory if `spring.roo.jpa.require.schema-object-name` configuration setting it’s `true`. "
+ "Default if option present: `AUTO`.") final IdentifierStrategy identifierStrategy,
@CliOption(key = "extends", mandatory = false, unspecifiedDefaultValue = "java.lang.Object",
optionContext = SUPERCLASS, help = "The fully qualified name of the superclass. "
+ "Default if option not present: `java.lang.Object`.") final JavaType superclass,
@CliOption(key = "implements", mandatory = false, optionContext = INTERFACE,
help = "The fully qualified name of the interface to implement.") final JavaType implementsType,
@CliOption(key = "abstract", mandatory = false, specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false",
help = "Whether the generated class should be marked as abstract. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean createAbstract,
@CliOption(key = "schema", mandatory = false,
help = "The JPA table schema name to use for this entity.") final String schema,
@CliOption(key = "catalog", mandatory = false,
help = "The JPA table catalog name to use for this entity.") final String catalog,
@CliOption(key = "identifierField", mandatory = false,
help = "The JPA identifier field name to use for this entity.") final String identifierField,
@CliOption(key = "identifierType", mandatory = false, optionContext = "java-lang,project",
unspecifiedDefaultValue = IDENTIFIER_DEFAULT_TYPE,
specifiedDefaultValue = "java.lang.Long",
help = "The data type that will be used for the JPA identifier field. "
+ "Default: `java.lang.Long`.") final JavaType identifierType,
@CliOption(key = "inheritanceType", mandatory = false,
help = "The JPA @Inheritance value (apply to base class)") final InheritanceType inheritanceType,
@CliOption(key = "mappedSuperclass", mandatory = false, specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false", help = "Apply @MappedSuperclass for this entity. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean mappedSuperclass,
@CliOption(key = "serializable", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Whether the generated class should implement `java.io.Serializable`."
+ "Default if option present: `true`; default if option not present: `false`.") final boolean serializable,
@CliOption(key = "permitReservedWords", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Indicates whether reserved words are ignored by Roo. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean permitReservedWords,
@CliOption(key = "entityName", mandatory = false,
help = "The name used to refer to the entity in queries.") final String entityName,
@CliOption(key = "readOnly", mandatory = false, unspecifiedDefaultValue = "false",
specifiedDefaultValue = "true",
help = "Whether the generated entity should be used for read operations only. "
+ "Default if option present: `true`; default if option not present `false`.") final boolean readOnly,
@CliOption(
key = "plural",
mandatory = false,
help = "Specify the plural of this new entity. If not provided, a calculated plural will be used by default.") String plural,
@CliOption(
key = "entityFormatExpression",
mandatory = false,
help = "The SpEL expression used to format the entity when showing it in presentation layer e.g. "
+ "`{#fieldA} {#fieldB}`. It adds the `value` attribute to `io.springlets.format.EntityFormat` "
+ "annotation.") String formatExpression,
@CliOption(
key = "entityFormatMessage",
mandatory = false,
help = "The message key used to obtain a localized SpEL expression to format the entity when "
+ "showing it in presentation layer. It adds the `message` attribute to "
+ "`io.springlets.format.EntityFormat` annotation and creates a message in all message bundles "
+ "with the provided key. Message value should be modified by developer. This kind of format "
+ "has more priority that 'expression' format added with `--entityFormatExpression`.") String formatMessage,
ShellContext shellContext) {
Validate.isTrue(!identifierType.isPrimitive(), "Identifier type cannot be a primitive");
// Check if exists other entity with the same name
final String entityFilePathIdentifier =
pathResolver.getCanonicalPath(name.getModule(), Path.SRC_MAIN_JAVA, name);
if (fileManager.exists(entityFilePathIdentifier) && shellContext.isForce()) {
fileManager.delete(entityFilePathIdentifier);
} else if (fileManager.exists(entityFilePathIdentifier) && !shellContext.isForce()) {
throw new IllegalArgumentException(
String
.format(
"Entity '%s' already exists and cannot be created. Try to use a "
+ "different entity name on --class parameter or use --force parameter to overwrite it.",
name));
}
// Check valid value for --extends
if (superclass == null && shellContext.getParameters().get("extends").equals("")) {
throw new IllegalArgumentException(
"Option --extends must have a value when specified. Please, assign it a value.");
}
// ROO-3723: Add warning when using --extends with incompatible parameters
if (superclass != null && !("java.lang.Object").equals(superclass.getFullyQualifiedTypeName())
&& !shellContext.isForce()) {
this.checkExtendsOverride(identifierColumn, identifierField, identifierStrategy,
identifierType, sequenceName, versionColumn, versionField, versionType);
}
// ROO-3764: Check reserved words only if doesn't permit reserved words
// and table name has not been specified
if (!permitReservedWords && StringUtils.isBlank(table)) {
// Use try to catch exception and show custom message for this situation
try {
ReservedWords.verifyReservedWordsNotPresent(name);
} catch (IllegalStateException exception) {
LOGGER.log(Level.INFO,
"ERROR: You are trying to use a reserved word as entity name. You have the following options:\n"
+ "1. Change provided entity name.\n"
+ "2. Specify a valid table name using --table parameter.\n"
+ "3. Use parameter --permitReservedWords to force use of reserved words.\n");
return;
}
} else if (!permitReservedWords && StringUtils.isNotBlank(table)) {
// If table name has been specified but doesn't permit reserved words,
// check SQL reserved words on table name and JAVA reserved words on
// entity name
// Use try to catch exception and show custom message for this situation
try {
ReservedWords.verifyReservedSqlKeywordsNotPresent(table);
} catch (IllegalStateException exception) {
LOGGER.log(Level.INFO,
"ERROR: You are trying to use a SQL reserved word as table name. You have the following options:\n"
+ "1. Specify a valid table name using --table parameter.\n"
+ "2. Use parameter --permitReservedWords to force use of reserved words.\n");
return;
}
// Use try to catch exception and show custom message for this situation
try {
ReservedWords.verifyReservedJavaKeywordsNotPresent(name);
} catch (IllegalStateException exception) {
LOGGER
.log(
Level.INFO,
"ERROR: You are trying to use a Java reserved word as entity name. You have the following options:\n"
+ "1. Change provided entity name.\n"
+ "2. Use parameter --permitReservedWords to force use of reserved words.\n");
return;
}
}
// Reject attempts to name the entity "Test", due to possible clashes
// with data on demand (see ROO-50)
// We will allow this to happen, though if the user insists on it via
// --permitReservedWords (see ROO-666)
if (!BeanInfoUtils.isEntityReasonablyNamed(name)) {
if (!permitReservedWords) {
throw new IllegalArgumentException(
"Entity name rejected as conflicts with test execution defaults; please remove "
+ "'Test' and/or 'TestCase'");
}
}
// Create entity's annotations
final List<AnnotationMetadataBuilder> annotationBuilder =
new ArrayList<AnnotationMetadataBuilder>();
final AnnotationMetadataBuilder javaBeanAnnotationBuilder =
new AnnotationMetadataBuilder(ROO_JAVA_BEAN);
if (readOnly) {
// ROO-3838: "ReadOnly" entities should not have setter methods.
javaBeanAnnotationBuilder.addBooleanAttribute("settersByDefault", false);
}
annotationBuilder.add(javaBeanAnnotationBuilder);
annotationBuilder.add(ROO_TO_STRING_BUILDER);
annotationBuilder.add(getEntityAnnotationBuilder(table, schema, catalog, inheritanceType,
mappedSuperclass, entityName, readOnly, formatExpression, formatMessage));
// Add @RooEquals only if it's superclass is not an entity
ClassOrInterfaceTypeDetails superclassCid = typeLocationService.getTypeDetails(superclass);
if (superclassCid == null || superclassCid.getAnnotation(ROO_JPA_ENTITY) == null) {
final AnnotationMetadataBuilder equalsAnnotationBuilder = ROO_EQUALS_BUILDER;
equalsAnnotationBuilder.addBooleanAttribute("isJpaEntity", true);
annotationBuilder.add(equalsAnnotationBuilder);
}
// Add @RooSerializable
if (serializable) {
annotationBuilder.add(ROO_SERIALIZABLE_BUILDER);
}
// ROO-3817: Including @RooPlural annotation if needed
if (StringUtils.isNotEmpty(plural)) {
AnnotationMetadataBuilder pluralAnnotation = new AnnotationMetadataBuilder(ROO_PLURAL);
pluralAnnotation.addStringAttribute("value", plural);
annotationBuilder.add(pluralAnnotation);
}
// Produce the entity itself
jpaOperations.newEntity(name, createAbstract, superclass, implementsType, identifierField,
identifierType, identifierColumn, sequenceName, identifierStrategy, versionField,
versionType, versionColumn, inheritanceType, annotationBuilder);
// Update entity identifier class if required (identifierClass should be
// only an embeddable class)
if (!(identifierType.getPackage().getFullyQualifiedPackageName().startsWith("java."))) {
jpaOperations.updateEmbeddableToIdentifier(identifierType, identifierField, identifierColumn);
}
}
/**
* Returns a builder for the entity-related annotation to be added to a newly
* created JPA entity
*
* @param table
* @param schema
* @param catalog
* @param inheritanceType
* @param mappedSuperclass
* @param entityName
* @param readOnly
* @return a non-<code>null</code> builder
*/
private AnnotationMetadataBuilder getEntityAnnotationBuilder(final String table,
final String schema, final String catalog, final InheritanceType inheritanceType,
final boolean mappedSuperclass, final String entityName, final boolean readOnly,
final String formatExpression, final String formatMessage) {
final AnnotationMetadataBuilder entityAnnotationBuilder =
new AnnotationMetadataBuilder(ROO_JPA_ENTITY);
// Attributes that apply to all JPA entities (active record or not)
if (catalog != null) {
entityAnnotationBuilder.addStringAttribute("catalog", catalog);
}
if (entityName != null) {
entityAnnotationBuilder.addStringAttribute("entityName", entityName);
}
if (inheritanceType != null) {
entityAnnotationBuilder.addStringAttribute("inheritanceType", inheritanceType.name());
}
if (mappedSuperclass) {
entityAnnotationBuilder.addBooleanAttribute("mappedSuperclass", mappedSuperclass);
}
if (schema != null) {
entityAnnotationBuilder.addStringAttribute("schema", schema);
}
if (table != null) {
entityAnnotationBuilder.addStringAttribute("table", table);
}
// ROO-3868: New entity visualization support using a new format annotation
if (StringUtils.isNotBlank(formatExpression)) {
entityAnnotationBuilder.addStringAttribute("entityFormatExpression", formatExpression);
}
if (StringUtils.isNotBlank(formatMessage)) {
entityAnnotationBuilder.addStringAttribute("entityFormatMessage", formatMessage);
}
// ROO-3708: Generate readOnly entities
if (readOnly) {
entityAnnotationBuilder.addBooleanAttribute("readOnly", true);
}
return entityAnnotationBuilder;
}
private boolean isJdk6OrHigher() {
final String ver = System.getProperty("java.version");
return ver.indexOf("1.6.") > -1 || ver.indexOf("1.7.") > -1;
}
/**
* Returns a String with the JavaPackage name to show. It will return a full
* name if provided package doesn't contain the module top level JavaPackage.
* Otherwise, the package name will be shortened using `~`.
*
* @param module the String with the module name.
* @param javaPackage the JavaPackage to extract the String to return.
* @return a String with the value to show for the java package.
*/
private String getPackageStringValue(String module, String javaPackageName) {
// Get project top level package from application class
Set<JavaType> applicationTypes =
typeLocationService.findTypesWithAnnotation(SpringJavaType.SPRING_BOOT_APPLICATION);
Validate.isTrue(!applicationTypes.isEmpty(), "Couldn't find a main class "
+ "annotated with `@SpringBootApplication` in the project.");
String topLevelPackage =
projectOperations.getTopLevelPackage(module).getFullyQualifiedPackageName();
// If package name contains top level package name, shorten it
if (javaPackageName.contains(topLevelPackage)) {
javaPackageName = javaPackageName.replace(topLevelPackage, "~");
}
return javaPackageName;
}
/**
* Check if superclass of the extended entity which it's going to be created
* will override any specified param and shows a message if so. If user uses
* the --force global param it will be possible to execute the command for
* creating the entity.
*
* @param identifierColumn
* @param identifierField
* @param identifierStrategy
* @param identifierType
* @param sequenceName
* @param versionColumn
* @param versionField
* @param versionType
*/
private void checkExtendsOverride(String identifierColumn, String identifierField,
IdentifierStrategy identifierStrategy, JavaType identifierType, String sequenceName,
String versionColumn, String versionField, JavaType versionType) {
if (identifierColumn != null || identifierField != null || identifierStrategy != null
|| !IDENTIFIER_DEFAULT_TYPE.equals(identifierType.getFullyQualifiedTypeName())
|| sequenceName != null || versionColumn != null || versionField != null
|| !VERSION_DEFAULT_TYPE.equals(versionType.getFullyQualifiedTypeName())) {
throw new IllegalArgumentException(
"Identifier and version fields will be overwritten by superclass fields. Please, "
+ "use --force to execute the command anyway.");
}
}
/**
* 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;
}
}