diff --git a/src/main/java/org/dcsa/core/controller/ExtendedBaseController.java b/src/main/java/org/dcsa/core/controller/ExtendedBaseController.java index 7969bc3..c646e4b 100644 --- a/src/main/java/org/dcsa/core/controller/ExtendedBaseController.java +++ b/src/main/java/org/dcsa/core/controller/ExtendedBaseController.java @@ -24,9 +24,13 @@ public String getType() { return getService().getModelClass().getSimpleName(); } - @GetMapping() + protected ExtendedRequest newExtendedRequest() { + return new ExtendedRequest<>(extendedParameters, r2dbcDialect, getService().getModelClass()); + } + + @GetMapping public Flux findAll(ServerHttpResponse response, ServerHttpRequest request) { - ExtendedRequest extendedRequest = new ExtendedRequest<>(extendedParameters, r2dbcDialect, getService().getModelClass()); + ExtendedRequest extendedRequest = newExtendedRequest(); try { extendedRequest.parseParameter(request.getQueryParams()); } catch (GetException getException) { diff --git a/src/main/java/org/dcsa/core/extendedrequest/ExtendedRequest.java b/src/main/java/org/dcsa/core/extendedrequest/ExtendedRequest.java index a4aefc5..000535d 100644 --- a/src/main/java/org/dcsa/core/extendedrequest/ExtendedRequest.java +++ b/src/main/java/org/dcsa/core/extendedrequest/ExtendedRequest.java @@ -1,33 +1,27 @@ package org.dcsa.core.extendedrequest; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import org.dcsa.core.exception.GetException; import org.dcsa.core.query.DBEntityAnalysis; +import org.dcsa.core.query.impl.AbstractQueryFactory; +import org.dcsa.core.query.impl.PreparedQuery; import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.relational.core.dialect.RenderContextFactory; import org.springframework.data.relational.core.sql.*; -import org.springframework.data.relational.core.sql.render.RenderContext; -import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.r2dbc.core.PreparedOperation; -import org.springframework.r2dbc.core.binding.BindTarget; -import org.springframework.r2dbc.core.binding.Bindings; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.Key; @@ -51,7 +45,7 @@ * * object. */ @RequiredArgsConstructor -public class ExtendedRequest { +public class ExtendedRequest extends AbstractQueryFactory { public static final String PARAMETER_SPLIT = "&"; public static final String CURSOR_SPLIT = "="; @@ -124,6 +118,20 @@ public void parseParameter(Map> params, boolean fromCursor) finishedParsingParameters(); } + protected CursorBackedFilterCondition getFilterCondition() { + return filterCondition; + } + + @Override + protected Set getJoinAliasInUse() { + return joinAliasInUse; + } + + @Override + protected SelectBuilder.SelectFromAndJoin applyLimitOffset(SelectBuilder.SelectFromAndJoin t) { + return getPagination().applyLimitOffset(t); + } + // For sub-classes to hook into this protected void finishedParsingParameters() { filterCondition = getQueryParameterParser().build(); @@ -213,66 +221,37 @@ private static Map> convertToQueryStringToHashMap(String so return data; } - public DatabaseClient.GenericExecuteSpec getCount(DatabaseClient databaseClient) { - return databaseClient.sql(this.getCountQuery()); - } - - public DatabaseClient.GenericExecuteSpec getFindAll(DatabaseClient databaseClient) { - return databaseClient.sql(this.getQuery()); - } - - public Select getSelectQuery() { - List expressions = dbEntityAnalysis.getAllSelectableFields().stream().map(queryField -> { - markQueryFieldInUse(queryField); - return queryField.getSelectColumn(); - }).collect(Collectors.toList()); - Sort sort = getSort(); - - return generateBaseQuery(Select.builder().select(expressions)) - .orderBy(sort.getOrderByFields()).build(); - } - - public Select getSelectCountQuery() { - return generateBaseQuery(Select.builder().select( - Functions.count(Expressions.asterisk()).as("count") - )).build(); + protected SelectBuilder.BuildSelect applyOrder(SelectBuilder.SelectOrdered builder) { + return builder.orderBy(getSort().getOrderByFields()); } + @Override protected SelectBuilder.SelectOrdered generateBaseQuery(SelectBuilder.SelectAndFrom selectBuilder) { if (filterCondition == null) { finishedParsingParameters(); } - Pagination pagination = getPagination(); - if (selectDistinct) { - selectBuilder = selectBuilder.distinct(); - } - SelectBuilder.SelectWhere selectWhere = applyJoins(pagination.applyLimitOffset(selectBuilder.from( - dbEntityAnalysis.getTableAndJoins().getPrimaryTable() - ))); - Condition con = filterCondition.computeCondition(r2dbcDialect); - if (TrueCondition.INSTANCE.equals(con)) { - return selectWhere; - } - return selectWhere.where(con); + return super.generateBaseQuery(selectBuilder); } - public void setQueryCount(Integer count) { - getPagination().setTotal(count); + @Override + protected Condition generateCondition() { + return getFilterCondition().computeCondition(r2dbcDialect); } - protected SelectBuilder.SelectWhere applyJoins(SelectBuilder.SelectFromAndJoin selectBuilder) { - if (!joinAliasInUse.isEmpty()) { - return dbEntityAnalysis.getTableAndJoins().applyJoins(selectBuilder, joinAliasInUse); - } - return selectBuilder; + public void setQueryCount(Integer count) { + getPagination().setTotal(count); } - public PreparedOperation generateCountQuery() { if (filterCondition == null) { finishedParsingParameters(); } + return super.generateCountQuery(); + } + + protected PreparedQuery createPreparedOperation(Select select) { RenderContextFactory factory = new RenderContextFactory(r2dbcDialect); - return PreparedQuery.of(getSelectCountQuery(), factory.createRenderContext(), filterCondition.getBindings()); + return PreparedQuery.of(select, factory.createRenderContext(), filterCondition.getBindings()); } /** @@ -319,13 +298,20 @@ protected DBEntityAnalysis.DBEntityAnalysisBuilder prepareDBEntityAnalysis() return DBEntityAnalysis.builder(this.modelClass).loadFieldsAndJoinsFromModel(); } - public PreparedOperation generateSelectQuery() { + List expressions = dbEntityAnalysis.getAllSelectableFields().stream().map(queryField -> { + markQueryFieldInUse(queryField); + return queryField.getSelectColumn(); + }).collect(Collectors.toList()); + return this.generateSelectQuery(expressions); + } + + protected PreparedOperation { - - @Getter - private final Select source; - private final RenderContext renderContext; - private final Bindings bindings; - - @Override - public void bindTo(BindTarget target) { - bindings.apply(target); - } - - @Override - public String toQuery() { - SqlRenderer sqlRenderer = SqlRenderer.create(this.renderContext); - return sqlRenderer.render(source); - } - } } diff --git a/src/main/java/org/dcsa/core/extendedrequest/QueryField.java b/src/main/java/org/dcsa/core/extendedrequest/QueryField.java index 5b5f625..8fc90d3 100644 --- a/src/main/java/org/dcsa/core/extendedrequest/QueryField.java +++ b/src/main/java/org/dcsa/core/extendedrequest/QueryField.java @@ -1,6 +1,8 @@ package org.dcsa.core.extendedrequest; +import org.springframework.data.domain.Sort; import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.OrderByField; import java.lang.reflect.Field; @@ -19,7 +21,18 @@ default boolean isSelectable() { default String getDatePattern() { return null; } + default String getFieldPath() { + return null; + } default Field getCombinedModelField() { return null; } + + default OrderByField asOrderByField(Sort.Direction direction) { + Column column = getSelectColumn(); + if (column == null) { + column = getInternalQueryColumn(); + } + return OrderByField.from(column, direction); + } } diff --git a/src/main/java/org/dcsa/core/extendedrequest/QueryFields.java b/src/main/java/org/dcsa/core/extendedrequest/QueryFields.java index 98b2871..baf56b6 100644 --- a/src/main/java/org/dcsa/core/extendedrequest/QueryFields.java +++ b/src/main/java/org/dcsa/core/extendedrequest/QueryFields.java @@ -44,6 +44,7 @@ public static QueryField queryFieldFromFieldWithSelectPrefix(Class combinedMo selectColumn = internalColumn.as(SqlIdentifier.quoted(namePrefix + selectName)); } return FieldBackedQueryField.of( + namePrefix + combinedModelField.getName(), combinedModelField, internalColumn, selectColumn, @@ -84,6 +85,10 @@ public Column getSelectColumn() { @Data(staticConstructor = "of") private static class FieldBackedQueryField implements QueryField { + @NonNull + @Getter + private final String fieldPath; + @NonNull @Getter private final Field combinedModelField; diff --git a/src/main/java/org/dcsa/core/extendedrequest/QueryParameterParser.java b/src/main/java/org/dcsa/core/extendedrequest/QueryParameterParser.java index f2e6acf..4b98dfb 100644 --- a/src/main/java/org/dcsa/core/extendedrequest/QueryParameterParser.java +++ b/src/main/java/org/dcsa/core/extendedrequest/QueryParameterParser.java @@ -1,16 +1,16 @@ package org.dcsa.core.extendedrequest; import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; import lombok.RequiredArgsConstructor; import org.dcsa.core.exception.GetException; import org.dcsa.core.query.DBEntityAnalysis; +import org.dcsa.core.query.impl.ComparisonType; +import org.dcsa.core.query.impl.DelegatingCursorBackedFilterCondition; +import org.dcsa.core.query.impl.InlineableFilterCondition; import org.springframework.data.annotation.Transient; -import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.relational.core.sql.*; import org.springframework.r2dbc.core.binding.BindMarker; -import org.springframework.r2dbc.core.binding.Bindings; import org.springframework.r2dbc.core.binding.MutableBindings; import java.lang.reflect.Field; @@ -21,16 +21,14 @@ import java.time.format.DateTimeParseException; import java.time.temporal.Temporal; import java.util.*; -import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; + +import static org.dcsa.core.query.impl.QueryGeneratorUtils.andAllFilters; @RequiredArgsConstructor public class QueryParameterParser { - public static final FilterCondition EMPTY_CONDITION = InlineableFilterCondition.of(TrueCondition.INSTANCE); - protected final ExtendedParameters extendedParameters; protected final R2dbcDialect r2dbcDialect; protected final DBEntityAnalysis dbAnalysis; @@ -275,165 +273,5 @@ protected QueryField getQueryFieldFromJSONName(String jsonName) { return queryField; } - private static FilterCondition isubstr(Expression lhs, Expression rhsOrig) { - Expression rhs = surroundWithWildCards(rhsOrig); - return r2dbcDialect -> { - if (r2dbcDialect instanceof PostgresDialect) { - return Comparison.create(lhs, "ILIKE", rhs); - } - return Conditions.like(Functions.upper(lhs), Functions.upper(rhs)); - }; - } - - private static FilterCondition iequal(Expression lhs, Expression rhs) { - return InlineableFilterCondition.of(Conditions.isEqual(Functions.upper(lhs), Functions.upper(rhs))); - } - - private static Expression surroundWithWildCards(Expression rhs) { - List params = new ArrayList<>(3); - params.add(SQL.literalOf("%")); - params.add(rhs); - params.add(SQL.literalOf("%")); - return SimpleFunction.create("CONCAT", params); - } - @Getter - @RequiredArgsConstructor - protected enum ComparisonType { - GT(true, Conditions::isGreater), - GTE(true, Conditions::isGreaterOrEqualTo), - EQ(false, Conditions::isEqual), - LTE(true, Conditions::isLessOrEqualTo), - LT(true, Conditions::isLess), - NEQ(false, Conditions::isNotEqual), - SUBSTR(false, Conditions::like), - IEQ(false, true, QueryParameterParser::iequal), - ISUBSTR(false, true, QueryParameterParser::isubstr), - ; - - private final boolean requiredOrdering; - private final boolean convertToString; - private final BiFunction singleValueConverter; - - ComparisonType(boolean requiredOrdering, BiFunction conditionBiFunction) { - this(requiredOrdering, false, (lhs, rhs) -> InlineableFilterCondition.of(conditionBiFunction.apply(lhs, rhs))); - } - - public Expression defaultFieldConversion(QueryField queryField) { - boolean needsCast = convertToString; - if (needsCast) { - Class valueType = queryField.getType(); - if (valueType.isEnum() || String.class.equals(valueType)) { - needsCast = false; - } - } - if (needsCast) { - Column aliased = queryField.getInternalQueryColumn().as(SqlIdentifier.unquoted("VARCHAR")); - return SimpleFunction.create("CAST", Collections.singletonList(aliased)); - } - return queryField.getInternalQueryColumn(); - } - - public FilterCondition singleNonNullValueCondition(QueryField queryField, Expression expression) { - return singleNonNullValueCondition(defaultFieldConversion(queryField), expression); - } - - public FilterCondition singleNonNullValueCondition(Expression field, Expression expression) { - return singleValueConverter.apply(field, expression); - } - - public FilterCondition multiValueCondition(QueryField queryField, List expressions) { - return multiValueCondition(defaultFieldConversion(queryField), expressions); - } - - public FilterCondition multiValueCondition(Expression field, List expressions) { - if (expressions.isEmpty()) { - throw new IllegalArgumentException("Right-hand side expression list must be non-empty"); - } - if (expressions.size() == 1) { - return singleNonNullValueCondition(field, expressions.get(0)); - } else if (this == EQ || this == NEQ) { - if (this == EQ) { - return InlineableFilterCondition.of(Conditions.in(field, expressions)); - } - return InlineableFilterCondition.of(Conditions.notIn(field, expressions)); - } else { - return andAllFilters( - expressions.stream().map(e -> singleValueConverter.apply(field, e)).collect(Collectors.toList()), - true - ); - } - } - } - - private static FilterCondition andAllFilters(List filters, boolean nestOnMultiple) { - if (filters.isEmpty()) { - return EMPTY_CONDITION; - } - if (filters.size() == 1) { - return filters.get(0); - } - if (filters.stream().allMatch(FilterCondition::isInlineable)) { - return InlineableFilterCondition.of(andAll(filters.stream().map(FilterCondition::computeCondition), nestOnMultiple)); - } - return (r2dbcDialect) -> andAll(filters.stream().map(f -> f.computeCondition(r2dbcDialect)), nestOnMultiple); - } - - private static Condition andAll(Stream conditions, boolean nestOnMultiple) { - return andAll(conditions::iterator, nestOnMultiple); - } - - private static Condition andAll(Iterable conditions, boolean nestOnMultiple) { - Iterator iter = conditions.iterator(); - assert iter.hasNext(); - Condition con = iter.next(); - if (!iter.hasNext()) { - return con; - } - do { - con = con.and(iter.next()); - } while (iter.hasNext()); - if (!nestOnMultiple) { - return con; - } - return Conditions.nest(con); - } - - @RequiredArgsConstructor(staticName = "of") - private static class InlineableFilterCondition implements FilterCondition { - private final Condition condition; - - public Condition computeCondition(R2dbcDialect r2dbcDialect) { - return condition; - } - - public boolean isInlineable() { - return true; - } - } - - @RequiredArgsConstructor(staticName = "of") - private static class DelegatingCursorBackedFilterCondition implements CursorBackedFilterCondition { - - private final FilterCondition delegate; - - @Getter - private final Map> cursorParameters; - - @Getter - private final Set referencedQueryFields; - - @Getter - private final Bindings bindings; - - @Override - public Condition computeCondition(R2dbcDialect r2dbcDialect) { - return delegate.computeCondition(r2dbcDialect); - } - - @Override - public boolean isInlineable() { - return delegate.isInlineable(); - } - } } diff --git a/src/main/java/org/dcsa/core/extendedrequest/TableAndJoins.java b/src/main/java/org/dcsa/core/extendedrequest/TableAndJoins.java index 7e495ec..25830a9 100644 --- a/src/main/java/org/dcsa/core/extendedrequest/TableAndJoins.java +++ b/src/main/java/org/dcsa/core/extendedrequest/TableAndJoins.java @@ -7,6 +7,7 @@ import org.springframework.data.relational.core.sql.SelectBuilder; import org.springframework.data.relational.core.sql.Table; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Set; @@ -20,10 +21,12 @@ public class TableAndJoins { private final Table primaryTable; private final Set knownAliases = new HashSet<>(); private final LinkedHashMap joins = new LinkedHashMap<>(); + private final String primaryTableAlias; public TableAndJoins(Table primaryTable) { this.primaryTable = primaryTable; - knownAliases.add(ReflectUtility.getAliasId(primaryTable)); + primaryTableAlias = ReflectUtility.getAliasId(primaryTable); + knownAliases.add(primaryTableAlias); } public void addJoinDescriptor(JoinDescriptor joinDescriptor) { @@ -41,43 +44,15 @@ public JoinDescriptor getJoinDescriptor(String aliasId) { return joins.get(aliasId); } - private static void applyJoinToStringBuilder(StringBuilder sb, JoinDescriptor descriptor) { - Column lhs = descriptor.getLHSColumn(); - Column rhs = descriptor.getRHSColumn(); - Table table = descriptor.getRHSTable(); - String joinLine = table.toString() + " ON " + lhs.toString() + "=" + rhs.toString(); - switch (descriptor.getJoinType()) { - case JOIN: - sb.append(" JOIN "); - break; - case LEFT_OUTER_JOIN: - sb.append(" LEFT JOIN "); - break; - case RIGHT_OUTER_JOIN: - throw new UnsupportedOperationException("Rewrite to use LEFT JOIN instead"); - default: - throw new UnsupportedOperationException("Unsupported join type: " + descriptor.getJoinType()); - } - sb.append(joinLine); + public boolean hasJoins() { + return !joins.isEmpty(); } - protected void generateFromAndJoins(StringBuilder sb, Set selectedJoinAliases) { - int selectedJoins = 0; - sb.append(" FROM "); - sb.append(primaryTable.toString()); - for (JoinDescriptor joinDescriptor : joins.values()) { - if (selectedJoinAliases.contains(joinDescriptor.getJoinAliasId())) { - applyJoinToStringBuilder(sb, joinDescriptor); - selectedJoins++; - } - } - if (selectedJoinAliases.size() != selectedJoins) { - throw new IllegalArgumentException("selectedJoinAliases contained an unknown alias"); + public Set getAvailableJoinAliases(boolean includePrimary) { + if (includePrimary) { + return Collections.unmodifiableSet(knownAliases); } - } - - public boolean hasJoins() { - return !joins.isEmpty(); + return Collections.unmodifiableSet(joins.keySet()); } public SelectBuilder.SelectFromAndJoinCondition applyJoins(SelectBuilder.SelectFromAndJoin selectFromAndJoin, Set selectedJoinAliases) { @@ -92,6 +67,10 @@ public SelectBuilder.SelectFromAndJoinCondition applyJoins(SelectBuilder.SelectF selectedJoins++; } } + // "Forgive" if the primary table is included as well. + if (selectedJoinAliases.contains(primaryTableAlias)) { + selectedJoins++; + } if (selectedJoinAliases.size() != selectedJoins) { throw new IllegalArgumentException("selectedJoinAliases contained an unknown alias"); } diff --git a/src/main/java/org/dcsa/core/query/DBEntityAnalysis.java b/src/main/java/org/dcsa/core/query/DBEntityAnalysis.java index 969cdfe..c254305 100644 --- a/src/main/java/org/dcsa/core/query/DBEntityAnalysis.java +++ b/src/main/java/org/dcsa/core/query/DBEntityAnalysis.java @@ -18,6 +18,7 @@ public interface DBEntityAnalysis { QueryField getQueryFieldFromSelectName(String selectColumnName) throws IllegalArgumentException; List getAllSelectableFields(); TableAndJoins getTableAndJoins(); + Class getEntityType(); static DBEntityAnalysisBuilder builder(Class entityType) { return new DefaultDBEntityAnalysisBuilder<>(entityType); diff --git a/src/main/java/org/dcsa/core/query/QueryFactoryBuilder.java b/src/main/java/org/dcsa/core/query/QueryFactoryBuilder.java new file mode 100644 index 0000000..d85e6f7 --- /dev/null +++ b/src/main/java/org/dcsa/core/query/QueryFactoryBuilder.java @@ -0,0 +1,57 @@ +package org.dcsa.core.query; + +import org.apache.commons.lang3.concurrent.BackgroundInitializer; +import org.dcsa.core.query.impl.AbstractQueryFactory; +import org.dcsa.core.query.impl.QueryFactoryBuilderImpl; +import org.springframework.data.r2dbc.dialect.R2dbcDialect; +import org.springframework.data.relational.core.sql.SelectBuilder; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +public interface QueryFactoryBuilder { + + QueryFactoryBuilder distinct(); + QueryFactoryBuilder limitOffset(Function limitOffsetFunction); + QueryFactoryBuilder order(Function orderFunction); + QueryFactoryBuilder order(BiFunction, SelectBuilder.BuildSelect> orderFunction); + QueryFactoryBuilder r2dbcDialect(R2dbcDialect r2dbcDialect); + FrozenBuilder> freeze(); + + ConditionBuilder conditions(); + AbstractQueryFactory build(); + + static QueryFactoryBuilder builder(DBEntityAnalysis dbEntityAnalysis) { + return QueryFactoryBuilderImpl.of(dbEntityAnalysis, false, null, null, null, null); + } + + interface ConditionBuilder { + + ComparisonBuilder fieldByJsonName(String jsonName); + ComparisonBuilder fieldByJavaField(String javaField); + QueryFactoryBuilder withQuery(); + + FrozenBuilder> freeze(); + AbstractQueryFactory build(); + } + + interface FrozenBuilder { + B copyBuilder(); + } + + interface ComparisonBuilder { + ConditionBuilder isNull(); + ConditionBuilder isNotNull(); + ConditionBuilder greaterThan(Object value); + ConditionBuilder greaterThanOrEqualTo(Object value); + ConditionBuilder equalTo(Object value); + ConditionBuilder lessThanOrEqualTo(Object value); + ConditionBuilder lessThan(Object value); + default ConditionBuilder oneOf(Object ... values) { + return oneOf(Arrays.asList(values)); + } + ConditionBuilder oneOf(List values); + } +} diff --git a/src/main/java/org/dcsa/core/query/impl/AbstractQueryFactory.java b/src/main/java/org/dcsa/core/query/impl/AbstractQueryFactory.java new file mode 100644 index 0000000..2fedc04 --- /dev/null +++ b/src/main/java/org/dcsa/core/query/impl/AbstractQueryFactory.java @@ -0,0 +1,66 @@ +package org.dcsa.core.query.impl; + +import lombok.RequiredArgsConstructor; +import org.dcsa.core.extendedrequest.TableAndJoins; +import org.dcsa.core.query.DBEntityAnalysis; +import org.springframework.data.relational.core.sql.*; +import org.springframework.r2dbc.core.PreparedOperation; + +import java.util.List; +import java.util.Set; + +@RequiredArgsConstructor +public abstract class AbstractQueryFactory { + + public abstract DBEntityAnalysis getDbEntityAnalysis(); + + protected abstract Set getJoinAliasInUse(); + + protected abstract boolean isSelectDistinct(); + + protected abstract SelectBuilder.SelectFromAndJoin applyLimitOffset(SelectBuilder.SelectFromAndJoin t); + + protected SelectBuilder.SelectOrdered generateBaseQuery(SelectBuilder.SelectAndFrom selectBuilder) { + DBEntityAnalysis dbEntityAnalysis = getDbEntityAnalysis(); + TableAndJoins tableAndJoins = dbEntityAnalysis.getTableAndJoins(); + // Run this before the call to applyJoins(...) as it can affect which joins will be used. + Condition con = generateCondition(); + + if (isSelectDistinct()) { + selectBuilder = selectBuilder.distinct(); + } + SelectBuilder.SelectWhere selectWhere = applyJoins(tableAndJoins, applyLimitOffset(selectBuilder.from( + tableAndJoins.getPrimaryTable() + ))); + if (con == null || TrueCondition.INSTANCE.equals(con)) { + return selectWhere; + } + return selectWhere.where(con); + } + + protected abstract Condition generateCondition(); + + protected abstract SelectBuilder.BuildSelect applyOrder(SelectBuilder.SelectOrdered builder); + + public abstract PreparedOperation generateSelectQuery(List expressions) { + return createPreparedOperation(applyOrder(generateBaseQuery(Select.builder().select(expressions))).build()); + } + + public PreparedOperation createPreparedOperation(Select select); + + private SelectBuilder.SelectWhere applyJoins(TableAndJoins tableAndJoins, SelectBuilder.SelectFromAndJoin selectBuilder) { + Set joinAliasInUse = getJoinAliasInUse(); + if (!joinAliasInUse.isEmpty()) { + return tableAndJoins.applyJoins(selectBuilder, joinAliasInUse); + } + return selectBuilder; + } +} diff --git a/src/main/java/org/dcsa/core/query/impl/ComparisonType.java b/src/main/java/org/dcsa/core/query/impl/ComparisonType.java new file mode 100644 index 0000000..0a29467 --- /dev/null +++ b/src/main/java/org/dcsa/core/query/impl/ComparisonType.java @@ -0,0 +1,108 @@ +package org.dcsa.core.query.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.dcsa.core.extendedrequest.FilterCondition; +import org.dcsa.core.extendedrequest.QueryField; +import org.dcsa.core.extendedrequest.QueryParameterParser; +import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.relational.core.sql.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +import static org.dcsa.core.query.impl.QueryGeneratorUtils.andAllFilters; + +@Getter +@RequiredArgsConstructor +public enum ComparisonType { + GT(true, Conditions::isGreater), + GTE(true, Conditions::isGreaterOrEqualTo), + EQ(false, Conditions::isEqual), + LTE(true, Conditions::isLessOrEqualTo), + LT(true, Conditions::isLess), + NEQ(false, Conditions::isNotEqual), + SUBSTR(false, Conditions::like), + IEQ(false, true, ComparisonType::iequal), + ISUBSTR(false, true, ComparisonType::isubstr), + ; + + private final boolean requiredOrdering; + private final boolean convertToString; + private final BiFunction singleValueConverter; + + ComparisonType(boolean requiredOrdering, BiFunction conditionBiFunction) { + this(requiredOrdering, false, (lhs, rhs) -> InlineableFilterCondition.of(conditionBiFunction.apply(lhs, rhs))); + } + + public Expression defaultFieldConversion(QueryField queryField) { + boolean needsCast = convertToString; + if (needsCast) { + Class valueType = queryField.getType(); + if (valueType.isEnum() || String.class.equals(valueType)) { + needsCast = false; + } + } + if (needsCast) { + Column aliased = queryField.getInternalQueryColumn().as(SqlIdentifier.unquoted("VARCHAR")); + return SimpleFunction.create("CAST", Collections.singletonList(aliased)); + } + return queryField.getInternalQueryColumn(); + } + + public FilterCondition singleNonNullValueCondition(QueryField queryField, Expression expression) { + return singleNonNullValueCondition(defaultFieldConversion(queryField), expression); + } + + public FilterCondition singleNonNullValueCondition(Expression field, Expression expression) { + return singleValueConverter.apply(field, expression); + } + + public FilterCondition multiValueCondition(QueryField queryField, List expressions) { + return multiValueCondition(defaultFieldConversion(queryField), expressions); + } + + public FilterCondition multiValueCondition(Expression field, List expressions) { + if (expressions.isEmpty()) { + throw new IllegalArgumentException("Right-hand side expression list must be non-empty"); + } + if (expressions.size() == 1) { + return singleNonNullValueCondition(field, expressions.get(0)); + } else if (this == EQ || this == NEQ) { + if (this == EQ) { + return InlineableFilterCondition.of(Conditions.in(field, expressions)); + } + return InlineableFilterCondition.of(Conditions.notIn(field, expressions)); + } else { + return andAllFilters( + expressions.stream().map(e -> singleValueConverter.apply(field, e)).collect(Collectors.toList()), + true + ); + } + } + + private static FilterCondition isubstr(Expression lhs, Expression rhsOrig) { + Expression rhs = surroundWithWildCards(rhsOrig); + return r2dbcDialect -> { + if (r2dbcDialect instanceof PostgresDialect) { + return Comparison.create(lhs, "ILIKE", rhs); + } + return Conditions.like(Functions.upper(lhs), Functions.upper(rhs)); + }; + } + + private static FilterCondition iequal(Expression lhs, Expression rhs) { + return InlineableFilterCondition.of(Conditions.isEqual(Functions.upper(lhs), Functions.upper(rhs))); + } + + private static Expression surroundWithWildCards(Expression rhs) { + List params = new ArrayList<>(3); + params.add(SQL.literalOf("%")); + params.add(rhs); + params.add(SQL.literalOf("%")); + return SimpleFunction.create("CONCAT", params); + } +} diff --git a/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysis.java b/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysis.java index dac3a59..03c4c6a 100644 --- a/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysis.java +++ b/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysis.java @@ -13,7 +13,10 @@ @RequiredArgsConstructor public class DefaultDBEntityAnalysis implements DBEntityAnalysis { + @Getter + private final Class entityType; private final Map jsonName2QueryField; + private final Map javaFieldName2QueryField; private final Map selectName2QueryField; private final Set declaredButNotSelectable; @Getter @@ -36,7 +39,7 @@ public QueryField getQueryFieldFromJSONName(String jsonName) throws IllegalArgum @Override public QueryField getQueryFieldFromJavaFieldName(String javaFieldName) throws IllegalArgumentException { - throw new UnsupportedOperationException("Not implemented yet"); + return getFieldFromTable(javaFieldName2QueryField, javaFieldName); } public QueryField getQueryFieldFromSelectName(String selectName) throws IllegalArgumentException { diff --git a/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysisBuilder.java b/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysisBuilder.java index 135748c..fdc6e58 100644 --- a/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysisBuilder.java +++ b/src/main/java/org/dcsa/core/query/impl/DefaultDBEntityAnalysisBuilder.java @@ -2,7 +2,10 @@ import lombok.RequiredArgsConstructor; import org.dcsa.core.extendedrequest.*; -import org.dcsa.core.model.*; +import org.dcsa.core.model.ForeignKey; +import org.dcsa.core.model.JoinedWithModel; +import org.dcsa.core.model.PrimaryModel; +import org.dcsa.core.model.ViaJoinAlias; import org.dcsa.core.query.DBEntityAnalysis; import org.dcsa.core.util.ReflectUtility; import org.springframework.data.annotation.Transient; @@ -20,6 +23,7 @@ public class DefaultDBEntityAnalysisBuilder implements DBEntityAnalysis.DBEnt private final Class entityType; private final Map jsonName2DbField = new HashMap<>(); + private final Map javaFieldName2DbField = new HashMap<>(); private final Map selectName2DbField = new HashMap<>(); private final List allSelectableFields = new ArrayList<>(); private final Map internalQueryName2DbField = new HashMap<>(); @@ -129,11 +133,11 @@ private void loadForeignKeysFromEntityClass() { initJoinAliasTable(); } - loadJoinsAndFieldsAndForeignKeysDeep(entityType, model -> model.isAssignableFrom(entityType), "", "", null); + loadJoinsAndFieldsAndForeignKeysDeep(entityType, model -> model.isAssignableFrom(entityType), "", "", null, new HashSet<>()); } private void loadJoinsAndFieldsAndForeignKeysDeep(Class modelType, Predicate> skipFieldRegistration, - String prefix, String joinAlias, Table table) { + String prefix, String joinAlias, Table table, Set seenFields) { if (joinAlias.equals("")) { joinAlias = ReflectUtility.getTableName(modelType); } @@ -146,6 +150,12 @@ private void loadJoinsAndFieldsAndForeignKeysDeep(Class modelType, Predicate< if (field.isAnnotationPresent(Transient.class)) { continue; } + String fieldName = field.getName(); + // Cope with fields being shadowed in subclasses - we take the "first" (subclass) definition we find. + if (seenFields.contains(fieldName)) { + continue; + } + seenFields.add(fieldName); if (field.isAnnotationPresent(ForeignKey.class)) { ForeignKey foreignKey = field.getAnnotation(ForeignKey.class); @@ -171,7 +181,7 @@ private void loadJoinsAndFieldsAndForeignKeysDeep(Class modelType, Predicate< // load fields recursively with new prefix String newPrefix = prefix + ReflectUtility.transformFromFieldNameToJsonName(intoField) + "."; - loadJoinsAndFieldsAndForeignKeysDeep(intoField.getType(), skipFieldRegistration, newPrefix, intoJoinAlias, null); + loadJoinsAndFieldsAndForeignKeysDeep(intoField.getType(), skipFieldRegistration, newPrefix, intoJoinAlias, null, new HashSet<>()); } if (skipFieldRegistration != null && skipFieldRegistration.test(modelType)) { @@ -184,7 +194,7 @@ private void loadJoinsAndFieldsAndForeignKeysDeep(Class modelType, Predicate< Class superClass = modelType.getSuperclass(); if (superClass != Object.class) { - loadJoinsAndFieldsAndForeignKeysDeep(superClass, skipFieldRegistration, prefix, joinAlias, table); + loadJoinsAndFieldsAndForeignKeysDeep(superClass, skipFieldRegistration, prefix, joinAlias, table, seenFields); } } @@ -375,6 +385,11 @@ private void registerField(Field field, Class modelType, boolean selectable) public DBEntityAnalysis.DBEntityAnalysisBuilder registerQueryField(QueryField queryField) { detectClash(jsonName2DbField, queryField.getJsonName(), queryField, "JSON key", "Probably one or both of them has a invalid @JsonProperty"); + String fieldPath = queryField.getFieldPath(); + if (fieldPath != null) { + detectClash(javaFieldName2DbField, fieldPath, queryField, "JavaFieldName", + "This should not be possible"); + } if (queryField.isSelectable()) { Column column = Objects.requireNonNull(queryField.getSelectColumn(), "selectColumn"); SqlIdentifier name = column.getName(); @@ -569,7 +584,9 @@ public DBEntityAnalysis build() { this.verifyFieldsAndJoins(); used = true; return new DefaultDBEntityAnalysis<>( + entityType, Collections.unmodifiableMap(jsonName2DbField), + Collections.unmodifiableMap(javaFieldName2DbField), Collections.unmodifiableMap(selectName2DbField), Collections.unmodifiableSet(declaredButNotSelectable), Collections.unmodifiableList(allSelectableFields), diff --git a/src/main/java/org/dcsa/core/query/impl/DelegatingCursorBackedFilterCondition.java b/src/main/java/org/dcsa/core/query/impl/DelegatingCursorBackedFilterCondition.java new file mode 100644 index 0000000..e754208 --- /dev/null +++ b/src/main/java/org/dcsa/core/query/impl/DelegatingCursorBackedFilterCondition.java @@ -0,0 +1,39 @@ +package org.dcsa.core.query.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.dcsa.core.extendedrequest.CursorBackedFilterCondition; +import org.dcsa.core.extendedrequest.FilterCondition; +import org.dcsa.core.extendedrequest.QueryField; +import org.springframework.data.r2dbc.dialect.R2dbcDialect; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.r2dbc.core.binding.Bindings; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RequiredArgsConstructor(staticName = "of") +public class DelegatingCursorBackedFilterCondition implements CursorBackedFilterCondition { + + private final FilterCondition delegate; + + @Getter + private final Map> cursorParameters; + + @Getter + private final Set referencedQueryFields; + + @Getter + private final Bindings bindings; + + @Override + public Condition computeCondition(R2dbcDialect r2dbcDialect) { + return delegate.computeCondition(r2dbcDialect); + } + + @Override + public boolean isInlineable() { + return delegate.isInlineable(); + } +} diff --git a/src/main/java/org/dcsa/core/query/impl/GenericQueryFactory.java b/src/main/java/org/dcsa/core/query/impl/GenericQueryFactory.java new file mode 100644 index 0000000..f6941b4 --- /dev/null +++ b/src/main/java/org/dcsa/core/query/impl/GenericQueryFactory.java @@ -0,0 +1,68 @@ +package org.dcsa.core.query.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.dcsa.core.query.DBEntityAnalysis; +import org.springframework.data.relational.core.dialect.RenderContextFactory; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.SelectBuilder; +import org.springframework.r2dbc.core.PreparedOperation; +import org.springframework.r2dbc.core.binding.Bindings; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +@RequiredArgsConstructor(staticName = "of") +class GenericQueryFactory extends AbstractQueryFactory { + + @Getter + private final DBEntityAnalysis dbEntityAnalysis; + + @Getter + private final boolean selectDistinct; + + private final Function limitOffsets; + private final Function order; + @Getter + private final Set joinAliasInUse; + private final Condition conditions; + private final List selectExpressions; + + private final RenderContextFactory renderContextFactory; + private final Bindings bindings; + + + @Override + public PreparedOperation createPreparedOperation(Select select) { + return PreparedQuery.of(select, renderContextFactory.createRenderContext(), bindings); + } + + @Override + protected Condition generateCondition() { + return conditions; + } + + @Override + protected SelectBuilder.SelectFromAndJoin applyLimitOffset(SelectBuilder.SelectFromAndJoin builder) { + if (limitOffsets != null) { + return limitOffsets.apply(builder); + } + return builder; + } + + @Override + protected SelectBuilder.BuildSelect applyOrder(SelectBuilder.SelectOrdered builder) { + if (order != null) { + return order.apply(builder); + } + return builder; + } +} diff --git a/src/main/java/org/dcsa/core/query/impl/InlineableFilterCondition.java b/src/main/java/org/dcsa/core/query/impl/InlineableFilterCondition.java new file mode 100644 index 0000000..0ed4a8a --- /dev/null +++ b/src/main/java/org/dcsa/core/query/impl/InlineableFilterCondition.java @@ -0,0 +1,19 @@ +package org.dcsa.core.query.impl; + +import lombok.RequiredArgsConstructor; +import org.dcsa.core.extendedrequest.FilterCondition; +import org.springframework.data.r2dbc.dialect.R2dbcDialect; +import org.springframework.data.relational.core.sql.Condition; + +@RequiredArgsConstructor(staticName = "of") +public class InlineableFilterCondition implements FilterCondition { + private final Condition condition; + + public Condition computeCondition(R2dbcDialect r2dbcDialect) { + return condition; + } + + public boolean isInlineable() { + return true; + } +} diff --git a/src/main/java/org/dcsa/core/query/impl/PreparedQuery.java b/src/main/java/org/dcsa/core/query/impl/PreparedQuery.java new file mode 100644 index 0000000..865f042 --- /dev/null +++ b/src/main/java/org/dcsa/core/query/impl/PreparedQuery.java @@ -0,0 +1,30 @@ +package org.dcsa.core.query.impl; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.render.RenderContext; +import org.springframework.data.relational.core.sql.render.SqlRenderer; +import org.springframework.r2dbc.core.PreparedOperation; +import org.springframework.r2dbc.core.binding.BindTarget; +import org.springframework.r2dbc.core.binding.Bindings; + +@RequiredArgsConstructor(staticName = "of") +public class PreparedQuery implements PreparedOperation