├── NOTICE ├── lombok.config ├── src ├── main │ └── java │ │ └── com │ │ └── amazon │ │ └── rdsdata │ │ └── client │ │ ├── EmptyResultSetException.java │ │ ├── MappingOptions.java │ │ ├── ObjectWriter.java │ │ ├── PropertyWriter.java │ │ ├── ObjectMapper.java │ │ ├── FieldPropertyWriter.java │ │ ├── PlaceholderUtils.java │ │ ├── SetterPropertyWriter.java │ │ ├── PropertyObjectWriter.java │ │ ├── ConstructorObjectWriter.java │ │ ├── FieldMapper.java │ │ ├── MappingException.java │ │ ├── ExecutionResult.java │ │ ├── Executor.java │ │ ├── RdsData.java │ │ └── TypeConverter.java └── test │ └── java │ └── com │ └── amazon │ └── rdsdata │ └── client │ ├── RequestFlagsTests.java │ ├── RetrieveNumberOfRecordsTests.java │ ├── SingleResultTests.java │ ├── testutil │ ├── SdkConstructs.java │ ├── TestBase.java │ └── MockingTools.java │ ├── SingleParameterWitherTests.java │ ├── MapToListTests.java │ ├── InlineParametersTests.java │ ├── MapToSingleTests.java │ ├── MappingOutputViaAllArgsConstructorTests.java │ ├── TransactionTests.java │ ├── InputTypesTest.java │ ├── MappingOutputViaFieldsTests.java │ ├── MappingOutputViaSettersTests.java │ ├── OutputTypesTest.java │ └── MappingInputTests.java ├── CODE_OF_CONDUCT.md ├── .gitignore ├── CONTRIBUTING.md ├── README.md └── LICENSE /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | # This file is generated by the 'io.freefair.lombok' Gradle plugin 2 | config.stopBubbling = true 3 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/EmptyResultSetException.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client; 2 | 3 | public class EmptyResultSetException extends RuntimeException { 4 | public EmptyResultSetException() { 5 | super("Result set is empty"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/MappingOptions.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.With; 6 | 7 | @AllArgsConstructor 8 | @Builder 9 | public class MappingOptions { 10 | public static MappingOptions DEFAULT = MappingOptions.builder() 11 | .useLabelForMapping(false) 12 | .ignoreMissingSetters(false) 13 | .build(); 14 | 15 | @With public final boolean useLabelForMapping; 16 | @With public final boolean ignoreMissingSetters; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/ObjectWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | abstract class ObjectWriter { 18 | public abstract T write(ExecutionResult.Row row); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/PropertyWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | interface PropertyWriter { 18 | void write(Object value); 19 | Class getType(); 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven 2 | target/ 3 | logs/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | ### Gradle 7 | .gradle 8 | /build/ 9 | /out/ 10 | !gradle/wrapper/gradle-wrapper.jar 11 | bin/ 12 | 13 | ### STS ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | 22 | ### IntelliJ IDEA ### 23 | .idea 24 | *.iws 25 | *.iml 26 | *.ipr 27 | log/ 28 | 29 | ### NetBeans ### 30 | nbproject/private/ 31 | build/ 32 | nbbuild/ 33 | dist/ 34 | nbdist/ 35 | .nb-gradle/ 36 | 37 | ### Mac 38 | .DS_Store 39 | */.DS_Store 40 | 41 | ### VS Code ### 42 | *.project 43 | *.factorypath 44 | 45 | # Compiled class file 46 | *.class 47 | 48 | # Log file 49 | *.log 50 | 51 | # BlueJ files 52 | *.ctxt 53 | 54 | # Mobile Tools for Java (J2ME) 55 | .mtj.tmp/ 56 | 57 | # Package Files # 58 | *.war 59 | *.nar 60 | *.ear 61 | *.zip 62 | *.tar.gz 63 | *.rar 64 | 65 | ### VSCode 66 | .vscode -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/RequestFlagsTests.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client; 2 | 3 | import com.amazon.rdsdata.client.testutil.TestBase; 4 | import lombok.val; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class RequestFlagsTests extends TestBase { 11 | @BeforeEach 12 | public void beforeEach() { 13 | mockReturnValue(); // return empty response by default 14 | } 15 | 16 | @Test 17 | public void shouldSetContinueAfterTimeoutFlag() { 18 | client.forSql("SELECT 1") 19 | .withContinueAfterTimeout() 20 | .execute(); 21 | 22 | val request = captureRequest(); 23 | assertThat(request.continueAfterTimeout()).isTrue(); 24 | } 25 | 26 | @Test 27 | public void shouldNotSetContinueAfterTimeoutFlagByDefault() { 28 | client.forSql("SELECT 1") 29 | .execute(); 30 | 31 | val request = captureRequest(); 32 | assertThat(request.continueAfterTimeout()).isFalse(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/RetrieveNumberOfRecordsTests.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client; 2 | 3 | import com.amazon.rdsdata.client.testutil.TestBase; 4 | import lombok.Value; 5 | import lombok.val; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class RetrieveNumberOfRecordsTests extends TestBase { 11 | 12 | @Test 13 | void shouldReturnUpdatedRecordsCount() { 14 | val numberOfRecordsUpdated = 1; 15 | mockReturnValue(numberOfRecordsUpdated); 16 | 17 | val result = client.forSql("INSERT INTO tbl1(a, b, c) VALUES(?, ?, ?)", 1, 2, 3) 18 | .execute() 19 | .getNumberOfRecordsUpdated(); 20 | 21 | assertThat(result).isEqualTo(numberOfRecordsUpdated); 22 | } 23 | 24 | @Test 25 | void shouldReturnZeroUpdatedRecordsCountForBatchUpdate() { 26 | mockReturnValues(); 27 | 28 | val dto = new Dto(1); 29 | 30 | val result = client.forSql("INSERT INTO tbl1(a) VALUES(:value)") 31 | .withParamSets(dto) 32 | .execute() 33 | .getNumberOfRecordsUpdated(); 34 | 35 | assertThat(result).isEqualTo(0); 36 | } 37 | 38 | @Value 39 | private static class Dto { 40 | public final int value; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/ObjectMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.val; 18 | 19 | import java.util.Map; 20 | import java.util.Set; 21 | import java.util.function.Function; 22 | import java.util.stream.Collectors; 23 | 24 | class ObjectMapper { 25 | private final Set placeholders; 26 | 27 | public ObjectMapper(String sql) { 28 | placeholders = PlaceholderUtils.findAll(sql); 29 | } 30 | 31 | public Map map(Object o) { 32 | val fieldMapper = new FieldMapper(o); 33 | return placeholders.stream() 34 | .collect(Collectors.toMap(Function.identity(), fieldMapper::read)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/SingleResultTests.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client; 2 | 3 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 4 | import com.amazon.rdsdata.client.testutil.TestBase; 5 | import lombok.val; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 9 | import static java.util.Collections.emptyList; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | public class SingleResultTests extends TestBase { 14 | @Test 15 | void shouldMapToSingleObject() { 16 | mockReturnValue(mockColumn("fieldName", SdkConstructs.longField(1L))); 17 | 18 | val result = client.forSql("SELECT *") 19 | .execute() 20 | .singleValue(Long.class); 21 | 22 | assertThat(result).isEqualTo(1L); 23 | } 24 | 25 | @Test 26 | void shouldThrowExceptionIfResultSetHasNoRows() { 27 | mockReturnValues(); 28 | 29 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().singleValue(Long.class)) 30 | .isInstanceOf(EmptyResultSetException.class); 31 | } 32 | 33 | @Test 34 | void shouldThrowExceptionIfResultSetHasNoColumns() { 35 | mockReturnValues(emptyList(), emptyList()); // add two empty rows 36 | 37 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().singleValue(Long.class)) 38 | .isInstanceOf(EmptyResultSetException.class); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/testutil/SdkConstructs.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client.testutil; 2 | 3 | import software.amazon.awssdk.core.SdkBytes; 4 | import software.amazon.awssdk.services.rdsdata.model.Field; 5 | import software.amazon.awssdk.services.rdsdata.model.SqlParameter; 6 | 7 | public final class SdkConstructs { 8 | public static Field longField(long value) { 9 | return Field.builder().longValue(value).build(); 10 | } 11 | 12 | public static Field doubleField(double value) { 13 | return Field.builder().doubleValue(value).build(); 14 | } 15 | 16 | public static Field stringField(String value) { 17 | return Field.builder().stringValue(value).build(); 18 | } 19 | 20 | public static Field blobField(byte[] value) { 21 | return Field.builder().blobValue(SdkBytes.fromByteArray(value)).build(); 22 | } 23 | 24 | public static Field booleanField(boolean value) { 25 | return Field.builder().booleanValue(value).build(); 26 | } 27 | 28 | public static Field nullField() { 29 | return Field.builder().isNull(true).build(); 30 | } 31 | 32 | public static SqlParameter parameter(String name, Field value) { 33 | return SqlParameter.builder() 34 | .name(name) 35 | .value(value) 36 | .build(); 37 | } 38 | 39 | public static SqlParameter parameter(String name, Field value, String hint) { 40 | return SqlParameter.builder() 41 | .name(name) 42 | .value(value) 43 | .typeHint(hint) 44 | .build(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/SingleParameterWitherTests.java: -------------------------------------------------------------------------------- 1 | package com.amazon.rdsdata.client; 2 | 3 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 4 | import com.amazon.rdsdata.client.testutil.TestBase; 5 | import lombok.val; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 11 | 12 | public class SingleParameterWitherTests extends TestBase { 13 | @BeforeEach 14 | public void beforeEach() { 15 | mockReturnValue(); // return empty response by default 16 | } 17 | 18 | @Test 19 | void shouldSupportWithParameter() { 20 | client.forSql("INSERT INTO table(a, b) VALUES(:a, :b)") 21 | .withParameter("a", 100) 22 | .withParameter("b", "hello") 23 | .execute(); 24 | 25 | val request = captureRequest(); 26 | assertThat(request.parameters()).containsExactly( 27 | SdkConstructs.parameter("a", SdkConstructs.longField(100L)), 28 | SdkConstructs.parameter("b", SdkConstructs.stringField("hello")) 29 | ); 30 | } 31 | 32 | @Test 33 | void shouldNotSupportWithParameterAfterOtherOperations() { 34 | assertThatThrownBy(() -> { 35 | client.forSql("INSERT INTO table(a, b) VALUES(:a, :b)") 36 | .withParamSets(new Object(), new Object()) 37 | .withParameter("a", 100); 38 | }) 39 | .isInstanceOf(IllegalArgumentException.class) 40 | .hasMessageContaining("Parameters are already supplied"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/MapToListTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import com.google.common.collect.ImmutableList; 20 | import lombok.Value; 21 | import lombok.val; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | public class MapToListTests extends TestBase { 28 | @Test 29 | void shouldMapViaAllArgsConstructor() { 30 | mockReturnValues( 31 | ImmutableList.of( // first row 32 | mockColumn("intField", SdkConstructs.longField(1L)), 33 | mockColumn("stringField", SdkConstructs.stringField("hello")) 34 | ), ImmutableList.of( // 2nd row 35 | mockColumn("intField", SdkConstructs.longField(2L)), 36 | mockColumn("stringField", SdkConstructs.stringField("world")) 37 | )); 38 | 39 | val result = client.forSql("SELECT *") 40 | .execute() 41 | .mapToList(TestBean.class); 42 | 43 | assertThat(result).containsExactly( 44 | new TestBean(1, "hello"), 45 | new TestBean(2, "world")); 46 | } 47 | 48 | @Value 49 | private static class TestBean { 50 | public final int intField; 51 | public final String stringField; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/FieldPropertyWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.AllArgsConstructor; 18 | import lombok.val; 19 | 20 | import java.lang.reflect.Field; 21 | import java.lang.reflect.Modifier; 22 | import java.util.Optional; 23 | 24 | import static lombok.AccessLevel.PRIVATE; 25 | 26 | @AllArgsConstructor(access = PRIVATE) 27 | class FieldPropertyWriter implements PropertyWriter { 28 | private Object instance; 29 | private Class fieldType; 30 | private Field field; 31 | 32 | static Optional fieldPropertyWriterFor(Object instance, String fieldName) { 33 | val instanceType = instance.getClass(); 34 | try { 35 | val field = getField(instanceType, fieldName); 36 | if (Modifier.isStatic(field.getModifiers())) { 37 | throw MappingException.staticField(instanceType, fieldName); 38 | } 39 | val writer = new FieldPropertyWriter(instance, field.getType(), field); 40 | return Optional.of(writer); 41 | } catch (NoSuchFieldException e) { 42 | return Optional.empty(); 43 | } 44 | } 45 | 46 | private static Field getField(Class instanceType, String fieldName) throws NoSuchFieldException { 47 | try { 48 | return instanceType.getField(fieldName); 49 | } catch (NoSuchFieldException e) { 50 | // Falling back to getDeclaredField() to find private fields 51 | return instanceType.getDeclaredField(fieldName); 52 | } 53 | } 54 | 55 | @Override 56 | public void write(Object value) { 57 | try { 58 | field.set(instance, value); 59 | } catch (IllegalAccessException e) { 60 | throw MappingException.cannotAccessField(instance.getClass(), field.getName()); 61 | } 62 | } 63 | 64 | @Override 65 | public Class getType() { 66 | return fieldType; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/PlaceholderUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.Value; 18 | import lombok.val; 19 | 20 | import java.util.HashMap; 21 | import java.util.HashSet; 22 | import java.util.Map; 23 | import java.util.Set; 24 | import java.util.regex.Pattern; 25 | 26 | import static com.google.common.base.Preconditions.checkArgument; 27 | 28 | class PlaceholderUtils { 29 | static String ERROR_NUMBER_OF_PARAMS_MISMATCH = "Number of placeholders does not match number of parameters"; 30 | 31 | private static Pattern REGEX_NAMED_PLACEHOLDER = Pattern.compile(":([a-zA-Z0-9_]+)"); 32 | 33 | // TODO: skip string literals and comments 34 | public static PlaceholderConvertResult convertToNamed(String sql, Object... parameters) { 35 | val parts = sql.split("\\?"); 36 | checkArgument(numberOfParametersMatches(parts, parameters), ERROR_NUMBER_OF_PARAMS_MISMATCH); 37 | 38 | val resultingSql = new StringBuilder(parts[0]); 39 | val parametersMap = new HashMap(); 40 | for (int i = 1; i < parts.length; i++) { 41 | resultingSql.append(":").append(i); 42 | resultingSql.append(parts[i]); 43 | parametersMap.put(String.valueOf(i), parameters[i - 1]); 44 | } 45 | 46 | return new PlaceholderConvertResult(resultingSql.toString(), parametersMap); 47 | } 48 | 49 | private static boolean numberOfParametersMatches(String[] parts, Object[] parameters) { 50 | return parts.length - 1 == parameters.length; 51 | } 52 | 53 | @Value 54 | public static class PlaceholderConvertResult { 55 | public final String sql; 56 | public final Map parameters; 57 | } 58 | 59 | // TODO: skip string literals and comments 60 | public static Set findAll(String sql) { 61 | val matcher = REGEX_NAMED_PLACEHOLDER.matcher(sql); 62 | val result = new HashSet(); 63 | while (matcher.find()) { 64 | result.add(matcher.group(1)); 65 | } 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/InlineParametersTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.TestBase; 18 | import lombok.val; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static com.amazon.rdsdata.client.PlaceholderUtils.ERROR_NUMBER_OF_PARAMS_MISMATCH; 23 | import static com.amazon.rdsdata.client.RdsData.ERROR_EMPTY_OR_NULL_SQL; 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 26 | 27 | public class InlineParametersTests extends TestBase { 28 | @BeforeEach 29 | public void beforeEach() { 30 | mockReturnValue(); // return empty response by default 31 | } 32 | 33 | @Test 34 | void shouldReplaceQuestionMarksWithNamedPlaceholders() { 35 | client.forSql("INSERT INTO tbl1(a, b, c) VALUES(?, ?, ?)", 1, 2, 3) 36 | .execute(); 37 | 38 | val request = captureRequest(); 39 | assertThat(request.sql()).isEqualTo("INSERT INTO tbl1(a, b, c) VALUES(:1, :2, :3)"); 40 | } 41 | 42 | @Test 43 | void shouldSupportNoParameters() { 44 | client.forSql("SELECT 1").execute(); 45 | 46 | val request = captureRequest(); 47 | assertThat(request.sql()).isEqualTo("SELECT 1"); 48 | assertThat(request.parameters()).isEmpty(); 49 | } 50 | 51 | @Test 52 | void shouldThrowExceptionIfSqlIsNull() { 53 | assertThatThrownBy(() -> client.forSql(null)) 54 | .isInstanceOf(IllegalArgumentException.class) 55 | .hasMessage(ERROR_EMPTY_OR_NULL_SQL); 56 | } 57 | 58 | @Test 59 | void shouldThrowExceptionIfSqlIsNullWithParams() { 60 | assertThatThrownBy(() -> client.forSql(null, 1, 2, 3)) 61 | .isInstanceOf(IllegalArgumentException.class) 62 | .hasMessage(ERROR_EMPTY_OR_NULL_SQL); 63 | } 64 | 65 | @Test 66 | void shouldThrowExceptionIfNumberOfParametersDontMatchNumberOfPlaceholders() { 67 | assertThatThrownBy(() -> client.forSql("INSERT INTO tbl1(a, b, c) VALUES(?, ?, ?)", 1, 2, 3, 4)) 68 | .isInstanceOf(IllegalArgumentException.class) 69 | .hasMessage(ERROR_NUMBER_OF_PARAMS_MISMATCH); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/MapToSingleTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import lombok.Value; 20 | import lombok.val; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.assertj.core.api.Assertions.assertThatCode; 26 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 27 | 28 | public class MapToSingleTests extends TestBase { 29 | @Test 30 | void shouldMapToSingleObject() { 31 | mockReturnValue( 32 | mockColumn("intField", SdkConstructs.longField(1L)), 33 | mockColumn("stringField", SdkConstructs.stringField("hello"))); 34 | 35 | val result = client.forSql("SELECT *") 36 | .execute() 37 | .mapToSingle(TestBean.class); 38 | 39 | assertThat(result).isEqualTo(new TestBean(1, "hello")); 40 | } 41 | 42 | @Value 43 | private static class TestBean { 44 | public final int intField; 45 | public final String stringField; 46 | } 47 | 48 | @Test 49 | void shouldThrowExceptionIfNoResults() { 50 | mockReturnValues(); // client returns empty result set 51 | 52 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(Object.class)) 53 | .isInstanceOf(MappingException.class) 54 | .hasMessage(MappingException.ERROR_EMPTY_RESULT_SET); 55 | } 56 | 57 | @Test 58 | void shouldTolerateNullMetadata() { 59 | returnNullMetadataAndResultSet(); 60 | 61 | // DML value results usually do not contain metadata and values 62 | assertThatCode(() -> client.forSql("INSERT INTO tbl VALUES(1)").execute()) 63 | .doesNotThrowAnyException(); 64 | } 65 | 66 | @Test 67 | void shouldUseLabelIfConfigured() { 68 | mockReturnValue( 69 | mockColumn("int_name", "intField", SdkConstructs.longField(1L)), 70 | mockColumn("string_name", "stringField", SdkConstructs.stringField("hello"))); 71 | 72 | val result = client 73 | .withMappingOptions(MappingOptions.DEFAULT.withUseLabelForMapping(true)) 74 | .forSql("INSERT INTO tbl VALUES(1)") 75 | .execute() 76 | .mapToSingle(TestBean.class); 77 | 78 | assertThat(result).isEqualTo(new TestBean(1, "hello")); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/SetterPropertyWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.AllArgsConstructor; 18 | import lombok.val; 19 | 20 | import java.lang.reflect.InvocationTargetException; 21 | import java.lang.reflect.Method; 22 | import java.lang.reflect.Modifier; 23 | import java.util.Optional; 24 | import java.util.stream.Stream; 25 | 26 | import static java.util.stream.Collectors.toList; 27 | import static lombok.AccessLevel.PRIVATE; 28 | 29 | @AllArgsConstructor(access = PRIVATE) 30 | class SetterPropertyWriter implements PropertyWriter { 31 | private Object instance; 32 | private Method setter; 33 | private String fieldName; 34 | 35 | static Optional setterPropertyWriterFor(Object instance, String fieldName) { 36 | val instanceType = instance.getClass(); 37 | val setterName = buildSetterName(fieldName); 38 | 39 | val possibleSetterMethods = Stream.of(instanceType.getMethods()) 40 | .filter(method -> method.getName().equals(setterName)) 41 | .filter(SetterPropertyWriter::isNotStatic) 42 | .filter(SetterPropertyWriter::hasOneParameter) 43 | .filter(SetterPropertyWriter::isPublic) 44 | .collect(toList()); 45 | 46 | if (possibleSetterMethods.size() > 1) { 47 | throw MappingException.ambiguousSetter(fieldName, possibleSetterMethods); 48 | } 49 | 50 | if (possibleSetterMethods.size() == 0) { 51 | return Optional.empty(); 52 | } 53 | 54 | return Optional.of(new SetterPropertyWriter(instance, possibleSetterMethods.get(0), fieldName)); 55 | } 56 | 57 | private static boolean isNotStatic(Method method) { 58 | return !Modifier.isStatic(method.getModifiers()); 59 | } 60 | 61 | private static boolean hasOneParameter(Method method) { 62 | return method.getParameterCount() == 1; 63 | } 64 | 65 | private static boolean isPublic(Method method) { 66 | return Modifier.isPublic(method.getModifiers()); 67 | } 68 | 69 | private static String buildSetterName(String fieldName) { 70 | return "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); 71 | } 72 | 73 | @Override 74 | public void write(Object value) { 75 | try { 76 | setter.invoke(instance, value); 77 | } catch (IllegalAccessException | InvocationTargetException e) { 78 | throw MappingException.cannotSetValue(fieldName, e); 79 | } 80 | } 81 | 82 | @Override 83 | public Class getType() { 84 | return setter.getParameterTypes()[0]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RDS Data API Client Library for Java 2 | 3 | The **RDS Data API Client Library for Java** provides an alternative way 4 | to use RDS Data API. Using this library, you can map your client-side 5 | classes to requests and responses of the Data API. This mapping support 6 | can ease integration with some specific Java types, such as `Date`, `Time`, 7 | and `BigDecimal`. 8 | 9 | ### Getting the Java Client Library for RDS Data API 10 | The Data API Java client library is open source in GitHub. You can build 11 | the library manually from the source files, but the best practice is to 12 | consume the library using Maven: 13 | 14 | **Version 2.x** 15 | 16 | ```xml 17 | 18 | software.amazon.rdsdata 19 | rds-data-api-client-library-java 20 | 2.0.0 21 | 22 | ``` 23 | 24 | **Version 1.x** (AWS SDK 1.x compatible) 25 | 26 | ```xml 27 | 28 | software.amazon.rdsdata 29 | rds-data-api-client-library-java 30 | 1.0.8 31 | 32 | ``` 33 | 34 | ### Using the Client Library 35 | Following, you can find some common examples of using the Data API Java client library. These examples assume that you have a table accounts with two columns: accountId and name. You also have the following data transfer object (DTO). 36 | 37 | ```java 38 | public class Account { 39 | int accountId; 40 | String name; 41 | // getters and setters omitted 42 | } 43 | ``` 44 | 45 | The client library enables you to pass DTOs as input parameters. The following example shows how customer DTOs are mapped to input parameters sets. 46 | 47 | ```java 48 | var account1 = new Account(1, "John"); 49 | var account2 = new Account(2, "Mary"); 50 | client.forSql("INSERT INTO accounts(accountId, name) VALUES(:accountId, :name)") 51 | .withParamSets(account1, account2) 52 | .execute(); 53 | ``` 54 | 55 | In some cases, it's easier to work with simple values as input parameters. You can do so with the following syntax. 56 | 57 | ```java 58 | client.forSql("INSERT INTO accounts(accountId, name) VALUES(:accountId, :name)") 59 | .withParameter("accountId", 3) 60 | .withParameter("name", "Karen") 61 | .execute(); 62 | ``` 63 | 64 | The following is another example that works with simple values as input parameters. 65 | 66 | ```java 67 | client.forSql("INSERT INTO accounts(accountId, name) VALUES(?, ?)", 4, "Peter") 68 | .execute(); 69 | ``` 70 | 71 | The client library provides automatic mapping to DTOs when an execution result is returned. The following examples show how the execution result is mapped to your DTOs. 72 | 73 | ```java 74 | List result = client.forSql("SELECT * FROM accounts") 75 | .execute() 76 | .mapToList(Account.class); 77 | ``` 78 | 79 | ```java 80 | Account result = client.forSql("SELECT * FROM accounts WHERE account_id = 1") 81 | .execute() 82 | .mapToSingle(Account.class); 83 | ``` 84 | 85 | In many cases, the database result set contains only a single value. In order to simplify retrieving such results, the client library offers the following API: 86 | 87 | ```java 88 | int numberOfAccounts = client.forSql("SELECT COUNT(*) FROM accounts") 89 | .execute() 90 | .singleValue(Integer.class); 91 | ``` -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/MappingOutputViaAllArgsConstructorTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import lombok.val; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static com.amazon.rdsdata.client.MappingException.ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS; 23 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 26 | 27 | public class MappingOutputViaAllArgsConstructorTests extends TestBase { 28 | @Test 29 | void shouldMapViaAllArgsConstructor() { 30 | mockReturnValue( 31 | mockColumn("stringValue", SdkConstructs.stringField("apple")), 32 | mockColumn("intValue", SdkConstructs.longField(15L))); 33 | 34 | val result = client.forSql("SELECT *") 35 | .execute() 36 | .mapToSingle(ConstructorWithParameterNames.class); 37 | assertThat(result.result).isEqualTo("apple15"); 38 | } 39 | 40 | private static class ConstructorWithParameterNames { 41 | public final String result; 42 | public ConstructorWithParameterNames(String stringValue, int intValue) { 43 | this.result = stringValue + intValue; 44 | } 45 | } 46 | 47 | @Test 48 | void shouldFailToMapIfConstructorHasMoreParameters() { 49 | mockReturnValue(mockColumn("stringValue", SdkConstructs.stringField("apple"))); 50 | 51 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(ConstructorWithExtraParameters.class)) 52 | .isInstanceOf(MappingException.class) 53 | .hasMessage(ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS, ConstructorWithExtraParameters.class.getName()); 54 | } 55 | 56 | private static class ConstructorWithExtraParameters { 57 | @SuppressWarnings("unused") 58 | public ConstructorWithExtraParameters(String stringValue, int intValue) { } 59 | } 60 | 61 | @Test 62 | void shouldFailToMapIfConstructorHasLessParameters() { 63 | mockReturnValue( 64 | mockColumn("stringValue1", SdkConstructs.stringField("apple")), 65 | mockColumn("stringValue2", SdkConstructs.stringField("orange"))); 66 | 67 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(ConstructorWithLessParameters.class)) 68 | .isInstanceOf(MappingException.class) 69 | .hasMessage(ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS, ConstructorWithLessParameters.class.getName()); 70 | } 71 | 72 | private static class ConstructorWithLessParameters { 73 | @SuppressWarnings("unused") 74 | public ConstructorWithLessParameters(String stringValue1) { } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/PropertyObjectWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.RequiredArgsConstructor; 18 | import lombok.val; 19 | 20 | import java.lang.reflect.Constructor; 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.util.List; 23 | import java.util.Optional; 24 | 25 | import static com.amazon.rdsdata.client.FieldPropertyWriter.fieldPropertyWriterFor; 26 | import static com.amazon.rdsdata.client.SetterPropertyWriter.setterPropertyWriterFor; 27 | 28 | @RequiredArgsConstructor 29 | class PropertyObjectWriter extends ObjectWriter { 30 | private final Class mapperClass; 31 | private final List fieldNames; 32 | private final MappingOptions mappingOptions; 33 | 34 | public static ObjectWriter create(Class mapperClass, List fieldNames, MappingOptions mappingOptions) { 35 | return new PropertyObjectWriter<>(mapperClass, fieldNames, mappingOptions); 36 | } 37 | 38 | @Override 39 | public T write(ExecutionResult.Row row) { 40 | val constructor = findNoArgsConstructor() 41 | .orElseThrow(() -> MappingException.cannotCreateInstanceViaNoArgsConstructor(mapperClass)); 42 | val instance = createInstance(constructor); 43 | setAllProperties(instance, row); 44 | return instance; 45 | } 46 | 47 | private Optional> findNoArgsConstructor() { 48 | try { 49 | return Optional.of(mapperClass.getDeclaredConstructor()); 50 | } catch (NoSuchMethodException e) { 51 | return Optional.empty(); 52 | } 53 | } 54 | 55 | private T createInstance(Constructor constructor) { 56 | try { 57 | return constructor.newInstance(); 58 | } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { 59 | throw MappingException.cannotCreateInstance(constructor.getDeclaringClass(), e); 60 | } 61 | } 62 | 63 | private void setAllProperties(T instance, ExecutionResult.Row row) { 64 | for (int i = 0; i < fieldNames.size(); i++) { 65 | val name = fieldNames.get(i); 66 | val index = i; 67 | findPropertyWriter(instance, name) 68 | .ifPresent(field -> setProperty(field, row, index)); 69 | } 70 | } 71 | 72 | private void setProperty(PropertyWriter propertyWriter, ExecutionResult.Row row, int index) { 73 | val value = row.getValue(index, propertyWriter.getType()); 74 | propertyWriter.write(value); 75 | } 76 | 77 | private Optional findPropertyWriter(Object instance, String fieldName) { 78 | val result = setterPropertyWriterFor(instance, fieldName) 79 | .map(Optional::of) 80 | .orElseGet(() -> fieldPropertyWriterFor(instance, fieldName)); 81 | 82 | if (!result.isPresent() && !mappingOptions.ignoreMissingSetters) { 83 | throw MappingException.noFieldOrSetter(instance.getClass(), fieldName); 84 | } 85 | return result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/TransactionTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.TestBase; 18 | import lombok.val; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.UUID; 23 | 24 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockBeginTransaction; 25 | import static java.util.Collections.emptyList; 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | public class TransactionTests extends TestBase { 29 | @BeforeEach 30 | void beforeEach() { 31 | mockReturnValue(); // return empty response by default 32 | } 33 | 34 | @Test 35 | public void shouldPropagateTransactionId() { 36 | val transactionId = UUID.randomUUID().toString(); 37 | 38 | client.forSql("SELECT *") 39 | .withTransactionId(transactionId) 40 | .execute(); 41 | 42 | val request = captureRequest(); 43 | assertThat(request.transactionId()).isEqualTo(transactionId); 44 | } 45 | 46 | @Test 47 | public void shouldPropagateTransactionIdForBatch() { 48 | val transactionId = UUID.randomUUID().toString(); 49 | 50 | client.forSql("SELECT *") 51 | .withTransactionId(transactionId) 52 | .withParamSets(emptyList(), emptyList()) 53 | .execute(); 54 | 55 | val request = captureBatchRequest(); 56 | assertThat(request.transactionId()).isEqualTo(transactionId); 57 | } 58 | 59 | @Test 60 | public void shouldBeginTransaction() { 61 | val transactionId = UUID.randomUUID().toString(); 62 | mockBeginTransaction(sdkClient, transactionId); 63 | 64 | val result = client.beginTransaction(); 65 | assertThat(result).isEqualTo(transactionId); 66 | 67 | val request = captureBeginTransactionRequest(); 68 | assertThat(request.database()).isEqualTo(SAMPLE_DB); 69 | assertThat(request.resourceArn()).isEqualTo(SAMPLE_RESOURCE_ARN); 70 | assertThat(request.secretArn()).isEqualTo(SAMPLE_SECRET_ARN); 71 | } 72 | 73 | @Test 74 | public void shouldCommitTransaction() { 75 | val transactionId = UUID.randomUUID().toString(); 76 | 77 | client.commitTransaction(transactionId); 78 | 79 | val request = captureCommitTransactionRequest(); 80 | assertThat(request.transactionId()).isEqualTo(transactionId); 81 | assertThat(request.resourceArn()).isEqualTo(SAMPLE_RESOURCE_ARN); 82 | assertThat(request.secretArn()).isEqualTo(SAMPLE_SECRET_ARN); 83 | } 84 | 85 | @Test 86 | public void shouldRollbackTransaction() { 87 | val transactionId = UUID.randomUUID().toString(); 88 | 89 | client.rollbackTransaction(transactionId); 90 | 91 | val request = captureRollbackTransactionRequest(); 92 | assertThat(request.transactionId()).isEqualTo(transactionId); 93 | assertThat(request.resourceArn()).isEqualTo(SAMPLE_RESOURCE_ARN); 94 | assertThat(request.secretArn()).isEqualTo(SAMPLE_SECRET_ARN); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/ConstructorObjectWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.val; 18 | 19 | import java.lang.reflect.Constructor; 20 | import java.lang.reflect.InvocationTargetException; 21 | import java.lang.reflect.Parameter; 22 | import java.util.Arrays; 23 | import java.util.HashSet; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Optional; 27 | import java.util.Set; 28 | import java.util.stream.IntStream; 29 | import java.util.stream.Stream; 30 | 31 | import static java.util.stream.Collectors.toMap; 32 | import static java.util.stream.Collectors.toSet; 33 | 34 | class ConstructorObjectWriter extends ObjectWriter { 35 | private final Constructor constructor; 36 | private final Map indexByName; 37 | 38 | ConstructorObjectWriter(Constructor constructor, List fieldNames) { 39 | this.constructor = constructor; 40 | this.indexByName = buildIndexByNameMap(fieldNames); 41 | } 42 | 43 | private static Map buildIndexByNameMap(List fieldNames) { 44 | return IntStream.range(0, fieldNames.size()) 45 | .boxed() 46 | .collect(toMap(fieldNames::get, i -> i)); 47 | } 48 | 49 | // Tries to create an ObjectWriter that populates object via all-args constructor 50 | public static Optional> create(Class mapperClass, List fieldNames) { 51 | if (fieldNames.size() == 0) { 52 | return Optional.empty(); 53 | } 54 | 55 | return Stream.of(mapperClass.getDeclaredConstructors()) 56 | .filter(c -> containsAllFields(c, fieldNames)) 57 | // TODO: check if public 58 | .findFirst() 59 | .map(c -> new ConstructorObjectWriter<>((Constructor) c, fieldNames)); 60 | } 61 | 62 | private static boolean containsAllFields(Constructor constructor, List fieldNames) { 63 | val parameterNames = getParameterNames(constructor); 64 | return parameterNames.equals(new HashSet<>(fieldNames)); 65 | } 66 | 67 | private static Set getParameterNames(Constructor constructor) { 68 | return Arrays.stream(constructor.getParameters()) 69 | .map(Parameter::getName) 70 | .collect(toSet()); 71 | } 72 | 73 | @Override 74 | public T write(ExecutionResult.Row row) { 75 | try { 76 | return constructor.newInstance(buildArgumentsList(row)); 77 | } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { 78 | throw MappingException.cannotCreateInstance(constructor.getDeclaringClass(), e); 79 | } 80 | } 81 | 82 | private Object[] buildArgumentsList(ExecutionResult.Row row) { 83 | val parameters = constructor.getParameters(); 84 | val args = new Object[parameters.length]; 85 | for (int i = 0; i < parameters.length; i++) { 86 | val name = parameters[i].getName(); 87 | val indexInFieldsList = indexByName.get(name); 88 | val value = row.getValue(indexInFieldsList, parameters[i].getType()); 89 | args[i] = value; 90 | } 91 | return args; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/FieldMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.RequiredArgsConstructor; 18 | import lombok.val; 19 | 20 | import java.lang.reflect.Field; 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.lang.reflect.Method; 23 | import java.util.Optional; 24 | 25 | import static com.google.common.base.Preconditions.checkArgument; 26 | 27 | @RequiredArgsConstructor 28 | class FieldMapper { 29 | static Object NULL = new Object(); 30 | 31 | static String ERROR_FIELD_NOT_FOUND = "Cannot find field or getter corresponding to placeholder '%s' in object '%s'"; 32 | static String ERROR_VOID_RETURN_TYPE_NOT_SUPPORTED = "Void return type is not supported"; 33 | 34 | private final Object object; 35 | 36 | public Object read(String fieldName) { 37 | return getValueFromGetter(fieldName) 38 | .orElseGet(() -> getValueFromField(fieldName) 39 | .orElseThrow(() -> buildCannotFindException(fieldName))); 40 | } 41 | 42 | private RuntimeException buildCannotFindException(String fieldName) { 43 | val errorMessage = String.format(ERROR_FIELD_NOT_FOUND, fieldName, object); 44 | return new IllegalArgumentException(errorMessage); 45 | } 46 | 47 | private Optional getValueFromField(String fieldName) { 48 | try { 49 | val field = getField(object, fieldName); 50 | field.setAccessible(true); 51 | val result = field.get(object); 52 | return Optional.of(result == null ? NULL : result); 53 | } catch (NoSuchFieldException e) { 54 | return Optional.empty(); 55 | } catch (IllegalAccessException e) { 56 | throw new IllegalStateException("Cannot access field " + fieldName + " from object " + object); 57 | } 58 | } 59 | 60 | private Field getField(Object object, String fieldName) throws NoSuchFieldException { 61 | try { 62 | return object.getClass().getField(fieldName); 63 | } catch (NoSuchFieldException e) { 64 | // Falling back to getDeclaredField() to access private fields 65 | return object.getClass().getDeclaredField(fieldName); 66 | } 67 | } 68 | 69 | private Optional getValueFromGetter(String fieldName) { 70 | val methodName = buildGetterName(fieldName); 71 | try { 72 | val method = getMethod(object, methodName); 73 | checkArgument(method.getReturnType() != void.class, ERROR_VOID_RETURN_TYPE_NOT_SUPPORTED); 74 | 75 | method.setAccessible(true); 76 | val result = method.invoke(object); 77 | return Optional.of(result == null ? NULL : result); 78 | } catch (NoSuchMethodException e) { 79 | return Optional.empty(); 80 | } catch (IllegalAccessException | InvocationTargetException e) { 81 | throw new IllegalStateException("Cannot access method " + methodName + " from object " + object); 82 | } 83 | } 84 | 85 | private Method getMethod(Object object, String methodName) throws NoSuchMethodException { 86 | try { 87 | return object.getClass().getMethod(methodName); 88 | } catch (NoSuchMethodException e) { 89 | // Falling back to getDeclaredMethod() to access private methods 90 | return object.getClass().getDeclaredMethod(methodName); 91 | } 92 | } 93 | 94 | private String buildGetterName(String fieldName) { 95 | return "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/testutil/TestBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client.testutil; 16 | 17 | import com.amazon.rdsdata.client.RdsData; 18 | import lombok.val; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.mockito.ArgumentCaptor; 21 | import software.amazon.awssdk.services.rdsdata.RdsDataClient; 22 | import software.amazon.awssdk.services.rdsdata.model.BatchExecuteStatementRequest; 23 | import software.amazon.awssdk.services.rdsdata.model.BeginTransactionRequest; 24 | import software.amazon.awssdk.services.rdsdata.model.CommitTransactionRequest; 25 | import software.amazon.awssdk.services.rdsdata.model.ExecuteStatementRequest; 26 | import software.amazon.awssdk.services.rdsdata.model.RollbackTransactionRequest; 27 | 28 | import java.util.List; 29 | 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.verify; 32 | 33 | public class TestBase { 34 | protected static final String SAMPLE_DB = "mydb"; 35 | protected static final String SAMPLE_RESOURCE_ARN = "arn:resource"; 36 | protected static final String SAMPLE_SECRET_ARN = "arn:secret"; 37 | 38 | protected RdsData client; 39 | protected RdsDataClient sdkClient = mock(RdsDataClient.class); 40 | 41 | @BeforeEach 42 | void createClient() { 43 | client = RdsData.builder() 44 | .sdkClient(sdkClient) 45 | .database(SAMPLE_DB) 46 | .resourceArn(SAMPLE_RESOURCE_ARN) 47 | .secretArn(SAMPLE_SECRET_ARN) 48 | .build(); 49 | } 50 | 51 | protected void mockReturnValue(MockingTools.ColumnDefinition... columns) { 52 | MockingTools.mockReturnValue(sdkClient, 0L, columns); 53 | } 54 | 55 | 56 | protected void mockReturnValue(long numberOfRecordsUpdated, MockingTools.ColumnDefinition... columns) { 57 | MockingTools.mockReturnValue(sdkClient, numberOfRecordsUpdated, columns); 58 | } 59 | 60 | @SafeVarargs 61 | protected final void mockReturnValues(List... rows) { 62 | MockingTools.mockReturnValues(sdkClient, 0L, rows); 63 | } 64 | 65 | protected final void returnNullMetadataAndResultSet() { 66 | MockingTools.returnNullMetadataAndResultSet(sdkClient); 67 | } 68 | 69 | protected ExecuteStatementRequest captureRequest() { 70 | val captor = ArgumentCaptor.forClass(ExecuteStatementRequest.class); 71 | verify(sdkClient).executeStatement(captor.capture()); 72 | return captor.getValue(); 73 | } 74 | 75 | protected BatchExecuteStatementRequest captureBatchRequest() { 76 | val captor = ArgumentCaptor.forClass(BatchExecuteStatementRequest.class); 77 | verify(sdkClient).batchExecuteStatement(captor.capture()); 78 | return captor.getValue(); 79 | } 80 | 81 | protected BeginTransactionRequest captureBeginTransactionRequest() { 82 | val captor = ArgumentCaptor.forClass(BeginTransactionRequest.class); 83 | verify(sdkClient).beginTransaction(captor.capture()); 84 | return captor.getValue(); 85 | } 86 | 87 | protected CommitTransactionRequest captureCommitTransactionRequest() { 88 | val captor = ArgumentCaptor.forClass(CommitTransactionRequest.class); 89 | verify(sdkClient).commitTransaction(captor.capture()); 90 | return captor.getValue(); 91 | } 92 | 93 | protected RollbackTransactionRequest captureRollbackTransactionRequest() { 94 | val captor = ArgumentCaptor.forClass(RollbackTransactionRequest.class); 95 | verify(sdkClient).rollbackTransaction(captor.capture()); 96 | return captor.getValue(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/MappingException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.val; 18 | import software.amazon.awssdk.services.rdsdata.model.Field; 19 | 20 | import java.lang.reflect.Method; 21 | import java.util.List; 22 | 23 | import static java.util.stream.Collectors.joining; 24 | 25 | public class MappingException extends RuntimeException { 26 | static final String ERROR_NO_FIELD_OR_SETTER = "Class '%s' does not contain field '%s' or a corresponding setter"; 27 | static final String ERROR_CANNOT_ACCESS_FIELD = "Cannot access field '%s' in class %s"; 28 | static final String ERROR_CANNOT_CREATE_INSTANCE = "Cannot create instance of type %s"; 29 | static final String ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS = "Cannot create instance of type %s: public no args constructor not found"; 30 | static final String ERROR_STATIC_FIELD = "Field '%s' in class %s is static"; 31 | static final String ERROR_CANNOT_SET_VALUE = "Cannot set value '%s'"; 32 | static final String ERROR_EMPTY_RESULT_SET = "Result set is empty"; 33 | static final String ERROR_CANNOT_CONVERT_TO_TYPE = "Cannot convert field %s to type %s"; 34 | static final String ERROR_AMBIGUOUS_SETTER = "Ambiguous setter for field %s. Possible setters found: %s"; 35 | 36 | private MappingException(String message) { 37 | super(message); 38 | } 39 | 40 | private MappingException(String message, Throwable cause) { 41 | super(message, cause); 42 | } 43 | 44 | static MappingException noFieldOrSetter(Class clazz, String fieldName) { 45 | val message = String.format(ERROR_NO_FIELD_OR_SETTER, clazz.getName(), fieldName); 46 | return new MappingException(message); 47 | } 48 | 49 | static MappingException cannotAccessField(Class clazz, String fieldName) { 50 | val message = String.format(ERROR_CANNOT_ACCESS_FIELD, fieldName, clazz.getName()); 51 | return new MappingException(message); 52 | } 53 | 54 | static MappingException cannotCreateInstanceViaNoArgsConstructor(Class clazz) { 55 | val message = String.format(ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS, clazz.getName()); 56 | return new MappingException(message); 57 | } 58 | 59 | static MappingException cannotCreateInstance(Class clazz, Throwable cause) { 60 | val message = String.format(ERROR_CANNOT_CREATE_INSTANCE, clazz.getName()); 61 | return new MappingException(message, cause); 62 | } 63 | 64 | static MappingException staticField(Class clazz, String fieldName) { 65 | val message = String.format(ERROR_STATIC_FIELD, fieldName, clazz.getName()); 66 | return new MappingException(message); 67 | } 68 | 69 | static MappingException cannotSetValue(String fieldName, Throwable cause) { 70 | val message = String.format(ERROR_CANNOT_SET_VALUE, fieldName); 71 | return new MappingException(message, cause); 72 | } 73 | 74 | static MappingException emptyResultSet() { 75 | return new MappingException(ERROR_EMPTY_RESULT_SET); 76 | } 77 | 78 | static MappingException cannotConvertToType(Field field, Class targetType) { 79 | val message = String.format(ERROR_CANNOT_CONVERT_TO_TYPE, field.toString(), targetType.toString()); 80 | return new MappingException(message); 81 | } 82 | 83 | static MappingException ambiguousSetter(String fieldName, List possibleSetters) { 84 | val settersListString = possibleSetters.stream() 85 | .map(Method::toString) 86 | .collect(joining(", ")); 87 | val message = String.format(ERROR_AMBIGUOUS_SETTER, fieldName, settersListString); 88 | return new MappingException(message); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/testutil/MockingTools.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client.testutil; 16 | 17 | import lombok.Value; 18 | import lombok.experimental.UtilityClass; 19 | import lombok.val; 20 | import software.amazon.awssdk.services.rdsdata.RdsDataClient; 21 | import software.amazon.awssdk.services.rdsdata.model.BeginTransactionRequest; 22 | import software.amazon.awssdk.services.rdsdata.model.BeginTransactionResponse; 23 | import software.amazon.awssdk.services.rdsdata.model.ColumnMetadata; 24 | import software.amazon.awssdk.services.rdsdata.model.ExecuteStatementRequest; 25 | import software.amazon.awssdk.services.rdsdata.model.ExecuteStatementResponse; 26 | import software.amazon.awssdk.services.rdsdata.model.Field; 27 | 28 | import java.util.Collection; 29 | import java.util.List; 30 | import java.util.stream.Stream; 31 | 32 | import static java.util.Arrays.asList; 33 | import static java.util.Collections.emptyList; 34 | import static java.util.stream.Collectors.toList; 35 | import static org.mockito.ArgumentMatchers.any; 36 | import static org.mockito.Mockito.when; 37 | 38 | @UtilityClass 39 | public class MockingTools { 40 | public static void mockReturnValue(RdsDataClient mockClient, 41 | long numberOfRecordsUpdated, 42 | ColumnDefinition... columns) { 43 | mockReturnValues(mockClient, numberOfRecordsUpdated, asList(columns)); 44 | } 45 | 46 | @SafeVarargs 47 | public static void mockReturnValues(RdsDataClient mockClient, 48 | long numberOfRecordsUpdated, 49 | List... rows) { 50 | List metadataList = rows.length > 0 ? buildColumnMetadataList(rows[0]) : emptyList(); 51 | 52 | val recordsList = Stream.of(rows) 53 | .map(row -> row.stream() 54 | .map(c -> c.field) 55 | .collect(toList())) 56 | .collect(toList()); 57 | 58 | when(mockClient.executeStatement(any(ExecuteStatementRequest.class))) 59 | .thenReturn(ExecuteStatementResponse.builder() 60 | .columnMetadata(metadataList) 61 | .records(recordsList) 62 | .numberOfRecordsUpdated(numberOfRecordsUpdated) 63 | .build()); 64 | } 65 | 66 | private List buildColumnMetadataList(List columns) { 67 | return columns.stream() 68 | .map(ColumnDefinition::getMetadata) 69 | .collect(toList()); 70 | } 71 | 72 | public static ColumnDefinition mockColumn(String name, Field field) { 73 | val metadata = ColumnMetadata.builder() 74 | .name(name) 75 | .build(); 76 | return new ColumnDefinition(metadata, field); 77 | } 78 | 79 | public static ColumnDefinition mockColumn(String name, String label, Field field) { 80 | val metadata = ColumnMetadata.builder() 81 | .name(name) 82 | .label(label) 83 | .build(); 84 | return new ColumnDefinition(metadata, field); 85 | } 86 | 87 | public static void returnNullMetadataAndResultSet(RdsDataClient mockClient) { 88 | when(mockClient.executeStatement(any(ExecuteStatementRequest.class))) 89 | .thenReturn(ExecuteStatementResponse.builder() 90 | .columnMetadata((Collection) null) 91 | .records((Collection) null) 92 | .numberOfRecordsUpdated(null) 93 | .build()); 94 | } 95 | 96 | @Value 97 | public static class ColumnDefinition { 98 | private ColumnMetadata metadata; 99 | private Field field; 100 | } 101 | 102 | public static void mockBeginTransaction(RdsDataClient mockClient, String transactionId) { 103 | when(mockClient.beginTransaction(any(BeginTransactionRequest.class))) 104 | .thenReturn(BeginTransactionResponse.builder() 105 | .transactionId(transactionId) 106 | .build()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/ExecutionResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.AllArgsConstructor; 18 | import software.amazon.awssdk.services.rdsdata.model.ColumnMetadata; 19 | import software.amazon.awssdk.services.rdsdata.model.Field; 20 | 21 | import java.util.List; 22 | 23 | import static java.util.Collections.emptyList; 24 | import static java.util.stream.Collectors.toList; 25 | 26 | public class ExecutionResult { 27 | private final List fieldNames; 28 | private final List rows; 29 | private final Long numberOfRecordsUpdated; 30 | private final MappingOptions mappingOptions; 31 | 32 | ExecutionResult(List metadata, 33 | List> fields, 34 | Long numberOfRecordsUpdated, 35 | MappingOptions mappingOptions) { 36 | this.rows = convertToRows(fields); 37 | this.numberOfRecordsUpdated = numberOfRecordsUpdated; 38 | this.mappingOptions = mappingOptions; 39 | 40 | this.fieldNames = extractFieldNames(metadata); 41 | } 42 | 43 | private List extractFieldNames(List metadata) { 44 | if (metadata == null) { 45 | return emptyList(); 46 | } 47 | 48 | return metadata.stream() 49 | .map(this::getFieldName) 50 | .collect(toList()); 51 | } 52 | 53 | private String getFieldName(ColumnMetadata columnMetadata) { 54 | if (mappingOptions.useLabelForMapping) 55 | return columnMetadata.label(); 56 | return columnMetadata.name(); 57 | } 58 | 59 | private List convertToRows(List> records) { 60 | if (records == null) { 61 | return emptyList(); 62 | } 63 | 64 | return records.stream() 65 | .map(Row::new) 66 | .collect(toList()); 67 | } 68 | 69 | /** 70 | * Will return the number of records inserted/updated by the query. 71 | * 72 | * @return the number of records updated. 73 | */ 74 | public Long getNumberOfRecordsUpdated() { 75 | return numberOfRecordsUpdated; 76 | } 77 | 78 | /** 79 | * Maps the first row from the result set retrieved from RDS Data API to the instance of the specified class. 80 | * If the result set is empty, a {@link MappingException} is thrown. 81 | * @param mapperClass class to map to 82 | * @return an instance of the specified class with the mapped data 83 | * @throws MappingException if failed to map RDS Data API results to the specified class 84 | */ 85 | public T mapToSingle(Class mapperClass) { 86 | if (rows.isEmpty()) { 87 | throw MappingException.emptyResultSet(); 88 | } 89 | 90 | return mapToSingle(mapperClass, fieldNames, rows.get(0)); 91 | } 92 | 93 | private T mapToSingle(Class mapperClass, List fieldNames, Row row) { 94 | // TODO: check that columnMetadata array has the same length as fields 95 | 96 | // TODO: this can be cached 97 | ObjectWriter writer = ConstructorObjectWriter.create(mapperClass, fieldNames) 98 | .orElseGet(() -> PropertyObjectWriter.create(mapperClass, fieldNames, mappingOptions)); 99 | return writer.write(row); 100 | } 101 | 102 | /** 103 | * Maps the result set retrieved from RDS Data API to the list of instances of the specified class. 104 | * @param mapperClass class to map to 105 | * @return a {@link List} of instances of the specified class with the mapped data 106 | * @throws MappingException if failed to map RDS Data API results to the specified class 107 | */ 108 | public List mapToList(Class mapperClass) { 109 | return rows.stream() 110 | .map(row -> mapToSingle(mapperClass, fieldNames, row)) 111 | .collect(toList()); 112 | } 113 | 114 | /** 115 | * Returns the single value from the first row and the first column from the result set, converting it to the type {@link T} 116 | * @param convertToType type to convert to 117 | * @return a value of a type {@link T} from the first row and the first column from the result set 118 | * @throws EmptyResultSetException if the result set is empty 119 | */ 120 | public T singleValue(Class convertToType) { 121 | if (rows.size() == 0 || rows.get(0).columnCount() == 0) 122 | throw new EmptyResultSetException(); 123 | 124 | return rows.get(0).getValue(0, convertToType); 125 | } 126 | 127 | @AllArgsConstructor 128 | static class Row { 129 | private List fields; 130 | 131 | public T getValue(int index, Class type) { 132 | return (T) TypeConverter.fromField(fields.get(index), type); 133 | } 134 | 135 | public int columnCount() { 136 | return fields.size(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/InputTypesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.TestBase; 18 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 19 | import lombok.val; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.params.ParameterizedTest; 23 | import org.junit.jupiter.params.provider.Arguments; 24 | import org.junit.jupiter.params.provider.MethodSource; 25 | import software.amazon.awssdk.services.rdsdata.model.Field; 26 | 27 | import java.math.BigDecimal; 28 | import java.math.BigInteger; 29 | import java.time.LocalDateTime; 30 | import java.util.UUID; 31 | import java.util.stream.Stream; 32 | 33 | import static com.amazon.rdsdata.client.TypeConverter.DATE_FORMATTER; 34 | import static com.amazon.rdsdata.client.TypeConverter.DATE_TIME_FORMATTER; 35 | import static com.amazon.rdsdata.client.TypeConverter.ERROR_PARAMETER_OF_UNKNOWN_TYPE; 36 | import static com.amazon.rdsdata.client.TypeConverter.TIME_FORMATTER; 37 | import static org.assertj.core.api.Assertions.assertThat; 38 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 39 | import static org.junit.jupiter.params.provider.Arguments.arguments; 40 | 41 | public class InputTypesTest extends TestBase { 42 | @BeforeEach 43 | public void beforeEach() { 44 | mockReturnValue(); // return empty response by default 45 | } 46 | 47 | @ParameterizedTest 48 | @MethodSource("typesThatDontNeedHints") 49 | void shouldSupportDifferentTypesOfParameters(Object parameter, Field expectedValue) { 50 | client.forSql("INSERT INTO tbl1(a) VALUES(?)", parameter) 51 | .execute(); 52 | 53 | val request = captureRequest(); 54 | assertThat(request.parameters()).containsExactly(SdkConstructs.parameter("1", expectedValue)); 55 | } 56 | 57 | @SuppressWarnings("RedundantCast") 58 | private static Stream typesThatDontNeedHints() { 59 | val bytes = new byte[] {1, 2, 3}; 60 | return Stream.of( 61 | arguments((byte) 11, SdkConstructs.longField(11L)), 62 | arguments((int) 12, SdkConstructs.longField(12L)), 63 | arguments((long) 13, SdkConstructs.longField(13L)), 64 | arguments((char) 14, SdkConstructs.longField(14L)), 65 | arguments(1.5f, SdkConstructs.doubleField(1.5d)), 66 | arguments(2.5d, SdkConstructs.doubleField(2.5d)), 67 | arguments("hello", SdkConstructs.stringField("hello")), 68 | arguments(bytes, SdkConstructs.blobField(bytes)), 69 | arguments(true, SdkConstructs.booleanField(true)), 70 | arguments(EnumType.VALUE_1, SdkConstructs.stringField("VALUE_1")) 71 | ); 72 | } 73 | 74 | @ParameterizedTest 75 | @MethodSource("typesThatNeedHints") 76 | void shouldSupportDifferentTypesOfParametersUsingHunts(Object parameter, Field expectedValue, String hint) { 77 | client.forSql("INSERT INTO tbl1(a) VALUES(?)", parameter) 78 | .execute(); 79 | 80 | val request = captureRequest(); 81 | assertThat(request.parameters()).containsExactly(SdkConstructs.parameter("1", expectedValue, hint)); 82 | } 83 | 84 | private static Stream typesThatNeedHints() { 85 | val now = LocalDateTime.now(); 86 | val uuid = UUID.randomUUID(); 87 | return Stream.of( 88 | arguments(BigDecimal.valueOf(1.5), SdkConstructs.stringField("1.5"), "DECIMAL"), 89 | arguments(BigInteger.valueOf(15), SdkConstructs.stringField("15"), "DECIMAL"), 90 | arguments(now, SdkConstructs.stringField(DATE_TIME_FORMATTER.format(now)), "TIMESTAMP"), 91 | arguments(now.toLocalDate(), SdkConstructs.stringField(DATE_FORMATTER.format(now.toLocalDate())), "DATE"), 92 | arguments(now.toLocalTime(), SdkConstructs.stringField(TIME_FORMATTER.format(now.toLocalTime())), "TIME"), 93 | arguments(uuid, SdkConstructs.stringField(uuid.toString()), "UUID") 94 | 95 | ); 96 | } 97 | 98 | @Test 99 | void shouldSupportNullParameter() { 100 | client.forSql("INSERT INTO tbl1(a) VALUES(?)", (Object) null) 101 | .execute(); 102 | 103 | val request = captureRequest(); 104 | assertThat(request.parameters()).containsExactly(SdkConstructs.parameter("1", SdkConstructs.nullField())); 105 | } 106 | 107 | @Test 108 | void shouldSupportNullVarargParameter() { 109 | client.forSql("INSERT INTO tbl1(a) VALUES(?)", (Object[]) null) 110 | .execute(); 111 | 112 | val request = captureRequest(); 113 | assertThat(request.parameters()).containsExactly(SdkConstructs.parameter("1", SdkConstructs.nullField())); 114 | } 115 | 116 | @Test 117 | void shouldThrowExceptionIfParameterIsOfUnknownType() { 118 | assertThatThrownBy(() -> client.forSql("INSERT INTO tbl1(a) VALUES(?)", new Object()).execute()) 119 | .isInstanceOf(IllegalArgumentException.class) 120 | .hasMessageStartingWith(ERROR_PARAMETER_OF_UNKNOWN_TYPE); 121 | } 122 | 123 | private enum EnumType { 124 | VALUE_1 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/Executor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.RequiredArgsConstructor; 18 | import lombok.val; 19 | 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | import static java.util.Arrays.asList; 25 | import static java.util.Collections.emptyList; 26 | import static java.util.Collections.emptyMap; 27 | import static java.util.Collections.singletonList; 28 | import static java.util.stream.Collectors.toList; 29 | 30 | @RequiredArgsConstructor 31 | public class Executor { 32 | private final String sql; 33 | private final RdsData rdsData; 34 | private List paramSets = emptyList(); 35 | private String transactionId = ""; // RDS Data API understands empty string as "no transaction" 36 | private boolean continueAfterTimeout = false; 37 | 38 | /** 39 | * Sets a single parameter set 40 | * @param param object which fields will be used as a source for parameters 41 | * @return a reference to this object so that method calls can be chained together 42 | */ 43 | public Executor withParameter(Object param) { 44 | this.paramSets = singletonList(param); 45 | return this; 46 | } 47 | 48 | /** 49 | * Sets multiple parameter sets 50 | * @param params {@link List} of objects which fields will be used as sources for parameters 51 | * @return a reference to this object so that method calls can be chained together 52 | */ 53 | public Executor withParamSets(List params) { 54 | this.paramSets = params; 55 | return this; 56 | } 57 | 58 | /** 59 | * Sets multiple parameter sets 60 | * @param params vararg array of objects which fields will be sources for parameters 61 | * @return a reference to this object so that method calls can be chained together 62 | */ 63 | public Executor withParamSets(Object... params) { 64 | return withParamSets(asList(params)); 65 | } 66 | 67 | /** 68 | * Sets a single named parameter. 69 | * Should not be combined with {@link #withParameter(Object)} and {@link #withParamSets(Object...)} 70 | * @param parameterName Name of the parameter 71 | * @param value value (can be of any supported type) 72 | * @return a reference to this object so that method calls can be chained together 73 | */ 74 | public Executor withParameter(String parameterName, Object value) { 75 | if (paramSets.isEmpty()) { 76 | paramSets = singletonList(new HashMap()); 77 | } 78 | 79 | val firstParamSet = paramSets.get(0); 80 | if (!(firstParamSet instanceof Map)) { 81 | throw new IllegalArgumentException("Parameters are already supplied"); 82 | } 83 | 84 | //noinspection unchecked 85 | ((Map) paramSets.get(0)).put(parameterName, value); 86 | return this; 87 | } 88 | 89 | /** 90 | * Executes the SQL query. 91 | * 92 | * If only one parameter set was added to this {@link Executor} before, or no parameters at all, 93 | * ExecuteStatement API will be called 94 | * 95 | * If more than one parameter set was added (via withParamSets() methods), 96 | * BatchExecuteStatement API will be used 97 | * 98 | * @return a {@link ExecutionResult} instance 99 | */ 100 | public ExecutionResult execute() { 101 | return paramSets.size() > 1 ? executeAsBatch() : executeAsSingle(); 102 | } 103 | 104 | private ExecutionResult executeAsBatch() { 105 | val paramSetsAsMaps = paramSets.stream() 106 | .map(paramSet -> toMap(sql, paramSet)) 107 | .collect(toList()); 108 | return rdsData.batchExecuteStatement(transactionId, sql, paramSetsAsMaps); 109 | } 110 | 111 | private ExecutionResult executeAsSingle() { 112 | val firstParamSetAsMap = paramSets.stream() 113 | .findFirst() 114 | .map(paramSet -> toMap(sql, paramSet)) 115 | .orElse(emptyMap()); 116 | return rdsData.executeStatement(transactionId, sql, firstParamSetAsMap, continueAfterTimeout); 117 | } 118 | 119 | private Map toMap(String sql, Object paramSet) { 120 | if (paramSet instanceof Map) { 121 | // TODO: check that all keys are strings 122 | return (Map) paramSet; 123 | } 124 | 125 | val mapper = new ObjectMapper(sql); 126 | return mapper.map(paramSet); 127 | } 128 | 129 | /** 130 | * Specifies that the query should be executed in a transaction 131 | * @param transactionId transaction ID 132 | * @return a reference to this object so that method calls can be chained together 133 | */ 134 | public Executor withTransactionId(String transactionId) { 135 | this.transactionId = transactionId; 136 | return this; 137 | } 138 | 139 | /** 140 | * Specifies that the query should continue to be executed even after timeout 141 | * @return a reference to this object so that method calls can be chained 142 | */ 143 | public Executor withContinueAfterTimeout() { 144 | this.continueAfterTimeout = true; 145 | return this; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/MappingOutputViaFieldsTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import lombok.AccessLevel; 20 | import lombok.NoArgsConstructor; 21 | import lombok.val; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import static com.amazon.rdsdata.client.MappingException.ERROR_CANNOT_ACCESS_FIELD; 25 | import static com.amazon.rdsdata.client.MappingException.ERROR_CANNOT_CREATE_INSTANCE; 26 | import static com.amazon.rdsdata.client.MappingException.ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS; 27 | import static com.amazon.rdsdata.client.MappingException.ERROR_NO_FIELD_OR_SETTER; 28 | import static com.amazon.rdsdata.client.MappingException.ERROR_STATIC_FIELD; 29 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 30 | import static java.lang.String.format; 31 | import static org.assertj.core.api.Assertions.assertThat; 32 | import static org.assertj.core.api.Assertions.assertThatCode; 33 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 34 | 35 | public class MappingOutputViaFieldsTests extends TestBase { 36 | @Test 37 | void shouldMapToClassWithPublicFields() { 38 | mockReturnValue(mockColumn("value", SdkConstructs.stringField("apple"))); 39 | 40 | val result = client.forSql("SELECT *") 41 | .execute() 42 | .mapToSingle(PublicFields.class); 43 | assertThat(result.value).isEqualTo("apple"); 44 | } 45 | 46 | @NoArgsConstructor 47 | private static class PublicFields { 48 | public String value; 49 | } 50 | 51 | @Test 52 | void shouldThrowExceptionIfFieldDoesNotExist() { 53 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 54 | 55 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(Object.class)) 56 | .isInstanceOf(MappingException.class) 57 | .hasMessage(ERROR_NO_FIELD_OR_SETTER, Object.class.getName(), "field"); 58 | } 59 | 60 | @Test 61 | void shouldThrowExceptionIfFieldIsPrivate() { 62 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 63 | 64 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(PrivateField.class)) 65 | .isInstanceOf(MappingException.class) 66 | .hasMessage(ERROR_CANNOT_ACCESS_FIELD, "field", PrivateField.class.getName()); 67 | } 68 | 69 | @SuppressWarnings("unused") 70 | public static class PrivateField { 71 | private String field; 72 | } 73 | 74 | @Test 75 | void shouldThrowExceptionIfFieldIsFinal() { 76 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 77 | 78 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(FinalField.class)) 79 | .isInstanceOf(MappingException.class) 80 | .hasMessage(ERROR_CANNOT_ACCESS_FIELD, "field", FinalField.class.getName()); 81 | } 82 | 83 | public static class FinalField { 84 | public final String field = "grape"; 85 | } 86 | 87 | @Test 88 | void shouldThrowExceptionIfConstructorIsNotAccessible() { 89 | mockReturnValue(); 90 | 91 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(PrivateConstructor.class)) 92 | .isInstanceOf(MappingException.class) 93 | .hasMessage(ERROR_CANNOT_CREATE_INSTANCE, PrivateConstructor.class.getName()); 94 | } 95 | 96 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 97 | public static class PrivateConstructor { 98 | } 99 | 100 | @Test 101 | void shouldThrowExceptionIfFieldIsStatic() { 102 | mockReturnValue(mockColumn("field", SdkConstructs.longField(1111L))); 103 | 104 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(StaticField.class)) 105 | .isInstanceOf(MappingException.class) 106 | .hasMessageStartingWith(format(ERROR_STATIC_FIELD, "field", StaticField.class.getName())); 107 | } 108 | 109 | @SuppressWarnings("unused") 110 | public static class StaticField { 111 | public static int field; 112 | } 113 | 114 | @Test 115 | void shouldUseFieldIfSetterIsNotAvailable() { 116 | mockReturnValue(mockColumn("field", SdkConstructs.longField(333L))); 117 | 118 | val dto = client.forSql("SELECT *").execute().mapToSingle(SetterAndField.class); 119 | assertThat(dto.field).isEqualTo(333L); 120 | } 121 | 122 | public static class SetterAndField { 123 | public int field; 124 | private void setField(int value) { this.field = -1; } 125 | } 126 | 127 | @Test 128 | void shouldThrowExceptionIfNoArgsConstructorNotFound() { 129 | mockReturnValue(mockColumn("field", SdkConstructs.longField(333L))); 130 | 131 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(WithoutNoArgConstructor.class)) 132 | .isInstanceOf(MappingException.class) 133 | .hasMessage(ERROR_CANNOT_CREATE_INSTANCE_VIA_NOARGS, WithoutNoArgConstructor.class.getName()); 134 | } 135 | 136 | @SuppressWarnings("unused") 137 | public static class WithoutNoArgConstructor { 138 | public int field; 139 | public WithoutNoArgConstructor(int x) { } 140 | } 141 | 142 | @Test 143 | void shouldAccessFieldsFromParentClass() { 144 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 145 | 146 | assertThatCode(() -> client.forSql("SELECT *").execute().mapToSingle(ChildWithoutFields.class)) 147 | .doesNotThrowAnyException(); 148 | } 149 | 150 | public static class ChildWithoutFields extends ParentWithField { 151 | } 152 | 153 | public static class ParentWithField { 154 | public String field; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/MappingOutputViaSettersTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import lombok.NoArgsConstructor; 20 | import lombok.val; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static com.amazon.rdsdata.client.MappingException.ERROR_NO_FIELD_OR_SETTER; 24 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | import static org.assertj.core.api.Assertions.assertThatCode; 27 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 28 | 29 | public class MappingOutputViaSettersTests extends TestBase { 30 | @Test 31 | void shouldMapToFieldsOfDifferentType() { 32 | mockReturnValue(mockColumn("stringValue", SdkConstructs.stringField("apple"))); 33 | 34 | val result = client.forSql("SELECT *") 35 | .execute() 36 | .mapToSingle(Setter.class); 37 | assertThat(result.value).isEqualTo("apple"); 38 | } 39 | 40 | @NoArgsConstructor 41 | @SuppressWarnings("unused") 42 | private static class Setter { 43 | public String value; 44 | public void setStringValue(String value) { this.value = value; } 45 | } 46 | 47 | @Test 48 | void shouldNotUsePrivateSetter() { 49 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 50 | 51 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(PrivateSetter.class)) 52 | .isInstanceOf(MappingException.class) 53 | .hasMessage(ERROR_NO_FIELD_OR_SETTER, PrivateSetter.class.getName(), "field"); 54 | } 55 | 56 | @SuppressWarnings("unused") 57 | public static class PrivateSetter { 58 | private void setField(String value) { } 59 | } 60 | 61 | @Test 62 | void shouldNotUseSetterWithMoreThanOneParameter() { 63 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 64 | 65 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(SetterWithTwoParameters.class)) 66 | .isInstanceOf(MappingException.class) 67 | .hasMessage(ERROR_NO_FIELD_OR_SETTER, SetterWithTwoParameters.class.getName(), "field"); 68 | } 69 | 70 | @SuppressWarnings("unused") 71 | public static class SetterWithTwoParameters { 72 | public void setField(String value, int secondParameter) { } 73 | } 74 | 75 | @Test 76 | void shouldNotUseSetterWithNoParameters() { 77 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 78 | 79 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(SetterWithNoParameters.class)) 80 | .isInstanceOf(MappingException.class) 81 | .hasMessage(ERROR_NO_FIELD_OR_SETTER, SetterWithNoParameters.class.getName(), "field"); 82 | } 83 | 84 | @SuppressWarnings("unused") 85 | public static class SetterWithNoParameters { 86 | public void setField() { } 87 | } 88 | 89 | @Test 90 | void shouldNotUseStaticSetter() { 91 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 92 | 93 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(StaticSetter.class)) 94 | .isInstanceOf(MappingException.class) 95 | .hasMessage(ERROR_NO_FIELD_OR_SETTER, StaticSetter.class.getName(), "field"); 96 | } 97 | 98 | @SuppressWarnings("unused") 99 | public static class StaticSetter { 100 | public static void setField(String value) { } 101 | } 102 | 103 | @Test 104 | void shouldThrowExceptionIfSetterIsNotFound() { 105 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 106 | 107 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(NoCorrespondingSetter.class)) 108 | .isInstanceOf(MappingException.class) 109 | .hasMessage(ERROR_NO_FIELD_OR_SETTER, NoCorrespondingSetter.class.getName(), "field"); 110 | } 111 | 112 | @Test 113 | void shouldNotThrowExceptionIfMissingSetterIsIgnored() { 114 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 115 | 116 | val mappingOptions = MappingOptions.builder() 117 | .ignoreMissingSetters(true) 118 | .build(); 119 | 120 | assertThatCode(() -> client.withMappingOptions(mappingOptions).forSql("SELECT *").execute().mapToSingle(NoCorrespondingSetter.class)) 121 | .doesNotThrowAnyException(); 122 | } 123 | 124 | @SuppressWarnings("unused") 125 | public static class NoCorrespondingSetter { 126 | public void setAnotherField(String value) { } 127 | } 128 | 129 | @Test 130 | void shouldPreferSetterToField() { 131 | mockReturnValue(mockColumn("field", SdkConstructs.stringField("apple"))); 132 | 133 | val dto = client.forSql("SELECT *").execute().mapToSingle(SetterAndField.class); 134 | assertThat(dto.field).isEqualTo("orange"); 135 | } 136 | 137 | @SuppressWarnings("unused") 138 | public static class SetterAndField { 139 | public String field; 140 | public void setField(String value) { this.field = "orange"; } 141 | } 142 | 143 | @Test 144 | void shouldThrowExceptionIfMoreThanOneSetter() { 145 | mockReturnValue(mockColumn("field", SdkConstructs.longField(123L))); 146 | 147 | assertThatThrownBy(() -> client.forSql("SELECT *").execute().mapToSingle(AmbiguousSetter.class)) 148 | .isInstanceOf(MappingException.class) 149 | .hasMessageContaining("Ambiguous setter"); 150 | } 151 | 152 | @SuppressWarnings("unused") 153 | public static class AmbiguousSetter { 154 | public void setField(Integer value) { } 155 | public void setField(Long value) { } 156 | } 157 | 158 | @Test 159 | void shouldUseSettersFromParentClass() { 160 | mockReturnValue(mockColumn("stringValue", SdkConstructs.stringField("apple"))); 161 | 162 | val result = client.forSql("SELECT *") 163 | .execute() 164 | .mapToSingle(ChildClass.class); 165 | assertThat(result.value).isEqualTo("apple"); 166 | } 167 | 168 | @NoArgsConstructor 169 | private static class ChildClass extends ParentClassWithSetter { 170 | } 171 | 172 | @NoArgsConstructor 173 | private static class ParentClassWithSetter { 174 | public String value; 175 | 176 | @SuppressWarnings("unused") 177 | public void setStringValue(String value) { this.value = value; } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/RdsData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import lombok.Builder; 18 | import lombok.With; 19 | import lombok.val; 20 | import software.amazon.awssdk.services.rdsdata.RdsDataClient; 21 | import software.amazon.awssdk.services.rdsdata.model.BatchExecuteStatementRequest; 22 | import software.amazon.awssdk.services.rdsdata.model.BeginTransactionRequest; 23 | import software.amazon.awssdk.services.rdsdata.model.CommitTransactionRequest; 24 | import software.amazon.awssdk.services.rdsdata.model.DecimalReturnType; 25 | import software.amazon.awssdk.services.rdsdata.model.ExecuteStatementRequest; 26 | import software.amazon.awssdk.services.rdsdata.model.ResultSetOptions; 27 | import software.amazon.awssdk.services.rdsdata.model.RollbackTransactionRequest; 28 | import software.amazon.awssdk.services.rdsdata.model.SqlParameter; 29 | 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.stream.Collectors; 33 | 34 | import static com.amazon.rdsdata.client.MappingOptions.DEFAULT; 35 | import static com.google.common.base.Preconditions.checkArgument; 36 | import static com.google.common.base.Strings.isNullOrEmpty; 37 | import static java.util.Collections.emptyList; 38 | import static java.util.Collections.singletonList; 39 | import static java.util.stream.Collectors.toList; 40 | 41 | @Builder 42 | public class RdsData { 43 | static String ERROR_EMPTY_OR_NULL_SQL = "SQL parameter is null or empty"; 44 | 45 | private RdsDataClient sdkClient; 46 | @With private String database; 47 | private String secretArn; 48 | private String resourceArn; 49 | 50 | @Builder.Default 51 | @With private MappingOptions mappingOptions = DEFAULT; 52 | 53 | /** 54 | * Starts a new transaction 55 | * @return transaction ID 56 | */ 57 | public String beginTransaction() { 58 | val request = BeginTransactionRequest.builder() 59 | .database(database) 60 | .resourceArn(resourceArn) 61 | .secretArn(secretArn) 62 | .build(); 63 | val response = sdkClient.beginTransaction(request); 64 | return response.transactionId(); 65 | } 66 | 67 | /** 68 | * Commits the given transaction 69 | * @param transactionId transaction ID 70 | */ 71 | public void commitTransaction(String transactionId) { 72 | val request = CommitTransactionRequest.builder() 73 | .transactionId(transactionId) 74 | .resourceArn(resourceArn) 75 | .secretArn(secretArn) 76 | .build(); 77 | sdkClient.commitTransaction(request); 78 | } 79 | 80 | /** 81 | * Rolls back the given transaction 82 | * @param transactionId transaction ID 83 | */ 84 | public void rollbackTransaction(String transactionId) { 85 | val request = RollbackTransactionRequest.builder() 86 | .transactionId(transactionId) 87 | .resourceArn(resourceArn) 88 | .secretArn(secretArn) 89 | .build(); 90 | sdkClient.rollbackTransaction(request); 91 | } 92 | 93 | /** 94 | * Creates an {@link Executor} for the given SQL 95 | * @param sql SQL statement 96 | * @return an {@link Executor} instance 97 | * @see Executor 98 | */ 99 | public Executor forSql(String sql) { 100 | checkArgument(!isNullOrEmpty(sql), ERROR_EMPTY_OR_NULL_SQL); 101 | 102 | return new Executor(sql, this); 103 | } 104 | 105 | /** 106 | * Creates an {@link Executor} for the given SQL with parameters. For each parameter, the SQL statement must 107 | * contain a placeholder "?" 108 | * @param sql SQL statement with placeholders 109 | * @param params vararg array with parameters 110 | * @return an {@link Executor} instance 111 | * @see Executor 112 | */ 113 | public Executor forSql(String sql, Object... params) { 114 | checkArgument(!isNullOrEmpty(sql), ERROR_EMPTY_OR_NULL_SQL); 115 | if (params == null) { 116 | // for case when forSql() is called with one null parameter and Java handled it as the entire vararg is null 117 | params = new Object[] { null }; 118 | } 119 | 120 | val result = PlaceholderUtils.convertToNamed(sql, params); 121 | return new Executor(result.sql, this) 122 | .withParamSets(singletonList(result.parameters)); 123 | } 124 | 125 | ExecutionResult executeStatement(String transactionId, String sql, Map params, boolean continueAfterTimeout) { 126 | val request = ExecuteStatementRequest.builder() 127 | .database(database) 128 | .resourceArn(resourceArn) 129 | .secretArn(secretArn) 130 | .sql(sql) 131 | .parameters(toSqlParameterList(params)) 132 | .transactionId(transactionId) 133 | .continueAfterTimeout(continueAfterTimeout) 134 | .resultSetOptions(ResultSetOptions.builder() 135 | .decimalReturnType(DecimalReturnType.STRING) 136 | .build()) 137 | .includeResultMetadata(true) 138 | .build(); 139 | 140 | val response = sdkClient.executeStatement(request); 141 | 142 | return new ExecutionResult(response.columnMetadata(), 143 | response.records(), 144 | response.numberOfRecordsUpdated(), 145 | mappingOptions); 146 | } 147 | 148 | ExecutionResult batchExecuteStatement(String transactionId, String sql, List> params) { 149 | val request = BatchExecuteStatementRequest.builder() 150 | .database(database) 151 | .resourceArn(resourceArn) 152 | .secretArn(secretArn) 153 | .sql(sql) 154 | .transactionId(transactionId) 155 | .parameterSets(toSqlParameterSets(params)) 156 | .build(); 157 | sdkClient.batchExecuteStatement(request); 158 | return new ExecutionResult(emptyList(), emptyList(), 0L, mappingOptions); 159 | } 160 | 161 | private List toSqlParameterList(Map params) { 162 | return params.entrySet().stream() 163 | .map(this::toSqlParameter) 164 | .collect(toList()); 165 | } 166 | 167 | private SqlParameter toSqlParameter(Map.Entry mapEntry) { 168 | val parameterName = mapEntry.getKey(); 169 | val value = mapEntry.getValue(); 170 | 171 | val parameterBuilder = SqlParameter.builder() 172 | .name(parameterName) 173 | .value(TypeConverter.toField(value)); 174 | 175 | TypeConverter.getTypeHint(value) 176 | .ifPresent(hint -> parameterBuilder.typeHint(hint.name())); 177 | 178 | return parameterBuilder.build(); 179 | } 180 | 181 | private List> toSqlParameterSets(List> params) { 182 | return params.stream() 183 | .map(this::toSqlParameterList) 184 | .collect(Collectors.toList()); 185 | } 186 | } -------------------------------------------------------------------------------- /src/main/java/com/amazon/rdsdata/client/TypeConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import software.amazon.awssdk.core.SdkBytes; 18 | import software.amazon.awssdk.services.rdsdata.model.Field; 19 | import software.amazon.awssdk.services.rdsdata.model.TypeHint; 20 | 21 | import java.math.BigDecimal; 22 | import java.math.BigInteger; 23 | import java.time.LocalDate; 24 | import java.time.LocalDateTime; 25 | import java.time.LocalTime; 26 | import java.time.format.DateTimeFormatter; 27 | import java.time.format.DateTimeParseException; 28 | import java.util.Optional; 29 | import java.util.UUID; 30 | 31 | import static software.amazon.awssdk.services.rdsdata.model.TypeHint.DATE; 32 | import static software.amazon.awssdk.services.rdsdata.model.TypeHint.DECIMAL; 33 | import static software.amazon.awssdk.services.rdsdata.model.TypeHint.TIME; 34 | import static software.amazon.awssdk.services.rdsdata.model.TypeHint.TIMESTAMP; 35 | 36 | class TypeConverter { 37 | static String ERROR_PARAMETER_OF_UNKNOWN_TYPE = "Unknown parameter type: "; 38 | 39 | static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]"); 40 | static DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss[.SSS]"); 41 | static DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 42 | 43 | static Field toField(Object o) { 44 | if (o == null || o == FieldMapper.NULL) { 45 | return Field.builder().isNull(true).build(); 46 | } else if (o instanceof Byte || o instanceof Integer || o instanceof Long) { 47 | return Field.builder().longValue(((Number) o).longValue()).build(); 48 | } else if (o instanceof Double || o instanceof Float) { 49 | return Field.builder().doubleValue(((Number) o).doubleValue()).build(); 50 | } else if (o instanceof Character) { 51 | return Field.builder().longValue((long) (Character) o).build(); 52 | } else if (o instanceof String) { 53 | return Field.builder().stringValue(o.toString()).build(); 54 | } else if (o instanceof Boolean) { 55 | return Field.builder().booleanValue((Boolean) o).build(); 56 | } else if (o instanceof byte[]) { 57 | return Field.builder().blobValue(SdkBytes.fromByteArray((byte[]) o)).build(); 58 | } else if (o instanceof BigDecimal || o instanceof BigInteger) { 59 | return Field.builder().stringValue(o.toString()).build(); 60 | } else if (o instanceof LocalDateTime) { 61 | return Field.builder().stringValue(DATE_TIME_FORMATTER.format((LocalDateTime) o)).build(); 62 | } else if (o instanceof LocalDate) { 63 | return Field.builder().stringValue(DATE_FORMATTER.format((LocalDate) o)).build(); 64 | } else if (o instanceof LocalTime) { 65 | return Field.builder().stringValue(TIME_FORMATTER.format((LocalTime) o)).build(); 66 | } else if (o instanceof Enum) { 67 | return Field.builder().stringValue(((Enum) o).name()).build(); 68 | } else if (o instanceof UUID) { 69 | return Field.builder().stringValue(o.toString()).build(); 70 | } 71 | 72 | throw new IllegalArgumentException(ERROR_PARAMETER_OF_UNKNOWN_TYPE + o.getClass().getName()); 73 | } 74 | 75 | static Optional getTypeHint(Object o) { 76 | if (o instanceof BigDecimal || o instanceof BigInteger) { 77 | return Optional.of(DECIMAL); 78 | } else if (o instanceof LocalDateTime) { 79 | return Optional.of(TIMESTAMP); 80 | } else if (o instanceof LocalDate) { 81 | return Optional.of(DATE); 82 | } else if (o instanceof LocalTime) { 83 | return Optional.of(TIME); 84 | } else if (o instanceof UUID) { 85 | return Optional.of(TypeHint.UUID); 86 | } 87 | 88 | return Optional.empty(); 89 | } 90 | 91 | @SuppressWarnings("unchecked") 92 | static Object fromField(Field field, Class type) { 93 | // TODO: Class comparison by == (or .equals) may not work if classes belong to different classloaders 94 | if (field.isNull() != null && field.isNull()) { 95 | return null; 96 | } if (type == String.class) { 97 | return field.stringValue(); 98 | } else if (type == Byte.class || type == byte.class) { 99 | return field.longValue().byteValue(); 100 | } else if (type == Integer.class || type == int.class) { 101 | return field.longValue().intValue(); 102 | } else if (type == Long.class || type == long.class) { 103 | return field.longValue(); 104 | } else if (type == Character.class || type == char.class) { 105 | return (char) field.longValue().longValue(); 106 | } else if (type == Double.class || type == double.class) { 107 | return field.doubleValue(); 108 | } else if (type == Float.class || type == float.class) { 109 | return field.doubleValue().floatValue(); 110 | } else if (type == byte[].class) { 111 | return field.blobValue().asByteArray(); 112 | } else if (type == Boolean.class || type == boolean.class) { 113 | return field.booleanValue(); 114 | } else if (type == BigDecimal.class) { 115 | return toBigDecimal(field); 116 | } else if (type == BigInteger.class) { 117 | return toBigInteger(field); 118 | } else if (Enum.class.isAssignableFrom(type)) { 119 | return Enum.valueOf((Class) type, field.stringValue()); 120 | } else if (type == UUID.class) { 121 | return java.util.UUID.fromString(field.stringValue()); 122 | } else if (type == LocalDateTime.class) { 123 | return LocalDateTime.from(DATE_TIME_FORMATTER.parse(field.stringValue())); 124 | } else if (type == LocalDate.class) { 125 | return dateFromString(field.stringValue()); 126 | } else if (type == LocalTime.class) { 127 | return timeFromString(field.stringValue()); 128 | } 129 | 130 | // TODO: handle this case 131 | return null; 132 | } 133 | 134 | private static LocalDate dateFromString(String dateString) { 135 | try { 136 | // date can be provided in format "yyyy-MM-dd HH:mm:ss[.SSS]" 137 | return LocalDate.from(DATE_TIME_FORMATTER.parse(dateString)); 138 | } catch (DateTimeParseException e) { 139 | // ... or as "yyyy-MM-dd" 140 | return LocalDate.from(DATE_FORMATTER.parse(dateString)); 141 | } 142 | } 143 | 144 | private static LocalTime timeFromString(String timeString) { 145 | try { 146 | // time can be provided in format "yyyy-MM-dd HH:mm:ss[.SSS]" 147 | return LocalTime.from(DATE_TIME_FORMATTER.parse(timeString)); 148 | } catch (DateTimeParseException e) { 149 | // ... or as "HH:mm:ss" 150 | return LocalTime.from(TIME_FORMATTER.parse(timeString)); 151 | } 152 | } 153 | 154 | private static BigDecimal toBigDecimal(Field field) { 155 | if (field.stringValue() != null) { 156 | return new BigDecimal(field.stringValue()); 157 | } else if (field.longValue() != null) { 158 | return BigDecimal.valueOf(field.longValue()); 159 | } else if (field.doubleValue() != null) { 160 | return BigDecimal.valueOf(field.doubleValue()); 161 | } 162 | 163 | throw MappingException.cannotConvertToType(field, BigDecimal.class); 164 | } 165 | 166 | private static BigInteger toBigInteger(Field field) { 167 | if (field.stringValue() != null) { 168 | return new BigInteger(field.stringValue()); 169 | } else if (field.longValue() != null) { 170 | return BigInteger.valueOf(field.longValue()); 171 | } 172 | 173 | throw MappingException.cannotConvertToType(field, BigInteger.class); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/OutputTypesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import lombok.NoArgsConstructor; 20 | import lombok.val; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import java.math.BigDecimal; 24 | import java.math.BigInteger; 25 | import java.nio.ByteBuffer; 26 | import java.time.LocalDate; 27 | import java.time.LocalDateTime; 28 | import java.time.LocalTime; 29 | import java.util.UUID; 30 | 31 | import static com.amazon.rdsdata.client.MappingException.ERROR_CANNOT_CONVERT_TO_TYPE; 32 | import static com.amazon.rdsdata.client.testutil.MockingTools.mockColumn; 33 | import static org.assertj.core.api.Assertions.assertThat; 34 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 35 | 36 | public class OutputTypesTest extends TestBase { 37 | @Test 38 | void shouldMapToFieldsOfDifferentType() { 39 | val bytes = new byte[] {1, 2, 3}; 40 | val uuid = UUID.randomUUID(); 41 | mockReturnValue( 42 | mockColumn("stringValue", SdkConstructs.stringField("apple")), 43 | mockColumn("byteValue", SdkConstructs.longField(3L)), 44 | mockColumn("intValue", SdkConstructs.longField(4L)), 45 | mockColumn("longValue", SdkConstructs.longField(5L)), 46 | mockColumn("charValue", SdkConstructs.longField(6L)), 47 | mockColumn("boxedLongValue", SdkConstructs.longField(7L)), 48 | mockColumn("doubleValue", SdkConstructs.doubleField(1.5d)), 49 | mockColumn("floatValue", SdkConstructs.doubleField(2.5d)), 50 | mockColumn("blob", SdkConstructs.blobField(bytes)), 51 | mockColumn("booleanValue", SdkConstructs.booleanField(true)), 52 | mockColumn("nullField", SdkConstructs.nullField()), 53 | mockColumn("enumType", SdkConstructs.stringField("VALUE_1")), 54 | mockColumn("uuid", SdkConstructs.stringField(uuid.toString())) 55 | ); 56 | 57 | val result = client.forSql("SELECT *") 58 | .execute() 59 | .mapToSingle(FieldsOfDifferentTypes.class); 60 | 61 | assertThat(result.stringValue).isEqualTo("apple"); 62 | assertThat(result.byteValue).isEqualTo((byte) 3); 63 | assertThat(result.intValue).isEqualTo(4); 64 | assertThat(result.longValue).isEqualTo(5L); 65 | assertThat(result.charValue).isEqualTo((char) 6); 66 | assertThat(result.boxedLongValue).isEqualTo(7L); 67 | assertThat(result.doubleValue).isEqualTo(1.5d); 68 | assertThat(result.floatValue).isEqualTo(2.5d); 69 | assertThat(result.blob).isEqualTo(bytes); 70 | assertThat(result.booleanValue).isEqualTo(true); 71 | assertThat(result.nullField).isNull(); 72 | assertThat(result.enumType).isEqualTo(EnumType.VALUE_1); 73 | assertThat(result.uuid).isEqualTo(uuid); 74 | } 75 | 76 | @NoArgsConstructor 77 | private static class FieldsOfDifferentTypes { 78 | public String stringValue; 79 | public byte byteValue; 80 | public int intValue; 81 | public long longValue; 82 | public char charValue; 83 | public Long boxedLongValue; 84 | public double doubleValue; 85 | public double floatValue; 86 | public byte[] blob; 87 | public boolean booleanValue; 88 | public String nullField; 89 | public EnumType enumType; 90 | public UUID uuid; 91 | } 92 | 93 | @Test 94 | void shouldMapToFieldsOfTemporalTypes() { 95 | mockReturnValue( 96 | mockColumn("localDateTime", SdkConstructs.stringField("2021-08-23 14:30:16.223")), 97 | mockColumn("localDateFromDateWithTime", SdkConstructs.stringField("2021-08-23 14:30:16.223")), 98 | mockColumn("localDateFromDate", SdkConstructs.stringField("2021-08-23")), 99 | mockColumn("localTimeFromDateWithTime", SdkConstructs.stringField("2021-08-23 14:30:16.223")), 100 | mockColumn("localTimeFromTime", SdkConstructs.stringField("14:30:16")) 101 | ); 102 | 103 | val result = client.forSql("SELECT *") 104 | .execute() 105 | .mapToSingle(TemporalTypes.class); 106 | 107 | assertThat(result.localDateTime).isEqualTo(LocalDateTime.of(2021, 8, 23, 14, 30, 16, 223_000_000)); 108 | assertThat(result.localDateFromDateWithTime).isEqualTo(LocalDate.of(2021, 8, 23)); 109 | assertThat(result.localDateFromDate).isEqualTo(LocalDate.of(2021, 8, 23)); 110 | assertThat(result.localTimeFromDateWithTime).isEqualTo(LocalTime.of(14, 30, 16, 223_000_000)); 111 | assertThat(result.localTimeFromTime).isEqualTo(LocalTime.of(14, 30, 16)); 112 | } 113 | 114 | @NoArgsConstructor 115 | private static class TemporalTypes { 116 | public LocalDateTime localDateTime; 117 | public LocalDate localDateFromDateWithTime; 118 | public LocalDate localDateFromDate; 119 | public LocalTime localTimeFromDateWithTime; 120 | public LocalTime localTimeFromTime; 121 | } 122 | 123 | @Test 124 | void shouldMapToFieldsOfDecimalTypes() { 125 | mockReturnValue( 126 | mockColumn("bigDecimalFromString", SdkConstructs.stringField("12.25")), 127 | mockColumn("bigDecimalFromLong", SdkConstructs.longField(12L)), 128 | mockColumn("bigDecimalFromDouble", SdkConstructs.doubleField(12.5)), 129 | mockColumn("bigIntegerFromString", SdkConstructs.stringField("333")), 130 | mockColumn("bigIntegerFromLong", SdkConstructs.longField(444L)) 131 | ); 132 | 133 | val result = client.forSql("SELECT *") 134 | .execute() 135 | .mapToSingle(DecimalTypes.class); 136 | 137 | assertThat(result.bigDecimalFromString).isEqualTo(BigDecimal.valueOf(1225, 2)); 138 | assertThat(result.bigDecimalFromLong).isEqualTo(BigDecimal.valueOf(12)); 139 | assertThat(result.bigDecimalFromDouble).isEqualTo(BigDecimal.valueOf(12.5)); 140 | assertThat(result.bigIntegerFromString).isEqualTo(BigInteger.valueOf(333)); 141 | assertThat(result.bigIntegerFromLong).isEqualTo(BigInteger.valueOf(444)); 142 | } 143 | 144 | @NoArgsConstructor 145 | private static class DecimalTypes { 146 | public BigDecimal bigDecimalFromString; 147 | public BigDecimal bigDecimalFromLong; 148 | public BigDecimal bigDecimalFromDouble; 149 | public BigInteger bigIntegerFromString; 150 | public BigInteger bigIntegerFromLong; 151 | } 152 | 153 | @Test 154 | void shouldThrowExceptionIfFieldCannotBeConvertedToBigDecimal() { 155 | val field = SdkConstructs.booleanField(true); 156 | mockReturnValue(mockColumn("bigDecimal", field)); 157 | 158 | assertThatThrownBy(() -> client.forSql("SELECT *") 159 | .execute() 160 | .mapToSingle(DtoWithDecimalAndInteger.class)) 161 | .isInstanceOf(MappingException.class) 162 | .hasMessage(ERROR_CANNOT_CONVERT_TO_TYPE, field, BigDecimal.class.toString()); 163 | } 164 | 165 | @NoArgsConstructor 166 | @SuppressWarnings("unused") 167 | private static class DtoWithDecimalAndInteger { 168 | public BigDecimal bigDecimal; 169 | public BigInteger bigInteger; 170 | } 171 | 172 | @Test 173 | void shouldThrowExceptionIfFieldCannotBeConvertedToBigInteger() { 174 | val field = SdkConstructs.booleanField(true); 175 | mockReturnValue(mockColumn("bigInteger", field)); 176 | 177 | assertThatThrownBy(() -> client.forSql("SELECT *") 178 | .execute() 179 | .mapToSingle(DtoWithDecimalAndInteger.class)) 180 | .isInstanceOf(MappingException.class) 181 | .hasMessage(ERROR_CANNOT_CONVERT_TO_TYPE, field, BigInteger.class.toString()); 182 | } 183 | 184 | private enum EnumType { 185 | VALUE_1 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/test/java/com/amazon/rdsdata/client/MappingInputTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. 3 | * Licensed under the Apache License, Version 2.0 (the 4 | * "License"); you may not use this file except in compliance 5 | * with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package com.amazon.rdsdata.client; 16 | 17 | import com.amazon.rdsdata.client.testutil.SdkConstructs; 18 | import com.amazon.rdsdata.client.testutil.TestBase; 19 | import com.google.common.collect.ImmutableList; 20 | import lombok.Getter; 21 | import lombok.Value; 22 | import lombok.val; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Disabled; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import static com.amazon.rdsdata.client.FieldMapper.ERROR_FIELD_NOT_FOUND; 28 | import static com.amazon.rdsdata.client.FieldMapper.ERROR_VOID_RETURN_TYPE_NOT_SUPPORTED; 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 31 | 32 | class MappingInputTests extends TestBase { 33 | @BeforeEach 34 | void beforeEach() { 35 | mockReturnValue(); // return empty response by default 36 | } 37 | 38 | @Test 39 | void shouldMapDtoViaGetters() { 40 | val dto = new Getters(); 41 | 42 | client.forSql("INSERT INTO tbl1(a, b) VALUES(:firstName, :lastName)") 43 | .withParamSets(dto) 44 | .execute(); 45 | 46 | val request = captureRequest(); 47 | assertThat(request.parameters()).containsExactlyInAnyOrder( 48 | SdkConstructs.parameter("firstName", SdkConstructs.stringField("John")), 49 | SdkConstructs.parameter("lastName", SdkConstructs.stringField("Doe")) 50 | ); 51 | } 52 | 53 | @SuppressWarnings("unused") 54 | private static class Getters { 55 | public String getFirstName() { return "John"; } 56 | public String getLastName() { return "Doe"; } 57 | } 58 | 59 | @Test 60 | void shouldSupportNullValuesFromGetters() { 61 | val dto = new NullGetter(); 62 | 63 | client.forSql("INSERT INTO tbl1(a) VALUES(:data)") 64 | .withParamSets(dto) 65 | .execute(); 66 | 67 | val request = captureRequest(); 68 | assertThat(request.parameters()).containsExactlyInAnyOrder( 69 | SdkConstructs.parameter("data", SdkConstructs.nullField()) 70 | ); 71 | } 72 | 73 | @SuppressWarnings("unused") 74 | private static class NullGetter { 75 | public String getData() { return null; } 76 | } 77 | 78 | @Test 79 | void shouldMapDtoViaFields() { 80 | val dto = new Fields(); 81 | 82 | client.forSql("INSERT INTO tbl1(a, b) VALUES(:firstName, :lastName)") 83 | .withParamSets(dto) 84 | .execute(); 85 | 86 | val request = captureRequest(); 87 | assertThat(request.parameters()).containsExactlyInAnyOrder( 88 | SdkConstructs.parameter("firstName", SdkConstructs.stringField("John")), 89 | SdkConstructs.parameter("lastName", SdkConstructs.stringField("Doe")) 90 | ); 91 | } 92 | 93 | @SuppressWarnings("unused") 94 | private static class Fields { 95 | private final String firstName = "John"; 96 | private final String lastName = "Doe"; 97 | } 98 | 99 | @Test 100 | void shouldSupportNullValuesFromFields() { 101 | val dto = new NullField(); 102 | 103 | client.forSql("INSERT INTO tbl1(a) VALUES(:data)") 104 | .withParamSets(dto) 105 | .execute(); 106 | 107 | val request = captureRequest(); 108 | assertThat(request.parameters()).containsExactlyInAnyOrder( 109 | SdkConstructs.parameter("data", SdkConstructs.nullField()) 110 | ); 111 | } 112 | 113 | @SuppressWarnings("unused") 114 | private static class NullField { 115 | private final String data = null; 116 | } 117 | 118 | @Test 119 | void shouldMapDtoWithBothBoxedAndUnboxedFields() { 120 | val dto = new BoxedAndUnboxedInts(); 121 | 122 | client.forSql("INSERT INTO tbl1(a, b) VALUES(:boxed, :unboxed)") 123 | .withParamSets(dto) 124 | .execute(); 125 | 126 | val request = captureRequest(); 127 | assertThat(request.parameters()).containsExactlyInAnyOrder( 128 | SdkConstructs.parameter("unboxed", SdkConstructs.longField(1L)), 129 | SdkConstructs.parameter("boxed", SdkConstructs.longField(2L)) 130 | ); 131 | } 132 | 133 | @SuppressWarnings("unused") 134 | private static class BoxedAndUnboxedInts { 135 | private final int unboxed = 1; 136 | private final Integer boxed = 2; 137 | } 138 | 139 | @Test 140 | void shouldThrowExceptionIfFieldNotFound() { 141 | assertThatThrownBy(() -> 142 | client.forSql("INSERT INTO tbl1(a) VALUES(:fn)") 143 | .withParamSets(new Object()) 144 | .execute()) 145 | .isInstanceOf(IllegalArgumentException.class) 146 | .hasMessageStartingWith(ERROR_FIELD_NOT_FOUND.substring(0, 40)); 147 | } 148 | 149 | @Test 150 | void shouldThrowExceptionIfFieldReturnTypeIsVoid() { 151 | val dto = new VoidGetter(); 152 | assertThatThrownBy(() -> 153 | client.forSql("INSERT INTO tbl1(a) VALUES(:void)") 154 | .withParamSets(dto) 155 | .execute()) 156 | .isInstanceOf(IllegalArgumentException.class) 157 | .hasMessage(ERROR_VOID_RETURN_TYPE_NOT_SUPPORTED); 158 | } 159 | 160 | @SuppressWarnings("unused") 161 | private static class VoidGetter { 162 | public void getVoid() { } 163 | } 164 | 165 | @Test 166 | void shouldThrowNotFoundExceptionIfFieldNameIsANumber() { 167 | assertThatThrownBy(() -> 168 | client.forSql("INSERT INTO tbl1(a) VALUES(:333)") 169 | .withParamSets(new Object()) 170 | .execute()) 171 | .isInstanceOf(IllegalArgumentException.class) 172 | .hasMessageStartingWith(ERROR_FIELD_NOT_FOUND.substring(0, 40)); 173 | } 174 | 175 | @Test 176 | @Disabled // TODO: handle getters of type "is...()" 177 | void shouldMapDtoIsBooleanGetter() { 178 | client.forSql("INSERT INTO tbl1(a) VALUES(:enabled)") 179 | .withParamSets(new IsGetter()) 180 | .execute(); 181 | 182 | val request = captureRequest(); 183 | assertThat(request.parameters()).containsExactlyInAnyOrder( 184 | SdkConstructs.parameter("enabled", SdkConstructs.booleanField(true)) 185 | ); 186 | } 187 | 188 | @SuppressWarnings("unused") 189 | private static class IsGetter { 190 | public boolean isEnabled() { return true;} 191 | } 192 | 193 | @Test 194 | void shouldMapDtosForBatchUpdate() { 195 | val dto1 = new DtoForBatch(1); 196 | val dto2 = new DtoForBatch(2); 197 | 198 | client.forSql("INSERT INTO tbl1(a) VALUES(:value)") 199 | .withParamSets(dto1, dto2) 200 | .execute(); 201 | 202 | val request = captureBatchRequest(); 203 | assertThat(request.parameterSets()).containsExactly( 204 | // expecting list of lists of parameters 205 | ImmutableList.of( 206 | SdkConstructs.parameter("value", SdkConstructs.longField(1L)) 207 | ), 208 | ImmutableList.of( 209 | SdkConstructs.parameter("value", SdkConstructs.longField(2L)) 210 | ) 211 | ); 212 | } 213 | 214 | @Value 215 | private static class DtoForBatch { 216 | public final int value; 217 | } 218 | 219 | @Test 220 | void shouldMapInputParameterWhenOneParamSetIsPassed() { 221 | val dto = new SampleDto("value1"); 222 | 223 | client.forSql("SELECT * FROM table WHERE param1 = :param1") 224 | .withParameter(dto) 225 | .execute(); 226 | 227 | val request = captureRequest(); 228 | assertThat(request.parameters()).containsExactlyInAnyOrder( 229 | SdkConstructs.parameter("param1", SdkConstructs.stringField("value1")) 230 | ); 231 | } 232 | 233 | @Value 234 | private static class SampleDto { 235 | public final String param1; 236 | } 237 | 238 | @Test 239 | void shouldMapDtoWithFieldInChildClass() { 240 | val dto = new ParentWithNoFields(); 241 | 242 | client.forSql("INSERT INTO tbl1(value) VALUES(:value)") 243 | .withParamSets(dto) 244 | .execute(); 245 | 246 | val request = captureRequest(); 247 | assertThat(request.parameters()).containsExactlyInAnyOrder( 248 | SdkConstructs.parameter("value", SdkConstructs.longField(1L)) 249 | ); 250 | } 251 | 252 | private static class ParentWithNoFields extends ChildWithField { 253 | } 254 | 255 | private static class ChildWithField { 256 | @SuppressWarnings("unused") 257 | public final int value = 1; 258 | } 259 | 260 | @Test 261 | void shouldMapDtoWithMethodInChildClass() { 262 | val dto = new ParentWithNoMethods(); 263 | 264 | client.forSql("INSERT INTO tbl1(value) VALUES(:value)") 265 | .withParamSets(dto) 266 | .execute(); 267 | 268 | val request = captureRequest(); 269 | assertThat(request.parameters()).containsExactlyInAnyOrder( 270 | SdkConstructs.parameter("value", SdkConstructs.longField(1L)) 271 | ); 272 | } 273 | 274 | private static class ParentWithNoMethods extends ChildWithMethod { 275 | } 276 | 277 | private static class ChildWithMethod { 278 | @SuppressWarnings("unused") 279 | @Getter private final int value = 1; 280 | } 281 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | --------------------------------------------------------------------------------