All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.web3j.openapi.client.ClientInvocationHandler.kt Maven / Gradle / Ivy

/*
 * Copyright 2019 Web3 Labs Ltd.
 *
 * 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.web3j.openapi.client

import mu.KLogging
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Proxy
import java.net.URL
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
import javax.ws.rs.ClientErrorException
import javax.ws.rs.client.WebTarget
import javax.ws.rs.sse.SseEventSource

/**
 * Invocation handler for proxied resources.
 *
 * Handles contract events using a Server-Sent Event (SSE) request.
 *
 * Also implements an exception mapping mechanism to avoid reporting
 * [ClientErrorException]s to the client.
 *
 * @see [org.web3j.openapi.core.EventResource.onEvent]
 * @see [ClientException]
 */
internal class ClientInvocationHandler(
    private val target: WebTarget,
    private val client: Any
) : InvocationHandler {

    override fun invoke(proxy: Any, method: Method, args: Array?): Any? {
        return if (method.isEvent()) {
            logger.debug { "Invoking event method: $method" }
            @Suppress("UNCHECKED_CAST")
            invokeOnEvent(args!![0] as Consumer)
        } else {
            logger.debug { "Invoking client method: $method" }
            invokeClient(method, args)
        }
    }

    private fun  invokeOnEvent(onEvent: Consumer): CompletableFuture {
        @Suppress("UNCHECKED_CAST")
        val eventType = onEvent.typeArguments[0] as Class
        val source = SseEventSource.target(clientTarget()).build()
        return SseEventSourceResult(source, onEvent, eventType).also {
            it.open()
        }
    }

    private fun invokeClient(method: Method, args: Array?): Any {
        try {
            // Invoke the original method on the client
            return method.invoke(client, *(args ?: arrayOf())).let {
                if (Proxy.isProxyClass(it.javaClass)) {
                    // The result is a Jersey web resource
                    // so we need to wrap it again
                    Proxy.newProxyInstance(
                        method.returnType.classLoader,
                        arrayOf(method.returnType),
                        ClientInvocationHandler(target, it)
                    )
                } else {
                    it
                }
            }
        } catch (e: InvocationTargetException) {
            throw handleInvocationException(e, method)
        } catch (e: ClientErrorException) {
            throw handleClientError(e, method)
        }
    }

    private fun handleInvocationException(error: InvocationTargetException, method: Method): Throwable {
        return error.targetException.let {
            if (it is ClientErrorException) {
                handleClientError(it, method)
            } else {
                logger.error {
                    "Unexpected exception while invoking method $method: " +
                            (error.message ?: error::class.java.canonicalName)
                }
                error.targetException
            }
        }
    }

    private fun handleClientError(error: ClientErrorException, method: Method): RuntimeException {
        logger.error {
            "Client exception while invoking method $method: " +
                    (error.message ?: error.response.statusInfo.reasonPhrase)
        }
        return ClientException.of(error)
    }

    private fun clientTarget(): WebTarget {
        val resourcePath = client.toString()
            .removePrefix("JerseyWebTarget { ")
            .removeSuffix(" }")
            .run { URL(this).path }
        return target.path(resourcePath)
    }

    private fun Method.isEvent() = name == "onEvent" &&
            parameterTypes.size == 1 &&
            parameterTypes[0] == Consumer::class.java &&
            returnType == CompletableFuture::class.java

    private val Any.typeArguments: List>
        get() {
            val parameterizedType = this::class.java.genericInterfaces[0] as ParameterizedType
            return parameterizedType.actualTypeArguments.map { it as Class<*> }
        }

    private class SseEventSourceResult(
        private val source: SseEventSource,
        onEvent: Consumer,
        eventType: Class
    ) : CompletableFuture() {
        init {
            source.register(
                { onEvent.accept(it.readData(eventType)) },
                { completeExceptionally(it) },
                { complete(null) }
            )
            whenComplete { _, _ ->
                // Close the source gracefully by client
                if (source.isOpen) source.close()
            }
        }
        fun open() {
            Thread {
                source.open()
                while (source.isOpen) {
                    logger.debug { "Listening on event source..." }
                    Thread.sleep(5000)
                }
            }.start()
        }
    }

    companion object : KLogging()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy