diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java index c0ba598da47..3c31f601bfe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java @@ -25,6 +25,7 @@ import java.io.InputStream; * Encapsulates information about a single library that may be packed into the archive. * * @author Phillip Webb + * @author Scott Frederick * @since 1.1.2 * @see Libraries */ @@ -38,6 +39,8 @@ public class Library { private final boolean unpackRequired; + private final LibraryCoordinates coordinates; + /** * Create a new {@link Library}. * @param file the source file @@ -66,10 +69,15 @@ public class Library { * @param unpackRequired if the library needs to be unpacked before it can be used */ public Library(String name, File file, LibraryScope scope, boolean unpackRequired) { + this(name, file, scope, unpackRequired, null); + } + + public Library(String name, File file, LibraryScope scope, boolean unpackRequired, LibraryCoordinates coordinates) { this.name = (name != null) ? name : file.getName(); this.file = file; this.scope = scope; this.unpackRequired = unpackRequired; + this.coordinates = coordinates; } /** @@ -114,6 +122,14 @@ public class Library { return this.unpackRequired; } + /** + * Return the {@linkplain LibraryCoordinates coordinates} of the library. + * @return the coordinates + */ + public LibraryCoordinates getCoordinates() { + return this.coordinates; + } + long getLastModified() { return this.file.lastModified(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java new file mode 100644 index 00000000000..8167d1f2b92 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java @@ -0,0 +1,77 @@ +/* + * 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.loader.tools; + +import org.springframework.util.Assert; + +/** + * Encapsulates information about the Maven artifact coordinates of a library. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public final class LibraryCoordinates { + + private final String groupId; + + private final String artifactId; + + private final String version; + + /** + * Create a new instance from discrete elements. + * @param groupId the group ID + * @param artifactId the artifact ID + * @param version the version + */ + public LibraryCoordinates(String groupId, String artifactId, String version) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + /** + * Create a new instance from a String value in the form + * {@code groupId:artifactId:version} where the version is optional. + * @param coordinates the coordinates + */ + public LibraryCoordinates(String coordinates) { + String[] elements = coordinates.split(":"); + Assert.isTrue(elements.length >= 2, "Coordinates must contain at least 'groupId:artifactId'"); + this.groupId = elements[0]; + this.artifactId = elements[1]; + if (elements.length > 2) { + this.version = elements[2]; + } + else { + this.version = null; + } + } + + public String getGroupId() { + return this.groupId; + } + + public String getArtifactId() { + return this.artifactId; + } + + public String getVersion() { + return this.version; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java new file mode 100644 index 00000000000..b4847ead1b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java @@ -0,0 +1,77 @@ +/* + * 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.loader.tools.layer; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Layers; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.layer.classes.ResourceStrategy; +import org.springframework.boot.loader.tools.layer.library.LibraryStrategy; + +/** + * Implementation of {@link Layers} representing user-provided layers. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public class CustomLayers implements Layers { + + private final List layers; + + private final List resourceStrategies; + + private final List libraryStrategies; + + public CustomLayers(List layers, List resourceStrategies, + List libraryStrategies) { + this.layers = new ArrayList<>(layers); + this.resourceStrategies = new ArrayList<>(resourceStrategies); + this.libraryStrategies = new ArrayList<>(libraryStrategies); + } + + @Override + public Iterator iterator() { + return this.layers.iterator(); + } + + @Override + public Layer getLayer(String resourceName) { + for (ResourceStrategy strategy : this.resourceStrategies) { + Layer matchingLayer = strategy.getMatchingLayer(resourceName); + if (matchingLayer != null) { + return matchingLayer; + } + } + throw new IllegalStateException("Resource '" + resourceName + "' did not match any layer."); + } + + @Override + public Layer getLayer(Library library) { + for (LibraryStrategy strategy : this.libraryStrategies) { + Layer matchingLayer = strategy.getMatchingLayer(library); + if (matchingLayer != null) { + return matchingLayer; + } + } + throw new IllegalStateException("Library '" + library.getName() + "' did not match any layer."); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/AbstractResourceFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/AbstractResourceFilter.java new file mode 100644 index 00000000000..80387279ef1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/AbstractResourceFilter.java @@ -0,0 +1,51 @@ +/* + * 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.loader.tools.layer.classes; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for {@link ResourceFilter} implementations. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public abstract class AbstractResourceFilter implements ResourceFilter { + + private final List includes = new ArrayList<>(); + + private final List excludes = new ArrayList<>(); + + public AbstractResourceFilter(List includes, List excludes) { + this.includes.addAll(includes); + this.excludes.addAll(excludes); + } + + @Override + public boolean isResourceIncluded(String resourceName) { + return isMatch(resourceName, this.includes); + } + + @Override + public boolean isResourceExcluded(String resourceName) { + return isMatch(resourceName, this.excludes); + } + + protected abstract boolean isMatch(String resourceName, List toMatch); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/FilteredResourceStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/FilteredResourceStrategy.java new file mode 100644 index 00000000000..cbbae4699ed --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/FilteredResourceStrategy.java @@ -0,0 +1,61 @@ +/* + * 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.loader.tools.layer.classes; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.util.Assert; + +/** + * A {@link ResourceStrategy} with custom filters. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public class FilteredResourceStrategy implements ResourceStrategy { + + private final Layer layer; + + private final List filters = new ArrayList<>(); + + public FilteredResourceStrategy(String layer, List filters) { + Assert.notEmpty(filters, "Filters should not be empty for custom strategy."); + this.layer = new Layer(layer); + this.filters.addAll(filters); + } + + public Layer getLayer() { + return this.layer; + } + + @Override + public Layer getMatchingLayer(String resourceName) { + boolean isIncluded = false; + for (ResourceFilter filter : this.filters) { + if (filter.isResourceExcluded(resourceName)) { + return null; + } + if (!isIncluded && filter.isResourceIncluded(resourceName)) { + isIncluded = true; + } + } + return (isIncluded) ? this.layer : null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/LocationFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/LocationFilter.java new file mode 100644 index 00000000000..6a9dbb1f388 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/LocationFilter.java @@ -0,0 +1,42 @@ +/* + * 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.loader.tools.layer.classes; + +import java.util.List; + +import org.springframework.util.AntPathMatcher; + +/** + * An implementation of {@link ResourceFilter} based on the resource location. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public class LocationFilter extends AbstractResourceFilter { + + private static final AntPathMatcher MATCHER = new AntPathMatcher(); + + public LocationFilter(List includes, List excludes) { + super(includes, excludes); + } + + @Override + protected boolean isMatch(String resourceName, List toMatch) { + return toMatch.stream().anyMatch((pattern) -> MATCHER.match(pattern, resourceName)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java new file mode 100644 index 00000000000..cc87fa45493 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceFilter.java @@ -0,0 +1,41 @@ +/* + * 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.loader.tools.layer.classes; + +/** + * A filter that can tell if a resource has been included or excluded. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public interface ResourceFilter { + + /** + * Return true if the resource is included by the filter. + * @param resourceName the resource name + * @return true if the resource is included + */ + boolean isResourceIncluded(String resourceName); + + /** + * Return true if the resource is included by the filter. + * @param resourceName the resource name + * @return true if the resource is excluded + */ + boolean isResourceExcluded(String resourceName); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java new file mode 100644 index 00000000000..e041bb590d0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/classes/ResourceStrategy.java @@ -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.loader.tools.layer.classes; + +import org.springframework.boot.loader.tools.Layer; + +/** + * A strategy used to match a resource to a layer. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public interface ResourceStrategy { + + /** + * Return a {@link Layer} for the given resource. If no matching layer is found, + * {@code null} is returned. + * @param resourceName the name of the resource + * @return the matching layer or {@code null} + */ + Layer getMatchingLayer(String resourceName); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java new file mode 100644 index 00000000000..0e1f7337fd5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilter.java @@ -0,0 +1,100 @@ +/* + * 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.loader.tools.layer.library; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; + +/** + * An implementation of {@link LibraryFilter} based on the library's coordinates. + * + * @author Madhura Bhave + * @author Scott Frederick + * @since 2.3.0 + */ +public class CoordinateFilter implements LibraryFilter { + + private final List includes = new ArrayList<>(); + + private final List excludes = new ArrayList<>(); + + public CoordinateFilter(List includes, List excludes) { + this.includes.addAll(includes); + this.excludes.addAll(excludes); + } + + @Override + public boolean isLibraryIncluded(Library library) { + return isMatch(library, this.includes); + } + + @Override + public boolean isLibraryExcluded(Library library) { + return isMatch(library, this.excludes); + } + + private boolean isMatch(Library library, List toMatch) { + StringBuilder builder = new StringBuilder(); + LibraryCoordinates coordinates = library.getCoordinates(); + if (coordinates != null) { + if (coordinates.getGroupId() != null) { + builder.append(coordinates.getGroupId()); + } + builder.append(":"); + if (coordinates.getArtifactId() != null) { + builder.append(coordinates.getArtifactId()); + } + builder.append(":"); + if (coordinates.getVersion() != null) { + builder.append(coordinates.getVersion()); + } + } + else { + builder.append("::"); + } + String input = builder.toString(); + for (String patternString : toMatch) { + Pattern pattern = buildPatternForString(patternString); + if (pattern.matcher(input).matches()) { + return true; + } + } + return false; + } + + private Pattern buildPatternForString(String pattern) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '.') { + builder.append("\\."); + } + else if (c == '*') { + builder.append(".*"); + } + else { + builder.append(c); + } + } + return Pattern.compile(builder.toString()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java new file mode 100644 index 00000000000..3cb6cfb0153 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategy.java @@ -0,0 +1,62 @@ +/* + * 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.loader.tools.layer.library; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Library; +import org.springframework.util.Assert; + +/** + * A {@link LibraryStrategy} with custom filters. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public class FilteredLibraryStrategy implements LibraryStrategy { + + private final Layer layer; + + private final List filters = new ArrayList<>(); + + public FilteredLibraryStrategy(String layer, List filters) { + Assert.notEmpty(filters, "Filters should not be empty for custom strategy."); + this.layer = new Layer(layer); + this.filters.addAll(filters); + } + + public Layer getLayer() { + return this.layer; + } + + @Override + public Layer getMatchingLayer(Library library) { + boolean isIncluded = false; + for (LibraryFilter filter : this.filters) { + if (filter.isLibraryExcluded(library)) { + return null; + } + if (!isIncluded && filter.isLibraryIncluded(library)) { + isIncluded = true; + } + } + return (isIncluded) ? this.layer : null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java new file mode 100644 index 00000000000..ea1ae8e465a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryFilter.java @@ -0,0 +1,43 @@ +/* + * 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.loader.tools.layer.library; + +import org.springframework.boot.loader.tools.Library; + +/** + * A filter that can tell if a {@link Library} has been included or excluded. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public interface LibraryFilter { + + /** + * Return true if the {@link Library} is included by the filter. + * @param library the library + * @return true if the library is included + */ + boolean isLibraryIncluded(Library library); + + /** + * Return true if the {@link Library} is excluded by the filter. + * @param library the library + * @return true if the library is excluded + */ + boolean isLibraryExcluded(Library library); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java new file mode 100644 index 00000000000..93e9b3ff72e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/library/LibraryStrategy.java @@ -0,0 +1,38 @@ +/* + * 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.loader.tools.layer.library; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Library; + +/** + * A strategy used to match a library to a layer. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public interface LibraryStrategy { + + /** + * Return a {@link Layer} for the given {@link Library}. If no matching layer is + * found, {@code null} is returned. + * @param library the library + * @return the matching layer or {@code null} + */ + Layer getMatchingLayer(Library library); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java new file mode 100644 index 00000000000..b110b5c3f4a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java @@ -0,0 +1,68 @@ +/* + * 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.loader.tools; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link LibraryCoordinates}. + * + * @author Scott Frederick + */ +class LibraryCoordinatesTests { + + @Test + void parseCoordinatesWithAllElements() { + LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library:1.0.0"); + assertThat(coordinates.getGroupId()).isEqualTo("com.acme"); + assertThat(coordinates.getArtifactId()).isEqualTo("my-library"); + assertThat(coordinates.getVersion()).isEqualTo("1.0.0"); + } + + @Test + void parseCoordinatesWithoutVersion() { + LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library"); + assertThat(coordinates.getGroupId()).isEqualTo("com.acme"); + assertThat(coordinates.getArtifactId()).isEqualTo("my-library"); + assertThat(coordinates.getVersion()).isNull(); + } + + @Test + void parseCoordinatesWithEmptyElements() { + LibraryCoordinates coordinates = new LibraryCoordinates(":my-library:"); + assertThat(coordinates.getGroupId()).isEqualTo(""); + assertThat(coordinates.getArtifactId()).isEqualTo("my-library"); + assertThat(coordinates.getVersion()).isNull(); + } + + @Test + void parseCoordinatesWithExtraElements() { + LibraryCoordinates coordinates = new LibraryCoordinates("com.acme:my-library:1.0.0.BUILD-SNAPSHOT:11111"); + assertThat(coordinates.getGroupId()).isEqualTo("com.acme"); + assertThat(coordinates.getArtifactId()).isEqualTo("my-library"); + assertThat(coordinates.getVersion()).isEqualTo("1.0.0.BUILD-SNAPSHOT"); + } + + @Test + void parseCoordinatesWithoutMinimumElements() { + assertThatIllegalArgumentException().isThrownBy(() -> new LibraryCoordinates("com.acme")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java new file mode 100644 index 00000000000..09577065bf2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/CustomLayersTests.java @@ -0,0 +1,101 @@ +/* + * 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.loader.tools.layer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy; +import org.springframework.boot.loader.tools.layer.classes.LocationFilter; +import org.springframework.boot.loader.tools.layer.library.CoordinateFilter; +import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CustomLayers}. + * + * @author Stephane Nicoll + */ +class CustomLayersTests { + + @Test + void customLayersAreAvailable() { + Layer first = new Layer("first"); + Layer second = new Layer("second"); + CustomLayers customLayers = new CustomLayers(Arrays.asList(first, second), Collections.emptyList(), + Collections.emptyList()); + List actualLayers = new ArrayList<>(); + customLayers.iterator().forEachRemaining(actualLayers::add); + assertThat(actualLayers).containsExactly(first, second); + } + + @Test + void layerForResourceIsFound() { + FilteredResourceStrategy resourceStrategy = new FilteredResourceStrategy("test", Collections + .singletonList(new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList()))); + Layer targetLayer = new Layer("test"); + CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer), + Collections.singletonList(resourceStrategy), Collections.emptyList()); + assertThat(customLayers.getLayer("META-INF/manifest.mf")).isNotNull().isEqualTo(targetLayer); + } + + @Test + void layerForResourceIsNotFound() { + FilteredResourceStrategy resourceStrategy = new FilteredResourceStrategy("test", Collections + .singletonList(new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList()))); + CustomLayers customLayers = new CustomLayers(Collections.singletonList(new Layer("test")), + Collections.singletonList(resourceStrategy), Collections.emptyList()); + assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer("com/acme")); + } + + @Test + void layerForLibraryIsFound() { + FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test", Collections + .singletonList(new CoordinateFilter(Collections.singletonList("com.acme:*"), Collections.emptyList()))); + Layer targetLayer = new Layer("test"); + CustomLayers customLayers = new CustomLayers(Collections.singletonList(targetLayer), Collections.emptyList(), + Collections.singletonList(libraryStrategy)); + assertThat(customLayers.getLayer(mockLibrary("com.acme:test"))).isNotNull().isEqualTo(targetLayer); + } + + @Test + void layerForLibraryIsNotFound() { + FilteredLibraryStrategy libraryStrategy = new FilteredLibraryStrategy("test", Collections + .singletonList(new CoordinateFilter(Collections.singletonList("com.acme:*"), Collections.emptyList()))); + CustomLayers customLayers = new CustomLayers(Collections.singletonList(new Layer("test")), + Collections.emptyList(), Collections.singletonList(libraryStrategy)); + assertThatIllegalStateException().isThrownBy(() -> customLayers.getLayer(mockLibrary("org.another:test"))); + } + + private Library mockLibrary(String coordinates) { + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates(coordinates)); + return library; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/classes/FilteredResourceStrategyTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/classes/FilteredResourceStrategyTests.java new file mode 100644 index 00000000000..3689e78f532 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/classes/FilteredResourceStrategyTests.java @@ -0,0 +1,104 @@ +/* + * 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.loader.tools.layer.classes; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link FilteredResourceStrategy}. + * + * @author Madhura Bhave + */ +class FilteredResourceStrategyTests { + + @Test + void createWhenFiltersNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new FilteredResourceStrategy("custom", null)); + } + + @Test + void createWhenFiltersEmptyShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FilteredResourceStrategy("custom", Collections.emptyList())); + } + + @Test + void getLayerShouldReturnLayerName() { + FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom", + Collections.singletonList(new TestFilter1())); + assertThat(strategy.getLayer().toString()).isEqualTo("custom"); + } + + @Test + void getMatchingLayerWhenFilterMatchesIncludes() { + FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom", + Collections.singletonList(new TestFilter1())); + assertThat(strategy.getMatchingLayer("ABCD").toString()).isEqualTo("custom"); + } + + @Test + void matchesWhenFilterMatchesIncludesAndExcludesFromSameFilter() { + FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom", + Collections.singletonList(new TestFilter1())); + assertThat(strategy.getMatchingLayer("AZ")).isNull(); + } + + @Test + void matchesWhenFilterMatchesIncludesAndExcludesFromAnotherFilter() { + List filters = new ArrayList<>(); + filters.add(new TestFilter1()); + filters.add(new TestFilter2()); + FilteredResourceStrategy strategy = new FilteredResourceStrategy("custom", filters); + assertThat(strategy.getMatchingLayer("AY")).isNull(); + } + + private static class TestFilter1 implements ResourceFilter { + + @Override + public boolean isResourceIncluded(String resourceName) { + return resourceName.startsWith("A"); + } + + @Override + public boolean isResourceExcluded(String resourceName) { + return resourceName.endsWith("Z"); + } + + } + + private static class TestFilter2 implements ResourceFilter { + + @Override + public boolean isResourceIncluded(String resourceName) { + return resourceName.startsWith("B"); + } + + @Override + public boolean isResourceExcluded(String resourceName) { + return resourceName.endsWith("Y"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/classes/LocationFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/classes/LocationFilterTests.java new file mode 100644 index 00000000000..805906c7a35 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/classes/LocationFilterTests.java @@ -0,0 +1,57 @@ +/* + * 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.loader.tools.layer.classes; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LocationFilter}. + * + * @author Madhura Bhave + * @author Stephane Nicoll + */ +class LocationFilterTests { + + @Test + void isResourceIncludedWhenPatternMatchesWithWildcard() { + LocationFilter filter = new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList()); + assertThat(filter.isResourceIncluded("META-INF/resources/application.yml")).isTrue(); + } + + @Test + void isResourceIncludedWhenPatternDoesNotMatch() { + LocationFilter filter = new LocationFilter(Collections.singletonList("META-INF/**"), Collections.emptyList()); + assertThat(filter.isResourceIncluded("src/main/resources/application.yml")).isFalse(); + } + + @Test + void isResourceExcludedWhenPatternMatchesWithWildcard() { + LocationFilter filter = new LocationFilter(Collections.emptyList(), Collections.singletonList("META-INF/**")); + assertThat(filter.isResourceExcluded("META-INF/resources/application.yml")).isTrue(); + } + + @Test + void isResourceExcludedWhenPatternDoesNotMatch() { + LocationFilter filter = new LocationFilter(Collections.emptyList(), Collections.singletonList("META-INF/**")); + assertThat(filter.isResourceExcluded("src/main/resources/application.yml")).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java new file mode 100644 index 00000000000..864f27add76 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/CoordinateFilterTests.java @@ -0,0 +1,111 @@ +/* + * 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.loader.tools.layer.library; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CoordinateFilter}. + * + * @author Madhura Bhave + * @author Scott Frederick + */ +class CoordinateFilterTests { + + @Test + void isLibraryIncludedWhenGroupIdIsNullAndToMatchHasWildcard() { + List includes = Collections.singletonList("*:*"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates(null, null, null)); + assertThat(filter.isLibraryIncluded(library)).isTrue(); + } + + @Test + void isLibraryIncludedWhenArtifactIdIsNullAndToMatchHasWildcard() { + List includes = Collections.singletonList("org.acme:*"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", null, null)); + assertThat(filter.isLibraryIncluded(library)).isTrue(); + } + + @Test + void isLibraryIncludedWhenVersionIsNullAndToMatchHasWildcard() { + List includes = Collections.singletonList("org.acme:something:*"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "something", null)); + assertThat(filter.isLibraryIncluded(library)).isTrue(); + } + + @Test + void isLibraryIncludedWhenGroupIdDoesNotMatch() { + List includes = Collections.singletonList("org.acme:*"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("other.foo", null, null)); + assertThat(filter.isLibraryIncluded(library)).isFalse(); + } + + @Test + void isLibraryIncludedWhenArtifactIdDoesNotMatch() { + List includes = Collections.singletonList("org.acme:test:*"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "other", null)); + assertThat(filter.isLibraryIncluded(library)).isFalse(); + } + + @Test + void isLibraryIncludedWhenArtifactIdMatches() { + List includes = Collections.singletonList("org.acme:test:*"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "test", null)); + assertThat(filter.isLibraryIncluded(library)).isTrue(); + } + + @Test + void isLibraryIncludedWhenVersionDoesNotMatch() { + List includes = Collections.singletonList("org.acme:test:*SNAPSHOT"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "test", "1.0.0")); + assertThat(filter.isLibraryIncluded(library)).isFalse(); + } + + @Test + void isLibraryIncludedWhenVersionMatches() { + List includes = Collections.singletonList("org.acme:test:*SNAPSHOT"); + CoordinateFilter filter = new CoordinateFilter(includes, Collections.emptyList()); + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(new LibraryCoordinates("org.acme", "test", "1.0.0-SNAPSHOT")); + assertThat(filter.isLibraryIncluded(library)).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java new file mode 100644 index 00000000000..5607512d2f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/library/FilteredLibraryStrategyTests.java @@ -0,0 +1,119 @@ +/* + * 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.loader.tools.layer.library; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryScope; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link FilteredLibraryStrategy}. + * + * @author Madhura Bhave + */ +class FilteredLibraryStrategyTests { + + @Test + void createWhenFiltersNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new FilteredLibraryStrategy("custom", null)); + } + + @Test + void createWhenFiltersEmptyShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FilteredLibraryStrategy("custom", Collections.emptyList())); + } + + @Test + void getLayerShouldReturnLayerName() { + FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom", + Collections.singletonList(new TestFilter1Library())); + assertThat(strategy.getLayer().toString()).isEqualTo("custom"); + } + + @Test + void getMatchingLayerWhenFilterMatchesIncludes() { + FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom", + Collections.singletonList(new TestFilter1Library())); + Library library = mockLibrary("A-Compile", LibraryScope.COMPILE); + assertThat(strategy.getMatchingLayer(library).toString()).isEqualTo("custom"); + } + + @Test + void matchesWhenFilterMatchesIncludesAndExcludesFromSameFilter() { + FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom", + Collections.singletonList(new TestFilter1Library())); + Library library = mockLibrary("A-Runtime", LibraryScope.RUNTIME); + assertThat(strategy.getMatchingLayer(library)).isNull(); + } + + @Test + void matchesWhenFilterMatchesIncludesAndExcludesFromAnotherFilter() { + List filters = new ArrayList<>(); + filters.add(new TestFilter1Library()); + filters.add(new TestFilter2Library()); + FilteredLibraryStrategy strategy = new FilteredLibraryStrategy("custom", filters); + Library library = mockLibrary("A-Provided", LibraryScope.PROVIDED); + assertThat(strategy.getMatchingLayer(library)).isNull(); + } + + private Library mockLibrary(String name, LibraryScope runtime) { + Library library = mock(Library.class); + given(library.getName()).willReturn(name); + given(library.getScope()).willReturn(runtime); + return library; + } + + private static class TestFilter1Library implements LibraryFilter { + + @Override + public boolean isLibraryIncluded(Library library) { + return library.getName().contains("A"); + } + + @Override + public boolean isLibraryExcluded(Library library) { + return library.getScope().equals(LibraryScope.RUNTIME); + } + + } + + private static class TestFilter2Library implements LibraryFilter { + + @Override + public boolean isLibraryIncluded(Library library) { + return library.getName().contains("B"); + } + + @Override + public boolean isLibraryExcluded(Library library) { + return library.getScope().equals(LibraryScope.PROVIDED); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc index e61a6211eec..8973f3b8055 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc @@ -71,6 +71,129 @@ The `layout` property defaults to a guess based on the archive type (`jar` or `w * `ZIP` (alias to `DIR`): similar to the `JAR` layout using `PropertiesLauncher`. * `NONE`: Bundle all dependencies and project resources. Does not bundle a bootstrap loader. +[[repackage-layers]] +=== Layered jar + +By default, a repackaged jar contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively. +For cases where a docker image needs to be built from the contents of the jar, the jar format can be enhanced to support layer folders. +To use this feature, the layering feature must be enabled: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + + org.springframework.boot + spring-boot-maven-plugin + {gradle-project-version} + + + true + + + + + + +---- + +By default, the following layers are created: + +* `application` for any other classes and resources. +* `resources` for static resources at the default locations, i.e. `META-INF/resources/`, `resources/`, `static/`, `public/`. +* `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`. +* `dependencies` for any other dependency. + +The layers order is important as it determines how likely previous layers can be cached when part of the application changes. +The default order is `application`, `resources`, `snapshot-dependencies` and `dependencies`. +Content that is likely to change should be added first, followed by layers that are less likely to change. + + + +[[repackage-layers-configuration]] +==== Custom Layers configuration +Depending on your application, you may want to tune how layers are created and add new ones. +This can be done using a separate configuration file that should be registered as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + + org.springframework.boot + spring-boot-maven-plugin + {gradle-project-version} + + + true + ${project.basedir}/src/layers.xml + + + + + + +---- + +The configuration file lists the layers and their order as well as the strategies to apply to libraries and classes. +The following example shows what the implicit layer configuration described above does: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + application + resources + snapshots + dependencies + + + + + *:*:*SNAPSHOT + + + + + *:* + + + + + + + META-INF/resources/** + resources/** + static/** + public/** + + + + + ** + + + + +---- + +Each `layer-content` element defines a strategy to include an entry of the jar in a layer. +When an entry matches a strategy, it is included in the layer and further strategies are ignored. +This is illustrated by the `dependencies` and `application` layers that have a "catch-all" include filter used to add any libraries or classes that were not processed by previous strategies. + +The content of a libraries layer can be customized using filters on the coordinates. +The format is `groupId:artifactId[:version]`. +In the example above, any artifact whose version ends with `SNAPSHOT` is going to be included in the `snapshot-dependencies` layer. + +The content of a classes layer can be customized using filters on location of the entry using Ant-style pattern matching. + + + include::goals/repackage.adoc[leveloffset=+1] @@ -400,31 +523,8 @@ This example excludes any artifact belonging to the `com.foo` group: -[[repackage-layered-jars]] -==== Packaging layered jars -By default, the repackaged jar contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively. -For cases where a docker image needs to be built from the contents of the jar, the jar format can be enhanced to support layer folders. -To use this feature, the layering feature must be enabled: - -[source,xml,indent=0,subs="verbatim,attributes"] ----- - - - - - org.springframework.boot - spring-boot-maven-plugin - {gradle-project-version} - - - true - - - - - - ----- +[[repackage-layered-jars-tools]] +==== Layered jar tools When you create a layered jar, the `spring-boot-layertools` jar will be added as a dependency to your jar. With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers. @@ -440,13 +540,59 @@ If you wish to exclude this dependency, you can do so in the following manner: spring-boot-maven-plugin {gradle-project-version} - + true false - + ----- \ No newline at end of file +---- + + + +[[repackage-layered-jars-additional-layers]] +==== Custom layers configuration + +While the default setup creates two layers for libraries, you may want to isolate the dependencies of your project in a dedicated layer. +This allows to reuse the cache for external dependencies when an internal dependency has changed, as shown by the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + application + resources + snapshots + company-dependencies + dependencies + + + + + *:*:*SNAPSHOT + + + + + com.acme:* + + + + + *:* + + + + + ... + + +---- + +The configuration above creates an additional `company-dependencies` layer with all libraries with the `com.acme` groupId. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index a1e69ef8044..b03f4afcb35 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -305,4 +305,15 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { }); } + @TestTemplate + void whenJarIsRepackagedWithTheCustomLayeredLayout(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered-custom").execute((project) -> { + File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/layers/application/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/layers/my-dependencies-name/lib/jar-release") + .hasEntryWithNameStartingWith("BOOT-INF/layers/snapshot-dependencies/lib/jar-snapshot") + .hasEntryWithNameStartingWith("BOOT-INF/layers/configuration/classes/application.yml"); + }); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-release/pom.xml new file mode 100644 index 00000000000..a06fe545f18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-snapshot/pom.xml new file mode 100644 index 00000000000..ab31e719baf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/pom.xml new file mode 100644 index 00000000000..a987ad7980c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-layered + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + true + ${project.basedir}/src/layers.xml + + + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml new file mode 100644 index 00000000000..b47c40590a3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -0,0 +1,35 @@ + + + configuration + application + snapshot-dependencies + my-dependencies-name + + + + + *:*:*-SNAPSHOT + + + + + *:*:* + + + + + + + **/application*.* + + + + + ** + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..ca2b9a2f0e5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/resources/application.yml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/resources/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/pom.xml new file mode 100644 index 00000000000..fdd98953811 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml index bea5f831493..96d431fee84 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml @@ -22,10 +22,10 @@ repackage - + true false - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml index 48b928c57cd..d8f190dcbb8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml @@ -22,9 +22,9 @@ repackage - + true - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index c8780a2a97b..f6474ef1818 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -16,12 +16,17 @@ package org.springframework.boot.maven; +import java.io.File; +import java.io.FileInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.function.Supplier; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; @@ -31,6 +36,8 @@ import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; import org.springframework.boot.loader.tools.Layout; import org.springframework.boot.loader.tools.LayoutFactory; @@ -109,7 +116,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo * @since 2.3.0 */ @Parameter - private Layered layered; + private Layers layers; /** * Return a {@link Packager} configured for this MOJO. @@ -126,13 +133,31 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo getLog().info("Layout: " + this.layout); packager.setLayout(this.layout.layout()); } - if (this.layered != null && this.layered.isEnabled()) { + if (this.layers != null && this.layers.isEnabled()) { + if (this.layers.getConfiguration() != null) { + try { + Document document = getDocumentIfAvailable(this.layers.getConfiguration()); + CustomLayersProvider customLayersProvider = new CustomLayersProvider(); + packager.setLayers(customLayersProvider.getLayers(document)); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to process custom layers configuration " + + this.layers.getConfiguration().getAbsolutePath(), ex); + } + } packager.setLayout(new LayeredJar()); - packager.setIncludeRelevantJarModeJars(this.layered.isIncludeLayerTools()); + packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools()); } return packager; } + private Document getDocumentIfAvailable(File configurationFile) throws Exception { + InputSource inputSource = new InputSource(new FileInputStream(configurationFile)); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputSource); + } + /** * Return {@link Libraries} that the packager can use. * @param unpacks any libraries that require unpack diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java index d9da896a190..028cec4ff42 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java @@ -31,6 +31,7 @@ import org.apache.maven.plugin.logging.Log; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCallback; +import org.springframework.boot.loader.tools.LibraryCoordinates; import org.springframework.boot.loader.tools.LibraryScope; /** @@ -39,6 +40,7 @@ import org.springframework.boot.loader.tools.LibraryScope; * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Scott Frederick * @since 1.0.0 */ public class ArtifactsLibraries implements Libraries { @@ -78,7 +80,9 @@ public class ArtifactsLibraries implements Libraries { name = artifact.getGroupId() + "-" + name; this.log.debug("Renamed to: " + name); } - callback.library(new Library(name, artifact.getFile(), scope, isUnpackRequired(artifact))); + LibraryCoordinates coordinates = new LibraryCoordinates(artifact.getGroupId(), artifact.getArtifactId(), + artifact.getVersion()); + callback.library(new Library(name, artifact.getFile(), scope, isUnpackRequired(artifact), coordinates)); } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java new file mode 100644 index 00000000000..9bda9539158 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java @@ -0,0 +1,169 @@ +/* + * 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.maven; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.layer.CustomLayers; +import org.springframework.boot.loader.tools.layer.classes.FilteredResourceStrategy; +import org.springframework.boot.loader.tools.layer.classes.ResourceFilter; +import org.springframework.boot.loader.tools.layer.classes.ResourceStrategy; +import org.springframework.boot.loader.tools.layer.library.CoordinateFilter; +import org.springframework.boot.loader.tools.layer.library.FilteredLibraryStrategy; +import org.springframework.boot.loader.tools.layer.library.LibraryFilter; +import org.springframework.boot.loader.tools.layer.library.LibraryStrategy; + +/** + * Produces a {@link CustomLayers} based on the given {@link Document}. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public class CustomLayersProvider { + + public CustomLayers getLayers(Document document) { + Element root = document.getDocumentElement(); + NodeList nl = root.getChildNodes(); + List layers = new ArrayList<>(); + List libraryStrategies = new ArrayList<>(); + List resourceStrategies = new ArrayList<>(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + String nodeName = ele.getNodeName(); + if ("layers".equals(nodeName)) { + layers.addAll(getLayers(ele)); + } + if ("libraries".equals(nodeName)) { + libraryStrategies.addAll(getLibraryStrategies(ele.getChildNodes())); + } + if ("classes".equals(nodeName)) { + resourceStrategies.addAll(getResourceStrategies(ele.getChildNodes())); + } + } + } + return new CustomLayers(layers, resourceStrategies, libraryStrategies); + } + + private List getLibraryStrategies(NodeList nodes) { + List strategy = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + Node item = nodes.item(i); + if (item instanceof Element) { + Element element = (Element) item; + String layer = element.getAttribute("layer"); + if ("layer-content".equals(element.getTagName())) { + List filters = new ArrayList<>(); + NodeList filterList = item.getChildNodes(); + if (filterList.getLength() == 0) { + throw new IllegalArgumentException("Filters for layer-content must not be empty."); + } + for (int k = 0; k < filterList.getLength(); k++) { + Node filter = filterList.item(k); + if (filter instanceof Element) { + List includeList = getPatterns((Element) filter, "include"); + List excludeList = getPatterns((Element) filter, "exclude"); + addLibraryFilter(filters, filter, includeList, excludeList); + } + } + strategy.add(new FilteredLibraryStrategy(layer, filters)); + } + } + } + return strategy; + } + + private void addLibraryFilter(List filters, Node filter, List includeList, + List excludeList) { + if ("coordinates".equals(filter.getNodeName())) { + filters.add(new CoordinateFilter(includeList, excludeList)); + } + } + + private List getResourceStrategies(NodeList strategies) { + List strategy = new ArrayList<>(); + for (int i = 0; i < strategies.getLength(); i++) { + Node item = strategies.item(i); + List filters = new ArrayList<>(); + if (item instanceof Element) { + Element element = (Element) item; + String layer = element.getAttribute("layer"); + if ("layer-content".equals(element.getTagName())) { + NodeList filterList = item.getChildNodes(); + if (filterList.getLength() == 0) { + throw new IllegalArgumentException("Filters for layer-content must not be empty."); + } + for (int k = 0; k < filterList.getLength(); k++) { + Node filter = filterList.item(k); + if (filter instanceof Element) { + List includeList = getPatterns((Element) filter, "include"); + List excludeList = getPatterns((Element) filter, "exclude"); + addFilter(filters, filter, includeList, excludeList); + } + } + strategy.add(new FilteredResourceStrategy(layer, filters)); + } + } + } + return strategy; + } + + private void addFilter(List filters, Node filter, List includeList, + List excludeList) { + if ("locations".equals(filter.getNodeName())) { + filters.add( + new org.springframework.boot.loader.tools.layer.classes.LocationFilter(includeList, excludeList)); + } + } + + private List getPatterns(Element element, String key) { + NodeList patterns = element.getElementsByTagName(key); + List values = new ArrayList<>(); + for (int j = 0; j < patterns.getLength(); j++) { + Node item = patterns.item(j); + if (item instanceof Element) { + values.add(item.getTextContent()); + } + } + return values; + } + + private List getLayers(Element element) { + List layers = new ArrayList<>(); + NodeList nl = element.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + String nodeName = ele.getNodeName(); + if ("layer".equals(nodeName)) { + layers.add(new Layer(ele.getTextContent())); + } + } + } + return layers; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layered.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java similarity index 69% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layered.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java index 2c7772035ae..6228c2c20c9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layered.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -16,18 +16,22 @@ package org.springframework.boot.maven; +import java.io.File; + /** * Layer configuration options. * * @author Madhura Bhave * @since 2.3.0 */ -public class Layered { +public class Layers { private boolean enabled; private boolean includeLayerTools = true; + private File configuration; + /** * Whether layered jar layout is enabled. * @return true if the layered layout is enabled. @@ -44,4 +48,18 @@ public class Layered { return this.includeLayerTools; } + /** + * The location of the layers configuration file. If no file is provided, a default + * configuration is used with four layers: {@code application}, {@code resources}, + * {@code snapshot-dependencies} and {@code dependencies}. + * @return the layers configuration file + */ + public File getConfiguration() { + return this.configuration; + } + + public void setConfiguration(File configuration) { + this.configuration = configuration; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration.xsd new file mode 100644 index 00000000000..94862ba7c6b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-configuration.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java new file mode 100644 index 00000000000..a10f81ed89a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java @@ -0,0 +1,97 @@ +/* + * 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.maven; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.layer.CustomLayers; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CustomLayersProvider}. + * + * @author Madhura Bhave + * @author Scott Frederick + */ +public class CustomLayersProviderTests { + + private CustomLayersProvider customLayersProvider; + + @BeforeEach + void setup() { + this.customLayersProvider = new CustomLayersProvider(); + } + + @Test + void getLayerResolverWhenDocumentValid() throws Exception { + CustomLayers layers = this.customLayersProvider.getLayers(getDocument("layers.xml")); + assertThat(layers).extracting("name").containsExactly("configuration", "application", "my-resources", + "snapshot-dependencies", "my-deps", "my-dependencies-name"); + Library snapshot = mockLibrary("test-SNAPSHOT.jar", "org.foo", "1.0.0-SNAPSHOT"); + Library groupId = mockLibrary("my-library", "com.acme", null); + Library otherDependency = mockLibrary("other-library", "org.foo", null); + assertThat(layers.getLayer(snapshot).toString()).isEqualTo("snapshot-dependencies"); + assertThat(layers.getLayer(groupId).toString()).isEqualTo("my-deps"); + assertThat(layers.getLayer(otherDependency).toString()).isEqualTo("my-dependencies-name"); + assertThat(layers.getLayer("META-INF/resources/test.css").toString()).isEqualTo("my-resources"); + assertThat(layers.getLayer("application.yml").toString()).isEqualTo("configuration"); + assertThat(layers.getLayer("test").toString()).isEqualTo("application"); + } + + private Library mockLibrary(String name, String groupId, String version) { + Library library = mock(Library.class); + given(library.getName()).willReturn(name); + given(library.getCoordinates()).willReturn(new LibraryCoordinates(groupId, null, version)); + return library; + } + + @Test + void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("library-layer-no-filter.xml"))) + .withMessage("Filters for layer-content must not be empty."); + } + + @Test + void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.customLayersProvider.getLayers(getDocument("resource-layer-no-filter.xml"))) + .withMessage("Filters for layer-content must not be empty."); + } + + private Document getDocument(String resourceName) throws Exception { + ClassPathResource resource = new ClassPathResource(resourceName); + InputSource inputSource = new InputSource(resource.getInputStream()); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + return documentBuilder.parse(inputSource); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml new file mode 100644 index 00000000000..732fbbafceb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -0,0 +1,47 @@ + + + configuration + application + my-resources + snapshot-dependencies + my-deps + my-dependencies-name + + + + + *:*:*-SNAPSHOT + + + + + com.acme:* + + + + + *:*:* + + + + + + + META-INF/resources/** + + + + + **/application*.* + + + + + ** + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml new file mode 100644 index 00000000000..a09440498fc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/library-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + my-deps + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml new file mode 100644 index 00000000000..d0e3c63c683 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + my-layer + + + + + \ No newline at end of file