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

Casl 534 java proxy fix #347

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
84 changes: 65 additions & 19 deletions here-naksha-lib-base/src/jvmMain/kotlin/naksha/base/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ actual class Platform {
actual fun intern(s: String, cd: Boolean): String = s

@JvmStatic
actual fun isAssignable(source: KClass<*>, target: KClass<*>): Boolean = source.java.isAssignableFrom(target.java)
actual fun isAssignable(source: KClass<*>, target: KClass<*>): Boolean =
source.java.isAssignableFrom(target.java)

@JvmStatic
actual fun isProxyKlass(klass: KClass<*>): Boolean = Proxy::class.isSuperclassOf(klass)
Expand Down Expand Up @@ -234,7 +235,7 @@ actual class Platform {
actual fun <K : Any, V : Any> newAtomicMap(): AtomicMap<K, V> = JvmAtomicMap()

@JvmStatic
actual fun <R: Any> newAtomicRef(startValue: R?): AtomicRef<R> = JvmAtomicRef(startValue)
actual fun <R : Any> newAtomicRef(startValue: R?): AtomicRef<R> = JvmAtomicRef(startValue)

@JvmStatic
actual fun newAtomicInt(startValue: Int): AtomicInt = JvmAtomicInt(startValue)
Expand All @@ -246,7 +247,8 @@ actual class Platform {
actual fun newByteArray(size: Int): ByteArray = ByteArray(size)

@JvmStatic
actual fun newDataView(byteArray: ByteArray, offset: Int, size: Int): PlatformDataView = JvmDataView(byteArray, offset, size)
actual fun newDataView(byteArray: ByteArray, offset: Int, size: Int): PlatformDataView =
JvmDataView(byteArray, offset, size)

@JvmStatic
actual fun <T : Any> newWeakRef(referent: T): WeakRef<T> = JvmWeakRef(referent)
Expand All @@ -267,7 +269,8 @@ actual class Platform {
* @return The [JvmObject] or _null_.
*/
@JvmStatic
fun toJvmObject(o: Any?): JvmObject? = if (o is Proxy) o.platformObject() as? JvmObject else if (o is JvmObject) o else null
fun toJvmObject(o: Any?): JvmObject? =
if (o is Proxy) o.platformObject() as? JvmObject else if (o is JvmObject) o else null

@JvmStatic
actual fun toInt(value: Any): Int = when (value) {
Expand Down Expand Up @@ -301,7 +304,8 @@ actual class Platform {
}

@JvmStatic
actual fun toInt64RawBits(d: Double): Int64 = longToInt64(java.lang.Double.doubleToRawLongBits(d))
actual fun toInt64RawBits(d: Double): Int64 =
longToInt64(java.lang.Double.doubleToRawLongBits(d))

@JvmStatic
actual fun longToInt64(value: Long): Int64 {
Expand All @@ -326,10 +330,12 @@ actual class Platform {
actual fun isNumber(o: Any?): Boolean = o is Number

@JvmStatic
actual fun isScalar(o: Any?): Boolean = o == null || o is Number || o is String || o is Boolean
actual fun isScalar(o: Any?): Boolean =
o == null || o is Number || o is String || o is Boolean

@JvmStatic
actual fun isInteger(o: Any?): Boolean = o is Byte || o is Short || o is Int || o is Long || o is JvmInt64
actual fun isInteger(o: Any?): Boolean =
o is Byte || o is Short || o is Int || o is Long || o is JvmInt64

@JvmStatic
actual fun isDouble(o: Any?): Boolean = o is Double
Expand All @@ -341,11 +347,13 @@ actual class Platform {
actual fun hashCodeOf(o: Any?): Int = throw UnsupportedOperationException()

@JvmStatic
actual fun <T : Any> newInstanceOf(klass: KClass<out T>): T = klass.primaryConstructor?.call() ?: throw IllegalArgumentException()
actual fun <T : Any> newInstanceOf(klass: KClass<out T>): T =
klass.primaryConstructor?.call() ?: throw IllegalArgumentException()

@JvmStatic
@Suppress("UNCHECKED_CAST")
actual fun <T : Any> allocateInstance(klass: KClass<out T>): T = unsafe.allocateInstance(klass.java) as T
actual fun <T : Any> allocateInstance(klass: KClass<out T>): T =
unsafe.allocateInstance(klass.java) as T

@JvmStatic
actual fun initializeKlass(klass: KClass<*>) {
Expand Down Expand Up @@ -373,6 +381,7 @@ actual class Platform {
}
copy as T
}

is JvmList -> {
val copy = JvmList(obj.size)
for (value in obj) {
Expand All @@ -381,6 +390,7 @@ actual class Platform {
}
copy as T
}

else -> obj
}
}
Expand Down Expand Up @@ -504,19 +514,47 @@ actual class Platform {
*/
@Suppress("UNCHECKED_CAST")
@JvmStatic
actual fun <T : Proxy> proxy(pobject: PlatformObject, klass: KClass<T>, doNotOverride: Boolean): T {
actual fun <T : Proxy> proxy(
pobject: PlatformObject,
klass: KClass<T>,
doNotOverride: Boolean
): T {
require(pobject is JvmObject)
val symbol = Symbols.of(klass)
var proxy = pobject.getSymbol(symbol)
if (proxy != null) {
if (klass.isInstance(proxy)) return proxy as T
if (doNotOverride) throw IllegalStateException("The symbol $symbol is already bound to incompatible type")
}
proxy = klass.primaryConstructor!!.call()

val constructor: KFunction<T> = primaryNonArgConstructorOf(klass)
?: firstNonArgConstructorOf(klass)
?: throw IllegalArgumentException("Unable to find primary or non-arg constructor for class: ${klass.qualifiedName}")
proxy = constructor.call()
Copy link
Member

@xeus2001 xeus2001 Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, any constructor that does not require arguments is fine for us, we should always first try the primary constructor, but if that does not match, try to find any other constructor.
These checks and searching for the correct constructor can be time consuming, I would suggest to add a cache here, a concurrent hash-map, to cache the found constructors. Like:

var constructor = cache[klass]
if (constructor == null) {
  constructor = ... find the correct one
  cache[klass] = constructor
}

We do not need any synchronization here, because we can assume that the finding operation is idempotent, so even when multiple threads in parallel enter the search and override each other, the cache should work after that as intended.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

proxy.bind(pobject, symbol)
return proxy
}

/**
* Returns primary non-arg constructor of [klass]
* If primary constructor of [klass] does require some parameters, an exception is thrown. Proxy-based classes should not require any args for primary constructor.
*/
private fun <T : Proxy> primaryNonArgConstructorOf(klass: KClass<T>): KFunction<T>? {
return klass.primaryConstructor?.also { primaryConstructor ->
require(primaryConstructor.parameters.isEmpty()) { "Primary constructor of Proxy classes can't have any arguments, invalid class: ${klass.qualifiedName}" }
}
}

/**
* Returns first non-arg constructor for [klass] or null, if none matches.
* This is mainly to address Java proxies, as Java spec does not define 'primary constructor' (whereas Kotlin does).
*/
private fun <T : Proxy> firstNonArgConstructorOf(klass: KClass<T>): KFunction<T>? {
return klass.constructors.firstOrNull { constructor: KFunction<T> ->
constructor.parameters.isEmpty()
}
}

/**
* The default logger singleton to be used as initial value by the default [loggerThreadLocal]. This is based upon
* [SLF4j](https://www.slf4j.org/). If an application has a different logger singleton, it can simply place this variable. If the
Expand Down Expand Up @@ -545,7 +583,8 @@ actual class Platform {
* @return The thread local.
*/
@JvmStatic
actual fun <T> newThreadLocal(initializer: (() -> T)?): PlatformThreadLocal<T> = JvmThreadLocal(initializer)
actual fun <T> newThreadLocal(initializer: (() -> T)?): PlatformThreadLocal<T> =
JvmThreadLocal(initializer)

/**
* The nano-time when the class is initialized.
Expand All @@ -557,13 +596,15 @@ actual class Platform {
* The epoch microseconds when the class is initialized.
*/
@JvmField
internal val epochMicros = (System.currentTimeMillis() * 1000) + ((startNanos / 1000) % 1000)
internal val epochMicros =
(System.currentTimeMillis() * 1000) + ((startNanos / 1000) % 1000)

/**
* The epoch nanoseconds when the class is initialized.
*/
@JvmField
internal val epochNanos = (System.currentTimeMillis() * 1_000_000) + (startNanos % 1_000_000)
internal val epochNanos =
(System.currentTimeMillis() * 1_000_000) + (startNanos % 1_000_000)

/**
* Returns the current epoch milliseconds.
Expand All @@ -577,13 +618,15 @@ actual class Platform {
* @return current epoch microseconds.
*/
@JvmStatic
actual fun currentMicros(): Int64 = longToInt64(epochMicros + ((System.nanoTime() - startNanos) / 1000))
actual fun currentMicros(): Int64 =
longToInt64(epochMicros + ((System.nanoTime() - startNanos) / 1000))

/**
* Returns the current epoch nanoseconds.
* @return current epoch nanoseconds.
*/
actual fun currentNanos(): Int64 = longToInt64(epochNanos + (System.nanoTime() - startNanos))
actual fun currentNanos(): Int64 =
longToInt64(epochNanos + (System.nanoTime() - startNanos))

/**
* Generates a new random number between 0 and 1 (therefore with 53-bit random bits).
Expand Down Expand Up @@ -674,7 +717,8 @@ actual class Platform {
val compressor = lz4Factory.fastCompressor()
val maxCompressedLength = compressor.maxCompressedLength(raw.size)
val compressed = ByteArray(maxCompressedLength)
val compressedLength = compressor.compress(raw, 0, raw.size, compressed, 0, maxCompressedLength)
val compressedLength =
compressor.compress(raw, 0, raw.size, compressed, 0, maxCompressedLength)
return compressed.copyOf(compressedLength)
}

Expand All @@ -688,7 +732,8 @@ actual class Platform {
// TODO: Simple multiplication of the compressed by 12 is not optimal!
val decompressor = lz4Factory.fastDecompressor()
val restored = ByteArray(compressed.size * 12)
val decompressedLength = decompressor.decompress(compressed, 0, restored, 0, restored.size)
val decompressedLength =
decompressor.decompress(compressed, 0, restored, 0, restored.size)
if (decompressedLength < restored.size) {
return restored.copyOf(decompressedLength)
}
Expand All @@ -714,7 +759,8 @@ actual class Platform {
actual fun stackTrace(t: Throwable): String = t.stackTraceToString()

@JvmStatic
actual fun normalize(value: String, form: NormalizerForm): String = Normalizer.normalize(value, Normalizer.Form.valueOf(form.name))
actual fun normalize(value: String, form: NormalizerForm): String =
Normalizer.normalize(value, Normalizer.Form.valueOf(form.name))

init {
initialize()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2017-2024 HERE Europe B.V.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/
package naksha.base;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class JavaProxyTest {

@Test
void shouldAllowProxyingInJava() {
// Given:
ProxyParent parent = new ProxyParent();

// When:
var child = parent.proxy(Platform.klassOf(ProxyParent.class));

// Then:
assertNotNull(child);
assertInstanceOf(ProxyChild.class, child);
}

@Test
void shouldFailForProxyWithoutNonArgConstructor() {
// Given:
ProxyParent parent = new ProxyParent();

// Then:
assertThrows(IllegalArgumentException.class, () -> {
parent.proxy(Platform.klassOf(ProxyChildWithoutNonArgConstructor.class));
});
}

static class ProxyParent extends AnyObject {}

static class ProxyChild extends ProxyParent {}

static class ProxyChildWithoutNonArgConstructor extends ProxyParent {
ProxyChildWithoutNonArgConstructor(String unusedParam) {}
}
}
79 changes: 79 additions & 0 deletions here-naksha-lib-model/src/jvmTest/java/NakshaFeatureProxyTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2017-2024 HERE Europe B.V.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import kotlin.reflect.full.IllegalCallableAccessException;
import naksha.base.Platform;
import naksha.geo.PointCoord;
import naksha.geo.SpPoint;
import naksha.model.objects.NakshaFeature;
import org.junit.jupiter.api.Test;

class NakshaFeatureProxyTest {

@Test
void shouldAllowProxyingFeature() {
// Given:
NakshaFeature nakshaFeature = new NakshaFeature();
nakshaFeature.setId("my_id");
nakshaFeature.setGeometry(new SpPoint(new PointCoord(10, 20)));

// When:
CustomFeature proxiedFeature = nakshaFeature.proxy(Platform.klassOf(CustomFeature.class));

// Then:
assertEquals(nakshaFeature.getId(), proxiedFeature.getId());
assertEquals(nakshaFeature.getGeometry(), proxiedFeature.getGeometry());
}

@Test
void shouldFailForProxyWithoutNonArgConstructor() {
// Given:
NakshaFeature nakshaFeature = new NakshaFeature();

// Then:
assertThrows(IllegalArgumentException.class, () -> {
nakshaFeature.proxy(Platform.klassOf(CustomFeatureWithoutNonArgConstructor.class));
});
}

@Test
void shouldFailForNonPublicProxy() {
// Given:
NakshaFeature nakshaFeature = new NakshaFeature();

// Then:
assertThrows(IllegalCallableAccessException.class, () -> {
nakshaFeature.proxy(Platform.klassOf(NonPublicCustomFeature.class));
});
}

public static class CustomFeature extends NakshaFeature {

public CustomFeature() {}
}

public static class CustomFeatureWithoutNonArgConstructor extends NakshaFeature {

public CustomFeatureWithoutNonArgConstructor(String unusedParam) {}
}

static class NonPublicCustomFeature extends NakshaFeature {}
}
Loading