Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move method reflection from ExtensionFactoryDelegate to ExtensionMetadata #2414

Merged
merged 2 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.jdbi.v3.core.config.ConfigRegistry;
import org.jdbi.v3.core.extension.ExtensionMetadata.Builder;
import org.jdbi.v3.core.extension.ExtensionMetadata.ExtensionHandlerInvoker;
import org.jdbi.v3.core.internal.JdbiClassUtils;

import static java.lang.String.format;

Expand All @@ -35,6 +33,9 @@
import static org.jdbi.v3.core.extension.ExtensionHandler.EQUALS_HANDLER;
import static org.jdbi.v3.core.extension.ExtensionHandler.HASHCODE_HANDLER;
import static org.jdbi.v3.core.extension.ExtensionHandler.NULL_HANDLER;
import static org.jdbi.v3.core.internal.JdbiClassUtils.EQUALS_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.HASHCODE_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.TOSTRING_METHOD;

final class ExtensionFactoryDelegate implements ExtensionFactory {

Expand Down Expand Up @@ -125,41 +126,24 @@ public <E> E attach(Class<E> extensionType, HandleSupplier handleSupplier) {
// those will only be added if they don't already exist in the method handler map.

// If these methods are added, they are special because they operate on the proxy object itself, not the underlying object
checkMethodPresent(extensionType, Object.class, "toString").ifPresent(method -> {
ExtensionHandler toStringHandler = (h, target, args) ->
"Jdbi extension proxy for " + extensionType.getName() + "@" + Integer.toHexString(proxy.hashCode());
handlers.put(method, extensionMetaData.new ExtensionHandlerInvoker(proxy, method, toStringHandler, handleSupplier, instanceConfig));
});
ExtensionHandler toStringHandler = (h, target, args) ->
"Jdbi extension proxy for " + extensionType.getName() + "@" + Integer.toHexString(proxy.hashCode());
handlers.put(TOSTRING_METHOD, extensionMetaData.new ExtensionHandlerInvoker(proxy, TOSTRING_METHOD, toStringHandler, handleSupplier, instanceConfig));

checkMethodPresent(extensionType, Object.class, "equals", Object.class).ifPresent(method -> handlers.put(method,
extensionMetaData.new ExtensionHandlerInvoker(proxy, method, EQUALS_HANDLER, handleSupplier, instanceConfig)));
checkMethodPresent(extensionType, Object.class, "hashCode").ifPresent(method -> handlers.put(method,
extensionMetaData.new ExtensionHandlerInvoker(proxy, method, HASHCODE_HANDLER, handleSupplier, instanceConfig)));
handlers.put(EQUALS_METHOD, extensionMetaData.new ExtensionHandlerInvoker(proxy, EQUALS_METHOD, EQUALS_HANDLER, handleSupplier, instanceConfig));
handlers.put(HASHCODE_METHOD, extensionMetaData.new ExtensionHandlerInvoker(proxy, HASHCODE_METHOD, HASHCODE_HANDLER, handleSupplier, instanceConfig));

// add all methods that are delegated to the underlying object / existing handlers
extensionMetaData.getExtensionMethods().forEach(method ->
handlers.put(method, extensionMetaData.createExtensionHandlerInvoker(delegatedInstance, method, handleSupplier, instanceConfig)));

// finalize is double special. Add this unconditionally, even if subclasses try to override it.
JdbiClassUtils.safeMethodLookup(extensionType, "finalize").ifPresent(method -> handlers.put(method,
extensionMetaData.getFinalizer().ifPresent(method -> handlers.put(method,
extensionMetaData.new ExtensionHandlerInvoker(proxy, method, NULL_HANDLER, handleSupplier, instanceConfig)));


return extensionType.cast(proxy);
}

/** returns Optional.empty() if the method exists in the extension type, otherwise a fallback method. */
private Optional<Method> checkMethodPresent(Class<?> extensionType, Class<?> klass, String methodName, Class<?>... parameterTypes) {
Optional<Method> method = JdbiClassUtils.safeMethodLookup(extensionType, methodName, parameterTypes);
if (method.isPresent()) {
// does the method actually exist in the type itself (e.g. overridden by the implementation class?)
// if yes, return absent, so the default hander is not added.
return Optional.empty();
} else {
return JdbiClassUtils.safeMethodLookup(klass, methodName, parameterTypes);
}
}

@Override
public String toString() {
return "ExtensionFactoryDelegate for " + delegatedFactory.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
@Alpha
public interface ExtensionHandler {

/** Implementation for the {@link Object#equals(Object)} method. Each object using this handler is only equal to ifself. */
/** Implementation for the {@link Object#equals(Object)} method. Each object using this handler is only equal to itself. */
ExtensionHandler EQUALS_HANDLER = (handleSupplier, target, args) -> target == args[0];

/** Implementation for the {@link Object#hashCode()} method. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public final class ExtensionMetadata {
private final ConfigCustomizer instanceConfigCustomizer;
private final Map<Method, ? extends ConfigCustomizer> methodConfigCustomizers;
private final Map<Method, ExtensionHandler> methodHandlers;
private final Optional<Method> finalizer;

/**
* Returns a new {@link ExtensionMetadata.Builder} instance.
Expand All @@ -61,11 +62,13 @@ private ExtensionMetadata(
Class<?> extensionType,
ConfigCustomizer instanceConfigCustomizer,
Map<Method, ? extends ConfigCustomizer> methodConfigCustomizers,
Map<Method, ExtensionHandler> methodHandlers) {
Map<Method, ExtensionHandler> methodHandlers,
Optional<Method> finalizer) {
this.extensionType = extensionType;
this.instanceConfigCustomizer = instanceConfigCustomizer;
this.methodConfigCustomizers = Collections.unmodifiableMap(methodConfigCustomizers);
this.methodHandlers = Collections.unmodifiableMap(methodHandlers);
this.finalizer = finalizer;
}

public Class<?> extensionType() {
Expand Down Expand Up @@ -109,6 +112,14 @@ public Set<Method> getExtensionMethods() {
return methodHandlers.keySet();
}

/**
* Returns a reference to a method that overrides {@link Object#finalize()} if it exists.
* @return An {@link Optional} containing a {@link Method} if a finalizer exists.
*/
Optional<Method> getFinalizer() {
return finalizer;
}

/**
* Creates an {@link ExtensionHandlerInvoker} instance for a specific method.
* @param target The target object on which the invoker should work
Expand Down Expand Up @@ -140,6 +151,8 @@ public static final class Builder {

private final Collection<Method> extensionTypeMethods = new HashSet<>();

private final Optional<Method> finalizer;

Builder(Class<?> extensionType) {
this.extensionType = extensionType;

Expand All @@ -157,6 +170,8 @@ public static final class Builder {
throw new UnableToCreateExtensionException("%s has ambiguous methods (%s) found, please resolve with an explicit override",
extensionType, methods);
});

this.finalizer = JdbiClassUtils.safeMethodLookup(extensionType, "finalize");
}

/**
Expand Down Expand Up @@ -270,7 +285,7 @@ public ExtensionMetadata build() {
.forEach(configCustomizer -> this.addMethodConfigCustomizer(method, configCustomizer)));
}

return new ExtensionMetadata(extensionType, instanceConfigCustomizer, methodConfigCustomizers, methodHandlers);
return new ExtensionMetadata(extensionType, instanceConfigCustomizer, methodConfigCustomizers, methodHandlers, finalizer);
}

private Optional<ExtensionHandler> findExtensionHandlerFor(Class<?> extensionType, Method method) {
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/org/jdbi/v3/core/internal/JdbiClassUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@
*/
public final class JdbiClassUtils {

/** Constant for {@link Object#equals(Object)}. */
public static final Method EQUALS_METHOD = methodLookup(Object.class, "equals", Object.class);

/** Constant for {@link Object#hashCode()}. */
public static final Method HASHCODE_METHOD = methodLookup(Object.class, "hashCode");

/** Constant for {@link Object#toString()}. */
public static final Method TOSTRING_METHOD = methodLookup(Object.class, "toString");


private JdbiClassUtils() {
throw new UtilityClassException();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,13 @@
import org.jdbi.v3.core.extension.Extensions;
import org.jdbi.v3.core.internal.exceptions.Unchecked;

public class OnDemandExtensions implements JdbiConfig<OnDemandExtensions> {
private static final Method EQUALS_METHOD;
private static final Method HASHCODE_METHOD;
private static final Method TOSTRING_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.EQUALS_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.HASHCODE_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.TOSTRING_METHOD;

public class OnDemandExtensions implements JdbiConfig<OnDemandExtensions> {
private Factory onDemandExtensionFactory;

static {
EQUALS_METHOD = JdbiClassUtils.methodLookup(Object.class, "equals", Object.class);
HASHCODE_METHOD = JdbiClassUtils.methodLookup(Object.class, "hashCode");
TOSTRING_METHOD = JdbiClassUtils.methodLookup(Object.class, "toString");
}

public OnDemandExtensions() {
onDemandExtensionFactory = (jdbi, extensionType, extraTypes) -> Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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 org.jdbi.v3.core.extension;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.extension.annotation.UseExtensionHandler;
import org.jdbi.v3.core.junit5.H2DatabaseExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static org.assertj.core.api.Assertions.assertThat;

public class TestDefaultExtensionMethods {

@RegisterExtension
public H2DatabaseExtension h2Extension = H2DatabaseExtension.instance();

private Handle handle;

@BeforeEach
public void setUp() {
handle = h2Extension.getSharedHandle();
handle.getConfig(Extensions.class).register(new ExtensionFrameworkTestFactory());
}

@Test
public void testDefaultMethodToString() {
DefaultMethodInterface dmo1 = handle.attach(DefaultMethodInterface.class);
assertThat(dmo1.toString()).startsWith("Jdbi extension proxy for " + DefaultMethodInterface.class.getName());

DefaultMethodInterface dmo2 = handle.attach(DefaultMethodInterface.class);

assertThat(dmo1.toString()).startsWith("Jdbi extension proxy for " + DefaultMethodInterface.class.getName());
assertThat(dmo2.toString()).startsWith("Jdbi extension proxy for " + DefaultMethodInterface.class.getName());
assertThat(dmo1.equals(dmo2)).isFalse();
assertThat(dmo2.equals(dmo1)).isFalse();

assertThat(dmo1.hashCode()).isNotEqualTo(dmo2.hashCode());
assertThat(dmo1.hashCode()).isEqualTo(System.identityHashCode(dmo1));
}

@Test
public void testDefaultMethodEquals() {
DefaultMethodInterface dmo1 = handle.attach(DefaultMethodInterface.class);
DefaultMethodInterface dmo2 = handle.attach(DefaultMethodInterface.class);

assertThat(dmo1.equals(dmo2)).isFalse();
assertThat(dmo2.equals(dmo1)).isFalse();
}

@Test
public void testDefaultMethodHashCode() {
DefaultMethodInterface dmo1 = handle.attach(DefaultMethodInterface.class);
DefaultMethodInterface dmo2 = handle.attach(DefaultMethodInterface.class);

assertThat(dmo1.hashCode()).isNotEqualTo(dmo2.hashCode());
assertThat(dmo1.hashCode()).isEqualTo(System.identityHashCode(dmo1));
assertThat(dmo2.hashCode()).isEqualTo(System.identityHashCode(dmo2));
}

@Test
public void testFinalizeOverride() throws Throwable {
FinalizeInterface fm = handle.attach(FinalizeInterface.class);
assertThat(fm.testFinalize()).isEqualTo("a ok");
}

public interface DefaultMethodInterface {
@ForTest
void testMethod();
}

public interface FinalizeInterface {
@ForTest
void testMethod();

default String testFinalize() {
this.finalize();
return "a ok";
}

default void finalize() {}
}

@Retention(RetentionPolicy.RUNTIME)
@UseExtensionHandler(id = "test", value = TestExtensionAnnotations.Foo.Impl.class)
public @interface ForTest {

class Impl implements ExtensionHandler {

@Override
public Object invoke(HandleSupplier handleSupplier, Object target, Object[] args) {
return "foo";
}
}
}
}