From ae7ea1eaedac12ef21910604e982e1863c251493 Mon Sep 17 00:00:00 2001 From: callum-rutledge Date: Mon, 29 Sep 2025 15:18:03 +1300 Subject: [PATCH 1/3] Error if an Entity is overloading a getter method --- .../graphql/builder/ObjectEntity.java | 6 +- .../graphql/builder/TypeBuilder.java | 103 ++++++++++-------- .../DuplicateMethodNameException.java | 12 ++ .../builder/DuplicateMethodNameTest.java | 33 ++++++ .../duplicateMethodNames/invalid/House.java | 19 ++++ .../duplicateMethodNames/valid/Hotel.java | 21 ++++ 6 files changed, 146 insertions(+), 48 deletions(-) create mode 100644 graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java create mode 100644 graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java create mode 100644 graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java create mode 100644 graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java diff --git a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/ObjectEntity.java b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/ObjectEntity.java index bf0fbdeb..2102f16b 100644 --- a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/ObjectEntity.java +++ b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/ObjectEntity.java @@ -65,11 +65,7 @@ public InputTypeBuilder buildResolver() { @Override protected GraphQLNamedOutputType buildType() { - try { - return typeBuilder.buildType(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } + return typeBuilder.buildType(); } @Override diff --git a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java index 098059a2..26036e93 100644 --- a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java +++ b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java @@ -14,14 +14,19 @@ import com.phocassoftware.graphql.builder.annotations.Entity; import com.phocassoftware.graphql.builder.annotations.GraphQLDescription; import com.phocassoftware.graphql.builder.annotations.GraphQLIgnore; +import com.phocassoftware.graphql.builder.exceptions.DuplicateMethodNameException; import graphql.schema.FieldCoordinates; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLObjectType.Builder; import graphql.schema.GraphQLTypeReference; + +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; public abstract class TypeBuilder { @@ -33,7 +38,7 @@ public TypeBuilder(EntityProcessor entityProcessor, TypeMeta meta) { this.meta = meta; } - public GraphQLNamedOutputType buildType() throws ReflectiveOperationException { + public GraphQLNamedOutputType buildType() { Builder graphType = GraphQLObjectType.newObject(); String typeName = EntityUtil.getName(meta); graphType.name(typeName); @@ -154,8 +159,7 @@ private void addInterface(Builder graphType, GraphQLInterfaceType.Builder interf interfaceBuilder.withInterface(interfaceName); } - protected abstract void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) - throws ReflectiveOperationException; + protected abstract void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder); public static class ObjectType extends TypeBuilder { @@ -164,22 +168,37 @@ public ObjectType(EntityProcessor entityProcessor, TypeMeta meta) { } @Override - protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) - throws ReflectiveOperationException { + protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) { var type = meta.getType(); - for (Method method : type.getMethods()) { - try { + var methods = type.getMethods(); + + var validMethods = new HashMap(); + + var duplicates = Arrays + .stream(methods) + .filter(method -> { var name = EntityUtil.getter(method); - if (name.isEmpty()) { - continue; - } - var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name.get()), meta, method); + if (name.isEmpty()) return false; + var exists = validMethods.containsKey(name.get()); + validMethods.put(name.get(), method); + return exists; + }) + .map(Method::getName) + .toArray(String[]::new); + + if (duplicates.length > 0) { + throw new DuplicateMethodNameException(typeName, duplicates); + } + + validMethods.forEach((name, method) -> { + try { + var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name), meta, method); graphType.field(f); interfaceBuilder.field(f); } catch (RuntimeException e) { throw new RuntimeException("Failed to process method " + method, e); } - } + }); } } @@ -190,43 +209,41 @@ public Record(EntityProcessor entityProcessor, TypeMeta meta) { } @Override - protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) - throws ReflectiveOperationException { + protected void processFields(String typeName, Builder graphType, graphql.schema.GraphQLInterfaceType.Builder interfaceBuilder) { var type = meta.getType(); - for (var field : type.getDeclaredFields()) { + var methods = Arrays + .stream(type.getDeclaredFields()) + .filter(field -> !field.isSynthetic()) + .filter(field -> !field.getDeclaringClass().equals(Object.class)) + .filter(field -> !field.isAnnotationPresent(GraphQLIgnore.class)) + .filter(field -> !Modifier.isAbstract(field.getModifiers())) + .filter(field -> !field.getDeclaringClass().isInterface()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .toList(); + var validMethods = new HashMap(); + var duplicates = methods.stream().filter(field -> { try { - if (field.isSynthetic()) { - continue; - } - if (field.getDeclaringClass().equals(Object.class)) { - continue; - } - if (field.isAnnotationPresent(GraphQLIgnore.class)) { - continue; - } - // will also be on implementing class - if (Modifier.isAbstract(field.getModifiers()) || field.getDeclaringClass().isInterface()) { - continue; - } - if (Modifier.isStatic(field.getModifiers())) { - continue; - } else { - var method = type.getMethod(field.getName()); - if (method.isAnnotationPresent(GraphQLIgnore.class)) { - continue; - } - - var name = EntityUtil.getName(field.getName(), field, method); - - var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name), meta, method); - graphType.field(f); - interfaceBuilder.field(f); - } - } catch (RuntimeException e) { + var method = type.getMethod(field.getName()); + if (method.isAnnotationPresent(GraphQLIgnore.class)) return false; + var name = EntityUtil.getName(field.getName(), field, method); + var duplicate = validMethods.containsKey(name); + validMethods.put(name, method); + return duplicate; + } catch (NoSuchMethodException e) { throw new RuntimeException("Failed to process method " + field, e); } + }).map(Field::getName).toArray(String[]::new); + + if (duplicates.length > 0) { + throw new DuplicateMethodNameException(typeName, duplicates); } + + validMethods.forEach((name, method) -> { + var f = entityProcessor.getMethodProcessor().process(null, FieldCoordinates.coordinates(typeName, name), meta, method); + graphType.field(f); + interfaceBuilder.field(f); + }); } } } diff --git a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java new file mode 100644 index 00000000..d7ebf75f --- /dev/null +++ b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java @@ -0,0 +1,12 @@ +package com.phocassoftware.graphql.builder.exceptions; + +import java.util.Arrays; + +public class DuplicateMethodNameException extends RuntimeException { + + private static final String MESSAGE_TEMPLATE = "Class: %s has overloaded method(s): %s. GraphQLBuilder does not support overloading methods"; + + public DuplicateMethodNameException(String typeName, String... methodNames) { + super(MESSAGE_TEMPLATE.formatted(typeName, Arrays.toString(methodNames))); + } +} diff --git a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java new file mode 100644 index 00000000..002fff8a --- /dev/null +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java @@ -0,0 +1,33 @@ +package com.phocassoftware.graphql.builder; + +import com.phocassoftware.graphql.builder.exceptions.DuplicateMethodNameException; +import graphql.GraphQL; +import graphql.introspection.IntrospectionWithDirectivesSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DuplicateMethodNameTest { + + @Test + void testFailsToBuildSchema() { + assertThrows( + DuplicateMethodNameException.class, + () -> GraphQL + .newGraphQL( + new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.phocassoftware.graphql.builder.duplicateMethodNames.invalid")) + ) + .build() + ); + } + + @Test + void testSucceedsWithOverloadsAnnotatedWithIgnore() { + var schema = GraphQL + .newGraphQL(new IntrospectionWithDirectivesSupport().apply(SchemaBuilder.build("com.phocassoftware.graphql.builder.duplicateMethodNames.valid"))) + .build(); + assertNotNull(schema); + } + +} diff --git a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java new file mode 100644 index 00000000..770e9cd1 --- /dev/null +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java @@ -0,0 +1,19 @@ +package com.phocassoftware.graphql.builder.duplicateMethodNames.invalid; + +import com.phocassoftware.graphql.builder.annotations.Query; + +public class House { + + @Query + public static House getHouse() { + return new House(); + } + + public String getStreetAddress() { + return "House Name"; + } + + public String getStreetAddress(String append) { + return getStreetAddress() + append; + } +} diff --git a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java new file mode 100644 index 00000000..b504f317 --- /dev/null +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java @@ -0,0 +1,21 @@ +package com.phocassoftware.graphql.builder.duplicateMethodNames.valid; + +import com.phocassoftware.graphql.builder.annotations.GraphQLIgnore; +import com.phocassoftware.graphql.builder.annotations.Query; + +public class Hotel { + + @Query + public static Hotel getHotel() { + return new Hotel(); + } + + @GraphQLIgnore + public String getStreetAddress() { + return "House Name"; + } + + public String getStreetAddress(String append) { + return getStreetAddress() + append; + } +} From f7a68addc42ceb9179a542e541ee9117e2f65700 Mon Sep 17 00:00:00 2001 From: callum-rutledge Date: Mon, 29 Sep 2025 15:20:02 +1300 Subject: [PATCH 2/3] cleanup --- .../com/phocassoftware/graphql/builder/TypeBuilder.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java index 26036e93..5497fa28 100644 --- a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java +++ b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/TypeBuilder.java @@ -179,9 +179,7 @@ protected void processFields(String typeName, Builder graphType, graphql.schema. .filter(method -> { var name = EntityUtil.getter(method); if (name.isEmpty()) return false; - var exists = validMethods.containsKey(name.get()); - validMethods.put(name.get(), method); - return exists; + return validMethods.put(name.get(), method) != null; }) .map(Method::getName) .toArray(String[]::new); @@ -227,9 +225,7 @@ protected void processFields(String typeName, Builder graphType, graphql.schema. var method = type.getMethod(field.getName()); if (method.isAnnotationPresent(GraphQLIgnore.class)) return false; var name = EntityUtil.getName(field.getName(), field, method); - var duplicate = validMethods.containsKey(name); - validMethods.put(name, method); - return duplicate; + return validMethods.put(name, method) != null; } catch (NoSuchMethodException e) { throw new RuntimeException("Failed to process method " + field, e); } From 32b5eb4c18ffe593eb26f087b3e7e25ed5ad85b3 Mon Sep 17 00:00:00 2001 From: callum-rutledge Date: Mon, 29 Sep 2025 15:23:03 +1300 Subject: [PATCH 3/3] License --- .../exceptions/DuplicateMethodNameException.java | 11 +++++++++++ .../graphql/builder/DuplicateMethodNameTest.java | 11 +++++++++++ .../builder/duplicateMethodNames/invalid/House.java | 11 +++++++++++ .../builder/duplicateMethodNames/valid/Hotel.java | 11 +++++++++++ 4 files changed, 44 insertions(+) diff --git a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java index d7ebf75f..05e0a2ad 100644 --- a/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java +++ b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java @@ -1,3 +1,14 @@ +/* + * 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 com.phocassoftware.graphql.builder.exceptions; import java.util.Arrays; diff --git a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java index 002fff8a..65d82171 100644 --- a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java @@ -1,3 +1,14 @@ +/* + * 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 com.phocassoftware.graphql.builder; import com.phocassoftware.graphql.builder.exceptions.DuplicateMethodNameException; diff --git a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java index 770e9cd1..12b742db 100644 --- a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java @@ -1,3 +1,14 @@ +/* + * 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 com.phocassoftware.graphql.builder.duplicateMethodNames.invalid; import com.phocassoftware.graphql.builder.annotations.Query; diff --git a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java index b504f317..5af5a9fe 100644 --- a/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java @@ -1,3 +1,14 @@ +/* + * 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 com.phocassoftware.graphql.builder.duplicateMethodNames.valid; import com.phocassoftware.graphql.builder.annotations.GraphQLIgnore;