diff --git a/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java b/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java index e3acac8cc..6c6f57fa5 100644 --- a/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java +++ b/backend/services/syson-services/src/main/java/org/eclipse/syson/util/ServiceMethod.java @@ -13,6 +13,7 @@ package org.eclipse.syson.util; import java.io.Serializable; +import java.lang.invoke.MethodType; import java.lang.invoke.SerializedLambda; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -72,6 +73,10 @@ * } * *

+ * Overloaded services: if several service methods share the same name, use the factory overloads that also take the + * service class and Java parameter types, for example + * {@code ServiceMethod.of1(EObjectServices.class, EObjectServices::eGet, EObject.class, EStructuralFeature.class)}. + *

* Performance: this uses reflection once per reference at startup to read a method name. The cost is negligible * compared to normal init work. * @@ -80,12 +85,15 @@ */ public final class ServiceMethod { + private final Method declaration; + private final String name; private final int arity; - private ServiceMethod(String name, int arity) { - this.name = name; + private ServiceMethod(Method declaration, int arity) { + this.declaration = declaration; + this.name = declaration.getName(); this.arity = arity; } @@ -98,6 +106,15 @@ public String name() { return this.name; } + /** + * the Java declaration that will be called from AQL. + * + * @return the declaration. + */ + public Method declaration() { + return this.declaration; + } + /** * Build {@code aql:self.method(...)} for the captured service name. *

@@ -149,50 +166,173 @@ public String aql(String var, String... params) { * Instance method with signature {@code R method(T self)}. */ public static ServiceMethod of0(Inst0 ref) { - return new ServiceMethod(methodName(ref), 0); + return new ServiceMethod(method(ref), 0); + } + + /** + * Instance method with signature {@code R method(T self)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on the + * {@code self} type. + */ + public static ServiceMethod of0(Class selfType, Inst0 ref) { + return new ServiceMethod(method(ref, selfType), 0); + } + + /** + * Instance method with signature {@code R method(T self)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on the declaring + * service and {@code self} types. + */ + public static ServiceMethod of0(Class serviceType, Inst0 ref, Class selfType) { + return new ServiceMethod(method(serviceType, ref, selfType), 0); } /** * Instance method with signature {@code R method(T self, P1 p1)}. */ public static ServiceMethod of1(Inst1 ref) { - return new ServiceMethod(methodName(ref), 1); + return new ServiceMethod(method(ref), 1); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on parameter + * types. + */ + public static ServiceMethod of1(Class selfType, Class p1Type, Inst1 ref) { + return new ServiceMethod(method(ref, selfType, p1Type), 1); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1)}. + *

+ * Use this overload when the referenced Java service is overloaded and you need to disambiguate on the declaring + * service and parameter types. + */ + public static ServiceMethod of1(Class serviceType, Inst1 ref, Class selfType, Class p1Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type), 1); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2)}. */ public static ServiceMethod of2(Inst2 ref) { - return new ServiceMethod(methodName(ref), 2); + return new ServiceMethod(method(ref), 2); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2)}. + */ + public static ServiceMethod of2(Class selfType, Class p1Type, Class p2Type, Inst2 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type), 2); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2)}. + */ + public static ServiceMethod of2(Class serviceType, Inst2 ref, Class selfType, Class p1Type, Class p2Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type), 2); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. */ public static ServiceMethod of3(Inst3 ref) { - return new ServiceMethod(methodName(ref), 3); + return new ServiceMethod(method(ref), 3); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. + */ + public static ServiceMethod of3(Class selfType, Class p1Type, Class p2Type, Class p3Type, Inst3 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type), 3); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. + */ + public static ServiceMethod of3(Class serviceType, Inst3 ref, Class selfType, Class p1Type, Class p2Type, + Class p3Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type), 3); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4)}. */ public static ServiceMethod of4(Inst4 ref) { - return new ServiceMethod(methodName(ref), 4); + return new ServiceMethod(method(ref), 4); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4)}. + */ + public static ServiceMethod of4(Class selfType, Class p1Type, Class p2Type, Class p3Type, Class p4Type, + Inst4 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type, p4Type), 4); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4)}. + */ + public static ServiceMethod of4(Class serviceType, Inst4 ref, Class selfType, Class p1Type, + Class p2Type, Class p3Type, Class p4Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type, p4Type), 4); } /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5)}. */ public static ServiceMethod of5(Inst5 ref) { - return new ServiceMethod(methodName(ref), 5); + return new ServiceMethod(method(ref), 5); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5)}. + */ + public static ServiceMethod of5(Class selfType, Class p1Type, Class p2Type, Class p3Type, Class p4Type, + Class p5Type, Inst5 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type), 5); } + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5)}. + */ + // CHECKSTYLE:OFF + public static ServiceMethod of5(Class serviceType, Inst5 ref, Class selfType, Class p1Type, + Class p2Type, Class p3Type, Class p4Type, Class p5Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type), 5); + } + // CHECKSTYLE:ON + /** * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6)}. */ public static ServiceMethod of6(Inst6 ref) { - return new ServiceMethod(methodName(ref), 6); + return new ServiceMethod(method(ref), 6); + } + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6)}. + */ + // CHECKSTYLE:OFF + public static ServiceMethod of6(Class selfType, Class p1Type, Class p2Type, Class p3Type, Class p4Type, + Class p5Type, Class p6Type, Inst6 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type, p6Type), 6); } + // CHECKSTYLE:ON + + /** + * Instance method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6)}. + */ + // CHECKSTYLE:OFF + public static ServiceMethod of6(Class serviceType, Inst6 ref, Class selfType, + Class p1Type, Class p2Type, Class p3Type, Class p4Type, Class p5Type, Class p6Type) { + return new ServiceMethod(method(serviceType, ref, selfType, p1Type, p2Type, p3Type, p4Type, p5Type, p6Type), 6); + } + // CHECKSTYLE:ON // ---------------------- Factories for static methods ---------------------- @@ -200,28 +340,56 @@ public static ServiceMethod of6(Inst6 ServiceMethod ofStatic0(IStat0 ref) { - return new ServiceMethod(methodName(ref), 0); + return new ServiceMethod(method(ref), 0); + } + + /** + * Static method with signature {@code R method(T self)}. + */ + public static ServiceMethod ofStatic0(Class selfType, IStat0 ref) { + return new ServiceMethod(method(ref, selfType), 0); } /** * Static method with signature {@code R method(T self, P1 p1)}. */ public static ServiceMethod ofStatic1(IStat1 ref) { - return new ServiceMethod(methodName(ref), 1); + return new ServiceMethod(method(ref), 1); + } + + /** + * Static method with signature {@code R method(T self, P1 p1)}. + */ + public static ServiceMethod ofStatic1(Class selfType, Class p1Type, IStat1 ref) { + return new ServiceMethod(method(ref, selfType, p1Type), 1); } /** * Static method with signature {@code R method(T self, P1 p1, P2 p2)}. */ public static ServiceMethod ofStatic2(IStat2 ref) { - return new ServiceMethod(methodName(ref), 2); + return new ServiceMethod(method(ref), 2); + } + + /** + * Static method with signature {@code R method(T self, P1 p1, P2 p2)}. + */ + public static ServiceMethod ofStatic2(Class selfType, Class p1Type, Class p2Type, IStat2 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type), 2); } /** * Static method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. */ public static ServiceMethod ofStatic3(IStat3 ref) { - return new ServiceMethod(methodName(ref), 3); + return new ServiceMethod(method(ref), 3); + } + + /** + * Static method with signature {@code R method(T self, P1 p1, P2 p2, P3 p3)}. + */ + public static ServiceMethod ofStatic3(Class selfType, Class p1Type, Class p2Type, Class p3Type, IStat3 ref) { + return new ServiceMethod(method(ref, selfType, p1Type, p2Type, p3Type), 3); } // ---------------------- SAMs for method references ---------------------- @@ -427,14 +595,42 @@ public interface IStat3 extends Serializable { // ---------------------- Lambda -> method name ---------------------- - private static String methodName(Serializable lambdaRef) { + private static Method method(Serializable lambdaRef, Class... expectedParameterTypes) { + try { + SerializedLambda lambda = serializedLambda(lambdaRef); + Class implementationClass = Class.forName(lambda.getImplClass().replace('/', '.'), false, lambdaRef.getClass().getClassLoader()); + MethodType methodType = MethodType.fromMethodDescriptorString(lambda.getImplMethodSignature(), implementationClass.getClassLoader()); + Method method = thisClassMethod(implementationClass, lambda.getImplMethodName(), methodType.parameterArray()); + if (expectedParameterTypes.length > 0 && !Arrays.equals(method.getParameterTypes(), expectedParameterTypes)) { + throw new IllegalArgumentException( + MessageFormat.format("Resolved method {0} has parameters {1} but expected {2}", method, Arrays.toString(method.getParameterTypes()), Arrays.toString(expectedParameterTypes))); + } + return method; + } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | SecurityException | IllegalAccessException e) { + throw new IllegalStateException("Cannot resolve method declaration from lambda", e); + } + } + + private static Method method(Class expectedServiceType, Serializable lambdaRef, Class... expectedParameterTypes) { + Method method = method(lambdaRef, expectedParameterTypes); + if (!expectedServiceType.isAssignableFrom(method.getDeclaringClass())) { + throw new IllegalArgumentException(MessageFormat.format("Resolved method {0} is declared on {1} but expected a service assignable to {2}", method, + method.getDeclaringClass().getName(), expectedServiceType.getName())); + } + return method; + } + + private static SerializedLambda serializedLambda(Serializable lambdaRef) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + Method writeReplace = lambdaRef.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + return (SerializedLambda) writeReplace.invoke(lambdaRef); + } + + private static Method thisClassMethod(Class implementationClass, String methodName, Class[] parameterTypes) throws NoSuchMethodException { try { - Method m = lambdaRef.getClass().getDeclaredMethod("writeReplace"); - m.setAccessible(true); - SerializedLambda sl = (SerializedLambda) m.invoke(lambdaRef); - return sl.getImplMethodName(); - } catch (InvocationTargetException | NoSuchMethodException | SecurityException | IllegalAccessException e) { - throw new IllegalStateException("Cannot resolve method name from lambda", e); + return implementationClass.getDeclaredMethod(methodName, parameterTypes); + } catch (NoSuchMethodException exception) { + return implementationClass.getMethod(methodName, parameterTypes); } } @@ -457,4 +653,3 @@ private void checkArity(String... params) { } } } - diff --git a/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java b/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java index c6ad72f78..64e384b24 100644 --- a/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java +++ b/backend/services/syson-services/src/test/java/org/eclipse/syson/util/ServiceMethodTest.java @@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.lang.reflect.Method; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,23 +27,261 @@ */ public class ServiceMethodTest { + private static final String AQL_VALUE = "'value'"; + + private static final String AQL_INT = "2"; + @Test @DisplayName("GIVEN a service method with arity 0, WHEN an AQL expression is constructed with 0 parameter, THEN the expression is returned") public void givenServiceMethodWithArity0WhenAQLExpressionIsConstructedWith0ParameterThenExpressionIsReturned() { - String expression = ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aqlSelf(); - assertThat(expression).isEqualTo("aql:self.serviceWithArity0()"); - expression = ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aql("var"); - assertThat(expression).isEqualTo("aql:var.serviceWithArity0()"); + String expression = ServiceMethod.of0(ArityFixture::instance0).aqlSelf(); + assertThat(expression).isEqualTo("aql:self.instance0()"); + expression = ServiceMethod.of0(ArityFixture::instance0).aql("var"); + assertThat(expression).isEqualTo("aql:var.instance0()"); } @Test @DisplayName("GIVEN a service method with arity 0, WHEN an AQL expression is constructed with 1 parameter, THEN an exception is thrown") public void givenServiceMethodWithArity0WhenAQLExpressionIsConstructedWith1ParameterThenExceptionIsThrown() { - assertThatThrownBy(() -> ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aqlSelf("param1")).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> ServiceMethod.of0(ServiceMethodTest::serviceWithArity0).aql("var", "param1")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aqlSelf("param1")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aql("var", "param1")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("GIVEN instance service factories, WHEN each arity is used, THEN the expected expressions are returned") + public void givenInstanceServiceFactoriesWhenEachArityIsUsedThenTheExpectedExpressionsAreReturned() throws NoSuchMethodException { + assertThat(ServiceMethod.of0(ArityFixture::instance0).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance0", Object.class)); + assertThat(ServiceMethod.of1(ArityFixture::instance1).aql("ctx", AQL_VALUE)).isEqualTo("aql:ctx.instance1('value')"); + assertThat(ServiceMethod.of2(ArityFixture::instance2).aqlSelf(AQL_VALUE, AQL_INT)).isEqualTo("aql:self.instance2('value',2)"); + assertThat(ServiceMethod.of3(ArityFixture::instance3).aqlSelf(AQL_VALUE, AQL_INT, "true")).isEqualTo("aql:self.instance3('value',2,true)"); + assertThat(ServiceMethod.of4(ArityFixture::instance4).aqlSelf(AQL_VALUE, AQL_INT, "true", "4.0")) + .isEqualTo("aql:self.instance4('value',2,true,4.0)"); + assertThat(ServiceMethod.of5(ArityFixture::instance5).aqlSelf(AQL_VALUE, AQL_INT, "true", "4.0", "5L")) + .isEqualTo("aql:self.instance5('value',2,true,4.0,5L)"); + assertThat(ServiceMethod.of6(ArityFixture::instance6).aqlSelf(AQL_VALUE, AQL_INT, "true", "4.0", "5L", "6.0f")) + .isEqualTo("aql:self.instance6('value',2,true,4.0,5L,6.0f)"); + } + + @Test + @DisplayName("GIVEN typed instance service factories, WHEN each overload is used, THEN the expected declarations are resolved") + public void givenTypedInstanceServiceFactoriesWhenEachOverloadIsUsedThenTheExpectedDeclarationsAreResolved() throws NoSuchMethodException { + assertThat(ServiceMethod.of0(Object.class, ArityFixture::instance0).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance0", Object.class)); + assertThat(ServiceMethod.of0(ArityFixture.class, ArityFixture::instance0, Object.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance0", Object.class)); + assertThat(ServiceMethod.of1(Object.class, String.class, ArityFixture::instance1).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance1", Object.class, String.class)); + assertThat(ServiceMethod.of1(ArityFixture.class, ArityFixture::instance1, Object.class, String.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance1", Object.class, String.class)); + assertThat(ServiceMethod.of2(Object.class, String.class, Integer.class, ArityFixture::instance2).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance2", Object.class, String.class, Integer.class)); + assertThat(ServiceMethod.of2(ArityFixture.class, ArityFixture::instance2, Object.class, String.class, Integer.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance2", Object.class, String.class, Integer.class)); + assertThat(ServiceMethod.of3(Object.class, String.class, Integer.class, Boolean.class, ArityFixture::instance3).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance3", Object.class, String.class, Integer.class, Boolean.class)); + assertThat(ServiceMethod.of3(ArityFixture.class, ArityFixture::instance3, Object.class, String.class, Integer.class, Boolean.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance3", Object.class, String.class, Integer.class, Boolean.class)); + assertThat(ServiceMethod.of4(Object.class, String.class, Integer.class, Boolean.class, Double.class, ArityFixture::instance4).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance4", Object.class, String.class, Integer.class, Boolean.class, Double.class)); + assertThat(ServiceMethod.of4(ArityFixture.class, ArityFixture::instance4, Object.class, String.class, Integer.class, Boolean.class, Double.class).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance4", Object.class, String.class, Integer.class, Boolean.class, Double.class)); + assertThat(ServiceMethod.of5(Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, ArityFixture::instance5).declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance5", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class)); + assertThat(ServiceMethod.of5(ArityFixture.class, ArityFixture::instance5, Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class) + .declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance5", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class)); + assertThat(ServiceMethod.of6(Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class, ArityFixture::instance6) + .declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance6", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class)); + assertThat(ServiceMethod + .of6(ArityFixture.class, ArityFixture::instance6, Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class) + .declaration()) + .isEqualTo(ArityFixture.class.getDeclaredMethod("instance6", Object.class, String.class, Integer.class, Boolean.class, Double.class, Long.class, Float.class)); + } + + @Test + @DisplayName("GIVEN static service factories, WHEN each arity is used, THEN the expected expressions are returned") + public void givenStaticServiceFactoriesWhenEachArityIsUsedThenTheExpectedExpressionsAreReturned() throws NoSuchMethodException { + assertThat(ServiceMethod.ofStatic0(StaticFixture::static0).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static0", Object.class)); + assertThat(ServiceMethod.ofStatic0(Object.class, StaticFixture::static0).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static0", Object.class)); + assertThat(ServiceMethod.ofStatic1(StaticFixture::static1).aqlSelf(AQL_VALUE)).isEqualTo("aql:self.static1('value')"); + assertThat(ServiceMethod.ofStatic1(Object.class, String.class, StaticFixture::static1).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static1", Object.class, String.class)); + assertThat(ServiceMethod.ofStatic2(StaticFixture::static2).aql("ctx", AQL_VALUE, AQL_INT)).isEqualTo("aql:ctx.static2('value',2)"); + assertThat(ServiceMethod.ofStatic2(Object.class, String.class, Integer.class, StaticFixture::static2).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static2", Object.class, String.class, Integer.class)); + assertThat(ServiceMethod.ofStatic3(StaticFixture::static3).aqlSelf(AQL_VALUE, AQL_INT, "true")).isEqualTo("aql:self.static3('value',2,true)"); + assertThat(ServiceMethod.ofStatic3(Object.class, String.class, Integer.class, Boolean.class, StaticFixture::static3).declaration()) + .isEqualTo(StaticFixture.class.getDeclaredMethod("static3", Object.class, String.class, Integer.class, Boolean.class)); + } + + @Test + @DisplayName("GIVEN a service method with another arity, WHEN the wrong number of parameters is provided, THEN an exception is thrown") + public void givenServiceMethodWithAnotherArityWhenWrongNumberOfParametersIsProvidedThenExceptionIsThrown() { + assertThatThrownBy(() -> ServiceMethod.of3(ArityFixture::instance3).aqlSelf(AQL_VALUE, AQL_INT)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("instance3") + .hasMessageContaining("arity of 3"); + } + + @Test + @DisplayName("GIVEN an invalid AQL variable, WHEN aql is called, THEN an exception is thrown") + public void givenInvalidAQLVariableWhenAqlIsCalledThenExceptionIsThrown() { + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aql((String) null)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> ServiceMethod.of0(ArityFixture::instance0).aql("")).isInstanceOf(IllegalArgumentException.class); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + @DisplayName("GIVEN wrong expected parameter types, WHEN the declaration is resolved, THEN an exception is thrown") + public void givenWrongExpectedParameterTypesWhenDeclarationIsResolvedThenAnExceptionIsThrown() { + ServiceMethod.Inst1 ref = ArityFixture::instance1; + + assertThatThrownBy(() -> ServiceMethod.of1(Object.class, Integer.class, (ServiceMethod.Inst1) ref)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("expected"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + @DisplayName("GIVEN a wrong expected service type, WHEN the declaration is resolved, THEN an exception is thrown") + public void givenWrongExpectedServiceTypeWhenDeclarationIsResolvedThenAnExceptionIsThrown() { + ServiceMethod.Inst1 ref = ArityFixture::instance1; + + assertThatThrownBy(() -> ServiceMethod.of1((Class) WrongService.class, (ServiceMethod.Inst1) ref, Object.class, String.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("expected a service assignable"); + } + + @Test + @DisplayName("GIVEN overloaded services, WHEN explicit signature types are provided, THEN the expected declaration is resolved") + public void givenOverloadedServicesWhenExplicitSignatureTypesAreProvidedThenTheExpectedDeclarationIsResolved() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of1(OverloadedService.class, OverloadedService::overloaded, Object.class, Integer.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", Object.class, Integer.class); + assertThat(serviceMethod.name()).isEqualTo("overloaded"); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(Integer.class); + assertThat(serviceMethod.aqlSelf("42")).isEqualTo("aql:self.overloaded(42)"); } - private Object serviceWithArity0(Object self) { - return null; + @Test + @DisplayName("GIVEN overloaded services, WHEN explicit self type is provided, THEN arity-0 overloads can be selected") + public void givenOverloadedServicesWhenExplicitSelfTypeIsProvidedThenArity0OverloadsCanBeSelected() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of0(OverloadedService.class, OverloadedService::overloaded, String.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", String.class); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(String.class); + assertThat(serviceMethod.aqlSelf()).isEqualTo("aql:self.overloaded()"); + } + + @Test + @DisplayName("GIVEN overloaded services, WHEN another explicit self type is provided, THEN the matching arity-0 overload is selected") + public void givenOverloadedServicesWhenAnotherExplicitSelfTypeIsProvidedThenTheMatchingArity0OverloadIsSelected() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of0(OverloadedService.class, OverloadedService::overloaded, Integer.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", Integer.class); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(Integer.class); + assertThat(serviceMethod.aqlSelf()).isEqualTo("aql:self.overloaded()"); + } + + @Test + @DisplayName("GIVEN overloaded services, WHEN another explicit parameter type is provided, THEN the matching arity-1 overload is selected") + public void givenOverloadedServicesWhenAnotherExplicitParameterTypeIsProvidedThenTheMatchingArity1OverloadIsSelected() throws NoSuchMethodException { + ServiceMethod serviceMethod = ServiceMethod.of1(OverloadedService.class, OverloadedService::overloaded, Object.class, String.class); + + Method expectedDeclaration = OverloadedService.class.getDeclaredMethod("overloaded", Object.class, String.class); + assertThat(serviceMethod.declaration()).isEqualTo(expectedDeclaration); + assertThat(serviceMethod.declaration().getReturnType()).isEqualTo(String.class); + assertThat(serviceMethod.aqlSelf(AQL_VALUE)).isEqualTo("aql:self.overloaded('value')"); + } + + /** + * Fixture used to validate factory methods across supported instance arities. + */ + private static final class ArityFixture { + private Object instance0(Object self) { + return self; + } + + private Object instance1(Object self, String p1) { + return p1; + } + + private Object instance2(Object self, String p1, Integer p2) { + return p2; + } + + private Object instance3(Object self, String p1, Integer p2, Boolean p3) { + return p3; + } + + private Object instance4(Object self, String p1, Integer p2, Boolean p3, Double p4) { + return p4; + } + + private Object instance5(Object self, String p1, Integer p2, Boolean p3, Double p4, Long p5) { + return p5; + } + + private Object instance6(Object self, String p1, Integer p2, Boolean p3, Double p4, Long p5, Float p6) { + return p6; + } + } + + /** + * Fixture used to validate factory methods for static service references. + */ + private static final class StaticFixture { + private static Object static0(Object self) { + return self; + } + + private static Object static1(Object self, String p1) { + return p1; + } + + private static Object static2(Object self, String p1, Integer p2) { + return p2; + } + + private static Object static3(Object self, String p1, Integer p2, Boolean p3) { + return p3; + } + } + + /** + * Fixture used to validate service type mismatch checks. + */ + private static final class WrongService { + private WrongService() { + // Utility fixture. + } + } + + /** + * Fixture used to validate overloaded method resolution. + */ + private static final class OverloadedService { + private String overloaded(String self) { + return self; + } + + private Integer overloaded(Integer self) { + return self; + } + + private String overloaded(Object self, String value) { + return value; + } + + private Integer overloaded(Object self, Integer value) { + return value; + } } } diff --git a/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java b/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java index 1889cff1a..572455381 100644 --- a/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java +++ b/backend/tests/syson-tests/src/test/java/org/eclipse/syson/tests/GeneralPurposeTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024, 2025 Obeo. + * Copyright (c) 2024, 2026 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -67,6 +67,8 @@ public class GeneralPurposeTests { private static final String CHECKSTYLE_INTERFACE_IS_TYPE = "@SuppressWarnings(\"checkstyle:InterfaceIsType\")"; + private static final String CHECKSTYLE_RAWTYPES_UNCHECKED = "@SuppressWarnings({ \"rawtypes\", \"unchecked\" })"; + private static final String NON_NLS = "$NON-NLS-"; private static final String BUILDER = "Builder"; @@ -223,6 +225,8 @@ private void testNoSuppressWarnings(int index, String line, Path javaFilePath, L isValidUsage = true; } else if (line.contains(CHECKSTYLE_INTERFACE_IS_TYPE)) { isValidUsage = true; + } else if (line.contains(CHECKSTYLE_RAWTYPES_UNCHECKED)) { + isValidUsage = true; } if (!isValidUsage) { fail(this.createErrorMessage("@SuppressWarnings", javaFilePath, index)); diff --git a/doc/content/modules/developer-guide/pages/index.adoc b/doc/content/modules/developer-guide/pages/index.adoc index c496a342a..1f4d4f290 100644 --- a/doc/content/modules/developer-guide/pages/index.adoc +++ b/doc/content/modules/developer-guide/pages/index.adoc @@ -549,6 +549,7 @@ When to use which call: Type inference: - if the compiler says something like _The type X does not define methodName(Object, Object,...)_, add a type witness to the factory so it can match the real signature. +- if the service method is overloaded, use the factory overload that also takes the service class and Java parameter types to disambiguate the method reference. Examples: @@ -562,6 +563,9 @@ ServiceMethod. of1(DiagramMutation // predicate isActor(Element) ServiceMethod. of0(ModelQueryAQLService::isActor).aqlSelf(); + +// overloaded EObjectServices::eGet(EObject, EStructuralFeature) +ServiceMethod.of1(EObjectServices.class, EObjectServices::eGet, EObject.class, EStructuralFeature.class).aqlSelf(E_STRUCTURAL_FEATURE); ---- [#generate_openapi]