From 03567dc57ed1b1b4c6ee246bdcd9eb54cc0e5d74 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Tue, 1 Aug 2017 12:48:14 -0400 Subject: [PATCH] Enable partial updates using partial entity maps. --- .gitignore | 2 + helenus-core.iml | 6 +- pom.xml | 2 +- .../core/operation/InsertOperation.java | 3 - .../core/reflect/MapperInvocationHandler.java | 222 +++++++++--------- .../value/StatementColumnValuePreparer.java | 3 + .../core/simple/InsertPartialTest.java | 69 ++++++ .../helenus/test/unit/core/dsl/DslTest.java | 8 +- .../test/unit/core/dsl/WrapperTest.java | 8 + 9 files changed, 199 insertions(+), 124 deletions(-) create mode 100644 src/test/java/net/helenus/test/integration/core/simple/InsertPartialTest.java diff --git a/.gitignore b/.gitignore index 2dbd73e..ddb2c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.iml +.idea infer-out *.class diff --git a/helenus-core.iml b/helenus-core.iml index 147f8ae..42b1cc5 100644 --- a/helenus-core.iml +++ b/helenus-core.iml @@ -35,11 +35,7 @@ - - - - - + diff --git a/pom.xml b/pom.xml index 8d5ee46..fec721c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 net.helenus helenus-core - 2.0.3-SNAPSHOT + 2.0.4-SNAPSHOT jar helenus diff --git a/src/main/java/net/helenus/core/operation/InsertOperation.java b/src/main/java/net/helenus/core/operation/InsertOperation.java index e12ee4a..5744448 100644 --- a/src/main/java/net/helenus/core/operation/InsertOperation.java +++ b/src/main/java/net/helenus/core/operation/InsertOperation.java @@ -60,11 +60,9 @@ public final class InsertOperation extends AbstractOperation implements InvocationHandler { - - private final Map src; - private final Class iface; - - public MapperInvocationHandler(Class iface, Map src) { - this.src = src; - this.iface = iface; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - - if (method.isDefault()) { - // NOTE: This is reflection magic to invoke (non-recursively) a default method implemented on an interface - // that we've proxied (in ReflectionDslInstantiator). I found the answer in this article. - // https://zeroturnaround.com/rebellabs/recognize-and-conquer-java-proxies-default-methods-and-method-handles/ - - // First, we need an instance of a private inner-class found in MethodHandles. - Constructor constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); - constructor.setAccessible(true); - - // Now we need to lookup and invoke special the default method on the interface class. - final Class declaringClass = method.getDeclaringClass(); - Object result = constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE) - .unreflectSpecial(method, declaringClass) - .bindTo(proxy) - .invokeWithArguments(args); - return result; - } - - String methodName = method.getName(); - - if ("equals".equals(methodName) && method.getParameterCount() == 1) { - Object otherObj = args[0]; - if (otherObj == null) { - return false; - } - if (Proxy.isProxyClass(otherObj.getClass())) { - return this == Proxy.getInvocationHandler(otherObj); - } - return false; - } - - if (method.getParameterCount() != 0 || method.getReturnType() == void.class) { - throw new HelenusException("invalid getter method " + method); - } - - if ("hashCode".equals(methodName)) { - return hashCode(); - } - - if ("toString".equals(methodName)) { - return iface.getSimpleName() + ": " + src.toString(); - } - - if (MapExportable.TO_MAP_METHOD.equals(methodName)) { - return Collections.unmodifiableMap(src); - } - - Object value = src.get(methodName); - - if (value == null) { - - Class returnType = method.getReturnType(); - - if (returnType.isPrimitive()) { - - DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(returnType); - if (type == null) { - throw new HelenusException("unknown primitive type " + returnType); - } - - return type.getDefaultValue(); - - } - - } - - return value; - } - -} +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.core.reflect; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.Map; + +import net.helenus.support.HelenusException; + +public class MapperInvocationHandler implements InvocationHandler { + + private final Map src; + private final Class iface; + + public MapperInvocationHandler(Class iface, Map src) { + this.src = src; + this.iface = iface; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + if (method.isDefault()) { + // NOTE: This is reflection magic to invoke (non-recursively) a default method implemented on an interface + // that we've proxied (in ReflectionDslInstantiator). I found the answer in this article. + // https://zeroturnaround.com/rebellabs/recognize-and-conquer-java-proxies-default-methods-and-method-handles/ + + // First, we need an instance of a private inner-class found in MethodHandles. + Constructor constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); + constructor.setAccessible(true); + + // Now we need to lookup and invoke special the default method on the interface class. + final Class declaringClass = method.getDeclaringClass(); + Object result = constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE) + .unreflectSpecial(method, declaringClass) + .bindTo(proxy) + .invokeWithArguments(args); + return result; + } + + String methodName = method.getName(); + + if ("equals".equals(methodName) && method.getParameterCount() == 1) { + Object otherObj = args[0]; + if (otherObj == null) { + return false; + } + if (Proxy.isProxyClass(otherObj.getClass())) { + return this == Proxy.getInvocationHandler(otherObj); + } + return false; + } + + if (method.getParameterCount() != 0 || method.getReturnType() == void.class) { + throw new HelenusException("invalid getter method " + method); + } + + if ("hashCode".equals(methodName)) { + return hashCode(); + } + + if ("toString".equals(methodName)) { + return iface.getSimpleName() + ": " + src.toString(); + } + + if (MapExportable.TO_MAP_METHOD.equals(methodName)) { + return Collections.unmodifiableMap(src); + } + + Object value = src.get(methodName); + + if (value == null && src.containsKey(methodName)) { + + Class returnType = method.getReturnType(); + + if (returnType.isPrimitive()) { + + DefaultPrimitiveTypes type = DefaultPrimitiveTypes.lookup(returnType); + if (type == null) { + throw new HelenusException("unknown primitive type " + returnType); + } + + return type.getDefaultValue(); + + } + + } + + return value; + } + +} diff --git a/src/main/java/net/helenus/mapping/value/StatementColumnValuePreparer.java b/src/main/java/net/helenus/mapping/value/StatementColumnValuePreparer.java index 60ee3b0..717cf7a 100644 --- a/src/main/java/net/helenus/mapping/value/StatementColumnValuePreparer.java +++ b/src/main/java/net/helenus/mapping/value/StatementColumnValuePreparer.java @@ -35,6 +35,9 @@ public final class StatementColumnValuePreparer implements ColumnValuePreparer { @Override public Object prepareColumnValue(Object value, HelenusProperty prop) { + if (value == null) + return null; + if (value instanceof BindMarker) { return value; } diff --git a/src/test/java/net/helenus/test/integration/core/simple/InsertPartialTest.java b/src/test/java/net/helenus/test/integration/core/simple/InsertPartialTest.java new file mode 100644 index 0000000..a0ecbba --- /dev/null +++ b/src/test/java/net/helenus/test/integration/core/simple/InsertPartialTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Helenus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.helenus.test.integration.core.simple; + +import net.helenus.core.Helenus; +import net.helenus.core.HelenusSession; +import net.helenus.core.operation.InsertOperation; +import net.helenus.support.DslPropertyException; +import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class InsertPartialTest extends AbstractEmbeddedCassandraTest { + + static HelenusSession session; + static User user; + static Random rnd = new Random(); + + @BeforeClass + public static void beforeTests() { + session = Helenus.init(getSession()).showCql().add(User.class).autoCreateDrop().get(); + user = Helenus.dsl(User.class); + } + + @Test + public void testPartialInsert() throws Exception { + Map map = new HashMap(); + Long id = rnd.nextLong(); + map.put("id", id); + map.put("age", 5); + InsertOperation insert = session.insert(Helenus.map(User.class, map)); + String cql = "INSERT INTO simple_users (id,age) VALUES (" + id.toString() + ",5) IF NOT EXISTS;"; + Assert.assertEquals(cql, insert.cql()); + insert.sync(); + } + + @Test + public void testPartialUpsert() throws Exception { + Map map = new HashMap(); + Long id = rnd.nextLong(); + map.put("id", id); + map.put("age", 5); + InsertOperation upsert = session.upsert(Helenus.map(User.class, map)); + String cql = "INSERT INTO simple_users (id,age) VALUES (" + id.toString() + ",5);"; + Assert.assertEquals(cql, upsert.cql()); + upsert.sync(); + } + +} diff --git a/src/test/java/net/helenus/test/unit/core/dsl/DslTest.java b/src/test/java/net/helenus/test/unit/core/dsl/DslTest.java index 658ae4a..cd825a1 100644 --- a/src/test/java/net/helenus/test/unit/core/dsl/DslTest.java +++ b/src/test/java/net/helenus/test/unit/core/dsl/DslTest.java @@ -23,20 +23,20 @@ import org.junit.Test; public class DslTest { static Account account; - + @BeforeClass public static void beforeTests() { account = Helenus.dsl(Account.class); } - + @Test public void testToString() throws Exception { System.out.println(account); } - + @Test(expected=DslPropertyException.class) public void test() throws Exception { account.id(); } - + } diff --git a/src/test/java/net/helenus/test/unit/core/dsl/WrapperTest.java b/src/test/java/net/helenus/test/unit/core/dsl/WrapperTest.java index 3f50a83..679f37b 100644 --- a/src/test/java/net/helenus/test/unit/core/dsl/WrapperTest.java +++ b/src/test/java/net/helenus/test/unit/core/dsl/WrapperTest.java @@ -18,6 +18,8 @@ package net.helenus.test.unit.core.dsl; import java.util.HashMap; import java.util.Map; +import net.helenus.core.HelenusSession; +import net.helenus.mapping.value.ValueProviderMap; import org.junit.Assert; import org.junit.Test; @@ -45,9 +47,15 @@ public class WrapperTest { @Test public void testPrimitive() throws Exception { + // NOTE: noramlly a ValueProviderMap for the entity would include all keys for an entity + // at creation time. This test need to validate that MapperInvocationHander will return + // the correct default value for an entity, the twist is that if the key doesn't exist + // in the map then it returns null (so as to support the partial update feature). Here we + // need to setup the test with a null value for the key we'd like to test. Map map = new HashMap(); map.put("id", 123L); + map.put("active", null); Account account = Helenus.map(Account.class, map);