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..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 @@ -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,35 @@ 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; + return validMethods.put(name.get(), method) != null; + }) + .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 +207,39 @@ 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); + return validMethods.put(name, method) != null; + } 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..05e0a2ad --- /dev/null +++ b/graphql-builder/src/main/java/com/phocassoftware/graphql/builder/exceptions/DuplicateMethodNameException.java @@ -0,0 +1,23 @@ +/* + * 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; + +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..65d82171 --- /dev/null +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/DuplicateMethodNameTest.java @@ -0,0 +1,44 @@ +/* + * 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; +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..12b742db --- /dev/null +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/invalid/House.java @@ -0,0 +1,30 @@ +/* + * 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; + +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..5af5a9fe --- /dev/null +++ b/graphql-builder/src/test/java/com/phocassoftware/graphql/builder/duplicateMethodNames/valid/Hotel.java @@ -0,0 +1,32 @@ +/* + * 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; +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; + } +}