diff --git a/connectors/java/databasetable/README.md b/connectors/java/databasetable/README.md new file mode 100644 index 00000000..31ecce00 --- /dev/null +++ b/connectors/java/databasetable/README.md @@ -0,0 +1,111 @@ + +## Table of Contents +1. [Introduction](#introduction) +2. [Capabilities and Features](#capabilities-and-features) +3. [JSON Mode](#json-mode) +4. [Custom SQL Query Mode](#custom-sql-query-mode) +5. [Basic Mode](#basic-mode) +6. [JDBC Driver](#jdbc-driver) +7. [Limitations](#limitations) +8. [Build](#build) + +# Introduction +### databasetable-connector +Identity connector for generic relational database table. + +# Capabilities and Features +- Schema: YES +- Provisioning: YES +- Live Synchronization: YES - Using last modification timestamps +- Password: YES +- Activation: SIMULATED - Configured capability +- Script execution: No + +# Connector Modes +- Connector supports 3 different modes +- Modes can be changed via configuration in resource +- Three supported modes are: `JSON`, `CUSTOM_SQL_QUERY` and `BASIC` + +## JSON Mode +- JSON Mode supports arbitrary number of ObjectClasses and their attributes +- When using JSON mode each object class needs to be defined in JSON file +- JSON File should be properly secured and protected with appropriate permissions +- Database, connector mode and credentials need to be defined in configuration as usual +- Every ObjectClass defined in JSON File must have the following parameters: + +``` + { + "objectClassName": "AnyObjectClass", + "configurationProperties": { + "Table": "TableName", + "Key Column": "KeyColumnName", + "Enable writing empty string": true, + "Change Log Column (Sync)": "ChangeLogColumn", + "Sync Order Column": "SyncOrderColumn", + "Sync Order Asc": true, + "Suppress Password": true + } + } +``` +- For better understanding use `JSON_example.json` in the `samples` folder + +## Custom SQL Query Mode +- In custom SQL Query mode the connectors expects SQL Query file. +- Where clause is supported +- Sql Query can be in-line or with each part in new line +- Using aggregate functions is supported +- Key column, database, connector mode and credentials need to be defined in configuration as usual +- Example `SQL_query.sql` can be found inside the `samples` folder +- Columns and table need to be defined in the sql query as shown: +``` +SELECT column1, column2 FROM table +``` +or +``` +SELECT column1 as col1, column2 as col2 FROM table +``` + +## Basic Mode +- Configuration requires specifying the key column, database, connector mode, database table and credentials. +- An example configuration file `resource_basic.xml` can be found inside the `samples` folder. +- + +## JDBC Driver +- Supported JDBC Drivers are: + - MySQL Connector/J 5.1.6 + - Microsoft SQL Server 2005 JDBC Driver 1.2 + - Oracle Database 10g Release 2 (10.2.0.2) JDBC Driver + - Apache Derby 10.4 +- JDBC driver and it respective Url template: + +| jdbcDriver | jdbcUrlTemplate | +|-------------------------------------------------------------------------------------|:---------------------------------------| +| oracle.jdbc.driver.OracleDriver, org.apache.derby.jdbc.EmbeddedDriver (for testing) | jdbc:oracle:thin:@%h:%p:%d | +| com.mysql.cj.jdbc.Driver, com.mysql.jdbc.Driver | jdbc:mysql://%h:%p/%d | +| org.postgresql.Driver | jdbc:postgresql://%h:%p/%d | +| com.microsoft.sqlserver.jdbc.SQLServerDriver | jdbc:sqlserver://%h:%p;databaseName=%d;| + +## Limitations +There are few limitation for specific connector modes. +### SQL QUERY +- Does not support create, update or delete + +### BASIC +- Does not support multiple database tables + +## Build +``` +mvn clean install +``` +## Build without Tests +``` +mvn clean install -DskipTests=True +``` +After successful build, you can find connector-databasetable-1.5.2.0-SNAPSHOT.jar in target directory. + +## TODO +- An update to support new associations in version 4.9, this will enable the connector to handle traditional database relations. + +# Status +- Tested only on MySQL 9.0.0 +- Tested with MidPoint version 4.8.1. \ No newline at end of file diff --git a/connectors/java/databasetable/pom.xml b/connectors/java/databasetable/pom.xml index 059d5637..d8ab68a4 100755 --- a/connectors/java/databasetable/pom.xml +++ b/connectors/java/databasetable/pom.xml @@ -50,6 +50,7 @@ 17 10.17.1.0 + 1.1.4 org.identityconnectors.databasetable DatabaseTableConnector @@ -88,6 +89,16 @@ ${derby.version} test + + javax.json + javax.json-api + ${javax.json.version} + + + org.glassfish + javax.json + 1.1.4 + diff --git a/connectors/java/databasetable/samples/Json_example.json b/connectors/java/databasetable/samples/Json_example.json new file mode 100644 index 00000000..e4b4925e --- /dev/null +++ b/connectors/java/databasetable/samples/Json_example.json @@ -0,0 +1,30 @@ +{ + "objects": [ + { + "objectClassName": "User", + "configurationProperties": { + "Table": "pouzivatelia", + "Key Column": "key_column", + "Enable writing empty string": true, + "Change Log Column (Sync)": "change_log_column", + "Sync Order Column": "sync_order_column", + "Sync Order Asc": true, + "Suppress Password": true, + "Native Timestamps": true + } + }, + { + "objectClassName": "Group", + "configurationProperties": { + "Table": "skupiny", + "Key Column": "key_column", + "Enable writing empty string": true, + "Change Log Column (Sync)": "change_log_column", + "Sync Order Column": "sync_order_column", + "Sync Order Asc": true, + "Suppress Password": true, + "All native": true + } + } + ] +} diff --git a/connectors/java/databasetable/samples/SQL_query.sql b/connectors/java/databasetable/samples/SQL_query.sql new file mode 100644 index 00000000..b898de1d --- /dev/null +++ b/connectors/java/databasetable/samples/SQL_query.sql @@ -0,0 +1,5 @@ +SELECT name, count(*) as NumberOfUsers +FROM users +Where test is null and name = 'Marc' +Group by name +Having count(*) > 0 \ No newline at end of file diff --git a/connectors/java/databasetable/samples/resource_basic.xml b/connectors/java/databasetable/samples/resource_basic.xml new file mode 100644 index 00000000..0d73823d --- /dev/null +++ b/connectors/java/databasetable/samples/resource_basic.xml @@ -0,0 +1,96 @@ + + + Example Resource Basic + Example resource for the databaseTable connector + + + + + + connectorType + org.identityconnectors.databasetable.DatabaseTableConnector + + + connectorVersion + 1.5.2.0-SNAPSHOT + + + + + + + localhost + 3306 + root + + password + + test + users + key_column + com.mysql.cj.jdbc.Driver + jdbc:mysql://%h:%p/%d + Basic + change_log_column + + + + + + account + user + true + + ri:AccountObjectClass + + + UserType + + + icfs:name + + + name + + + + + ri:name + + + fullName + + + + + + + + + + + + + false + + + true + + + + + + connector + + + + + diff --git a/connectors/java/databasetable/samples/resource_json.xml b/connectors/java/databasetable/samples/resource_json.xml new file mode 100644 index 00000000..03d1ef38 --- /dev/null +++ b/connectors/java/databasetable/samples/resource_json.xml @@ -0,0 +1,154 @@ + + + Example Resource JSON + Example resource for the new databaseTable connector with multiple tables + + + + + + connectorType + org.identityconnectors.databasetable.DatabaseTableConnector + + + connectorVersion + 1.5.2.0-SNAPSHOT + + + + + + + localhost + 3306 + root + + password + + test + com.mysql.cj.jdbc.Driver + jdbc:mysql://%h:%p/%d + Json_example.json + JSON + true + + + + + + account + user + true + + ri:User + + + UserType + + + icfs:name + + + name + + + + + ri:name + + + fullName + + + + + ri:Group + entitlement + group + objectToSubject + ri:members + ri:name + + + + + entitlement + group + + ri:Group + + + RoleType + + + icfs:name + + + name + + + + + ri:name + + + description + + + + + ri:User + account + user + objectToSubject + ri:members + ri:name + + + + Unlinked + unlinked + + + + + + Unmatched + unmatched + + + + + + + + + + + + + + + false + + + true + + + + + + connector + + + + + diff --git a/connectors/java/databasetable/samples/resource_sql.xml b/connectors/java/databasetable/samples/resource_sql.xml new file mode 100644 index 00000000..7a368a33 --- /dev/null +++ b/connectors/java/databasetable/samples/resource_sql.xml @@ -0,0 +1,111 @@ + + + Example Resource SQL + Example resource for the new databaseTable connector using sql script + + + + + + connectorType + org.identityconnectors.databasetable.DatabaseTableConnector + + + connectorVersion + 1.5.2.0-SNAPSHOT + + + + + + + + localhost + 3306 + root + + password + + test + key_column + com.mysql.cj.jdbc.Driver + jdbc:mysql://%h:%p/%d + SQL_query.sql + custom_sql_query + change_log_column + + + + + + account + user + + ri:AccountObjectClass + + + UserType + + + icfs:name + + + name + + + + + ri:name + + + fullName + + + + + + unlinked + + + + + + unmatched + + + + + + + + + + + + + + + + false + + + true + + + + + + connector + + + + + diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java index ef6c5ea2..192a199d 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConfiguration.java @@ -34,6 +34,11 @@ import org.identityconnectors.framework.spi.ConfigurationProperty; import org.identityconnectors.framework.spi.operations.DiscoverConfigurationOp; import org.identityconnectors.framework.spi.operations.SyncOp; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Objects; + /** * Implements the {@link Configuration} interface to provide all the necessary @@ -50,6 +55,8 @@ enum Validation { FULL, BASIC } + + /** * Type of validation. @@ -721,6 +728,92 @@ public void setSQLStateConfigurationException(String[] sqlStateConfigurationExce this.sqlStateConfigurationException = sqlStateConfigurationException; } + /** + * Path for the JSON file with configuration + */ + private String jsonFilePath; + + /** + * JsonFilePath getter + * + * @return JsonFile path + */ + @ConfigurationProperty(order = 32, + displayMessageKey = "CUSTOM_JSONFILE_DISPLAY_KEY", + helpMessageKey = "CUSTOM_JSONFILE_HELP_KEY") + + public String getJsonFilePath() { + return this.jsonFilePath; + } + + /** + * JsonFilePath Setter + */ + public void setJsonFilePath(String jsonFilePath) { + this.jsonFilePath = jsonFilePath; + } + + /** + * Path for the SQL file + */ + private String sqlFilePath; + @ConfigurationProperty(order = 33, + displayMessageKey = "CUSTOM_SQL_FILE_PATH_DISPLAY_KEY", + helpMessageKey = "CUSTOM_SQL_FILE_PATH_HELP_KEY") + + /** + * SqlFilePath getter + * + * @return SqlFile path + */ + public String getSqlFilePath() { + return this.sqlFilePath; + } + + /** + * SqlFilePath Setter + */ + public void setSqlFilePath(String sqlFilePath) { + this.sqlFilePath = sqlFilePath; + } + + /** + * Which connector type is used. Possible values can be Json, Basic, Custom_SQL_Query + */ + private String connectorMode; + @ConfigurationProperty(order = 34, + displayMessageKey = "CUSTOM_CONNECTOR_MODE_DISPLAY_KEY", + helpMessageKey = "CUSTOM_CONNECTOR_MODE_HELP_KEY") + + /** + * ConnectorMode getter + * + * @return ConnectorMode value + */ + public String getConnectorMode() { + return this.connectorMode; + } + + /** + * ConnectorMode Setter + */ + public void setConnectorMode(String connectorMode) { + if (connectorMode == null) { + connectorMode = "Basic"; + } + switch (connectorMode.toLowerCase()) { + case "basic": + this.connectorMode = "Basic"; + break; + case "json": + this.connectorMode = "Json"; + break; + case "custom_sql_query": + this.connectorMode = "Custom_SQL_Query"; + break; + } + } + // ======================================================================= // Configuration Interface // ======================================================================= @@ -792,28 +885,45 @@ public void validate() { private void validateConfigurationForTable() { // check that there is a table to query. - if (StringUtil.isBlank(getTable())) { - throw new IllegalArgumentException(getMessage(MSG_TABLE_BLANK)); - } - // determine if you can get a key column - if (StringUtil.isBlank(getKeyColumn())) { - throw new IllegalArgumentException(getMessage(MSG_KEY_COLUMN_BLANK)); + + if(Objects.equals(this.connectorMode, "Json")) { + if(StringUtil.isBlank(getJsonFilePath())) { + throw new IllegalArgumentException(getMessage(MSG_JSON_FILE_BLANK)); + } + } else if (Objects.equals(this.connectorMode, "Custom_SQL_Query")){ + if(StringUtil.isBlank(getSqlFilePath())) { + throw new IllegalArgumentException(getMessage(MSG_SQL_FILE_BLANK)); + } + + if (StringUtil.isBlank(getKeyColumn())) { + throw new IllegalArgumentException(getMessage(MSG_KEY_COLUMN_BLANK)); + } } else { - if (getKeyColumn().equalsIgnoreCase(getChangeLogColumn())) { - throw new IllegalArgumentException(getMessage(MSG_KEY_COLUMN_EQ_CHANGE_LOG_COLUMN)); + if (StringUtil.isBlank(getTable())) { + throw new IllegalArgumentException(getMessage(MSG_TABLE_BLANK)); } - } - // key column, password column - if (StringUtil.isNotBlank(getPasswordColumn())) { - if (getPasswordColumn().equalsIgnoreCase(getKeyColumn())) { - throw new IllegalArgumentException(getMessage(MSG_PASSWD_COLUMN_EQ_KEY_COLUMN)); + // determine if you can get a key column + if (StringUtil.isBlank(getKeyColumn())) { + throw new IllegalArgumentException(getMessage(MSG_KEY_COLUMN_BLANK)); + } else { + if (getKeyColumn().equalsIgnoreCase(getChangeLogColumn())) { + throw new IllegalArgumentException(getMessage(MSG_KEY_COLUMN_EQ_CHANGE_LOG_COLUMN)); + } } + // key column, password column + if (StringUtil.isNotBlank(getPasswordColumn())) { + if (getPasswordColumn().equalsIgnoreCase(getKeyColumn())) { + throw new IllegalArgumentException(getMessage(MSG_PASSWD_COLUMN_EQ_KEY_COLUMN)); + } - if (getPasswordColumn().equalsIgnoreCase(getChangeLogColumn())) { - throw new IllegalArgumentException(getMessage(MSG_PASSWD_COLUMN_EQ_CHANGE_LOG_COLUMN)); + if (getPasswordColumn().equalsIgnoreCase(getChangeLogColumn())) { + throw new IllegalArgumentException(getMessage(MSG_PASSWD_COLUMN_EQ_CHANGE_LOG_COLUMN)); + } } } + + try { DatabaseTableSQLUtil.quoteName(getQuoting(), "test"); } catch (IllegalArgumentException e) { diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java index 7023c645..b1eb71e7 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConnector.java @@ -28,6 +28,8 @@ import java.sql.*; import java.text.MessageFormat; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.identityconnectors.common.Assertions; import org.identityconnectors.common.CollectionUtil; @@ -35,6 +37,9 @@ import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.databasetable.config.JsonConfigHandler; +import org.identityconnectors.databasetable.config.SqlHandler; +import org.identityconnectors.databasetable.config.UniversalObjectClassHandler; import org.identityconnectors.databasetable.mapping.MappingStrategy; import org.identityconnectors.databasetable.mapping.misc.SQLColumnTypeInfo; import org.identityconnectors.dbcommon.*; @@ -96,13 +101,18 @@ public class DatabaseTableConnector implements PoolableConnector, CreateOp, Sear /** * Default attributes to get, created and cached from the schema */ - private Set defaultAttributesToGet; + private Map> defaultAttributesToGet; /** * Same of the data types must be converted */ private Map columnSQLTypes; + /** + * Stores values from different tables + */ + private Map> columnSQLTypesCollection; + /** * Cached value for required columns */ @@ -119,6 +129,9 @@ public DatabaseTableConfiguration getConfiguration() { return this.config; } + private Map universalObjectClassHandlers; + private SqlHandler sqlHandler; + /** * Init the connector * {@inheritDoc} @@ -126,12 +139,24 @@ public DatabaseTableConfiguration getConfiguration() { public void init(Configuration cfg) { log.info("init DatabaseTable connector"); this.config = (DatabaseTableConfiguration) cfg; + + // Creates and sets config classes according to connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + JsonConfigHandler jsonConfigHandler = new JsonConfigHandler(this.config); + this.universalObjectClassHandlers = jsonConfigHandler.getUniversalObjectClassHandlers(); + } else if (Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + this.sqlHandler = new SqlHandler(this.config); + this.config.setTable(this.sqlHandler.getTable()); + } + this.schema = null; this.defaultAttributesToGet = null; this.columnSQLTypes = null; + this.columnSQLTypesCollection = null; log.ok("init DatabaseTable connector ok, connection is valid"); } + /** * {@inheritDoc} */ @@ -181,6 +206,7 @@ public void dispose() { this.defaultAttributesToGet = null; this.schema = null; this.columnSQLTypes = null; + this.columnSQLTypesCollection = null; } /** @@ -188,59 +214,77 @@ public void dispose() { * {@inheritDoc} */ public Uid create(ObjectClass oclass, Set attrs, OperationOptions options) { + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")){ + throw new UnsupportedOperationException("Create operation is not supported in 'Custom' connector mode"); + } + log.info("create account, check the ObjectClass"); - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } - log.ok("Object class ok"); - if (attrs == null || attrs.size() == 0) { - throw new IllegalArgumentException(config.getMessage(MSG_INVALID_ATTRIBUTE_SET)); + if (attrs == null || attrs.isEmpty()) { + throw new IllegalArgumentException(this.config.getMessage(MSG_INVALID_ATTRIBUTE_SET)); } log.ok("Attribute set is not empty"); //Name must be present in attribute set or must be generated UID set on Name name = AttributeUtil.getNameFromAttributes(attrs); if (name == null) { - throw new IllegalArgumentException(config.getMessage(MSG_NAME_BLANK)); + throw new IllegalArgumentException(this.config.getMessage(MSG_NAME_BLANK)); } + final String accountName = name.getNameValue(); log.ok("Required Name attribure value {0} for create", accountName); - final String tblname = config.getTable(); + // Sets table name and isEnableEmptyString from config specified by connector mode + String tblname; + boolean isEnableEmptyString; + if(Objects.equals(this.config.getConnectorMode(), "Json")){ + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + isEnableEmptyString = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).isEnableEmptyString(); + } else { + tblname = this.config.getTable(); + isEnableEmptyString = this.config.isEnableEmptyString(); + } + // start the insert statement final InsertIntoBuilder bld = new InsertIntoBuilder(); log.info("Creating account: {0}", accountName); Set missingRequiredColumns = CollectionUtil.newCaseInsensitiveSet(); - if (config.isEnableEmptyString()) { + if (isEnableEmptyString) { final Set mrc = getStringColumnReguired(); log.info("Empty String is enabled, add missing required columns {0}", mrc); missingRequiredColumns.addAll(mrc); } log.info("process and check the Attribute Set"); - //All attribute names should be in create columns statement + //All attribute names should be in create columns statement for (Attribute attr : attrs) { - // quoted column name - final String columnName = getColumnName(attr.getName()); - Object value = AttributeUtil.getSingleValue(attr); - //Empty String - if (isToBeEmpty(columnName, value)) { - log.info("create account, attribute for a column {0} is null and should be empty", columnName); - value = EMPTY_STR; + if (attr == null) { + throw new IllegalArgumentException("Attribute cannot be null"); + } else { + // quoted column name + final String columnName = getColumnName(oclass, attr.getName()); + Object value = AttributeUtil.getSingleValue(attr); + //Empty String + if (isToBeEmpty(columnName, value)) { + log.info("create account, attribute for a column {0} is null and should be empty", columnName); + value = EMPTY_STR; + } + final SQLColumnTypeInfo sqlColumnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), columnName); + log.info("attribute {0} fit column {1} and sql type {2}", attr.getName(), columnName, sqlColumnTypeInfo); + bld.addBind(new SQLParam(quoteName(columnName), value, sqlColumnTypeInfo.getTypeCode(), sqlColumnTypeInfo.getTypeName())); + missingRequiredColumns.remove(columnName); + log.ok("attribute {0} was added to insert", attr.getName()); } - final SQLColumnTypeInfo sqlColumnTypeInfo = getColumnTypeInfo(columnName); - log.info("attribute {0} fit column {1} and sql type {2}", attr.getName(), columnName, sqlColumnTypeInfo); - bld.addBind(new SQLParam(quoteName(columnName), value, sqlColumnTypeInfo.getTypeCode(), sqlColumnTypeInfo.getTypeName())); - missingRequiredColumns.remove(columnName); - log.ok("attribute {0} was added to insert", attr.getName()); } // Bind empty string for not-null columns which are not in attribute set list - if (config.isEnableEmptyString()) { + if (isEnableEmptyString) { log.info("there are columns not matched in attribute set which should be empty"); for (String mCol : missingRequiredColumns) { - SQLColumnTypeInfo typeInfo = getColumnTypeInfo(mCol); + SQLColumnTypeInfo typeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), mCol); bld.addBind(new SQLParam(quoteName(mCol), EMPTY_STR, typeInfo.getTypeCode(), typeInfo.getTypeName())); log.ok("Required empty value to column {0} added", mCol); } @@ -259,17 +303,18 @@ public Uid create(ObjectClass oclass, Set attrs, OperationOptions opt log.info("Create account {0} commit", accountName); commit(); } catch (SQLException e) { - evaluateAndHandleException(e, true, true, false, MSG_CAN_NOT_CREATE, accountName); } finally { IOUtil.quietClose(pstmt); closeConnection(); } - log.ok("Account {0} created", accountName); + log.ok("Account {0} created"); // create and return the uid.. return new Uid(accountName); + } + /** * Test to throw the exception * @@ -295,63 +340,82 @@ private boolean isToBeEmpty(final String columnName, Object value) { * {@inheritDoc} */ public void delete(final ObjectClass oclass, final Uid uid, final OperationOptions options) { + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")){ + throw new UnsupportedOperationException("Delete operation is not supported in 'Custom' connector mode"); + } + log.info("delete account, check the ObjectClass"); final String SQL_DELETE = "DELETE FROM {0} WHERE {1} = ?"; PreparedStatement stmt = null; // create the SQL string.. - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null ) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); - } - log.ok("The ObjectClass is ok"); + } else { + log.ok("The ObjectClass is ok"); - if (uid == null || (uid.getUidValue() == null)) { - throw new IllegalArgumentException(config.getMessage(MSG_UID_BLANK)); - } - final String accountUid = uid.getUidValue(); - log.ok("The Uid is present"); + if (uid == null) { + throw new IllegalArgumentException("Uid cannot be null"); + } else { + if (uid.getUidValue() == null) { + throw new IllegalArgumentException(this.config.getMessage(MSG_UID_BLANK)); + } else { + final String accountUid = uid.getUidValue(); + log.ok("The Uid is present"); + + String tblname; + String keycol; + // Sets table name and key column from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")){ + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + keycol = quoteName(this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn()); + } else { + tblname = this.config.getTable(); + keycol = this.config.getKeyColumn(); + } - final String tblname = config.getTable(); - final String keycol = quoteName(config.getKeyColumn()); - final String sql = MessageFormat.format(SQL_DELETE, tblname, keycol); - try { - log.info("delete account SQL {0}", sql); - openConnection(); - // create a prepared call.. - stmt = getConn().getConnection().prepareStatement(sql); - // set object to delete.. - final SQLColumnTypeInfo sqlColumnTypeInfo = getColumnTypeInfo(config.getKeyColumn()); - SQLUtil.setSQLParam(stmt, 1, new SQLParam(quoteName(config.getKeyColumn()), accountUid, sqlColumnTypeInfo.getTypeCode(), sqlColumnTypeInfo.getTypeName())); - // stmt.setString(1, accountUid); - // uid to delete.. - log.info("Deleting account Uid: {0}", accountUid); - final int dr = stmt.executeUpdate(); - if (dr < 1) { - SQLUtil.rollbackQuietly(getConn()); - // TODO: Before we throw this error we should recheck that the account is really not there with select. - // This may happen when the "table" is view and update returns UPDATED 0. - // The consequences of killing the shadow are quite annoying, so we should double-check. - // Of course, the previous behaviour quietly ignoring the problem was not good either. - handleUnknownUid(MSG_EXP_UNKNOWN_UID, accountUid); - } - if (dr > 1) { - SQLUtil.rollbackQuietly(getConn()); - // TODO: This one is even stranger - again, should it make the shadow dead? - // It likely should not be ignored like before, I agree with that. - handleUnknownUid(MSG_EXP_TOO_MANY_UID, accountUid); - } - log.info("Delete account {0} commit", accountUid); - commit(); - } catch (SQLException e) { + final String sql = MessageFormat.format(SQL_DELETE, tblname, keycol); + try { + log.info("delete account SQL {0}", sql); + openConnection(); + // create a prepared call.. + stmt = getConn().getConnection().prepareStatement(sql); + // set object to delete.. + final SQLColumnTypeInfo sqlColumnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), keycol); + SQLUtil.setSQLParam(stmt, 1, new SQLParam(quoteName(keycol), accountUid, sqlColumnTypeInfo.getTypeCode(), sqlColumnTypeInfo.getTypeName())); + // stmt.setString(1, accountUid); + // uid to delete.. + log.info("Deleting account Uid: {0}", accountUid); + final int dr = stmt.executeUpdate(); + if (dr < 1) { + SQLUtil.rollbackQuietly(getConn()); + // TODO: Before we throw this error we should recheck that the account is really not there with select. + // This may happen when the "table" is view and update returns UPDATED 0. + // The consequences of killing the shadow are quite annoying, so we should double-check. + // Of course, the previous behaviour quietly ignoring the problem was not good either. + handleUnknownUid(MSG_EXP_UNKNOWN_UID, accountUid); + } + if (dr > 1) { + SQLUtil.rollbackQuietly(getConn()); + // TODO: This one is even stranger - again, should it make the shadow dead? + // It likely should not be ignored like before, I agree with that. + handleUnknownUid(MSG_EXP_TOO_MANY_UID, accountUid); + } + log.info("Delete account {0} commit", accountUid); + commit(); + } catch (SQLException e) { + SQLUtil.rollbackQuietly(getConn()); + evaluateAndHandleException(e, false, true, false, MSG_CAN_NOT_DELETE, accountUid); + } finally { + IOUtil.quietClose(stmt); + closeConnection(); + } + log.ok("Account Uid {0} deleted", accountUid); - SQLUtil.rollbackQuietly(getConn()); - evaluateAndHandleException(e, false, true, false, MSG_CAN_NOT_DELETE, accountUid); - } finally { - IOUtil.quietClose(stmt); - closeConnection(); + } + } } - log.ok("Account Uid {0} deleted", accountUid); } /** @@ -359,18 +423,22 @@ public void delete(final ObjectClass oclass, final Uid uid, final OperationOptio * {@inheritDoc} */ public Uid update(ObjectClass oclass, Uid uid, Set attrs, OperationOptions options) { + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")){ + throw new UnsupportedOperationException("Update operation is not supported in 'Custom' connector mode"); + } + log.info("update account, check the ObjectClass"); final String sqlTemplate = "UPDATE {0} SET {1} WHERE {2} = ?"; // create the sql statement.. - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } log.ok("The ObjectClass is ok"); - if (attrs == null || attrs.size() == 0) { - throw new IllegalArgumentException(config.getMessage(MSG_INVALID_ATTRIBUTE_SET)); + if (attrs == null || attrs.isEmpty()) { + throw new IllegalArgumentException(this.config.getMessage(MSG_INVALID_ATTRIBUTE_SET)); } log.ok("Attribute set is not empty"); @@ -396,14 +464,14 @@ public Uid update(ObjectClass oclass, Uid uid, Set attrs, OperationOp // All attributes needs to be updated except the UID if (!attribute.is(Uid.NAME)) { final String attributeName = attribute.getName(); - final String columnName = getColumnName(attributeName); + final String columnName = getColumnName(oclass, attributeName); Object value = AttributeUtil.getSingleValue(attribute); // Handle the empty string values if (isToBeEmpty(columnName, value)) { log.info("Append empty attribute {0} for required columnName {1}", attributeName, columnName); value = DatabaseTableConstants.EMPTY_STR; } - final SQLColumnTypeInfo sqlColumnTypeInfo = getColumnTypeInfo(columnName); + final SQLColumnTypeInfo sqlColumnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), columnName); final SQLParam param = new SQLParam(quoteName(columnName), value, sqlColumnTypeInfo.getTypeCode(), sqlColumnTypeInfo.getTypeName()); updateSet.addBind(param); log.ok("Appended to update statement the attribute {0} for columnName {1} and sql type code {2}", attributeName, columnName, sqlColumnTypeInfo.getTypeCode()); @@ -411,10 +479,20 @@ public Uid update(ObjectClass oclass, Uid uid, Set attrs, OperationOp } log.info("Update account {0}", accountName); + String tblname; + String keycol; + + // Sets table name and key column from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")){ + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + keycol = quoteName(this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn()); + } else { + tblname = this.config.getTable(); + keycol = this.config.getKeyColumn(); + } + // Format the update query - final String tblname = config.getTable(); - final String keycol = quoteName(config.getKeyColumn()); - SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(config.getKeyColumn()); + SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), keycol); updateSet.addValue(new SQLParam(keycol, accountUid, columnTypeInfo.getTypeCode(), columnTypeInfo.getTypeName())); final String sql = MessageFormat.format(sqlTemplate, tblname, updateSet.getSQL(), keycol); PreparedStatement stmt = null; @@ -441,7 +519,6 @@ public Uid update(ObjectClass oclass, Uid uid, Set attrs, OperationOp log.info("Update account {0} commit", accountName); commit(); } catch (SQLException e) { - evaluateAndHandleException(e, true, true, false, MSG_CAN_NOT_UPDATE, accountName); } finally { IOUtil.quietClose(stmt); @@ -451,13 +528,14 @@ public Uid update(ObjectClass oclass, Uid uid, Set attrs, OperationOp return ret; } + /** * Creates a Database Table filter translator. * {@inheritDoc} */ public FilterTranslator createFilterTranslator(ObjectClass oclass, OperationOptions options) { log.info("check the ObjectClass"); - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } log.ok("The ObjectClass is ok"); @@ -472,7 +550,7 @@ public void executeQuery(ObjectClass oclass, FilterWhereBuilder where, ResultsHa OperationOptions options) { log.info("check the ObjectClass and result handler"); // Contract tests - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } @@ -482,44 +560,320 @@ public void executeQuery(ObjectClass oclass, FilterWhereBuilder where, ResultsHa log.ok("The ObjectClass and result handler is ok"); //Names - final String tblname = config.getTable(); - final Set columnNamesToGet = resolveColumnNamesToGet(options); - log.ok("Column Names {0} To Get", columnNamesToGet); - // For all account query there is no need to replace or quote anything - final DatabaseQueryBuilder query = new DatabaseQueryBuilder(tblname, columnNamesToGet); - query.setWhere(where); + String tblname; + // Sets table name from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")){ + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + } else { + tblname = config.getTable(); + } + + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + executeSqlQuery(oclass, where, handler); + } else { + final Set columnNamesToGet = resolveColumnNamesToGet(oclass, options); + log.ok("Column Names {0} To Get", columnNamesToGet); + // For all account query there is no need to replace or quote anything + ResultSet result = null; + PreparedStatement statement = null; + final DatabaseQueryBuilder query = new DatabaseQueryBuilder(tblname, columnNamesToGet); + query.setWhere(where); + + try { + openConnection(); + statement = getConn().prepareStatement(query); + result = statement.executeQuery(); + log.ok("executeQuery {0} on {1}", query.getSQL(), oclass); + while (result.next()) { + final Map columnValues = getConn().getColumnValues(result); + log.ok("Column values {0} from result set ", columnValues); + // create the connector object + final ConnectorObjectBuilder bld = buildConnectorObject(oclass,columnValues); + if (!handler.handle(bld.build())) { + log.ok("Stop processing of the result set"); + break; + } + } + // commit changes + log.info("commit executeQuery"); + commit(); + } catch (SQLException e) { + log.error(e, "Query {0} on {1} error", query.getSQL(), oclass); + + SQLUtil.rollbackQuietly(getConn()); + evaluateAndHandleException(e, true, true, false, MSG_CAN_NOT_READ, tblname); + } finally { + IOUtil.quietClose(result); + IOUtil.quietClose(statement); + closeConnection(); + } + log.ok("Query Account committed"); + } + } + + private String buildSqlQuery(FilterWhereBuilder where) { + String whereCondition = ""; + String sqlQuery = this.sqlHandler.getSqlQuery(); + + if(where != null) { + whereCondition = where.getWhereClause(); + int minIndex = sqlQuery.length(); + + int whereIndex = this.sqlHandler.getWhereIndex(); + if (whereIndex == -1 || whereIndex > minIndex) { + return sqlQuery + " WHERE " + whereCondition; + } + + if(this.sqlHandler.getWhereClause()!=null) { + return sqlQuery.substring(0,whereIndex) + "WHERE " + whereCondition + " AND " + sqlQuery.substring(whereIndex+6); + } + + return sqlQuery.substring(0,whereIndex) + "WHERE " + whereCondition + " " + sqlQuery.substring(whereIndex); + } + return sqlQuery; + } + + private boolean isNumericType(int sqlType) { + switch (sqlType) { + case -7: // BIT + case -6: // TINYINT + case -5: // BIGINT + case 2: // NUMERIC + case 3: // DECIMAL + case 4: // INTEGER + case 5: // SMALLINT + case 6: // FLOAT + case 7: // REAL + case 8: // DOUBLE + return true; + default: + return false; + } + } + + private static String detectOperatorFor(String whereSql, String sourceName, Map aliasBySource) { + if (whereSql == null) { + whereSql = ""; + } + if (sourceName == null || sourceName.trim().isEmpty()) { + return "="; + } + + + String alias = (aliasBySource != null) ? aliasBySource.get(sourceName) : null; + + java.util.List candidates = new java.util.ArrayList<>(); + if (alias != null && !alias.isEmpty()) { + candidates.add(alias); + } + candidates.add(sourceName); + + for (String cand : candidates) { + String patName = java.util.regex.Pattern.quote(cand); + java.util.regex.Pattern pat = java.util.regex.Pattern.compile("(?i)" + patName + "\\s*(=|like|equals)\\b"); + java.util.regex.Matcher m = pat.matcher(whereSql); + if (m.find()) { + String op = m.group(1); + if ("equals".equalsIgnoreCase(op)) { + return "="; + } + if ("like".equalsIgnoreCase(op)) { + return "LIKE"; + } + return "="; + } + } + + return "="; + } + + private String getSqlTypeName(int sqlType) { + switch (sqlType) { + case -7: return "BIT"; + case -6: return "TINYINT"; + case -5: return "BIGINT"; + case -4: return "LONGVARBINARY"; + case -3: return "VARBINARY"; + case -2: return "BINARY"; + case -1: return "LONGVARCHAR"; + case 1: return "CHAR"; + case 2: return "NUMERIC"; + case 3: return "DECIMAL"; + case 4: return "INTEGER"; + case 5: return "SMALLINT"; + case 6: return "FLOAT"; + case 7: return "REAL"; + case 8: return "DOUBLE"; + case 12: return "VARCHAR"; + case 16: return "BOOLEAN"; + case 91: return "DATE"; + case 92: return "TIME"; + case 93: return "TIMESTAMP"; + case 2004: return "BLOB"; + case 2005: return "CLOB"; + default: return "SQL Type:" + sqlType; + } + } + + private Object convertToNumericType(String value, int sqlType) throws NumberFormatException { + if (value == null || value.trim().isEmpty()) { + return null; + } + + String trimmedValue = value.trim(); + + switch (sqlType) { + case -7: // BIT + case 16: // BOOLEAN + return Boolean.parseBoolean(trimmedValue); + + case -6: // TINYINT + return Byte.parseByte(trimmedValue); + case 5: // SMALLINT + return Short.parseShort(trimmedValue); + + case 4: // INTEGER + return Integer.parseInt(trimmedValue); + + case -5: // BIGINT + return Long.parseLong(trimmedValue); + + case 6: // FLOAT + case 7: // REAL + return Float.parseFloat(trimmedValue); + + case 8: // DOUBLE + return Double.parseDouble(trimmedValue); + + case 2: // NUMERIC + case 3: // DECIMAL + return new java.math.BigDecimal(trimmedValue); + + default: + return value; + } + } + + private void executeSqlQuery(ObjectClass oclass, FilterWhereBuilder where, ResultsHandler handler) { + List params = new ArrayList<>(); + if (where != null) { + params = where.getParams(); + + String originalWhereSql = where.getWhere() != null ? where.getWhere().toString() : ""; + + List replacedParams = new ArrayList<>(); + if (!this.sqlHandler.getColumnAliasBySource().isEmpty()) { + + ListIterator it = params.listIterator(); + while (it.hasNext()) { + SQLParam p = it.next(); + String oldName = p.getName(); + String newName = this.sqlHandler.getSourceByColumnAlias().get(oldName); + Object value = p.getValue(); + + if(isNumericType(p.getSqlType())) { + if (value instanceof String) { + String strValue = (String) value; + String originalValue = strValue; + + if (strValue.startsWith("%")) { + strValue = strValue.substring(1); + } + + if (strValue.endsWith("%")) { + strValue = strValue.substring(0, strValue.length() - 1); + } + + if (!originalValue.equals(strValue)) { + log.info("Removed leading/trailing % from value ''{0}'' -> ''{1}'' for column {2}", + originalValue, strValue, p.getName()); + value = strValue; + } + + if (isNumericType(p.getSqlType())) { + if (strValue.contains("%") || strValue.contains("_")) { + String cleanValue = strValue.replace("%", "").replace("_", ""); + + log.warn("Column {0} is numeric type {1} but received wildcard value ''{2}''. " + + "Removing all wildcards and using exact match with value ''{3}''", + p.getName(), getSqlTypeName(p.getSqlType()), strValue, cleanValue); + + strValue = cleanValue; + } + + try { + value = convertToNumericType(strValue, p.getSqlType()); + log.ok("Converted string ''{0}'' to numeric type {1} for column {2}", + strValue, getSqlTypeName(p.getSqlType()), p.getName()); + } catch (NumberFormatException e) { + log.error("Cannot convert ''{0}'' to numeric type for column {1}. Using string value.", + strValue, p.getName()); + value = strValue; + } + } + } + } + + if (newName != null && !newName.equals(oldName)) { + replacedParams.add(new SQLParam(newName, value, p.getSqlType(), p.getSqlTypeName())); + } else { + replacedParams.add(p); + } + } + params = replacedParams; + + where = new FilterWhereBuilder(); + boolean first = true; + for (SQLParam p : params) { + if (!first) { + where.getWhere().append(" AND "); + } + + String operator = detectOperatorFor(originalWhereSql, p.getName(), this.sqlHandler.getColumnAliasBySource()); + if(isNumericType(p.getSqlType())) {operator = "=";} + + Object v = p.getValue(); + where.addBind( + new SQLParam(p.getName(), v, p.getSqlType(), p.getSqlTypeName()), + operator + ); + + first = false; + } + } + } + + String sqlQuery = buildSqlQuery(where); + + PreparedStatement stmt = null; ResultSet result = null; - PreparedStatement statement = null; try { - openConnection(); - statement = getConn().prepareStatement(query); - result = statement.executeQuery(); - log.ok("executeQuery {0} on {1}", query.getSQL(), oclass); + stmt = getConn().prepareStatement(sqlQuery, params); + result = stmt.executeQuery(); + log.ok("executeSqlQuery {0} on {1}", sqlQuery, oclass); while (result.next()) { final Map columnValues = getConn().getColumnValues(result); log.ok("Column values {0} from result set ", columnValues); - // create the connector object - final ConnectorObjectBuilder bld = buildConnectorObject(columnValues); + final ConnectorObjectBuilder bld = buildConnectorObject(oclass,columnValues); if (!handler.handle(bld.build())) { log.ok("Stop processing of the result set"); break; } } - // commit changes - log.info("commit executeQuery account"); + log.info("commit executeSqlQuery"); commit(); + } catch (SQLException e) { - log.error(e, "Query {0} on {1} error", query.getSQL(), oclass); + log.error(e, "sqlQuery {0} on {1} error", sqlQuery, oclass); SQLUtil.rollbackQuietly(getConn()); - evaluateAndHandleException(e, true, true, false, MSG_CAN_NOT_READ, tblname); + evaluateAndHandleException(e, true, true, false, MSG_CAN_NOT_READ, this.sqlHandler.getColumns()); } finally { IOUtil.quietClose(result); - IOUtil.quietClose(statement); + IOUtil.quietClose(stmt); closeConnection(); } - log.ok("Query Account committed"); } /** @@ -528,7 +882,7 @@ public void executeQuery(ObjectClass oclass, FilterWhereBuilder where, ResultsHa public void sync(ObjectClass oclass, SyncToken token, SyncResultsHandler handler, OperationOptions options) { log.info("check the ObjectClass and result handler"); // Contract tests - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } log.ok("The object class is ok"); @@ -537,30 +891,54 @@ public void sync(ObjectClass oclass, SyncToken token, SyncResultsHandler handler } log.ok("The result handles is not null"); //check if password column is defined in the config - if (StringUtil.isBlank(config.getChangeLogColumn())) { + + // Names + String tblname; + String changeLogColumn; + // Sets table name and change log column from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + changeLogColumn = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getChangeLogColumn(); + } else if (Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + tblname = this.sqlHandler.getTable(); + changeLogColumn = config.getChangeLogColumn(); + } else { + tblname = config.getTable(); + changeLogColumn = config.getChangeLogColumn(); + } + + if (StringUtil.isBlank(changeLogColumn)) { throw new IllegalArgumentException(config.getMessage(MSG_CHANGELOG_COLUMN_BLANK)); } log.ok("The change log column is ok"); - // Names - final String tblname = config.getTable(); - final String changeLogColumnName = quoteName(config.getChangeLogColumn()); - log.ok("Change log attribute {0} map to column name {1}", config.getChangeLogColumn(), changeLogColumnName); - final Set columnNames = resolveColumnNamesToGet(options); + log.ok("Change log attribute {0} map to column name {1}", changeLogColumn, quoteName(changeLogColumn)); + + final Set columnNames = resolveColumnNamesToGet(oclass, options); log.ok("Column Names {0} To Get", columnNames); final List orderBy = new ArrayList<>(); //Add also the token column - columnNames.add(changeLogColumnName); + columnNames.add(quoteName(changeLogColumn)); //Set ORDER BY on Sync Data - String syncOrderByColumnName = changeLogColumnName; + String syncOrderByColumnName = quoteName(changeLogColumn); if (StringUtil.isNotBlank(config.getSyncOrderColumn())) { syncOrderByColumnName = config.getSyncOrderColumn(); + } else if (Objects.equals(this.config.getConnectorMode(), "Json")) { + if(StringUtil.isNotBlank(this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getSyncOrderColumn())) { + syncOrderByColumnName = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getSyncOrderColumn(); + } } + Boolean syncOrderByAsc = true; + // Sets sync order by ascending from config specified by connector mode if it is not empty if (config.getSyncOrderAsc() != null) { syncOrderByAsc = config.getSyncOrderAsc(); + } else if(Objects.equals(this.config.getConnectorMode(), "Json")) { + if(this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getSyncOrderAsc() != null) { + syncOrderByAsc = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getSyncOrderAsc(); + } } orderBy.add(new OrderBy(syncOrderByColumnName, syncOrderByAsc)); @@ -571,27 +949,36 @@ public void sync(ObjectClass oclass, SyncToken token, SyncResultsHandler handler if (token != null && token.getValue() != null) { final Object tokenVal = token.getValue(); log.info("Sync token is {0}", tokenVal); - final SQLColumnTypeInfo sqlColumnTypeinfo = getColumnTypeInfo(config.getChangeLogColumn()); - where.addBind(new SQLParam(changeLogColumnName, tokenVal, sqlColumnTypeinfo.getTypeCode(), sqlColumnTypeinfo.getTypeName()), ">"); + final SQLColumnTypeInfo sqlColumnTypeinfo = getColumnTypeInfo(oclass.getObjectClassValue(), changeLogColumn); + where.addBind(new SQLParam(quoteName(changeLogColumn), tokenVal, sqlColumnTypeinfo.getTypeCode(), sqlColumnTypeinfo.getTypeName()), ">"); } - final DatabaseQueryBuilder query = new DatabaseQueryBuilder(tblname, columnNames); - query.setWhere(where); - query.setOrderBy(orderBy); + DatabaseQueryBuilder query; ResultSet result = null; PreparedStatement statement = null; try { openConnection(); - statement = getConn().prepareStatement(query); + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + String sqlQuery = buildSqlQuery(where); + statement = getConn().prepareStatement(sqlQuery, where.getParams()); + log.info("execute sync query {0} on {1}", sqlQuery, oclass); + } else { + query = new DatabaseQueryBuilder(tblname, columnNames); + query.setWhere(where); + query.setOrderBy(orderBy); + statement = getConn().prepareStatement(query); + log.info("execute sync query {0} on {1}", query.getSQL(), oclass); + } + result = statement.executeQuery(); - log.info("execute sync query {0} on {1}", query.getSQL(), oclass); + while (result.next()) { final Map columnValues = getConn().getColumnValues(result); log.ok("Column values {0} from sync result set ", columnValues); // create the connector object.. - final SyncDeltaBuilder sdb = buildSyncDelta(columnValues); + final SyncDeltaBuilder sdb = buildSyncDelta(oclass,columnValues); if (!handler.handle(sdb.build())) { log.ok("Stop processing of the sync result set"); break; @@ -620,20 +1007,38 @@ public SyncToken getLatestSyncToken(ObjectClass oclass) { log.info("check the ObjectClass"); final String SQL_SELECT = "SELECT MAX( {0} ) FROM {1}"; // Contract tests - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } log.ok("The object class is ok"); - //check if password column is defined in the config - if (StringUtil.isBlank(config.getChangeLogColumn())) { - throw new IllegalArgumentException(config.getMessage(MSG_CHANGELOG_COLUMN_BLANK)); + // Format the update query + String tblname; + String chlogName; + + //check if password column is defined in the config and sets table name and isEnableEmptyString from config specified + // by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + if (StringUtil.isBlank(this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getChangeLogColumn())) { + throw new IllegalArgumentException(config.getMessage(MSG_CHANGELOG_COLUMN_BLANK)); + } + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + chlogName = quoteName(this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getChangeLogColumn()); + } else if (Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + if (StringUtil.isBlank(config.getChangeLogColumn())) { + throw new IllegalArgumentException(config.getMessage(MSG_CHANGELOG_COLUMN_BLANK)); + } + tblname = this.sqlHandler.getTable(); + chlogName = quoteName(config.getChangeLogColumn()); + } else { + if (StringUtil.isBlank(config.getChangeLogColumn())) { + throw new IllegalArgumentException(config.getMessage(MSG_CHANGELOG_COLUMN_BLANK)); + } + tblname = config.getTable(); + chlogName = quoteName(config.getChangeLogColumn()); } log.ok("The change log column is ok"); - // Format the update query - final String tblname = config.getTable(); - final String chlogName = quoteName(config.getChangeLogColumn()); final String sql = MessageFormat.format(SQL_SELECT, chlogName, tblname); SyncToken ret = null; @@ -654,7 +1059,7 @@ public SyncToken getLatestSyncToken(ObjectClass oclass) { } } if (ret == null) { - SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(chlogName); + SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), chlogName); ret = new SyncToken(SQLUtil.jdbc2AttributeValue(SQLUtil.getCurrentJdbcTime(columnTypeInfo.getTypeCode()))); } log.ok("getLatestSyncToken", ret); @@ -745,7 +1150,7 @@ public Uid authenticate(ObjectClass oclass, String username, GuardedString passw final String SQL_AUTH_QUERY = "SELECT {0} FROM {1} WHERE ( {0} = ? ) AND ( {2} = ? )"; log.info("check the ObjectClass"); - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } log.ok("The object class is ok"); @@ -764,12 +1169,25 @@ public Uid authenticate(ObjectClass oclass, String username, GuardedString passw } log.ok("The password is ok"); - final String keyColumnName = quoteName(config.getKeyColumn()); + String keyCol; final String passwordColumnName = quoteName(config.getPasswordColumn()); - String sql = MessageFormat.format(SQL_AUTH_QUERY, keyColumnName, config.getTable(), passwordColumnName); - SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(config.getKeyColumn()); + String tblname; + // Sets table name and key column from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + keyCol = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn(); + } else if (Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + tblname = this.sqlHandler.getTable(); + keyCol = config.getKeyColumn(); + } else { + tblname = config.getTable(); + keyCol = config.getKeyColumn(); + } + + String sql = MessageFormat.format(SQL_AUTH_QUERY, quoteName(keyCol), tblname, passwordColumnName); + SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), keyCol); final List values = new ArrayList<>(); - values.add(new SQLParam(keyColumnName, username, columnTypeInfo.getTypeCode(), columnTypeInfo.getTypeName())); // real username + values.add(new SQLParam(quoteName(keyCol), username, columnTypeInfo.getTypeCode(), columnTypeInfo.getTypeName())); // real username values.add(new SQLParam(passwordColumnName, password)); // real password PreparedStatement stmt = null; @@ -796,7 +1214,7 @@ public Uid authenticate(ObjectClass oclass, String username, GuardedString passw } catch (SQLException e) { SQLUtil.rollbackQuietly(getConn()); - evaluateAndHandleException(e, false, true, false, MSG_CAN_NOT_READ, config.getTable()); + evaluateAndHandleException(e, false, true, false, MSG_CAN_NOT_READ, tblname); } finally { IOUtil.quietClose(result); IOUtil.quietClose(stmt); @@ -815,7 +1233,7 @@ public Uid resolveUsername(ObjectClass oclass, String username, OperationOptions final String SQL_AUTH_QUERY = "SELECT {0} FROM {1} WHERE ( {0} = ? )"; log.info("check the ObjectClass"); - if (oclass == null || (!oclass.equals(ObjectClass.ACCOUNT))) { + if (oclass == null) { throw new IllegalArgumentException(config.getMessage(MSG_ACCOUNT_OBJECT_CLASS_REQUIRED)); } log.ok("The object class is ok"); @@ -829,13 +1247,27 @@ public Uid resolveUsername(ObjectClass oclass, String username, OperationOptions } log.ok("The username is ok"); - final String keyColumnName = quoteName(config.getKeyColumn()); final String passwordColumnName = quoteName(config.getPasswordColumn()); - String sql = MessageFormat.format(SQL_AUTH_QUERY, keyColumnName, config.getTable(), passwordColumnName); + String keyCol; + String tblname; + + // Sets table name and key column from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + tblname = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getTable(); + keyCol = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn(); + } else if (Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + tblname = this.sqlHandler.getTable(); + keyCol = config.getKeyColumn(); + } else { + tblname = config.getTable(); + keyCol = config.getKeyColumn(); + } + + String sql = MessageFormat.format(SQL_AUTH_QUERY, quoteName(keyCol), tblname, passwordColumnName); final List values = new ArrayList<>(); - SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(config.getKeyColumn()); - values.add(new SQLParam(keyColumnName, username, columnTypeInfo.getTypeCode(), columnTypeInfo.getTypeName())); // real username + SQLColumnTypeInfo columnTypeInfo = getColumnTypeInfo(oclass.getObjectClassValue(), keyCol); + values.add(new SQLParam(quoteName(keyCol), username, columnTypeInfo.getTypeCode(), columnTypeInfo.getTypeName())); // real username PreparedStatement stmt = null; ResultSet result = null; @@ -861,7 +1293,7 @@ public Uid resolveUsername(ObjectClass oclass, String username, OperationOptions } catch (SQLException e) { SQLUtil.rollbackQuietly(getConn()); - evaluateAndHandleException(e, false, true, false, MSG_CAN_NOT_READ, config.getTable()); + evaluateAndHandleException(e, false, true, false, MSG_CAN_NOT_READ, tblname); } finally { IOUtil.quietClose(result); IOUtil.quietClose(stmt); @@ -888,14 +1320,19 @@ public String quoteName(String value) { * @param columnName the column name * @return the cached column type */ - public SQLColumnTypeInfo getColumnTypeInfo(String columnName) { + public SQLColumnTypeInfo getColumnTypeInfo(String objectClass, String columnName) { if (columnSQLTypes == null) { cacheSchema(); + } else if (Objects.equals(this.config.getConnectorMode(), "Json")) { + if(columnSQLTypesCollection.size() < this.universalObjectClassHandlers.size()) { + cacheSchema(); + } } // no null here :) - assert columnSQLTypes != null; + assert columnSQLTypesCollection != null; + assert columnSQLTypesCollection.get(objectClass) != null; - return columnSQLTypes.get(columnName); + return columnSQLTypesCollection.get(objectClass).get(columnName); } /** @@ -903,14 +1340,22 @@ public SQLColumnTypeInfo getColumnTypeInfo(String columnName) { * * @return the Column Name value */ - public String getColumnName(String attributeName) { + public String getColumnName(ObjectClass oclass, String attributeName) { if (Name.NAME.equalsIgnoreCase(attributeName)) { log.ok("attribute name {0} map to key column", attributeName); - return config.getKeyColumn(); + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + return this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn(); + } else { + return config.getKeyColumn(); + } } if (Uid.NAME.equalsIgnoreCase(attributeName)) { log.ok("attribute name {0} map to key column", attributeName); - return config.getKeyColumn(); + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + return this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn(); + } else { + return config.getKeyColumn(); + } } if (!StringUtil.isBlank(config.getPasswordColumn()) && OperationalAttributes.PASSWORD_NAME.equalsIgnoreCase(attributeName)) { @@ -924,70 +1369,188 @@ public String getColumnName(String attributeName) { * Cache schema, defaultAtributesToGet, columnClassNamens */ private void cacheSchema() { - /* - * First, compute the account attributes based on the database schema - */ - final Set attrInfoSet = buildSelectBasedAttributeInfos(); - - log.info("cacheSchema"); - // Cache the attributes to get - defaultAttributesToGet = new HashSet<>(); - for (AttributeInfo info : attrInfoSet) { - if (info.isReturnedByDefault()) { - defaultAttributesToGet.add(info.getName()); + final SchemaBuilder schemaBld = new SchemaBuilder(getClass()); + defaultAttributesToGet = new HashMap<>(); + if (universalObjectClassHandlers != null) { + for (String oClass : universalObjectClassHandlers.keySet()) { + final Set attrInfoSet = buildSelectBasedAttributeInfos(oClass, universalObjectClassHandlers.get(oClass).getTable(), universalObjectClassHandlers.get(oClass).getKeyColumn()); + + log.info("cacheSchemaNew"); + // Cache the attributes to get + + Set attrs = new HashSet<>(); + for (AttributeInfo info : attrInfoSet) { + if (info.isReturnedByDefault()) { + attrs.add(info.getName()); + } + } + defaultAttributesToGet.put(oClass, attrs); + + /* + * Add any other operational attributes to the attrInfoSet + */ + // attrInfoSet.add(OperationalAttributeInfos.ENABLE); + + /* + * Use SchemaBuilder to build the schema. Currently, only ACCOUNT type is supported. + */ + + final ObjectClassInfoBuilder ociB = new ObjectClassInfoBuilder(); + ociB.setType(oClass); + ociB.addAllAttributeInfo(attrInfoSet); + + final ObjectClassInfo oci = ociB.build(); + schemaBld.defineObjectClass(oci); + + /* + * Note: AuthenticateOp, and all the 'SPIOperation'-s are by default added by Reflection API to the Schema. + * + * See for details: SchemaBuilder.defineObjectClass() --> FrameworkUtil.getDefaultSupportedOperations() + * ReflectionUtil.getAllInterfaces(connector); is the line that *does* acquire the implemented interfaces by the + * connector class. + */ + if (StringUtil.isBlank(config.getPasswordColumn())) { // remove the AuthenticateOp + log.info("no password column, remove the AuthenticateOp"); + schemaBld.removeSupportedObjectClass(AuthenticateOp.class, oci); + } + + if (StringUtil.isBlank(config.getChangeLogColumn())) { // remove the SyncOp + log.info("no changeLog column, remove the SyncOp"); + schemaBld.removeSupportedObjectClass(SyncOp.class, oci); + } + } + } else { + /* + * First, compute the account attributes based on the database schema + */ + String table = config.getTable(); + String keyCol = config.getKeyColumn(); + + Set attrInfoSet; + + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + attrInfoSet = buildSQLSelectBasedAttributeInfos(ObjectClass.ACCOUNT_NAME, + this.sqlHandler.getColumnSchemaFormat(), + this.sqlHandler.getSqlQuery()); + } else { + attrInfoSet = buildSelectBasedAttributeInfos(ObjectClass.ACCOUNT_NAME, table,keyCol); } - } - /* - * Add any other operational attributes to the attrInfoSet - */ - // attrInfoSet.add(OperationalAttributeInfos.ENABLE); + log.info("cacheSchema"); + // Cache the attributes to get + Set attrs = new HashSet<>(); + for (AttributeInfo info : attrInfoSet) { + if (info.isReturnedByDefault()) { + attrs.add(info.getName()); + } + } + defaultAttributesToGet.put(ObjectClass.ACCOUNT_NAME,attrs); + + /* + * Add any other operational attributes to the attrInfoSet + */ + // attrInfoSet.add(OperationalAttributeInfos.ENABLE); + + + final ObjectClassInfoBuilder ociB = new ObjectClassInfoBuilder(); + ociB.setType(ObjectClass.ACCOUNT_NAME); + ociB.addAllAttributeInfo(attrInfoSet); + + final ObjectClassInfo oci = ociB.build(); + schemaBld.defineObjectClass(oci); + + /* + * Note: AuthenticateOp, and all the 'SPIOperation'-s are by default added by Reflection API to the Schema. + * + * See for details: SchemaBuilder.defineObjectClass() --> FrameworkUtil.getDefaultSupportedOperations() + * ReflectionUtil.getAllInterfaces(connector); is the line that *does* acquire the implemented interfaces by the + * connector class. + */ + if (StringUtil.isBlank(config.getPasswordColumn())) { // remove the AuthenticateOp + log.info("no password column, remove the AuthenticateOp"); + schemaBld.removeSupportedObjectClass(AuthenticateOp.class, oci); + } - /* - * Use SchemaBuilder to build the schema. Currently, only ACCOUNT type is supported. - */ - final SchemaBuilder schemaBld = new SchemaBuilder(getClass()); + if (StringUtil.isBlank(config.getChangeLogColumn())) { // remove the SyncOp + log.info("no changeLog column, remove the SyncOp"); + schemaBld.removeSupportedObjectClass(SyncOp.class, oci); + } + } + schema = schemaBld.build(); + log.ok("schema builded"); + } - final ObjectClassInfoBuilder ociB = new ObjectClassInfoBuilder(); - ociB.setType(ObjectClass.ACCOUNT_NAME); - ociB.addAllAttributeInfo(attrInfoSet); + private Set buildSQLSelectBasedAttributeInfos(String oClass, + Map columnSchemaFormat, + String sql) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null"); + } + if (columnSchemaFormat == null || columnSchemaFormat.isEmpty()) { + throw new IllegalArgumentException("columnSchemaFormat must not be null or empty (alias -> table.column)"); + } - final ObjectClassInfo oci = ociB.build(); - schemaBld.defineObjectClass(oci); + String core = sql.trim(); + if (core.endsWith(";")) { + core = core.substring(0, core.length() - 1); + } - /* - * Note: AuthenticateOp, and all the 'SPIOperation'-s are by default added by Reflection API to the Schema. - * - * See for details: SchemaBuilder.defineObjectClass() --> FrameworkUtil.getDefaultSupportedOperations() - * ReflectionUtil.getAllInterfaces(connector); is the line that *does* acquire the implemented interfaces by the - * connector class. - */ - if (StringUtil.isBlank(config.getPasswordColumn())) { // remove the AuthenticateOp - log.info("no password column, remove the AuthenticateOp"); - schemaBld.removeSupportedObjectClass(AuthenticateOp.class, oci); + StringBuilder projection = new StringBuilder(); + int i = 0; + for (String alias : columnSchemaFormat.keySet()) { + if (alias == null || alias.isEmpty()) { + continue; + } + if (i++ > 0) { + projection.append(", "); + } + projection.append("__src.").append(quoteName(alias)) + .append(" AS ").append(quoteName(alias)); } - if (StringUtil.isBlank(config.getChangeLogColumn())) { // remove the SyncOp - log.info("no changeLog column, remove the SyncOp"); - schemaBld.removeSupportedObjectClass(SyncOp.class, oci); + if (projection.length() == 0) { + throw new IllegalArgumentException("columnSchemaFormat has no usable aliases."); } - schema = schemaBld.build(); - log.ok("schema builded"); + String wrapped = "SELECT " + projection + " FROM (" + core + ") AS __src WHERE 1=0"; + + Set attrInfo; + ResultSet rset = null; + Statement stmt = null; + try { + stmt = getConn().getConnection().createStatement(); + log.info("executeQuery ''{0}''", wrapped); + rset = stmt.executeQuery(wrapped); + log.ok("query executed"); + attrInfo = buildAttributeInfoSet(oClass, rset); + log.info("commit get schema"); + commit(); + } catch (SQLException ex) { + log.error(ex, "buildSelectBasedAttributeInfo in SQL: ''{0}''", wrapped); + SQLUtil.rollbackQuietly(getConn()); + throw new ConnectorException(config.getMessage(MSG_CAN_NOT_READ, config.getTable()), ex); + } finally { + IOUtil.quietClose(rset); + IOUtil.quietClose(stmt); + } + log.ok("schema created"); + return attrInfo; } + /** * Get the schema using a SELECT query. * * @return Schema based on a empty SELECT query. */ - private Set buildSelectBasedAttributeInfos() { + private Set buildSelectBasedAttributeInfos(String oClass, String table, String keyColumn) { // Template for a empty query to get the columns of the table. final String schemaQuery = "SELECT * FROM {0} WHERE {1} IS NULL"; log.info("get schema from the table"); Set attrInfo; - String sql = MessageFormat.format(schemaQuery, config.getTable(), quoteName(config.getKeyColumn())); + String sql = MessageFormat.format(schemaQuery, table, quoteName(keyColumn)); + // check out the result etc.. ResultSet rset = null; Statement stmt = null; @@ -999,7 +1562,7 @@ private Set buildSelectBasedAttributeInfos() { rset = stmt.executeQuery(sql); log.ok("query executed"); // get the results queued.. - attrInfo = buildAttributeInfoSet(rset); + attrInfo = buildAttributeInfoSet(oClass, rset); // commit changes log.info("commit get schema"); commit(); @@ -1018,8 +1581,13 @@ private Set buildSelectBasedAttributeInfos() { /** * Return the set of AttributeInfo based on the database query meta-data. */ - private Set buildAttributeInfoSet(ResultSet rset) throws SQLException { + private Set buildAttributeInfoSet(String oClass, ResultSet rset) throws SQLException { log.info("build AttributeInfoSet"); + + if(columnSQLTypesCollection == null) { + columnSQLTypesCollection = CollectionUtil.newCaseInsensitiveMap(); + } + Set attrInfo = new HashSet<>(); columnSQLTypes = CollectionUtil.newCaseInsensitiveMap(); stringColumnRequired = CollectionUtil.newCaseInsensitiveSet(); @@ -1030,30 +1598,80 @@ private Set buildAttributeInfoSet(ResultSet rset) throws SQLExcep log.ok("Name of the parameter being evaluated : {0}", name); - final AttributeInfoBuilder attrBld = new AttributeInfoBuilder(); - final int columnType = meta.getColumnType(i); - final String columnTypeName = meta.getColumnTypeName(i); + AttributeInfoBuilder attrBld = new AttributeInfoBuilder(); + Integer columnType = meta.getColumnType(i); + String columnTypeName = meta.getColumnTypeName(i); + if(meta.getColumnType(i) == 5) { + columnType = 4; + columnTypeName = "int"; + } + + boolean attributeFound = false; columnSQLTypes.put(name, new SQLColumnTypeInfo(columnTypeName, columnType)); - if (name.equalsIgnoreCase(config.getKeyColumn())) { - // name attribute - attrBld.setName(Name.NAME); - //The generate UID make the Name attribute is nor required - attrBld.setRequired(true); - attrInfo.add(attrBld.build()); - log.ok("key column in name attribute in the schema"); - } else if (name.equalsIgnoreCase(config.getPasswordColumn())) { - // Password attribute - attrInfo.add(OperationalAttributeInfos.PASSWORD); - log.ok("password column in password attribute in the schema"); - } else if (name.equalsIgnoreCase(config.getChangeLogColumn())) { - // skip changelog column from the schema. It is not part of the contract - log.ok("skip changelog column from the schema"); + MappingStrategy msTmp; + + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + if (name.equalsIgnoreCase(this.universalObjectClassHandlers.get(oClass).getKeyColumn())) { + // name attribute + attrBld.setName(Name.NAME); + //The generate UID make the Name attribute is nor required + attrBld.setRequired(true); + attrInfo.add(attrBld.build()); + attrBld = new AttributeInfoBuilder(); + attrBld.setName(name); + attrInfo.add(attrBld.build()); + log.ok("key column in name attribute in the schema"); + attributeFound = true; + } else if (name.equalsIgnoreCase(this.universalObjectClassHandlers.get(oClass).getPasswordColumn())) { + // Password attribute + attrInfo.add(OperationalAttributeInfos.PASSWORD); + log.ok("password column in password attribute in the schema"); + attributeFound = true; + } else if (name.equalsIgnoreCase(this.universalObjectClassHandlers.get(oClass).getChangeLogColumn())) { + // skip changelog column from the schema. It is not part of the contract + log.ok("skip changelog column from the schema"); + } + msTmp = conn.createMappingStrategy(conn.getConnection(), this.universalObjectClassHandlers.get(oClass)); } else { + if (name.equalsIgnoreCase(config.getKeyColumn())) { + // name attribute + attrBld.setName(Name.NAME); + //The generate UID make the Name attribute is nor required + attrBld.setRequired(true); + if (Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + attrBld.setUpdateable(false); + attrBld.setCreateable(false); + } + attrInfo.add(attrBld.build()); + attrBld = new AttributeInfoBuilder(); + if (this.sqlHandler.getColumnAliasBySource().containsKey(name)){ + String newName = this.sqlHandler.getColumnAliasBySource().get(name); + attrBld.setName(newName); + }else { + attrBld.setName(name); + } + attrBld.setReadable(false); + attrBld.setReturnedByDefault(false); + attrInfo.add(attrBld.build()); + log.ok("key column in name attribute in the schema"); + attributeFound = true; + } else if (name.equalsIgnoreCase(config.getPasswordColumn())) { + // Password attribute + attrInfo.add(OperationalAttributeInfos.PASSWORD); + log.ok("password column in password attribute in the schema"); + attributeFound = true; + } else if (name.equalsIgnoreCase(config.getChangeLogColumn())) { + // skip changelog column from the schema. It is not part of the contract + log.ok("skip changelog column from the schema"); + } + msTmp = getConn().getSms(); + } + + if (!attributeFound) { // All other attributed taken from the table log.ok("Building attribute info set for standard attribute. "); - MappingStrategy msTmp = getConn().getSms(); log.ok("Datatype fetch finished, used strategy : {0}", msTmp.getClass()); Class dataType = msTmp.getSQLAttributeType(columnType, columnTypeName); @@ -1076,11 +1694,18 @@ private Set buildAttributeInfoSet(ResultSet rset) throws SQLExcep log.ok("the column name {0} is string type and required", name); stringColumnRequired.add(name); } + + if(Objects.equals(this.config.getConnectorMode(), "Custom_SQL_Query")) { + attrBld.setUpdateable(false); + attrBld.setCreateable(false); + } + attrBld.setReturnedByDefault(isReturnedByDefault(dataType)); attrInfo.add(attrBld.build()); log.ok("the column name {0} has data type {1}", name, dataType); } } + columnSQLTypesCollection.put(oClass, columnSQLTypes); log.ok("the Attribute InfoSet is done"); return attrInfo; } @@ -1102,15 +1727,29 @@ private boolean isReturnedByDefault(final Class dataType) { * @param columnValues from the result set * @return ConnectorObjectBuilder object */ - private ConnectorObjectBuilder buildConnectorObject(Map columnValues) { + private ConnectorObjectBuilder buildConnectorObject(ObjectClass oclass, Map columnValues) { log.info("build ConnectorObject"); String uidValue = null; ConnectorObjectBuilder bld = new ConnectorObjectBuilder(); for (Map.Entry colValue : columnValues.entrySet()) { - final String columnName = colValue.getKey(); + String columnName = colValue.getKey(); final SQLParam param = colValue.getValue(); + // Map the special - if (columnName.equalsIgnoreCase(config.getKeyColumn())) { + String keyCol; + String changeLogCol; + // Sets table name and change log column from config specified by connector mode + if(Objects.equals(this.config.getConnectorMode(), "Json")) { + keyCol = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getKeyColumn(); + changeLogCol = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getChangeLogColumn(); + } else { + keyCol = this.config.getKeyColumn(); + changeLogCol = this.config.getChangeLogColumn(); + } + if(!this.sqlHandler.getColumnAliasBySource().isEmpty() && this.sqlHandler.getColumnAliasBySource().containsKey(columnName)) { + columnName = this.sqlHandler.getColumnAliasBySource().get(columnName); + } + if (columnName.equalsIgnoreCase(keyCol)) { if (param == null || param.getValue() == null) { log.error("Name cannot be null."); String msg = "Name cannot be null."; @@ -1133,7 +1772,7 @@ private ConnectorObjectBuilder buildConnectorObject(Map column bld.addAttribute(AttributeBuilder.build(OperationalAttributes.PASSWORD_NAME)); } } - } else if (columnName.equalsIgnoreCase(config.getChangeLogColumn())) { + } else if (columnName.equalsIgnoreCase(changeLogCol)) { //No changelogcolumn attribute in the results log.ok("changelogcolumn attribute in the result"); } else { @@ -1165,7 +1804,7 @@ private ConnectorObjectBuilder buildConnectorObject(Map column // Add Uid attribute to object bld.setUid(new Uid(uidValue)); // only deals w/ accounts.. - bld.setObjectClass(ObjectClass.ACCOUNT); + bld.setObjectClass(oclass); log.ok("ConnectorObject is builded"); return bld; } @@ -1174,14 +1813,21 @@ private ConnectorObjectBuilder buildConnectorObject(Map column * Construct a SyncDeltaBuilder the sync builder *

Taking care about special attributes

* + * @param oclass * @param columnValues from the resultSet * @return SyncDeltaBuilder the sync builder */ - private SyncDeltaBuilder buildSyncDelta(Map columnValues) { + private SyncDeltaBuilder buildSyncDelta(ObjectClass oclass, Map columnValues) { log.info("buildSyncDelta"); SyncDeltaBuilder bld = new SyncDeltaBuilder(); // Find a token - SQLParam tokenParam = columnValues.get(config.getChangeLogColumn()); + String changeLogCol; + if(Objects.equals(config.getConnectorMode(), "Json")) { + changeLogCol = this.universalObjectClassHandlers.get(oclass.getObjectClassValue()).getChangeLogColumn(); + } else { + changeLogCol = config.getChangeLogColumn(); + } + SQLParam tokenParam = columnValues.get(changeLogCol); if (tokenParam == null) { throw new IllegalArgumentException(config.getMessage(MSG_INVALID_SYNC_TOKEN_VALUE)); @@ -1195,7 +1841,7 @@ private SyncDeltaBuilder buildSyncDelta(Map columnValues) { // To be sure that sync token is present bld.setToken(new SyncToken(token)); - bld.setObject(buildConnectorObject(columnValues).build()); + bld.setObject(buildConnectorObject(oclass,columnValues).build()); // only deals w/ updates bld.setDeltaType(SyncDeltaType.CREATE_OR_UPDATE); @@ -1203,8 +1849,8 @@ private SyncDeltaBuilder buildSyncDelta(Map columnValues) { return bld; } - private Set resolveColumnNamesToGet(OperationOptions options) { - Set attributesToGet = getDefaultAttributesToGet(); + private Set resolveColumnNamesToGet(ObjectClass oclass, OperationOptions options) { + Set attributesToGet = getDefaultAttributesToGet(oclass); if (options != null && options.getAttributesToGet() != null) { attributesToGet = CollectionUtil.newSet(options.getAttributesToGet()); attributesToGet.add(Uid.NAME); // Ensure the Uid colum is there @@ -1212,7 +1858,7 @@ private Set resolveColumnNamesToGet(OperationOptions options) { // Replace attributes to quoted columnNames Set columnNamesToGet = new HashSet<>(); for (String attributeName : attributesToGet) { - final String columnName = getColumnName(attributeName); + final String columnName = getColumnName(oclass, attributeName); columnNamesToGet.add(quoteName(columnName)); } return columnNamesToGet; @@ -1223,12 +1869,12 @@ private Set resolveColumnNamesToGet(OperationOptions options) { * * @return the Set of default attribute names */ - private Set getDefaultAttributesToGet() { - if (defaultAttributesToGet == null) { + private Set getDefaultAttributesToGet(ObjectClass oclass) { + if (defaultAttributesToGet.get(oclass.getObjectClassValue()) == null) { cacheSchema(); } assert defaultAttributesToGet != null; - return defaultAttributesToGet; + return defaultAttributesToGet.get(oclass.getObjectClassValue()); } /** @@ -1332,17 +1978,6 @@ private void handleBasedOnSQLState(SQLException e, Boolean logErr, Boolean check logErr, config.getMessage(message, messageParameters)); return; } - /// TODO should we leave the defaults for already exists ? -/* if (alreadyExistsSqlStates == null) { - if (DEFAULT_SQLSTATE_UNIQUE_CONSTRAIN_VIOLATION.equals(sqlState) || - DEFAULT_SQLSTATE_INTEGRITY_CONSTRAIN_VIOLATION.equals(sqlState)) { - - log.ok("sqlState exception handling for AlreadyExistsException based on default sqlState values."); - evaluateAndThrow(new AlreadyExistsException(e), e, checkIfRethrow, - logErr, config.getMessage(MSG_OP_ALREADY_EXISTS, messageParameters)); - return; - } - }*/ } // DEFAULT diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java index f3f68e2d..910d7f0b 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableConstants.java @@ -76,6 +76,9 @@ public class DatabaseTableConstants { static final String MSG_EXP_UNKNOWN_UID = "exception.unknown.uid"; static final String MSG_EXP_TOO_MANY_UID = "exception.more.than.one.uid"; + static final String MSG_JSON_FILE_BLANK = "json.file.blank"; + static final String MSG_SQL_FILE_BLANK = "sql.file.blank"; + // public static final String DEFAULT_SQLSTATE_UNIQUE_CONSTRAIN_VIOLATION = "23505"; // public static final String DEFAULT_SQLSTATE_INTEGRITY_CONSTRAIN_VIOLATION = "23000"; diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableFilterTranslator.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableFilterTranslator.java index 095335f9..b5aa5361 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableFilterTranslator.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableFilterTranslator.java @@ -57,8 +57,8 @@ public DatabaseTableFilterTranslator(DatabaseTableConnector connector, ObjectCla @Override protected SQLParam getSQLParam(Attribute attribute, ObjectClass oclass, OperationOptions options) { final Object value = AttributeUtil.getSingleValue(attribute); - String columnName = connector.quoteName(connector.getColumnName(attribute.getName())); - SQLColumnTypeInfo columnTypeInfo = connector.getColumnTypeInfo(columnName); + String columnName = connector.quoteName(connector.getColumnName(oclass, attribute.getName())); + SQLColumnTypeInfo columnTypeInfo = connector.getColumnTypeInfo(oclass.getObjectClassValue(), columnName); if (columnTypeInfo != null) { } else { @@ -74,7 +74,7 @@ protected SQLParam getSQLParam(Attribute attribute, ObjectClass oclass, Operatio if (quoteValue.contains(firstChar) && quoteValue.contains(lastChar)) { String trimmedColumnName = columnName.substring(1, columnName.length() - 1); //2nd attempt to find the column type information, removing double quotes - columnTypeInfo = connector.getColumnTypeInfo(trimmedColumnName); + columnTypeInfo = connector.getColumnTypeInfo(oclass.getObjectClassValue(), trimmedColumnName); } } else { diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableSQLUtil.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableSQLUtil.java index b3aa1628..3e92cd0a 100644 --- a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableSQLUtil.java +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/DatabaseTableSQLUtil.java @@ -129,7 +129,7 @@ public static Map getColumnValues(final MappingStrategy sms, R final ResultSetMetaData meta = resultSet.getMetaData(); int count = meta.getColumnCount(); for (int i = 1; i <= count; i++) { - final String name = meta.getColumnName(i); + final String name = meta.getColumnLabel(i); final int sqlType = meta.getColumnType(i); final String sqlTypeName = meta.getColumnTypeName(i); diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/JsonConfigHandler.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/JsonConfigHandler.java new file mode 100644 index 00000000..020c7abb --- /dev/null +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/JsonConfigHandler.java @@ -0,0 +1,83 @@ +package org.identityconnectors.databasetable.config; + +import org.identityconnectors.databasetable.DatabaseTableConfiguration; + +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; + +public class JsonConfigHandler { + + private Map universalObjectClassHandlers; + + public JsonConfigHandler(DatabaseTableConfiguration config) { + this.universalObjectClassHandlers = new HashMap<>(); + this.parseJson(config); + } + + public Map getUniversalObjectClassHandlers() { return this.universalObjectClassHandlers;} + + // Method for parsing a JSON file and creating a list of SchemaType objects + public void parseJson(DatabaseTableConfiguration config) { + try (FileReader fileReader = new FileReader(config.getJsonFilePath()); + JsonReader jsonReader = Json.createReader(fileReader)) { + + // Loading a JSON file and converting it to a JsonObject + JsonObject jsonSchema = jsonReader.readObject(); + // Retrieving an array of objects from the file + JsonArray objectsArray = jsonSchema.getJsonArray("objects"); + + // Iterating through all objects in the array + for (int i = 0; i < objectsArray.size(); i++) { + UniversalObjectClassHandler universalObjectClassHandler = new UniversalObjectClassHandler(); + + JsonObject object = objectsArray.getJsonObject(i); + + // Retrieving values from the current object + universalObjectClassHandler.setConfig(config); + universalObjectClassHandler.setObjectClassName(object.getString("objectClassName")); + + JsonObject configProperties = object.getJsonObject("configurationProperties"); + + // Retrieving configuration property values from the new configuration object + universalObjectClassHandler.setTable(configProperties.getString("Table")); + universalObjectClassHandler.setKeyColumn(configProperties.getString("Key Column")); + + if (configProperties.containsKey("Enable writing empty string")) { + universalObjectClassHandler.setEnableWritingEmptyString(configProperties.getBoolean("Enable writing empty string")); + } + if (configProperties.containsKey("Change Log Column (Sync)")) { + universalObjectClassHandler.setChangeLogColumn(configProperties.getString("Change Log Column (Sync)")); + } + if (configProperties.containsKey("Sync Order Column")) { + universalObjectClassHandler.setSyncOrderColumn(configProperties.getString("Sync Order Column")); + } + if (configProperties.containsKey("Sync Order Asc")) { + universalObjectClassHandler.setSyncOrderAsc(configProperties.getBoolean("Sync Order Asc")); + } + if (configProperties.containsKey("Suppress Password")) { + universalObjectClassHandler.setSuppressPassword(configProperties.getBoolean("Suppress Password")); + } + // Retrieving optional configuration property values from the new configuration object + if (configProperties.containsKey("All native")) { + universalObjectClassHandler.setSuppressPassword(configProperties.getBoolean("All native")); + } + if (configProperties.containsKey("Native Timestamps")) { + universalObjectClassHandler.setSuppressPassword(configProperties.getBoolean("Native Timestamps")); + } + + // Creating an instance of SchemaTypeAttribute for each configuration property + this.universalObjectClassHandlers.put(universalObjectClassHandler.getObjectClassName(), universalObjectClassHandler); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/SqlHandler.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/SqlHandler.java new file mode 100644 index 00000000..76aa10ab --- /dev/null +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/SqlHandler.java @@ -0,0 +1,745 @@ +package org.identityconnectors.databasetable.config; + +import org.identityconnectors.databasetable.DatabaseTableConfiguration; +import org.identityconnectors.databasetable.DatabaseTableConnector; +import org.identityconnectors.databasetable.mapping.misc.JoinDef; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.identityconnectors.common.logging.Log; + +public class SqlHandler { + + private String baseTable; + private String whereClause; + private Set columns; + private String sqlQuery; + + private int whereIndex = -1; + + private final Map columnAliasBySource = new LinkedHashMap<>(); + private final Map sourceByColumnAlias = new LinkedHashMap<>(); + private final Map tableAliases = new LinkedHashMap<>(); + private final List joins = new ArrayList<>(); + private final Map columnSchemaFormat = new LinkedHashMap<>(); + static Log log = Log.getLog(DatabaseTableConnector.class); + + public SqlHandler(DatabaseTableConfiguration config) { + this.parseSql(config); + } + + public void parseSql(DatabaseTableConfiguration config) { + try (BufferedReader reader = new BufferedReader(new FileReader(config.getSqlFilePath()))) { + StringBuilder sqlBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sqlBuilder.append(line).append("\n"); + } + String raw = sqlBuilder.toString(); + + setSqlQuery(raw); + parseFromAndJoins(this.sqlQuery); + parseSelect(this.sqlQuery); + setWhereClause(this.sqlQuery); + + log.ok("commit SQL was parsed"); + log.ok("Table aliases are as follows:\n", tableAliases); + log.ok("Column aliases are as follows:\n", sourceByColumnAlias); + log.ok("Joins are as follows:\n", joins); + + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void setSqlQuery(String sqlQuery) { + sqlQuery = sqlQuery.replaceAll("\\s+", " ").trim(); + this.sqlQuery = sqlQuery; + } + + private void parseSelect(String sql) { + String up = sql.toUpperCase(Locale.ROOT); + int sel = up.indexOf("SELECT "); + int from = up.indexOf(" FROM "); + if (sel < 0 || from < 0 || from <= sel + 6) { + throw new RuntimeException("SELECT/FROM segment not found or malformed."); + } + String selectPart = sql.substring(sel + 7, from).trim(); + + List items = splitSelectItems(selectPart); + Set rawCols = new LinkedHashSet<>(); + + Pattern asAlias = Pattern.compile("(?i)\\s+AS\\s+([\\w\"`\\[\\].]+)\\s*$"); + Pattern spaceAlias = Pattern.compile("(?i)\\s+([\\w\"`\\[\\].]+)\\s*$"); + + for (String item : items) { + String trimmed = item.trim(); + String sourceExpr = trimmed; + String alias = null; + + Matcher mAs = asAlias.matcher(trimmed); + if (mAs.find()) { + alias = stripQuotes(mAs.group(1)); + sourceExpr = trimmed.substring(0, mAs.start()).trim(); + } else { + Matcher mSp = spaceAlias.matcher(trimmed); + if (mSp.find()) { + String last = stripQuotes(mSp.group(1)); + if (!last.equalsIgnoreCase("DESC") + && !last.equalsIgnoreCase("ASC") + && !last.equalsIgnoreCase("NULLS") + && !last.equalsIgnoreCase("FIRST") + && !last.equalsIgnoreCase("LAST")) { + alias = last; + sourceExpr = trimmed.substring(0, mSp.start()).trim(); + } + } + } + + rawCols.add(sourceExpr); + + String resolvedForSchema = guessSourceColumn(sourceExpr); + if (resolvedForSchema == null) { + resolvedForSchema = extractPrimaryColumnFromExpr(sourceExpr); + } + + if (alias != null) { + columnAliasBySource.put(sourceExpr, alias); + sourceByColumnAlias.put(alias, sourceExpr); + + if (resolvedForSchema != null) { + columnSchemaFormat.put(alias, resolvedForSchema); + } + } else { + if (resolvedForSchema != null) { + columnSchemaFormat.put(resolvedForSchema, resolvedForSchema); + } + } + } + + this.columns = rawCols; + + if (!columnSchemaFormat.isEmpty()) { + log.ok("Column schema format (alias -> table.column):\n", columnSchemaFormat); + } + } + + private String extractPrimaryColumnFromExpr(String expr) { + if (expr == null || expr.isEmpty()) { + return null; + } + + String noStrings = stripStringLiterals(expr); + + Pattern colRef = Pattern.compile("(?i)(?:^|[^A-Za-z0-9_])([A-Za-z0-9_\"`\\[\\]]+)\\.([A-Za-z0-9_\"`\\[\\]]+)(?:[^A-Za-z0-9_]|$)"); + Matcher m = colRef.matcher(noStrings); + while (m.find()) { + String qualifierRaw = stripQuotes(m.group(1)); + String columnRaw = stripQuotes(m.group(2)); + String qualified = resolveQualified(qualifierRaw, columnRaw); + if (qualified != null) { + return qualified; + } + } + + return null; + } + + private String resolveQualified(String qualifier, String column) { + if (column == null || column.isEmpty()) { + return null; + } + String qual = (qualifier != null) ? qualifier.trim() : ""; + String col = column.trim(); + + int qdot = qual.lastIndexOf('.'); + String lastSegment = (qdot >= 0) ? qual.substring(qdot + 1).trim() : qual; + + String mapped = this.tableAliases.get(lastSegment); + if (mapped != null && !mapped.isEmpty()) { + return mapped + "." + col; + } + + if (!qual.isEmpty()) { + return qual + "." + col; + } else { + return col; + } + } + + private String stripStringLiterals(String s) { + StringBuilder out = new StringBuilder(s.length()); + boolean inSingle = false; + boolean inDouble = false; + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (!inDouble && c == '\'') { + if (inSingle) { + char next = (i + 1 < s.length()) ? s.charAt(i + 1) : '\0'; + if (next == '\'') { + i++; + out.append(' '); + out.append(' '); + continue; + } else { + inSingle = false; + out.append(' '); + continue; + } + } else { + inSingle = true; + out.append(' '); + continue; + } + } + + if (!inSingle && c == '\"') { + if (inDouble) { + char next = (i + 1 < s.length()) ? s.charAt(i + 1) : '\0'; + if (next == '\"') { + i++; + out.append(' '); + out.append(' '); + continue; + } else { + inDouble = false; + out.append(' '); + continue; + } + } else { + inDouble = true; + out.append(' '); + continue; + } + } + + if (inSingle || inDouble) { + out.append(' '); + } else { + out.append(c); + } + } + + return out.toString(); + } + + + private void parseFromAndJoins(String sql) { + String up = sql.toUpperCase(Locale.ROOT); + + int fromIdx = up.indexOf(" FROM "); + if (fromIdx < 0) { + throw new RuntimeException("FROM keyword does not exist."); + } + + int end = findFirstOf(up, List.of(" WHERE ", " GROUP BY ", " ORDER BY ", ";"), fromIdx + 6); + if (end < 0) { + end = sql.length(); + } + + String fromPart = sql.substring(fromIdx + 6, end).trim(); + + int joinPos = indexOfJoin(fromPart.toUpperCase(Locale.ROOT)); + String firstTableChunk = (joinPos >= 0) ? fromPart.substring(0, joinPos).trim() : fromPart; + + String[] toks = firstTableChunk.split("\\s+"); + if (toks.length >= 1) { + String t0 = stripComma(toks[0]); + String realTable = stripQuotes(t0); + String alias = null; + if (toks.length >= 3 && toks[1].equalsIgnoreCase("AS")) { + alias = stripQuotes(stripComma(toks[2])); + } else if (toks.length >= 2 && !toks[1].equalsIgnoreCase("AS")) { + alias = stripQuotes(stripComma(toks[1])); + } + + this.baseTable = realTable; + if (alias != null) { + tableAliases.put(alias, realTable); + } else { + tableAliases.put(realTable, realTable); + } + } else { + throw new RuntimeException("Cannot parse base table from FROM clause."); + } + + if (joinPos >= 0) { + String joinSection = fromPart.substring(joinPos).trim(); + parseJoins(joinSection); + } + } + + private void parseJoins(String joinSection) { + Pattern p = Pattern.compile( + "(?i)\\b((LEFT|RIGHT|INNER|FULL|CROSS)\\s+JOIN|JOIN)\\s+([^\\s]+)(?:\\s+(?:AS\\s+)?(\\w+))?\\s+ON\\s+(.+?)(?=(\\s+(?:LEFT|RIGHT|INNER|FULL|CROSS)\\s+JOIN|\\s+JOIN|\\s+WHERE|\\s+GROUP\\s+BY|\\s+ORDER\\s+BY|;|$))" + ); + Matcher m = p.matcher(joinSection); + while (m.find()) { + String joinType = m.group(1).trim(); + String tbl = stripQuotes(m.group(3).trim()); + String alias = m.group(4) != null ? stripQuotes(m.group(4).trim()) : null; + String on = m.group(5).trim(); + + if (alias != null) { + tableAliases.put(alias, tbl); + } + tableAliases.put(tbl, tbl); + + joins.add(new JoinDef(joinType.toUpperCase(Locale.ROOT), tbl, alias, on)); + } + } + + private String guessSourceColumn(String expr) { + String e = expr.trim(); + if (e.startsWith("(") && e.endsWith(")")) { + e = e.substring(1, e.length() - 1).trim(); + } + if (e.matches(".*\\s+.*")) { + return null; + } + if (e.contains("(") || e.contains(")")) { + return null; + } + + int dot = e.lastIndexOf('.'); + if (dot < 0) { + return e; + } + + String qualifier = e.substring(0, dot).trim(); + String column = e.substring(dot + 1).trim(); + + int qdot = qualifier.lastIndexOf('.'); + String lastSegment = (qdot >= 0) ? qualifier.substring(qdot + 1).trim() : qualifier; + + String mapped = this.tableAliases.get(lastSegment); + if (mapped != null && !mapped.isEmpty() && !mapped.equals(lastSegment)) { + if (qdot >= 0) { + qualifier = qualifier.substring(0, qdot + 1) + mapped; + } else { + qualifier = mapped; + } + } + + return qualifier + "." + column; + } + + private List splitSelectItems(String selectPart) { + List res = new ArrayList<>(); + StringBuilder cur = new StringBuilder(); + int paren = 0; + boolean inSingle = false; + boolean inDouble = false; + + for (int i = 0; i < selectPart.length(); i++) { + char c = selectPart.charAt(i); + if (c == '\'' && !inDouble) { + inSingle = !inSingle; + cur.append(c); + continue; + } + if (c == '\"' && !inSingle) { + inDouble = !inDouble; + cur.append(c); + continue; + } + if (!inSingle && !inDouble) { + if (c == '(') { + paren++; + } else if (c == ')') { + paren--; + } else if (c == ',' && paren == 0) { + res.add(cur.toString()); + cur.setLength(0); + continue; + } + } + cur.append(c); + } + if (cur.length() > 0) { + res.add(cur.toString()); + } + return res; + } + + private int findFirstOf(String haystackUp, List needlesUp, int fromIdx) { + int best = -1; + for (String n : needlesUp) { + int idx = haystackUp.indexOf(n, fromIdx); + if (idx >= 0 && (best < 0 || idx < best)) { + best = idx; + } + } + return best; + } + + private int indexOfJoin(String fromUp) { + Pattern p = Pattern.compile("\\b(LEFT|RIGHT|INNER|FULL|CROSS)\\s+JOIN\\b|\\bJOIN\\b"); + Matcher m = p.matcher(fromUp); + if (m.find()) { + return m.start(); + } + return -1; + } + + private String stripQuotes(String s) { + if (s == null) { + return null; + } + String x = s.trim(); + if ((x.startsWith("\"") && x.endsWith("\"")) || + (x.startsWith("`") && x.endsWith("`")) || + (x.startsWith("[") && x.endsWith("]"))) { + return x.substring(1, x.length() - 1); + } + return x; + } + + private String stripComma(String s) { + if (s == null) { + return null; + } + String x = s.trim(); + if (x.endsWith(",")) { + return x.substring(0, x.length() - 1); + } + return x; + } + + private void setWhereClause(String sql) { + if (sql == null || sql.isEmpty()) { + this.whereIndex = -1; + this.whereClause = null; + return; + } + + int lastFrom = findLastTopLevelKeyword(sql, "FROM"); + if (lastFrom < 0) { + this.whereIndex = -1; + this.whereClause = null; + return; + } + + int wIdx = findFirstTopLevelKeywordFrom(sql, "WHERE", lastFrom + 4); + if (wIdx >= 0) { + this.whereIndex = wIdx; + + int end = findFirstOfTopLevelClauses(sql, wIdx + 5); + if (end < 0) { + end = sql.length(); + } + + int contentStart = wIdx + "WHERE".length(); + if (contentStart < sql.length() && Character.isWhitespace(sql.charAt(contentStart))) { + contentStart++; + } + this.whereClause = sql.substring(contentStart, end).trim(); + return; + } + + int tail = findFirstOfTopLevelClauses(sql, lastFrom + 4); + if (tail < 0) { + tail = sql.length(); + } + this.whereIndex = tail; + this.whereClause = null; + } + + private static int findLastTopLevelKeyword(String sql, String keyword) { + String up = sql.toUpperCase(Locale.ROOT); + String kw = keyword.toUpperCase(Locale.ROOT); + + int depth = 0; + Character inQuote = null; + boolean inLineComment = false; + boolean inBlockComment = false; + + int lastIdx = -1; + + for (int i = 0; i < up.length(); i++) { + char c = up.charAt(i); + char n = (i + 1 < up.length()) ? up.charAt(i + 1) : '\0'; + + // komentáre + if (inQuote == null) { + if (!inBlockComment && !inLineComment && c == '-' && n == '-') { + inLineComment = true; i++; + continue; + } + if (!inBlockComment && !inLineComment && c == '/' && n == '*') { + inBlockComment = true; i++; + continue; + } + if (inLineComment && (c == '\n' || c == '\r')) { + inLineComment = false; + continue; + } + if (inBlockComment && c == '*' && n == '/') { + inBlockComment = false; i++; + continue; + } + if (inLineComment || inBlockComment) { + continue; + } + } + + // stringy / identifikátory + if (!inLineComment && !inBlockComment) { + if (inQuote != null) { + if (c == inQuote) { + char next = (i + 1 < up.length()) ? up.charAt(i + 1) : '\0'; + if (next == inQuote) { + i++; + continue; + } + inQuote = null; + } + continue; + } else { + if (c == '\'' || c == '"' || c == '`') { + inQuote = c; + continue; + } + } + } + + // hĺbka zátvoriek + if (c == '(') { + depth++; + continue; + } + if (c == ')') { + if (depth > 0) { + depth--; + } + continue; + } + + // match iba na depth==0 a hraniciach slova + if (depth == 0) { + int m = kw.length(); + if (i + m <= up.length() && up.regionMatches(i, kw, 0, m)) { + boolean leftOk = (i == 0) || (!Character.isLetterOrDigit(up.charAt(i - 1)) && up.charAt(i - 1) != '_'); + boolean rightOk = (i + m == up.length()) || (!Character.isLetterOrDigit(up.charAt(i + m)) && up.charAt(i + m) != '_'); + if (leftOk && rightOk) { + lastIdx = i; + } + } + } + } + return lastIdx; + } + + private static int findFirstTopLevelKeywordFrom(String sql, String keyword, int startIndex) { + String up = sql.toUpperCase(Locale.ROOT); + String kw = keyword.toUpperCase(Locale.ROOT); + + int depth = 0; + Character inQuote = null; + boolean inLineComment = false; + boolean inBlockComment = false; + + for (int i = Math.max(0, startIndex); i < up.length(); i++) { + char c = up.charAt(i); + char n = (i + 1 < up.length()) ? up.charAt(i + 1) : '\0'; + + // komentáre + if (inQuote == null) { + if (!inBlockComment && !inLineComment && c == '-' && n == '-') { + inLineComment = true; i++; + continue; + } + if (!inBlockComment && !inLineComment && c == '/' && n == '*') { + inBlockComment = true; i++; + continue; + } + if (inLineComment && (c == '\n' || c == '\r')) { + inLineComment = false; + continue; + } + if (inBlockComment && c == '*' && n == '/') { + inBlockComment = false; i++; + continue; + } + if (inLineComment || inBlockComment) { + continue; + } + } + + // stringy / identifikátory + if (!inLineComment && !inBlockComment) { + if (inQuote != null) { + if (c == inQuote) { + char next = (i + 1 < up.length()) ? up.charAt(i + 1) : '\0'; + if (next == inQuote) { + i++; + continue; + } + inQuote = null; + } + continue; + } else { + if (c == '\'' || c == '"' || c == '`') { + inQuote = c; + continue; + } + } + } + + // hĺbka + if (c == '(') { + depth++; + continue; + } + if (c == ')') { + if (depth > 0) { + depth--; + } + continue; + } + + if (depth == 0) { + int m = kw.length(); + if (i + m <= up.length() && up.regionMatches(i, kw, 0, m)) { + boolean leftOk = (i == 0) || (!Character.isLetterOrDigit(up.charAt(i - 1)) && up.charAt(i - 1) != '_'); + boolean rightOk = (i + m == up.length()) || (!Character.isLetterOrDigit(up.charAt(i + m)) && up.charAt(i + m) != '_'); + if (leftOk && rightOk) { + return i; + } + } + } + } + return -1; + } + + private static int findFirstOfTopLevelClauses(String sql, int startIndex) { + String up = sql.toUpperCase(Locale.ROOT); + String[] clauses = new String[] { "GROUP BY", "HAVING", "ORDER BY", "LIMIT", "OFFSET", "FETCH" }; + + int depth = 0; + Character inQuote = null; + boolean inLineComment = false; + boolean inBlockComment = false; + + for (int i = Math.max(0, startIndex); i < up.length(); i++) { + char c = up.charAt(i); + char n = (i + 1 < up.length()) ? up.charAt(i + 1) : '\0'; + + // komentáre + if (inQuote == null) { + if (!inBlockComment && !inLineComment && c == '-' && n == '-') { + inLineComment = true; i++; + continue; + } + if (!inBlockComment && !inLineComment && c == '/' && n == '*') { + inBlockComment = true; i++; + continue; + } + if (inLineComment && (c == '\n' || c == '\r')) { + inLineComment = false; + continue; + } + if (inBlockComment && c == '*' && n == '/') { + inBlockComment = false; i++; + continue; + } + if (inLineComment || inBlockComment) { + continue; + } + } + + // stringy / identifikátory + if (!inLineComment && !inBlockComment) { + if (inQuote != null) { + if (c == inQuote) { + char next = (i + 1 < up.length()) ? up.charAt(i + 1) : '\0'; + if (next == inQuote) { + i++; + continue; + } + inQuote = null; + } + continue; + } else { + if (c == '\'' || c == '"' || c == '`') { + inQuote = c; + continue; + } + } + } + + // hĺbka + if (c == '(') { + depth++; + continue; + } + if (c == ')') { + if (depth > 0) { + depth--; + } + continue; + } + + if (depth != 0) { + continue; + } + + for (String clause : clauses) { + int m = clause.length(); + if (i + m <= up.length() && up.regionMatches(i, clause, 0, m)) { + boolean leftOk = (i == 0) || (!Character.isLetterOrDigit(up.charAt(i - 1)) && up.charAt(i - 1) != '_'); + boolean rightOk = (i + m == up.length()) || (!Character.isLetterOrDigit(up.charAt(i + m)) && up.charAt(i + m) != '_'); + if (leftOk && rightOk) { + return i; + } + } + } + } + return -1; + } + + public String getSqlQuery() { + return this.sqlQuery; + } + + public String getTable() { + return this.baseTable; + } + + public Set getColumns() { + return this.columns; + } + + public String getWhereClause() { + return this.whereClause; + } + + public int getWhereIndex() { + return this.whereIndex; + } + + public Map getColumnAliasBySource() { + return Collections.unmodifiableMap(columnAliasBySource); + } + + public Map getSourceByColumnAlias() { + return Collections.unmodifiableMap(sourceByColumnAlias); + } + + public Map getTableAliases() { + return Collections.unmodifiableMap(tableAliases); + } + + public List getJoins() { + return Collections.unmodifiableList(joins); + } + + public Map getColumnSchemaFormat() { + return Collections.unmodifiableMap(columnSchemaFormat); + } +} diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/UniversalObjectClassHandler.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/UniversalObjectClassHandler.java new file mode 100644 index 00000000..bd05bffe --- /dev/null +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/config/UniversalObjectClassHandler.java @@ -0,0 +1,115 @@ +package org.identityconnectors.databasetable.config; + +import org.identityconnectors.databasetable.DatabaseTableConfiguration; + + +public class UniversalObjectClassHandler extends DatabaseTableConfiguration { + private DatabaseTableConfiguration config; + private String objectClassName; + + private String table; + private String keyColumn; + private boolean enableWritingEmptyString; + private String changeLogColumnSync; + private String syncOrderColumn; + private boolean syncOrderAsc; + private boolean suppressPassword; + private boolean allNative; + private boolean nativeTimeStamp; + + public DatabaseTableConfiguration getConfig() { + return config; + } + + public void setConfig(DatabaseTableConfiguration config) { + this.config = config; + } + + public String getObjectClassName() { + return objectClassName; + } + + public void setObjectClassName(String objectClassName) { + this.objectClassName = objectClassName; + } + + @Override + public String getTable() { + return table; + } + + @Override + public void setTable(String table) { + this.table = table; + } + + @Override + public String getKeyColumn() { + return keyColumn; + } + + @Override + public void setKeyColumn(String keyColumn) { + this.keyColumn = keyColumn; + } + + public boolean getEnableWritingEmptyString() { + return enableWritingEmptyString; + } + + public void setEnableWritingEmptyString(boolean enableWritingEmptyString) { + this.enableWritingEmptyString = enableWritingEmptyString; + } + + public String getChangeLogColumnSync() { + return changeLogColumnSync; + } + + public void setChangeLogColumnSync(String changeLogColumnSync) { + this.changeLogColumnSync = changeLogColumnSync; + } + + @Override + public String getSyncOrderColumn() { + return syncOrderColumn; + } + + @Override + public void setSyncOrderColumn(String syncOrderColumn) { + this.syncOrderColumn = syncOrderColumn; + } + + @Override + public Boolean getSyncOrderAsc() { + return syncOrderAsc; + } + + public void setSyncOrderAsc(Boolean syncOrderAsc) { + this.syncOrderAsc = syncOrderAsc; + } + + @Override + public boolean getSuppressPassword() { + return suppressPassword; + } + + public void setSuppressPassword(boolean suppressPassword) { + this.suppressPassword = suppressPassword; + } + + public boolean getAllNative() { + return allNative; + } + + public void setAllNative(boolean allNative) { + this.allNative = allNative; + } + + public boolean getNativeTimeStamp() { + return nativeTimeStamp; + } + + public void setNativeTimeStamp(boolean nativeTimeStamp) { + this.nativeTimeStamp = nativeTimeStamp; + } +} diff --git a/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/mapping/misc/JoinDef.java b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/mapping/misc/JoinDef.java new file mode 100644 index 00000000..c535bf19 --- /dev/null +++ b/connectors/java/databasetable/src/main/java/org/identityconnectors/databasetable/mapping/misc/JoinDef.java @@ -0,0 +1,29 @@ +package org.identityconnectors.databasetable.mapping.misc; + +public class JoinDef { + private final String type; // "JOIN", "LEFT JOIN", "INNER JOIN", ... + private final String table; // reálna tabuľka + private final String alias; // môže byť null + private final String onClause; // surový ON výraz + + public JoinDef(String type, String table, String alias, String onClause) { + this.type = type; + this.table = table; + this.alias = alias; + this.onClause = onClause; + } + public String getType() { return type; } + public String getTable() { return table; } + public String getAlias() { return alias; } + public String getOnClause() { return onClause; } + + @Override + public String toString() { + return "JoinDef{" + + "type='" + type + '\'' + + ", table='" + table + '\'' + + ", alias='" + alias + '\'' + + ", onClause='" + onClause + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties b/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties index d4085bce..40212427 100644 --- a/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties +++ b/connectors/java/databasetable/src/main/resources/org/identityconnectors/databasetable/Messages.properties @@ -119,3 +119,11 @@ SQL_STATE_INVALID_ATTRIBUTE_VALUE_DISPLAY=Invalid Attribute Value SQL state code SQL_STATE_INVALID_ATTRIBUTE_VALUE_HELP=Collection of values representing SQL state codes which can be interpreted to create an Invalid Attribute Value exception. SQL_STATE_CONFIGURATION_EXCEPTION_DISPLAY=Configuration Exception SQL state codes SQL_STATE_CONFIGURATION_EXCEPTION_HELP=Collection of values representing SQL state codes which can be interpreted to create an Configuration exception. +CUSTOM_JSONFILE_DISPLAY_KEY=JSON file path +CUSTOM_JSONFILE_HELP_KEY=Enter the path for the JSON file +json.file.blank=Json file path configuration property is empty. +CUSTOM_CONNECTOR_MODE_DISPLAY_KEY=Connector version +CUSTOM_CONNECTOR_MODE_HELP_KEY=Define which connector version you want to use: Basic, Json or Custom_SQL_Query +CUSTOM_SQL_FILE_PATH_DISPLAY_KEY=SQL query file path to get all +CUSTOM_SQL_FILE_PATH_HELP_KEY=Enter the path for the SQL file query to get all +sql.file.blank=SQL query file path configuration property is empty. \ No newline at end of file