diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java index ef574390bd..926fe5275a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/JdbcMappingContext.java @@ -81,7 +81,7 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert RelationalPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { BasicJdbcPersistentProperty persistentProperty = new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder, this.getNamingStrategy()); - persistentProperty.setForceQuote(isForceQuote()); + applyDefaults(persistentProperty); return persistentProperty; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java new file mode 100644 index 0000000000..df245ba738 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java @@ -0,0 +1,168 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping; + +import java.util.Optional; + +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.Expression; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * SQL-specific {@link RelationalPersistentEntity} implementation that adds SQL-specific meta-data such as the table and + * schema name. + * + * @author Jens Schauder + * @author Greg Turnquist + * @author Bastian Wilhelm + * @author Mikhail Polivakha + * @author Kurt Niemi + */ +class BasicRelationalPersistentEntity extends BasicPersistentEntity + implements RelationalPersistentEntity { + + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + + private final NamingStrategy namingStrategy; + + private final Lazy tableName; + private final @Nullable Expression tableNameExpression; + + private final Lazy> schemaName; + private final @Nullable Expression schemaNameExpression; + private final ExpressionEvaluator expressionEvaluator; + private boolean forceQuote = true; + + /** + * Creates a new {@link BasicRelationalPersistentEntity} for the given {@link TypeInformation}. + * + * @param information must not be {@literal null}. + */ + BasicRelationalPersistentEntity(TypeInformation information, NamingStrategy namingStrategy, + ExpressionEvaluator expressionEvaluator) { + + super(information); + + this.namingStrategy = namingStrategy; + this.expressionEvaluator = expressionEvaluator; + + Lazy> defaultSchema = Lazy.of(() -> { + if (StringUtils.hasText(namingStrategy.getSchema())) { + return Optional.of(createDerivedSqlIdentifier(namingStrategy.getSchema())); + } + return Optional.empty(); + }); + + if (isAnnotationPresent(Table.class)) { + + Table table = getRequiredAnnotation(Table.class); + + // TODO: support expressions for schema + this.tableName = StringUtils.hasText(table.value()) ? Lazy.of(() -> createSqlIdentifier(table.value())) + : Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getTableName(getType()))); + this.tableNameExpression = detectExpression(table.value()); + + this.schemaName = StringUtils.hasText(table.schema()) + ? Lazy.of(() -> Optional.of(createSqlIdentifier(table.schema()))) + : defaultSchema; + this.schemaNameExpression = detectExpression(table.schema()); + + } else { + + this.tableName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getTableName(getType()))); + this.tableNameExpression = null; + this.schemaName = defaultSchema; + this.schemaNameExpression = null; + } + } + + /** + * Returns a SpEL {@link Expression} if the given {@link String} is actually an expression that does not evaluate to a + * {@link LiteralExpression} (indicating that no subsequent evaluation is necessary). + * + * @param potentialExpression can be {@literal null} + * @return can be {@literal null}. + */ + @Nullable + private static Expression detectExpression(@Nullable String potentialExpression) { + + if (!StringUtils.hasText(potentialExpression)) { + return null; + } + + Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION); + return expression instanceof LiteralExpression ? null : expression; + } + + private SqlIdentifier createSqlIdentifier(String name) { + return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name); + } + + private SqlIdentifier createDerivedSqlIdentifier(String name) { + return new DerivedSqlIdentifier(name, isForceQuote()); + } + + public boolean isForceQuote() { + return forceQuote; + } + + public void setForceQuote(boolean forceQuote) { + this.forceQuote = forceQuote; + } + + @Override + public SqlIdentifier getTableName() { + + if (tableNameExpression == null) { + return tableName.get(); + } + + return createSqlIdentifier(expressionEvaluator.evaluate(tableNameExpression)); + } + + @Override + public SqlIdentifier getQualifiedTableName() { + + SqlIdentifier schema = schemaName.get().orElse(null); + + if (schema == null) { + return getTableName(); + } + + if (schemaNameExpression != null) { + schema = createSqlIdentifier(expressionEvaluator.evaluate(schemaNameExpression)); + } + + return SqlIdentifier.from(schema, getTableName()); + } + + @Override + public SqlIdentifier getIdColumn() { + return getRequiredIdProperty().getColumnName(); + } + + @Override + public String toString() { + return String.format("BasicRelationalPersistentEntity<%s>", getType()); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index f372e8bb01..4694bcd5e9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -25,29 +25,40 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.Lazy; import org.springframework.data.util.Optionals; +import org.springframework.expression.Expression; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Meta data about a property to be used by repository implementations. + * SQL-specific {@link org.springframework.data.mapping.PersistentProperty} implementation. * * @author Jens Schauder * @author Greg Turnquist * @author Florian Lüdiger * @author Bastian Wilhelm + * @author Kurt Niemi */ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty implements RelationalPersistentProperty { + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private final Lazy columnName; + private final @Nullable Expression columnNameExpression; private final Lazy> collectionIdColumnName; private final Lazy collectionKeyColumnName; private final Lazy isEmbedded; private final Lazy embeddedPrefix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; + private ExpressionEvaluator spelExpressionProcessor = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); /** * Creates a new {@link BasicRelationalPersistentProperty}. @@ -88,12 +99,20 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity Optional.ofNullable(findAnnotation(Column.class)) // - .map(Column::value) // - .filter(StringUtils::hasText) // - .map(this::createSqlIdentifier) // - .orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)))); + if (isAnnotationPresent(Column.class)) { + Column column = getRequiredAnnotation(Column.class); + + columnName = StringUtils.hasText(column.value()) ? Lazy.of(() -> createSqlIdentifier(column.value())) + : Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); + columnNameExpression = detectExpression(column.value()); + + } else { + columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); + columnNameExpression = null; + } + + // TODO: support expressions for MappedCollection this.collectionIdColumnName = Lazy.of(() -> Optionals .toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)) // .map(MappedCollection::idColumn), // @@ -110,6 +129,29 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)))); } + void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) { + this.spelExpressionProcessor = spelExpressionProcessor; + } + + /** + * Returns a SpEL {@link Expression} if the given {@link String} is actually an expression that does not evaluate to a + * {@link LiteralExpression} (indicating that no subsequent evaluation is necessary). + * + * @param potentialExpression can be {@literal null} + * @return can be {@literal null}. + */ + @Nullable + private static Expression detectExpression(@Nullable String potentialExpression) { + + if (!StringUtils.hasText(potentialExpression)) { + return null; + } + + Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION); + return expression instanceof LiteralExpression ? null : expression; + } + + private SqlIdentifier createSqlIdentifier(String name) { return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name); } @@ -138,7 +180,12 @@ public boolean isEntity() { @Override public SqlIdentifier getColumnName() { - return columnName.get(); + + if (columnNameExpression == null) { + return columnName.get(); + } + + return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression)); } @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Column.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Column.java index 96b29e8bc8..e39f0dd614 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Column.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Column.java @@ -34,7 +34,8 @@ public @interface Column { /** - * The mapping column name. + * The column name. The attribute supports SpEL expressions to dynamically calculate the column name on a + * per-operation basis. */ String value() default ""; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java new file mode 100644 index 0000000000..b3ea71f02f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java @@ -0,0 +1,48 @@ +package org.springframework.data.relational.core.mapping; + +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.util.Assert; + +/** + * Provide support for processing SpEL expressions in @Table and @Column annotations, or anywhere we want to use SpEL + * expressions and sanitize the result of the evaluated SpEL expression. The default sanitization allows for digits, + * alphabetic characters and _ characters and strips out any other characters. Custom sanitization (if desired) can be + * achieved by creating a class that implements the {@link SqlIdentifierSanitizer} interface and then invoking the + * {@link #setSpelExpressionResultSanitizer(SqlIdentifierSanitizer)} method. + * + * @author Kurt Niemi + * @see SqlIdentifierSanitizer + * @since 3.1 + */ +class ExpressionEvaluator { + + private EvaluationContextProvider provider; + + private SqlIdentifierSanitizer sanitizer = SqlIdentifierSanitizer.words(); + + public ExpressionEvaluator(EvaluationContextProvider provider) { + this.provider = provider; + } + + public String evaluate(Expression expression) throws EvaluationException { + + Assert.notNull(expression, "Expression must not be null."); + + String result = expression.getValue(provider.getEvaluationContext(null), String.class); + + return sanitizer.sanitize(result); + } + + public void setSanitizer(SqlIdentifierSanitizer sanitizer) { + + Assert.notNull(sanitizer, "SqlIdentifierSanitizer must not be null"); + + this.sanitizer = sanitizer; + } + + public void setProvider(EvaluationContextProvider provider) { + this.provider = provider; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index c6712a4c9a..966761b749 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -15,10 +15,14 @@ */ package org.springframework.data.relational.core.mapping; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -37,6 +41,8 @@ public class RelationalMappingContext private final NamingStrategy namingStrategy; private boolean forceQuote = true; + private final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); + /** * Creates a new {@link RelationalMappingContext}. */ @@ -77,11 +83,24 @@ public void setForceQuote(boolean forceQuote) { this.forceQuote = forceQuote; } + public void setSqlIdentifierSanitizer(SqlIdentifierSanitizer sanitizer) { + this.expressionEvaluator.setSanitizer(sanitizer); + } + + public NamingStrategy getNamingStrategy() { + return this.namingStrategy; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.expressionEvaluator.setProvider(new ExtensionAwareEvaluationContextProvider(applicationContext)); + } + @Override protected RelationalPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - RelationalPersistentEntityImpl entity = new RelationalPersistentEntityImpl<>(typeInformation, - this.namingStrategy); + BasicRelationalPersistentEntity entity = new BasicRelationalPersistentEntity<>(typeInformation, + this.namingStrategy, this.expressionEvaluator); entity.setForceQuote(isForceQuote()); return entity; @@ -93,13 +112,14 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert BasicRelationalPersistentProperty persistentProperty = new BasicRelationalPersistentProperty(property, owner, simpleTypeHolder, this.namingStrategy); - persistentProperty.setForceQuote(isForceQuote()); + applyDefaults(persistentProperty); return persistentProperty; } - public NamingStrategy getNamingStrategy() { - return this.namingStrategy; + protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) { + persistentProperty.setForceQuote(isForceQuote()); + persistentProperty.setSpelExpressionProcessor(this.expressionEvaluator); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java deleted file mode 100644 index 6bab03ff8c..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2017-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping; - -import java.util.Optional; - -import org.springframework.data.mapping.model.BasicPersistentEntity; -import org.springframework.data.relational.core.sql.SqlIdentifier; -import org.springframework.data.util.Lazy; -import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; - -/** - * Meta data a repository might need for implementing persistence operations for instances of type {@code T} - * - * @author Jens Schauder - * @author Greg Turnquist - * @author Bastian Wilhelm - * @author Mikhail Polivakha - */ -class RelationalPersistentEntityImpl extends BasicPersistentEntity - implements RelationalPersistentEntity { - - private final NamingStrategy namingStrategy; - private final Lazy> tableName; - private final Lazy> schemaName; - private boolean forceQuote = true; - - /** - * Creates a new {@link RelationalPersistentEntityImpl} for the given {@link TypeInformation}. - * - * @param information must not be {@literal null}. - */ - RelationalPersistentEntityImpl(TypeInformation information, NamingStrategy namingStrategy) { - - super(information); - - this.namingStrategy = namingStrategy; - - this.tableName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) // - .map(Table::value) // - .filter(StringUtils::hasText) // - .map(this::createSqlIdentifier)); - - this.schemaName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) // - .map(Table::schema) // - .filter(StringUtils::hasText) // - .map(this::createSqlIdentifier)); - } - - private SqlIdentifier createSqlIdentifier(String name) { - return isForceQuote() ? SqlIdentifier.quoted(name) : SqlIdentifier.unquoted(name); - } - - private SqlIdentifier createDerivedSqlIdentifier(String name) { - return new DerivedSqlIdentifier(name, isForceQuote()); - } - - public boolean isForceQuote() { - return forceQuote; - } - - public void setForceQuote(boolean forceQuote) { - this.forceQuote = forceQuote; - } - - @Override - public SqlIdentifier getTableName() { - - Optional explicitlySpecifiedTableName = tableName.get(); - SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); - - return explicitlySpecifiedTableName.orElse(schemalessTableIdentifier); - } - - @Override - public SqlIdentifier getQualifiedTableName() { - - SqlIdentifier schema = determineCurrentEntitySchema(); - Optional explicitlySpecifiedTableName = tableName.get(); - - SqlIdentifier schemalessTableIdentifier = createDerivedSqlIdentifier(namingStrategy.getTableName(getType())); - - if (schema == null) { - return explicitlySpecifiedTableName.orElse(schemalessTableIdentifier); - } - - return explicitlySpecifiedTableName.map(sqlIdentifier -> SqlIdentifier.from(schema, sqlIdentifier)) - .orElse(SqlIdentifier.from(schema, schemalessTableIdentifier)); - } - - /** - * @return {@link SqlIdentifier} representing the current entity schema. If the schema is not specified, neither - * explicitly, nor via {@link NamingStrategy}, then return {@link null} - */ - @Nullable - private SqlIdentifier determineCurrentEntitySchema() { - - Optional explicitlySpecifiedSchema = schemaName.get(); - return explicitlySpecifiedSchema.orElseGet( - () -> StringUtils.hasText(namingStrategy.getSchema()) ? createDerivedSqlIdentifier(namingStrategy.getSchema()) - : null); - } - - @Override - public SqlIdentifier getIdColumn() { - return getRequiredIdProperty().getColumnName(); - } - - @Override - public String toString() { - return String.format("RelationalPersistentEntityImpl<%s>", getType()); - } -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java new file mode 100644 index 0000000000..68b11a09f1 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SqlIdentifierSanitizer.java @@ -0,0 +1,42 @@ +package org.springframework.data.relational.core.mapping; + +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * Functional interface to sanitize SQL identifiers for SQL usage. Useful to guard SpEL expression results. + * + * @author Kurt Niemi + * @author Mark Paluch + * @since 3.1 + * @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer) + */ +@FunctionalInterface +public interface SqlIdentifierSanitizer { + + /** + * A sanitizer to allow words only. Non-words are removed silently. + * + * @return + */ + static SqlIdentifierSanitizer words() { + + Pattern pattern = Pattern.compile("[^\\w_]"); + + return name -> { + + Assert.notNull(name, "Input to sanitize must not be null"); + + return pattern.matcher(name).replaceAll(""); + }; + } + + /** + * Sanitize a SQL identifier to either remove unwanted character sequences or to throw an exception. + * + * @param sqlIdentifier the identifier name. + * @return sanitized SQL identifier. + */ + String sanitize(String sqlIdentifier); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Table.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Table.java index 9f153afb32..ed34a1ee85 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Table.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/Table.java @@ -15,8 +15,6 @@ */ package org.springframework.data.relational.core.mapping; -import org.springframework.core.annotation.AliasFor; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -24,6 +22,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + /** * The annotation to configure the mapping from a class to a database table. * @@ -38,25 +38,26 @@ public @interface Table { /** - * The mapping table name. + * The table name. The attribute supports SpEL expressions to dynamically calculate the table name on a per-operation + * basis. */ @AliasFor("name") String value() default ""; /** - * The mapping table name. + * The table name. The attribute supports SpEL expressions to dynamically calculate the table name on a per-operation + * basis. */ @AliasFor("value") String name() default ""; /** - * Name of the schema (or user, for example in case of oracle), in which this table resides in - * The behavior is the following:
- * If the {@link Table#schema()} is specified, then it will be - * used as a schema of current table, i.e. as a prefix to the name of the table, which can - * be specified in {@link Table#value()}.
- * If the {@link Table#schema()} is not specified, then spring data will assume the default schema, - * The default schema itself can be provided by the means of {@link NamingStrategy#getSchema()} + * Name of the schema (or user, for example in case of oracle), in which this table resides in The behavior is the + * following:
+ * If the {@link Table#schema()} is specified, then it will be used as a schema of current table, i.e. as a prefix to + * the name of the table, which can be specified in {@link Table#value()}.
+ * If the {@link Table#schema()} is not specified, then spring data will assume the default schema, The default schema + * itself can be provided by the means of {@link NamingStrategy#getSchema()} */ String schema() default ""; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java new file mode 100644 index 0000000000..05087f92c6 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java @@ -0,0 +1,256 @@ +/* + * Copyright 2018-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.relational.core.sql.SqlIdentifier.*; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.BasicRelationalPersistentEntityUnitTests.MyConfig; +import org.springframework.data.relational.core.sql.IdentifierProcessing; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.spel.spi.EvaluationContextExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Unit tests for {@link BasicRelationalPersistentEntity}. + * + * @author Oliver Gierke + * @author Kazuki Shimizu + * @author Bastian Wilhelm + * @author Mark Paluch + * @author Mikhail Polivakha + * @author Kurt Niemi + */ +@SpringJUnitConfig(classes = MyConfig.class) +class BasicRelationalPersistentEntityUnitTests { + + @Autowired ApplicationContext applicationContext; + private RelationalMappingContext mappingContext = new RelationalMappingContext(); + + @Test // DATAJDBC-106 + void discoversAnnotatedTableName() { + + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class); + + assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); + assertThat(entity.getQualifiedTableName()).isEqualTo(quoted("dummy_sub_entity")); + assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); + } + + @Test // DATAJDBC-294 + void considerIdColumnName() { + + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class); + + assertThat(entity.getIdColumn()).isEqualTo(quoted("renamedId")); + } + + @Test // DATAJDBC-296 + void emptyTableAnnotationFallsBackToNamingStrategy() { + + RelationalPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(DummyEntityWithEmptyAnnotation.class); + + assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + assertThat(entity.getQualifiedTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); + } + + @Test // DATAJDBC-491 + void namingStrategyWithSchemaReturnsCompositeTableName() { + + mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); + RelationalPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(DummyEntityWithEmptyAnnotation.class); + + SqlIdentifier simpleExpected = quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION"); + SqlIdentifier fullExpected = SqlIdentifier.from(quoted("MY_SCHEMA"), simpleExpected); + + assertThat(entity.getQualifiedTableName()).isEqualTo(fullExpected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); + + assertThat(entity.getQualifiedTableName().toSql(IdentifierProcessing.ANSI)) + .isEqualTo("\"MY_SCHEMA\".\"DUMMY_ENTITY_WITH_EMPTY_ANNOTATION\""); + } + + @Test // GH-1099 + void testRelationalPersistentEntitySchemaNameChoice() { + + mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class); + + SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE"); + SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); + } + + @Test // GH-1325 + void testRelationalPersistentEntitySpelExpression() { + + mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); + RelationalPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(EntityWithSchemaAndTableSpelExpression.class); + + SqlIdentifier simpleExpected = quoted("USE_THE_FORCE"); + SqlIdentifier expected = SqlIdentifier.from(quoted("HELP_ME_OBI_WON"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); + } + + @Test // GH-1325 + void testRelationalPersistentEntitySpelExpression_Sanitized() { + + mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(LittleBobbyTables.class); + + SqlIdentifier simpleExpected = quoted("RobertDROPTABLEstudents"); + SqlIdentifier expected = SqlIdentifier.from(quoted("RandomSQLToExecute"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); + } + + @Test // GH-1325 + void testRelationalPersistentEntitySpelExpression_NonSpelExpression() { + + mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class); + + SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE"); + SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); + } + + @Test // GH-1099 + void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() { + + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchema.class); + + SqlIdentifier simpleExpected = quoted("ENTITY_WITH_SCHEMA"); + SqlIdentifier expected = SqlIdentifier.from(quoted("ANAKYN_SKYWALKER"), simpleExpected); + assertThat(entity.getQualifiedTableName()).isEqualTo(expected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); + } + + @Test // GH-1325 + void considersSpelExtensions() { + + mappingContext.setApplicationContext(applicationContext); + RelationalPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(WithConfiguredSqlIdentifiers.class); + + assertThat(entity.getTableName()).isEqualTo(SqlIdentifier.quoted("my_table")); + assertThat(entity.getIdColumn()).isEqualTo(SqlIdentifier.quoted("my_column")); + } + + @Table(schema = "ANAKYN_SKYWALKER") + private static class EntityWithSchema { + @Id private Long id; + } + + @Table(schema = "DART_VADER", name = "I_AM_THE_SENATE") + private static class EntityWithSchemaAndName { + @Id private Long id; + } + + @Table(schema = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}", + name = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredTableName}") + private static class EntityWithSchemaAndTableSpelExpression { + @Id private Long id; + public static String desiredTableName = "USE_THE_FORCE"; + public static String desiredSchemaName = "HELP_ME_OBI_WON"; + } + + @Table(schema = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}", + name = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}") + private static class LittleBobbyTables { + @Id private Long id; + public static String desiredTableName = "Robert'); DROP TABLE students;--"; + public static String desiredSchemaName = "Random SQL To Execute;"; + } + + @Table("dummy_sub_entity") + static class DummySubEntity { + @Id + @Column("renamedId") Long id; + } + + @Table() + static class DummyEntityWithEmptyAnnotation { + @Id + @Column() Long id; + } + + enum NamingStrategyWithSchema implements NamingStrategy { + INSTANCE; + + @Override + public String getSchema() { + return "my_schema"; + } + } + + @Table("#{myExtension.getTableName()}") + static class WithConfiguredSqlIdentifiers { + @Id + @Column("#{myExtension.getColumnName()}") Long id; + } + + @Configuration + public static class MyConfig { + + @Bean + public MyExtension extension() { + return new MyExtension(); + } + + } + + public static class MyExtension implements EvaluationContextExtension { + + @Override + public String getExtensionId() { + return "my"; + } + + public String getTableName() { + return "my_table"; + } + + public String getColumnName() { + return "my_column"; + } + + @Override + public Map getProperties() { + return Map.of("myExtension", this); + } + + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java index ce3a77ea2d..2353db4370 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentPropertyUnitTests.java @@ -40,6 +40,7 @@ * @author Oliver Gierke * @author Florian Lüdiger * @author Bastian Wilhelm + * @author Kurt Niemi */ public class BasicRelationalPersistentPropertyUnitTests { @@ -68,6 +69,19 @@ public void detectsAnnotatedColumnAndKeyName() { assertThat(listProperty.getKeyColumn()).isEqualTo(quoted("dummy_key_column_name")); } + @Test // GH-1325 + void testRelationalPersistentEntitySpelExpressions() { + + assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()).isEqualTo(quoted("THE_FORCE_IS_WITH_YOU")); + assertThat(entity.getRequiredPersistentProperty("littleBobbyTables").getColumnName()) + .isEqualTo(quoted("DROPALLTABLES")); + + // Test that sanitizer does affect non-spel expressions + assertThat(entity.getRequiredPersistentProperty( + "poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot").getColumnName()) + .isEqualTo(quoted("--; DROP ALL TABLES;--")); + } + @Test // DATAJDBC-111 public void detectsEmbeddedEntity() { @@ -149,6 +163,22 @@ private static class DummyEntity { // DATACMNS-106 private @Column("dummy_name") String name; + public static String spelExpression1Value = "THE_FORCE_IS_WITH_YOU"; + + public static String littleBobbyTablesValue = "--; DROP ALL TABLES;--"; + @Column(value="#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentPropertyUnitTests$DummyEntity" + + ").spelExpression1Value}") + private String spelExpression1; + + @Column(value="#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentPropertyUnitTests$DummyEntity" + + ").littleBobbyTablesValue}") + private String littleBobbyTables; + + @Column(value="--; DROP ALL TABLES;--") + private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot; + // DATAJDBC-111 private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategyUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategyUnitTests.java index a931b71364..0d8fe4f782 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategyUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategyUnitTests.java @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntityImplUnitTests.DummySubEntity; +import org.springframework.data.relational.core.mapping.BasicRelationalPersistentEntityUnitTests.DummySubEntity; /** * Unit tests for the {@link NamingStrategy}. diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java deleted file mode 100644 index 9de2750579..0000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2018-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.mapping; - -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.relational.core.sql.SqlIdentifier.*; - -import org.junit.jupiter.api.Test; -import org.springframework.data.annotation.Id; -import org.springframework.data.relational.core.sql.IdentifierProcessing; -import org.springframework.data.relational.core.sql.SqlIdentifier; - -/** - * Unit tests for {@link RelationalPersistentEntityImpl}. - * - * @author Oliver Gierke - * @author Kazuki Shimizu - * @author Bastian Wilhelm - * @author Mark Paluch - * @author Mikhail Polivakha - */ -class RelationalPersistentEntityImplUnitTests { - - private RelationalMappingContext mappingContext = new RelationalMappingContext(); - - @Test // DATAJDBC-106 - void discoversAnnotatedTableName() { - - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class); - - assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); - assertThat(entity.getQualifiedTableName()).isEqualTo(quoted("dummy_sub_entity")); - assertThat(entity.getTableName()).isEqualTo(quoted("dummy_sub_entity")); - } - - @Test // DATAJDBC-294 - void considerIdColumnName() { - - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(DummySubEntity.class); - - assertThat(entity.getIdColumn()).isEqualTo(quoted("renamedId")); - } - - @Test // DATAJDBC-296 - void emptyTableAnnotationFallsBackToNamingStrategy() { - - RelationalPersistentEntity entity = mappingContext - .getRequiredPersistentEntity(DummyEntityWithEmptyAnnotation.class); - - assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); - assertThat(entity.getQualifiedTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); - assertThat(entity.getTableName()).isEqualTo(quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION")); - } - - @Test // DATAJDBC-491 - void namingStrategyWithSchemaReturnsCompositeTableName() { - - mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity entity = mappingContext - .getRequiredPersistentEntity(DummyEntityWithEmptyAnnotation.class); - - SqlIdentifier simpleExpected = quoted("DUMMY_ENTITY_WITH_EMPTY_ANNOTATION"); - SqlIdentifier fullExpected = SqlIdentifier.from(quoted("MY_SCHEMA"), simpleExpected); - - assertThat(entity.getQualifiedTableName()) - .isEqualTo(fullExpected); - assertThat(entity.getTableName()) - .isEqualTo(simpleExpected); - - assertThat(entity.getQualifiedTableName().toSql(IdentifierProcessing.ANSI)) - .isEqualTo("\"MY_SCHEMA\".\"DUMMY_ENTITY_WITH_EMPTY_ANNOTATION\""); - } - - @Test // GH-1099 - void testRelationalPersistentEntitySchemaNameChoice() { - - mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndName.class); - - SqlIdentifier simpleExpected = quoted("I_AM_THE_SENATE"); - SqlIdentifier expected = SqlIdentifier.from(quoted("DART_VADER"), simpleExpected); - assertThat(entity.getQualifiedTableName()).isEqualTo(expected); - assertThat(entity.getTableName()).isEqualTo(simpleExpected); - } - - @Test // GH-1099 - void specifiedSchemaGetsCombinedWithNameFromNamingStrategy() { - - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchema.class); - - SqlIdentifier simpleExpected = quoted("ENTITY_WITH_SCHEMA"); - SqlIdentifier expected = SqlIdentifier.from(quoted("ANAKYN_SKYWALKER"), simpleExpected); - assertThat(entity.getQualifiedTableName()).isEqualTo(expected); - assertThat(entity.getTableName()).isEqualTo(simpleExpected); - } - - @Table(schema = "ANAKYN_SKYWALKER") - private static class EntityWithSchema { - @Id private Long id; - } - - @Table(schema = "DART_VADER", name = "I_AM_THE_SENATE") - private static class EntityWithSchemaAndName { - @Id private Long id; - } - - @Table("dummy_sub_entity") - static class DummySubEntity { - @Id @Column("renamedId") Long id; - } - - @Table() - static class DummyEntityWithEmptyAnnotation { - @Id @Column() Long id; - } - - enum NamingStrategyWithSchema implements NamingStrategy { - INSTANCE; - - @Override - public String getSchema() { - return "my_schema"; - } - } -}