Compare commits
No commits in common. 'main' and '2.4.x' have entirely different histories.
@ -1,7 +0,0 @@
|
|||||||
# .git-blame-ignore-revs
|
|
||||||
# Reformat code following spring-javaformat upgrade
|
|
||||||
df5898a1464112f185d295d585740de696934a12
|
|
||||||
c4de86c244acdcff69ed0aecacd254399be79ce2
|
|
||||||
b07269a018a4a9d4c029aba7dd8a15fa66df681c
|
|
||||||
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
|||||||
name: Print JVM thread dumps
|
|
||||||
description: Prints a thread dump for all running JVMs
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- run: |
|
|
||||||
for java_pid in $(jps -q -J-XX:+PerfDisableSharedMem); do
|
|
||||||
echo "------------------------ pid $java_pid ------------------------"
|
|
||||||
cat /proc/$java_pid/cmdline | xargs -0 echo
|
|
||||||
jcmd $java_pid Thread.print -l
|
|
||||||
jcmd $java_pid GC.heap_info
|
|
||||||
done
|
|
||||||
exit 0
|
|
||||||
shell: bash
|
|
@ -1,6 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
@ -1,43 +0,0 @@
|
|||||||
name: Build Pull Request
|
|
||||||
on: pull_request
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build pull request
|
|
||||||
runs-on: ubuntu22-8-32
|
|
||||||
if: ${{ github.repository == 'spring-projects/spring-boot' }}
|
|
||||||
steps:
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
java-version: '17'
|
|
||||||
distribution: 'liberica'
|
|
||||||
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Validate Gradle wrapper
|
|
||||||
uses: gradle/wrapper-validation-action@v1
|
|
||||||
|
|
||||||
- name: Set up Gradle
|
|
||||||
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
CI: 'true'
|
|
||||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
|
||||||
run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build
|
|
||||||
|
|
||||||
- name: Print JVM thread dumps when cancelled
|
|
||||||
uses: ./.github/actions/print-jvm-thread-dumps
|
|
||||||
if: cancelled()
|
|
||||||
|
|
||||||
- name: Upload build reports
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: build-reports
|
|
||||||
path: '**/build/reports/'
|
|
@ -1,13 +1,10 @@
|
|||||||
name: "Validate Gradle Wrapper"
|
name: "Validate Gradle Wrapper"
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validation:
|
validation:
|
||||||
name: "Validation"
|
name: "Validation"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: gradle/wrapper-validation-action@v1
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
.name
|
|
||||||
*.xml
|
|
||||||
/modules/
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
@ -1,121 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<code_scheme name="Project" version="173">
|
|
||||||
<option name="AUTODETECT_INDENTS" value="false" />
|
|
||||||
<option name="OTHER_INDENT_OPTIONS">
|
|
||||||
<value>
|
|
||||||
<option name="USE_TAB_CHARACTER" value="true" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
|
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
|
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
|
||||||
<value>
|
|
||||||
<package name="java" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="javax" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="org.springframework" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="true" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="RIGHT_MARGIN" value="90" />
|
|
||||||
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
|
|
||||||
<GroovyCodeStyleSettings>
|
|
||||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
|
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
|
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
|
||||||
<value>
|
|
||||||
<emptyLine />
|
|
||||||
<package name="javax" withSubpackages="true" static="false" />
|
|
||||||
<package name="java" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="org.springframework" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="true" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
</GroovyCodeStyleSettings>
|
|
||||||
<JavaCodeStyleSettings>
|
|
||||||
<option name="CLASS_NAMES_IN_JAVADOC" value="3" />
|
|
||||||
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
|
|
||||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
|
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
|
|
||||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
|
||||||
<value />
|
|
||||||
</option>
|
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
|
||||||
<value>
|
|
||||||
<package name="java" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="javax" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="org.springframework" withSubpackages="true" static="false" />
|
|
||||||
<emptyLine />
|
|
||||||
<package name="" withSubpackages="true" static="true" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
|
|
||||||
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
|
|
||||||
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
|
|
||||||
<option name="JD_KEEP_INVALID_TAGS" value="false" />
|
|
||||||
<option name="JD_KEEP_EMPTY_LINES" value="false" />
|
|
||||||
</JavaCodeStyleSettings>
|
|
||||||
<JetCodeStyleSettings>
|
|
||||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
|
||||||
<value>
|
|
||||||
<package name="java.util" alias="false" withSubpackages="false" />
|
|
||||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="false" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="20" />
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="20" />
|
|
||||||
</JetCodeStyleSettings>
|
|
||||||
<editorconfig>
|
|
||||||
<option name="ENABLED" value="false" />
|
|
||||||
</editorconfig>
|
|
||||||
<codeStyleSettings language="Groovy">
|
|
||||||
<indentOptions>
|
|
||||||
<option name="USE_TAB_CHARACTER" value="true" />
|
|
||||||
</indentOptions>
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="JAVA">
|
|
||||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
|
||||||
<option name="BLANK_LINES_AROUND_FIELD" value="1" />
|
|
||||||
<option name="BLANK_LINES_AROUND_FIELD_IN_INTERFACE" value="1" />
|
|
||||||
<option name="ELSE_ON_NEW_LINE" value="true" />
|
|
||||||
<option name="CATCH_ON_NEW_LINE" value="true" />
|
|
||||||
<option name="FINALLY_ON_NEW_LINE" value="true" />
|
|
||||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
|
||||||
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
|
|
||||||
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
|
|
||||||
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
|
||||||
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
|
|
||||||
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
|
|
||||||
<indentOptions>
|
|
||||||
<option name="USE_TAB_CHARACTER" value="true" />
|
|
||||||
</indentOptions>
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="JSON">
|
|
||||||
<indentOptions>
|
|
||||||
<option name="TAB_SIZE" value="2" />
|
|
||||||
</indentOptions>
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="XML">
|
|
||||||
<indentOptions>
|
|
||||||
<option name="USE_TAB_CHARACTER" value="true" />
|
|
||||||
</indentOptions>
|
|
||||||
</codeStyleSettings>
|
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<indentOptions>
|
|
||||||
<option name="USE_TAB_CHARACTER" value="true" />
|
|
||||||
</indentOptions>
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
|
||||||
</component>
|
|
@ -1,5 +0,0 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
@ -1,6 +0,0 @@
|
|||||||
<component name="CopyrightManager">
|
|
||||||
<copyright>
|
|
||||||
<option name="notice" value="Copyright 2012-&#36;today.year 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." />
|
|
||||||
<option name="myName" value="java" />
|
|
||||||
</copyright>
|
|
||||||
</component>
|
|
@ -1,7 +0,0 @@
|
|||||||
<component name="CopyrightManager">
|
|
||||||
<settings>
|
|
||||||
<module2copyright>
|
|
||||||
<element module="java" copyright="java" />
|
|
||||||
</module2copyright>
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
@ -1,17 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="LombokGetterMayBeUsed" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
<inspection_tool class="NullableProblems" enabled="false" level="WARNING" enabled_by_default="false">
|
|
||||||
<option name="REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL" value="true" />
|
|
||||||
<option name="REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL" value="true" />
|
|
||||||
<option name="REPORT_NOTNULL_PARAMETER_OVERRIDES_NULLABLE" value="true" />
|
|
||||||
<option name="REPORT_NOT_ANNOTATED_PARAMETER_OVERRIDES_NOTNULL" value="true" />
|
|
||||||
<option name="REPORT_NOT_ANNOTATED_GETTER" value="true" />
|
|
||||||
<option name="REPORT_NOT_ANNOTATED_SETTER_PARAMETER" value="true" />
|
|
||||||
<option name="REPORT_ANNOTATION_NOT_PROPAGATED_TO_OVERRIDERS" value="true" />
|
|
||||||
<option name="REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="UnqualifiedFieldAccess" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
@ -1,3 +0,0 @@
|
|||||||
<component name="DependencyValidationManager">
|
|
||||||
<scope name="java" pattern="file:*.java&&!file:*package-info.java" />
|
|
||||||
</component>
|
|
@ -1,3 +0,0 @@
|
|||||||
# Enable auto-env through the sdkman_auto_env config
|
|
||||||
# Add key=value pairs of SDKs to use below
|
|
||||||
java=17.0.8.1-librca
|
|
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0"?>
|
|
||||||
<!DOCTYPE module PUBLIC
|
|
||||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
|
||||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
|
||||||
<module name="com.puppycrawl.tools.checkstyle.Checker">
|
|
||||||
<module name="io.spring.javaformat.checkstyle.SpringChecks">
|
|
||||||
<property name="excludes" value="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck" />
|
|
||||||
</module>
|
|
||||||
</module>
|
|
@ -1 +1 @@
|
|||||||
javaFormatVersion=0.0.38
|
javaFormatVersion=0.0.29
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023-2023 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.boot.build;
|
|
||||||
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.gradle.plugins.ide.api.XmlFileContentMerger;
|
|
||||||
import org.gradle.plugins.ide.eclipse.EclipsePlugin;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.Classpath;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.ClasspathEntry;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.EclipseClasspath;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.Library;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conventions that are applied in the presence of the {@link EclipsePlugin} to work
|
|
||||||
* around buildship issue {@code #1238}.
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class EclipseConventions {
|
|
||||||
|
|
||||||
void apply(Project project) {
|
|
||||||
project.getPlugins().withType(EclipsePlugin.class, (eclipse) -> {
|
|
||||||
EclipseModel eclipseModel = project.getExtensions().getByType(EclipseModel.class);
|
|
||||||
eclipseModel.classpath(this::configureClasspath);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureClasspath(EclipseClasspath classpath) {
|
|
||||||
classpath.file(this::configureClasspathFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureClasspathFile(XmlFileContentMerger merger) {
|
|
||||||
merger.whenMerged((content) -> {
|
|
||||||
if (content instanceof Classpath classpath) {
|
|
||||||
classpath.getEntries().removeIf(this::isKotlinPluginContributedBuildDirectory);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isKotlinPluginContributedBuildDirectory(ClasspathEntry entry) {
|
|
||||||
return (entry instanceof Library library) && isKotlinPluginContributedBuildDirectory(library.getPath())
|
|
||||||
&& isTest(library);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isKotlinPluginContributedBuildDirectory(String path) {
|
|
||||||
return path.contains("/main") && (path.contains("/build/classes/") || path.contains("/build/resources/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTest(Library library) {
|
|
||||||
Object value = library.getEntryAttributes().get("test");
|
|
||||||
return (value instanceof String string && Boolean.parseBoolean(string));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023-2023 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.boot.build;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.gradle.api.JavaVersion;
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.gradle.api.internal.IConventionAware;
|
|
||||||
import org.gradle.api.plugins.JavaPluginExtension;
|
|
||||||
import org.gradle.plugins.ide.eclipse.EclipseWtpPlugin;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
|
|
||||||
import org.gradle.plugins.ide.eclipse.model.Facet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conventions that are applied in the presence of the {WarPlugin}. When the plugin is
|
|
||||||
* applied:
|
|
||||||
* <ul>
|
|
||||||
* <li>Update Eclipse WTP Plugin facets to use Servlet 5.0</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
public class WarConventions {
|
|
||||||
|
|
||||||
void apply(Project project) {
|
|
||||||
project.getPlugins().withType(EclipseWtpPlugin.class, (wtp) -> {
|
|
||||||
project.getTasks().getByName(EclipseWtpPlugin.ECLIPSE_WTP_FACET_TASK_NAME).doFirst((task) -> {
|
|
||||||
EclipseModel eclipseModel = project.getExtensions().getByType(EclipseModel.class);
|
|
||||||
((IConventionAware) eclipseModel.getWtp().getFacet()).getConventionMapping()
|
|
||||||
.map("facets", () -> getFacets(project));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Facet> getFacets(Project project) {
|
|
||||||
JavaVersion javaVersion = project.getExtensions().getByType(JavaPluginExtension.class).getSourceCompatibility();
|
|
||||||
List<Facet> facets = new ArrayList<>();
|
|
||||||
facets.add(new Facet(Facet.FacetType.fixed, "jst.web", null));
|
|
||||||
facets.add(new Facet(Facet.FacetType.installed, "jst.web", "5.0"));
|
|
||||||
facets.add(new Facet(Facet.FacetType.installed, "jst.java", javaVersion.toString()));
|
|
||||||
return facets;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,226 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022-2023 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.boot.build.architecture;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.tngtech.archunit.base.DescribedPredicate;
|
|
||||||
import com.tngtech.archunit.core.domain.JavaClass;
|
|
||||||
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
|
|
||||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
|
||||||
import com.tngtech.archunit.core.domain.JavaMethod;
|
|
||||||
import com.tngtech.archunit.core.domain.JavaParameter;
|
|
||||||
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
|
|
||||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
|
||||||
import com.tngtech.archunit.lang.ArchCondition;
|
|
||||||
import com.tngtech.archunit.lang.ArchRule;
|
|
||||||
import com.tngtech.archunit.lang.ConditionEvents;
|
|
||||||
import com.tngtech.archunit.lang.EvaluationResult;
|
|
||||||
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
|
||||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
|
||||||
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
|
||||||
import org.gradle.api.DefaultTask;
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
import org.gradle.api.Task;
|
|
||||||
import org.gradle.api.file.DirectoryProperty;
|
|
||||||
import org.gradle.api.file.FileCollection;
|
|
||||||
import org.gradle.api.file.FileTree;
|
|
||||||
import org.gradle.api.provider.ListProperty;
|
|
||||||
import org.gradle.api.tasks.IgnoreEmptyDirectories;
|
|
||||||
import org.gradle.api.tasks.Input;
|
|
||||||
import org.gradle.api.tasks.InputFiles;
|
|
||||||
import org.gradle.api.tasks.Internal;
|
|
||||||
import org.gradle.api.tasks.Optional;
|
|
||||||
import org.gradle.api.tasks.OutputDirectory;
|
|
||||||
import org.gradle.api.tasks.PathSensitive;
|
|
||||||
import org.gradle.api.tasks.PathSensitivity;
|
|
||||||
import org.gradle.api.tasks.SkipWhenEmpty;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link Task} that checks for architecture problems.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
public abstract class ArchitectureCheck extends DefaultTask {
|
|
||||||
|
|
||||||
private FileCollection classes;
|
|
||||||
|
|
||||||
public ArchitectureCheck() {
|
|
||||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
|
||||||
getRules().addAll(allPackagesShouldBeFreeOfTangles(),
|
|
||||||
allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(),
|
|
||||||
allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(),
|
|
||||||
noClassesShouldCallStepVerifierStepVerifyComplete(),
|
|
||||||
noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList());
|
|
||||||
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
void checkArchitecture() throws IOException {
|
|
||||||
JavaClasses javaClasses = new ClassFileImporter()
|
|
||||||
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
|
|
||||||
List<EvaluationResult> violations = getRules().get()
|
|
||||||
.stream()
|
|
||||||
.map((rule) -> rule.evaluate(javaClasses))
|
|
||||||
.filter(EvaluationResult::hasViolation)
|
|
||||||
.toList();
|
|
||||||
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
|
|
||||||
outputFile.getParentFile().mkdirs();
|
|
||||||
if (!violations.isEmpty()) {
|
|
||||||
StringBuilder report = new StringBuilder();
|
|
||||||
for (EvaluationResult violation : violations) {
|
|
||||||
report.append(violation.getFailureReport().toString());
|
|
||||||
report.append(String.format("%n"));
|
|
||||||
}
|
|
||||||
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
|
|
||||||
StandardOpenOption.TRUNCATE_EXISTING);
|
|
||||||
throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
outputFile.createNewFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchRule allPackagesShouldBeFreeOfTangles() {
|
|
||||||
return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization() {
|
|
||||||
return ArchRuleDefinition.methods()
|
|
||||||
.that()
|
|
||||||
.areAnnotatedWith("org.springframework.context.annotation.Bean")
|
|
||||||
.and()
|
|
||||||
.haveRawReturnType(Predicates.assignableTo("org.springframework.beans.factory.config.BeanPostProcessor"))
|
|
||||||
.should(onlyHaveParametersThatWillNotCauseEagerInitialization())
|
|
||||||
.andShould()
|
|
||||||
.beStatic()
|
|
||||||
.allowEmptyShould(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchCondition<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() {
|
|
||||||
DescribedPredicate<CanBeAnnotated> notAnnotatedWithLazy = DescribedPredicate
|
|
||||||
.not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy"));
|
|
||||||
DescribedPredicate<JavaClass> notOfASafeType = DescribedPredicate
|
|
||||||
.not(Predicates.assignableTo("org.springframework.beans.factory.ObjectProvider")
|
|
||||||
.or(Predicates.assignableTo("org.springframework.context.ApplicationContext"))
|
|
||||||
.or(Predicates.assignableTo("org.springframework.core.env.Environment")));
|
|
||||||
return new ArchCondition<>("not have parameters that will cause eager initialization") {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void check(JavaMethod item, ConditionEvents events) {
|
|
||||||
item.getParameters()
|
|
||||||
.stream()
|
|
||||||
.filter(notAnnotatedWithLazy)
|
|
||||||
.filter((parameter) -> notOfASafeType.test(parameter.getRawType()))
|
|
||||||
.forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter,
|
|
||||||
parameter.getDescription() + " will cause eager initialization as it is "
|
|
||||||
+ notAnnotatedWithLazy.getDescription() + " and is "
|
|
||||||
+ notOfASafeType.getDescription())));
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() {
|
|
||||||
return ArchRuleDefinition.methods()
|
|
||||||
.that()
|
|
||||||
.areAnnotatedWith("org.springframework.context.annotation.Bean")
|
|
||||||
.and()
|
|
||||||
.haveRawReturnType(
|
|
||||||
Predicates.assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor"))
|
|
||||||
.should(haveNoParameters())
|
|
||||||
.andShould()
|
|
||||||
.beStatic()
|
|
||||||
.allowEmptyShould(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchCondition<JavaMethod> haveNoParameters() {
|
|
||||||
return new ArchCondition<>("have no parameters") {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void check(JavaMethod item, ConditionEvents events) {
|
|
||||||
List<JavaParameter> parameters = item.getParameters();
|
|
||||||
if (!parameters.isEmpty()) {
|
|
||||||
events
|
|
||||||
.add(SimpleConditionEvent.violated(item, item.getDescription() + " should have no parameters"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() {
|
|
||||||
return ArchRuleDefinition.noClasses()
|
|
||||||
.should()
|
|
||||||
.callMethod("reactor.test.StepVerifier$Step", "verifyComplete")
|
|
||||||
.because("it can block indefinitely and expectComplete().verify(Duration) should be used instead");
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() {
|
|
||||||
return ArchRuleDefinition.noClasses()
|
|
||||||
.should()
|
|
||||||
.callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration")
|
|
||||||
.because("expectComplete().verify(Duration) should be used instead");
|
|
||||||
}
|
|
||||||
|
|
||||||
private ArchRule noClassesShouldCallCollectorsToList() {
|
|
||||||
return ArchRuleDefinition.noClasses()
|
|
||||||
.should()
|
|
||||||
.callMethod(Collectors.class, "toList")
|
|
||||||
.because("java.util.stream.Stream.toList() should be used instead");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClasses(FileCollection classes) {
|
|
||||||
this.classes = classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Internal
|
|
||||||
public FileCollection getClasses() {
|
|
||||||
return this.classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@InputFiles
|
|
||||||
@SkipWhenEmpty
|
|
||||||
@IgnoreEmptyDirectories
|
|
||||||
@PathSensitive(PathSensitivity.RELATIVE)
|
|
||||||
final FileTree getInputClasses() {
|
|
||||||
return this.classes.getAsFileTree();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Optional
|
|
||||||
@InputFiles
|
|
||||||
@PathSensitive(PathSensitivity.RELATIVE)
|
|
||||||
public abstract DirectoryProperty getResourcesDirectory();
|
|
||||||
|
|
||||||
@OutputDirectory
|
|
||||||
public abstract DirectoryProperty getOutputDirectory();
|
|
||||||
|
|
||||||
@Internal
|
|
||||||
public abstract ListProperty<ArchRule> getRules();
|
|
||||||
|
|
||||||
@Input
|
|
||||||
// The rules themselves can't be an input as they aren't serializable so we use their
|
|
||||||
// descriptions instead
|
|
||||||
abstract ListProperty<String> getRuleDescriptions();
|
|
||||||
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.architecture;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.gradle.api.Plugin;
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
import org.gradle.api.Task;
|
|
||||||
import org.gradle.api.plugins.JavaBasePlugin;
|
|
||||||
import org.gradle.api.plugins.JavaPluginExtension;
|
|
||||||
import org.gradle.api.tasks.SourceSet;
|
|
||||||
import org.gradle.api.tasks.TaskProvider;
|
|
||||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
|
||||||
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link Plugin} for verifying a project's architecture.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
public class ArchitecturePlugin implements Plugin<Project> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void apply(Project project) {
|
|
||||||
project.getPlugins().withType(JavaBasePlugin.class, (javaPlugin) -> registerTasks(project));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerTasks(Project project) {
|
|
||||||
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
|
|
||||||
List<TaskProvider<ArchitectureCheck>> packageTangleChecks = new ArrayList<>();
|
|
||||||
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
|
|
||||||
TaskProvider<ArchitectureCheck> checkPackageTangles = project.getTasks()
|
|
||||||
.register("checkArchitecture" + StringUtils.capitalize(sourceSet.getName()), ArchitectureCheck.class,
|
|
||||||
(task) -> {
|
|
||||||
task.setClasses(sourceSet.getOutput().getClassesDirs());
|
|
||||||
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
|
|
||||||
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
|
|
||||||
+ " source set.");
|
|
||||||
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
|
||||||
});
|
|
||||||
packageTangleChecks.add(checkPackageTangles);
|
|
||||||
}
|
|
||||||
if (!packageTangleChecks.isEmpty()) {
|
|
||||||
TaskProvider<Task> checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME);
|
|
||||||
checkTask.configure((check) -> check.dependsOn(packageTangleChecks));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.artifactory;
|
||||||
|
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Artifactory repository to which a build of Spring Boot can be published.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public final class ArtifactoryRepository {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private ArtifactoryRepository(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArtifactoryRepository forProject(Project project) {
|
||||||
|
return new ArtifactoryRepository(determineArtifactoryRepo(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String determineArtifactoryRepo(Project project) {
|
||||||
|
String version = project.getVersion().toString();
|
||||||
|
int modifierIndex = version.lastIndexOf('-');
|
||||||
|
if (modifierIndex == -1) {
|
||||||
|
return "release";
|
||||||
|
}
|
||||||
|
String type = version.substring(modifierIndex + 1);
|
||||||
|
if (type.startsWith("M") || type.startsWith("RC")) {
|
||||||
|
return "milestone";
|
||||||
|
}
|
||||||
|
return "snapshot";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.artifacts;
|
|
||||||
|
|
||||||
import org.gradle.api.Project;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information about artifacts produced by a build.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
* @author Scott Frederick
|
|
||||||
*/
|
|
||||||
public final class ArtifactRelease {
|
|
||||||
|
|
||||||
private static final String SNAPSHOT = "snapshot";
|
|
||||||
|
|
||||||
private static final String MILESTONE = "milestone";
|
|
||||||
|
|
||||||
private static final String RELEASE = "release";
|
|
||||||
|
|
||||||
private static final String SPRING_REPO = "https://repo.spring.io/%s";
|
|
||||||
|
|
||||||
private static final String MAVEN_REPO = "https://repo.maven.apache.org/maven2";
|
|
||||||
|
|
||||||
private final String type;
|
|
||||||
|
|
||||||
private ArtifactRelease(String type) {
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getType() {
|
|
||||||
return this.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDownloadRepo() {
|
|
||||||
return (this.isRelease()) ? MAVEN_REPO : String.format(SPRING_REPO, this.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRelease() {
|
|
||||||
return RELEASE.equals(this.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ArtifactRelease forProject(Project project) {
|
|
||||||
return new ArtifactRelease(determineReleaseType(project));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String determineReleaseType(Project project) {
|
|
||||||
String version = project.getVersion().toString();
|
|
||||||
int modifierIndex = version.lastIndexOf('-');
|
|
||||||
if (modifierIndex == -1) {
|
|
||||||
return RELEASE;
|
|
||||||
}
|
|
||||||
String type = version.substring(modifierIndex + 1);
|
|
||||||
if (type.startsWith("M") || type.startsWith("RC")) {
|
|
||||||
return MILESTONE;
|
|
||||||
}
|
|
||||||
return SNAPSHOT;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves library updates.
|
|
||||||
*
|
|
||||||
* @author Moritz Halbritter
|
|
||||||
*/
|
|
||||||
public interface LibraryUpdateResolver {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds library updates.
|
|
||||||
* @param librariesToUpgrade libraries to update
|
|
||||||
* @param librariesByName libraries indexed by name
|
|
||||||
* @return library which have updates
|
|
||||||
*/
|
|
||||||
List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> librariesToUpgrade,
|
|
||||||
Map<String, Library> librariesByName);
|
|
||||||
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
|
|
||||||
class LibraryWithVersionOptions {
|
|
||||||
|
|
||||||
private final Library library;
|
|
||||||
|
|
||||||
private final List<VersionOption> versionOptions;
|
|
||||||
|
|
||||||
LibraryWithVersionOptions(Library library, List<VersionOption> versionOptions) {
|
|
||||||
this.library = library;
|
|
||||||
this.versionOptions = versionOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
Library getLibrary() {
|
|
||||||
return this.library;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<VersionOption> getVersionOptions() {
|
|
||||||
return this.versionOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.BiPredicate;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import org.gradle.api.Task;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.BomExtension;
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
import org.springframework.boot.build.bom.bomr.ReleaseSchedule.Release;
|
|
||||||
import org.springframework.boot.build.bom.bomr.github.Milestone;
|
|
||||||
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Task} to move to snapshot dependencies.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
public abstract class MoveToSnapshots extends UpgradeDependencies {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(MoveToSnapshots.class);
|
|
||||||
|
|
||||||
private final URI REPOSITORY_URI = URI.create("https://repo.spring.io/snapshot/");
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public MoveToSnapshots(BomExtension bom) {
|
|
||||||
super(bom, true);
|
|
||||||
getRepositoryUris().add(this.REPOSITORY_URI);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@TaskAction
|
|
||||||
void upgradeDependencies() {
|
|
||||||
super.upgradeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String issueTitle(Upgrade upgrade) {
|
|
||||||
String snapshotVersion = upgrade.getVersion().toString();
|
|
||||||
String releaseVersion = snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length());
|
|
||||||
return "Upgrade to " + upgrade.getLibrary().getName() + " " + releaseVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String commitMessage(Upgrade upgrade, int issueNumber) {
|
|
||||||
return "Start building against " + upgrade.getLibrary().getName() + " " + releaseVersion(upgrade) + " snapshots"
|
|
||||||
+ "\n\nSee gh-" + issueNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String releaseVersion(Upgrade upgrade) {
|
|
||||||
String snapshotVersion = upgrade.getVersion().toString();
|
|
||||||
return snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean eligible(Library library) {
|
|
||||||
return library.isConsiderSnapshots() && super.eligible(library);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<BiPredicate<Library, DependencyVersion>> determineUpdatePredicates(Milestone milestone) {
|
|
||||||
ReleaseSchedule releaseSchedule = new ReleaseSchedule();
|
|
||||||
Map<String, List<Release>> releases = releaseSchedule.releasesBetween(OffsetDateTime.now(),
|
|
||||||
milestone.getDueOn());
|
|
||||||
List<BiPredicate<Library, DependencyVersion>> predicates = super.determineUpdatePredicates(milestone);
|
|
||||||
predicates.add((library, candidate) -> {
|
|
||||||
List<Release> releasesForLibrary = releases.get(library.getCalendarName());
|
|
||||||
if (releasesForLibrary != null) {
|
|
||||||
for (Release release : releasesForLibrary) {
|
|
||||||
if (candidate.isSnapshotFor(release.getVersion())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (log.isInfoEnabled()) {
|
|
||||||
log.info("Ignoring " + candidate + ". No release of " + library.getName() + " scheduled before "
|
|
||||||
+ milestone.getDueOn());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
return predicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
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.Future;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link LibraryUpdateResolver} decorator that uses multiple threads to find library
|
|
||||||
* updates.
|
|
||||||
*
|
|
||||||
* @author Moritz Halbritter
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
class MultithreadedLibraryUpdateResolver implements LibraryUpdateResolver {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(MultithreadedLibraryUpdateResolver.class);
|
|
||||||
|
|
||||||
private final int threads;
|
|
||||||
|
|
||||||
private final LibraryUpdateResolver delegate;
|
|
||||||
|
|
||||||
MultithreadedLibraryUpdateResolver(int threads, LibraryUpdateResolver delegate) {
|
|
||||||
this.threads = threads;
|
|
||||||
this.delegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> librariesToUpgrade,
|
|
||||||
Map<String, Library> librariesByName) {
|
|
||||||
LOGGER.info("Looking for updates using {} threads", this.threads);
|
|
||||||
ExecutorService executorService = Executors.newFixedThreadPool(this.threads);
|
|
||||||
try {
|
|
||||||
return librariesToUpgrade.stream()
|
|
||||||
.map((library) -> executorService.submit(
|
|
||||||
() -> this.delegate.findLibraryUpdates(Collections.singletonList(library), librariesByName)))
|
|
||||||
.flatMap(this::getResult)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Stream<LibraryWithVersionOptions> getResult(Future<List<LibraryWithVersionOptions>> job) {
|
|
||||||
try {
|
|
||||||
return job.get().stream();
|
|
||||||
}
|
|
||||||
catch (InterruptedException ex) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RuntimeException(ex);
|
|
||||||
}
|
|
||||||
catch (ExecutionException ex) {
|
|
||||||
throw new RuntimeException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
|
||||||
import org.springframework.web.client.RestOperations;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release schedule for Spring projects, retrieved from
|
|
||||||
* <a href="https://calendar.spring.io">https://calendar.spring.io</a>.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
class ReleaseSchedule {
|
|
||||||
|
|
||||||
private static final Pattern LIBRARY_AND_VERSION = Pattern.compile("([A-Za-z0-9 ]+) ([0-9A-Za-z.-]+)");
|
|
||||||
|
|
||||||
private final RestOperations rest;
|
|
||||||
|
|
||||||
ReleaseSchedule() {
|
|
||||||
this(new RestTemplate());
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseSchedule(RestOperations rest) {
|
|
||||||
this.rest = rest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
|
||||||
Map<String, List<Release>> releasesBetween(OffsetDateTime start, OffsetDateTime end) {
|
|
||||||
ResponseEntity<List> response = this.rest
|
|
||||||
.getForEntity("https://calendar.spring.io/releases?start=" + start + "&end=" + end, List.class);
|
|
||||||
List<Map<String, String>> body = response.getBody();
|
|
||||||
Map<String, List<Release>> releasesByLibrary = new LinkedCaseInsensitiveMap<>();
|
|
||||||
body.stream()
|
|
||||||
.map(this::asRelease)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.forEach((release) -> releasesByLibrary.computeIfAbsent(release.getLibraryName(), (l) -> new ArrayList<>())
|
|
||||||
.add(release));
|
|
||||||
return releasesByLibrary;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Release asRelease(Map<String, String> entry) {
|
|
||||||
LocalDate due = LocalDate.parse(entry.get("start"));
|
|
||||||
String title = entry.get("title");
|
|
||||||
Matcher matcher = LIBRARY_AND_VERSION.matcher(title);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String library = matcher.group(1);
|
|
||||||
String version = matcher.group(2);
|
|
||||||
return new Release(library, DependencyVersion.parse(version), due);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Release {
|
|
||||||
|
|
||||||
private final String libraryName;
|
|
||||||
|
|
||||||
private final DependencyVersion version;
|
|
||||||
|
|
||||||
private final LocalDate dueOn;
|
|
||||||
|
|
||||||
Release(String libraryName, DependencyVersion version, LocalDate dueOn) {
|
|
||||||
this.libraryName = libraryName;
|
|
||||||
this.version = version;
|
|
||||||
this.dueOn = dueOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getLibraryName() {
|
|
||||||
return this.libraryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
DependencyVersion getVersion() {
|
|
||||||
return this.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDate getDueOn() {
|
|
||||||
return this.dueOn;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.SortedSet;
|
|
||||||
import java.util.function.BiPredicate;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
import org.springframework.boot.build.bom.Library.Group;
|
|
||||||
import org.springframework.boot.build.bom.Library.Module;
|
|
||||||
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard implementation for {@link LibraryUpdateResolver}.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
class StandardLibraryUpdateResolver implements LibraryUpdateResolver {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardLibraryUpdateResolver.class);
|
|
||||||
|
|
||||||
private final VersionResolver versionResolver;
|
|
||||||
|
|
||||||
private final BiPredicate<Library, DependencyVersion> predicate;
|
|
||||||
|
|
||||||
StandardLibraryUpdateResolver(VersionResolver versionResolver,
|
|
||||||
List<BiPredicate<Library, DependencyVersion>> predicates) {
|
|
||||||
this.versionResolver = versionResolver;
|
|
||||||
this.predicate = (library, dependencyVersion) -> predicates.stream()
|
|
||||||
.allMatch((predicate) -> predicate.test(library, dependencyVersion));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> librariesToUpgrade,
|
|
||||||
Map<String, Library> librariesByName) {
|
|
||||||
List<LibraryWithVersionOptions> result = new ArrayList<>();
|
|
||||||
for (Library library : librariesToUpgrade) {
|
|
||||||
if (isLibraryExcluded(library)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
LOGGER.info("Looking for updates for {}", library.getName());
|
|
||||||
long start = System.nanoTime();
|
|
||||||
List<VersionOption> versionOptions = getVersionOptions(library, librariesByName);
|
|
||||||
result.add(new LibraryWithVersionOptions(library, versionOptions));
|
|
||||||
LOGGER.info("Found {} updates for {}, took {}", versionOptions.size(), library.getName(),
|
|
||||||
Duration.ofNanos(System.nanoTime() - start));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isLibraryExcluded(Library library) {
|
|
||||||
return library.getName().equals("Spring Boot");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<VersionOption> getVersionOptions(Library library, Map<String, Library> libraries) {
|
|
||||||
return determineResolvedVersionOptions(library);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<VersionOption> determineResolvedVersionOptions(Library library) {
|
|
||||||
Map<String, SortedSet<DependencyVersion>> moduleVersions = new LinkedHashMap<>();
|
|
||||||
for (Group group : library.getGroups()) {
|
|
||||||
for (Module module : group.getModules()) {
|
|
||||||
moduleVersions.put(group.getId() + ":" + module.getName(),
|
|
||||||
getLaterVersionsForModule(group.getId(), module.getName(), library));
|
|
||||||
}
|
|
||||||
for (String bom : group.getBoms()) {
|
|
||||||
moduleVersions.put(group.getId() + ":" + bom, getLaterVersionsForModule(group.getId(), bom, library));
|
|
||||||
}
|
|
||||||
for (String plugin : group.getPlugins()) {
|
|
||||||
moduleVersions.put(group.getId() + ":" + plugin,
|
|
||||||
getLaterVersionsForModule(group.getId(), plugin, library));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return moduleVersions.values()
|
|
||||||
.stream()
|
|
||||||
.flatMap(SortedSet::stream)
|
|
||||||
.distinct()
|
|
||||||
.filter((dependencyVersion) -> this.predicate.test(library, dependencyVersion))
|
|
||||||
.map((version) -> (VersionOption) new VersionOption.ResolvedVersionOption(version,
|
|
||||||
getMissingModules(moduleVersions, version)))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getMissingModules(Map<String, SortedSet<DependencyVersion>> moduleVersions,
|
|
||||||
DependencyVersion version) {
|
|
||||||
List<String> missingModules = new ArrayList<>();
|
|
||||||
moduleVersions.forEach((name, versions) -> {
|
|
||||||
if (!versions.contains(version)) {
|
|
||||||
missingModules.add(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return missingModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SortedSet<DependencyVersion> getLaterVersionsForModule(String groupId, String artifactId, Library library) {
|
|
||||||
return this.versionResolver.resolveVersions(groupId, artifactId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,283 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiPredicate;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
|
||||||
import org.apache.maven.artifact.versioning.VersionRange;
|
|
||||||
import org.gradle.api.DefaultTask;
|
|
||||||
import org.gradle.api.InvalidUserDataException;
|
|
||||||
import org.gradle.api.internal.tasks.userinput.UserInputHandler;
|
|
||||||
import org.gradle.api.provider.ListProperty;
|
|
||||||
import org.gradle.api.provider.Property;
|
|
||||||
import org.gradle.api.tasks.Input;
|
|
||||||
import org.gradle.api.tasks.Optional;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
import org.gradle.api.tasks.TaskExecutionException;
|
|
||||||
import org.gradle.api.tasks.options.Option;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.BomExtension;
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
|
|
||||||
import org.springframework.boot.build.bom.bomr.github.GitHub;
|
|
||||||
import org.springframework.boot.build.bom.bomr.github.GitHubRepository;
|
|
||||||
import org.springframework.boot.build.bom.bomr.github.Issue;
|
|
||||||
import org.springframework.boot.build.bom.bomr.github.Milestone;
|
|
||||||
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for tasks that upgrade dependencies in a bom.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
* @author Moritz Halbritter
|
|
||||||
*/
|
|
||||||
public abstract class UpgradeDependencies extends DefaultTask {
|
|
||||||
|
|
||||||
private final BomExtension bom;
|
|
||||||
|
|
||||||
private final boolean movingToSnapshots;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public UpgradeDependencies(BomExtension bom) {
|
|
||||||
this(bom, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected UpgradeDependencies(BomExtension bom, boolean movingToSnapshots) {
|
|
||||||
this.bom = bom;
|
|
||||||
getThreads().convention(2);
|
|
||||||
this.movingToSnapshots = movingToSnapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input
|
|
||||||
@Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
|
|
||||||
public abstract Property<String> getMilestone();
|
|
||||||
|
|
||||||
@Input
|
|
||||||
@Optional
|
|
||||||
@Option(option = "threads", description = "Number of Threads to use for update resolution")
|
|
||||||
public abstract Property<Integer> getThreads();
|
|
||||||
|
|
||||||
@Input
|
|
||||||
@Optional
|
|
||||||
@Option(option = "libraries", description = "Regular expression that identifies the libraries to upgrade")
|
|
||||||
public abstract Property<String> getLibraries();
|
|
||||||
|
|
||||||
@Input
|
|
||||||
abstract ListProperty<URI> getRepositoryUris();
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
void upgradeDependencies() {
|
|
||||||
GitHubRepository repository = createGitHub().getRepository(this.bom.getUpgrade().getGitHub().getOrganization(),
|
|
||||||
this.bom.getUpgrade().getGitHub().getRepository());
|
|
||||||
List<String> issueLabels = verifyLabels(repository);
|
|
||||||
Milestone milestone = determineMilestone(repository);
|
|
||||||
List<Upgrade> upgrades = resolveUpgrades(milestone);
|
|
||||||
applyUpgrades(repository, issueLabels, milestone, upgrades);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyUpgrades(GitHubRepository repository, List<String> issueLabels, Milestone milestone,
|
|
||||||
List<Upgrade> upgrades) {
|
|
||||||
Path buildFile = getProject().getBuildFile().toPath();
|
|
||||||
Path gradleProperties = new File(getProject().getRootProject().getProjectDir(), "gradle.properties").toPath();
|
|
||||||
UpgradeApplicator upgradeApplicator = new UpgradeApplicator(buildFile, gradleProperties);
|
|
||||||
List<Issue> existingUpgradeIssues = repository.findIssues(issueLabels, milestone);
|
|
||||||
System.out.println("Applying upgrades...");
|
|
||||||
System.out.println("");
|
|
||||||
for (Upgrade upgrade : upgrades) {
|
|
||||||
System.out.println(upgrade.getLibrary().getName() + " " + upgrade.getVersion());
|
|
||||||
String title = issueTitle(upgrade);
|
|
||||||
Issue existingUpgradeIssue = findExistingUpgradeIssue(existingUpgradeIssues, upgrade);
|
|
||||||
try {
|
|
||||||
Path modified = upgradeApplicator.apply(upgrade);
|
|
||||||
int issueNumber = getOrOpenUpgradeIssue(repository, issueLabels, milestone, title,
|
|
||||||
existingUpgradeIssue);
|
|
||||||
if (existingUpgradeIssue != null && existingUpgradeIssue.getState() == Issue.State.CLOSED) {
|
|
||||||
existingUpgradeIssue.label(Arrays.asList("type: task", "status: superseded"));
|
|
||||||
}
|
|
||||||
System.out.println(" Issue: " + issueNumber + " - " + title
|
|
||||||
+ getExistingUpgradeIssueMessageDetails(existingUpgradeIssue));
|
|
||||||
if (new ProcessBuilder().command("git", "add", modified.toFile().getAbsolutePath())
|
|
||||||
.start()
|
|
||||||
.waitFor() != 0) {
|
|
||||||
throw new IllegalStateException("git add failed");
|
|
||||||
}
|
|
||||||
String commitMessage = commitMessage(upgrade, issueNumber);
|
|
||||||
if (new ProcessBuilder().command("git", "commit", "-m", commitMessage).start().waitFor() != 0) {
|
|
||||||
throw new IllegalStateException("git commit failed");
|
|
||||||
}
|
|
||||||
System.out.println(" Commit: " + commitMessage.substring(0, commitMessage.indexOf('\n')));
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
throw new TaskExecutionException(this, ex);
|
|
||||||
}
|
|
||||||
catch (InterruptedException ex) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getOrOpenUpgradeIssue(GitHubRepository repository, List<String> issueLabels, Milestone milestone,
|
|
||||||
String title, Issue existingUpgradeIssue) {
|
|
||||||
if (existingUpgradeIssue != null && existingUpgradeIssue.getState() == Issue.State.OPEN) {
|
|
||||||
return existingUpgradeIssue.getNumber();
|
|
||||||
}
|
|
||||||
String body = (existingUpgradeIssue != null) ? "Supersedes #" + existingUpgradeIssue.getNumber() : "";
|
|
||||||
return repository.openIssue(title, body, issueLabels, milestone);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getExistingUpgradeIssueMessageDetails(Issue existingUpgradeIssue) {
|
|
||||||
if (existingUpgradeIssue == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (existingUpgradeIssue.getState() != Issue.State.CLOSED) {
|
|
||||||
return " (completes existing upgrade)";
|
|
||||||
}
|
|
||||||
return " (supersedes #" + existingUpgradeIssue.getNumber() + " " + existingUpgradeIssue.getTitle() + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> verifyLabels(GitHubRepository repository) {
|
|
||||||
Set<String> availableLabels = repository.getLabels();
|
|
||||||
List<String> issueLabels = this.bom.getUpgrade().getGitHub().getIssueLabels();
|
|
||||||
if (!availableLabels.containsAll(issueLabels)) {
|
|
||||||
List<String> unknownLabels = new ArrayList<>(issueLabels);
|
|
||||||
unknownLabels.removeAll(availableLabels);
|
|
||||||
throw new InvalidUserDataException(
|
|
||||||
"Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels));
|
|
||||||
}
|
|
||||||
return issueLabels;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GitHub createGitHub() {
|
|
||||||
Properties bomrProperties = new Properties();
|
|
||||||
try (Reader reader = new FileReader(new File(System.getProperty("user.home"), ".bomr.properties"))) {
|
|
||||||
bomrProperties.load(reader);
|
|
||||||
String username = bomrProperties.getProperty("bomr.github.username");
|
|
||||||
String password = bomrProperties.getProperty("bomr.github.password");
|
|
||||||
return GitHub.withCredentials(username, password);
|
|
||||||
}
|
|
||||||
catch (IOException ex) {
|
|
||||||
throw new InvalidUserDataException("Failed to load .bomr.properties from user home", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Milestone determineMilestone(GitHubRepository repository) {
|
|
||||||
List<Milestone> milestones = repository.getMilestones();
|
|
||||||
java.util.Optional<Milestone> matchingMilestone = milestones.stream()
|
|
||||||
.filter((milestone) -> milestone.getName().equals(getMilestone().get()))
|
|
||||||
.findFirst();
|
|
||||||
if (!matchingMilestone.isPresent()) {
|
|
||||||
throw new InvalidUserDataException("Unknown milestone: " + getMilestone().get());
|
|
||||||
}
|
|
||||||
return matchingMilestone.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Issue findExistingUpgradeIssue(List<Issue> existingUpgradeIssues, Upgrade upgrade) {
|
|
||||||
String toMatch = "Upgrade to " + upgrade.getLibrary().getName();
|
|
||||||
for (Issue existingUpgradeIssue : existingUpgradeIssues) {
|
|
||||||
String title = existingUpgradeIssue.getTitle();
|
|
||||||
int lastSpaceIndex = title.lastIndexOf(' ');
|
|
||||||
if (lastSpaceIndex > -1) {
|
|
||||||
title = title.substring(0, lastSpaceIndex);
|
|
||||||
}
|
|
||||||
if (title.equals(toMatch)) {
|
|
||||||
return existingUpgradeIssue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private List<Upgrade> resolveUpgrades(Milestone milestone) {
|
|
||||||
List<Upgrade> upgrades = new InteractiveUpgradeResolver(getServices().get(UserInputHandler.class),
|
|
||||||
new MultithreadedLibraryUpdateResolver(getThreads().get(),
|
|
||||||
new StandardLibraryUpdateResolver(new MavenMetadataVersionResolver(getRepositoryUris().get()),
|
|
||||||
determineUpdatePredicates(milestone))))
|
|
||||||
.resolveUpgrades(matchingLibraries(), this.bom.getLibraries());
|
|
||||||
return upgrades;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<BiPredicate<Library, DependencyVersion>> determineUpdatePredicates(Milestone milestone) {
|
|
||||||
List<BiPredicate<Library, DependencyVersion>> updatePredicates = new ArrayList<>();
|
|
||||||
updatePredicates.add(this::compilesWithUpgradePolicy);
|
|
||||||
updatePredicates.add(this::isAnUpgrade);
|
|
||||||
updatePredicates.add(this::isNotProhibited);
|
|
||||||
return updatePredicates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean compilesWithUpgradePolicy(Library library, DependencyVersion candidate) {
|
|
||||||
return this.bom.getUpgrade().getPolicy().test(candidate, library.getVersion().getVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isAnUpgrade(Library library, DependencyVersion candidate) {
|
|
||||||
return library.getVersion().getVersion().isUpgrade(candidate, this.movingToSnapshots);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNotProhibited(Library library, DependencyVersion candidate) {
|
|
||||||
return !library.getProhibitedVersions()
|
|
||||||
.stream()
|
|
||||||
.anyMatch((prohibited) -> isProhibited(prohibited, candidate.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isProhibited(ProhibitedVersion prohibited, String candidate) {
|
|
||||||
boolean result = false;
|
|
||||||
VersionRange range = prohibited.getRange();
|
|
||||||
result = result || (range != null && range.containsVersion(new DefaultArtifactVersion(candidate)));
|
|
||||||
result = result || prohibited.getStartsWith().stream().anyMatch(candidate::startsWith);
|
|
||||||
result = result || prohibited.getStartsWith().stream().anyMatch(candidate::endsWith);
|
|
||||||
result = result || prohibited.getStartsWith().stream().anyMatch(candidate::contains);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Library> matchingLibraries() {
|
|
||||||
List<Library> matchingLibraries = this.bom.getLibraries().stream().filter(this::eligible).toList();
|
|
||||||
if (matchingLibraries.isEmpty()) {
|
|
||||||
throw new InvalidUserDataException("No libraries to upgrade");
|
|
||||||
}
|
|
||||||
return matchingLibraries;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean eligible(Library library) {
|
|
||||||
String pattern = getLibraries().getOrNull();
|
|
||||||
if (pattern == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Predicate<String> libraryPredicate = Pattern.compile(pattern).asPredicate();
|
|
||||||
return libraryPredicate.test(library.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract String issueTitle(Upgrade upgrade);
|
|
||||||
|
|
||||||
protected abstract String commitMessage(Upgrade upgrade, int issueNumber);
|
|
||||||
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.boot.build.bom.Library;
|
|
||||||
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An option for a library update.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
class VersionOption {
|
|
||||||
|
|
||||||
private final DependencyVersion version;
|
|
||||||
|
|
||||||
VersionOption(DependencyVersion version) {
|
|
||||||
this.version = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
DependencyVersion getVersion() {
|
|
||||||
return this.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return this.version.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class AlignedVersionOption extends VersionOption {
|
|
||||||
|
|
||||||
private final Library alignedWith;
|
|
||||||
|
|
||||||
AlignedVersionOption(DependencyVersion version, Library alignedWith) {
|
|
||||||
super(version);
|
|
||||||
this.alignedWith = alignedWith;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return super.toString() + " (aligned with " + this.alignedWith.getName() + " "
|
|
||||||
+ this.alignedWith.getVersion().getVersion() + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class ResolvedVersionOption extends VersionOption {
|
|
||||||
|
|
||||||
private final List<String> missingModules;
|
|
||||||
|
|
||||||
ResolvedVersionOption(DependencyVersion version, List<String> missingModules) {
|
|
||||||
super(version);
|
|
||||||
this.missingModules = missingModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
if (this.missingModules.isEmpty()) {
|
|
||||||
return super.toString();
|
|
||||||
}
|
|
||||||
return super.toString() + " (some modules are missing: "
|
|
||||||
+ StringUtils.collectionToDelimitedString(this.missingModules, ", ") + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.bom.bomr.version;
|
|
||||||
|
|
||||||
import org.apache.maven.artifact.versioning.ArtifactVersion;
|
|
||||||
import org.apache.maven.artifact.versioning.ComparableVersion;
|
|
||||||
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A fallback {@link DependencyVersion} to handle versions with four or five components
|
|
||||||
* that cannot be handled by {@link ArtifactVersion} because the fourth component is
|
|
||||||
* numeric.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
* @author Moritz Halbritter
|
|
||||||
*/
|
|
||||||
final class MultipleComponentsDependencyVersion extends ArtifactVersionDependencyVersion {
|
|
||||||
|
|
||||||
private final String original;
|
|
||||||
|
|
||||||
private MultipleComponentsDependencyVersion(ArtifactVersion artifactVersion, String original) {
|
|
||||||
super(artifactVersion, new ComparableVersion(original));
|
|
||||||
this.original = original;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return this.original;
|
|
||||||
}
|
|
||||||
|
|
||||||
static MultipleComponentsDependencyVersion parse(String input) {
|
|
||||||
String[] components = input.split("\\.");
|
|
||||||
if (components.length == 4 || components.length == 5) {
|
|
||||||
ArtifactVersion artifactVersion = new DefaultArtifactVersion(
|
|
||||||
components[0] + "." + components[1] + "." + components[2]);
|
|
||||||
if (artifactVersion.getQualifier() != null && artifactVersion.getQualifier().equals(input)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new MultipleComponentsDependencyVersion(artifactVersion, input);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.bom.bomr.version;
|
||||||
|
|
||||||
|
import org.apache.maven.artifact.versioning.ArtifactVersion;
|
||||||
|
import org.apache.maven.artifact.versioning.ComparableVersion;
|
||||||
|
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fallback {@link DependencyVersion} to handle versions with four components that
|
||||||
|
* cannot be handled by {@link ArtifactVersion} because the fourth component is numeric.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
final class NumericQualifierDependencyVersion extends ArtifactVersionDependencyVersion {
|
||||||
|
|
||||||
|
private final String original;
|
||||||
|
|
||||||
|
private NumericQualifierDependencyVersion(ArtifactVersion artifactVersion, String original) {
|
||||||
|
super(artifactVersion, new ComparableVersion(original));
|
||||||
|
this.original = original;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.original;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NumericQualifierDependencyVersion parse(String input) {
|
||||||
|
String[] components = input.split("\\.");
|
||||||
|
if (components.length == 4) {
|
||||||
|
ArtifactVersion artifactVersion = new DefaultArtifactVersion(
|
||||||
|
components[0] + "." + components[1] + "." + components[2]);
|
||||||
|
if (artifactVersion.getQualifier() != null && artifactVersion.getQualifier().equals(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new NumericQualifierDependencyVersion(artifactVersion, input);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2023-2023 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.boot.build.classpath;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.gradle.api.DefaultTask;
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
import org.gradle.api.artifacts.Configuration;
|
|
||||||
import org.gradle.api.artifacts.component.ModuleComponentSelector;
|
|
||||||
import org.gradle.api.artifacts.result.DependencyResult;
|
|
||||||
import org.gradle.api.artifacts.result.ResolutionResult;
|
|
||||||
import org.gradle.api.file.FileCollection;
|
|
||||||
import org.gradle.api.tasks.Classpath;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tasks to check that none of classpath's direct dependencies are unconstrained.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
public class CheckClasspathForUnconstrainedDirectDependencies extends DefaultTask {
|
|
||||||
|
|
||||||
private Configuration classpath;
|
|
||||||
|
|
||||||
public CheckClasspathForUnconstrainedDirectDependencies() {
|
|
||||||
getOutputs().upToDateWhen((task) -> true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Classpath
|
|
||||||
public FileCollection getClasspath() {
|
|
||||||
return this.classpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClasspath(Configuration classpath) {
|
|
||||||
this.classpath = classpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
void checkForUnconstrainedDirectDependencies() {
|
|
||||||
ResolutionResult resolutionResult = this.classpath.getIncoming().getResolutionResult();
|
|
||||||
Set<? extends DependencyResult> dependencies = resolutionResult.getRoot().getDependencies();
|
|
||||||
Set<String> unconstrainedDependencies = dependencies.stream()
|
|
||||||
.map(DependencyResult::getRequested)
|
|
||||||
.filter(ModuleComponentSelector.class::isInstance)
|
|
||||||
.map(ModuleComponentSelector.class::cast)
|
|
||||||
.map((selector) -> selector.getGroup() + ":" + selector.getModule())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
Set<String> constraints = resolutionResult.getAllDependencies()
|
|
||||||
.stream()
|
|
||||||
.filter(DependencyResult::isConstraint)
|
|
||||||
.map(DependencyResult::getRequested)
|
|
||||||
.filter(ModuleComponentSelector.class::isInstance)
|
|
||||||
.map(ModuleComponentSelector.class::cast)
|
|
||||||
.map((selector) -> selector.getGroup() + ":" + selector.getModule())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
unconstrainedDependencies.removeAll(constraints);
|
|
||||||
if (!unconstrainedDependencies.isEmpty()) {
|
|
||||||
throw new GradleException("Found unconstrained direct dependencies: " + unconstrainedDependencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2021 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.boot.build.cli;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
import org.gradle.api.DefaultTask;
|
||||||
|
import org.gradle.api.file.RegularFile;
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.InputFile;
|
||||||
|
import org.gradle.api.tasks.OutputDirectory;
|
||||||
|
import org.gradle.api.tasks.PathSensitive;
|
||||||
|
import org.gradle.api.tasks.PathSensitivity;
|
||||||
|
import org.gradle.api.tasks.TaskExecutionException;
|
||||||
|
|
||||||
|
import org.springframework.boot.build.artifactory.ArtifactoryRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for generating a package manager definition file such as a Scoop manifest or
|
||||||
|
* a Homebrew formula.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public abstract class AbstractPackageManagerDefinitionTask extends DefaultTask {
|
||||||
|
|
||||||
|
private Provider<RegularFile> archive;
|
||||||
|
|
||||||
|
private File template;
|
||||||
|
|
||||||
|
private File outputDir;
|
||||||
|
|
||||||
|
public AbstractPackageManagerDefinitionTask() {
|
||||||
|
getInputs().property("version", getProject().provider(getProject()::getVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputFile
|
||||||
|
@PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
public RegularFile getArchive() {
|
||||||
|
return this.archive.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArchive(Provider<RegularFile> archive) {
|
||||||
|
this.archive = archive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputFile
|
||||||
|
@PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
public File getTemplate() {
|
||||||
|
return this.template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTemplate(File template) {
|
||||||
|
this.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OutputDirectory
|
||||||
|
public File getOutputDir() {
|
||||||
|
return this.outputDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOutputDir(File outputDir) {
|
||||||
|
this.outputDir = outputDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createDescriptor(Map<String, Object> additionalProperties) {
|
||||||
|
getProject().copy((copy) -> {
|
||||||
|
copy.from(this.template);
|
||||||
|
copy.into(this.outputDir);
|
||||||
|
Map<String, Object> properties = new HashMap<>(additionalProperties);
|
||||||
|
properties.put("hash", sha256(this.archive.get().getAsFile()));
|
||||||
|
properties.put("repo", ArtifactoryRepository.forProject(getProject()));
|
||||||
|
properties.put("project", getProject());
|
||||||
|
copy.expand(properties);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sha256(File file) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
return new DigestUtils(digest).digestAsHex(file);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new TaskExecutionException(this, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.cli;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import org.gradle.api.Task;
|
||||||
|
import org.gradle.api.tasks.TaskAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Task} for creating a Scoop manifest.
|
||||||
|
*
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
public class ScoopManifest extends AbstractPackageManagerDefinitionTask {
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
void createManifest() {
|
||||||
|
String version = getProject().getVersion().toString();
|
||||||
|
createDescriptor(Collections.singletonMap("scoopVersion", version.substring(0, version.lastIndexOf('.'))));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2021 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple builder to help construct Asciidoc markup.
|
|
||||||
*
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class Asciidoc {
|
|
||||||
|
|
||||||
private final StringBuilder content;
|
|
||||||
|
|
||||||
Asciidoc() {
|
|
||||||
this.content = new StringBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
Asciidoc appendWithHardLineBreaks(Object... items) {
|
|
||||||
for (Object item : items) {
|
|
||||||
appendln("`+", item, "+` +");
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Asciidoc appendln(Object... items) {
|
|
||||||
return append(items).newLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
Asciidoc append(Object... items) {
|
|
||||||
for (Object item : items) {
|
|
||||||
this.content.append(item);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Asciidoc newLine() {
|
|
||||||
return append(System.lineSeparator());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return this.content.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple builder to help construct Asciidoc markup.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class AsciidocBuilder {
|
||||||
|
|
||||||
|
private final StringBuilder content;
|
||||||
|
|
||||||
|
AsciidocBuilder() {
|
||||||
|
this.content = new StringBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciidocBuilder appendKey(Object... items) {
|
||||||
|
for (Object item : items) {
|
||||||
|
appendln("`+", item, "+` +");
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciidocBuilder newLine() {
|
||||||
|
return append(System.lineSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciidocBuilder appendln(Object... items) {
|
||||||
|
return append(items).newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
AsciidocBuilder append(Object... items) {
|
||||||
|
for (Object item : items) {
|
||||||
|
this.content.append(item);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.content.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2022 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParseException;
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
import org.gradle.api.file.FileTree;
|
|
||||||
import org.gradle.api.file.RegularFileProperty;
|
|
||||||
import org.gradle.api.tasks.InputFiles;
|
|
||||||
import org.gradle.api.tasks.OutputFile;
|
|
||||||
import org.gradle.api.tasks.PathSensitive;
|
|
||||||
import org.gradle.api.tasks.PathSensitivity;
|
|
||||||
import org.gradle.api.tasks.SourceTask;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link SourceTask} that checks additional Spring configuration metadata files.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
public class CheckAdditionalSpringConfigurationMetadata extends SourceTask {
|
|
||||||
|
|
||||||
private final RegularFileProperty reportLocation;
|
|
||||||
|
|
||||||
public CheckAdditionalSpringConfigurationMetadata() {
|
|
||||||
this.reportLocation = getProject().getObjects().fileProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OutputFile
|
|
||||||
public RegularFileProperty getReportLocation() {
|
|
||||||
return this.reportLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@InputFiles
|
|
||||||
@PathSensitive(PathSensitivity.RELATIVE)
|
|
||||||
public FileTree getSource() {
|
|
||||||
return super.getSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
void check() throws JsonParseException, IOException {
|
|
||||||
Report report = createReport();
|
|
||||||
File reportFile = getReportLocation().get().getAsFile();
|
|
||||||
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
|
||||||
if (report.hasProblems()) {
|
|
||||||
throw new GradleException(
|
|
||||||
"Problems found in additional Spring configuration metadata. See " + reportFile + " for details.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Report createReport() throws IOException, JsonParseException, JsonMappingException {
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
Report report = new Report();
|
|
||||||
for (File file : getSource().getFiles()) {
|
|
||||||
Analysis analysis = report.analysis(getProject().getProjectDir().toPath().relativize(file.toPath()));
|
|
||||||
Map<String, Object> json = objectMapper.readValue(file, Map.class);
|
|
||||||
check("groups", json, analysis);
|
|
||||||
check("properties", json, analysis);
|
|
||||||
check("hints", json, analysis);
|
|
||||||
}
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void check(String key, Map<String, Object> json, Analysis analysis) {
|
|
||||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.get(key);
|
|
||||||
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
|
|
||||||
List<String> sortedNames = sortedCopy(names);
|
|
||||||
for (int i = 0; i < names.size(); i++) {
|
|
||||||
String actual = names.get(i);
|
|
||||||
String expected = sortedNames.get(i);
|
|
||||||
if (!actual.equals(expected)) {
|
|
||||||
analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
|
|
||||||
+ "' but found '" + actual + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> sortedCopy(Collection<String> original) {
|
|
||||||
List<String> copy = new ArrayList<>(original);
|
|
||||||
Collections.sort(copy);
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Report implements Iterable<String> {
|
|
||||||
|
|
||||||
private final List<Analysis> analyses = new ArrayList<>();
|
|
||||||
|
|
||||||
private Analysis analysis(Path path) {
|
|
||||||
Analysis analysis = new Analysis(path);
|
|
||||||
this.analyses.add(analysis);
|
|
||||||
return analysis;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasProblems() {
|
|
||||||
for (Analysis analysis : this.analyses) {
|
|
||||||
if (!analysis.problems.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<String> iterator() {
|
|
||||||
List<String> lines = new ArrayList<>();
|
|
||||||
for (Analysis analysis : this.analyses) {
|
|
||||||
lines.add(analysis.source.toString());
|
|
||||||
lines.add("");
|
|
||||||
if (analysis.problems.isEmpty()) {
|
|
||||||
lines.add("No problems found.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lines.addAll(analysis.problems);
|
|
||||||
}
|
|
||||||
lines.add("");
|
|
||||||
}
|
|
||||||
return lines.iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Analysis {
|
|
||||||
|
|
||||||
private final List<String> problems = new ArrayList<>();
|
|
||||||
|
|
||||||
private final Path source;
|
|
||||||
|
|
||||||
private Analysis(Path source) {
|
|
||||||
this.source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonParseException;
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.gradle.api.DefaultTask;
|
|
||||||
import org.gradle.api.GradleException;
|
|
||||||
import org.gradle.api.file.RegularFileProperty;
|
|
||||||
import org.gradle.api.tasks.Input;
|
|
||||||
import org.gradle.api.tasks.InputFile;
|
|
||||||
import org.gradle.api.tasks.OutputFile;
|
|
||||||
import org.gradle.api.tasks.PathSensitive;
|
|
||||||
import org.gradle.api.tasks.PathSensitivity;
|
|
||||||
import org.gradle.api.tasks.SourceTask;
|
|
||||||
import org.gradle.api.tasks.TaskAction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link SourceTask} that checks {@code spring-configuration-metadata.json} files.
|
|
||||||
*
|
|
||||||
* @author Andy Wilkinson
|
|
||||||
*/
|
|
||||||
public class CheckSpringConfigurationMetadata extends DefaultTask {
|
|
||||||
|
|
||||||
private List<String> exclusions = new ArrayList<>();
|
|
||||||
|
|
||||||
private final RegularFileProperty reportLocation;
|
|
||||||
|
|
||||||
private final RegularFileProperty metadataLocation;
|
|
||||||
|
|
||||||
public CheckSpringConfigurationMetadata() {
|
|
||||||
this.metadataLocation = getProject().getObjects().fileProperty();
|
|
||||||
this.reportLocation = getProject().getObjects().fileProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OutputFile
|
|
||||||
public RegularFileProperty getReportLocation() {
|
|
||||||
return this.reportLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@InputFile
|
|
||||||
@PathSensitive(PathSensitivity.RELATIVE)
|
|
||||||
public RegularFileProperty getMetadataLocation() {
|
|
||||||
return this.metadataLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExclusions(List<String> exclusions) {
|
|
||||||
this.exclusions = exclusions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input
|
|
||||||
public List<String> getExclusions() {
|
|
||||||
return this.exclusions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
void check() throws JsonParseException, IOException {
|
|
||||||
Report report = createReport();
|
|
||||||
File reportFile = getReportLocation().get().getAsFile();
|
|
||||||
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
|
||||||
if (report.hasProblems()) {
|
|
||||||
throw new GradleException(
|
|
||||||
"Problems found in Spring configuration metadata. See " + reportFile + " for details.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Report createReport() throws IOException, JsonParseException, JsonMappingException {
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
File file = this.metadataLocation.get().getAsFile();
|
|
||||||
Report report = new Report(getProject().getProjectDir().toPath().relativize(file.toPath()));
|
|
||||||
Map<String, Object> json = objectMapper.readValue(file, Map.class);
|
|
||||||
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
|
|
||||||
for (Map<String, Object> property : properties) {
|
|
||||||
String name = (String) property.get("name");
|
|
||||||
if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(name)) {
|
|
||||||
report.propertiesWithNoDescription.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isExcluded(String propertyName) {
|
|
||||||
for (String exclusion : this.exclusions) {
|
|
||||||
if (propertyName.equals(exclusion)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (exclusion.endsWith(".*")) {
|
|
||||||
if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private boolean isDeprecated(Map<String, Object> property) {
|
|
||||||
return (Map<String, Object>) property.get("deprecation") != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDescribed(Map<String, Object> property) {
|
|
||||||
return property.get("description") != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Report implements Iterable<String> {
|
|
||||||
|
|
||||||
private final List<String> propertiesWithNoDescription = new ArrayList<>();
|
|
||||||
|
|
||||||
private final Path source;
|
|
||||||
|
|
||||||
private Report(Path source) {
|
|
||||||
this.source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasProblems() {
|
|
||||||
return !this.propertiesWithNoDescription.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<String> iterator() {
|
|
||||||
List<String> lines = new ArrayList<>();
|
|
||||||
lines.add(this.source.toString());
|
|
||||||
lines.add("");
|
|
||||||
if (this.propertiesWithNoDescription.isEmpty()) {
|
|
||||||
lines.add("No problems found.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lines.add("The following properties have no description:");
|
|
||||||
lines.add("");
|
|
||||||
lines.addAll(this.propertiesWithNoDescription.stream().map((line) -> "\t" + line).toList());
|
|
||||||
}
|
|
||||||
lines.add("");
|
|
||||||
return lines.iterator();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table entry regrouping a list of configuration properties sharing the same description.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class CompoundConfigurationTableEntry extends ConfigurationTableEntry {
|
||||||
|
|
||||||
|
private final Set<String> configurationKeys;
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
CompoundConfigurationTableEntry(String key, String description) {
|
||||||
|
this.key = key;
|
||||||
|
this.description = description;
|
||||||
|
this.configurationKeys = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addConfigurationKeys(ConfigurationProperty... properties) {
|
||||||
|
Stream.of(properties).map(ConfigurationProperty::getName).forEach(this.configurationKeys::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void write(AsciidocBuilder builder) {
|
||||||
|
builder.append("|[[" + this.key + "]]<<" + this.key + ",");
|
||||||
|
this.configurationKeys.forEach(builder::appendKey);
|
||||||
|
builder.appendln(">>");
|
||||||
|
builder.newLine().appendln("|").appendln("|+++", this.description, "+++");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2021 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table row regrouping a list of configuration properties sharing the same description.
|
|
||||||
*
|
|
||||||
* @author Brian Clozel
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class CompoundRow extends Row {
|
|
||||||
|
|
||||||
private final Set<String> propertyNames;
|
|
||||||
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
CompoundRow(Snippet snippet, String prefix, String description) {
|
|
||||||
super(snippet, prefix);
|
|
||||||
this.description = description;
|
|
||||||
this.propertyNames = new TreeSet<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void addProperty(ConfigurationProperty property) {
|
|
||||||
this.propertyNames.add(property.getDisplayName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void write(Asciidoc asciidoc) {
|
|
||||||
asciidoc.append("|");
|
|
||||||
asciidoc.append("[[" + getAnchor() + "]]");
|
|
||||||
asciidoc.append("<<" + getAnchor() + ",");
|
|
||||||
this.propertyNames.forEach(asciidoc::appendWithHardLineBreaks);
|
|
||||||
asciidoc.appendln(">>");
|
|
||||||
asciidoc.appendln("|+++", this.description, "+++");
|
|
||||||
asciidoc.appendln("|");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.gradle.api.file.FileCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write Asciidoc documents with configuration properties listings.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class ConfigurationMetadataDocumentWriter {
|
||||||
|
|
||||||
|
public void writeDocument(Path outputDirectory, DocumentOptions options, FileCollection metadataFiles)
|
||||||
|
throws IOException {
|
||||||
|
assertValidOutputDirectory(outputDirectory);
|
||||||
|
if (!Files.exists(outputDirectory)) {
|
||||||
|
Files.createDirectory(outputDirectory);
|
||||||
|
}
|
||||||
|
List<ConfigurationTable> tables = createConfigTables(ConfigurationProperties.fromFiles(metadataFiles), options);
|
||||||
|
for (ConfigurationTable table : tables) {
|
||||||
|
writeConfigurationTable(table, outputDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertValidOutputDirectory(Path outputDirPath) {
|
||||||
|
if (outputDirPath == null) {
|
||||||
|
throw new IllegalArgumentException("output path should not be null");
|
||||||
|
}
|
||||||
|
if (Files.exists(outputDirPath) && !Files.isDirectory(outputDirPath)) {
|
||||||
|
throw new IllegalArgumentException("output path already exists and is not a directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ConfigurationTable> createConfigTables(Map<String, ConfigurationProperty> metadataProperties,
|
||||||
|
DocumentOptions options) {
|
||||||
|
List<ConfigurationTable> tables = new ArrayList<>();
|
||||||
|
List<String> unmappedKeys = metadataProperties.values().stream().filter((property) -> !property.isDeprecated())
|
||||||
|
.map(ConfigurationProperty::getName).collect(Collectors.toList());
|
||||||
|
Map<String, CompoundConfigurationTableEntry> overrides = getOverrides(metadataProperties, unmappedKeys,
|
||||||
|
options);
|
||||||
|
options.getMetadataSections().forEach((id, keyPrefixes) -> tables
|
||||||
|
.add(createConfigTable(metadataProperties, unmappedKeys, overrides, id, keyPrefixes)));
|
||||||
|
if (!unmappedKeys.isEmpty()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"The following keys were not written to the documentation: " + String.join(", ", unmappedKeys));
|
||||||
|
}
|
||||||
|
if (!overrides.isEmpty()) {
|
||||||
|
throw new IllegalStateException("The following keys were not written to the documentation: "
|
||||||
|
+ String.join(", ", overrides.keySet()));
|
||||||
|
}
|
||||||
|
return tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, CompoundConfigurationTableEntry> getOverrides(
|
||||||
|
Map<String, ConfigurationProperty> metadataProperties, List<String> unmappedKeys, DocumentOptions options) {
|
||||||
|
Map<String, CompoundConfigurationTableEntry> overrides = new HashMap<>();
|
||||||
|
options.getOverrides().forEach((keyPrefix, description) -> {
|
||||||
|
CompoundConfigurationTableEntry entry = new CompoundConfigurationTableEntry(keyPrefix, description);
|
||||||
|
List<String> matchingKeys = unmappedKeys.stream().filter((key) -> key.startsWith(keyPrefix))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for (String matchingKey : matchingKeys) {
|
||||||
|
entry.addConfigurationKeys(metadataProperties.get(matchingKey));
|
||||||
|
}
|
||||||
|
overrides.put(keyPrefix, entry);
|
||||||
|
unmappedKeys.removeAll(matchingKeys);
|
||||||
|
});
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigurationTable createConfigTable(Map<String, ConfigurationProperty> metadataProperties,
|
||||||
|
List<String> unmappedKeys, Map<String, CompoundConfigurationTableEntry> overrides, String id,
|
||||||
|
List<String> keyPrefixes) {
|
||||||
|
ConfigurationTable table = new ConfigurationTable(id);
|
||||||
|
for (String keyPrefix : keyPrefixes) {
|
||||||
|
List<String> matchingOverrides = overrides.keySet().stream()
|
||||||
|
.filter((overrideKey) -> overrideKey.startsWith(keyPrefix)).collect(Collectors.toList());
|
||||||
|
matchingOverrides.forEach((match) -> table.addEntry(overrides.remove(match)));
|
||||||
|
}
|
||||||
|
List<String> matchingKeys = unmappedKeys.stream()
|
||||||
|
.filter((key) -> keyPrefixes.stream().anyMatch(key::startsWith)).collect(Collectors.toList());
|
||||||
|
for (String matchingKey : matchingKeys) {
|
||||||
|
ConfigurationProperty property = metadataProperties.get(matchingKey);
|
||||||
|
table.addEntry(new SingleConfigurationTableEntry(property));
|
||||||
|
}
|
||||||
|
unmappedKeys.removeAll(matchingKeys);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeConfigurationTable(ConfigurationTable table, Path outputDirectory) throws IOException {
|
||||||
|
Path outputFilePath = outputDirectory.resolve(table.getId() + ".adoc");
|
||||||
|
Files.deleteIfExists(outputFilePath);
|
||||||
|
Files.createFile(outputFilePath);
|
||||||
|
try (OutputStream outputStream = Files.newOutputStream(outputFilePath)) {
|
||||||
|
outputStream.write(table.toAsciidocTable().getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asciidoctor table listing configuration properties sharing to a common theme.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class ConfigurationTable {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
private final Set<ConfigurationTableEntry> entries;
|
||||||
|
|
||||||
|
ConfigurationTable(String id) {
|
||||||
|
this.id = id;
|
||||||
|
this.entries = new TreeSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addEntry(ConfigurationTableEntry... entries) {
|
||||||
|
this.entries.addAll(Arrays.asList(entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
String toAsciidocTable() {
|
||||||
|
AsciidocBuilder builder = new AsciidocBuilder();
|
||||||
|
builder.appendln("[cols=\"2,1,1\", options=\"header\"]");
|
||||||
|
builder.appendln("|===");
|
||||||
|
builder.appendln("|Key|Default Value|Description");
|
||||||
|
builder.appendln();
|
||||||
|
this.entries.forEach((entry) -> {
|
||||||
|
entry.write(builder);
|
||||||
|
builder.appendln();
|
||||||
|
});
|
||||||
|
return builder.appendln("|===").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for entries in {@link ConfigurationTable}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
abstract class ConfigurationTableEntry implements Comparable<ConfigurationTableEntry> {
|
||||||
|
|
||||||
|
protected String key;
|
||||||
|
|
||||||
|
String getKey() {
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract void write(AsciidocBuilder builder);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ConfigurationTableEntry other = (ConfigurationTableEntry) obj;
|
||||||
|
return this.key.equals(other.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.key.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(ConfigurationTableEntry other) {
|
||||||
|
return this.key.compareTo(other.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for generating documentation for configuration properties.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public final class DocumentOptions {
|
||||||
|
|
||||||
|
private final Map<String, List<String>> metadataSections;
|
||||||
|
|
||||||
|
private final Map<String, String> overrides;
|
||||||
|
|
||||||
|
private DocumentOptions(Map<String, List<String>> metadataSections, Map<String, String> overrides) {
|
||||||
|
this.metadataSections = metadataSections;
|
||||||
|
this.overrides = overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> getMetadataSections() {
|
||||||
|
return this.metadataSections;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> getOverrides() {
|
||||||
|
return this.overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder for DocumentOptions.
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
Map<String, List<String>> metadataSections = new HashMap<>();
|
||||||
|
|
||||||
|
Map<String, String> overrides = new HashMap<>();
|
||||||
|
|
||||||
|
SectionSpec addSection(String name) {
|
||||||
|
return new SectionSpec(this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder addOverride(String keyPrefix, String description) {
|
||||||
|
this.overrides.put(keyPrefix, description);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentOptions build() {
|
||||||
|
return new DocumentOptions(this.metadataSections, this.overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for a documentation section listing properties for a specific theme.
|
||||||
|
*/
|
||||||
|
public static class SectionSpec {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final Builder builder;
|
||||||
|
|
||||||
|
SectionSpec(Builder builder, String name) {
|
||||||
|
this.builder = builder;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder withKeyPrefixes(String... keyPrefixes) {
|
||||||
|
this.builder.metadataSections.put(this.name, Arrays.asList(keyPrefixes));
|
||||||
|
return this.builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2021 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class for rows in {@link Table}.
|
|
||||||
*
|
|
||||||
* @author Brian Clozel
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
abstract class Row implements Comparable<Row> {
|
|
||||||
|
|
||||||
private final Snippet snippet;
|
|
||||||
|
|
||||||
private final String id;
|
|
||||||
|
|
||||||
protected Row(Snippet snippet, String id) {
|
|
||||||
this.snippet = snippet;
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (this == obj) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj == null || getClass() != obj.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Row other = (Row) obj;
|
|
||||||
return this.id.equals(other.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return this.id.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(Row other) {
|
|
||||||
return this.id.compareTo(other.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
String getAnchor() {
|
|
||||||
return this.snippet.getAnchor() + "." + this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract void write(Asciidoc asciidoc);
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2020 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.boot.build.context.properties;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table entry containing a single configuration property.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
class SingleConfigurationTableEntry extends ConfigurationTableEntry {
|
||||||
|
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
private final String defaultValue;
|
||||||
|
|
||||||
|
private final String anchor;
|
||||||
|
|
||||||
|
SingleConfigurationTableEntry(ConfigurationProperty property) {
|
||||||
|
this.key = property.getName();
|
||||||
|
this.anchor = this.key;
|
||||||
|
if (property.getType() != null && property.getType().startsWith("java.util.Map")) {
|
||||||
|
this.key += ".*";
|
||||||
|
}
|
||||||
|
this.description = property.getDescription();
|
||||||
|
this.defaultValue = getDefaultValue(property.getDefaultValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDefaultValue(Object defaultValue) {
|
||||||
|
if (defaultValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (defaultValue.getClass().isArray()) {
|
||||||
|
return Arrays.stream((Object[]) defaultValue).map(Object::toString)
|
||||||
|
.collect(Collectors.joining("," + System.lineSeparator()));
|
||||||
|
}
|
||||||
|
return defaultValue.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void write(AsciidocBuilder builder) {
|
||||||
|
builder.appendln("|[[" + this.anchor + "]]<<" + this.anchor + ",`+", this.key, "+`>>");
|
||||||
|
writeDefaultValue(builder);
|
||||||
|
writeDescription(builder);
|
||||||
|
builder.appendln();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDefaultValue(AsciidocBuilder builder) {
|
||||||
|
String defaultValue = (this.defaultValue != null) ? this.defaultValue : "";
|
||||||
|
if (defaultValue.isEmpty()) {
|
||||||
|
builder.appendln("|");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|");
|
||||||
|
builder.appendln("|`+", defaultValue, "+`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDescription(AsciidocBuilder builder) {
|
||||||
|
if (this.description == null || this.description.isEmpty()) {
|
||||||
|
builder.append("|");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
String cleanedDescription = this.description.replace("|", "\\|").replace("<", "<").replace(">", ">");
|
||||||
|
builder.append("|+++", cleanedDescription, "+++");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table row containing a single configuration property.
|
|
||||||
*
|
|
||||||
* @author Brian Clozel
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class SingleRow extends Row {
|
|
||||||
|
|
||||||
private final String displayName;
|
|
||||||
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
private final String defaultValue;
|
|
||||||
|
|
||||||
SingleRow(Snippet snippet, ConfigurationProperty property) {
|
|
||||||
super(snippet, property.getName());
|
|
||||||
this.displayName = property.getDisplayName();
|
|
||||||
this.description = property.getDescription();
|
|
||||||
this.defaultValue = getDefaultValue(property.getDefaultValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDefaultValue(Object defaultValue) {
|
|
||||||
if (defaultValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (defaultValue.getClass().isArray()) {
|
|
||||||
return Arrays.stream((Object[]) defaultValue)
|
|
||||||
.map(Object::toString)
|
|
||||||
.collect(Collectors.joining("," + System.lineSeparator()));
|
|
||||||
}
|
|
||||||
return defaultValue.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void write(Asciidoc asciidoc) {
|
|
||||||
asciidoc.append("|");
|
|
||||||
asciidoc.append("[[" + getAnchor() + "]]");
|
|
||||||
asciidoc.appendln("<<" + getAnchor() + ",`+", this.displayName, "+`>>");
|
|
||||||
writeDescription(asciidoc);
|
|
||||||
writeDefaultValue(asciidoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeDescription(Asciidoc builder) {
|
|
||||||
if (this.description == null || this.description.isEmpty()) {
|
|
||||||
builder.appendln("|");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
String cleanedDescription = this.description.replace("|", "\\|").replace("<", "<").replace(">", ">");
|
|
||||||
builder.appendln("|+++", cleanedDescription, "+++");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeDefaultValue(Asciidoc builder) {
|
|
||||||
String defaultValue = (this.defaultValue != null) ? this.defaultValue : "";
|
|
||||||
if (defaultValue.isEmpty()) {
|
|
||||||
builder.appendln("|");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|");
|
|
||||||
builder.appendln("|`+", defaultValue, "+`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,102 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2021 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A configuration properties snippet.
|
|
||||||
*
|
|
||||||
* @author Brian Clozed
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class Snippet {
|
|
||||||
|
|
||||||
private final String anchor;
|
|
||||||
|
|
||||||
private final String title;
|
|
||||||
|
|
||||||
private final Set<String> prefixes;
|
|
||||||
|
|
||||||
private final Map<String, String> overrides;
|
|
||||||
|
|
||||||
Snippet(String anchor, String title, Consumer<Config> config) {
|
|
||||||
Set<String> prefixes = new LinkedHashSet<>();
|
|
||||||
Map<String, String> overrides = new LinkedHashMap<>();
|
|
||||||
if (config != null) {
|
|
||||||
config.accept(new Config() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(String prefix) {
|
|
||||||
prefixes.add(prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void accept(String prefix, String description) {
|
|
||||||
overrides.put(prefix, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.anchor = anchor;
|
|
||||||
this.title = title;
|
|
||||||
this.prefixes = prefixes;
|
|
||||||
this.overrides = overrides;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getAnchor() {
|
|
||||||
return this.anchor;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTitle() {
|
|
||||||
return this.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
void forEachPrefix(Consumer<String> action) {
|
|
||||||
this.prefixes.forEach(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
void forEachOverride(BiConsumer<String, String> action) {
|
|
||||||
this.overrides.forEach(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback to configure the snippet.
|
|
||||||
*/
|
|
||||||
interface Config {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accept the given prefix using the meta-data description.
|
|
||||||
* @param prefix the prefix to accept
|
|
||||||
*/
|
|
||||||
void accept(String prefix);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accept the given prefix with a defined description.
|
|
||||||
* @param prefix the prefix to accept
|
|
||||||
* @param description the description to use
|
|
||||||
*/
|
|
||||||
void accept(String prefix, String description);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2012-2023 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.boot.build.context.properties;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.gradle.api.file.FileCollection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration properties snippets.
|
|
||||||
*
|
|
||||||
* @author Brian Clozed
|
|
||||||
* @author Phillip Webb
|
|
||||||
*/
|
|
||||||
class Snippets {
|
|
||||||
|
|
||||||
private final ConfigurationProperties properties;
|
|
||||||
|
|
||||||
private final List<Snippet> snippets = new ArrayList<>();
|
|
||||||
|
|
||||||
Snippets(FileCollection configurationPropertyMetadata) {
|
|
||||||
this.properties = ConfigurationProperties.fromFiles(configurationPropertyMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(String anchor, String title, Consumer<Snippet.Config> config) {
|
|
||||||
this.snippets.add(new Snippet(anchor, title, config));
|
|
||||||
}
|
|
||||||
|
|
||||||
void writeTo(Path outputDirectory) throws IOException {
|
|
||||||
createDirectory(outputDirectory);
|
|
||||||
Set<String> remaining = this.properties.stream()
|
|
||||||
.filter((property) -> !property.isDeprecated())
|
|
||||||
.map(ConfigurationProperty::getName)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
for (Snippet snippet : this.snippets) {
|
|
||||||
Set<String> written = writeSnippet(outputDirectory, snippet, remaining);
|
|
||||||
remaining.removeAll(written);
|
|
||||||
}
|
|
||||||
if (!remaining.isEmpty()) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"The following keys were not written to the documentation: " + String.join(", ", remaining));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> writeSnippet(Path outputDirectory, Snippet snippet, Set<String> remaining) throws IOException {
|
|
||||||
Table table = new Table();
|
|
||||||
Set<String> added = new HashSet<>();
|
|
||||||
snippet.forEachOverride((prefix, description) -> {
|
|
||||||
CompoundRow row = new CompoundRow(snippet, prefix, description);
|
|
||||||
remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> {
|
|
||||||
if (added.add(name)) {
|
|
||||||
row.addProperty(this.properties.get(name));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
table.addRow(row);
|
|
||||||
});
|
|
||||||
snippet.forEachPrefix((prefix) -> {
|
|
||||||
remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> {
|
|
||||||
if (added.add(name)) {
|
|
||||||
table.addRow(new SingleRow(snippet, this.properties.get(name)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Asciidoc asciidoc = getAsciidoc(snippet, table);
|
|
||||||
writeAsciidoc(outputDirectory, snippet, asciidoc);
|
|
||||||
return added;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Asciidoc getAsciidoc(Snippet snippet, Table table) {
|
|
||||||
Asciidoc asciidoc = new Asciidoc();
|
|
||||||
// We have to prepend 'appendix.' as a section id here, otherwise the
|
|
||||||
// spring-asciidoctor-extensions:section-id asciidoctor extension complains
|
|
||||||
asciidoc.appendln("[[appendix." + snippet.getAnchor() + "]]");
|
|
||||||
asciidoc.appendln("== ", snippet.getTitle());
|
|
||||||
table.write(asciidoc);
|
|
||||||
return asciidoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeAsciidoc(Path outputDirectory, Snippet snippet, Asciidoc asciidoc) throws IOException {
|
|
||||||
String[] parts = (snippet.getAnchor()).split("\\.");
|
|
||||||
Path path = outputDirectory;
|
|
||||||
for (int i = 0; i < parts.length; i++) {
|
|
||||||
String name = (i < parts.length - 1) ? parts[i] : parts[i] + ".adoc";
|
|
||||||
path = path.resolve(name);
|
|
||||||
}
|
|
||||||
createDirectory(path.getParent());
|
|
||||||
Files.deleteIfExists(path);
|
|
||||||
try (OutputStream outputStream = Files.newOutputStream(path)) {
|
|
||||||
outputStream.write(asciidoc.toString().getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createDirectory(Path path) throws IOException {
|
|
||||||
assertValidOutputDirectory(path);
|
|
||||||
if (!Files.exists(path)) {
|
|
||||||
Files.createDirectory(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertValidOutputDirectory(Path path) {
|
|
||||||
if (path == null) {
|
|
||||||
throw new IllegalArgumentException("Directory path should not be null");
|
|
||||||
}
|
|
||||||
if (Files.exists(path) && !Files.isDirectory(path)) {
|
|
||||||
throw new IllegalArgumentException("Path already exists and is not a directory");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue