From b191fc929ceb3c0b82315dea460f1a94e5dc3f85 Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Fri, 17 Jul 2020 06:59:05 +0300 Subject: [PATCH 1/6] Introduce DSRequest.alternateOutputs(), and initial but not properly working @manyToMany support. --- README.md | 4 + .../srg/smartclient/AbstractDSHandler.java | 10 +- .../srg/smartclient/DSDeclarationBuilder.java | 8 +- .../java/org/srg/smartclient/JDBCHandler.java | 103 ++++- .../org/srg/smartclient/JpaDSDispatcher.java | 1 + ...t.java => SmartClientRelationSupport.java} | 4 +- .../srg/smartclient/isomorphic/DSField.java | 13 + .../srg/smartclient/isomorphic/DSRequest.java | 19 + .../{ => jpa}/JPAAwareHandlerFactory.java | 361 +++++++----------- .../smartclient/jpa/JPARelationSupport.java | 179 +++++++++ .../org/srg/smartclient/jpa/JpaRelation.java | 23 ++ .../srg/smartclient/jpa/JpaRelationType.java | 24 ++ .../smartclient/AbstractJDBCHandlerTest.java | 12 + .../jpa/JPAAwareHandlerFactoryTest.java | 1 - .../smartclient/jpa/JpaDSDispatcherTest.java | 24 ++ 15 files changed, 536 insertions(+), 250 deletions(-) rename smartclient-core/src/main/java/org/srg/smartclient/{RelationSupport.java => SmartClientRelationSupport.java} (99%) rename smartclient-core/src/main/java/org/srg/smartclient/{ => jpa}/JPAAwareHandlerFactory.java (65%) create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/jpa/JPARelationSupport.java create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelation.java create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelationType.java diff --git a/README.md b/README.md index 18dde2f..ea881b1 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,7 @@ https://www.seguetech.com/transferring-dynamic-query-batch-job-smartclient/ https://www.seguetech.com/transferring-a-dynamic-query-to-a-batch-job-in-smartclient-part-2/ https://www.seguetech.com/handle-data-treegrid/ + + +https://isomorphic.atlassian.net/wiki/spaces/Main/pages/524566/Getting+Started + diff --git a/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java index bb3723e..8a37ed2 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java @@ -9,7 +9,7 @@ import java.util.*; -public abstract class AbstractDSHandler extends RelationSupport implements DSHandler { +public abstract class AbstractDSHandler extends SmartClientRelationSupport implements DSHandler { private static Logger logger = LoggerFactory.getLogger(AbstractDSHandler.class); private final IDSRegistry dsRegistry; @@ -101,11 +101,15 @@ protected DataSource getDataSourceById(String dsId) { } protected ImportFromRelation describeImportFrom(DSField importFromField) { - return RelationSupport.describeImportFrom(dsId -> this.getDataSourceHandlerById(dsId), this.getDataSource(), importFromField); + return SmartClientRelationSupport.describeImportFrom(dsId -> this.getDataSourceHandlerById(dsId), this.getDataSource(), importFromField); } protected ForeignKeyRelation describeForeignKey(DSField foreignKeyField) { - return RelationSupport.describeForeignKey(dsId -> this.getDataSourceHandlerById(dsId), this.getDataSource(), foreignKeyField); + return SmartClientRelationSupport.describeForeignKey(dsId -> this.getDataSourceHandlerById(dsId), this.getDataSource(), foreignKeyField); + } + + protected ForeignRelation describeForeignRelation(String relation) { + return SmartClientRelationSupport.describeForeignRelation( dsId -> this.getDataSourceHandlerById(dsId), relation); } protected ForeignRelation determineEffectiveField(DSField dsf) { diff --git a/smartclient-core/src/main/java/org/srg/smartclient/DSDeclarationBuilder.java b/smartclient-core/src/main/java/org/srg/smartclient/DSDeclarationBuilder.java index e05b316..7368765 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/DSDeclarationBuilder.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/DSDeclarationBuilder.java @@ -13,13 +13,13 @@ * * @author srg */ -abstract class DSDeclarationBuilder { +public abstract class DSDeclarationBuilder { private DSDeclarationBuilder() {} private static final Logger logger = LoggerFactory.getLogger(DSDeclarationBuilder.class); - private static class BuilderContext extends RelationSupport { + private static class BuilderContext extends SmartClientRelationSupport { private String dsName; private int qntGeneratedFields; private StringBuilder builder; @@ -64,7 +64,7 @@ public void write_if_notBlank(String str, String fmt, Object... args) { // } public ForeignKeyRelation describeForeignKey(DSField foreignKeyField) { - return RelationSupport.describeForeignKey(this.dsRegistry, this.dataSource, foreignKeyField); + return SmartClientRelationSupport.describeForeignKey(this.dsRegistry, this.dataSource, foreignKeyField); } } @@ -214,7 +214,7 @@ protected static void buildField(BuilderContext ctx, DSField f) throws ClassNotF // https://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/docs/JpaHibernateRelations.html - final RelationSupport.ForeignKeyRelation foreignKeyRelation = ctx.describeForeignKey(f); + final SmartClientRelationSupport.ForeignKeyRelation foreignKeyRelation = ctx.describeForeignKey(f); ctx.write("\t\t\t,type:\"%s\"\n", foreignKeyRelation.foreign().dataSourceId()); diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java index 413d1e2..42b9e90 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java @@ -179,8 +179,8 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { if (request.getOutputs() == null || request.getOutputs().isBlank()) { requestedFields = getDataSource().getFields(); } else { - requestedFields = request.getOutputs() == null ? null : Stream.of(request.getOutputs().split(",")) - .map(str -> str.trim().toLowerCase()) + requestedFields = Stream.of(request.getOutputs().split(",")) + .map(str -> str.trim()) .filter(s -> !s.isEmpty() && !s.isBlank()) .map( fn -> { final DSField dsf = getField( fn); @@ -195,6 +195,89 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { .collect(Collectors.toList()); } + final Map> additionalOutputs; + + if (request.getAdditionalOutputs() == null || request.getAdditionalOutputs().isBlank()){ + additionalOutputs = Collections.EMPTY_MAP; + } else { + additionalOutputs = (Map)Stream.of(request.getAdditionalOutputs().split(",")) + .map(str -> str.trim()) + .filter(s -> !s.isEmpty() && !s.isBlank()) + .map( descr -> { + final String parsed[] = descr.split("!"); + + if (parsed.length != 2) { + throw new RuntimeException("Data source '%s': Invalid additionalOutputs value '%s', valid format is 'localFieldName!relatedDataSourceID.relatedDataSourceFieldName'." + .formatted( + dataSource().getId(), + descr + ) + ); + } + + final String sourceFieldName = parsed[0].trim(); + + final DSField sourceField = JDBCHandler.this.getField(sourceFieldName); + if (sourceField == null) { + throw new RuntimeException("Data source '%s': Invalid additionalOutputs value '%s', nothing known about field '%s'." + .formatted( + dataSource().getId(), + descr, + sourceFieldName + ) + ); + } + + final ForeignKeyRelation fkRelation; + + try { + fkRelation = describeForeignKey(sourceField); + } catch (Throwable t) { + throw new RuntimeException("Data source '%s': Invalid additionalOutputs value '%s', " + .formatted( + dataSource().getId(), + descr + ), + t + ); + } + + final ForeignRelation fRelation; + + try { + fRelation = describeForeignRelation(parsed[1].trim()); + } catch (Throwable t) { + throw new RuntimeException("Data source '%s': Invalid additionalOutputs value '%s', " + .formatted( + dataSource().getId(), + descr + ), + t + ); + } + + if (!fkRelation.foreign().dataSourceId().equals(fRelation.dataSourceId())) { + throw new RuntimeException("Data source '%s': Invalid additionalOutputs value '%s', " + .formatted( + dataSource().getId(), + descr + ) + ); + } + + return new AbstractMap.SimpleImmutableEntry(sourceField, fRelation); + }) + .collect( + Collectors.groupingBy( + e -> e.getKey(), + Collectors.mapping( + e -> e.getValue(), + Collectors.toList() + ) + ) + ); + } + final String selectClause = String.format("SELECT %s", String.join(",\n " , requestedFields @@ -409,16 +492,21 @@ SELECT count(*) FROM ( ) ); - final ForeignKeyRelation foreignKeyRelation = describeForeignKey(dsf); + final ForeignKeyRelation fkRelation = describeForeignKey(dsf); + + final List ffs = additionalOutputs.get(dsf); + final String entityOutputs = ffs == null ? null : ffs.stream() + .map(fk -> fk.fieldName()) + .collect(Collectors.joining(", ")); final DSResponse response; try { - response = fetchForeignEntityById(foreignKeyRelation, pkValues); + response = fetchForeignEntityById(fkRelation, entityOutputs, pkValues); assert response != null; } catch ( Throwable t) { throw new RuntimeException("Subsequent entity fetch failed: %s, filters: %s" .formatted( - foreignKeyRelation, + fkRelation, pkValues.entrySet().stream() .map( e -> "'%s': %s" .formatted( @@ -436,7 +524,7 @@ SELECT count(*) FROM ( throw new RuntimeException("Subsequent entity fetch failed: %s, %s, filters: %s" .formatted( response.getData().getGeneralFailureMessage(), - foreignKeyRelation, + fkRelation, pkValues.entrySet().stream() .map( e -> "'%s': %s" .formatted( @@ -468,7 +556,7 @@ SELECT count(*) FROM ( data); } - protected DSResponse fetchForeignEntityById(ForeignKeyRelation foreignKeyRelation, Map filtersAndKeys) throws Exception { + protected DSResponse fetchForeignEntityById(ForeignKeyRelation foreignKeyRelation, String outputs, Map filtersAndKeys) throws Exception { logger.debug("Performing foreign fetch for relation '%s' with criteria: %s" .formatted( foreignKeyRelation, @@ -490,6 +578,7 @@ protected DSResponse fetchForeignEntityById(ForeignKeyRelation foreignKeyRelatio final DSRequest fetchEntity = new DSRequest(); fetchEntity.setDataSource(dsHandler.id()); fetchEntity.setOperationType(DSRequest.OperationType.FETCH); + fetchEntity.setOutputs(outputs); /** * if type is not provided this indicates that the only PKs should be fetched. diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JpaDSDispatcher.java b/smartclient-core/src/main/java/org/srg/smartclient/JpaDSDispatcher.java index fb7ceee..9a59ffa 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JpaDSDispatcher.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JpaDSDispatcher.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.srg.smartclient.jpa.JPAAwareHandlerFactory; import javax.persistence.*; diff --git a/smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java b/smartclient-core/src/main/java/org/srg/smartclient/SmartClientRelationSupport.java similarity index 99% rename from smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java rename to smartclient-core/src/main/java/org/srg/smartclient/SmartClientRelationSupport.java index df212dd..2a26199 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/SmartClientRelationSupport.java @@ -6,7 +6,7 @@ import java.util.List; import java.util.stream.Collectors; -public class RelationSupport { +public class SmartClientRelationSupport { public static record ForeignKeyRelation( DataSource dataSource, DSField sourceField, @@ -46,7 +46,7 @@ public String toString() { } } - protected static record ForeignRelation( + public static record ForeignRelation( String dataSourceId, DataSource dataSource, diff --git a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSField.java b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSField.java index 2ca24d0..668b585 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSField.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSField.java @@ -84,6 +84,11 @@ public enum FieldType { private String rootValue; private String dbName; + /** + * The table name to use when qualifying the column name for this field during server-side SQL query generation. + */ + private String tableName; + /** * Indicates that this field should always be Array-valued. If the value derived from * {@link com.smartgwt.client.data.DataSource#getDataFormat XML or JSON data} is singular, it will be wrapped in an Array. @@ -371,6 +376,14 @@ public void setMultiple(boolean multiple) { this.multiple = multiple; } + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSRequest.java b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSRequest.java index 60d45e5..207506b 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSRequest.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DSRequest.java @@ -65,6 +65,17 @@ public enum TextMatchStyle { * Note that you cannot use this property to request a superset of the fields that would normally be returned, because that would be a security hole. It is possible to configure individual OperationBindings to return extra fields, but this must be done in the server's DataSource descriptor; it cannot be altered on the fly from the client side. */ private String outputs; + + /** + * https://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/data/DSRequest.html#getAdditionalOutputs-- + * + * For fetch, add or update operation, an optional comma separated list of fields to fetch from another, related DataSource. + * Fields should be specified in the format "localFieldName!relatedDataSourceID.relatedDataSourceFieldName". where relatedDataSourceID is the ID of the related dataSource, and relatedDataSourceFieldName is the field for which you want to fetch related values. The returned field values will be stored on the data returned to the client under the specified localFieldName. Note that this will be applied in addition to any specified outputs. + * + * Note that as with DataSourceField.includeFrom, the related dataSource must be linked to the primary datasource via a foreignKey relationship. + */ + private String additionalOutputs; + private TextMatchStyle textMatchStyle; private List sortBy; // private Map data; @@ -175,6 +186,14 @@ public void setOutputs(String outputs) { this.outputs = outputs; } + public String getAdditionalOutputs() { + return additionalOutputs; + } + + public void setAdditionalOutputs(String additionalOutputs) { + this.additionalOutputs = additionalOutputs; + } + public static class MapData extends LinkedMap implements IDSRequestData { } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java similarity index 65% rename from smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java rename to smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java index c1407e5..1bed380 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java @@ -1,9 +1,10 @@ -package org.srg.smartclient; +package org.srg.smartclient.jpa; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.srg.smartclient.*; import org.srg.smartclient.annotations.SmartClientField; import org.srg.smartclient.isomorphic.DSField; import org.srg.smartclient.isomorphic.DataSource; @@ -11,7 +12,6 @@ import javax.persistence.*; import javax.persistence.metamodel.*; import java.lang.reflect.Field; -import java.lang.reflect.Member; import java.util.*; import java.util.stream.Collectors; @@ -250,11 +250,14 @@ protected DSField describeField( Metamodel mm, String dsId, EntityType DSField describeField( Metamodel mm, String dsId, EntityType1) { - throw new IllegalStateException("Should be implemeted soon"); + if (!jpaRelation.joinColumns().isEmpty()){ + if (jpaRelation.joinColumns().size() >1) { + throw new IllegalStateException("Should be implemented soon"); } else { foreignFieldName = fff.getName(); } @@ -310,7 +313,7 @@ protected DSField describeField( Metamodel mm, String dsId, EntityType DSField describeField( Metamodel mm, String dsId, EntityType DSField describeField( Metamodel mm, String dsId, EntityType type = pa.getElementType(); @@ -359,51 +361,134 @@ protected DSField describeField( Metamodel mm, String dsId, EntityType javaType = type.getJavaType(); - f.setMultiple(true); - // should be hidden by default - if (f.isHidden() == null) { - f.setHidden(true); + // Fields with Entity type should be hidden by default + if (f.isHidden() == null) { + f.setHidden(true); + } + + f.setMultiple(true); + + // -- Determine a foreign relation + final Class javaType = type.getJavaType(); + final Set dsIdFields = getDSIDField(mm, javaType); + + DSField fkField = null; + if (dsIdFields.size() == 1) { + fkField = dsIdFields.iterator().next(); + + } else if (jpaRelation.mappedByFieldName() != null + && !jpaRelation.mappedByFieldName().isBlank()) { + + for (DSField dsf: dsIdFields) { + if (dsf.getName().equals(jpaRelation.mappedByFieldName())) { + fkField = dsf; + break; } + } + } - final Set dsIdFields = getDSIDField(mm, javaType); + if (fkField == null) { + throw new IllegalStateException( + "Datasource '%s', field '%s': Can't determine a foreignKey field for '%s.%s'" + .formatted( dsId, + f.getName(), + f.getForeignDisplayField(), + attr.getDeclaringType(), + attr.getName() + ) + ); + } - DSField fff = null; - if (dsIdFields.size() == 1) { - fff = dsIdFields.iterator().next(); + final String foreignDsId = getDsId(javaType); + +// SmartClientRelationSupport.describeForeignKey(dsId -> get); +// +// final SmartClientRelationSupport.ForeignKeyRelation fkRelation = new SmartClientRelationSupport.ForeignKeyRelation( +// foreignDsId, +// null, +// fkField.getName(), +// fkField +// ); + + final SmartClientRelationSupport.ForeignRelation fkRelation = new SmartClientRelationSupport.ForeignRelation( + foreignDsId, + null, + fkField.getName(), + fkField + ); - } else if (jpaRelation.mappedByFieldName != null - && !jpaRelation.mappedByFieldName.isBlank()) { + // -- - for (DSField dsf: dsIdFields) { - if (dsf.getName().equals(jpaRelation.mappedByFieldName)) { - fff = dsf; - break; - } - } + switch (pat) { + case MANY_TO_MANY: + /** + * Many-To-Many Relations + * + * An example of Many-To-Many relation is that Students have multiple Courses and each Course has multiple Students. In Java each Student bean has a list of Courses and each Course bean has a list of Students. In database tables are linked using additional table holding references to both students and courses. + * To set up Many-To-Many relation between data sources you need to set up One-To-Many relation on both sides. + * + * For example students DataSourceField for CourseDS data source: + * + * + * + * and courses DataSourceField for StudentDS data source: + * + * + * Note that type attribute can be safely omitted here. + * Note that alternative type declaration to be ID of related data source (as in regular One-To-Many relation case) would work as expected, but is not recommended to use, cause it would result in getting lots of copies of same data. Smartclient server will prevent infinite loops, but still lots of unnecessary data will be sent to client. + * + * @see JPA & Hibernate Relations + * @see many-to-many relationship in SQL datasource? + * @see example shows the simple use of custom SQL clauses to provide a DataSource that joins multiple tables + */ + if (jpaRelation.joinTable() == null) { + throw new RuntimeException( + "Datasource '%s', field '%s': Can't determine a join table name for relation '%s'." + .formatted( dsId, + f.getName(), + jpaRelation + ) + ); } - if (fff == null) { - throw new IllegalStateException( - "Datasource '%s', field '%s': Can't determine a foreignKey field for '%s.%s'" - .formatted( dsId, - f.getName(), - f.getForeignDisplayField(), - attr.getDeclaringType(), - attr.getName() + final String joinTableName; + + if (jpaRelation.joinTable().name() != null + || jpaRelation.joinTable().name().isBlank()) { + + + + joinTableName = "%s_%s" + .formatted( + fkRelation.dataSourceId(), + fkRelation.fieldName() + ); + + logger.debug( + "Data source '%s': join table name is not provided for a relation %s, auto generated name '%s' will be used" + .formatted(dsId, + jpaRelation, + joinTableName ) ); + + } else { + joinTableName = jpaRelation.joinTable().name(); } + f.setTableName(joinTableName); + + + +// break; + + case ONE_TO_MANY: f.setForeignKey( "%s.%s" .formatted( - getDsId(javaType), - fff.getName() + fkRelation.dataSourceId(), + fkRelation.fieldName() ) ); @@ -519,194 +604,4 @@ protected Set getDSIDField( Metamodel mm, Class clazz) { ); } } - - - // https://www.smartclient.com/smartclient-release/isomorphic/system/reference/?id=group..jpaHibernateRelations - private static JpaRelation describeRelation(Metamodel mm, Attribute attribute) { - if (!attribute.isAssociation()) { - return null; - } - final Attribute.PersistentAttributeType pat = attribute.getPersistentAttributeType(); - final JpaRelationType relationType = JpaRelationType.from(pat); - - final EntityType entityType = (EntityType) attribute.getDeclaringType(); - final Field javaField = (Field) attribute.getJavaMember(); - - // -- Mapped by - final EntityType mappedByEntity; - final Attribute mappedByAttribute; - final Field mappedByField; - final String mappedByFieldName = determineMappedBy(mm, relationType, javaField); - if (mappedByFieldName != null && !mappedByFieldName.isBlank()) { - if (attribute instanceof SingularAttribute sa) { - mappedByEntity = (EntityType) sa.getType(); - } else if (attribute instanceof PluralAttribute pa) { - mappedByEntity = (EntityType) pa.getElementType(); - } else { - throw new IllegalStateException("Attribute '%s.%s' has unsupported attribute implementation class '%s'." - .formatted( - attribute.getDeclaringType(), - attribute.getName(), - attribute.getClass() - ) - ); - } - - mappedByAttribute = mappedByEntity.getAttribute( mappedByFieldName ); - - final Object o = mappedByAttribute.getJavaMember(); - if (o instanceof Field ) { - mappedByField = (Field) o; - } else { - throw new IllegalStateException(""); - } - } else { - mappedByEntity = null; - mappedByField = null; - mappedByAttribute = null; - } - - // -- Join columns - List joinColumns = determineJoinColumns(javaField); - - if (joinColumns.isEmpty()) { - joinColumns = determineJoinColumns(entityType); - } - - List mappedByJoinColumns = mappedByField == null ? Collections.EMPTY_LIST : determineJoinColumns(mappedByField); - - if (mappedByJoinColumns.isEmpty() - && mappedByFieldName != null - && !mappedByFieldName.isBlank()) { - mappedByJoinColumns = determineJoinColumns(mappedByEntity); - - } - - if (joinColumns.isEmpty() && mappedByJoinColumns.isEmpty()) { - throw new IllegalStateException("Cant't build JpaRelation for '%s.%s': join column is not found." - .formatted(attribute.getDeclaringType(), attribute.getName())); - } - - return new JpaRelation(relationType, null, joinColumns, mappedByFieldName, mappedByJoinColumns); - } - - private static String determineMappedBy(Metamodel mm, JpaRelationType type, Field field) { - final String mappedBy = switch (type) { - case ONE_TO_MANY -> field.getAnnotation(OneToMany.class).mappedBy(); - case ONE_TO_ONE -> field.getAnnotation(OneToOne.class).mappedBy(); - case MANY_TO_MANY -> field.getAnnotation(ManyToMany.class).mappedBy(); - - // manyToOne does not support mappedBy - case MANY_TO_ONE -> null; - default -> null; - }; - - return mappedBy; - } - - private static List determineJoinColumns(EntityType entityType) { - final List joinColumns; - - if (!entityType.hasSingleIdAttribute()) { - /** - * Entity has a composite key, therefore it is also require to look for @JoinColumn annotations - * at the MappedBy entity, if any - */ - final Set> idAttr = entityType.getIdClassAttributes(); - - joinColumns = idAttr.stream() - .filter(a -> a.isAssociation()) - .map(a -> { - final Member jm = a.getJavaMember(); - List jc = determineJoinColumns((Field) jm); - - if (jc.isEmpty()) { - /** - * it seems that Entity IdClass is not annotated, and it is highly possible that - * all the annotations were put at the correspondent entity fields. - * - * I can't find any JPA MetaModel API that returns attributes for the Entity, - * all of them returns attributes for the IdClass. Unfortunately, as a result, - - * the only way to get correspondent entity fields is to use a Java Reflection API. - */ - final Class entityJavaType = entityType.getJavaType(); - - Field entityField = null; - try { - entityField = entityJavaType.getDeclaredField(a.getName()); - } catch (NoSuchFieldException e) { - } - - if (entityField != null) { - assert !jm.equals(entityField); - jc = determineJoinColumns(entityField); - } - } - assert jc != null; - return jc; - }) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } else { - joinColumns = Collections.EMPTY_LIST; - } - - return joinColumns; - } - - private static final List determineJoinColumns(Field field) { - final List joinColumns; - - final JoinColumn joinColumnAnnotation = field.getAnnotation(JoinColumn.class); - if (joinColumnAnnotation != null) { - joinColumns = Collections.singletonList(joinColumnAnnotation); - } else { - final JoinColumns joinColumnsAnnotation = field.getAnnotation(JoinColumns.class); - if (joinColumnsAnnotation != null){ - joinColumns = Arrays.asList(joinColumnsAnnotation.value()); - } else { - final JoinTable joinTableAnnotation = field.getAnnotation(JoinTable.class); - if (joinTableAnnotation != null) { - joinColumns = Arrays.asList(joinTableAnnotation.joinColumns()); - } else { - joinColumns = Collections.EMPTY_LIST; - } - } - } - - return joinColumns; - } - - protected enum JpaRelationType { - BASIC, - ONE_TO_MANY, - ONE_TO_ONE, - MANY_TO_ONE, - MANY_TO_MANY; - - public static JpaRelationType from(Attribute.PersistentAttributeType pat) { - return switch (pat) { - case BASIC -> JpaRelationType.BASIC; - - case MANY_TO_ONE -> JpaRelationType.MANY_TO_ONE; - case ONE_TO_MANY -> JpaRelationType.ONE_TO_MANY; - case ONE_TO_ONE -> JpaRelationType.ONE_TO_ONE; - case MANY_TO_MANY -> JpaRelationType.MANY_TO_MANY; - - default -> throw new IllegalStateException(); - }; - } - } - - protected static record JpaRelation( - JpaRelationType type, - - // - String idClassName, - - List joinColumns, - - String mappedByFieldName, - List mappedByJoinColumn - ){ } } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPARelationSupport.java b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPARelationSupport.java new file mode 100644 index 0000000..4d94c42 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPARelationSupport.java @@ -0,0 +1,179 @@ +package org.srg.smartclient.jpa; + +import javax.persistence.*; +import javax.persistence.metamodel.*; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.util.*; +import java.util.stream.Collectors; + +public class JPARelationSupport { + + // https://www.smartclient.com/smartclient-release/isomorphic/system/reference/?id=group..jpaHibernateRelations + public static JpaRelation describeRelation(Metamodel mm, Attribute attribute) { + if (!attribute.isAssociation()) { + return null; + } + final Attribute.PersistentAttributeType pat = attribute.getPersistentAttributeType(); + final JpaRelationType relationType = JpaRelationType.from(pat); + + final EntityType sourceEntityType = (EntityType) attribute.getDeclaringType(); + final Field javaField = (Field) attribute.getJavaMember(); + + // -- Mapped by + final EntityType foreignEntityType; + final Attribute mappedByAttribute; + final Field mappedByField; + final String mappedByFieldName = determineMappedBy(mm, relationType, javaField); + if (mappedByFieldName != null && !mappedByFieldName.isBlank()) { + if (attribute instanceof SingularAttribute sa) { + foreignEntityType = (EntityType) sa.getType(); + } else if (attribute instanceof PluralAttribute pa) { + foreignEntityType = (EntityType) pa.getElementType(); + } else { + throw new IllegalStateException("Attribute '%s.%s' has unsupported attribute implementation class '%s'." + .formatted( + attribute.getDeclaringType(), + attribute.getName(), + attribute.getClass() + ) + ); + } + + mappedByAttribute = foreignEntityType.getAttribute( mappedByFieldName ); + + final Object o = mappedByAttribute.getJavaMember(); + if (o instanceof Field ) { + mappedByField = (Field) o; + } else { + throw new IllegalStateException(""); + } + } else { + foreignEntityType = null; + mappedByField = null; + mappedByAttribute = null; + } + + // -- Join columns + List joinColumns = determineJoinColumns(javaField); + + if (joinColumns.isEmpty()) { + joinColumns = determineJoinColumns(sourceEntityType); + } + + List mappedByJoinColumns = mappedByField == null ? Collections.EMPTY_LIST : determineJoinColumns(mappedByField); + + if (mappedByJoinColumns.isEmpty() + && mappedByFieldName != null + && !mappedByFieldName.isBlank()) { + mappedByJoinColumns = determineJoinColumns(foreignEntityType); + + } + + if (joinColumns.isEmpty() && mappedByJoinColumns.isEmpty()) { + throw new IllegalStateException("Cant't build JpaRelation for '%s.%s': join column is not found." + .formatted(attribute.getDeclaringType(), attribute.getName())); + } + + // -- Join Table + final JoinTable joinTable; + + if (mappedByAttribute == null) { + joinTable = javaField.getAnnotation(JoinTable.class); + } else { + joinTable = mappedByField.getAnnotation(JoinTable.class); + } + + return new JpaRelation(relationType,sourceEntityType, foreignEntityType, null, joinColumns, mappedByFieldName, mappedByJoinColumns, + joinTable == null? null : joinTable); + } + + private static String determineMappedBy(Metamodel mm, JpaRelationType type, Field field) { + final String mappedBy = switch (type) { + case ONE_TO_MANY -> field.getAnnotation(OneToMany.class).mappedBy(); + case ONE_TO_ONE -> field.getAnnotation(OneToOne.class).mappedBy(); + case MANY_TO_MANY -> field.getAnnotation(ManyToMany.class).mappedBy(); + + // manyToOne does not support mappedBy + case MANY_TO_ONE -> null; + default -> null; + }; + + return mappedBy; + } + + private static List determineJoinColumns(EntityType entityType) { + final List joinColumns; + + if (!entityType.hasSingleIdAttribute()) { + /** + * Entity has a composite key, therefore it is also require to look for @JoinColumn annotations + * at the MappedBy entity, if any + */ + final Set> idAttr = entityType.getIdClassAttributes(); + + joinColumns = idAttr.stream() + .filter(a -> a.isAssociation()) + .map(a -> { + final Member jm = a.getJavaMember(); + List jc = determineJoinColumns((Field) jm); + + if (jc.isEmpty()) { + /** + * it seems that Entity IdClass is not annotated, and it is highly possible that + * all the annotations were put at the correspondent entity fields. + * + * I can't find any JPA MetaModel API that returns attributes for the Entity, + * all of them returns attributes for the IdClass. Unfortunately, as a result, - + * the only way to get correspondent entity fields is to use a Java Reflection API. + */ + final Class entityJavaType = entityType.getJavaType(); + + Field entityField = null; + try { + entityField = entityJavaType.getDeclaredField(a.getName()); + } catch (NoSuchFieldException e) { + } + + if (entityField != null) { + assert !jm.equals(entityField); + jc = determineJoinColumns(entityField); + } + } + assert jc != null; + return jc; + }) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } else { + joinColumns = Collections.EMPTY_LIST; + } + + return joinColumns; + } + + private static final List determineJoinColumns(Field field) { + final List joinColumns; + + final JoinColumn joinColumnAnnotation = field.getAnnotation(JoinColumn.class); + if (joinColumnAnnotation != null) { + joinColumns = Collections.singletonList(joinColumnAnnotation); + } else { + final JoinColumns joinColumnsAnnotation = field.getAnnotation(JoinColumns.class); + if (joinColumnsAnnotation != null){ + joinColumns = Arrays.asList(joinColumnsAnnotation.value()); + } else { + final JoinTable joinTableAnnotation = field.getAnnotation(JoinTable.class); + if (joinTableAnnotation != null) { + joinColumns = Arrays.asList(joinTableAnnotation.joinColumns()); + } else { + joinColumns = Collections.EMPTY_LIST; + } + } + } + + return joinColumns; + } + +} + diff --git a/smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelation.java b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelation.java new file mode 100644 index 0000000..aab592e --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelation.java @@ -0,0 +1,23 @@ +package org.srg.smartclient.jpa; + +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.metamodel.EntityType; +import java.util.List; + +public record JpaRelation( + JpaRelationType type, + + EntityType sourceEntityType, + EntityType foreigEntityType, + + // + String idClassName, + + List joinColumns, + + String mappedByFieldName, + List mappedByJoinColumn, + + JoinTable joinTable +){ } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelationType.java b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelationType.java new file mode 100644 index 0000000..a1a222f --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaRelationType.java @@ -0,0 +1,24 @@ +package org.srg.smartclient.jpa; + +import javax.persistence.metamodel.Attribute; + +public enum JpaRelationType { + BASIC, + ONE_TO_MANY, + ONE_TO_ONE, + MANY_TO_ONE, + MANY_TO_MANY; + + public static JpaRelationType from(Attribute.PersistentAttributeType pat) { + return switch (pat) { + case BASIC -> JpaRelationType.BASIC; + + case MANY_TO_ONE -> JpaRelationType.MANY_TO_ONE; + case ONE_TO_MANY -> JpaRelationType.ONE_TO_MANY; + case ONE_TO_ONE -> JpaRelationType.ONE_TO_ONE; + case MANY_TO_MANY -> JpaRelationType.MANY_TO_MANY; + + default -> throw new IllegalStateException(); + }; + } +} diff --git a/smartclient-core/src/test/java/org/srg/smartclient/AbstractJDBCHandlerTest.java b/smartclient-core/src/test/java/org/srg/smartclient/AbstractJDBCHandlerTest.java index e4f9110..97aafcc 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/AbstractJDBCHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/AbstractJDBCHandlerTest.java @@ -20,6 +20,18 @@ public abstract class AbstractJDBCHandlerTest { protected enum ExtraField { + ManyToMany(""" + [ + { + name:"teamMembers" + ,tableName:"project_team" + ,type:"integer" + ,foreignKey:"EmployeeDS.id" + ,multiple:true + } + ] + """), + OneToMany_FetchEntireEntities(""" [ { diff --git a/smartclient-core/src/test/java/org/srg/smartclient/jpa/JPAAwareHandlerFactoryTest.java b/smartclient-core/src/test/java/org/srg/smartclient/jpa/JPAAwareHandlerFactoryTest.java index 9c11272..707dd62 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/jpa/JPAAwareHandlerFactoryTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/jpa/JPAAwareHandlerFactoryTest.java @@ -3,7 +3,6 @@ import net.javacrumbs.jsonunit.core.Option; import org.junit.jupiter.api.Test; import org.srg.smartclient.JDBCHandler; -import org.srg.smartclient.JPAAwareHandlerFactory; import org.srg.smartclient.JsonTestSupport; import javax.persistence.EntityManagerFactory; diff --git a/smartclient-core/src/test/java/org/srg/smartclient/jpa/JpaDSDispatcherTest.java b/smartclient-core/src/test/java/org/srg/smartclient/jpa/JpaDSDispatcherTest.java index 6c8b2ce..59caef3 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/jpa/JpaDSDispatcherTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/jpa/JpaDSDispatcherTest.java @@ -426,6 +426,7 @@ public void manyToOneRelation() { final String projectDsId = dispatcher.registerJPAEntity(Project.class); final DSRequest request = new DSRequest(); + request.setOutputs("id, name, client, clientName"); request.setStartRow(0); request.setDataSource( projectDsId); @@ -483,6 +484,8 @@ public void oneToManyRelation() { final DSRequest request = new DSRequest(); request.setStartRow(0); request.setOutputs("id, projects"); + request.setAdditionalOutputs("projects!ProjectDS.name, projects!ProjectDS.client, projects!ProjectDS.clientName, projects!ProjectDS.id"); + request.setDataSource( clientDsId); final Collection responses = dispatcher.dispatch(request); @@ -590,6 +593,27 @@ public void oneToManyRelationWithCompositeForeignKey() { ); } + @Test + public void manyToManyRelation() { + dispatcher.registerJPAEntity(Employee.class); + dispatcher.registerJPAEntity(Client.class); + dispatcher.registerJPAEntity(ClientData.class); + + final String projectDs = dispatcher.registerJPAEntity(Project.class); + + // -- + final DSRequest request = new DSRequest(); + request.setStartRow(0); + request.setOutputs("id, name, teamMembers"); + request.setAdditionalOutputs("teamMembers!EmployeeDS.id, teamMembers!EmployeeDS.name"); + request.setDataSource( projectDs); + + final Collection responses = dispatcher.dispatch(request); + JsonTestSupport.assertJsonEquals( + """ + [ + ]""", responses); + } @Test public void loadSqlDataSourceFromResource() { From 8d0a46a4da11cbf84e5848407111ac449d5aaf92 Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Fri, 17 Jul 2020 06:59:35 +0300 Subject: [PATCH 2/6] Initial OperationBindings --- .../annotations/OperationBinding.java | 32 +++++++++++++++++++ .../annotations/OperationBindings.java | 18 +++++++++++ 2 files changed, 50 insertions(+) create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBinding.java create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBindings.java diff --git a/smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBinding.java b/smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBinding.java new file mode 100644 index 0000000..51bc8b7 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBinding.java @@ -0,0 +1,32 @@ +package org.srg.smartclient.annotations; + +import org.srg.smartclient.isomorphic.DSRequest; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * https://www.smartclient.com/smartgwtee-release/javadoc/com/smartgwt/client/docs/serverds/OperationBinding.html + * + * An operationBinding tells a DataSource how to execute one of the basic DS operations: fetch, add, update, remove. + */ +@Repeatable(OperationBindings.class) +@Target({TYPE, ANNOTATION_TYPE}) +@Retention(RUNTIME) +public @interface OperationBinding { + DSRequest.OperationType operationType(); + + /** + * https://www.smartclient.com/smartgwtee-release/javadoc/com/smartgwt/client/docs/serverds/OperationBinding.html#whereClause + * + * This property can be specified on an operationBinding to provide the server with a bespoke WHERE clause to use when constructing the SQL query to perform this operation. The property should be a valid expression in the syntax of the underlying database. + * + * https://www.smartclient.com/smartgwt-latest/javadoc/com/smartgwt/client/docs/CustomQuerying.html + */ + String whereClause() default ""; +} diff --git a/smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBindings.java b/smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBindings.java new file mode 100644 index 0000000..602b9c0 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/annotations/OperationBindings.java @@ -0,0 +1,18 @@ +package org.srg.smartclient.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies multiple operation bindings. + * @see OperationBinding + * + */ +@Target({TYPE}) +@Retention(RUNTIME) +public @interface OperationBindings { + OperationBinding[] value(); +} From eb13e78e995bfb47b85ffc991bd5702b87c2ffde Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Sun, 19 Jul 2020 15:48:11 +0300 Subject: [PATCH 3/6] JDBCHandler.java code cleanup. --- .../java/org/srg/smartclient/JDBCHandler.java | 86 +++++++++---------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java index 42b9e90..ad21a9e 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java @@ -17,7 +17,7 @@ public interface JDBCPolicy { void withConnectionDo(String database, Utils.CheckedFunction callback) throws Exception; } - private Logger logger = LoggerFactory.getLogger(getClass()); + private final Logger logger = LoggerFactory.getLogger(getClass()); private final JDBCPolicy policy; @@ -63,7 +63,7 @@ private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { .formatted(foreignKeyRelation.foreign().dataSourceId()); } - /** + /* * Correspondent entity will be fetched by the subsequent query, * therefore it is required to reserve space in the response */ @@ -180,7 +180,7 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { requestedFields = getDataSource().getFields(); } else { requestedFields = Stream.of(request.getOutputs().split(",")) - .map(str -> str.trim()) + .map(String::trim) .filter(s -> !s.isEmpty() && !s.isBlank()) .map( fn -> { final DSField dsf = getField( fn); @@ -200,11 +200,11 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { if (request.getAdditionalOutputs() == null || request.getAdditionalOutputs().isBlank()){ additionalOutputs = Collections.EMPTY_MAP; } else { - additionalOutputs = (Map)Stream.of(request.getAdditionalOutputs().split(",")) - .map(str -> str.trim()) + additionalOutputs = Stream.of(request.getAdditionalOutputs().split(",")) + .map(String::trim) .filter(s -> !s.isEmpty() && !s.isBlank()) .map( descr -> { - final String parsed[] = descr.split("!"); + final String[] parsed = descr.split("!"); if (parsed.length != 2) { throw new RuntimeException("Data source '%s': Invalid additionalOutputs value '%s', valid format is 'localFieldName!relatedDataSourceID.relatedDataSourceFieldName'." @@ -265,13 +265,13 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { ); } - return new AbstractMap.SimpleImmutableEntry(sourceField, fRelation); + return new AbstractMap.SimpleImmutableEntry<>(sourceField, fRelation); }) .collect( Collectors.groupingBy( - e -> e.getKey(), + AbstractMap.SimpleImmutableEntry::getKey, Collectors.mapping( - e -> e.getValue(), + AbstractMap.SimpleImmutableEntry::getValue, Collectors.toList() ) ) @@ -279,35 +279,31 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { } final String selectClause = String.format("SELECT %s", - String.join(",\n " , - requestedFields - .stream() - .map( dsf -> formatFieldNameForSqlSelectClause(dsf)) - .collect(Collectors.toList()) - ) + requestedFields + .stream() + .map(this::formatFieldNameForSqlSelectClause) + .collect(Collectors.joining(",\n ")) ); // -- FROM final String fromClause = String.format("FROM %s", getDataSource().getTableName()); // -- JOIN ON - final String joinClause = String.join(" \n ", - getFields() - .stream() - .filter( dsf -> dsf.isIncludeField()) - .map( dsf -> { + final String joinClause = getFields() + .stream() + .filter(DSField::isIncludeField) + .map( dsf -> { - final ImportFromRelation relation = describeImportFrom(dsf); + final ImportFromRelation relation = describeImportFrom(dsf); - return " JOIN %s ON %s.%s = %s.%s" - .formatted( - relation.foreignDataSource().getTableName(), - this.getDataSource().getTableName(), relation.sourceField().getDbName(), - relation.foreignDataSource().getTableName(), relation.foreignKey().getDbName() - ); - }) - .collect(Collectors.toList()) - ); + return " JOIN %s ON %s.%s = %s.%s" + .formatted( + relation.foreignDataSource().getTableName(), + this.getDataSource().getTableName(), relation.sourceField().getDbName(), + relation.foreignDataSource().getTableName(), relation.foreignKey().getDbName() + ); + }) + .collect(Collectors.joining(" \n ")); // // -- ORDER BY @@ -341,21 +337,19 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { data = new LinkedList<>(); } - final int totalRows[] = new int[] {-1}; + final int[] totalRows = new int[] {-1}; policy.withConnectionDo(this.getDataSource().getDbName(), conn-> { final String genericQuery = String.join("\n ", Arrays.asList(selectClause, fromClause, joinClause /*, whereClause*//*, orderClause*//*, paginationClause*/ )); final String whereClause = filterData.isEmpty() ? "" : " \n\tWHERE \n\t\t" + - String.join("\n\t\t AND ", - filterData.stream() - .map(fd -> fd.sql("opaque")) - .collect(Collectors.toList()) - ); + filterData.stream() + .map(fd -> fd.sql("opaque")) + .collect(Collectors.joining("\n\t\t AND ")); // -- calculate total - /** + /* * Opaque query is required for a proper filtering by calculated fields */ final String countQuery = """ @@ -373,7 +367,7 @@ SELECT count(*) FROM ( countQuery, filterData.stream() .flatMap(fd -> StreamSupport.stream(fd.values().spliterator(), false)) - .map(d-> "%s".formatted(d)) + .map("%s"::formatted) .collect(Collectors.joining(", ")) ) ); @@ -393,7 +387,6 @@ SELECT count(*) FROM ( } // -- fetch data - final String orderClause = request.getSortBy() == null ? "" : "ORDER BY \n" + request.getSortBy().stream() .map(s -> { @@ -440,7 +433,7 @@ SELECT count(*) FROM ( opaqueFetchQuery, filterData.stream() .flatMap(fd -> StreamSupport.stream(filterData.spliterator(), false)) - .map(d-> "%s".formatted(d)) + .map("%s"::formatted) .collect(Collectors.joining(", ")) ) ); @@ -481,7 +474,7 @@ SELECT count(*) FROM ( for(int j=0; j ffs = additionalOutputs.get(dsf); final String entityOutputs = ffs == null ? null : ffs.stream() - .map(fk -> fk.fieldName()) + .map(ForeignRelation::fieldName) .collect(Collectors.joining(", ")); final DSResponse response; @@ -580,7 +573,7 @@ protected DSResponse fetchForeignEntityById(ForeignKeyRelation foreignKeyRelatio fetchEntity.setOperationType(DSRequest.OperationType.FETCH); fetchEntity.setOutputs(outputs); - /** + /* * if type is not provided this indicates that the only PKs should be fetched. * * @see JPA & Hibernate Relations @@ -598,8 +591,8 @@ protected DSResponse fetchForeignEntityById(ForeignKeyRelation foreignKeyRelatio final DataSource foreignDS = foreignKeyRelation.foreign().dataSource(); final String pkNames = foreignDS.getFields().stream() - .filter(dsf -> dsf.isPrimaryKey()) - .map(dsf -> dsf.getName()) + .filter(DSField::isPrimaryKey) + .map(DSField::getName) .collect(Collectors.joining(", ")); fetchEntity.setOutputs(pkNames); @@ -607,8 +600,7 @@ protected DSResponse fetchForeignEntityById(ForeignKeyRelation foreignKeyRelatio fetchEntity.wrapAndSetData(Map.of(foreignKeyRelation.foreign().fieldName(), filtersAndKeys.values().iterator().next())); - final DSResponse response = dsHandler.handle(fetchEntity); - return response; + return dsHandler.handle(fetchEntity); } protected List generateFilterData(DSRequest.TextMatchStyle textMatchStyle, IDSRequestData data ) { From 15ed699609bdfb3e67f0e5bef5c44fc29ef5a292 Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Mon, 20 Jul 2020 12:09:59 +0300 Subject: [PATCH 4/6] JDBCHandler.java Initial SQLTemplateEngine. --- pom.xml | 7 ++ smartclient-core/pom.xml | 7 ++ .../srg/smartclient/AbstractDSHandler.java | 59 ++++++++++++- .../java/org/srg/smartclient/JDBCHandler.java | 21 ++++- .../smartclient/isomorphic/DataSource.java | 21 +++-- .../isomorphic/OperationBinding.java | 87 +++++++++++++++++++ .../smartclient/sql/SQLTemplateEngine.java | 58 +++++++++++++ .../smartclient/AdvancedJDBCHandlerTest.java | 1 + .../org/srg/smartclient/JDBCHandlerTest.java | 14 +++ 9 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/isomorphic/OperationBinding.java create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java diff --git a/pom.xml b/pom.xml index 04cde45..262f141 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,13 @@ + + org.freemarker + freemarker + 2.3.30 + true + + com.h2database h2 diff --git a/smartclient-core/pom.xml b/smartclient-core/pom.xml index 99391a2..2ff20fa 100644 --- a/smartclient-core/pom.xml +++ b/smartclient-core/pom.xml @@ -32,6 +32,13 @@ true + + org.freemarker + freemarker + + + + org.apache.commons commons-lang3 diff --git a/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java index 8a37ed2..9413568 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java @@ -2,10 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.srg.smartclient.isomorphic.DSField; -import org.srg.smartclient.isomorphic.DSRequest; -import org.srg.smartclient.isomorphic.DSResponse; -import org.srg.smartclient.isomorphic.DataSource; +import org.srg.smartclient.isomorphic.*; import java.util.*; @@ -15,6 +12,7 @@ public abstract class AbstractDSHandler extends SmartClientRelationSupport imple private final IDSRegistry dsRegistry; private final DataSource datasource; private transient Map fieldMap; + private transient Map> bindingsMap; public AbstractDSHandler(IDSRegistry dsRegistry, DataSource datasource) { this.dsRegistry = dsRegistry; @@ -35,6 +33,31 @@ protected Map getFieldMap() { return fieldMap; } + protected Map> getBindingsMap() { + if (bindingsMap == null) { + + if (dataSource().getOperationBindings() == null) { + bindingsMap = Map.of(); + } else { + final Map> m = new LinkedHashMap<>(); + + for (OperationBinding b : dataSource().getOperationBindings()) { + List bindings = m.get(b.getOperationType()); + if (bindings == null) { + bindings = new LinkedList<>(); + m.put(b.getOperationType(), bindings); + } + + bindings.add(b); + } + + bindingsMap = Collections.unmodifiableMap(m); + } + } + + return bindingsMap; + } + protected DSField getField(String fieldName) { return getFieldMap().get(fieldName); } @@ -127,4 +150,32 @@ protected ForeignRelation determineEffectiveField(DSField dsf) { return new ForeignRelation(effectiveDS.getId(), effectiveDS, effectiveField.getName(), effectiveField); } + + public OperationBinding getEffectiveOperationBinding(DSRequest.OperationType operationType) { + final Map> bm = getBindingsMap(); + + if (bm == null) { + return null; + } + + final List bindings = bm.get(operationType); + + if (bindings == null || bindings.isEmpty()) { + return null; + } + + final OperationBinding b; + if (bindings.size() >1) { + throw new IllegalStateException("Data source '%s': multiple bindings have not been supported yet, operation type '%s'." + .formatted( + dataSource().getId(), + operationType + ) + ); + } else { + b = bindings.get(0); + } + + return b; + } } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java index ad21a9e..3ec7480 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.srg.smartclient.isomorphic.*; +import org.srg.smartclient.sql.SQLTemplateEngine; import java.sql.*; import java.util.*; @@ -165,8 +166,20 @@ protected String formatFieldNameForSqlSelectClause(DSField dsf) { } protected DSResponse handleFetch(DSRequest request) throws Exception { + assert request != null && DSRequest.OperationType.FETCH.equals(request.getOperationType()); + final int pageSize = request.getEndRow() == -1 ? -1 : request.getEndRow() - request.getStartRow(); + final OperationBinding operationBinding = getEffectiveOperationBinding(request.getOperationType()); + + final boolean isTemplateEngineRequired = SQLTemplateEngine.isTemplateEngineRequired(this, request); +// final boolean potentiallyRequiresTemplateEngine = operationBinding != null && !( +// operationBinding.getAnsiJoinClause().isBlank() +// || operationBinding.getTableClause().isBlank() +// || operationBinding.getWhereClause().isBlank() +// ); + + // -- LIMIT final String paginationClause = pageSize <= 0 ? "" : String.format("LIMIT %d OFFSET %d", request.getEndRow(), request.getStartRow()); @@ -198,7 +211,7 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { final Map> additionalOutputs; if (request.getAdditionalOutputs() == null || request.getAdditionalOutputs().isBlank()){ - additionalOutputs = Collections.EMPTY_MAP; + additionalOutputs = Map.of(); } else { additionalOutputs = Stream.of(request.getAdditionalOutputs().split(",")) .map(String::trim) @@ -352,6 +365,7 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { /* * Opaque query is required for a proper filtering by calculated fields */ + @SuppressWarnings("SqlNoDataSourceInspection") final String countQuery = """ SELECT count(*) FROM ( %s @@ -417,6 +431,7 @@ SELECT count(*) FROM ( /** * Opaque query is required for a proper filtering by calculated fields */ + @SuppressWarnings("SqlNoDataSourceInspection") final String opaqueFetchQuery = """ SELECT * FROM ( %s @@ -619,6 +634,7 @@ protected List generateFilterData(DSRequest.TextMatchStyle textMat ); } + @SuppressWarnings("SwitchStatementWithTooFewBranches") final Object value = switch (dsf.getType()) { case TEXT -> switch (textMatchStyle) { case EXACT -> e.getValue(); @@ -631,6 +647,7 @@ protected List generateFilterData(DSRequest.TextMatchStyle textMat }; + @SuppressWarnings("SwitchStatementWithTooFewBranches") String filterStr = switch (dsf.getType()) { case TEXT -> "%s like ?"; default -> "%s = ?"; @@ -641,7 +658,7 @@ protected List generateFilterData(DSRequest.TextMatchStyle textMat }) .collect(Collectors.toList()); } else if (data == null){ - return Collections.EMPTY_LIST; + return List.of(); } else { throw new IllegalStateException("DataSource '%s': data has unsupported format '%s'." .formatted( diff --git a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DataSource.java b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DataSource.java index be7fad8..e3d4758 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DataSource.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/DataSource.java @@ -1,9 +1,6 @@ package org.srg.smartclient.isomorphic; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; // https://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/docs/serverds/DataSource.html //https://www.smartclient.com/isomorphic/system/reference/?id=class..DataSource @@ -46,6 +43,8 @@ public enum DSServerType { // */ // private boolean autoDeriveSchema; + private List operationBindings; + public void setId(String id) { this.id = id; } @@ -83,7 +82,7 @@ public List getFields() { } public void setFields(List fields) { - /** + /* * It is highly imported: * PKs fields MUST BE teh first ones in the field list */ @@ -134,11 +133,19 @@ public void setDbName(String dbName) { // this.autoDeriveSchema = autoDeriveSchema; // } + public List getOperationBindings() { + return operationBindings; + } + + public void setOperationBindings(List operationBindings) { + final List b = new ArrayList<>(operationBindings.size()); + this.operationBindings = Collections.unmodifiableList(b); + } + @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof DataSource)) return false; - DataSource that = (DataSource) o; + if (!(o instanceof DataSource that)) return false; return getId().equals(that.getId()); } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/OperationBinding.java b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/OperationBinding.java new file mode 100644 index 0000000..075e6f9 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/OperationBinding.java @@ -0,0 +1,87 @@ +package org.srg.smartclient.isomorphic; + +/** + * @see OperationBinding + * @see OperationBinding + * + * @see Custom Querying Overview + * @see Custom Querying + * @see + * + */ +public class OperationBinding { + private DSRequest.OperationType operationType; + private String operationId = ""; + + /** + * For a dataSource of serverType "sql", this property can be specified on an operationBinding to provide the + * server with a bespoke table clause to use when constructing the SQL query to perform this operation. + * + * The property should be a comma-separated list of tables and views, and you can use any special + * language constructs supported by the underlying database. The server will insert the text of this property + * immediately after the "FROM" token. + * + * See the documentation for customSQL for usage examples + */ + private String tableClause = ""; + + /** + * For a dataSource of serverType "sql", this property can be specified on an operationBinding to provide the + * server with a bespoke WHERE clause to use when constructing the SQL query to perform this operation. + * + * The property should be a valid expression in the syntax of the underlying database. The server will insert + * the text of this property immediately after the "WHERE" token. + * + * You may find the SmartClient-provided $criteria variable of particular use with this property. + * + * See the documentation for customSQL for usage examples + */ + private String whereClause = ""; + + /** + * For a dataSource of serverType "sql", this property can be specified on an operationBinding to provide the server with a bespoke ANSI-style joins clause to use when constructing the SQL query to perform this operation. The property should be a set of joins implemented with JOIN directives (as opposed to additional join expressions in the where clause), joining related tables to the main table or view defined in tableClause. The server will insert the text of this property immediately after the tableClause. + * + * See the documentation for customSQL for usage examples + */ + private String ansiJoinClause = ""; + + public DSRequest.OperationType getOperationType() { + return operationType; + } + + public void setOperationType(DSRequest.OperationType operationType) { + this.operationType = operationType; + } + + public String getOperationId() { + return operationId; + } + + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + public String getTableClause() { + return tableClause; + } + + public void setTableClause(String tableClause) { + this.tableClause = tableClause; + } + + public String getWhereClause() { + return whereClause; + } + + public void setWhereClause(String whereClause) { + this.whereClause = whereClause; + } + + public String getAnsiJoinClause() { + return ansiJoinClause; + } + + public void setAnsiJoinClause(String ansiJoinClause) { + this.ansiJoinClause = ansiJoinClause; + } +} diff --git a/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java b/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java new file mode 100644 index 0000000..a8ae656 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java @@ -0,0 +1,58 @@ +package org.srg.smartclient.sql; + +import org.srg.smartclient.JDBCHandler; +import org.srg.smartclient.isomorphic.DSRequest; +import org.srg.smartclient.isomorphic.OperationBinding; +import org.srg.smartclient.isomorphic.criteria.AdvancedCriteria; + +import java.util.HashMap; +import java.util.Map; + +/** + * @see Custom Querying Overview + * @see DefaultQueryClause + */ +public class SQLTemplateEngine { + + public static boolean isTemplateEngineRequired(JDBCHandler handler, DSRequest request) { + final OperationBinding operationBinding = handler.getEffectiveOperationBinding(request.getOperationType()); + + final boolean potentiallyRequiresTemplateEngine = operationBinding != null && !( + operationBinding.getAnsiJoinClause().isBlank() + || operationBinding.getTableClause().isBlank() + || operationBinding.getWhereClause().isBlank() + ); + + return potentiallyRequiresTemplateEngine; + } + + public static Map createContext( + DSRequest request, + String selectClause, + String fromClause, + String whereClause, + String orderClause + ) { + final Map ctx = new HashMap<>(); + + ctx.put("defaultSelectClause",selectClause); + ctx.put("defaultTableClause", fromClause); + ctx.put("defaultWhereClause", whereClause); + ctx.put("defaultOrderClause", orderClause); + + // "defaultAnsiJoinClause", null, +// "defaultValuesClause", null, + + + if (DSRequest.OperationType.FETCH.equals(request.getOperationType())) { + if (request.getData() instanceof Map criteria) { + ctx.put("criteria", criteria); + ctx.put("advancedCriteria", criteria); + } else if (request.getData() instanceof AdvancedCriteria ac) { + ctx.put("advancedCriteria", ac); + } + } + + return ctx; + } +} diff --git a/smartclient-core/src/test/java/org/srg/smartclient/AdvancedJDBCHandlerTest.java b/smartclient-core/src/test/java/org/srg/smartclient/AdvancedJDBCHandlerTest.java index 9c8e5c0..acb2901 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/AdvancedJDBCHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/AdvancedJDBCHandlerTest.java @@ -65,6 +65,7 @@ public void testAdvancedCriteria(ArgumentsAccessor argumentsAccessor) throws Exc } final DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setStartRow(0); // request.setEndRow(2); diff --git a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java index 9da2927..1afbfc8 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java @@ -18,6 +18,7 @@ protected Class getHandlerClass() { @Test public void fetchAll() throws Exception { DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); final DSResponse response = handler.handleFetch(request); JsonTestSupport.assertJsonEquals(""" @@ -58,7 +59,9 @@ public void fetchSpecifiedFields() throws Exception { withExtraFields(ExtraField.Email); DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setOutputs("id, email"); + final DSResponse response = handler.handleFetch(request); JsonTestSupport.assertJsonEquals(""" @@ -100,6 +103,7 @@ public void fetchSpecifiedFields() throws Exception { public void fetchPaginated() throws Exception { // -- the 1'st page DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setStartRow(0); request.setEndRow(2); @@ -176,6 +180,7 @@ public void fetchIncludeFromField() throws Exception { withHandlers(Handler.Location); final DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); final DSResponse response = handler.handleFetch(request); @@ -227,6 +232,7 @@ public void fetchIncludeFromField() throws Exception { public void fetchPaginatedWithSort() throws Exception { DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setStartRow(0); request.setEndRow(2); @@ -306,6 +312,7 @@ public void fetchWithTextFilter() throws Exception { withExtraFields(ExtraField.Email); DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setStartRow(0); request.setEndRow(2); request.setTextMatchStyle(DSRequest.TextMatchStyle.SUBSTRING); @@ -337,6 +344,7 @@ public void fetchWithIncludeFromField() throws Exception { withHandlers(Handler.Location); DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setStartRow(0); request.setEndRow(2); request.setTextMatchStyle(DSRequest.TextMatchStyle.SUBSTRING); @@ -374,6 +382,7 @@ public void fetchWithSQLCalculatedField() throws Exception { withExtraFields(ExtraField.SqlCalculated); DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setStartRow(0); request.setEndRow(2); @@ -410,6 +419,7 @@ public void fetchWithSQLCalculatedAsDisplayFieldForForeignRelation() throws Exce withExtraFields(ExtraField.SqlCalculated); final DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); final DSResponse response = h.handleFetch(request); JsonTestSupport.assertJsonEquals(""" @@ -451,6 +461,8 @@ public void fetchOneToMany_EntireEntity() throws Exception { withExtraFields(ExtraField.OneToMany_FetchEntireEntities, ExtraField.SqlCalculated); final DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); + final DSResponse response = handler.handleFetch(request); JsonTestSupport.assertJsonEquals(""" { @@ -518,7 +530,9 @@ public void fetchOneToMany_OnlyIds() throws Exception { withExtraFields(ExtraField.OneToMany_FetchOnlyIds, ExtraField.SqlCalculated); final DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setOutputs("id, name, roles"); + final DSResponse response = handler.handleFetch(request); JsonTestSupport.assertJsonEquals(""" From 90ec1fe2422f28e9362fb90b9ebc0958bf10996c Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Tue, 21 Jul 2020 09:40:05 +0300 Subject: [PATCH 5/6] SQLTemplateEngine: make it work for a very first time, tableClause, joinClause and orderClause. --- .../java/org/srg/smartclient/JDBCHandler.java | 137 +++++++++++------- .../smartclient/sql/SQLTemplateEngine.java | 29 ++-- .../org/srg/smartclient/JDBCHandlerTest.java | 1 + 3 files changed, 98 insertions(+), 69 deletions(-) diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java index 3ec7480..e97b651 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java @@ -96,7 +96,7 @@ private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { } - /** + /* * It is required to introduce a column alias, to avoid column name duplication */ return "%s AS %s" @@ -172,14 +172,6 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { final OperationBinding operationBinding = getEffectiveOperationBinding(request.getOperationType()); - final boolean isTemplateEngineRequired = SQLTemplateEngine.isTemplateEngineRequired(this, request); -// final boolean potentiallyRequiresTemplateEngine = operationBinding != null && !( -// operationBinding.getAnsiJoinClause().isBlank() -// || operationBinding.getTableClause().isBlank() -// || operationBinding.getWhereClause().isBlank() -// ); - - // -- LIMIT final String paginationClause = pageSize <= 0 ? "" : String.format("LIMIT %d OFFSET %d", request.getEndRow(), request.getStartRow()); @@ -291,7 +283,7 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { ); } - final String selectClause = String.format("SELECT %s", + final String selectClause = String.format("%s", //"SELECT %s", requestedFields .stream() .map(this::formatFieldNameForSqlSelectClause) @@ -299,7 +291,7 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { ); // -- FROM - final String fromClause = String.format("FROM %s", getDataSource().getTableName()); + final String fromClause = String.format("%s"/*"FROM %s"*/, getDataSource().getTableName()); // -- JOIN ON final String joinClause = getFields() @@ -309,7 +301,7 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { final ImportFromRelation relation = describeImportFrom(dsf); - return " JOIN %s ON %s.%s = %s.%s" + return "%s ON %s.%s = %s.%s"//" JOIN %s ON %s.%s = %s.%s" .formatted( relation.foreignDataSource().getTableName(), this.getDataSource().getTableName(), relation.sourceField().getDbName(), @@ -354,24 +346,89 @@ protected DSResponse handleFetch(DSRequest request) throws Exception { policy.withConnectionDo(this.getDataSource().getDbName(), conn-> { - final String genericQuery = String.join("\n ", Arrays.asList(selectClause, fromClause, joinClause /*, whereClause*//*, orderClause*//*, paginationClause*/ )); + // -- fetch data + final String orderClause = request.getSortBy() == null ? "" : " ORDER BY \n" + + request.getSortBy().stream() + .map(s -> { + String order = ""; + switch (s.charAt(0)) { + case '-': + order = " DESC"; + case '+': + s = s.substring(1); + default: + final DSField dsf = getField(s); - final String whereClause = filterData.isEmpty() ? "" : " \n\tWHERE \n\t\t" + - filterData.stream() + if (dsf == null) { + throw new RuntimeException("Data source '%s': nothing known about field '%s' listed in order by clause." + .formatted(getDataSource().getId(), s)); + } + + return "%s.%s%s" + .formatted( + "opaque", + formatFieldNameForSqlOrderClause(dsf), + order + ); + } + }) + .collect(Collectors.joining(", ")); + + final String whereClause = filterData.isEmpty() ? "" : filterData.stream() .map(fd -> fd.sql("opaque")) .collect(Collectors.joining("\n\t\t AND ")); + // -- generate query + final String genericQuery; + { + final Map templateContext = SQLTemplateEngine.createContext(request, selectClause, fromClause, joinClause, whereClause, ""); + + templateContext.put("effectiveSelectClause", selectClause); + + final String effectiveFROM = operationBinding == null + || operationBinding.getTableClause() == null + || operationBinding.getTableClause().isBlank() + ? fromClause : operationBinding.getTableClause(); + templateContext.put("effectiveTableClause", effectiveFROM); + + final String effectiveWhere = operationBinding == null + || operationBinding.getWhereClause() == null + || operationBinding.getWhereClause().isBlank() + ? whereClause : operationBinding.getWhereClause(); + templateContext.put("effectiveWhereClause", effectiveWhere); + + + final String effectiveJoin = operationBinding == null + || operationBinding.getAnsiJoinClause() == null + || operationBinding.getAnsiJoinClause().isBlank() + ? joinClause : operationBinding.getAnsiJoinClause(); + templateContext.put("effectiveAnsiJoinClause", effectiveJoin); + + genericQuery = SQLTemplateEngine.processSQL(templateContext, + """ + ( + SELECT ${effectiveSelectClause} + FROM ${effectiveTableClause} + <#if effectiveAnsiJoinClause?has_content> + JOIN ${effectiveAnsiJoinClause} + + ) opaque + <#if effectiveWhereClause?has_content> + WHERE ${effectiveWhereClause} + + """ + ); + } + + // -- calculate total + /* * Opaque query is required for a proper filtering by calculated fields */ @SuppressWarnings("SqlNoDataSourceInspection") - final String countQuery = """ - SELECT count(*) FROM ( - %s - ) opaque - %s - """.formatted( genericQuery, whereClause ); + final String countQuery = "SELECT count(*) FROM %s" + .formatted( genericQuery); if (logger.isDebugEnabled()) { @@ -400,46 +457,15 @@ SELECT count(*) FROM ( } } - // -- fetch data - final String orderClause = request.getSortBy() == null ? "" : "ORDER BY \n" + - request.getSortBy().stream() - .map(s -> { - String order = ""; - switch (s.charAt(0)) { - case '-': - order = " DESC"; - case '+': - s = s.substring(1); - default: - final DSField dsf = getField(s); - - if (dsf == null) { - throw new RuntimeException("Data source '%s': nothing known about field '%s' listed in order by clause." - .formatted(getDataSource().getId(), s)); - } - - return "%s.%s%s" - .formatted( - "opaque", - formatFieldNameForSqlOrderClause(dsf), - order - ); - } - }) - .collect(Collectors.joining(", ")); - - /** + /* * Opaque query is required for a proper filtering by calculated fields */ @SuppressWarnings("SqlNoDataSourceInspection") final String opaqueFetchQuery = """ - SELECT * FROM ( - %s - ) opaque + SELECT * FROM %s %s %s - %s - """.formatted(genericQuery, whereClause, orderClause, paginationClause); + """.formatted(genericQuery, orderClause, paginationClause); if (logger.isDebugEnabled()) { logger.debug("DataSource %s fetch query:\n%s\n\nparams:\n%s" @@ -681,7 +707,6 @@ default int setStatementParameters(int idx, PreparedStatement preparedStatement) } return idx; } - } protected static class FilterData implements IFilterData{ diff --git a/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java b/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java index a8ae656..3716bde 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/sql/SQLTemplateEngine.java @@ -1,35 +1,38 @@ package org.srg.smartclient.sql; -import org.srg.smartclient.JDBCHandler; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; import org.srg.smartclient.isomorphic.DSRequest; -import org.srg.smartclient.isomorphic.OperationBinding; import org.srg.smartclient.isomorphic.criteria.AdvancedCriteria; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; import java.util.HashMap; import java.util.Map; /** + * @see Custom Querying Overview * @see DefaultQueryClause */ public class SQLTemplateEngine { - public static boolean isTemplateEngineRequired(JDBCHandler handler, DSRequest request) { - final OperationBinding operationBinding = handler.getEffectiveOperationBinding(request.getOperationType()); - - final boolean potentiallyRequiresTemplateEngine = operationBinding != null && !( - operationBinding.getAnsiJoinClause().isBlank() - || operationBinding.getTableClause().isBlank() - || operationBinding.getWhereClause().isBlank() - ); - - return potentiallyRequiresTemplateEngine; + public static String processSQL(Map context, String sql) throws IOException, TemplateException { + final Configuration cfg = new Configuration(Configuration.VERSION_2_3_30); + final Template template = new Template("t", new StringReader(sql), cfg); + final Writer out = new StringWriter(); + template.process(context, out); + return out.toString(); } public static Map createContext( DSRequest request, String selectClause, String fromClause, + String joinClause, String whereClause, String orderClause ) { @@ -38,9 +41,9 @@ public static Map createContext( ctx.put("defaultSelectClause",selectClause); ctx.put("defaultTableClause", fromClause); ctx.put("defaultWhereClause", whereClause); + ctx.put("defaultAnsiJoinClause", joinClause); ctx.put("defaultOrderClause", orderClause); - // "defaultAnsiJoinClause", null, // "defaultValuesClause", null, diff --git a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java index 1afbfc8..95ac9ab 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerTest.java @@ -599,6 +599,7 @@ public void fetchWithOrderBySQLCalculatedField() throws Exception { withExtraFields(ExtraField.SqlCalculated); final DSRequest request = new DSRequest(); + request.setOperationType(DSRequest.OperationType.FETCH); request.setOutputs("id, calculated"); request.setSortBy(List.of("calculated")); From 85bac37e8549e51d3ff4679536ed406deb20caf2 Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Wed, 22 Jul 2020 13:57:46 +0300 Subject: [PATCH 6/6] Intermidiate, not working changes --- .../srg/smartclient/JDBCHandlerFactory.java | 4 ++ .../jpa/JPAAwareHandlerFactory.java | 56 ++++++++++++------- .../org/srg/smartclient/jpa/JpaDSField.java | 16 ++++++ 3 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/jpa/JpaDSField.java diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandlerFactory.java b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandlerFactory.java index a4abd8a..ee2aefe 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandlerFactory.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandlerFactory.java @@ -41,6 +41,10 @@ protected DataSource describeEntity(Class entityClass) { protected DSField describeField(String dsId, Field field) { final DSField f = new DSField(); + return describeField(f, dsId, field); + } + + protected F describeField( F f, String dsId, Field field) { f.setName( field.getName() ); diff --git a/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java index 1bed380..142565f 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/jpa/JPAAwareHandlerFactory.java @@ -8,6 +8,7 @@ import org.srg.smartclient.annotations.SmartClientField; import org.srg.smartclient.isomorphic.DSField; import org.srg.smartclient.isomorphic.DataSource; +import org.srg.smartclient.isomorphic.OperationBinding; import javax.persistence.*; import javax.persistence.metamodel.*; @@ -205,11 +206,12 @@ protected DataSource describeEntity(Metamodel mm, Class entityClass) { return ds; } - protected DSField describeField( Metamodel mm, String dsId, EntityType entityType, Attribute attr) { + protected JpaDSField describeField( Metamodel mm, String dsId, EntityType entityType, Attribute attr) { final Field field = (Field) attr.getJavaMember(); // -- Generic - final DSField f = describeField(dsId, field); + final JpaDSField f = new JpaDSField(); + describeField(f, dsId, field); // -- JPA final boolean attributeBelongsToCompositeId = !entityType.hasSingleIdAttribute() @@ -454,25 +456,25 @@ protected DSField describeField( Metamodel mm, String dsId, EntityType DSField describeField( Metamodel mm, String dsId, EntityType