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 0a13fd61..926fe527 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,8 +81,7 @@ public class JdbcMappingContext extends RelationalMappingContext { RelationalPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { BasicJdbcPersistentProperty persistentProperty = new BasicJdbcPersistentProperty(property, owner, simpleTypeHolder, this.getNamingStrategy()); - persistentProperty.setForceQuote(isForceQuote()); - persistentProperty.setSpelExpressionProcessor(getSpelExpressionProcessor()); + 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 00000000..83f84610 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java @@ -0,0 +1,159 @@ +/* + * 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 Lazy tableName; + private final @Nullable Expression tableNameExpression; + + private final Lazy> schemaName; + 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.expressionEvaluator = expressionEvaluator; + + Lazy> defaultSchema = Lazy.of(() -> StringUtils.hasText(namingStrategy.getSchema()) + ? Optional.of(createDerivedSqlIdentifier(namingStrategy.getSchema())) + : Optional.empty()); + + if (isAnnotationPresent(Table.class)) { + + Table table = getRequiredAnnotation(Table.class); + + this.tableName = Lazy.of(() -> StringUtils.hasText(table.value()) ? createSqlIdentifier(table.value()) + : createDerivedSqlIdentifier(namingStrategy.getTableName(getType()))); + this.tableNameExpression = detectExpression(table.value()); + + this.schemaName = StringUtils.hasText(table.schema()) + ? Lazy.of(() -> Optional.of(createSqlIdentifier(table.schema()))) + : defaultSchema; + + } else { + + this.tableName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getTableName(getType()))); + this.tableNameExpression = null; + this.schemaName = defaultSchema; + } + } + + /** + * 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; + if (schemaNameExpression != null) { + schema = createSqlIdentifier(expressionEvaluator.evaluate(schemaNameExpression)); + } else { + schema = schemaName.get().orElse(null); + } + + if (schema == null) { + return getTableName(); + } + + 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 a5f30b01..482775e9 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,13 +25,19 @@ import org.springframework.data.mapping.model.Property; 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 @@ -42,14 +48,17 @@ import org.springframework.util.StringUtils; 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 boolean isEmbedded; + private final String embeddedPrefix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; - private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor(); + private ExpressionEvaluator spelExpressionProcessor = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); /** * Creates a new {@link BasicRelationalPersistentProperty}. @@ -84,19 +93,26 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent Assert.notNull(namingStrategy, "NamingStrategy must not be null"); - this.isEmbedded = Lazy.of(() -> Optional.ofNullable(findAnnotation(Embedded.class)).isPresent()); + this.isEmbedded = isAnnotationPresent(Embedded.class); - this.embeddedPrefix = Lazy.of(() -> Optional.ofNullable(findAnnotation(Embedded.class)) // + this.embeddedPrefix = Optional.ofNullable(findAnnotation(Embedded.class)) // .map(Embedded::prefix) // - .orElse("")); + .orElse(""); - this.columnName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Column.class)) // - .map(Column::value) // - .map(spelExpressionProcessor::applySpelExpression) // - .filter(StringUtils::hasText) // - .map(this::createSqlIdentifier) // - .orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)))); + if (isAnnotationPresent(Column.class)) { + Column column = getRequiredAnnotation(Column.class); + + columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value()) + : 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), // @@ -112,12 +128,27 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent .map(this::createSqlIdentifier) // .orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)))); } - public SpelExpressionProcessor getSpelExpressionProcessor() { - return spelExpressionProcessor; + + void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) { + this.spelExpressionProcessor = spelExpressionProcessor; } - public void setSpelExpressionProcessor(SpelExpressionProcessor 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) { @@ -148,7 +179,12 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent @Override public SqlIdentifier getColumnName() { - return columnName.get(); + + if (columnNameExpression == null) { + return columnName.get(); + } + + return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression)); } @Override @@ -193,12 +229,12 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent @Override public boolean isEmbedded() { - return isEmbedded.get(); + return isEmbedded; } @Override public String getEmbeddedPrefix() { - return isEmbedded() ? embeddedPrefix.get() : null; + return isEmbedded() ? embeddedPrefix : null; } @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 96b29e8b..e39f0dd6 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 @@ import java.lang.annotation.Target; 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 00000000..da4d300c --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ExpressionEvaluator.java @@ -0,0 +1,47 @@ +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 #setSanitizer(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 5e3be949..966761b7 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; @@ -36,7 +40,8 @@ public class RelationalMappingContext private final NamingStrategy namingStrategy; private boolean forceQuote = true; - private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor(); + + private final ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT); /** * Creates a new {@link RelationalMappingContext}. @@ -78,21 +83,25 @@ public class RelationalMappingContext this.forceQuote = forceQuote; } - public SpelExpressionProcessor getSpelExpressionProcessor() { - return spelExpressionProcessor; + public void setSqlIdentifierSanitizer(SqlIdentifierSanitizer sanitizer) { + this.expressionEvaluator.setSanitizer(sanitizer); } - public void setSpelExpressionProcessor(SpelExpressionProcessor spelExpressionProcessor) { - this.spelExpressionProcessor = spelExpressionProcessor; + 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()); - entity.setSpelExpressionProcessor(getSpelExpressionProcessor()); return entity; } @@ -103,14 +112,14 @@ public class RelationalMappingContext BasicRelationalPersistentProperty persistentProperty = new BasicRelationalPersistentProperty(property, owner, simpleTypeHolder, this.namingStrategy); - persistentProperty.setForceQuote(isForceQuote()); - persistentProperty.setSpelExpressionProcessor(getSpelExpressionProcessor()); + 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 29b62e06..00000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImpl.java +++ /dev/null @@ -1,139 +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 - * @author Kurt Niemi - */ -class RelationalPersistentEntityImpl extends BasicPersistentEntity - implements RelationalPersistentEntity { - - private final NamingStrategy namingStrategy; - private final Lazy> tableName; - private final Lazy> schemaName; - private boolean forceQuote = true; - private SpelExpressionProcessor spelExpressionProcessor = new SpelExpressionProcessor(); - - /** - * 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) // - .map(spelExpressionProcessor::applySpelExpression) // - .filter(StringUtils::hasText) // - .map(this::createSqlIdentifier)); - - this.schemaName = Lazy.of(() -> Optional.ofNullable(findAnnotation(Table.class)) // - .map(Table::schema) // - .filter(StringUtils::hasText) // - .map(this::createSqlIdentifier)); - } - - public SpelExpressionProcessor getSpelExpressionProcessor() { - return spelExpressionProcessor; - } - - public void setSpelExpressionProcessor(SpelExpressionProcessor spelExpressionProcessor) { - this.spelExpressionProcessor = spelExpressionProcessor; - } - - 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/SpelExpressionProcessor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionProcessor.java deleted file mode 100644 index 5cb3bb9c..00000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionProcessor.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.springframework.data.relational.core.mapping; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.expression.EvaluationException; -import org.springframework.expression.Expression; -import org.springframework.expression.common.TemplateParserContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -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 SpelExpressionResultSanitizer} interface and then invoking the - * {@link #setSpelExpressionResultSanitizer(SpelExpressionResultSanitizer)} method. - * - * @author Kurt Niemi - * @see SpelExpressionResultSanitizer - * @since 3.1 - */ -public class SpelExpressionProcessor { - private SpelExpressionResultSanitizer spelExpressionResultSanitizer; - private StandardEvaluationContext evalContext = new StandardEvaluationContext(); - private SpelExpressionParser parser = new SpelExpressionParser(); - private TemplateParserContext templateParserContext = new TemplateParserContext(); - - public String applySpelExpression(String expression) throws EvaluationException { - - Assert.notNull(expression, "Expression must not be null."); - - // Only apply logic if we have the prefixes/suffixes required for a SpEL expression as firstly - // there is nothing to evaluate (i.e. whatever literal passed in is returned as-is) and more - // importantly we do not want to perform any sanitization logic. - if (!isSpellExpression(expression)) { - return expression; - } - - Expression expr = parser.parseExpression(expression, templateParserContext); - String result = expr.getValue(evalContext, String.class); - - // Normally an exception is thrown by the Spel parser on invalid syntax/errors but this will provide - // a consistent experience for any issues with Spel parsing. - if (result == null) { - throw new EvaluationException("Spel Parsing of expression \"" + expression + "\" failed."); - } - - String sanitizedResult = getSpelExpressionResultSanitizer().sanitize(result); - - return sanitizedResult; - } - - protected boolean isSpellExpression(String expression) { - - String trimmedExpression = expression.trim(); - if (trimmedExpression.startsWith(templateParserContext.getExpressionPrefix()) && - trimmedExpression.endsWith(templateParserContext.getExpressionSuffix())) { - return true; - } - - return false; - } - public SpelExpressionResultSanitizer getSpelExpressionResultSanitizer() { - - if (this.spelExpressionResultSanitizer == null) { - this.spelExpressionResultSanitizer = new SpelExpressionResultSanitizer() { - @Override - public String sanitize(String result) { - - String cleansedResult = result.replaceAll("[^\\w]", ""); - return cleansedResult; - } - }; - } - return this.spelExpressionResultSanitizer; - } - - public void setSpelExpressionResultSanitizer(SpelExpressionResultSanitizer spelExpressionResultSanitizer) { - this.spelExpressionResultSanitizer = spelExpressionResultSanitizer; - } - -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionResultSanitizer.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionResultSanitizer.java deleted file mode 100644 index c2c8983c..00000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/SpelExpressionResultSanitizer.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.springframework.data.relational.core.mapping; - -/** - * Interface for sanitizing Spel Expression results - * - * @author Kurt Niemi - */ -public interface SpelExpressionResultSanitizer { - public String sanitize(String result); -} 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 00000000..68b11a09 --- /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 9f153afb..ed34a1ee 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.Retention; 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 @@ import java.lang.annotation.Target; 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/RelationalPersistentEntityImplUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java similarity index 72% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntityImplUnitTests.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java index 7998a647..c2dd6ee4 100644 --- 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/BasicRelationalPersistentEntityUnitTests.java @@ -18,13 +18,22 @@ 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 RelationalPersistentEntityImpl}. + * Unit tests for {@link BasicRelationalPersistentEntity}. * * @author Oliver Gierke * @author Kazuki Shimizu @@ -33,8 +42,10 @@ import org.springframework.data.relational.core.sql.SqlIdentifier; * @author Mikhail Polivakha * @author Kurt Niemi */ -class RelationalPersistentEntityImplUnitTests { +@SpringJUnitConfig(classes = MyConfig.class) +class BasicRelationalPersistentEntityUnitTests { + @Autowired ApplicationContext applicationContext; private RelationalMappingContext mappingContext = new RelationalMappingContext(); @Test // DATAJDBC-106 @@ -76,10 +87,8 @@ class RelationalPersistentEntityImplUnitTests { 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()).isEqualTo(fullExpected); + assertThat(entity.getTableName()).isEqualTo(simpleExpected); assertThat(entity.getQualifiedTableName().toSql(IdentifierProcessing.ANSI)) .isEqualTo("\"MY_SCHEMA\".\"DUMMY_ENTITY_WITH_EMPTY_ANNOTATION\""); @@ -101,13 +110,15 @@ class RelationalPersistentEntityImplUnitTests { void testRelationalPersistentEntitySpelExpression() { mappingContext = new RelationalMappingContext(NamingStrategyWithSchema.INSTANCE); - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(EntityWithSchemaAndTableSpelExpression.class); + 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() { @@ -143,6 +154,17 @@ class RelationalPersistentEntityImplUnitTests { 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; @@ -153,19 +175,15 @@ class RelationalPersistentEntityImplUnitTests { @Id private Long id; } - @Table(schema = "HELP_ME_OBI_WON", - name="#{T(org.springframework.data.relational.core.mapping." + - "RelationalPersistentEntityImplUnitTests$EntityWithSchemaAndTableSpelExpression" + - ").desiredTableName}") + @Table(schema = "HELP_ME_OBI_WON", 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"; } - @Table(schema = "LITTLE_BOBBY_TABLES", - name="#{T(org.springframework.data.relational.core.mapping." + - "RelationalPersistentEntityImplUnitTests$LittleBobbyTables" + - ").desiredTableName}") + @Table(schema = "LITTLE_BOBBY_TABLES", 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;--"; @@ -173,12 +191,14 @@ class RelationalPersistentEntityImplUnitTests { @Table("dummy_sub_entity") static class DummySubEntity { - @Id @Column("renamedId") Long id; + @Id + @Column("renamedId") Long id; } @Table() static class DummyEntityWithEmptyAnnotation { - @Id @Column() Long id; + @Id + @Column() Long id; } enum NamingStrategyWithSchema implements NamingStrategy { @@ -189,4 +209,42 @@ class RelationalPersistentEntityImplUnitTests { 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/DefaultNamingStrategyUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategyUnitTests.java index a931b713..0d8fe4f7 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 java.util.List; 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}.