Add expression support for @MappedCollection annotation.
See #1325 Original pull request: #1461
This commit is contained in:
@@ -27,7 +27,6 @@ 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;
|
||||
@@ -53,12 +52,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
|
||||
private final Lazy<SqlIdentifier> columnName;
|
||||
private final @Nullable Expression columnNameExpression;
|
||||
private final Lazy<Optional<SqlIdentifier>> collectionIdColumnName;
|
||||
private final @Nullable Expression collectionIdColumnNameExpression;
|
||||
private final Lazy<SqlIdentifier> collectionKeyColumnName;
|
||||
private final @Nullable Expression collectionKeyColumnNameExpression;
|
||||
private final boolean isEmbedded;
|
||||
private final String embeddedPrefix;
|
||||
private final NamingStrategy namingStrategy;
|
||||
private boolean forceQuote = true;
|
||||
private ExpressionEvaluator spelExpressionProcessor = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT);
|
||||
private ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator(EvaluationContextProvider.DEFAULT);
|
||||
|
||||
/**
|
||||
* Creates a new {@link BasicRelationalPersistentProperty}.
|
||||
@@ -99,38 +100,58 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
|
||||
.map(Embedded::prefix) //
|
||||
.orElse("");
|
||||
|
||||
Lazy<Optional<SqlIdentifier>> collectionIdColumnName = null;
|
||||
Lazy<SqlIdentifier> collectionKeyColumnName = Lazy
|
||||
.of(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)));
|
||||
|
||||
if (isAnnotationPresent(MappedCollection.class)) {
|
||||
|
||||
MappedCollection mappedCollection = getRequiredAnnotation(MappedCollection.class);
|
||||
|
||||
if (StringUtils.hasText(mappedCollection.idColumn())) {
|
||||
collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(mappedCollection.idColumn())));
|
||||
}
|
||||
|
||||
this.collectionIdColumnNameExpression = detectExpression(mappedCollection.idColumn());
|
||||
|
||||
collectionKeyColumnName = Lazy.of(
|
||||
() -> StringUtils.hasText(mappedCollection.keyColumn()) ? createSqlIdentifier(mappedCollection.keyColumn())
|
||||
: createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this)));
|
||||
|
||||
this.collectionKeyColumnNameExpression = detectExpression(mappedCollection.keyColumn());
|
||||
} else {
|
||||
|
||||
this.collectionIdColumnNameExpression = null;
|
||||
this.collectionKeyColumnNameExpression = null;
|
||||
}
|
||||
|
||||
if (isAnnotationPresent(Column.class)) {
|
||||
|
||||
Column column = getRequiredAnnotation(Column.class);
|
||||
|
||||
columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value())
|
||||
this.columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value())
|
||||
: createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
|
||||
columnNameExpression = detectExpression(column.value());
|
||||
this.columnNameExpression = detectExpression(column.value());
|
||||
|
||||
if (collectionIdColumnName == null && StringUtils.hasText(column.value())) {
|
||||
collectionIdColumnName = Lazy.of(() -> Optional.of(createSqlIdentifier(column.value())));
|
||||
}
|
||||
|
||||
} else {
|
||||
columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
|
||||
columnNameExpression = null;
|
||||
this.columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
|
||||
this.columnNameExpression = null;
|
||||
}
|
||||
|
||||
// TODO: support expressions for MappedCollection
|
||||
this.collectionIdColumnName = Lazy.of(() -> Optionals
|
||||
.toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)) //
|
||||
.map(MappedCollection::idColumn), //
|
||||
Optional.ofNullable(findAnnotation(Column.class)) //
|
||||
.map(Column::value)) //
|
||||
.filter(StringUtils::hasText) //
|
||||
.findFirst() //
|
||||
.map(this::createSqlIdentifier)); //
|
||||
if (collectionIdColumnName == null) {
|
||||
collectionIdColumnName = Lazy.of(Optional.empty());
|
||||
}
|
||||
|
||||
this.collectionKeyColumnName = Lazy.of(() -> Optionals //
|
||||
.toStream(Optional.ofNullable(findAnnotation(MappedCollection.class)).map(MappedCollection::keyColumn)) //
|
||||
.filter(StringUtils::hasText).findFirst() //
|
||||
.map(this::createSqlIdentifier) //
|
||||
.orElseGet(() -> createDerivedSqlIdentifier(namingStrategy.getKeyColumn(this))));
|
||||
this.collectionIdColumnName = collectionIdColumnName;
|
||||
this.collectionKeyColumnName = collectionKeyColumnName;
|
||||
}
|
||||
|
||||
void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) {
|
||||
this.spelExpressionProcessor = spelExpressionProcessor;
|
||||
void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator) {
|
||||
this.expressionEvaluator = expressionEvaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +205,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
|
||||
return columnName.get();
|
||||
}
|
||||
|
||||
return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression));
|
||||
return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -195,13 +216,27 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
|
||||
@Override
|
||||
public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) {
|
||||
|
||||
return collectionIdColumnName.get()
|
||||
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
|
||||
if (collectionIdColumnNameExpression == null) {
|
||||
|
||||
return collectionIdColumnName.get()
|
||||
.orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
|
||||
}
|
||||
|
||||
return createSqlIdentifier(expressionEvaluator.evaluate(collectionIdColumnNameExpression));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlIdentifier getKeyColumn() {
|
||||
return isQualified() ? collectionKeyColumnName.get() : null;
|
||||
|
||||
if (!isQualified()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (collectionKeyColumnNameExpression == null) {
|
||||
return collectionKeyColumnName.get();
|
||||
}
|
||||
|
||||
return createSqlIdentifier(expressionEvaluator.evaluate(collectionKeyColumnNameExpression));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Kurt Niemi
|
||||
* @see SqlIdentifierSanitizer
|
||||
* @since 3.1
|
||||
* @since 3.2
|
||||
*/
|
||||
class ExpressionEvaluator {
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ import java.util.Set;
|
||||
public @interface MappedCollection {
|
||||
|
||||
/**
|
||||
* The column name for id column in the corresponding relationship table. Defaults to {@link NamingStrategy} usage if
|
||||
* the value is empty.
|
||||
* The column name for id column in the corresponding relationship table. The attribute supports SpEL expressions to
|
||||
* dynamically calculate the column name on a per-operation basis. Defaults to {@link NamingStrategy} usage if the
|
||||
* value is empty.
|
||||
*
|
||||
* @see NamingStrategy#getReverseColumnName(RelationalPersistentProperty)
|
||||
*/
|
||||
@@ -46,7 +47,8 @@ public @interface MappedCollection {
|
||||
|
||||
/**
|
||||
* The column name for key columns of {@link List} or {@link Map} collections in the corresponding relationship table.
|
||||
* Defaults to {@link NamingStrategy} usage if the value is empty.
|
||||
* The attribute supports SpEL expressions to dynamically calculate the column name on a per-operation basis. Defaults
|
||||
* to {@link NamingStrategy} usage if the value is empty.
|
||||
*
|
||||
* @see NamingStrategy#getKeyColumn(RelationalPersistentProperty)
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,13 @@ public class RelationalMappingContext
|
||||
this.forceQuote = forceQuote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link SqlIdentifierSanitizer} to sanitize
|
||||
* {@link org.springframework.data.relational.core.sql.SqlIdentifier identifiers} created from SpEL expressions.
|
||||
*
|
||||
* @param sanitizer must not be {@literal null}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public void setSqlIdentifierSanitizer(SqlIdentifierSanitizer sanitizer) {
|
||||
this.expressionEvaluator.setSanitizer(sanitizer);
|
||||
}
|
||||
@@ -119,7 +126,7 @@ public class RelationalMappingContext
|
||||
|
||||
protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) {
|
||||
persistentProperty.setForceQuote(isForceQuote());
|
||||
persistentProperty.setSpelExpressionProcessor(this.expressionEvaluator);
|
||||
persistentProperty.setExpressionEvaluator(this.expressionEvaluator);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Kurt Niemi
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
* @since 3.2
|
||||
* @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -72,14 +72,24 @@ public class BasicRelationalPersistentPropertyUnitTests {
|
||||
@Test // GH-1325
|
||||
void testRelationalPersistentEntitySpelExpressions() {
|
||||
|
||||
assertThat(entity.getRequiredPersistentProperty("spelExpression1").getColumnName()).isEqualTo(quoted("THE_FORCE_IS_WITH_YOU"));
|
||||
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;--"));
|
||||
assertThat(entity.getRequiredPersistentProperty("poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot")
|
||||
.getColumnName()).isEqualTo(quoted("--; DROP ALL TABLES;--"));
|
||||
}
|
||||
|
||||
@Test // GH-1325
|
||||
void shouldEvaluateMappedCollectionExpressions() {
|
||||
|
||||
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithMappedCollection.class);
|
||||
RelationalPersistentProperty property = entity.getRequiredPersistentProperty("someList");
|
||||
|
||||
assertThat(property.getKeyColumn()).isEqualTo(quoted("key_col"));
|
||||
assertThat(property.getReverseColumnName(null)).isEqualTo(quoted("id_col"));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-111
|
||||
@@ -166,18 +176,16 @@ public class BasicRelationalPersistentPropertyUnitTests {
|
||||
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"
|
||||
+ ").spelExpression1Value}") private String spelExpression1;
|
||||
|
||||
@Column(value="#{T(org.springframework.data.relational.core.mapping." +
|
||||
"BasicRelationalPersistentPropertyUnitTests$DummyEntity" +
|
||||
").littleBobbyTablesValue}")
|
||||
private String littleBobbyTables;
|
||||
@Column(value = "#{T(org.springframework.data.relational.core.mapping."
|
||||
+ "BasicRelationalPersistentPropertyUnitTests$DummyEntity"
|
||||
+ ").littleBobbyTablesValue}") private String littleBobbyTables;
|
||||
|
||||
@Column(value="--; DROP ALL TABLES;--")
|
||||
private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
|
||||
@Column(
|
||||
value = "--; DROP ALL TABLES;--") private String poorDeveloperProgrammaticallyAskingToShootThemselvesInTheFoot;
|
||||
|
||||
// DATAJDBC-111
|
||||
private @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddableEntity embeddableEntity;
|
||||
@@ -199,6 +207,11 @@ public class BasicRelationalPersistentPropertyUnitTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithMappedCollection {
|
||||
|
||||
@MappedCollection(idColumn = "#{'id_col'}", keyColumn = "#{'key_col'}") private List<Integer> someList;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private enum SomeEnum {
|
||||
ALPHA
|
||||
|
||||
Reference in New Issue
Block a user