Skip to content

Commit

Permalink
Add expression support for @MappedCollection annotation.
Browse files Browse the repository at this point in the history
See #1325
Original pull request: #1461
  • Loading branch information
mp911de committed May 31, 2023
1 parent a73be5a commit b92586f
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
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;
Expand All @@ -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}.
Expand Down Expand Up @@ -99,38 +100,58 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity<?,
.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)); //

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))));
if (collectionIdColumnName == null) {
collectionIdColumnName = Lazy.of(Optional.empty());
}

this.collectionIdColumnName = collectionIdColumnName;
this.collectionKeyColumnName = collectionKeyColumnName;
}

void setSpelExpressionProcessor(ExpressionEvaluator spelExpressionProcessor) {
this.spelExpressionProcessor = spelExpressionProcessor;
void setExpressionEvaluator(ExpressionEvaluator expressionEvaluator) {
this.expressionEvaluator = expressionEvaluator;
}

/**
Expand Down Expand Up @@ -184,7 +205,7 @@ public SqlIdentifier getColumnName() {
return columnName.get();
}

return createSqlIdentifier(spelExpressionProcessor.evaluate(columnNameExpression));
return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression));
}

@Override
Expand All @@ -195,13 +216,27 @@ public RelationalPersistentEntity<?> getOwner() {
@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*
* @author Kurt Niemi
* @see SqlIdentifierSanitizer
* @since 3.1
* @since 3.2
*/
class ExpressionEvaluator {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@
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)
*/
String idColumn() default "";

/**
* 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)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ public void setForceQuote(boolean forceQuote) {
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);
}
Expand Down Expand Up @@ -119,7 +126,7 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert

protected void applyDefaults(BasicRelationalPersistentProperty persistentProperty) {
persistentProperty.setForceQuote(isForceQuote());
persistentProperty.setSpelExpressionProcessor(this.expressionEvaluator);
persistentProperty.setExpressionEvaluator(this.expressionEvaluator);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* @author Kurt Niemi
* @author Mark Paluch
* @since 3.1
* @since 3.2
* @see RelationalMappingContext#setSqlIdentifierSanitizer(SqlIdentifierSanitizer)
*/
@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,24 @@ public void detectsAnnotatedColumnAndKeyName() {
@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
Expand Down Expand Up @@ -166,18 +176,16 @@ private static class DummyEntity {
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;
Expand All @@ -199,6 +207,11 @@ public List<Date> getListGetter() {
}
}

static class WithMappedCollection {

@MappedCollection(idColumn = "#{'id_col'}", keyColumn = "#{'key_col'}") private List<Integer> someList;
}

@SuppressWarnings("unused")
private enum SomeEnum {
ALPHA
Expand Down

0 comments on commit b92586f

Please sign in to comment.