diff --git a/core/src/main/java/io/neba/core/resourcemodels/factory/ClassBasedModelDefinition.java b/core/src/main/java/io/neba/core/resourcemodels/factory/ClassBasedModelDefinition.java index a7fc1e73..12edde8d 100644 --- a/core/src/main/java/io/neba/core/resourcemodels/factory/ClassBasedModelDefinition.java +++ b/core/src/main/java/io/neba/core/resourcemodels/factory/ClassBasedModelDefinition.java @@ -20,6 +20,7 @@ import javax.annotation.Nonnull; +import static io.neba.core.util.Annotations.annotations; import static java.lang.Character.toLowerCase; /** @@ -35,7 +36,7 @@ class ClassBasedModelDefinition implements ModelDefinition { @Nonnull @Override public ResourceModel getResourceModel() { - return c.getAnnotation(ResourceModel.class); + return annotations(c).get(ResourceModel.class); } @Nonnull diff --git a/core/src/main/java/io/neba/core/resourcemodels/factory/ModelFactory.java b/core/src/main/java/io/neba/core/resourcemodels/factory/ModelFactory.java index 83290c4e..9ac2d0c0 100644 --- a/core/src/main/java/io/neba/core/resourcemodels/factory/ModelFactory.java +++ b/core/src/main/java/io/neba/core/resourcemodels/factory/ModelFactory.java @@ -22,21 +22,14 @@ import javax.annotation.Nonnull; import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; +import static io.neba.core.util.Annotations.annotations; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; -import static java.util.Optional.empty; -import static java.util.Optional.of; -import static java.util.Optional.ofNullable; +import static java.util.Optional.*; import static java.util.stream.Collectors.toList; /** @@ -46,9 +39,10 @@ * the models, including injection of OSGi service dependencies via {@link javax.inject.Inject} and {@link io.neba.api.annotations.Filter}. */ class ModelFactory implements ResourceModelFactory { + private static final String SPRING_COMPONENT_STEREOTYPE = "org.springframework.stereotype.Component"; private final Bundle bundle; - private List> modelDefinitions; - private Map, ModelInstantiator> modelMetadata; + private final List> modelDefinitions; + private final Map, ModelInstantiator> modelMetadata; ModelFactory(Bundle bundle) { this.bundle = bundle; @@ -66,8 +60,10 @@ class ModelFactory implements ResourceModelFactory { .flatMap(this::streamUrls) .map(this::urlToClassName) .map(this::loadClass) - .filter(o -> o.map(c -> c.isAnnotationPresent(ResourceModel.class)).orElse(false)) + .filter(Optional::isPresent) .map(Optional::get) + .filter(c -> annotations(c).contains(ResourceModel.class)) + .filter(c -> !annotations(c).containsName(SPRING_COMPONENT_STEREOTYPE)) .map(ClassBasedModelDefinition::new) .distinct() .collect(toList())); diff --git a/core/src/test/java/io/neba/core/resourcemodels/factory/ModelFactoryTest.java b/core/src/test/java/io/neba/core/resourcemodels/factory/ModelFactoryTest.java index 94c67c60..64087f64 100644 --- a/core/src/test/java/io/neba/core/resourcemodels/factory/ModelFactoryTest.java +++ b/core/src/test/java/io/neba/core/resourcemodels/factory/ModelFactoryTest.java @@ -25,17 +25,22 @@ import org.mockito.junit.MockitoJUnitRunner; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; +import org.springframework.stereotype.Component; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.MalformedURLException; import java.net.URL; import java.util.Dictionary; import java.util.Hashtable; +import java.util.List; import java.util.Vector; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.*; /** * @author Olaf Otto @@ -61,32 +66,46 @@ public void setUp() throws Exception { doReturn(this.bundleContext).when(this.bundle).getBundleContext(); - // The actual protocol for OSGi bundles is "bundleresource:", but this protocol is not registered for unit tests. - URL modelClassResource = new URL("file://bundleId.bundleVersion" + "/" + ModelClass.class.getName().replace('.', '/') + ".class"); - URL nonModelClassResource = new URL("file://bundleId.bundleVersion" + "/" + NonModelClass.class.getName().replace('.', '/') + ".class"); + List> modelTypes = asList(ModelClass.class, ModelClassWithMetaAnnotation.class, NonModelClass.class, SpringModelClass.class, SpringModelClassWithMetaAnnotation.class); - Vector vector = new Vector<>(); - vector.add(modelClassResource); - vector.add(nonModelClassResource); + Vector vector = modelTypes.stream() + // The actual protocol for OSGi bundles is "bundleresource:", but this protocol is not registered for unit tests. + .map(cls -> "file://bundleId.bundleVersion" + "/" + cls.getName().replace('.', '/') + ".class") + .map(ModelFactoryTest::toUrl).collect(java.util.stream.Collectors.toCollection(Vector::new)); doReturn(vector.elements()).when(this.bundle).findEntries("/first/package", "*.class", true); - doReturn(ModelClass.class).when(this.bundle).loadClass(ModelClass.class.getName()); - doReturn(NonModelClass.class).when(this.bundle).loadClass(NonModelClass.class.getName()); + modelTypes.forEach(cls -> { + try { + doReturn(cls).when(this.bundle).loadClass(cls.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }); + doAnswer(inv -> inv.getArguments()[0]).when(callback).map(any()); this.testee = new ModelFactory(this.bundle); } @Test + @SuppressWarnings("rawtypes") public void testModelFactoryFindsResourceModel() { assertThat(this.testee.getModelDefinitions()) - .hasSize(1); - assertThat(this.testee.getModelDefinitions().iterator().next().getType()) - .isSameAs(ModelClass.class); - assertThat(this.testee.getModelDefinitions().iterator().next().getName()) - .isEqualTo("modelClass"); - assertThat(this.testee.getModelDefinitions().iterator().next().getResourceModel()) - .isSameAs(ModelClass.class.getAnnotation(ResourceModel.class)); + .hasSize(2); + + assertThat(this.testee.getModelDefinitions()) + .extracting(def -> (Class) def.getType()) + .containsExactly(ModelClass.class, ModelClassWithMetaAnnotation.class); + + assertThat(this.testee.getModelDefinitions()) + .extracting(ModelDefinition::getName) + .containsExactly("modelClass", "modelClassWithMetaAnnotation"); + + assertThat(this.testee.getModelDefinitions()).extracting(ModelDefinition::getResourceModel) + .containsExactly( + ModelClass.class.getAnnotation(ResourceModel.class), + ModelClassWithMetaAnnotation.class.getAnnotation(CustomModelStereotype.class).annotationType().getAnnotation(ResourceModel.class) + ); } @Test @@ -108,10 +127,58 @@ public void testModelDefinitionsAreUnmodifiable() { this.testee.getModelDefinitions().add(mock(ModelDefinition.class)); } + @Test + public void testSpringModelsAreExcluded() { + assertDetectedModelsDoesNotInclude(SpringModelClass.class); + } + + private void assertDetectedModelsDoesNotInclude(Class modelType) { + List> detectedTypes = + this.testee.getModelDefinitions() + .stream() + .map(ModelDefinition::getType) + .collect(toList()); + + assertThat(detectedTypes).doesNotContain(modelType); + } + @ResourceModel("some/type") public static class ModelClass { } + @CustomModelStereotype + public static class ModelClassWithMetaAnnotation { + } + public static class NonModelClass { } + + @Component + @ResourceModel("some/type") + public static class SpringModelClass { + } + + @CustomSpringModelStereotype + @ResourceModel("some/type") + public static class SpringModelClassWithMetaAnnotation { + } + + @ResourceModel("some/type") + @Retention(RetentionPolicy.RUNTIME) + public @interface CustomModelStereotype { + + } + + @Component + @Retention(RetentionPolicy.RUNTIME) + public @interface CustomSpringModelStereotype { + } + + private static URL toUrl(String s) { + try { + return new URL(s); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } } diff --git a/core/src/test/java/org/springframework/stereotype/Component.java b/core/src/test/java/org/springframework/stereotype/Component.java new file mode 100644 index 00000000..2537c065 --- /dev/null +++ b/core/src/test/java/org/springframework/stereotype/Component.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * 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 + * + * https://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.springframework.stereotype; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Component { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + */ + String value() default ""; +}