Skip to content

Commit

Permalink
neba-445 Support mixing neba core and spring -based models in the sam…
Browse files Browse the repository at this point in the history
…e pkg #445

neba-446  Support @ResourceModel as meta-annotation for neba core models #446
  • Loading branch information
olaf-otto committed Jun 26, 2024
1 parent 792252f commit c3dd1fb
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import javax.annotation.Nonnull;

import static io.neba.core.util.Annotations.annotations;
import static java.lang.Character.toLowerCase;

/**
Expand All @@ -35,7 +36,7 @@ class ClassBasedModelDefinition<T> implements ModelDefinition<T> {
@Nonnull
@Override
public ResourceModel getResourceModel() {
return c.getAnnotation(ResourceModel.class);
return annotations(c).get(ResourceModel.class);
}

@Nonnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -46,9 +39,10 @@
* the models, including injection of <em>OSGi service dependencies</em> 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<ModelDefinition<?>> modelDefinitions;
private Map<ModelDefinition<?>, ModelInstantiator<?>> modelMetadata;
private final List<ModelDefinition<?>> modelDefinitions;
private final Map<ModelDefinition<?>, ModelInstantiator<?>> modelMetadata;

ModelFactory(Bundle bundle) {
this.bundle = bundle;
Expand All @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Class<?>> modelTypes = asList(ModelClass.class, ModelClassWithMetaAnnotation.class, NonModelClass.class, SpringModelClass.class, SpringModelClassWithMetaAnnotation.class);

Vector<URL> vector = new Vector<>();
vector.add(modelClassResource);
vector.add(nonModelClassResource);
Vector<URL> 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
Expand All @@ -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<Class<?>> 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);
}
}
}
31 changes: 31 additions & 0 deletions core/src/test/java/org/springframework/stereotype/Component.java
Original file line number Diff line number Diff line change
@@ -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 "";
}

0 comments on commit c3dd1fb

Please sign in to comment.