NamedContextFactoryTests.java

/*
 * Copyright 2012-present 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.cloud.context.named;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.BDDAssertions.then;

/**
 * @author Spencer Gibb
 * @author Tommy Karlsson
 * @author Olga Maciaszek-Sharma
 */
public class NamedContextFactoryTests {

	@Test
	public void testChildContexts() {
		AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
		parent.register(BaseConfig.class);
		parent.refresh();
		testChildContexts(parent);
	}

	@Test
	void testBadThreadContextClassLoader() throws InterruptedException, ExecutionException, TimeoutException {
		AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
		parent.setClassLoader(ClassUtils.getDefaultClassLoader());
		parent.register(BaseConfig.class);
		parent.refresh();

		ExecutorService es = Executors.newSingleThreadExecutor(r -> {
			Thread t = new Thread(r);
			t.setContextClassLoader(new ThrowingClassLoader());
			return t;
		});
		es.submit(() -> this.testChildContexts(parent)).get(5, TimeUnit.SECONDS);

	}

	@Test
	void testGetAnnotatedBeanInstance() {
		AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
		parent.register(BaseConfig.class);
		parent.refresh();
		TestClientFactory factory = new TestClientFactory();
		factory.setApplicationContext(parent);
		factory.setConfigurations(List.of(getSpec("annotated", AnnotatedConfig.class)));

		TestType annotatedBean = factory.getAnnotatedInstance("annotated", ResolvableType.forType(TestType.class),
				TestBean.class);

		assertThat(annotatedBean.value()).isEqualTo(2);
	}

	@Test
	void testNoAnnotatedBeanInstance() {
		AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
		parent.register(BaseConfig.class);
		parent.refresh();
		TestClientFactory factory = new TestClientFactory();
		factory.setApplicationContext(parent);
		factory.setConfigurations(List.of(getSpec("not-annotated", NotAnnotatedConfig.class)));

		TestType annotatedBean = factory.getAnnotatedInstance("not-annotated", ResolvableType.forType(TestType.class),
				TestBean.class);
		assertThat(annotatedBean).isNull();
	}

	@Test
	void testMoreThanOneAnnotatedBeanInstance() {
		AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
		parent.register(BaseConfig.class);
		parent.refresh();
		TestClientFactory factory = new TestClientFactory();
		factory.setApplicationContext(parent);
		factory.setConfigurations(List.of(getSpec("many-annotated", ManyAnnotatedConfig.class)));

		assertThatIllegalStateException().isThrownBy(() -> factory.getAnnotatedInstance("many-annotated",
				ResolvableType.forType(TestType.class), TestBean.class));
	}

	private void testChildContexts(GenericApplicationContext parent) {
		TestClientFactory factory = new TestClientFactory();
		factory.setApplicationContext(parent);
		factory.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class), getSpec("bar", BarConfig.class)));

		Foo foo = factory.getInstance("foo", Foo.class);
		then(foo).as("foo was null").isNotNull();

		Bar bar = factory.getInstance("bar", Bar.class);
		then(bar).as("bar was null").isNotNull();

		then(factory.getContextNames()).as("context names not exposed").contains("foo", "bar");

		Bar foobar = factory.getInstance("foo", Bar.class);
		then(foobar).as("bar was not null").isNull();

		Baz fooBaz = factory.getInstance("foo", Baz.class);
		then(fooBaz).as("fooBaz was null").isNotNull();

		Object fooContainerFoo = factory.getInstance("foo", Container.class, Foo.class);
		then(fooContainerFoo).as("fooContainerFoo was null").isNotNull();

		Object fooContainerBar = factory.getInstance("foo", Container.class, Bar.class);
		then(fooContainerBar).as("fooContainerBar was not null").isNull();

		Object barContainerBar = factory.getInstance("bar", Container.class, Bar.class);
		then(barContainerBar).as("barContainerBar was null").isNotNull();

		Map<String, Baz> fooBazes = factory.getInstances("foo", Baz.class);
		then(fooBazes).as("fooBazes was null").isNotNull();
		then(fooBazes.size()).as("fooBazes size was wrong").isEqualTo(1);

		Map<String, Baz> barBazes = factory.getInstances("bar", Baz.class);
		then(barBazes).as("barBazes was null").isNotNull();
		then(barBazes.size()).as("barBazes size was wrong").isEqualTo(2);

		// get the contexts before destroy() to verify these are the old ones
		GenericApplicationContext fooContext = factory.getContext("foo");
		GenericApplicationContext barContext = factory.getContext("bar");

		then(fooContext.getClassLoader()).as("foo context classloader does not match parent")
			.isSameAs(parent.getClassLoader());

		then(fooContext.getBeanFactory().getBeanClassLoader())
			.as("foo context bean factory classloader does not match parent")
			.isSameAs(parent.getBeanFactory().getBeanClassLoader());

		assertThat(fooContext).hasFieldOrPropertyWithValue("customClassLoader", true);

		factory.destroy();

		then(fooContext.isActive()).as("foo context wasn't closed").isFalse();

		then(barContext.isActive()).as("bar context wasn't closed").isFalse();
	}

	private TestSpec getSpec(String name, Class<?> configClass) {
		return new TestSpec(name, new Class[] { configClass });
	}

	static class ThrowingClassLoader extends ClassLoader {

		ThrowingClassLoader() {
			super(null);
		}

		@Override
		public Class<?> loadClass(String name) throws ClassNotFoundException {
			throw new ClassNotFoundException(name);
		}

	}

	static class TestClientFactory extends NamedContextFactory<TestSpec> {

		TestClientFactory() {
			super(TestSpec.class, "testfactory", "test.client.name");
		}

	}

	static class TestSpec implements NamedContextFactory.Specification {

		private String name;

		private Class<?>[] configuration;

		TestSpec() {
		}

		TestSpec(String name, Class<?>[] configuration) {
			this.name = name;
			this.configuration = configuration;
		}

		@Override
		public String getName() {
			return this.name;
		}

		public void setName(String name) {
			this.name = name;
		}

		@Override
		public Class<?>[] getConfiguration() {
			return this.configuration;
		}

		public void setConfiguration(Class<?>[] configuration) {
			this.configuration = configuration;
		}

	}

	static class BaseConfig {

		@Bean
		Baz baz1() {
			return new Baz();
		}

	}

	static class Baz {

	}

	@ConditionalOnClass(Object.class)
	static class FooConfig {

		@Bean
		Foo foo() {
			return new Foo();
		}

		@Bean
		Container<Foo> fooContainer() {
			return new Container<>(new Foo());
		}

	}

	static class Foo {

	}

	static class BarConfig {

		@Bean
		Bar bar() {
			return new Bar();
		}

		@Bean
		Baz baz2() {
			return new Baz();
		}

		@Bean
		Container<Bar> barContainer() {
			return new Container<>(new Bar());
		}

	}

	static class Bar {

	}

	record Container<T>(T item) {

	}

	static class AnnotatedConfig {

		@Bean
		TestType test1() {
			return new TestType(1);
		}

		@TestBean
		@Bean
		TestType test2() {
			return new TestType(2);
		}

		@TestBean
		@Bean
		Bar bar() {
			return new Bar();
		}

	}

	static class NotAnnotatedConfig {

		@Bean
		TestType test1() {
			return new TestType(1);
		}

		@Bean
		TestType test2() {
			return new TestType(2);
		}

		@TestBean
		@Bean
		Bar bar() {
			return new Bar();
		}

	}

	static class ManyAnnotatedConfig {

		@TestBean
		@Bean
		TestType test1() {
			return new TestType(1);
		}

		@TestBean
		@Bean
		TestType test2() {
			return new TestType(2);
		}

		@Bean
		Bar bar() {
			return new Bar();
		}

	}

	record TestType(int value) {
	}

	@Target({ ElementType.TYPE, ElementType.METHOD })
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	@Inherited
	@interface TestBean {

	}

}