diff --git a/gxdynamodb/pom.xml b/gxdynamodb/pom.xml new file mode 100644 index 000000000..23687c610 --- /dev/null +++ b/gxdynamodb/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.genexus + parent + ${revision}${changelist} + + + gxdynamodb + GeneXus DynamoDB + + + + + software.amazon.awssdk + bom + 2.17.151 + pom + import + + + + + + software.amazon.awssdk + dynamodb + + + ${project.groupId} + gxclassR + ${project.version} + + + com.genexus + gxcommon + ${project.version} + compile + + + + + gxdynamodb + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + \ No newline at end of file diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DataStoreHelperDynamoDB.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DataStoreHelperDynamoDB.java new file mode 100644 index 000000000..c40e88f3c --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DataStoreHelperDynamoDB.java @@ -0,0 +1,75 @@ +package com.genexus.db.dynamodb; + +import com.genexus.CommonUtil; +import com.genexus.db.ServiceCursorBase; +import com.genexus.db.driver.GXConnection; +import com.genexus.db.driver.GXPreparedStatement; +import com.genexus.db.service.GXType; +import com.genexus.db.service.IQuery; +import com.genexus.db.service.ServiceDataStoreHelper; + +import java.sql.Date; +import java.sql.Timestamp; + +public class DataStoreHelperDynamoDB extends ServiceDataStoreHelper +{ + public DynamoQuery newQuery() + { + return new DynamoQuery(this); + } + public DynamoQuery newScan() + { + return new DynamoScan(this); + } + + public DynamoDBMap Map(String name) + { + return new DynamoDBMap(name); + } + + public Object empty(GXType gxtype) + { + switch(gxtype) + { + case Number: + case Int16: + case Int32: + case Int64: return 0; + case Date: return new Date(CommonUtil.nullDate().getTime()); + case DateTime: + case DateTime2: return new Timestamp(CommonUtil.nullDate().getTime()); + case Byte: + case NChar: + case NClob: + case NVarChar: + case Char: + case LongVarChar: + case Clob: + case VarChar: + case Raw: + case Blob: + case NText: + case Text: + case Image: + case UniqueIdentifier: + case Xml: + case DateAsChar: return ""; + case Boolean: return false; + case Decimal: return 0f; + + case Geography: + case Geopoint: + case Geoline: + case Geopolygon: + + case Undefined: + default: return null; + } + } + + @Override + public GXPreparedStatement getPreparedStatement(GXConnection con, IQuery query, ServiceCursorBase cursor, int cursorNum, boolean currentOf, Object[] parms) + { + return new GXPreparedStatement(new DynamoDBPreparedStatement(con.getJDBCConnection(), (DynamoQuery)query, cursor, parms, con), con, con.getHandle(), "", cursor.getCursorId(), currentOf); + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBConnection.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBConnection.java new file mode 100644 index 000000000..80ce0cdb4 --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBConnection.java @@ -0,0 +1,147 @@ +package com.genexus.db.dynamodb; + +import com.genexus.db.service.ServiceConnection; +import org.apache.commons.lang.StringUtils; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; + +import java.net.URI; +import java.sql.ResultSet; +import java.util.Enumeration; +import java.util.Properties; +import java.util.concurrent.Executor; + +public class DynamoDBConnection extends ServiceConnection +{ + private static final String GXDYNAMODB_VERSION = "1.0"; + private static final String GXDYNAMODB_PRODUCT_NAME = "DynamoDB"; + + private static final String CLIENT_ID = "user"; + private static final String CLIENT_SECRET = "password"; + private static final String REGION = "region"; + private static final String LOCAL_URL = "localurl"; + + + DynamoDbClient mDynamoDB; + Region mRegion = Region.US_EAST_1; + + public DynamoDBConnection(String connUrl, Properties initialConnProps) + { + super(connUrl, initialConnProps); // After initialization use props variable from super class to manage properties + initializeDBConnection(connUrl); + } + + private void initializeDBConnection(String connUrl) + { + String mLocalUrl = null, mClientId = null, mClientSecret = null; + for(Enumeration keys = props.keys(); keys.hasMoreElements(); ) + { + String key = (String)keys.nextElement(); + String value = props.getProperty(key, key); + switch(key.toLowerCase()) + { + case LOCAL_URL: mLocalUrl = value; break; + case CLIENT_ID: mClientId = value; break; + case CLIENT_SECRET: mClientSecret = value; break; + case REGION: mRegion = Region.of(value); break; + default: break; + } + } + DynamoDbClientBuilder builder = DynamoDbClient.builder().region(mRegion); + if(mLocalUrl != null) + builder = builder.endpointOverride(URI.create(mLocalUrl)); + if(StringUtils.isNotEmpty(mClientId) && + StringUtils.isNotEmpty(mClientSecret)) + { + AwsBasicCredentials mCredentials = AwsBasicCredentials.create(mClientId, mClientSecret); + builder = builder.credentialsProvider(StaticCredentialsProvider.create(mCredentials)); + } + mDynamoDB = builder.build(); + } + +//---------------------------------------------------------------------------------------------------- + + @Override + public void close() + { + mDynamoDB.close(); + mDynamoDB = null; + } + + @Override + public boolean isClosed() + { + return mDynamoDB != null; + } + + //---------------------------------------------------------------------------------------------------- + @Override + public String getDatabaseProductName() + { + return GXDYNAMODB_PRODUCT_NAME; + } + + @Override + public String getDatabaseProductVersion() + { + return ""; + } + + @Override + public String getDriverName() + { + return mDynamoDB.getClass().getName(); + } + + @Override + public String getDriverVersion() + { + return String.format("%s/%s", mDynamoDB.serviceName(), GXDYNAMODB_VERSION); + } + + // JDK8: + @Override + public void setSchema(String schema) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getSchema() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void abort(Executor executor) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getNetworkTimeout() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean generatedKeyAlwaysReturned() + { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBDriver.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBDriver.java new file mode 100644 index 000000000..ef716751c --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBDriver.java @@ -0,0 +1,73 @@ +package com.genexus.db.dynamodb; + +import java.sql.*; +import java.util.Properties; +import java.util.logging.Logger; + +public class DynamoDBDriver implements Driver +{ + private static final int MAJOR_VERSION = 1; + private static final int MINOR_VERSION = 0; + private static final String DRIVER_ID = "dynamodb:"; + + private static final DynamoDBDriver DYNAMODB_DRIVER; + static + { + DYNAMODB_DRIVER = new DynamoDBDriver(); + try + { + DriverManager.registerDriver(DYNAMODB_DRIVER); + }catch(SQLException e) + { + e.printStackTrace(); + } + } + + public DynamoDBDriver() + { + } + + @Override + public Connection connect(String url, Properties info) + { + if(!acceptsURL(url)) + return null; + return new DynamoDBConnection(url.substring(DRIVER_ID.length()), info); + } + + @Override + public boolean acceptsURL(String url) + { + return url.startsWith(DRIVER_ID); + } + + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) + { + return new DriverPropertyInfo[0]; + } + + @Override + public int getMajorVersion() + { + return MAJOR_VERSION; + } + + @Override + public int getMinorVersion() + { + return MINOR_VERSION; + } + + @Override + public boolean jdbcCompliant() + { + return false; + } + + @Override + public Logger getParentLogger() + { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBErrors.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBErrors.java new file mode 100644 index 000000000..619b17205 --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBErrors.java @@ -0,0 +1,7 @@ +package com.genexus.db.dynamodb; + +public class DynamoDBErrors +{ + public static final String ValidationException = "ValidationException"; + public static final CharSequence ValidationExceptionMessageKey = "The AttributeValue for a key attribute cannot contain an empty string value."; +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBHelper.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBHelper.java new file mode 100644 index 000000000..ca34af579 --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBHelper.java @@ -0,0 +1,122 @@ +package com.genexus.db.dynamodb; + +import com.genexus.db.service.VarValue; +import json.org.json.JSONArray; +import json.org.json.JSONObject; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.sql.SQLException; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DynamoDBHelper +{ + private static final SimpleDateFormat ISO_DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd"); + public static AttributeValue toAttributeValue(VarValue var) throws SQLException + { + if(var == null) + return null; + Object value = var.value; + if(value == null) + return null; + AttributeValue.Builder builder = AttributeValue.builder(); + switch (var.type) + { + case Number: + case Int16: + case Int32: + case Int64: + case Decimal: + return builder.n(value.toString()).build(); + case Date: + return builder.s(ISO_DATE_FORMATTER.format(value)).build(); + case DateTime: + case DateTime2: + Timestamp valueTs = (java.sql.Timestamp)value; + return builder.s(valueTs.toLocalDateTime().atOffset(ZoneOffset.UTC).toString()).build(); + case Boolean: + case Byte: + return builder.bool((Boolean) value).build(); + case Char: + case VarChar: + case LongVarChar: + case Text: + case NChar: + case NVarChar: + case NText: + + // Unused datatypes + case UniqueIdentifier: + case Xml: + case Geography: + case Geopoint: + case Geoline: + case Geopolygon: + return builder.s(value.toString()).build(); + case NClob: + case Clob: + case Raw: + case Blob: + return builder.b((SdkBytes) value).build(); + case Undefined: + case Image: + case DateAsChar: + default: + throw new SQLException(String.format("DynamoDB unsupported type (%s)", var.type)); + } + } + + public static String getString(AttributeValue attValue) + { + if(attValue == null) + return null; + String value = attValue.s(); + if (value != null) + return value; + else if (!attValue.ns().isEmpty()) + return setToString(attValue.ns()); + else if (!attValue.ss().isEmpty()) + return setToString(attValue.ss()); + else if(attValue.bool() != null) + return attValue.bool().toString(); + else if (attValue.hasM()) + return new JSONObject(convertToDictionary(attValue.m())).toString(); + else if (attValue.hasL()) + return new JSONArray(attValue.l().stream().map(DynamoDBHelper::getString).collect(Collectors.toList())).toString(); + return null; + } + + private static HashMap convertToDictionary(Map m) + { + HashMap dict = new HashMap<>(); + for (Map.Entry keyValues : m.entrySet()) + { + dict.put(keyValues.getKey(), getString(keyValues.getValue())); + } + return dict; + } + + private static String setToString(List nS) + { + return String.format("[ %s ]", String.join(", ", nS)); + } + + public static boolean addAttributeValue(String parmName, HashMap values, VarValue parm) throws SQLException + { + if(parm == null) + return false; + AttributeValue value = toAttributeValue(parm); + if (value != null) + { + values.put(parmName, value); + } + return true; + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBMap.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBMap.java new file mode 100644 index 000000000..29e16823e --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBMap.java @@ -0,0 +1,28 @@ +package com.genexus.db.dynamodb; + +import com.genexus.db.service.IODataMapName; + +public class DynamoDBMap extends IODataMapName{ + public DynamoDBMap(String name) { + super(mapAttributeMap(name)); + needsAttributeMap = _needsAttributeMap(name); + } + + private static String mapAttributeMap(String name) + { + if(_needsAttributeMap(name)) + return name.substring(1); + else return name; + } + + private static boolean _needsAttributeMap(String name) + { + return name.startsWith("#"); + } + + private final boolean needsAttributeMap; + + public boolean needsAttributeMap() { + return needsAttributeMap; + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBPreparedStatement.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBPreparedStatement.java new file mode 100644 index 000000000..d45aaf32d --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBPreparedStatement.java @@ -0,0 +1,311 @@ +package com.genexus.db.dynamodb; + +import com.genexus.db.Cursor; +import com.genexus.db.ServiceCursorBase; +import com.genexus.db.driver.GXConnection; +import com.genexus.db.service.IODataMap; +import com.genexus.db.service.QueryType; +import com.genexus.db.service.ServicePreparedStatement; +import com.genexus.db.service.VarValue; +import com.genexus.util.NameValuePair; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.*; + +import java.io.InputStream; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class DynamoDBPreparedStatement extends ServicePreparedStatement +{ + final DynamoQuery query; + final ServiceCursorBase cursor; + + DynamoDBPreparedStatement(Connection con, DynamoQuery query, ServiceCursorBase cursor, Object[] parms, GXConnection gxCon) + { + super(con, parms, gxCon); + this.query = query; + this.cursor = cursor; + } + + @Override + public ResultSet executeQuery() throws SQLException + { + DynamoDBResultSet resultSet = new DynamoDBResultSet(this); + _executeQuery(resultSet); + return resultSet; + } + + @Override + public int executeUpdate() throws SQLException + { + return _executeQuery(null); + } + + private static final Pattern FILTER_PATTERN = Pattern.compile("\\((.*) = :(.*)\\)"); + private static final Pattern VAR_PATTERN = Pattern.compile(".*(:.*)\\).*"); + private int _executeQuery(DynamoDBResultSet resultSet) throws SQLException + { + query.initializeParms(parms); + boolean isInsert = query.getQueryType() == QueryType.INS; + DynamoDbClient client = getClient(); + + HashMap values = new HashMap<>(); + + if(query.getQueryType() == QueryType.QUERY) + { + for (VarValue var : query.getVars().values()) + { + values.put(var.name, DynamoDBHelper.toAttributeValue(var)); + } + } + + HashMap keyCondition = new HashMap<>(); + HashMap expressionAttributeNames = null; + HashSet mappedNames = null; + + for (Iterator it = query.getAssignAtts(); it.hasNext(); ) + { + NameValuePair asg = it.next(); + String name = asg.name; + if(asg.name.startsWith("#")) + { + name = trimSharp(asg.name); + if (!isInsert) + { + if(expressionAttributeNames == null) + { + expressionAttributeNames = new HashMap<>(); + mappedNames = new HashSet<>(); + } + expressionAttributeNames.put(asg.name, name); + mappedNames.add(name); + } + } + String parmName = asg.value; + if(!DynamoDBHelper.addAttributeValue(isInsert ? name : ":" + name, values, query.getParm(parmName))) + throw new SQLException(String.format("Cannot assign attribute value (name: %s)", parmName)); + } + + String keyItemForUpd = query.getPartitionKey(); + if(keyItemForUpd != null && keyItemForUpd.startsWith("#")) + { + if(expressionAttributeNames == null) + { + expressionAttributeNames = new HashMap<>(); + mappedNames = new HashSet<>(); + } + String keyName = keyItemForUpd.substring(1); + expressionAttributeNames.put(keyItemForUpd, keyName); + mappedNames.add(keyName); + } + + for (Iterator it = Arrays.stream(query.selectList) + .filter(selItem -> ((DynamoDBMap) selItem).needsAttributeMap()) + .map(IODataMap::getName).iterator(); it.hasNext(); ) + { + if(expressionAttributeNames == null) + { + expressionAttributeNames = new HashMap<>(); + mappedNames = new HashSet<>(); + } + String mappedName = it.next(); + String key = "#" + mappedName; + expressionAttributeNames.put(key, mappedName); + mappedNames.add(mappedName); + } + + if(query.getQueryType() != QueryType.QUERY) + { + for (String keyFilter : query.getAllFilters().collect(Collectors.toList())) + { + Matcher match = FILTER_PATTERN.matcher(keyFilter); + if (match.matches() && match.groupCount() > 1) + { + String varName = String.format(":%s", match.group(2)); + String name = trimSharp(match.group(1)); + AttributeValue value = DynamoDBHelper.toAttributeValue(query.getParm(varName)); + if(value == null) + throw new SQLException(String.format("Cannot assign attribute value (name: %s)", varName)); + keyCondition.put(name, value); + } + } + } + + switch (query.getQueryType()) + { + case QUERY: + { + boolean issueScan = query instanceof DynamoScan; + if (!issueScan) + { // Check whether a query has to be demoted to scan due to empty parameters + for (String keyFilter : query.keyFilters) + { + Matcher match = VAR_PATTERN.matcher(keyFilter); + if (match.matches()) + { + String varName = match.group(1); + VarValue varValue = query.getParm(varName); + if (varValue != null && varValue.value.toString().isEmpty()) + { + issueScan = true; + break; + } + } + } + } + + Iterator> iterator; + if(issueScan) + { + ScanRequest.Builder builder = ScanRequest.builder() + .tableName(query.tableName) + .projectionExpression(String.join(",", query.projection)); + String filterString = query.getAllFilters().collect(Collectors.joining(" AND ")); + if(!filterString.isEmpty()) + builder.filterExpression(filterString).expressionAttributeValues(values); + if(expressionAttributeNames != null) + builder.expressionAttributeNames(expressionAttributeNames); + + iterator = client.scanPaginator(builder.build()) + .stream() + .flatMap(response -> response.items() + .stream() + .map(map -> new HashMap(map))) + .iterator(); + }else + { + QueryRequest.Builder builder = QueryRequest.builder() + .tableName(query.tableName) + .keyConditionExpression(String.join(" AND ", query.keyFilters)) + .expressionAttributeValues(values) + .projectionExpression(String.join(", ", query.projection)) + .indexName(query.getIndex()) + .scanIndexForward(query.isScanIndexForward()); + if(query.filters.length > 0) + builder.filterExpression(String.join(" AND ", query.filters)); + if(expressionAttributeNames != null) + builder.expressionAttributeNames(expressionAttributeNames); + + iterator = client.queryPaginator(builder.build()) + .stream() + .flatMap(response -> response.items() + .stream() + .map(map -> new HashMap(map))) + .iterator(); + } + resultSet.iterator = iterator; + return 0; + } + case INS: + { + PutItemRequest.Builder builder = PutItemRequest.builder() + .tableName(query.tableName) + .item(values) + .conditionExpression(String.format("attribute_not_exists(%s)", keyItemForUpd)); + if(expressionAttributeNames != null) + builder.expressionAttributeNames(expressionAttributeNames); + PutItemRequest request = builder.build(); + try + { + client.putItem(request); + }catch(ConditionalCheckFailedException recordAlreadyExists) + { + return Cursor.DUPLICATE; + } + break; + } + case UPD: + { + UpdateItemRequest request = UpdateItemRequest.builder() + .tableName(query.tableName) + .key(keyCondition) + .updateExpression(toAttributeUpdates(keyCondition, values, mappedNames)) + .conditionExpression(String.format("attribute_exists(%s)", keyItemForUpd)) + .expressionAttributeNames(expressionAttributeNames) + .expressionAttributeValues(values) + .build(); + try + { + client.updateItem(request); + }catch(ConditionalCheckFailedException recordNotFound) + { + return Cursor.EOF; + } + break; + } + case DLT: + { + DeleteItemRequest.Builder builder = DeleteItemRequest.builder() + .tableName(query.tableName) + .key(keyCondition) + .conditionExpression(String.format("attribute_exists(%s)", keyItemForUpd)); + if(expressionAttributeNames != null) + builder.expressionAttributeNames(expressionAttributeNames); + + DeleteItemRequest request = builder.build(); + try + { + client.deleteItem(request); + }catch(ConditionalCheckFailedException recordNotFound) + { + return Cursor.EOF; + } + break; + } + default: throw new UnsupportedOperationException(String.format("Invalid query type: %s", query.getQueryType())); + } + return 0; + } + + private String toAttributeUpdates(HashMap keyConditions, HashMap values, HashSet mappedNames) + { + StringBuilder updateExpression = new StringBuilder(); + for(Map.Entry item : values.entrySet()) + { + String keyName = item.getKey().substring(1); + if (!keyConditions.containsKey(keyName) && !keyName.startsWith("AV")) + { + if (mappedNames != null && mappedNames.contains(keyName)) + keyName = "#" + keyName; + updateExpression.append(updateExpression.length() == 0 ? "SET " : ", "); + updateExpression.append(keyName).append(" = ").append(item.getKey()); + } + } + return updateExpression.toString(); + } + + private static String trimSharp(String name) + { + return name.startsWith("#") ? name.substring(1) : name; + } + + DynamoDbClient getClient() throws SQLException + { + return ((DynamoDBConnection)getConnection()).mDynamoDB; + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) + { + parms[parameterIndex-1] = SdkBytes.fromInputStream(x); + } + + /// JDK8 + @Override + public void closeOnCompletion() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isCloseOnCompletion() + { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBResultSet.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBResultSet.java new file mode 100644 index 000000000..64739890e --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoDBResultSet.java @@ -0,0 +1,296 @@ +package com.genexus.db.dynamodb; + +import com.genexus.CommonUtil; +import com.genexus.ModelContext; +import com.genexus.db.service.IOServiceContext; +import com.genexus.db.service.ServiceResultSet; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.util.TimeZone; + +public class DynamoDBResultSet extends ServiceResultSet +{ + private final DynamoDBPreparedStatement stmt; + public DynamoDBResultSet(DynamoDBPreparedStatement stmt) throws SQLException + { + this.stmt = stmt; + } + + @Override + public boolean next() + { + try + { + if(iterator.hasNext()) + { + currentEntry = iterator.next(); + return true; + } + return false; + }catch(DynamoDbException e) + { + AwsErrorDetails details = e.awsErrorDetails(); + if(details != null && details.errorCode().equals(DynamoDBErrors.ValidationException) && + details.errorMessage().contains(DynamoDBErrors.ValidationExceptionMessageKey)) + return false; // Handles special case where a string key attribute is filtered with an empty value which is not supported on DynamoDB but should yield a not record found in GX + throw e; + } + } + + private static final IOServiceContext SERVICE_CONTEXT = null; + + private boolean lastWasNull; + @Override + public boolean wasNull() + { + return value == null || lastWasNull; + } + + private AttributeValue getAttValue(int columnIndex) + { + value = (AttributeValue)stmt.query.selectList[columnIndex-1].getValue(SERVICE_CONTEXT, currentEntry); + return value; + } + + private long getNumeric(int columnIndex) + { + AttributeValue value = getAttValue(columnIndex); + if(value != null) + { + lastWasNull = false; + String sNumber = value.n(); + if (sNumber != null) + return Long.parseLong(sNumber); + else if (value.bool() != null) + return value.bool() ? 1 : 0; + } + lastWasNull = true; + return 0; + } + + private double getDecimal(int columnIndex) + { + AttributeValue value = getAttValue(columnIndex); + if(value != null) + { + lastWasNull = false; + String sNumber = value.n(); + if (sNumber != null) + return Double.parseDouble(sNumber); + } + lastWasNull = true; + return 0; + } + + private static final DateTimeFormatter ISO_DATE_TIME_OR_DATE = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .optionalStart() + .optionalStart() + .appendLiteral('T') + .optionalEnd() + .optionalStart() + .appendLiteral(' ') + .optionalEnd() + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .optionalStart() + .appendOffsetId() + .optionalStart() + .appendLiteral('[') + .parseCaseSensitive() + .appendZoneRegionId() + .appendLiteral(']').toFormatter(); + + private static final DateTimeFormatter [] DATE_TIME_FORMATTERS = new DateTimeFormatter[] + { + DateTimeFormatter.ofPattern("M/d/yyyy[ H:mm:ss]"), + DateTimeFormatter.ofPattern("M/d/yyyy[ h:mm:ss a]"), + DateTimeFormatter.ofPattern("yyyy-M-d[ H:mm:ss.S]"), + DateTimeFormatter.ofPattern("yyyy-M-d[ H:mm:ss.S a]") + }; + + private Instant getInstant(int columnIndex) + { + String value = getString(columnIndex); + if(value == null || value.trim().isEmpty()) + { + lastWasNull = true; + return CommonUtil.nullDate().toInstant(); + } + + TemporalAccessor accessor = null; + + try + { + accessor = ISO_DATE_TIME_OR_DATE.parseBest(value, LocalDateTime::from, LocalDate::from); + }catch(DateTimeParseException dtpe) + { + for(DateTimeFormatter dateTimeFormatter:DATE_TIME_FORMATTERS) + { + try + { + accessor = dateTimeFormatter.parseBest(value, LocalDateTime::from, LocalDate::from); + break; + }catch(Exception ignored){ } + } + if(accessor == null) + { + return CommonUtil.resetTime(CommonUtil.nullDate()).toInstant(); + } + } + + + if(accessor instanceof LocalDateTime) + { + ModelContext ctx = ModelContext.getModelContext(); + TimeZone tz = ctx != null ? ctx.getClientTimeZone() : TimeZone.getDefault(); + return ((LocalDateTime) accessor).atZone(tz.toZoneId()).toInstant(); + } + else return LocalDate.from(accessor).atStartOfDay().toInstant(ZoneOffset.UTC); + } + + @Override + public T getAs(Class reference, int columnIndex, T defaultValue) + { + throw new UnsupportedOperationException(String.format("Data Type: %s", reference.getName())); + } + + @Override + public String getString(int columnIndex) + { + String value = DynamoDBHelper.getString(getAttValue(columnIndex)); + lastWasNull = value == null; + return value; + } + + @Override + public boolean getBoolean(int columnIndex) + { + AttributeValue value = getAttValue(columnIndex); + if(value != null) + { + Boolean boolValue = value.bool(); + if(boolValue != null) + { + lastWasNull = false; + return boolValue; + } + } + lastWasNull = true; + return false; + } + + @Override + public byte getByte(int columnIndex) + { + return (byte)getNumeric(columnIndex); + } + + @Override + public short getShort(int columnIndex) + { + return (short)getNumeric(columnIndex); + } + + @Override + public int getInt(int columnIndex) + { + return (int)getNumeric(columnIndex); + } + + @Override + public long getLong(int columnIndex) + { + return getNumeric(columnIndex); + } + + @Override + public float getFloat(int columnIndex) + { + return (float)getDecimal(columnIndex); + } + + @Override + public double getDouble(int columnIndex) + { + return getDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) + { + AttributeValue value = getAttValue(columnIndex); + if(value != null) + { + String sNumber = value.n(); + if (sNumber != null) + { + lastWasNull = false; + return new BigDecimal(sNumber); + } + } + lastWasNull = true; + return BigDecimal.ZERO; + } + + @Override + public java.sql.Date getDate(int columnIndex) + { + return java.sql.Date.valueOf(getTimestamp(columnIndex).toInstant().atOffset(ZoneOffset.UTC).toLocalDate()); + } + + @Override + public Time getTime(int columnIndex) + { + return getAs(Time.class, columnIndex, new Time(0)); + } + + @Override + public Timestamp getTimestamp(int columnIndex) + { + return java.sql.Timestamp.from(getInstant(columnIndex)); + } + + @Override + public InputStream getBinaryStream(int columnIndex) + { + AttributeValue value = getAttValue(columnIndex); + if(value != null) + { + SdkBytes bytes = value.b(); + if(bytes != null) + return bytes.asInputStream(); + } + lastWasNull = true; + return null; + } + + // JDK8 + @Override + public T getObject(int columnIndex, Class type) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public T getObject(String columnLabel, Class type) + { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoQuery.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoQuery.java new file mode 100644 index 000000000..314dbca0d --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoQuery.java @@ -0,0 +1,72 @@ +package com.genexus.db.dynamodb; + +import com.genexus.db.service.Query; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class DynamoQuery extends Query{ + private String index; + + private boolean scanIndexForward = true; + private static final String RANGE_KEY_INDEX = "RangeKey"; + private String partitionKey; + public String[] keyFilters = EMPTY_ARR_STRING; + + @Override + public DynamoQuery For(String tableName) + { + super.For(tableName); + return this; + } + + @Override + public DynamoQuery orderBy(String index) + { + index = index.trim(); + if(index.startsWith("(") && index.endsWith(")")) + { + scanIndexForward = false; + index = index.substring(1, index.length()-1); + } + if (!RANGE_KEY_INDEX.equals(index)) + setIndex(index); + return this; + } + + public DynamoQuery setKey(String partitionKey) + { + this.partitionKey = partitionKey; + return this; + } + + public DynamoQuery keyFilter(String[] keyFilters) + { + this.keyFilters = keyFilters; + return this; + } + + public String getPartitionKey(){ return partitionKey; } + + public DynamoQuery(DataStoreHelperDynamoDB dataStoreHelper) + { + super(dataStoreHelper); + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public boolean isScanIndexForward() { + return scanIndexForward; + } + + public Stream getAllFilters() + { + return Stream.concat(Arrays.stream(keyFilters), Arrays.stream(filters)); + } +} diff --git a/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoScan.java b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoScan.java new file mode 100644 index 000000000..5619edba7 --- /dev/null +++ b/gxdynamodb/src/main/java/com/genexus/db/dynamodb/DynamoScan.java @@ -0,0 +1,7 @@ +package com.genexus.db.dynamodb; + +public class DynamoScan extends DynamoQuery { + public DynamoScan(DataStoreHelperDynamoDB dataStoreHelperDynamoDB) { + super(dataStoreHelperDynamoDB); + } +} diff --git a/java/src/main/java/com/genexus/db/ServiceCursorBase.java b/java/src/main/java/com/genexus/db/ServiceCursorBase.java index 29e4461ff..b3996c838 100644 --- a/java/src/main/java/com/genexus/db/ServiceCursorBase.java +++ b/java/src/main/java/com/genexus/db/ServiceCursorBase.java @@ -195,12 +195,6 @@ void postExecute(AbstractDataStoreProviderBase connectionProvider, AbstractDataS default: throw new RuntimeException("Not implemented"); } - - //if(currentOf) - //{ // Si tengo currentof marco uncommited changes para hacer un rollback dado que en algunos casos - // de error (Bantotal) les estaban quedando locks en la bd - // connectionProvider.getConnection().setUncommitedChanges(); - //} } public boolean next(AbstractDataSource ds) throws SQLException diff --git a/java/src/main/java/com/genexus/db/service/GXType.java b/java/src/main/java/com/genexus/db/service/GXType.java new file mode 100644 index 000000000..d9cacf4e3 --- /dev/null +++ b/java/src/main/java/com/genexus/db/service/GXType.java @@ -0,0 +1,41 @@ +package com.genexus.db.service; + +public enum GXType +{ + Number(0), + Int16(1), + Int32(2), + Int64(3), + Date(4), + DateTime(5), + DateTime2(17), + Byte(6), + NChar(7), + NClob(8), + NVarChar(9), + Char(10), + LongVarChar(11), + Clob(12), + VarChar(13), + Raw(14), + Blob(15), + Undefined(16), + Boolean(18), + Decimal(19), + NText(20), + Text(21), + Image(22), + UniqueIdentifier(23), + Xml(24), + Geography(25), + Geopoint(26), + Geoline(27), + Geopolygon(28), + DateAsChar(29); + + private final int value; + GXType(final int value) + { + this.value = value; + } +} \ No newline at end of file diff --git a/java/src/main/java/com/genexus/db/service/IOMap.java b/java/src/main/java/com/genexus/db/service/IOMap.java new file mode 100644 index 000000000..ff9622723 --- /dev/null +++ b/java/src/main/java/com/genexus/db/service/IOMap.java @@ -0,0 +1,27 @@ +package com.genexus.db.service; + +import java.util.HashMap; + +public class IOMap implements IODataMap{ + private final String name; + + public IOMap(String name) + { + this.name = name; + } + + @Override + public Object getValue(IOServiceContext context, HashMap currentEntry) { + return null; + } + + @Override + public String getName() { + return null; + } + + @Override + public void setValue(HashMap currentEntry, Object value) { + + } +} diff --git a/java/src/main/java/com/genexus/db/service/Query.java b/java/src/main/java/com/genexus/db/service/Query.java new file mode 100644 index 000000000..9f794f6a3 --- /dev/null +++ b/java/src/main/java/com/genexus/db/service/Query.java @@ -0,0 +1,117 @@ +package com.genexus.db.service; + +import com.genexus.util.NameValuePair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.stream.Stream; + +public class Query implements IQuery { + protected static final String [] EMPTY_ARR_STRING = new String [0]; + final ServiceDataStoreHelper dataStoreHelper; + public Query(ServiceDataStoreHelper dataStoreHelper) + { + this.dataStoreHelper = dataStoreHelper; + } + + public String tableName; + public String[] projection = EMPTY_ARR_STRING; + public String[] orderBys = EMPTY_ARR_STRING; + public String[] filters = EMPTY_ARR_STRING; + + private final ArrayList mAssignAtts = new ArrayList<>(); + public Iterator getAssignAtts() { return mAssignAtts.iterator(); } + + public IODataMap[] selectList = new IODataMap[0]; + private final HashMap mVarValues = new HashMap<>(); + public HashMap getVars() { return mVarValues; } + + public VarValue getParm(String parmName) + { + return mVarValues.get(parmName); + } + + protected final HashMap parmTypes = new HashMap<>(); + + public Query For(String tableName) + { + this.tableName = tableName; + return this; + } + + public Query select(String[] columns) + { + this.projection = columns; + return this; + } + + public Query orderBy(String []orders) + { + orderBys = orders; + return this; + } + + public Query orderBy(String order) + { + return orderBy(new String[]{ order }); + } + + public Query filter(String[] filters) + { + this.filters = filters; + return this; + } + + public Query set(String name, String value) + { + mAssignAtts.add(new NameValuePair(name, value)); + return this; + } + + public Query setMaps(IODataMap[] selectList) + { + this.selectList = selectList; + return this; + } + + protected QueryType queryType = QueryType.QUERY; + @Override + public QueryType getQueryType() + { + return queryType; + } + + public Query setType(QueryType queryType) + { + this.queryType = queryType; + return this; + } + + public Query addConst(GXType gxType, Object parm) + { + String parmName = String.format(":const%d", mVarValues.size() + 1); + mVarValues.put(parmName, new VarValue(parmName, gxType, parm)); + return this; + } + + public Query setParmType(int parmId, GXType gxType) + { + parmTypes.put(parmId, gxType); + return this; + } + public T as(Class reference) + { + return reference.cast(this); + } + + public void initializeParms(Object[] parms) + { + for(int idx : parmTypes.keySet()) + { + String parmName = String.format(":parm%d", idx); + mVarValues.put(parmName, new VarValue(parmName, parmTypes.get(idx), parms[idx])); + } + } +} diff --git a/java/src/main/java/com/genexus/db/service/ServiceException.java b/java/src/main/java/com/genexus/db/service/ServiceException.java index 80332c882..24f294419 100644 --- a/java/src/main/java/com/genexus/db/service/ServiceException.java +++ b/java/src/main/java/com/genexus/db/service/ServiceException.java @@ -1,17 +1,29 @@ package com.genexus.db.service; +import java.sql.SQLException; + public class ServiceException extends Error { private String sqlState; private int vendorCode; public ServiceException(String reason, String sqlState, int vendorCode) { - super(reason); - this.sqlState = sqlState; - this.vendorCode = vendorCode; + this(reason, sqlState, vendorCode, null); } - - public String getSQLState() + + public ServiceException(String reason, String sqlState, int vendorCode, Throwable innerException) + { + super(reason, innerException); + this.sqlState = sqlState; + this.vendorCode = vendorCode; + } + + public static SQLException createSQLException(ServiceError serviceError, Throwable e) + { + return new SQLException(createServiceException(serviceError, e)); + } + + public String getSQLState() { return sqlState; } @@ -20,4 +32,14 @@ public int getVendorCode() { return vendorCode; } + + public static ServiceException createServiceException(ServiceError serviceError) + { + return new ServiceException(serviceError.toString(), serviceError.getSqlState(), serviceError.getCode()); + } + + public static ServiceException createServiceException(ServiceError serviceError, Throwable innerException) + { + return new ServiceException(serviceError.toString(), serviceError.getSqlState(), serviceError.getCode(), innerException); + } } diff --git a/java/src/main/java/com/genexus/db/service/ServiceResultSet.java b/java/src/main/java/com/genexus/db/service/ServiceResultSet.java index d31a46c37..144f97f7a 100644 --- a/java/src/main/java/com/genexus/db/service/ServiceResultSet.java +++ b/java/src/main/java/com/genexus/db/service/ServiceResultSet.java @@ -104,13 +104,7 @@ public double getDouble(int columnIndex) throws SQLException @Deprecated public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { - return getAs(BigDecimal.class, columnIndex, BigDecimal.ZERO); - } - - @Override - public byte[] getBytes(int columnIndex) throws SQLException - { - throw new UnsupportedOperationException("Not supported yet."); + return getBigDecimal(columnIndex).setScale(scale); } @Override @@ -150,7 +144,33 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException return getAs(InputStream.class, columnIndex, new ByteArrayInputStream(new byte[0])); } - @Override + @Override + public Object getObject(int columnIndex) throws SQLException + { + return getAs(Object.class, columnIndex, new Object()); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException + { + return getAs(Reader.class, columnIndex, new StringReader("")); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException + { + return getAs(BigDecimal.class, columnIndex, BigDecimal.ZERO); + } + + // Unsupported methods + + @Override + public byte[] getBytes(int columnIndex) throws SQLException + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override public String getString(String columnLabel) throws SQLException { throw new UnsupportedOperationException("Not supported yet."); @@ -272,12 +292,6 @@ public ResultSetMetaData getMetaData() throws SQLException throw new UnsupportedOperationException("Not supported yet."); } - @Override - public Object getObject(int columnIndex) throws SQLException - { - return getAs(Object.class, columnIndex, new Object()); - } - @Override public Object getObject(String columnLabel) throws SQLException { @@ -290,24 +304,12 @@ public int findColumn(String columnLabel) throws SQLException throw new UnsupportedOperationException("Not supported yet."); } - @Override - public Reader getCharacterStream(int columnIndex) throws SQLException - { - return getAs(Reader.class, columnIndex, new StringReader("")); - } - @Override public Reader getCharacterStream(String columnLabel) throws SQLException { throw new UnsupportedOperationException("Not supported yet."); } - @Override - public BigDecimal getBigDecimal(int columnIndex) throws SQLException - { - return getAs(BigDecimal.class, columnIndex, BigDecimal.ZERO); - } - @Override public BigDecimal getBigDecimal(String columnLabel) throws SQLException { diff --git a/java/src/main/java/com/genexus/db/service/VarValue.java b/java/src/main/java/com/genexus/db/service/VarValue.java new file mode 100644 index 000000000..29423dfa1 --- /dev/null +++ b/java/src/main/java/com/genexus/db/service/VarValue.java @@ -0,0 +1,15 @@ +package com.genexus.db.service; + +public class VarValue +{ + public String name; + public Object value; + public GXType type; + + public VarValue(String name, GXType type, Object value) + { + this.name = name; + this.type = type; + this.value = value; + } +} diff --git a/pom.xml b/pom.xml index 5b88c8aa0..cb02d9922 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ android gxgeospatial gxodata + gxdynamodb gxexternalproviders gxwebsocket gxwebsocketjakarta