Skip to content

Commit f529bdc

Browse files
committed
Merge branch 'release/1.0.0'
2 parents 5e775db + 30c9150 commit f529bdc

14 files changed

+264
-113
lines changed

.github/ISSUE_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Please use one of the following templates:
22

3-
https://github.com/GITHUB_ORGANIZATION/GITHUB_REPOSITORY/issues/new/choose
3+
https://github.com/toolisticon/spring-conditions/issues/new/choose

.github/ISSUE_TEMPLATE/1_bug_report.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name: 'Bug report'
33
about: 'Report a bug'
44
title:
5+
type: 'Bug'
56
labels: 'Type: bug'
67

78
---

.github/ISSUE_TEMPLATE/2_feature_request.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name: 'Feature request'
33
about: 'Suggest a new feature or enhancement'
44
title:
55
labels: 'Type: enhancement'
6+
type: 'Feature'
67
assignees:
78

89
---

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ updates:
55
schedule:
66
interval: daily
77
open-pull-requests-limit: 10
8-
default-labels:
8+
labels:
99
- "Type: dependencies"

README.md

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,24 @@
1-
# BASE_ARTIFACT_ID
1+
# spring-conditions
22

3-
Template repository for usage in organizations: toolisticon, holunda-io, holixon...
3+
Provides useful Spring-Boot conditions.
44

55
[![incubating](https://img.shields.io/badge/lifecycle-INCUBATING-orange.svg)](https://github.com/holisticon#open-source-lifecycle)
6-
[![Build Status](https://github.com/GITHUB_ORGANIZATION/GITHUB_REPOSITORY/workflows/Development%20branches/badge.svg)](https://github.com/GITHUB_ORGANIZATION/GITHUB_REPOSITORY/actions)
6+
[![Build Status](https://github.com/toolisticon/spring-conditions/workflows/Development%20branches/badge.svg)](https://github.com/toolisticon/spring-conditions/actions)
77
[![sponsored](https://img.shields.io/badge/sponsoredBy-Holisticon-RED.svg)](https://holisticon.de/)
8-
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/BASE_GROUP_ID/BASE_ARTIFACT_ID/badge.svg)](https://maven-badges.herokuapp.com/maven-central/BASE_GROUP_ID/BASE_ARTIFACT_ID)
8+
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.toolisticon.spring/spring-conditions/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.toolisticon.spring/spring-conditions)
99

10-
This repository is a **template repository** designed to be a template for the next project.
10+
## Supported conditions
1111

12-
## How to use
12+
- `@ConditionalOnMissingQualifiedBean`
1313

14-
* create a new repo on github (can be in any organization). Choose this project as template repository. Copy all branches, so the `master`exists in your repo (for the github actions)
15-
* on the command line: clone your new repo locally
16-
* in the `setup.sh` script: set your organization, repository and base package
17-
* run the `setup.sh` script, all placeholders are filled with your information
18-
* delete the setup-script
19-
* Update the `README.md`
20-
* in the `developers` section of the `pom.xml`: mention yourself ... it is your project.
14+
## Usage
2115

22-
## Things to change after usage of template
16+
```xml
2317

24-
To change the following values, modify the placeholders in `setup.sh` and run it.
25-
This is a one-time operation, you can safely delete the `setup.sh` file afterwards.
26-
27-
Of course, you can also edit manually .... and do not forget to change this `README.md` with YOUR project specific information :-).
28-
29-
### Maven pom.xml
30-
31-
* Maven coordinates: `groupId`, `artifactId` and `version`
32-
* Main description: `name`, `url`, `description`
33-
* SCM: `connection`, `url`, `developerConnection`
34-
35-
### Issue Template
36-
37-
* correct the URL to repo
38-
39-
### Issue Labels
40-
41-
* Check the release-notes.yml for details, but create the following labels: Type: dependencies, Type: bug, Type: documentation, Type: question, Type: enhancement
18+
<dependency>
19+
<groupId>io.toolisticon.spring</groupId>
20+
<artifactId>spring-conditions</artifactId>
21+
<version>1.0.0</version>
22+
</dependency>
4223

24+
```

pom.xml

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,51 @@
99
<relativePath/>
1010
</parent>
1111

12-
<groupId>BASE_GROUP_ID</groupId>
13-
<artifactId>BASE_ARTIFACT_ID</artifactId>
14-
<version>0.0.1-SNAPSHOT</version>
12+
<groupId>io.toolisticon.spring</groupId>
13+
<artifactId>spring-conditions</artifactId>
14+
<version>1.0.0</version>
1515
<name>${project.artifactId}</name>
16-
<description>BASE_DESCRIPTION</description>
17-
<url>https://github.com/GITHUB_ORGANIZATION/GITHUB_REPOSITORY/</url>
16+
<description>spring-conditions</description>
17+
<url>https://github.com/toolisticon/spring-conditions/</url>
1818

19-
<properties>
20-
<!-- TEST -->
21-
<mockito.version>5.4.0</mockito.version>
22-
<assertj.version>3.27.3</assertj.version>
23-
</properties>
19+
<dependencyManagement>
20+
<dependencies>
21+
<dependency>
22+
<groupId>org.springframework.boot</groupId>
23+
<artifactId>spring-boot-dependencies</artifactId>
24+
<version>3.4.3</version>
25+
<scope>import</scope>
26+
<type>pom</type>
27+
</dependency>
28+
</dependencies>
29+
</dependencyManagement>
2430

2531
<dependencies>
26-
<!-- KOTLIN -->
32+
<!-- SPRING -->
2733
<dependency>
28-
<groupId>org.jetbrains.kotlin</groupId>
29-
<artifactId>kotlin-stdlib-jdk8</artifactId>
34+
<groupId>org.springframework</groupId>
35+
<artifactId>spring-context</artifactId>
3036
</dependency>
3137
<dependency>
32-
<groupId>org.jetbrains.kotlin</groupId>
33-
<artifactId>kotlin-reflect</artifactId>
38+
<groupId>org.springframework.boot</groupId>
39+
<artifactId>spring-boot-autoconfigure</artifactId>
3440
</dependency>
3541
<dependency>
36-
<groupId>io.github.oshai</groupId>
37-
<artifactId>kotlin-logging-jvm</artifactId>
42+
<groupId>org.slf4j</groupId>
43+
<artifactId>slf4j-api</artifactId>
44+
</dependency>
45+
46+
<!-- KOTLIN -->
47+
<dependency>
48+
<groupId>org.jetbrains.kotlin</groupId>
49+
<artifactId>kotlin-stdlib-jdk8</artifactId>
3850
</dependency>
3951

4052
<!-- TEST -->
4153
<dependency>
4254
<groupId>org.mockito.kotlin</groupId>
4355
<artifactId>mockito-kotlin</artifactId>
44-
<version>${mockito.version}</version>
56+
<version>5.4.0</version>
4557
<scope>test</scope>
4658
</dependency>
4759
<dependency>
@@ -57,7 +69,7 @@
5769
<dependency>
5870
<groupId>org.assertj</groupId>
5971
<artifactId>assertj-core</artifactId>
60-
<version>${assertj.version}</version>
72+
<version>3.27.3</version>
6173
<scope>test</scope>
6274
</dependency>
6375
<dependency>
@@ -98,9 +110,9 @@
98110
</licenses>
99111

100112
<scm>
101-
<connection>scm:git:[email protected]:GITHUB_ORGANIZATION/GITHUB_REPOSITORY.git</connection>
102-
<url>scm:git:[email protected]:GITHUB_ORGANIZATION/GITHUB_REPOSITORY.git</url>
103-
<developerConnection>scm:git:[email protected]:GITHUB_ORGANIZATION/GITHUB_REPOSITORY.git</developerConnection>
113+
<connection>scm:git:[email protected]:toolisticon/spring-conditions.git</connection>
114+
<url>scm:git:[email protected]:toolisticon/spring-conditions.git</url>
115+
<developerConnection>scm:git:[email protected]:toolisticon/spring-conditions.git</developerConnection>
104116
<tag>HEAD</tag>
105117
</scm>
106118

setup.sh

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package io.toolisticon.spring.condition
2+
3+
import org.slf4j.LoggerFactory
4+
import org.springframework.beans.factory.NoSuchBeanDefinitionException
5+
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition
6+
import org.springframework.beans.factory.annotation.Qualifier
7+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
8+
import org.springframework.beans.factory.support.AbstractBeanDefinition
9+
import org.springframework.beans.factory.support.RootBeanDefinition
10+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome
11+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition
12+
import org.springframework.context.annotation.ConditionContext
13+
import org.springframework.context.annotation.ConfigurationCondition
14+
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase
15+
import org.springframework.core.annotation.AnnotationUtils
16+
import org.springframework.core.type.AnnotatedTypeMetadata
17+
import org.springframework.util.ObjectUtils
18+
import java.lang.invoke.MethodHandles
19+
import java.util.stream.Stream
20+
21+
/**
22+
* Inspired by [org.axonframework.springboot.util.AbstractQualifiedBeanCondition]
23+
* Abstract implementations for conditions that match against the availability of beans of a specific type with a
24+
* given qualifier.
25+
26+
* Initialize the condition, looking for properties on a given annotation
27+
*
28+
* @param annotationName The fully qualified class name of the annotation to find attributes on.
29+
* @param beanClassAttribute The attribute containing the bean class.
30+
* @param qualifierAttribute The attribute containing the qualifier.
31+
*/
32+
abstract class AbstractQualifiedBeanCondition(
33+
private val annotationName: String,
34+
private val beanClassAttribute: String,
35+
private val qualifierAttribute: String
36+
) : SpringBootCondition(), ConfigurationCondition {
37+
38+
companion object {
39+
private val logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass())
40+
}
41+
42+
override fun getConfigurationPhase(): ConfigurationPhase {
43+
return ConfigurationPhase.REGISTER_BEAN
44+
}
45+
46+
override fun getMatchOutcome(context: ConditionContext, metadata: AnnotatedTypeMetadata): ConditionOutcome {
47+
48+
val annotationAttributes = metadata.getAllAnnotationAttributes(annotationName, true)!!
49+
val beanType = annotationAttributes.getFirst(beanClassAttribute) as String
50+
val qualifierAttr = annotationAttributes.getFirst(qualifierAttribute) as String
51+
52+
val qualifier: String?
53+
val qualifierMatch: Boolean
54+
55+
if (qualifierAttr.startsWith("!")) {
56+
qualifier = qualifierAttr.substring(1)
57+
qualifierMatch = false
58+
} else {
59+
qualifier = qualifierAttr
60+
qualifierMatch = true
61+
}
62+
63+
val qualifiers = qualifier.split(",".toRegex()).toTypedArray()
64+
val conditionalClass: Class<*> = try {
65+
Class.forName(beanType)
66+
} catch (e: ClassNotFoundException) {
67+
val failureMessage = String.format(
68+
"Failed to extract a class instance for fully qualified class name [%s]",
69+
beanType
70+
)
71+
logger.warn(failureMessage, e)
72+
return ConditionOutcome(false, failureMessage)
73+
}
74+
val bf = context.beanFactory!!
75+
val anyMatch = Stream.of(*bf.getBeanNamesForType(conditionalClass))
76+
.anyMatch { beanName: String -> qualifierMatch == isOneMatching(beanName, bf, qualifiers) }
77+
val message = if (anyMatch) String.format(
78+
"Match found for class [%s] and qualifier [%s]",
79+
conditionalClass,
80+
qualifier
81+
) else String.format("No match found for class [%s] and qualifier [%s]", conditionalClass, qualifier)
82+
return buildOutcome(anyMatch, message)
83+
}
84+
85+
private fun isOneMatching(beanName: String, bf: ConfigurableListableBeanFactory, qualifiers: Array<String>): Boolean {
86+
for (qualifier in qualifiers) {
87+
if (isQualifierMatch(beanName, bf, qualifier)) {
88+
return true
89+
}
90+
}
91+
return false
92+
}
93+
94+
protected abstract fun buildOutcome(anyMatch: Boolean, message: String): ConditionOutcome
95+
96+
}
97+
98+
/**
99+
* Inspired by org.axonframework.spring.SpringUtils.isQualifierMatch
100+
*/
101+
fun isQualifierMatch(beanName: String, beanFactory: ConfigurableListableBeanFactory, qualifier: String): Boolean {
102+
return if (!beanFactory.containsBean(beanName)) {
103+
false
104+
} else {
105+
try {
106+
val bd = beanFactory.getMergedBeanDefinition(beanName)
107+
if (bd is AnnotatedBeanDefinition) {
108+
val factoryMethodMetadata = bd.factoryMethodMetadata
109+
val qualifierAttributes = factoryMethodMetadata!!.getAnnotationAttributes(Qualifier::class.java.name)
110+
if (qualifierAttributes != null && qualifier == qualifierAttributes["value"]) {
111+
return true
112+
}
113+
}
114+
if (bd is AbstractBeanDefinition) {
115+
val candidate = bd.getQualifier(Qualifier::class.java.name)
116+
if (candidate != null && qualifier == candidate.getAttribute("value") || qualifier == beanName || ObjectUtils.containsElement(
117+
beanFactory.getAliases(
118+
beanName
119+
), qualifier
120+
)
121+
) {
122+
return true
123+
}
124+
}
125+
var targetAnnotation: Qualifier?
126+
if (bd is RootBeanDefinition) {
127+
val factoryMethod = bd.resolvedFactoryMethod
128+
if (factoryMethod != null) {
129+
targetAnnotation = AnnotationUtils.getAnnotation(factoryMethod, Qualifier::class.java)
130+
if (targetAnnotation != null) {
131+
return qualifier == targetAnnotation.value
132+
}
133+
}
134+
}
135+
val beanType = beanFactory.getType(beanName)
136+
if (beanType != null) {
137+
targetAnnotation = AnnotationUtils.getAnnotation(beanType, Qualifier::class.java)
138+
if (targetAnnotation != null) {
139+
return qualifier == targetAnnotation.value
140+
}
141+
}
142+
} catch (_: NoSuchBeanDefinitionException) {
143+
}
144+
false
145+
}
146+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.toolisticon.spring.condition
2+
3+
import org.springframework.context.annotation.Conditional
4+
import kotlin.reflect.KClass
5+
6+
/**
7+
* Inspired by org.axonframework.springboot.util.ConditionalOnMissingQualifiedBean
8+
*
9+
* {@link Conditional} that only matches when for the specified bean class in the {@link BeanFactory} there is an
10+
* instance which has the given {@code qualifier} set on it.
11+
* <p>
12+
* The condition can only match the bean definitions that have been processed by the
13+
* application context so far and, as such, it is strongly recommended to use this
14+
* condition on auto-configuration classes only. If a candidate bean may be created by
15+
* another auto-configuration, make sure that the one using this condition runs after.
16+
*
17+
* @author Steven van Beelen
18+
* @author adopted by Simon Zambrovski
19+
*/
20+
@Target(
21+
AnnotationTarget.ANNOTATION_CLASS,
22+
AnnotationTarget.CLASS,
23+
AnnotationTarget.FUNCTION,
24+
AnnotationTarget.PROPERTY_GETTER,
25+
AnnotationTarget.PROPERTY_SETTER
26+
)
27+
@Retention(AnnotationRetention.RUNTIME)
28+
@MustBeDocumented
29+
@Conditional(
30+
OnMissingQualifiedBeanCondition::class
31+
)
32+
annotation class ConditionalOnMissingQualifiedBean(
33+
/**
34+
* The class type of bean that should be checked. The condition matches if the class specified is contained in the
35+
* [ApplicationContext], together with the specified `qualifier`.
36+
*/
37+
val beanClass: KClass<*> = Any::class,
38+
/**
39+
* The qualifier which all instances of the given {code beanClass} in the [ApplicationContext] will be matched
40+
* for. One may indicate that a qualifier should *not* be present by prefixing it with `!`, e.g:
41+
* `qualifier = "!unqualified"`.
42+
*
43+
*
44+
* Multiple qualifiers may be provided, separated with a comma (`,`). In that case, a bean matches when it is
45+
* assigned one of the given qualifiers.
46+
*/
47+
val qualifier: String
48+
)

0 commit comments

Comments
 (0)