Commit e49e62df authored by Madhura Bhave's avatar Madhura Bhave Committed by Stephane Nicoll

Add support for customizing layers in Maven

This commit adds an additional 'layers/configuration' property that can
be used to refer to a separate layers configuration file. This separate
file defines:

* The layers and their order of precedence,
* How libraries are handled using filters that match against the
coordinates of each library, and
* How classes are handled using filters that match against the location
of the entry

An XSD to validate the XML configuration file is available.

Closes gh-20295
Co-authored-by: 's avatarStephane Nicoll <snicoll@pivotal.io>
parent 896d2c85
...@@ -25,6 +25,7 @@ import java.io.InputStream; ...@@ -25,6 +25,7 @@ import java.io.InputStream;
* Encapsulates information about a single library that may be packed into the archive. * Encapsulates information about a single library that may be packed into the archive.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
* @since 1.1.2 * @since 1.1.2
* @see Libraries * @see Libraries
*/ */
...@@ -38,6 +39,8 @@ public class Library { ...@@ -38,6 +39,8 @@ public class Library {
private final boolean unpackRequired; private final boolean unpackRequired;
private final LibraryCoordinates coordinates;
/** /**
* Create a new {@link Library}. * Create a new {@link Library}.
* @param file the source file * @param file the source file
...@@ -66,10 +69,15 @@ public class Library { ...@@ -66,10 +69,15 @@ public class Library {
* @param unpackRequired if the library needs to be unpacked before it can be used * @param unpackRequired if the library needs to be unpacked before it can be used
*/ */
public Library(String name, File file, LibraryScope scope, boolean unpackRequired) { 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.name = (name != null) ? name : file.getName();
this.file = file; this.file = file;
this.scope = scope; this.scope = scope;
this.unpackRequired = unpackRequired; this.unpackRequired = unpackRequired;
this.coordinates = coordinates;
} }
/** /**
...@@ -114,6 +122,14 @@ public class Library { ...@@ -114,6 +122,14 @@ public class Library {
return this.unpackRequired; return this.unpackRequired;
} }
/**
* Return the {@linkplain LibraryCoordinates coordinates} of the library.
* @return the coordinates
*/
public LibraryCoordinates getCoordinates() {
return this.coordinates;
}
long getLastModified() { long getLastModified() {
return this.file.lastModified(); return this.file.lastModified();
} }
......
/*
* 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;
}
}
/*
* 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<Layer> layers;
private final List<ResourceStrategy> resourceStrategies;
private final List<LibraryStrategy> libraryStrategies;
public CustomLayers(List<Layer> layers, List<ResourceStrategy> resourceStrategies,
List<LibraryStrategy> libraryStrategies) {
this.layers = new ArrayList<>(layers);
this.resourceStrategies = new ArrayList<>(resourceStrategies);
this.libraryStrategies = new ArrayList<>(libraryStrategies);
}
@Override
public Iterator<Layer> 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.");
}
}
/*
* 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<String> includes = new ArrayList<>();
private final List<String> excludes = new ArrayList<>();
public AbstractResourceFilter(List<String> includes, List<String> 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<String> toMatch);
}
/*
* 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<ResourceFilter> filters = new ArrayList<>();
public FilteredResourceStrategy(String layer, List<ResourceFilter> 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;
}
}
/*
* 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<String> includes, List<String> excludes) {
super(includes, excludes);
}
@Override
protected boolean isMatch(String resourceName, List<String> toMatch) {
return toMatch.stream().anyMatch((pattern) -> MATCHER.match(pattern, resourceName));
}
}
/*
* 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);
}
/*
* 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);
}
/*
* 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<String> includes = new ArrayList<>();
private final List<String> excludes = new ArrayList<>();
public CoordinateFilter(List<String> includes, List<String> 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<String> 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());
}
}
/*
* 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<LibraryFilter> filters = new ArrayList<>();
public FilteredLibraryStrategy(String layer, List<LibraryFilter> 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;
}
}
/*
* 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);
}
/*
* 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);
}
/*
* 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"));
}
}
/*
* 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<Layer> 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;
}
}
/*
* 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<ResourceFilter> 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");
}
}
}
/*
* 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();
}
}
/*
* 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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();
}
}
/*
* 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<LibraryFilter> 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);
}
}
}
...@@ -71,6 +71,129 @@ The `layout` property defaults to a guess based on the archive type (`jar` or `w ...@@ -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`. * `ZIP` (alias to `DIR`): similar to the `JAR` layout using `PropertiesLauncher`.
* `NONE`: Bundle all dependencies and project resources. Does not bundle a bootstrap loader. * `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"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
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"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
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"]
----
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>application</layer>
<layer>resources</layer>
<layer>snapshots</layer>
<layer>dependencies</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="dependencies">
<coordinates>
<include>*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
<layer-content layer="resources">
<locations>
<include>META-INF/resources/**</include>
<include>resources/**</include>
<include>static/**</include>
<include>public/**</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</classes>
</layers-configuration>
----
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] include::goals/repackage.adoc[leveloffset=+1]
...@@ -400,31 +523,8 @@ This example excludes any artifact belonging to the `com.foo` group: ...@@ -400,31 +523,8 @@ This example excludes any artifact belonging to the `com.foo` group:
[[repackage-layered-jars]] [[repackage-layered-jars-tools]]
==== Packaging layered jars ==== Layered jar tools
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"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<layered>
<enabled>true</enabled>
</layered>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
When you create a layered jar, the `spring-boot-layertools` jar will be added as a dependency to your jar. 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. 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: ...@@ -440,13 +540,59 @@ If you wish to exclude this dependency, you can do so in the following manner:
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version> <version>{gradle-project-version}</version>
<configuration> <configuration>
<layered> <layers>
<enabled>true</enabled> <enabled>true</enabled>
<includeLayerTools>false</enabled> <includeLayerTools>false</enabled>
</layered> </layers>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>
---- ----
\ 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"]
----
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>application</layer>
<layer>resources</layer>
<layer>snapshots</layer>
<layer>company-dependencies</layer>
<layer>dependencies</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="company-dependencies">
<coordinates>
<include>com.acme:*</include>
</coordinates>
</layer-content>
<layer-content layer="dependencies">
<coordinates>
<include>*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
...
</classes>
</layers-configuration>
----
The configuration above creates an additional `company-dependencies` layer with all libraries with the `com.acme` groupId.
...@@ -305,4 +305,15 @@ class JarIntegrationTests extends AbstractArchiveIntegrationTests { ...@@ -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");
});
}
} }
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-release</artifactId>
<version>0.0.1.RELEASE</version>
<packaging>jar</packaging>
<name>jar</name>
<description>Release Jar dependency</description>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-snapshot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jar</name>
<description>Snapshot Jar dependency</description>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-layered</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-snapshot</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>jar-release</artifactId>
<version>0.0.1.RELEASE</version>
</dependency>
</dependencies>
</project>
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/layers/layers-configuration.xsd">
<layers>
<layer>configuration</layer>
<layer>application</layer>
<layer>snapshot-dependencies</layer>
<layer>my-dependencies-name</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*-SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="my-dependencies-name">
<coordinates>
<include>*:*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
<layer-content layer="configuration">
<locations>
<include>**/application*.*</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</classes>
</layers-configuration>
\ No newline at end of file
/*
* 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) {
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>aggregator</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<modules>
<module>jar-snapshot</module>
<module>jar-release</module>
<module>jar</module>
</modules>
</project>
...@@ -22,10 +22,10 @@ ...@@ -22,10 +22,10 @@
<goal>repackage</goal> <goal>repackage</goal>
</goals> </goals>
<configuration> <configuration>
<layered> <layers>
<enabled>true</enabled> <enabled>true</enabled>
<includeLayerTools>false</includeLayerTools> <includeLayerTools>false</includeLayerTools>
</layered> </layers>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
......
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
<goal>repackage</goal> <goal>repackage</goal>
</goals> </goals>
<configuration> <configuration>
<layered> <layers>
<enabled>true</enabled> <enabled>true</enabled>
</layered> </layers>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
......
...@@ -16,12 +16,17 @@ ...@@ -16,12 +16,17 @@
package org.springframework.boot.maven; package org.springframework.boot.maven;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; 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.artifact.Artifact;
import org.apache.maven.model.Dependency; import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoExecutionException;
...@@ -31,6 +36,8 @@ import org.apache.maven.project.MavenProject; ...@@ -31,6 +36,8 @@ import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter;
import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; 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.Layout;
import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.LayoutFactory;
...@@ -109,7 +116,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo ...@@ -109,7 +116,7 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
* @since 2.3.0 * @since 2.3.0
*/ */
@Parameter @Parameter
private Layered layered; private Layers layers;
/** /**
* Return a {@link Packager} configured for this MOJO. * Return a {@link Packager} configured for this MOJO.
...@@ -126,13 +133,31 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo ...@@ -126,13 +133,31 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo
getLog().info("Layout: " + this.layout); getLog().info("Layout: " + this.layout);
packager.setLayout(this.layout.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.setLayout(new LayeredJar());
packager.setIncludeRelevantJarModeJars(this.layered.isIncludeLayerTools()); packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools());
} }
return packager; 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. * Return {@link Libraries} that the packager can use.
* @param unpacks any libraries that require unpack * @param unpacks any libraries that require unpack
......
...@@ -31,6 +31,7 @@ import org.apache.maven.plugin.logging.Log; ...@@ -31,6 +31,7 @@ import org.apache.maven.plugin.logging.Log;
import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.Library;
import org.springframework.boot.loader.tools.LibraryCallback; import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryCoordinates;
import org.springframework.boot.loader.tools.LibraryScope; import org.springframework.boot.loader.tools.LibraryScope;
/** /**
...@@ -39,6 +40,7 @@ import org.springframework.boot.loader.tools.LibraryScope; ...@@ -39,6 +40,7 @@ import org.springframework.boot.loader.tools.LibraryScope;
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Scott Frederick
* @since 1.0.0 * @since 1.0.0
*/ */
public class ArtifactsLibraries implements Libraries { public class ArtifactsLibraries implements Libraries {
...@@ -78,7 +80,9 @@ public class ArtifactsLibraries implements Libraries { ...@@ -78,7 +80,9 @@ public class ArtifactsLibraries implements Libraries {
name = artifact.getGroupId() + "-" + name; name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + 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));
} }
} }
} }
......
/*
* 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<Layer> layers = new ArrayList<>();
List<LibraryStrategy> libraryStrategies = new ArrayList<>();
List<ResourceStrategy> 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<LibraryStrategy> getLibraryStrategies(NodeList nodes) {
List<LibraryStrategy> 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<LibraryFilter> 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<String> includeList = getPatterns((Element) filter, "include");
List<String> excludeList = getPatterns((Element) filter, "exclude");
addLibraryFilter(filters, filter, includeList, excludeList);
}
}
strategy.add(new FilteredLibraryStrategy(layer, filters));
}
}
}
return strategy;
}
private void addLibraryFilter(List<LibraryFilter> filters, Node filter, List<String> includeList,
List<String> excludeList) {
if ("coordinates".equals(filter.getNodeName())) {
filters.add(new CoordinateFilter(includeList, excludeList));
}
}
private List<ResourceStrategy> getResourceStrategies(NodeList strategies) {
List<ResourceStrategy> strategy = new ArrayList<>();
for (int i = 0; i < strategies.getLength(); i++) {
Node item = strategies.item(i);
List<ResourceFilter> 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<String> includeList = getPatterns((Element) filter, "include");
List<String> excludeList = getPatterns((Element) filter, "exclude");
addFilter(filters, filter, includeList, excludeList);
}
}
strategy.add(new FilteredResourceStrategy(layer, filters));
}
}
}
return strategy;
}
private void addFilter(List<ResourceFilter> filters, Node filter, List<String> includeList,
List<String> excludeList) {
if ("locations".equals(filter.getNodeName())) {
filters.add(
new org.springframework.boot.loader.tools.layer.classes.LocationFilter(includeList, excludeList));
}
}
private List<String> getPatterns(Element element, String key) {
NodeList patterns = element.getElementsByTagName(key);
List<String> 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<Layer> getLayers(Element element) {
List<Layer> 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;
}
}
...@@ -16,18 +16,22 @@ ...@@ -16,18 +16,22 @@
package org.springframework.boot.maven; package org.springframework.boot.maven;
import java.io.File;
/** /**
* Layer configuration options. * Layer configuration options.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @since 2.3.0 * @since 2.3.0
*/ */
public class Layered { public class Layers {
private boolean enabled; private boolean enabled;
private boolean includeLayerTools = true; private boolean includeLayerTools = true;
private File configuration;
/** /**
* Whether layered jar layout is enabled. * Whether layered jar layout is enabled.
* @return true if the layered layout is enabled. * @return true if the layered layout is enabled.
...@@ -44,4 +48,18 @@ public class Layered { ...@@ -44,4 +48,18 @@ public class Layered {
return this.includeLayerTools; 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;
}
} }
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema elementFormDefault="qualified"
xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/boot/layers">
<xsd:element name="layers-configuration" type="layersConfigurationType"/>
<xsd:complexType name="layersConfigurationType">
<xsd:sequence>
<xsd:element type="layersType" name="layers"/>
<xsd:element type="librariesType" name="libraries" minOccurs="0"/>
<xsd:element type="classesType" name="classes" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="layersType">
<xsd:sequence>
<xsd:element type="xsd:string" name="layer" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
The name of a layer. Each layer in the configuration must be referenced once and the
order matches how the content is likely to change. Put layers that are frequently
updated first, layers that are more stable (such as non-snapshot dependencies) last.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="librariesType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategies that should be applied to libraries. If no strategies are defined, two
layers are created out-of-the-box. A "snapshot-dependencies" layer with SNAPSHOT
libraries and a "dependencies" layer with all the other libraries.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element type="librariesLayerContentType" name="layer-content">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategy to apply on libraries.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="librariesLayerContentType" mixed="true">
<xsd:sequence>
<xsd:element type="filterType" name="coordinates" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute type="xsd:string" name="layer" use="required"/>
</xsd:complexType>
<xsd:complexType name="classesType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategies that should be applied to classes. If no strategies are defined, a single
"application" layer is created out-of-the-box.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element type="classesLayerContentType" name="layer-content" maxOccurs="unbounded"
minOccurs="0">
<xsd:annotation>
<xsd:documentation><![CDATA[
Strategy to apply on classes.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="classesLayerContentType" mixed="true">
<xsd:sequence>
<xsd:element type="filterType" name="locations" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute type="xsd:string" name="layer" use="required"/>
</xsd:complexType>
<xsd:complexType name="filterType">
<xsd:sequence>
<xsd:element type="xsd:string" name="exclude" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to exclude. An exclude pattern takes precedence over an
include pattern and must be declared first.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element type="xsd:string" name="include" minOccurs="0" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation><![CDATA[
Pattern of the elements to include.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
\ No newline at end of file
/*
* 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);
}
}
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>configuration</layer>
<layer>application</layer>
<layer>my-resources</layer>
<layer>snapshot-dependencies</layer>
<layer>my-deps</layer>
<layer>my-dependencies-name</layer>
</layers>
<libraries>
<layer-content layer="snapshot-dependencies">
<coordinates>
<include>*:*:*-SNAPSHOT</include>
</coordinates>
</layer-content>
<layer-content layer="my-deps">
<coordinates>
<include>com.acme:*</include>
</coordinates>
</layer-content>
<layer-content layer="my-dependencies-name">
<coordinates>
<include>*:*:*</include>
</coordinates>
</layer-content>
</libraries>
<classes>
<layer-content layer="my-resources">
<locations>
<include>META-INF/resources/**</include>
</locations>
</layer-content>
<layer-content layer="configuration">
<locations>
<include>**/application*.*</include>
</locations>
</layer-content>
<layer-content layer="application">
<locations>
<include>**</include>
</locations>
</layer-content>
</classes>
</layers-configuration>
\ No newline at end of file
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>my-deps</layer>
</layers>
<libraries>
<layer-content layer="my-deps"/>
</libraries>
</layers-configuration>
\ No newline at end of file
<layers-configuration xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-configuration.xsd">
<layers>
<layer>my-layer</layer>
</layers>
<classes>
<layer-content layer="my-layer"/>
</classes>
</layers-configuration>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment