From 509ef685913612a8de5f07bcf694ea2b11e52d27 Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Tue, 17 Aug 2021 11:03:50 +0300 Subject: [PATCH 1/3] fix fetching data for multiple includeFrom fields with the same displayField. Started refactoringvto reduce number of temporary object created for each request, introduced IDSRuntime. --- .../srg/smartclient/AbstractDSHandler.java | 43 ++--- .../srg/smartclient/AdvancedJDBCHandler.java | 5 +- .../srg/smartclient/DSDeclarationBuilder.java | 18 +- .../org/srg/smartclient/DSDispatcher.java | 136 ++++++++------- .../org/srg/smartclient/IDSDispatcher.java | 5 +- .../java/org/srg/smartclient/IDSLookup.java | 6 +- .../java/org/srg/smartclient/IDSRegistry.java | 7 - .../java/org/srg/smartclient/JDBCHandler.java | 89 +++++++--- .../srg/smartclient/JDBCHandlerFactory.java | 7 +- .../smartclient/JPAAwareHandlerFactory.java | 29 +-- .../org/srg/smartclient/JpaDSDispatcher.java | 67 ++++++- .../org/srg/smartclient/RelationSupport.java | 25 ++- .../org/srg/smartclient/SQLFetchContext.java | 96 +++++----- .../srg/smartclient/runtime/DSRuntime.java | 125 +++++++++++++ .../srg/smartclient/runtime/IDSRuntime.java | 10 ++ .../srg/smartclient/AbstractHandlerTest.java | 41 +++-- .../JDBCHandlerFetchIncludeFromTest.java | 142 +++++++++++++-- .../srg/smartclient/JDBCHandlerFetchTest.java | 12 +- .../org/srg/smartclient/JsonTestSupport.java | 3 +- .../srg/smartclient/RelationSupportTest.java | 165 ++++++++++++++++-- .../smartclient/jpa/JpaDSDispatcherTest.java | 44 ++--- .../resources/db/V1_0__init_test_schema.sql | 16 +- .../spring/AutomaticDSHandlerRegistrar.java | 58 +++--- .../DMIBeanPostProcessor.java | 2 +- .../org/srg/smartclient/SpringDMITest.java | 2 +- 25 files changed, 818 insertions(+), 335 deletions(-) delete mode 100644 smartclient-core/src/main/java/org/srg/smartclient/IDSRegistry.java create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/runtime/DSRuntime.java create mode 100644 smartclient-core/src/main/java/org/srg/smartclient/runtime/IDSRuntime.java 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 a2244b9..706b771 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/AbstractDSHandler.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.srg.smartclient.isomorphic.*; +import org.srg.smartclient.runtime.IDSRuntime; import java.util.*; @@ -10,12 +11,12 @@ public abstract class AbstractDSHandler extends RelationSupport implements DSHan private static final String META_DATA_PREFIX = "_"; private static final Logger logger = LoggerFactory.getLogger(AbstractDSHandler.class); - private final IDSRegistry dsRegistry; + private final IDSRuntime dsRuntime; private final DataSource datasource; private transient Map> bindingsMap; - public AbstractDSHandler(IDSRegistry dsRegistry, DataSource datasource) { - this.dsRegistry = dsRegistry; + public AbstractDSHandler(IDSRuntime dsRuntime, DataSource datasource) { + this.dsRuntime = dsRuntime; this.datasource = datasource; } @@ -120,33 +121,29 @@ private DSResponse failureDueToUnsupportedOperation(DSRequest request) { ); } - protected DSHandler getDataSourceHandlerById(String id) { - assert dsRegistry != null; - return dsRegistry.getDataSourceHandlerById(id); + protected IDSRuntime getDsRuntime() { + return dsRuntime; } - protected DataSource getDataSourceById(String dsId) { - assert dsRegistry != null; - return dsRegistry.getDataSourceById(dsId); + protected DSHandler getDataSourceHandlerById(String id) { + assert dsRuntime != null; + return dsRuntime.getDataSourceHandlerById(id); } - protected DataSource getDatasourceByTableName(String tableName) { - assert dsRegistry != null; + protected ImportFromRelation getImportFromRelation(DSField importFromField){ + return getDsRuntime().getImportFromRelation(this.id(), importFromField.getName()); + } - for (IHandler h: dsRegistry.handlers()) { - if (h instanceof DSHandler dsHandler) { - if (dsHandler.dataSource().getTableName().equalsIgnoreCase(tableName)) { - return dsHandler.dataSource(); - } - } - } - return null; + protected ForeignKeyRelation getForeignKeyRelation(DSField importFromField){ + return getDsRuntime().getForeignKeyRelation(this.id(), importFromField.getName()); } + @Deprecated(since ="Use getImportFromRelation() instead", forRemoval = true) protected ImportFromRelation describeImportFrom(DSField importFromField) { return RelationSupport.describeImportFrom(this::getDataSourceHandlerById, this.getDataSource(), importFromField); } + @Deprecated(since ="Use getForeignKeyRelation() instead", forRemoval = true) protected ForeignKeyRelation describeForeignKey(DSField foreignKeyField) { return RelationSupport.describeForeignKey(this::getDataSourceHandlerById, this.getDataSource(), foreignKeyField); } @@ -158,17 +155,21 @@ protected ForeignRelation describeForeignRelation(DataSource dataSource, DSField protected ForeignRelation determineEffectiveField(DSField dsf) { final DataSource effectiveDS; final DSField effectiveField; + final String effectiveRelatedTableAlias; if (dsf.isIncludeField()) { - final ImportFromRelation relation = describeImportFrom(dsf); + final ImportFromRelation relation = getImportFromRelation(dsf); effectiveDS = relation.getLast().foreign().dataSource(); effectiveField = relation.foreignDisplay(); + effectiveRelatedTableAlias = relation.getLast().foreign().getRelatedTableAlias(); } else { effectiveDS = getDataSource(); effectiveField = dsf; + // TODO: not sure what effectiveRelatedTableAlias should be there, figure this out later, as have more usecses + effectiveRelatedTableAlias = null; } - return new ForeignRelation(effectiveDS.getId(), effectiveDS, effectiveField.getName(), effectiveField); + return new ForeignRelation(effectiveDS.getId(), effectiveDS, effectiveField.getName(), effectiveField, null, effectiveRelatedTableAlias); } protected OperationBinding getEffectiveOperationBinding(DSRequest.OperationType operationType, String operationId) { diff --git a/smartclient-core/src/main/java/org/srg/smartclient/AdvancedJDBCHandler.java b/smartclient-core/src/main/java/org/srg/smartclient/AdvancedJDBCHandler.java index d28e9f5..bb0b1c0 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/AdvancedJDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/AdvancedJDBCHandler.java @@ -6,6 +6,7 @@ import org.srg.smartclient.isomorphic.IDSRequestData; import org.srg.smartclient.isomorphic.criteria.AdvancedCriteria; import org.srg.smartclient.isomorphic.criteria.Criteria; +import org.srg.smartclient.runtime.IDSRuntime; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -18,8 +19,8 @@ * https://stackoverrun.com/ru/q/5891230 */ public class AdvancedJDBCHandler extends JDBCHandler { - public AdvancedJDBCHandler(JDBCPolicy jdbcPolicy, IDSRegistry dsRegistry, DataSource datasource) { - super(jdbcPolicy, dsRegistry, datasource); + public AdvancedJDBCHandler(JDBCPolicy jdbcPolicy, IDSRuntime scRuntime, DataSource datasource) { + super(jdbcPolicy, scRuntime, datasource); } @Override 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 df69cdb..7a34a32 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/DSDeclarationBuilder.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/DSDeclarationBuilder.java @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory; import org.srg.smartclient.isomorphic.DSField; import org.srg.smartclient.isomorphic.DataSource; +import org.srg.smartclient.runtime.IDSRuntime; import java.util.Collection; import java.util.Map; @@ -23,11 +24,11 @@ private static class BuilderContext extends RelationSupport { private String dsName; private int qntGeneratedFields; private StringBuilder builder; - private final IDSRegistry dsRegistry; + private final IDSRuntime dsRuntime; private final DataSource dataSource; - public BuilderContext(IDSRegistry dsRegistry, DataSource dataSource) { - this.dsRegistry = dsRegistry; + public BuilderContext(IDSRuntime dsRuntime, DataSource dataSource) { + this.dsRuntime = dsRuntime; this.dataSource = dataSource; clear(); @@ -64,17 +65,18 @@ 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 RelationSupport.describeForeignKey(this.scRuntime, this.dataSource, foreignKeyField); + return this.dsRuntime.getForeignKeyRelation(this.dataSource.getId(), foreignKeyField.getName()); } } - public static String build(IDSRegistry dsRegistry, String dispatcherUrl, DSHandler dsHandler) throws ClassNotFoundException { - return build(dsRegistry, dispatcherUrl, dsHandler.dataSource(), dsHandler.allowAdvancedCriteria()); + public static String build(IDSRuntime scRuntime, String dispatcherUrl, DSHandler dsHandler) throws ClassNotFoundException { + return build(scRuntime, dispatcherUrl, dsHandler.dataSource(), dsHandler.allowAdvancedCriteria()); } - public static String build(IDSRegistry dsRegistry, String dispatcherUrl, DataSource dataSource, boolean allowAdvancedCriteria) throws ClassNotFoundException { + public static String build(IDSRuntime scRuntime, String dispatcherUrl, DataSource dataSource, boolean allowAdvancedCriteria) throws ClassNotFoundException { - final BuilderContext ctx = new BuilderContext(dsRegistry, dataSource); + final BuilderContext ctx = new BuilderContext(scRuntime, dataSource); ctx.dsName = dataSource.getId(); final Collection allFields = dataSource.getFields(); diff --git a/smartclient-core/src/main/java/org/srg/smartclient/DSDispatcher.java b/smartclient-core/src/main/java/org/srg/smartclient/DSDispatcher.java index c66b1b9..83d7c13 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/DSDispatcher.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/DSDispatcher.java @@ -19,6 +19,8 @@ import org.slf4j.LoggerFactory; import org.srg.smartclient.dmi.JDKDMIHandlerFactory; import org.srg.smartclient.isomorphic.*; +import org.srg.smartclient.runtime.DSRuntime; +import org.srg.smartclient.runtime.IDSRuntime; import org.srg.smartclient.utils.ContextualRuntimeException; import org.srg.smartclient.utils.Serde; import org.srg.smartclient.utils.Utils; @@ -31,10 +33,11 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -public class DSDispatcher implements IDSDispatcher { +public class DSDispatcher implements IDSDispatcher, IDSRuntime { private static Logger logger = LoggerFactory.getLogger(DSDispatcher.class); - private Map datasourceMap = new LinkedHashMap<>(); + private DSRuntime dsRuntime = DSRuntime.create(Collections.emptyList()); private JDBCHandlerFactory jdbcHandlerFactory = new JDBCHandlerFactory(); private JDBCHandler.JDBCPolicy jdbcPolicy; private JDKDMIHandlerFactory dmiHandlerFactory; @@ -53,11 +56,24 @@ protected JDBCHandler.JDBCPolicy getJdbcPolicy() { return jdbcPolicy; } + @Override + public Iterator iterator() { + return dsRuntime.iterator(); + } + + @Override + public RelationSupport.ForeignKeyRelation getForeignKeyRelation(String dsId, String fieldName) { + return dsRuntime.getForeignKeyRelation(dsId, fieldName); + } + + @Override + public RelationSupport.ImportFromRelation getImportFromRelation(String dsId, String fieldName) { + return dsRuntime.getImportFromRelation(dsId, fieldName); + } @Override - public IHandler getHandlerByName(String dsId) { - final IHandler ds = datasourceMap.get(dsId); - return ds; + public IHandler getHandlerById(String dsId) { + return dsRuntime.getHandlerById(dsId); // if (ds != null) { // return ds; @@ -66,27 +82,6 @@ public IHandler getHandlerByName(String dsId) { // return Utils.throw_it("Unknown datasource '%s'", dsId); } -// @Override -// public DataSource getDataSourceById(String dsId) { -// final IHandler handler = datasourceMap.get(dsId); -// if (handler == null){ -// return null; -// } -// -// if (handler instanceof DSHandler dsHandler) { -// return dsHandler.dataSource(); -// } -// -// throw new RuntimeException("Handler '%s' is not an instance of 'DSHandler'." -// .formatted(dsId) -// ); -// } - - @Override - public Collection handlers() { - return datasourceMap.values(); - } - private ObjectWriter createObjectWriter() { final SimpleFilterProvider filterProvider = new SimpleFilterProvider(); @@ -131,7 +126,7 @@ public void writeIndentation(JsonGenerator jg, int level) throws IOException { protected DSResponse handleRequest(DSRequest request) { try { - final IHandler ds = getHandlerByName(request.getDataSource()); + final IHandler ds = getHandlerById(request.getDataSource()); final DSResponse response = ds.handle(request); response.setOperationId( request.getOperationId()); @@ -237,12 +232,19 @@ public A generateDSJavaScript(A out, String dispatcherUrl out.append("const DISPATCHER_URL = \"%s\";\n" .formatted(dispatcherUrl)); + /* + * If dsIds is not provided, -- generate all registered handlers + */ if (dsId.length == 0) { - dsId = datasourceMap.keySet().toArray(new String[0]); + final List ids = StreamSupport.stream(dsRuntime.spliterator(), false) + .map(IHandler::id) + .collect(Collectors.toList()); + + dsId = ids.toArray(ids.toArray(new String[0])); } for (String name : dsId) { - final IHandler handler = getHandlerByName(name); + final IHandler handler = getHandlerById(name); if (handler instanceof DSHandler ds) { out.append(DSDeclarationBuilder.build(this, dispatcherUrl, ds)); @@ -326,7 +328,7 @@ public void loadFromResource(String path) throws Exception { logger.info("Resource URL: %s".formatted( url1)); if (url.getProtocol().equals("jar")) { - logger.debug("Scanning a jar file via URL: %s".formatted( url1)); + logger.debug("Scanning a jar file via URL: %s...".formatted( url1)); /* A JAR path */ @@ -369,23 +371,27 @@ public void loadFromResource(String path) throws Exception { } } - logger.debug(""" - Jar scan was completed, the following matches were found in the '%s': - """ - .formatted( - url1, - result.stream() - .collect(Collectors.joining(",\n")) - ) - ); - files.addAll(result); + if (result.isEmpty()) { + logger.debug("Jar scan was completed, nothing ound (jar: '%s')".formatted(url1)); + } else { + logger.debug( + "Jar scan was completed, %d matches were found(jar: '%s'):\n%s" + .formatted( + result.size(), + url1, + result.stream() + .collect(Collectors.joining(",\n ", " ", "")) + ) + ); + files.addAll(result); + } } else { File f = new File(url.getFile()); List result; if (f.isDirectory()) { - logger.info("loading Data Sources from resources, scanning resource directory '%s'." + logger.debug("Scanning resource directory '%s'..." .formatted(f)); @@ -396,44 +402,56 @@ public void loadFromResource(String path) throws Exception { result = List.of(f.getName()); } - logger.debug(""" - Directory scan was completed, the following matches were found in the '%s': - """ - .formatted( - url1, - result.stream() - .collect(Collectors.joining(",\n")) - ) - ); + if (result.isEmpty()) { + logger.debug("Directory scan was completed, nothing found (jar: '%s')".formatted(url1)); + } else { + logger.debug("Directory scan was completed, %d matches were found(dir: '%s'):\n%s" + .formatted( + result.size(), + url1, + result.stream() + .collect(Collectors.joining(",\n "," ","")) + ) + ); + } files.addAll(result); } } - logger.info(""" - BUILTIN RESOURCES SCAN COMPLETED, the following matches were found in the '%s': - %s - """.formatted( + logger.info("BUILTIN RESOURCES SCAN COMPLETED, %d matches were found(path '%s'):\n%s" + .formatted( + files.size(), url, files.stream() - .collect(Collectors.joining(",\n ")) + .collect(Collectors.joining(",\n ", " ", "")) ) ); + final List handlers = new LinkedList<>(); for (String s: files ) { try { final IHandler ds = loadDsFromResource(new File(s)); - registerHandler(ds); + handlers.add(ds); } catch (Exception ex) { logger.warn("Can't load Smart Client DSHandler from file '%s'".formatted(s), ex); } } + + registerHandlers(handlers.toArray(new IHandler[0])); } @Override - public void registerHandler(IHandler handler) { - datasourceMap.put(handler.id(), handler); - logger.info("A new DSHandler has been registered as '%s' ".formatted(handler.id())); + public void registerHandlers(IHandler... handlers) { + dsRuntime = dsRuntime.registerHandlers(handlers); + + logger.info("Following Handlers have been registered:\n%s" + .formatted( + Arrays.stream(handlers) + .map( IHandler::id) + .collect(Collectors.joining(",\n ", " ", "")) + ) + ); } @JsonFilter("PropertyFilter") diff --git a/smartclient-core/src/main/java/org/srg/smartclient/IDSDispatcher.java b/smartclient-core/src/main/java/org/srg/smartclient/IDSDispatcher.java index ed871f2..c434190 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/IDSDispatcher.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/IDSDispatcher.java @@ -2,6 +2,7 @@ import org.srg.smartclient.isomorphic.DSResponse; import org.srg.smartclient.isomorphic.IDSRequest; +import org.srg.smartclient.runtime.IDSRuntime; import java.util.Collection; @@ -9,7 +10,7 @@ * https://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/docs/DateFormatAndStorage.html * */ -public interface IDSDispatcher extends IDSRegistry { +public interface IDSDispatcher extends IDSRuntime { /** * Default path that will be used to look for datasource description files in application resources @@ -18,7 +19,7 @@ public interface IDSDispatcher extends IDSRegistry { Collection dispatch(IDSRequest request); A generateDSJavaScript(A out, String dispatcherUrl, String... dsId) throws Exception; - void registerHandler(IHandler handler); + void registerHandlers(IHandler... handlers); void loadFromResource(String path) throws Exception; default void loadFromResource() throws Exception { diff --git a/smartclient-core/src/main/java/org/srg/smartclient/IDSLookup.java b/smartclient-core/src/main/java/org/srg/smartclient/IDSLookup.java index e7e6be5..e863416 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/IDSLookup.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/IDSLookup.java @@ -3,10 +3,10 @@ import org.srg.smartclient.isomorphic.DataSource; public interface IDSLookup { - IHandler getHandlerByName(String id); + IHandler getHandlerById(String id); default DSHandler getDataSourceHandlerById(String id) { - final IHandler handler = this.getHandlerByName(id); + final IHandler handler = this.getHandlerById(id); if (handler instanceof DSHandler dsHandler) { return dsHandler; } @@ -17,7 +17,7 @@ default DSHandler getDataSourceHandlerById(String id) { } default DataSource getDataSourceById(String dsId) { - final IHandler handler = getHandlerByName(dsId); + final IHandler handler = getHandlerById(dsId); if (handler == null) { return null; } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/IDSRegistry.java b/smartclient-core/src/main/java/org/srg/smartclient/IDSRegistry.java deleted file mode 100644 index c51bb7f..0000000 --- a/smartclient-core/src/main/java/org/srg/smartclient/IDSRegistry.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.srg.smartclient; - -import java.util.Collection; - -public interface IDSRegistry extends IDSLookup{ - Collection handlers(); -} 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 ecf8eaa..d87699d 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java @@ -6,6 +6,7 @@ import org.srg.smartclient.isomorphic.criteria.AdvancedCriteria; import org.srg.smartclient.isomorphic.criteria.Criteria; import org.srg.smartclient.isomorphic.criteria.OperatorId; +import org.srg.smartclient.runtime.IDSRuntime; import org.srg.smartclient.utils.ContextualRuntimeException; import org.srg.smartclient.utils.Utils; @@ -28,8 +29,8 @@ public interface JDBCPolicy { private final JDBCPolicy policy; - public JDBCHandler(JDBCPolicy jdbcPolicy, IDSRegistry dsRegistry, DataSource datasource) { - super(dsRegistry, datasource); + public JDBCHandler(JDBCPolicy jdbcPolicy, IDSRuntime scRuntime, DataSource datasource) { + super(scRuntime, datasource); policy = jdbcPolicy; } @@ -292,10 +293,7 @@ public static EntitySubFetch create(boolean useSimpleCriteria, IDSLookup idsRegi protected DSResponse fetchForeignEntity(Connection connection, ForeignKeyRelation foreignKeyRelation, String outputs, IDSRequestData criteria) throws Exception { logger.debug("Performing foreign fetch for relation '%s' with criteria: %s" - .formatted( - foreignKeyRelation, - criteria - ) + .formatted(foreignKeyRelation, criteria) ); final DSHandler dsHandler = this.idsRegistry.getDataSourceHandlerById(foreignKeyRelation.foreign().dataSourceId()); @@ -786,9 +784,9 @@ protected List generateFilterData( if (value != null) { return new FilterData(effectiveField, filterStr, value); } else { - /** - * MariaDB does not handle properly NULL values in prepared statements, - * therefore it is reuired to use 'IS NULL' only + /* + MariaDB does not handle properly NULL values in prepared statements, + therefore it is reuired to use 'IS NULL' only */ return FilterData.createIsNullFilterData(effectiveField); } @@ -1111,13 +1109,50 @@ private static String generateJoin(List jds) { * @param * @return */ - public static String generateSQLJoin(List frls) { + public static String generateSQLJoin(List> frls) { + final List allDescrs = frls.stream() + .map( AbstractSQLContext::generateJoinDBDescr ) + .flatMap( Collection::stream ) + .collect(Collectors.toList()); + + + /* It is required to generate one join per unique Join Description */ + final List uniqueJDs = allDescrs.stream() + .filter(new Predicate<>() { + final List unique = new LinkedList<>(); + + @Override + public boolean test(JoinGenerator.JoinDBDescr jd) { + + for (JoinGenerator.JoinDBDescr u :unique) { + if ( + u.sourceTable().equals(jd.sourceTable()) + && u.sourceField().equals(jd.sourceField()) + && u.destTable().equals(jd.destTable()) + && u.destField().equals(jd.destField()) + && u.destTableAlias().equals(jd.destTableAlias()) + ) { + return false; + } + } + unique.add(jd); + return true; + } + }) + .collect(Collectors.toList()); + + return JoinGenerator.generateJoin(uniqueJDs); + } + + private static List generateJoinDBDescr(List frls) { // -- generate relation chain final String joinType = "LEFT"; final List jds = new LinkedList<>(); - for (IForeignKeyRelation foreignKeyRelation : frls) { + JoinGenerator.JoinDBDescr prevJoinDBDescr = null; + for (IForeignKeyRelation foreignKeyRelation : frls) { + final JoinGenerator.JoinDBDescr currJoinDBDescr; if(foreignKeyRelation.sourceField().isMultiple()) { /** * Implementation is not perfect and will require re-work as more use cases will come. @@ -1131,13 +1166,13 @@ public static String generateSQLJoin(List String generateSQLJoin(List String generateSQLJoin(List String generateSQLJoin(List c = (Class) Class.forName(ds.getServerConstructor()); - return (JDBCHandler) ConstructorUtils.invokeConstructor(c, jdbcPolicy, dsRegistry, ds); + return (JDBCHandler) ConstructorUtils.invokeConstructor(c, jdbcPolicy, scRuntime, ds); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } else { - return new AdvancedJDBCHandler(jdbcPolicy, dsRegistry, ds); + return new AdvancedJDBCHandler(jdbcPolicy, scRuntime, ds); } } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java b/smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java index d971ef1..6b025ae 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JPAAwareHandlerFactory.java @@ -7,7 +7,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 org.srg.smartclient.runtime.IDSRuntime; import org.srg.smartclient.utils.Utils; import javax.persistence.*; @@ -20,7 +20,7 @@ public class JPAAwareHandlerFactory extends JDBCHandlerFactory { private static final Logger logger = LoggerFactory.getLogger(JPAAwareHandlerFactory.class); public JDBCHandler createHandler(EntityManagerFactory emf, JDBCHandler.JDBCPolicy jdbcPolicy, - IDSRegistry dsRegistry, Class entityClass) { + IDSRuntime scRuntime, Class entityClass) { logger.trace("Building DataSource definition for JPA entity '%s'..." .formatted( @@ -31,36 +31,13 @@ public JDBCHandler createHandler(EntityManagerFactory emf, JDBCHandler.JDBCPolic final Metamodel mm = emf.getMetamodel(); final DataSource ds = this.describeEntity(mm, entityClass); - if (logger.isDebugEnabled()) { - - String dsDefinition; - try { - dsDefinition = DSDeclarationBuilder.build(dsRegistry, "", ds, true); - } catch (Exception e) { - dsDefinition = "Can't serialize Data Source definition, unexpected error occurred: %s" - .formatted( - e.getMessage() - ); - - logger.warn(dsDefinition, e); - } - - logger.debug("DataSource definition for entity '%s' has been built:\n%s" - .formatted( - entityClass.getCanonicalName(), - dsDefinition - ) - ); - } - logger.trace("Creating JDBCHandler Handler for JPA entity '%s'..." .formatted( entityClass.getCanonicalName() ) ); - JDBCHandler handler = createJDBCHandler(jdbcPolicy, dsRegistry, ds); - return handler; + return createJDBCHandler(jdbcPolicy, scRuntime, ds); } protected DataSource describeEntity(Metamodel mm, Class entityClass) { 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 aa4b76d..e818eb9 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JpaDSDispatcher.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JpaDSDispatcher.java @@ -2,13 +2,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.srg.smartclient.isomorphic.DataSource; import javax.persistence.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class JpaDSDispatcher extends DSDispatcher { private static Logger logger = LoggerFactory.getLogger(JpaDSDispatcher.class); private EntityManagerFactory emf; private JPAAwareHandlerFactory jpaAwareHandlerFactory = new JPAAwareHandlerFactory(); + private Map jpaHandlers = new ConcurrentHashMap<>(); public JpaDSDispatcher(EntityManagerFactory emf) { this.emf = emf; @@ -19,16 +23,61 @@ public JpaDSDispatcher(EntityManagerFactory emf, JDBCHandler.JDBCPolicy jdbcPoli this.emf = emf; } - public String registerJPAEntity(Class entityClass) { - final JDBCHandler handler = jpaAwareHandlerFactory.createHandler( - emf, - getJdbcPolicy(), - this, - entityClass - ); - registerHandler(handler); + public void registerJPAEntities(Class... entityClasses) { + final List handlers = new ArrayList<>(entityClasses.length); + final Map handlerMap = new HashMap<>(); - return handler.id(); + for (Class cls: entityClasses) { + JDBCHandler h = jpaAwareHandlerFactory.createHandler( + emf, + getJdbcPolicy(), + this, + cls + ); + + handlers.add(h); + handlerMap.put(cls, h); + } + + registerHandlers(handlers.toArray(new IHandler[0])); + jpaHandlers.putAll(handlerMap); + + // - Dump generated DataSource definitions, if enabled + if (logger.isDebugEnabled()) { + for(IHandler h :handlers) { + if (h instanceof DSHandler dsh) { + String dsDefinition; + try { + dsDefinition = DSDeclarationBuilder.build(this, "", dsh.dataSource(), true); + } catch (Exception e) { + dsDefinition = "Can't serialize Data Source definition, unexpected error occurred: %s" + .formatted( + e.getMessage() + ); + + logger.warn(dsDefinition, e); + } + + Class ec = null; + for (Map.Entry e: handlerMap.entrySet()) { + if (e.getValue().equals(h)) { + ec = e.getKey(); + } + } + + logger.debug("DataSource definition for entity '%s' has been built:\n%s" + .formatted( + ec.getCanonicalName(), + dsDefinition + ) + ); + } + } + } + } + + public IHandler getHandlerByClass(Class entityClass) { + return jpaHandlers.get(entityClass); } } diff --git a/smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java b/smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java index 936044e..bff36b7 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/RelationSupport.java @@ -117,19 +117,21 @@ public static class ForeignRelation { private final DataSource dataSource; private final String fieldName; private final DSField field; + private final String relatedTableAlias; private String sqlFieldAlias; protected ForeignRelation(String dataSourceId, DataSource dataSource, String fieldName, DSField field) { - this(dataSourceId, dataSource, fieldName, field, null); + this(dataSourceId, dataSource, fieldName, field, null, null); } - protected ForeignRelation(String dataSourceId, DataSource dataSource, String fieldName, DSField field, String sqlFieldAlias) { + protected ForeignRelation(String dataSourceId, DataSource dataSource, String fieldName, DSField field, String sqlFieldAlias, String relatedTableAlias) { this.dataSourceId = dataSourceId; this.dataSource = dataSource; this.fieldName = fieldName; this.field = field; this.sqlFieldAlias = sqlFieldAlias; + this.relatedTableAlias = relatedTableAlias; } public String dataSourceId() { @@ -152,20 +154,25 @@ public String getSqlFieldAlias() { return sqlFieldAlias; } + public String getRelatedTableAlias() { + return relatedTableAlias; + } + public ForeignRelation createWithSqlFieldAlias(String sqlFieldAlias) { final ForeignRelation effective = new ForeignRelation( this.dataSourceId(), this.dataSource(), this.fieldName(), this.field(), - sqlFieldAlias + sqlFieldAlias, + null ); return effective; } public String formatAsSQL() { - return formatAsSQL(dataSource().getTableName()); + return formatAsSQL(this.getRelatedTableAlias() == null || this.getRelatedTableAlias().isBlank() ? dataSource().getTableName() : this.getRelatedTableAlias()); } public String formatAsSQL(String aliasOrTable) { @@ -322,6 +329,11 @@ private static DSField determineForeignKeyField(DataSource dataSource, DSField i } public static ImportFromRelation describeImportFrom(IDSLookup idsRegistry, DataSource dataSource, DSField importFromField) { + if (importFromField == null) { + throw new IllegalArgumentException("Can't determine ImportFromRelation for '%s' datasource: importFrom field is NULL" + .formatted(dataSource.getId())); + } + if (!importFromField.isIncludeField()) { throw new IllegalStateException(); } @@ -404,11 +416,14 @@ public static ImportFromRelation describeImportFrom(IDSLookup idsRegistry, DataS ); } + final String relatedTableAlias = fkField.getRelatedTableAlias() == null || fkField.getRelatedTableAlias().isBlank() ? + fkField.getName() + "_" + foreignRelation.dataSource.getTableName() : fkField.getRelatedTableAlias(); + final ForeignKeyRelation frl = new ForeignKeyRelation( ds, fkField, false, - new ForeignRelation(foreignRelation.dataSource.getId(), foreignRelation.dataSource, foreignKey.getName(), foreignKey) + new ForeignRelation(foreignRelation.dataSource.getId(), foreignRelation.dataSource, foreignKey.getName(), foreignKey, null, relatedTableAlias) ); foreignKeyRelations.add(frl); diff --git a/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java b/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java index 6d03c2a..59625bd 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java @@ -16,6 +16,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.srg.smartclient.RelationSupport.*; + public class SQLFetchContext extends JDBCHandler.AbstractSQLContext { private static final Logger logger = LoggerFactory.getLogger(SQLFetchContext.class); private String genericQuery; @@ -24,7 +26,7 @@ public class SQLFetchContext extends JDBCHandler.Abstract private String paginationClause; private final List requestedFields = new LinkedList<>(); - private final Map> additionalOutputs = new HashMap<>(); + private final Map> additionalOutputs = new HashMap<>(); private List filterData = new LinkedList<>(); private Map templateContext; @@ -40,7 +42,7 @@ public List getRequestedFields() { return requestedFields; } - public Map> getAdditionalOutputs() { + public Map> getAdditionalOutputs() { return additionalOutputs; } @@ -66,7 +68,7 @@ protected String formatFieldNameForSqlOrderClause(DSField dsf) { private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { - final RelationSupport.ForeignRelation effectiveRelation; + final ForeignRelation effectiveRelation; final String effectiveColumn; if (AbstractDSHandler.isSubEntityFetchRequired(dsf)) { @@ -77,7 +79,7 @@ private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { // Populate extra information for the sake of troubleshooting final String extraInfo; - final RelationSupport.ForeignKeyRelation foreignKeyRelation = dsHandler().describeForeignKey(dsf); + final ForeignKeyRelation foreignKeyRelation = dsHandler().getForeignKeyRelation(dsf); if (logger.isDebugEnabled()) { extraInfo = "Sub-entity placeholder for '%s' (will be fetched as a subsequent request)" .formatted( @@ -92,7 +94,7 @@ private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { * Correspondent entity will be fetched by the subsequent query, * therefore it is required to reserve space in the response */ - effectiveRelation = new RelationSupport.ForeignRelation(foreignKeyRelation.dataSource().getId(), foreignKeyRelation.dataSource(), + effectiveRelation = new ForeignRelation(foreignKeyRelation.dataSource().getId(), foreignKeyRelation.dataSource(), dsf.getName(), dsf); effectiveColumn = "NULL /* %s */" @@ -107,7 +109,7 @@ private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { effectiveColumn = effectiveRelation.field().getCustomSelectExpression(); } else { if (dsHandler().isIncludeSummaryRequired(dsf)) { - final RelationSupport.ImportFromRelation ifr = dsHandler().describeImportFrom(dsf); + final ImportFromRelation ifr = dsHandler().getImportFromRelation(dsf); final String extraInfo; if (logger.isDebugEnabled()) { @@ -135,10 +137,39 @@ private String formatFieldNameFor(boolean formatForSelect, DSField dsf) { } } - final String formattedFieldName = JDBCHandler.formatColumnNameToAvoidAnyPotentialDuplication( - effectiveRelation.dataSource(), - effectiveRelation.field() - ); + final String formattedFieldName; + if (dsf.isIncludeField()) { + /* + * In case of multiple include from with the same displayField + * it is required to differentiate generated column aliases, + * otherwise such usage will introduce column name duplication: + * + * [{ + * name:"managerFullName" + * ,includeFrom:"EmployeeDS.name" + * ,includeVia:"manager" + * }, + * { + * name:"supervisorFullName" + * ,includeFrom:"EmployeeDS.name" + * ,includeVia:"supervisor" + * }] + * + * 'Duplicate column name "name_employee"': + * SELECT + * manager_employee.name AS name_employee, + * supervisor_employee.name AS name_employee + * FROM project + * LEFT JOIN employee manager_employee ON project.manager_id = manager_employee.id + * LEFT JOIN employee supervisor_employee ON project.supervisor_id = supervisor_employee.id + */ + formattedFieldName = dsf.getName(); + } else { + formattedFieldName = JDBCHandler.formatColumnNameToAvoidAnyPotentialDuplication( + effectiveRelation.dataSource(), + effectiveRelation.field() + ); + } if (!formatForSelect) { return formattedFieldName; @@ -232,7 +263,7 @@ protected void init() throws IOException, TemplateException { if (request().getAdditionalOutputs() != null && !request().getAdditionalOutputs().isBlank()) { - final Map> additionalOutputs = Stream.of(request().getAdditionalOutputs().split(",")) + final Map> additionalOutputs = Stream.of(request().getAdditionalOutputs().split(",")) .map(String::trim) .filter(s -> !s.isEmpty() && !s.isBlank()) .map(descr -> { @@ -262,10 +293,10 @@ protected void init() throws IOException, TemplateException { ); } - final RelationSupport.ForeignKeyRelation fkRelation; + final ForeignKeyRelation fkRelation; try { - fkRelation = dsHandler().describeForeignKey(sourceField); + fkRelation = dsHandler().getForeignKeyRelation(sourceField); } catch (Throwable t) { throw new ContextualRuntimeException("Data source '%s': Invalid additionalOutputs value '%s', " .formatted( @@ -277,7 +308,7 @@ protected void init() throws IOException, TemplateException { ); } - final RelationSupport.ForeignRelation fRelation; + final ForeignRelation fRelation; try { fRelation = dsHandler().describeForeignRelation(dataSource(), sourceField, parsed[1].trim()); @@ -383,7 +414,7 @@ protected void init() throws IOException, TemplateException { final String fromClause = dataSource().getTableName(); // -- JOIN ON - final List foreignKeyRelations = dsHandler().getFields() + final List> foreignKeyRelations = dsHandler().getFields() .stream() .filter(dsf -> dsf.isIncludeField() /* @@ -400,32 +431,9 @@ protected void init() throws IOException, TemplateException { && !dsf.isMultiple() ) .map(dsf -> { - final RelationSupport.ImportFromRelation relation = dsHandler().describeImportFrom(dsf); + final ImportFromRelation relation = dsHandler().getImportFromRelation(dsf); return relation.foreignKeyRelations(); }) - .flatMap( Collection::stream /*fkrls -> fkrls.stream()*/ ) - - /* It is required to generate one join per unique ForeignKeyRelation value */ - .filter(new Predicate<>() { - final List unique = new LinkedList<>(); - - @Override - public boolean test(RelationSupport.ForeignKeyRelation fkrl) { - - for (RelationSupport.ForeignKeyRelation f :unique) { - if ( - f.dataSource().equals(fkrl.dataSource()) - && f.sourceField().equals(fkrl.sourceField()) - && f.foreign().dataSource().equals(fkrl.foreign().dataSource()) - && f.foreign().field().equals(fkrl.foreign().field()) - ) { - return false; - } - } - unique.add(fkrl); - return true; - } - }) .collect(Collectors.toList()); final String joinClause = JDBCHandler.AbstractSQLContext.generateSQLJoin(foreignKeyRelations); @@ -479,10 +487,10 @@ public boolean test(RelationSupport.ForeignKeyRelation fkrl) { <#if effectiveAnsiJoinClause?has_content> ${effectiveAnsiJoinClause} - ) opaque + ) opaque <#if effectiveWhereClause?has_content> WHERE ${effectiveWhereClause} - + """; final String effectiveQuery = operationBinding() == null @@ -505,7 +513,7 @@ public boolean test(RelationSupport.ForeignKeyRelation fkrl) { } } - public static String fetchSummarized(RelationSupport.ImportFromRelation ifr) { + public static String fetchSummarized(ImportFromRelation ifr) { final DSField sourceField = ifr.sourceField(); if (!sourceField.isIncludeField() || !sourceField.isMultiple()) { @@ -561,7 +569,7 @@ public static String fetchSummarized(RelationSupport.ImportFromRelation ifr) { final ISQLForeignKeyRelation fkr; { - final RelationSupport.ForeignKeyRelation fkrO = ifr.toForeignKeyRelation(); + final ForeignKeyRelation fkrO = ifr.toForeignKeyRelation(); fkr = ISQLForeignKeyRelation.wrap( ifr.toForeignKeyRelation() ) @@ -584,7 +592,7 @@ public static String fetchSummarized(RelationSupport.ImportFromRelation ifr) { // -- generate join if it is ManyToMany and join table is provided if (fkr.sourceField().getJoinTable() != null) { - final String joinClause = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(fkr)); + final String joinClause = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(List.of(fkr))); sbld.append('\n') .append(joinClause); diff --git a/smartclient-core/src/main/java/org/srg/smartclient/runtime/DSRuntime.java b/smartclient-core/src/main/java/org/srg/smartclient/runtime/DSRuntime.java new file mode 100644 index 0000000..b85c176 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/runtime/DSRuntime.java @@ -0,0 +1,125 @@ +package org.srg.smartclient.runtime; + +import org.srg.smartclient.DSHandler; +import org.srg.smartclient.IHandler; +import org.srg.smartclient.RelationSupport; +import org.srg.smartclient.isomorphic.DSField; +import org.srg.smartclient.isomorphic.DataSource; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class DSRuntime implements IDSRuntime { + private DSRuntime() {} + + Map handlers = new HashMap<>(); + Map> fkRelations = new HashMap<>(); + Map> ifRelations = new HashMap<>(); + + @Override + public IHandler getHandlerById(String id) { + return handlers.get(id); + } + + /** + * Creates a new instance with combined set of handlers (Copy on write) + * + * @param handlers + * @return new instance + */ + public synchronized DSRuntime registerHandlers(IHandler... handlers) { + final DSRuntime r = new DSRuntime(); + r.handlers.putAll(this.handlers); + r.fkRelations.putAll(this.fkRelations); + r.ifRelations.putAll(this.ifRelations); + + for(IHandler h: handlers) { + r.handlers.put(h.id(), h); + } + + for(IHandler h: handlers) { + if (h instanceof DSHandler dsh) { + r.initDSRuntime(dsh.dataSource()); + } + } + + return r; + } + + @Override + public RelationSupport.ForeignKeyRelation getForeignKeyRelation(String dsId, String fieldName) { + final Map fks = fkRelations.get(dsId); + if (fks == null) { + throw new IllegalArgumentException("Nothing known about DataSource with id '%s'".formatted(dsId)); + } + + final RelationSupport.ForeignKeyRelation fkr = fks.get(fieldName); + if (fkr == null) { + throw new IllegalArgumentException("Nothing known about ForeignKey relation for '%s.%s'".formatted(dsId, fieldName)); + } + + return fkr; + } + + @Override + public RelationSupport.ImportFromRelation getImportFromRelation(String dsId, String fieldName){ + final Map fks = ifRelations.get(dsId); + if (fks == null) { + throw new IllegalArgumentException("Nothing known about DataSource with id '%s'".formatted(dsId)); + } + + final RelationSupport.ImportFromRelation fkr = fks.get(fieldName); + if (fkr == null) { + throw new IllegalArgumentException("Nothing known about Foreign ImportFrom relation for '%s.%s'".formatted(dsId, fieldName)); + } + + return fkr; + } + + protected void initDSRuntime(DataSource ds) { + for(DSField f: ds.getFields()) { + Map fks = this.fkRelations.get(ds.getId()); + if (fks == null) { + fks = new HashMap<>(); + this.fkRelations.put(ds.getId(), fks); + } + + Map ifs = this.ifRelations.get(ds.getId()); + if (ifs == null) { + ifs = new HashMap<>(); + this.ifRelations.put(ds.getId(), ifs); + } + + if (f.getForeignKey() != null && !f.getForeignKey().isBlank()) { + fks.put(f.getName(), RelationSupport.describeForeignKey(this, ds, f)); + } + + if (f.isIncludeField()) { + ifs.put(f.getName(), RelationSupport.describeImportFrom(this, ds, f)); + } + } + + } + + public static DSRuntime create(Iterable handlers) { + final DSRuntime r = new DSRuntime(); + + for (IHandler h :handlers) { + r.handlers.put(h.id(), h); + } + + for (IHandler h : r.handlers.values()) { + if ( h instanceof DataSource ds) { + r.initDSRuntime(ds); + } + } + + return r; + } + + @Override + public Iterator iterator() { + return handlers.values().iterator(); + } +} diff --git a/smartclient-core/src/main/java/org/srg/smartclient/runtime/IDSRuntime.java b/smartclient-core/src/main/java/org/srg/smartclient/runtime/IDSRuntime.java new file mode 100644 index 0000000..b4329d7 --- /dev/null +++ b/smartclient-core/src/main/java/org/srg/smartclient/runtime/IDSRuntime.java @@ -0,0 +1,10 @@ +package org.srg.smartclient.runtime; + +import org.srg.smartclient.IDSLookup; +import org.srg.smartclient.IHandler; +import org.srg.smartclient.RelationSupport; + +public interface IDSRuntime extends IDSLookup, Iterable { + RelationSupport.ForeignKeyRelation getForeignKeyRelation(String dsId, String fieldName); + RelationSupport.ImportFromRelation getImportFromRelation(String dsId, String fieldName); +} diff --git a/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java b/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java index 2ae3230..a2094c7 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java @@ -7,6 +7,7 @@ import org.mockito.Mockito; import org.srg.smartclient.isomorphic.DSField; import org.srg.smartclient.isomorphic.DataSource; +import org.srg.smartclient.runtime.IDSRuntime; import org.srg.smartclient.utils.Serde; import java.lang.reflect.Constructor; @@ -201,7 +202,7 @@ protected static class ExtraFieldBase { } - protected IDSRegistry dsRegistry; + protected IDSRuntime dsRuntime; protected H handler; @BeforeAll @@ -211,14 +212,14 @@ public static void setupDB() { @BeforeEach public void setupMockitoHooks() throws Exception { - dsRegistry = Mockito.mock(IDSRegistry.class); + dsRuntime = Mockito.mock(IDSRuntime.class); handler = withHandlers(Handler.Employee); } @AfterEach public void unsetMockitoHooks() { - Mockito.reset(dsRegistry); - dsRegistry = null; + Mockito.reset(dsRuntime); + dsRuntime = null; Mockito.reset(handler); handler = null; @@ -243,29 +244,49 @@ protected H doInitHandler(DataSource ds) throws Exception { .when(handler) .dataSource(); } else { - final Constructor constructor = hc.getConstructor(JDBCHandler.JDBCPolicy.class, IDSRegistry.class, DataSource.class); - final H instance = constructor.newInstance(getJDJdbcPolicy(), dsRegistry, ds); + final Constructor constructor = hc.getConstructor(JDBCHandler.JDBCPolicy.class, IDSRuntime.class, DataSource.class); + final H instance = constructor.newInstance(getJDJdbcPolicy(), dsRuntime, ds); handler = Mockito.spy(instance); } Mockito.doReturn(ds) - .when(dsRegistry) + .when(dsRuntime) .getDataSourceById( Mockito.matches(ds.getId()) ); Mockito.doReturn(handler) - .when(dsRegistry) + .when(dsRuntime) .getDataSourceHandlerById( Mockito.matches(ds.getId()) ); Mockito.doReturn(handler) - .when(dsRegistry) - .getHandlerByName( + .when(dsRuntime) + .getHandlerById( Mockito.matches(ds.getId()) ); + Mockito.when(dsRuntime.getImportFromRelation(Mockito.matches(ds.getId()),Mockito.anyString())) + .thenAnswer( (inv) -> { + String dsId = inv.getArgument(0); + assert dsId.equals(ds.getId()); + + String fieldName = inv.getArgument(1); + DSField dsf = ds.getField(fieldName); + return RelationSupport.describeImportFrom(dsRuntime, ds, dsf); + }); + + Mockito.when(dsRuntime.getForeignKeyRelation(Mockito.matches(ds.getId()),Mockito.anyString())) + .thenAnswer( (inv) -> { + String dsId = inv.getArgument(0); + assert dsId.equals(ds.getId()); + + String fieldName = inv.getArgument(1); + DSField dsf = ds.getField(fieldName); + return RelationSupport.describeForeignKey(dsRuntime, ds, dsf); + }); + return handler; } diff --git a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchIncludeFromTest.java b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchIncludeFromTest.java index 611e3b5..965e9e5 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchIncludeFromTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchIncludeFromTest.java @@ -7,6 +7,8 @@ import org.srg.smartclient.isomorphic.DSRequest; import org.srg.smartclient.isomorphic.DSResponse; +import java.util.List; + import static org.hamcrest.Matchers.equalToCompressingWhiteSpace; public class JDBCHandlerFetchIncludeFromTest extends AbstractJDBCHandlerTest { @@ -41,11 +43,11 @@ public void directIncludeFrom_without_includeVia() throws Exception { ); // -- Check generated SQL join clause - final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(ifr.foreignKeyRelations()); + final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(ifr.foreignKeyRelations())); MatcherAssert.assertThat( sqlJoin, equalToCompressingWhiteSpace(""" - LEFT JOIN locations ON employee.location_id = locations.id + LEFT JOIN locations location_locations ON employee.location_id = location_locations.id """ ) ); @@ -109,15 +111,15 @@ public void indirectIncludeFrom_without_includeVia() throws Exception { - final RelationSupport.ImportFromRelation ifr = h.describeImportFrom(includeFrom); + final RelationSupport.ImportFromRelation ifr = h.getImportFromRelation(includeFrom); // -- Check generated SQL join clause - final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(ifr.foreignKeyRelations()); + final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(ifr.foreignKeyRelations())); MatcherAssert.assertThat( sqlJoin, equalToCompressingWhiteSpace(""" - LEFT JOIN locations ON employee.location_id = locations.id - LEFT JOIN countries ON locations.country_id = countries.id + LEFT JOIN locations location_locations ON employee.location_id = location_locations.id + LEFT JOIN countries country_countries ON location_locations.country_id = country_countries.id """ ) ); @@ -153,6 +155,120 @@ public void indirectIncludeFrom_without_includeVia() throws Exception { }""", response); } + @Test + @Regression(""" + org.srg.smartclient.utils.ContextualRuntimeException: SQL count query execution failed. + Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Ambiguous column name "id"; SQL statement: + SELECT count(*) FROM ( + SELECT project.id AS id_project, + project.name AS name_project, + project.manager_id AS manager_id_project, + CONCAT(employee.id, '_', employee.name) AS calculated_employee, + project.supervisor_id AS supervisor_id_project, + CONCAT(employee.id, '_', employee.name) AS calculated_employee + FROM project + LEFT JOIN employee ON project.manager_id = employee.id + LEFT JOIN employee ON project.supervisor_id = employee.id + ) opaque + """) + public void multipleDirectIncludeFromTheSameDisplayField_without_includeVia() throws Exception { + final JDBCHandler h = RelationSupportTest.IncludeFrom_TestCases.Two_Direct_Same_The_Same_DisplayField_With_IncludeVia.apply(this); + + { + final DSField includeFrom = h.getField("managerFullName"); + + final RelationSupport.ForeignRelation frl = h.determineEffectiveField(includeFrom); + JsonTestSupport.assertJsonEquals(""" + { + dataSource:'EmployeeDS', + field: 'name', + sqlFieldAlias:null + }""", + frl, + Option.IGNORING_EXTRA_FIELDS + ); + + final RelationSupport.ImportFromRelation ifr = h.getImportFromRelation(includeFrom); + + // -- Check generated SQL join clause + final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(ifr.foreignKeyRelations())); + + MatcherAssert.assertThat( sqlJoin, + equalToCompressingWhiteSpace(""" + LEFT JOIN employee manager_employee ON project.manager_id = manager_employee.id + """ + ) + ); + } + + { + final DSField includeFrom = h.getField("supervisorFullName"); + + final RelationSupport.ForeignRelation frl = h.determineEffectiveField(includeFrom); + JsonTestSupport.assertJsonEquals(""" + { + dataSource:'EmployeeDS', + field: 'name', + sqlFieldAlias:null + }""", + frl, + Option.IGNORING_EXTRA_FIELDS + ); + + final RelationSupport.ImportFromRelation ifr = h.getImportFromRelation(includeFrom); + + // -- Check generated SQL join clause + final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(ifr.foreignKeyRelations())); + + MatcherAssert.assertThat( sqlJoin, + equalToCompressingWhiteSpace(""" + LEFT JOIN employee supervisor_employee ON project.supervisor_id = supervisor_employee.id + """ + ) + ); + } + + final DSRequest request = new DSRequest(); + request.setStartRow(0); + request.setEndRow(3); + + final DSResponse response = h.handleFetch(request); + + JsonTestSupport.assertJsonEquals(""" + { + status:0, + startRow:0, + endRow:3, + totalRows:5, + data:[ + { + id:1, + manager:4, + managerFullName:'manager1', + name:'Project 1 for client 1', + supervisor:5, + supervisorFullName:'manager2' + }, + { + id:2, + manager:4, + managerFullName:'manager1', + name:'Project 2 for client 1', + supervisor:5, + supervisorFullName:'manager2' + }, + { + id:3, + manager:5, + managerFullName:'manager2', + name:'Project 1 for client 2', + supervisor:4, + supervisorFullName:'manager1' + } + ] + }""", response); + } + @Test public void indirectIncludeFrom_with_includeVia() throws Exception { final JDBCHandler h = RelationSupportTest.IncludeFrom_TestCases.Indirect_With_IncludeVia.apply(this); @@ -176,15 +292,15 @@ public void indirectIncludeFrom_with_includeVia() throws Exception { - final RelationSupport.ImportFromRelation ifr = h.describeImportFrom(includeFrom); + final RelationSupport.ImportFromRelation ifr = h.getImportFromRelation(includeFrom); // -- Check generated SQL join clause - final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(ifr.foreignKeyRelations()); + final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(ifr.foreignKeyRelations())); MatcherAssert.assertThat( sqlJoin, equalToCompressingWhiteSpace(""" - LEFT JOIN locations ON employee.location_id = locations.id - LEFT JOIN countries ON locations.country_id = countries.id + LEFT JOIN locations location_locations ON employee.location_id = location_locations.id + LEFT JOIN countries country_countries ON location_locations.country_id = country_countries.id """ ) ); @@ -314,7 +430,7 @@ public void fetchOneToMany_EntireEntity_WithForeignDisplayField() throws Excepti Option.IGNORING_EXTRA_FIELDS ); - final RelationSupport.ImportFromRelation ifr = h.describeImportFrom(includeFrom); + final RelationSupport.ImportFromRelation ifr = h.getImportFromRelation(includeFrom); final DSResponse response = h.handleFetch(request); @@ -382,10 +498,10 @@ public void fetchManyToMany_EntireEntity_WithForeignDisplayField() throws Except - final RelationSupport.ImportFromRelation ifr = h.describeImportFrom(includeFrom); + final RelationSupport.ImportFromRelation ifr = h.getImportFromRelation(includeFrom); // -- Check generated SQL join clause - final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(ifr.foreignKeyRelations()); + final String sqlJoin = JDBCHandler.AbstractSQLContext.generateSQLJoin(List.of(ifr.foreignKeyRelations())); MatcherAssert.assertThat( sqlJoin, equalToCompressingWhiteSpace(""" diff --git a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchTest.java b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchTest.java index f3cdc41..3f062e6 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/JDBCHandlerFetchTest.java @@ -475,7 +475,7 @@ public void fetchOneToMany_EntireEntity() throws Exception { { id:1, name:'admin', - calculated:'1_admin', + calculated:'1_admin', roles:[ { role:'Admin', @@ -492,7 +492,7 @@ public void fetchOneToMany_EntireEntity() throws Exception { { id:2, name:'developer', - calculated:'2_developer', + calculated:'2_developer', roles:[ { role:'Developer', @@ -504,13 +504,13 @@ public void fetchOneToMany_EntireEntity() throws Exception { { id:3, name:'UseR3', - calculated:'3_UseR3', + calculated:'3_UseR3', roles:[] }, { id:4, name:'manager1', - calculated:'4_manager1', + calculated:'4_manager1', roles:[ { role:'PM', @@ -522,7 +522,7 @@ public void fetchOneToMany_EntireEntity() throws Exception { { id:5, name:'manager2', - calculated:'5_manager2', + calculated:'5_manager2', roles:[ { role:'PM', @@ -534,7 +534,7 @@ public void fetchOneToMany_EntireEntity() throws Exception { { id:6, name:'user2', - calculated:'6_user2', + calculated:'6_user2', roles:[ ] } diff --git a/smartclient-core/src/test/java/org/srg/smartclient/JsonTestSupport.java b/smartclient-core/src/test/java/org/srg/smartclient/JsonTestSupport.java index 483124f..f721096 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/JsonTestSupport.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/JsonTestSupport.java @@ -204,6 +204,7 @@ public void serialize(RelationSupport.ForeignRelation value, JsonGenerator gen, gen.writeStringField("fieldName", value.fieldName()); } + gen.writeObjectField("relatedTableAlias", value.getRelatedTableAlias()); gen.writeStringField("sqlFieldAlias", value.getSqlFieldAlias()); gen.writeEndObject(); @@ -218,9 +219,9 @@ public void serialize(RelationSupport.ImportFromRelation value, JsonGenerator ge gen.writeStringField("dataSource", value.dataSource().getId()); gen.writeObjectField("sourceField", value.sourceField().getName()); +// gen.writeObjectField("relatedTableAlias", value.relatedTableAlias()); gen.writeObjectField("foreignKeyRelations", value.foreignKeyRelations()); - gen.writeObjectField("foreignDisplay", value.foreignDisplay().getName()); gen.writeEndObject(); diff --git a/smartclient-core/src/test/java/org/srg/smartclient/RelationSupportTest.java b/smartclient-core/src/test/java/org/srg/smartclient/RelationSupportTest.java index 986190b..be84b44 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/RelationSupportTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/RelationSupportTest.java @@ -1,6 +1,8 @@ package org.srg.smartclient; +import net.javacrumbs.jsonunit.core.Option; import org.junit.jupiter.api.Test; +import org.srg.smartclient.isomorphic.DSField; class RelationSupportTest extends AbstractHandlerTest { @@ -16,7 +18,7 @@ public H apply(AbstractHandlerTest test) throws Excepti name:'location', foreignKey:'LocationDS.id', dbName:'location_id', - displayField: 'location_city' + displayField: 'location_city' }, { name:'location_city', @@ -27,6 +29,48 @@ public H apply(AbstractHandlerTest test) throws Excepti ); } }, + + Two_Direct_Same_The_Same_DisplayField_With_IncludeVia(){ + @Override + public H apply(AbstractHandlerTest test) throws Exception { + final H projectHandler = test.withHandlers(Handler.Project); + + withExtraFields(projectHandler, + """ + [ + { + name:"manager" + ,foreignKey:"EmployeeDS.id" + ,displayField:"managerFullName" + ,foreignDisplayField:"fullName" + ,dbName:"manager_id" + }, + { + name:"managerFullName" + ,type:"TEXT" + ,includeFrom:"EmployeeDS.name" + ,includeVia:"manager" + }, + + { + name:"supervisor" + ,foreignKey:"EmployeeDS.id" + ,displayField:"supervisorFullName" + ,foreignDisplayField:"fullName" + ,dbName:"supervisor_id" + }, + { + name:"supervisorFullName" + ,type:"TEXT" + ,includeFrom:"EmployeeDS.name" + ,includeVia:"supervisor" + } + + ]"""); + return projectHandler; + } + }, + Direct_With_IncludeVia() { @Override public H apply(AbstractHandlerTest test) throws Exception { @@ -50,7 +94,7 @@ public H apply(AbstractHandlerTest test) throws Excepti includeFrom:'EmployeeRoleDS.role', includeSummaryFunction:'CONCAT', multiple:true - }] + }] """, ExtraFieldBase.Employee_RolesFromEmployeeRole ); @@ -75,7 +119,7 @@ public H apply(AbstractHandlerTest test) throws Excepti multiple:true, includeSummaryFunction: 'CONCAT', customSQL:false - }] + }] """, ExtraFieldBase.Project_IncludeTeamMembersFromFromEmployee ); @@ -158,7 +202,7 @@ public void importFromRelation_directIncludeFrom_without_includeVia() throws Exc final DSHandler h = IncludeFrom_TestCases.Direct_Without_IncludeVia.apply(this); final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( - dsRegistry, + dsRuntime, h.dataSource(), h.dataSource().getField("location_city") ); @@ -175,7 +219,8 @@ public void importFromRelation_directIncludeFrom_without_includeVia() throws Exc foreign:{ dataSource:'LocationDS', field: 'id', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias:'location_locations' } } ], @@ -190,7 +235,7 @@ public void importFromRelation_indirectIncludeFrom_without_includeVia() throws E final DSHandler h = IncludeFrom_TestCases.Indirect_Without_IncludeVia.apply(this); final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( - dsRegistry, + dsRuntime, h.dataSource(), h.dataSource().getField("location_country") ); @@ -207,7 +252,8 @@ public void importFromRelation_indirectIncludeFrom_without_includeVia() throws E foreign:{ dataSource:'LocationDS', field:'id', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias:'location_locations' }, isInverse:false }, @@ -217,7 +263,8 @@ public void importFromRelation_indirectIncludeFrom_without_includeVia() throws E foreign:{ dataSource:'CountryDS', field: 'id', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias:'country_countries' }, isInverse:false } @@ -232,7 +279,7 @@ public void importFromRelation_indirectIncludeFrom_with_includeVia() throws Exce final DSHandler h = IncludeFrom_TestCases.Indirect_With_IncludeVia.apply(this); final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( - dsRegistry, + dsRuntime, h.dataSource(), h.dataSource().getField("location_country") ); @@ -249,7 +296,8 @@ public void importFromRelation_indirectIncludeFrom_with_includeVia() throws Exce foreign:{ dataSource:'LocationDS', field: 'id', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias:'location_locations' }, isInverse:false }, @@ -259,7 +307,8 @@ public void importFromRelation_indirectIncludeFrom_with_includeVia() throws Exce foreign:{ dataSource:'CountryDS', field: 'id', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias:'country_countries' }, isInverse:false } @@ -274,7 +323,7 @@ public void importFromRelation_directIncludeFrom_with_includeVia() throws Except final DSHandler h = IncludeFrom_TestCases.Direct_With_IncludeVia.apply(this); final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( - dsRegistry, + dsRuntime, h.dataSource(), h.dataSource().getField("employeeFullName") ); @@ -292,7 +341,8 @@ public void importFromRelation_directIncludeFrom_with_includeVia() throws Except foreign: { dataSource: 'EmployeeDS', field: 'id', - sqlFieldAlias: null + sqlFieldAlias: null, + relatedTableAlias: 'manager_employee' } } ] @@ -305,7 +355,7 @@ public void importFromRelation_directIncludeFrom_multiple_with_includeVia() thro final DSHandler h = IncludeFrom_TestCases.Direct_Multiple_ManyToMany_With_IncludeVia.apply(this); final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( - dsRegistry, + dsRuntime, h.dataSource(), h.dataSource().getField("employeeName") ); @@ -323,7 +373,8 @@ public void importFromRelation_directIncludeFrom_multiple_with_includeVia() thro foreign: { dataSource: 'EmployeeDS', field: 'id', - sqlFieldAlias: null + sqlFieldAlias: null, + relatedTableAlias: 'teamMembers_employee' } } ] @@ -339,7 +390,8 @@ public void importFromRelation_directIncludeFrom_multiple_with_includeVia() thro foreign: { dataSource:'EmployeeDS', field:'id', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias: null } }""", fkr ); @@ -353,12 +405,91 @@ public void importFromRelation_directIncludeFrom_multiple_with_includeVia() thro foreign: { dataSource:'EmployeeDS', field:'name', - sqlFieldAlias:null + sqlFieldAlias:null, + relatedTableAlias: null } }""", fkrDisplay ); } + @Test + public void importFromRelation_multipleDirectIncludeFromTheSameTable_with_includeVia() throws Exception { + final DSHandler h = IncludeFrom_TestCases.Two_Direct_Same_The_Same_DisplayField_With_IncludeVia.apply(this); + { + final DSField includeFrom = h.dataSource().getField("managerFullName"); + JsonTestSupport.assertJsonEquals(""" + { + name: 'managerFullName', + includeFrom: 'EmployeeDS.name', + includeVia: 'manager' + }""", includeFrom, Option.IGNORING_EXTRA_FIELDS); + + + final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( + dsRuntime, + h.dataSource(), + includeFrom + ); + + JsonTestSupport.assertJsonEquals(""" + { + dataSource: 'ProjectDS', + sourceField: 'managerFullName', + foreignDisplay: 'name', + foreignKeyRelations: [ + { + dataSource: 'ProjectDS', + sourceField: 'manager', + isInverse: false, + foreign: { + dataSource: 'EmployeeDS', + field: 'id', + sqlFieldAlias: null, + relatedTableAlias: 'manager_employee' + } + } + ] + }""", ifr + ); + } + + final DSField includeFrom = h.dataSource().getField("supervisorFullName"); + JsonTestSupport.assertJsonEquals(""" + { + name: 'supervisorFullName', + includeFrom: 'EmployeeDS.name', + includeVia: 'supervisor' + }""", includeFrom, Option.IGNORING_EXTRA_FIELDS); + + + final RelationSupport.ImportFromRelation ifr = RelationSupport.describeImportFrom( + dsRuntime, + h.dataSource(), + includeFrom + ); + + JsonTestSupport.assertJsonEquals(""" + { + dataSource: 'ProjectDS', + sourceField: 'supervisorFullName', + foreignDisplay: 'name', + foreignKeyRelations: [ + { + dataSource: 'ProjectDS', + sourceField: 'supervisor', + isInverse: false, + foreign: { + dataSource: 'EmployeeDS', + field: 'id', + sqlFieldAlias: null, + relatedTableAlias: 'supervisor_employee' + } + } + ] + }""", ifr + ); + } + @Override protected Class getHandlerClass() { return DSHandler.class; 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 709483b..28e382c 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 @@ -503,8 +503,8 @@ public List changeProperties(SerializationConfig config, Bea @Test public void oneToOneRelation() { - dispatcher.registerJPAEntity(Client.class); - final String clientDataDsId = dispatcher.registerJPAEntity(ClientData.class); + dispatcher.registerJPAEntities(Client.class, ClientData.class, Project.class, Employee.class, EmployeeStatus.class); + final String clientDataDsId = dispatcher.getHandlerByClass(ClientData.class).id(); final DSRequest request = new DSRequest(); request.setStartRow(0); @@ -541,8 +541,8 @@ public void oneToOneRelation() { @Test public void oneToOneRelationMappedBy() { - final String clientDsId = dispatcher.registerJPAEntity(Client.class); - dispatcher.registerJPAEntity(ClientData.class); + dispatcher.registerJPAEntities(Client.class, ClientData.class, Project.class, Employee.class, EmployeeStatus.class); + final String clientDsId = dispatcher.getHandlerByClass(Client.class).id(); final DSRequest request = new DSRequest(); request.setStartRow(0); @@ -637,9 +637,8 @@ public void oneToOneRelationMappedBy() { @Test public void manyToOneRelation() { - dispatcher.registerJPAEntity(Employee.class); - dispatcher.registerJPAEntity(Client.class); - final String projectDsId = dispatcher.registerJPAEntity(Project.class); + dispatcher.registerJPAEntities(EmployeeStatus.class, Employee.class, Client.class, Project.class); + final String projectDsId = dispatcher.getHandlerByClass(Project.class).id(); final DSRequest request = new DSRequest(); request.setStartRow(0); @@ -692,9 +691,8 @@ public void manyToOneRelation() { @Test public void oneToManyRelation() { - final String clientDsId = dispatcher.registerJPAEntity(Client.class); - dispatcher.registerJPAEntity(Employee.class); - dispatcher.registerJPAEntity(Project.class); + dispatcher.registerJPAEntities(Client.class, Employee.class, EmployeeStatus.class, Project.class); + final String clientDsId = dispatcher.getHandlerByClass(Client.class).id(); final DSRequest request = new DSRequest(); request.setStartRow(0); @@ -761,8 +759,8 @@ public void oneToManyRelation() { @Test public void oneToManyRelationWithCompositeForeignKey() { - final String employeeDsId = dispatcher.registerJPAEntity(Employee.class); - dispatcher.registerJPAEntity(EmployeeRole.class); + dispatcher.registerJPAEntities(Employee.class, EmployeeStatus.class, EmployeeRole.class, Project.class, Client.class); + final String employeeDsId = dispatcher.getHandlerByClass(Employee.class).id(); // -- final DSRequest request = new DSRequest(); @@ -838,9 +836,7 @@ public void oneToManyRelationWithCompositeForeignKey() { @Test public void loadSqlDataSourceFromResource() throws Exception { - dispatcher.registerJPAEntity(Employee.class); - dispatcher.registerJPAEntity(Client.class); - dispatcher.registerJPAEntity(ClientData.class); + dispatcher.registerJPAEntities(Employee.class, Client.class, Project.class, ClientData.class, EmployeeStatus.class); final String hprjDSId = "HProjectDS"; dispatcher.loadFromResource("HProject.ds.json"); @@ -898,8 +894,8 @@ public void loadSqlDataSourceFromResource() throws Exception { @Test public void oneToMany_WithAssociationOverride() { - final String employeeDsId = dispatcher.registerJPAEntity(Employee.class); - dispatcher.registerJPAEntity(EmployeeStatus.class); + dispatcher.registerJPAEntities(Employee.class, EmployeeStatus.class, Project.class, Client.class); + final String employeeDsId = dispatcher.getHandlerByClass(Employee.class).id(); // -- final DSRequest request = new DSRequest(); @@ -1036,11 +1032,8 @@ public void oneToMany_WithAssociationOverride() { @Test public void fetchManyToMany_MappedBy() { - dispatcher.registerJPAEntity(Project.class); - dispatcher.registerJPAEntity(Client.class); - dispatcher.registerJPAEntity(ClientData.class); - - final String employeeDs = dispatcher.registerJPAEntity(Employee.class); + dispatcher.registerJPAEntities(Project.class, Client.class, ClientData.class, EmployeeStatus.class, Employee.class); + final String employeeDs = dispatcher.getHandlerByClass(Employee.class).id(); // -- final DSRequest request = new DSRequest(); @@ -1122,11 +1115,8 @@ public void fetchManyToMany_MappedBy() { } @Test public void fetchManyToMany() { - dispatcher.registerJPAEntity(Employee.class); - dispatcher.registerJPAEntity(Client.class); - dispatcher.registerJPAEntity(ClientData.class); - - final String projectDs = dispatcher.registerJPAEntity(Project.class); + dispatcher.registerJPAEntities(Employee.class, Client.class, ClientData.class, Project.class, EmployeeStatus.class); + final String projectDs = dispatcher.getHandlerByClass(Project.class).id(); // -- final DSRequest request = new DSRequest(); diff --git a/smartclient-core/src/test/resources/db/V1_0__init_test_schema.sql b/smartclient-core/src/test/resources/db/V1_0__init_test_schema.sql index ef7b31e..a084898 100644 --- a/smartclient-core/src/test/resources/db/V1_0__init_test_schema.sql +++ b/smartclient-core/src/test/resources/db/V1_0__init_test_schema.sql @@ -86,6 +86,7 @@ CREATE TABLE project id INT NOT NULL, client_id INT, manager_id INT NULL, + supervisor_id INT NULL, name VARCHAR(255) NOT NULL, CONSTRAINT pkProjects @@ -95,7 +96,10 @@ CREATE TABLE project FOREIGN KEY (client_id) REFERENCES client (id), CONSTRAINT fkProject_ManagerId - FOREIGN KEY (manager_id) REFERENCES employee (id) + FOREIGN KEY (manager_id) REFERENCES employee (id), + + CONSTRAINT fkProject_SupervisorId + FOREIGN KEY (supervisor_id) REFERENCES employee (id) ); CREATE TABLE project_team @@ -179,11 +183,11 @@ INSERT INTO client VALUES (2, 'client 2'); INSERT INTO client_data VALUES (1, 'Data1: client 1', 1); INSERT INTO client_data VALUES (2, 'Data2: client 2', 2); -INSERT INTO project VALUES (1, 1, 4, 'Project 1 for client 1'); -INSERT INTO project VALUES (2, 1, 4, 'Project 2 for client 1'); -INSERT INTO project VALUES (3, 2, 5, 'Project 1 for client 2'); -INSERT INTO project VALUES (4, 2, 5, 'Project 2 for client 2'); -INSERT INTO project VALUES (5, 2, 5, 'Project 3 for client 2'); +INSERT INTO project VALUES (1, 1, 4, 5, 'Project 1 for client 1'); +INSERT INTO project VALUES (2, 1, 4, 5, 'Project 2 for client 1'); +INSERT INTO project VALUES (3, 2, 5, 4, 'Project 1 for client 2'); +INSERT INTO project VALUES (4, 2, 5, 4, 'Project 2 for client 2'); +INSERT INTO project VALUES (5, 2, 5, null, 'Project 3 for client 2'); INSERT INTO project_team VALUES (1, 1); INSERT INTO project_team VALUES (1, 2); diff --git a/smartclient-spring/src/main/java/org/srg/smartclient/spring/AutomaticDSHandlerRegistrar.java b/smartclient-spring/src/main/java/org/srg/smartclient/spring/AutomaticDSHandlerRegistrar.java index 5f83942..4431bbf 100644 --- a/smartclient-spring/src/main/java/org/srg/smartclient/spring/AutomaticDSHandlerRegistrar.java +++ b/smartclient-spring/src/main/java/org/srg/smartclient/spring/AutomaticDSHandlerRegistrar.java @@ -153,11 +153,6 @@ public void stop(Runnable callback) { callback.run(); } - private void registerJpaEntity(Class entityClass) throws Exception { - final JpaDSDispatcher jpaDSDispatcher = getJpaDSDispatcher(); - jpaDSDispatcher.registerJPAEntity(entityClass ); - } - protected void register(ListableBeanFactory beanFactory) throws Exception { final Set scans = new HashSet<>(); beanFactory.getBeansWithAnnotation(ComponentScan.class).forEach((name, instance) -> { @@ -229,45 +224,32 @@ protected void register(ListableBeanFactory beanFactory) throws Exception { logger.debug(msg); - // -- register - final HashSet registrationFailed = new HashSet<>(); - - for (Class c :entitiesFound) { - try { - registerJpaEntity(c); - } catch (Throwable t) { - registerFailedClass( - this.classesFailed, - c.getCanonicalName(), - "Class '%s' failed to be registered and will be skipped" - .formatted(c.getCanonicalName()), - t - ); - - registrationFailed.add(c); - } - } + if (!entitiesFound.isEmpty()) { + this.getJpaDSDispatcher().registerJPAEntities(entitiesFound.toArray(new Class[0])); - final int ignoredQnt = classesSkipped.size() + classesFailed.size(); - final String msg2 = "\nSmartClient JPA Handler Registration was completed, %d persistable entities were registered and %d were ignored:\n" - .formatted(entitiesFound.size() - classesFailed.size(), ignoredQnt) + + final int ignoredQnt = classesSkipped.size() + classesFailed.size(); + final String msg2 = "\nSmartClient JPA Handler Registration has been completed, %d JPA entities were registered and %d were ignored:\n" + .formatted(entitiesFound.size() - classesFailed.size(), ignoredQnt) + - entitiesFound.stream() - .map(Class::getCanonicalName) - .collect(Collectors.joining("\n ")) + + entitiesFound.stream() + .map(Class::getCanonicalName) + .collect(Collectors.joining("\n ")) + - classesSkipped.stream() - .map( s -> "\n [Skipped] %s, reason: %s".formatted(s.one, s.two)) - .collect(Collectors.joining()) + + classesSkipped.stream() + .map(s -> "\n [Skipped] %s, reason: %s".formatted(s.one, s.two)) + .collect(Collectors.joining()) + - classesFailed.stream() - .map( s -> "\n [Failed] %s, reason: %s".formatted(s.one, s.two)) - .collect(Collectors.joining()); + classesFailed.stream() + .map(s -> "\n [Failed] %s, reason: %s".formatted(s.one, s.two)) + .collect(Collectors.joining()); - if ( ignoredQnt <= 0) { - logger.info(msg2); + if (ignoredQnt <= 0) { + logger.info(msg2); + } else { + logger.warn(msg2); + } } else { - logger.warn(msg2); + logger.info("\nSmartClient JPA Handler Registration has been completed, no JPA entities were found"); } } diff --git a/smartclient-spring/src/main/java/org/srg/smartclient/spring/autoconfiguration/DMIBeanPostProcessor.java b/smartclient-spring/src/main/java/org/srg/smartclient/spring/autoconfiguration/DMIBeanPostProcessor.java index 8beee18..85ce157 100644 --- a/smartclient-spring/src/main/java/org/srg/smartclient/spring/autoconfiguration/DMIBeanPostProcessor.java +++ b/smartclient-spring/src/main/java/org/srg/smartclient/spring/autoconfiguration/DMIBeanPostProcessor.java @@ -84,7 +84,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw this.logger.info("{}: processing bean of type {}", this.getClass().getSimpleName(), proxy.getClass().getName()); final BeanDMIHandler h = new BeanDMIHandler(annotation.id(), bean, annotation.methodName() ); - dispatcher.registerHandler(h); + dispatcher.registerHandlers(h); } return bean; } diff --git a/smartclient-spring/src/test/java/org/srg/smartclient/SpringDMITest.java b/smartclient-spring/src/test/java/org/srg/smartclient/SpringDMITest.java index 4318fb8..948524f 100644 --- a/smartclient-spring/src/test/java/org/srg/smartclient/SpringDMITest.java +++ b/smartclient-spring/src/test/java/org/srg/smartclient/SpringDMITest.java @@ -66,7 +66,7 @@ public static void setupMapper() { @Test public void test() throws Exception { - final IHandler handler = dispatcher.getHandlerByName("dmi1"); + final IHandler handler = dispatcher.getHandlerById("dmi1"); Assert.notNull(handler, "DMI handler is not registered"); final DSRequest request = new DSRequest(); From 4a4661cb6457531edafd4e0699b9f5ab6e3408d9 Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Wed, 18 Aug 2021 10:28:20 +0300 Subject: [PATCH 2/3] fix NPE on Null OperationBindig in AbstractSQLContext --- .../src/main/java/org/srg/smartclient/JDBCHandler.java | 2 +- .../java/org/srg/smartclient/isomorphic/OperationBinding.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 d87699d..74b4b3c 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/JDBCHandler.java @@ -942,7 +942,7 @@ public static abstract class AbstractSQLContext { public AbstractSQLContext(H dsHandler, DSRequest request, OperationBinding operationBinding) { this.request = request; - this.operationBinding = operationBinding; + this.operationBinding = operationBinding == null ? OperationBinding.empty : operationBinding; this.dsHandler = dsHandler; } 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 index 29347f6..433a387 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/OperationBinding.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/isomorphic/OperationBinding.java @@ -10,6 +10,8 @@ * */ public class OperationBinding { + public static final OperationBinding empty = new OperationBinding(); + private DSRequest.OperationType operationType; private String operationId = ""; From a540edacd390a5cad3a264cd1442ad3fbe6e53bc Mon Sep 17 00:00:00 2001 From: Sergey Galkin Date: Mon, 23 Aug 2021 11:42:53 +0300 Subject: [PATCH 3/3] fix improper SQL select query generation when defaultWhereClause is empty and it is used by a custom whereClause. --- .../org/srg/smartclient/SQLFetchContext.java | 8 +- .../srg/smartclient/isomorphic/DSRequest.java | 43 ++++++--- .../srg/smartclient/AbstractHandlerTest.java | 4 + .../smartclient/AbstractJDBCHandlerTest.java | 59 +++++++++++- .../srg/smartclient/OperationBindingTest.java | 91 ++++++++++++++++++- 5 files changed, 184 insertions(+), 21 deletions(-) diff --git a/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java b/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java index 59625bd..1ada9b2 100644 --- a/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java +++ b/smartclient-core/src/main/java/org/srg/smartclient/SQLFetchContext.java @@ -456,7 +456,13 @@ protected void init() throws IOException, TemplateException { // -- generate query { - this.templateContext = SQLTemplateEngine.createContext(request(), selectClause, fromClause, joinClause, whereClause, ""); + this.templateContext = SQLTemplateEngine.createContext( + request(), + selectClause, + fromClause, + joinClause, + whereClause == null || whereClause.isBlank() ? "TRUE = TRUE" : whereClause, + ""); templateContext.put("effectiveSelectClause", selectClause); 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 7e98563..93a66cb 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 @@ -85,111 +85,126 @@ public Boolean getUseStrictJSON() { return useStrictJSON; } - public void setUseStrictJSON(Boolean useStrictJSON) { + public DSRequest setUseStrictJSON(Boolean useStrictJSON) { this.useStrictJSON = useStrictJSON; + return this; } public String getDataSource() { return dataSource; } - public void setDataSource(String dataSource) { + public DSRequest setDataSource(String dataSource) { this.dataSource = dataSource; + return this; } public OperationType getOperationType() { return operationType; } - public void setOperationType(OperationType operationType) { + public DSRequest setOperationType(OperationType operationType) { this.operationType = operationType; + return this; } public String getOperationId() { return operationId; } - public void setOperationId(String operationId) { + public DSRequest setOperationId(String operationId) { this.operationId = operationId; + return this; } public String getComponentId() { return componentId; } - public void setComponentId(String componentId) { + public DSRequest setComponentId(String componentId) { this.componentId = componentId; + return this; } public int getStartRow() { return startRow; } - public void setStartRow(int startRow) { + public DSRequest setStartRow(int startRow) { this.startRow = startRow; + return this; } public int getEndRow() { return endRow; } - public void setEndRow(int endRow) { + public DSRequest setEndRow(int endRow) { this.endRow = endRow; + return this; } public TextMatchStyle getTextMatchStyle() { return textMatchStyle; } - public void setTextMatchStyle(TextMatchStyle textMatchStyle) { + public DSRequest setTextMatchStyle(TextMatchStyle textMatchStyle) { this.textMatchStyle = textMatchStyle; + return this; } public List getSortBy() { return sortBy; } - public void setSortBy(List sortBy) { + public DSRequest setSortBy(List sortBy) { this.sortBy = sortBy; + return this; } public IDSRequestData getData() { return data; } - public void setData(IDSRequestData data) { + public DSRequest setData(IDSRequestData data) { this.data = data; + return this; } - public void wrapAndSetData(Map data) { + public DSRequest wrapAndSetData(Map data) { this.data = new MapData(); if(this.data instanceof Map m){ m.putAll(data); } + + return this; } public Map getOldValues() { return oldValues; } - public void setOldValues(Map oldValues) { + public DSRequest setOldValues(Map oldValues) { this.oldValues = oldValues; + return this; } public String getOutputs() { return outputs; } - public void setOutputs(String outputs) { + public DSRequest setOutputs(String outputs) { this.outputs = outputs; + return this; } public String getAdditionalOutputs() { return additionalOutputs; } - public void setAdditionalOutputs(String additionalOutputs) { + public DSRequest setAdditionalOutputs(String additionalOutputs) { this.additionalOutputs = additionalOutputs; + return this; } public static class MapData extends HashMap implements IDSRequestData { diff --git a/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java b/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java index a2094c7..1b12f88 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/AbstractHandlerTest.java @@ -308,6 +308,10 @@ protected H withExtraFields(String... extraFields) { protected H withHandler(String dataSourceDefinition) throws Exception { final DataSource ds = JsonTestSupport.fromJSON(DataSource.class, dataSourceDefinition); + return withDataSource(ds); + } + + protected H withDataSource(DataSource ds ) throws Exception { return doInitHandler(ds); } 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 c540077..123949a 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/AbstractJDBCHandlerTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/AbstractJDBCHandlerTest.java @@ -2,14 +2,21 @@ import org.flywaydb.core.Flyway; import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.h2.Driver; import org.h2.jdbcx.JdbcDataSource; +import org.h2.tools.DeleteDbFiles; +import org.h2.tools.Server; +import org.hibernate.procedure.spi.ParameterRegistrationImplementor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.srg.smartclient.isomorphic.DataSource; import org.srg.smartclient.utils.Serde; +import java.io.File; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.SQLException; import java.util.TimeZone; @@ -105,6 +112,8 @@ protected static class ExtraField extends AbstractHandlerTest.ExtraFieldBase{ private ExtraField(){} } + private static File dbHome = new File("./target/db"); + private Server dbServer; protected JdbcDataSource jdbcDataSource = new JdbcDataSource(); private Connection connection; private JDBCHandler.JDBCPolicy jdbcPolicy; @@ -119,10 +128,10 @@ public static void setupObjectMapper() { public static void shutdownDB() { } - protected Void initDB() { + protected Void initDB(javax.sql.DataSource ds) { Flyway flyway = new Flyway( new FluentConfiguration() - .dataSource(jdbcDataSource) + .dataSource(ds) .locations("classpath:db") ); @@ -139,18 +148,54 @@ protected JDBCHandler.JDBCPolicy getJDJdbcPolicy() { }; } + private static Server startDBServer(String jdbcUrls) { + String url = jdbcUrls; + String port = "9092"; + String user = "sa"; + String password = "sa"; + try { + final Server dbServer; + if (url.contains("/mem:")) { + dbServer = Server.createTcpServer("-tcpPort", port, "-baseDir", dbHome.getAbsolutePath(), "-tcpAllowOthers"); + } else { + dbServer = Server.createTcpServer("-tcpPort", port, "-ifExists", "-baseDir", dbHome.getAbsolutePath(), "-tcpAllowOthers"); + } + + System.out.println("Starting embedded database on port " + dbServer.getPort() + " with url " + url); + dbServer.start(); + System.out.println("Embedded database started. Data stored in: " + dbHome.getAbsolutePath()); + + // Clear all db files (if any) from previous run + DeleteDbFiles.execute(dbHome.getAbsolutePath(), "test", true); + + if (!url.contains("/mem:")) { + // Create DB + String url2 = String.format("jdbc:h2:%s/test;USER=%s;PASSWORD=%s", dbHome.getAbsolutePath(), user, password); + DriverManager.registerDriver(new Driver()); + DriverManager.getConnection(url2).close(); + } + + return dbServer; + } catch (SQLException e) { + throw new IllegalStateException("Unable to start database", e); + } + } + @BeforeEach public void setupDataSources() throws Exception { // SET TZ to UTC, otherwise timestamps stored in DB as GMT will be treated as local timezone TimeZone.setDefault(TimeZone.getTimeZone("UTC")); - jdbcDataSource.setURL("jdbc:h2:mem:test:˜/test;DB_CLOSE_DELAY=0;AUTOCOMMIT=OFF;database_to_lower=true"); + final String url = "jdbc:h2:mem:test:˜/test;DB_CLOSE_DELAY=0;AUTOCOMMIT=OFF;database_to_lower=true;USER=sa;PASSWORD=sa"; +// final String url ="jdbc:h2:tcp://localhost:9092/test;DB_CLOSE_DELAY=0;AUTOCOMMIT=OFF;database_to_lower=true;USER=sa;PASSWORD=sa"; + dbServer = startDBServer(url); + jdbcDataSource.setURL(url); jdbcDataSource.setUser("sa"); jdbcDataSource.setPassword("sa"); connection = jdbcDataSource.getConnection(); - initDB(); + initDB(jdbcDataSource); connection.commit(); jdbcPolicy = (db, callback) -> { @@ -162,11 +207,17 @@ public void setupDataSources() throws Exception { @AfterEach public void closeDbConnection() throws SQLException { + if (dbServer == null) { + return; + } + if (connection != null) { + connection.createStatement().execute("drop all objects delete files"); connection.close(); } connection = null; + dbServer.stop(); } } diff --git a/smartclient-core/src/test/java/org/srg/smartclient/OperationBindingTest.java b/smartclient-core/src/test/java/org/srg/smartclient/OperationBindingTest.java index e69fac4..f8d3c37 100644 --- a/smartclient-core/src/test/java/org/srg/smartclient/OperationBindingTest.java +++ b/smartclient-core/src/test/java/org/srg/smartclient/OperationBindingTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.srg.smartclient.isomorphic.DSRequest; import org.srg.smartclient.isomorphic.DSResponse; +import org.srg.smartclient.isomorphic.DataSource; import org.srg.smartclient.isomorphic.OperationBinding; import org.srg.smartclient.isomorphic.criteria.AdvancedCriteria; @@ -327,7 +328,6 @@ public void fetchFromFunctionWith_TableClause_AdvancedCriteria_ExcludeCriteria() }""", response); } - @Test public void fetchWith_CustomSQL_AdvancedCriteria() throws Exception { final JDBCHandler handler = withHandler(TIME_REPORT); @@ -400,7 +400,7 @@ public void fetchWith_CustomSQL_AdvancedCriteria() throws Exception { /* * Test wil fails if default binding will not be used: - * the default binding ignores non existent fields criteria sent within request. + * the default binding ignores non-existent fields criteria sent within request. */ @Test public void fetchWith_defaultBinding() throws Exception { @@ -447,4 +447,91 @@ public void fetchWith_defaultBinding() throws Exception { ] }""", response); } + + @Test + public void fetch_customTableClause_With_DefaultTables() throws Exception { + final OperationBinding ob = JsonTestSupport.fromJSON(OperationBinding.class, """ + { + operationType: 'fetch', + tableClause: '${defaultTableClause}' + } + """); + final DataSource ds = JsonTestSupport.fromJSON(DataSource.class, Handler.Project); + ds.setOperationBindings(Arrays.asList(ob)); + handler = withDataSource(ds); + + final DSRequest request = new DSRequest() + .setStartRow(0) + .setEndRow(2); + + final DSResponse response = handler.handleFetch(request); + + + JsonTestSupport.assertJsonEquals(""" + { + status:0, + startRow:0, + endRow:2, + totalRows:5, + data:[ + { + id:1, + name:'Project 1 for client 1' + }, + { + id:2, + name:'Project 2 for client 1' + } + ] + }""", response); + + } + + @Test + @Regression(""" + org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "ROW ()"; SQL statement: + SELECT count(*) FROM ( + SELECT employee.id AS id_employee, + employee.name AS name_employee + FROM employee + ) opaque + WHERE () AND opaque.name_employee LIKE 'man%' + """) + public void fetch_customWhereClause_With_Empty_DefaultWhere() throws Exception { + final OperationBinding ob = JsonTestSupport.fromJSON(OperationBinding.class, """ + { + operationType: 'fetch', + whereClause: "(${defaultWhereClause}) AND opaque.name_employee LIKE 'man%%'" + } + """); + final DataSource ds = JsonTestSupport.fromJSON(DataSource.class, Handler.Employee); + ds.setOperationBindings(Arrays.asList(ob)); + handler = withDataSource(ds); + + final DSRequest request = new DSRequest() + .setStartRow(0) + .setEndRow(2); + + final DSResponse response = handler.handleFetch(request); + + + JsonTestSupport.assertJsonEquals(""" + { + status:0, + startRow:0, + endRow:2, + totalRows:2, + data:[ + { + id:4, + name:'manager1' + }, + { + id:5, + name:'manager2' + } + ] + }""", response); + + } }