Add parent origin support
Update the `Origin` interface to include a default `getParent()` method which can be used to get the parent origin. The `TextResourceOrigin` has been updated to implement the method against the source `Resource`. A new `OriginTrackedResource` implementation allows any `Resource` to be decorated and carry an optional `Origin`. Ultimately this will allow us to include parent `Origin` information on properties loaded via a `PropertySourceLoader` without needing any changes to that interface. See gh-23018
This commit is contained in:
@@ -17,6 +17,11 @@
|
||||
package org.springframework.boot.origin;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Interface that uniquely represents the origin of an item. For example, an item loaded
|
||||
@@ -30,12 +35,24 @@ import java.io.File;
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
* @see OriginProvider
|
||||
* @see TextResourceOrigin
|
||||
*/
|
||||
public interface Origin {
|
||||
|
||||
/**
|
||||
* Return the parent origin for this instance if there is one. The parent origin
|
||||
* provides the origin of the item that created this one.
|
||||
* @return the parent origin or {@code null}
|
||||
* @since 2.4.0
|
||||
* @see Origin#parentsFrom(Object)
|
||||
*/
|
||||
default Origin getParent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the {@link Origin} that an object originated from. Checks if the source object
|
||||
* is an {@link OriginProvider} and also searches exception stacks.
|
||||
* is an {@link Origin} or {@link OriginProvider} and also searches exception stacks.
|
||||
* @param source the source object or {@code null}
|
||||
* @return an optional {@link Origin}
|
||||
*/
|
||||
@@ -53,4 +70,27 @@ public interface Origin {
|
||||
return origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the parents of the {@link Origin} that an object originated from. Checks if
|
||||
* the source object is an {@link Origin} or {@link OriginProvider} and also searches
|
||||
* exception stacks. Provides a list of all parents up to root {@link Origin},
|
||||
* starting with the most immediate parent.
|
||||
* @param source the source object or {@code null}
|
||||
* @return a list of parents or an empty list if the source is {@code null}, has no
|
||||
* origin, or no parent
|
||||
*/
|
||||
static List<Origin> parentsFrom(Object source) {
|
||||
Origin origin = from(source);
|
||||
if (origin == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Set<Origin> parents = new LinkedHashSet<>();
|
||||
origin = origin.getParent();
|
||||
while (origin != null && !parents.contains(origin)) {
|
||||
parents.add(origin);
|
||||
origin = origin.getParent();
|
||||
}
|
||||
return Collections.unmodifiableList(new ArrayList<>(parents));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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.origin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.WritableResource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Decorator that can be used to add {@link Origin} information to a {@link Resource} or
|
||||
* {@link WritableResource}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.4.0
|
||||
* @see #of(Resource, Origin)
|
||||
* @see #of(WritableResource, Origin)
|
||||
* @see OriginProvider
|
||||
*/
|
||||
public class OriginTrackedResource implements Resource, OriginProvider {
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
private final Origin origin;
|
||||
|
||||
/**
|
||||
* Create a new {@link OriginTrackedResource} instance.
|
||||
* @param resource the resource to track
|
||||
* @param origin the origin of the resource
|
||||
*/
|
||||
OriginTrackedResource(Resource resource, Origin origin) {
|
||||
Assert.notNull(resource, "Resource must not be null");
|
||||
this.resource = resource;
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return getResource().getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return getResource().exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return getResource().isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return getResource().isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return getResource().isFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
return getResource().getURL();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
return getResource().getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
return getResource().getFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableByteChannel readableChannel() throws IOException {
|
||||
return getResource().readableChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return getResource().contentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return getResource().lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws IOException {
|
||||
return getResource().createRelative(relativePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return getResource().getFilename();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return getResource().getDescription();
|
||||
}
|
||||
|
||||
public Resource getResource() {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Origin getOrigin() {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
OriginTrackedResource other = (OriginTrackedResource) obj;
|
||||
return this.resource.equals(other) && ObjectUtils.nullSafeEquals(this.origin, other.origin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = this.resource.hashCode();
|
||||
result = prime * result + ObjectUtils.nullSafeHashCode(this.origin);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.resource.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link OriginProvider origin tracked} version the given
|
||||
* {@link WritableResource}.
|
||||
* @param resource the tracked resource
|
||||
* @param origin the origin of the resource
|
||||
* @return a {@link OriginTrackedWritableResource} instance
|
||||
*/
|
||||
public static OriginTrackedWritableResource of(WritableResource resource, Origin origin) {
|
||||
return (OriginTrackedWritableResource) of((Resource) resource, origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link OriginProvider origin tracked} version the given
|
||||
* {@link Resource}.
|
||||
* @param resource the tracked resource
|
||||
* @param origin the origin of the resource
|
||||
* @return a {@link OriginTrackedResource} instance
|
||||
*/
|
||||
public static OriginTrackedResource of(Resource resource, Origin origin) {
|
||||
if (resource instanceof WritableResource) {
|
||||
return new OriginTrackedWritableResource((WritableResource) resource, origin);
|
||||
}
|
||||
return new OriginTrackedResource(resource, origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link OriginTrackedResource} for {@link WritableResource} instances.
|
||||
*/
|
||||
public static class OriginTrackedWritableResource extends OriginTrackedResource implements WritableResource {
|
||||
|
||||
/**
|
||||
* Create a new {@link OriginTrackedWritableResource} instance.
|
||||
* @param resource the resource to track
|
||||
* @param origin the origin of the resource
|
||||
*/
|
||||
OriginTrackedWritableResource(WritableResource resource, Origin origin) {
|
||||
super(resource, origin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableResource getResource() {
|
||||
return (WritableResource) super.getResource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return getResource().getOutputStream();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,11 +21,14 @@ import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link Origin} for an item loaded from a text resource. Provides access to the original
|
||||
* {@link Resource} that loaded the text and a {@link Location} within it.
|
||||
* {@link Resource} that loaded the text and a {@link Location} within it. If the provided
|
||||
* resource provides an {@link Origin} (e.g. it is an {@link OriginTrackedResource}), then
|
||||
* it will be used as the {@link Origin#getParent() origin parent}.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
* @see OriginTrackedResource
|
||||
*/
|
||||
public class TextResourceOrigin implements Origin {
|
||||
|
||||
@@ -54,6 +57,11 @@ public class TextResourceOrigin implements Origin {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Origin getParent() {
|
||||
return Origin.from(this.resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
|
||||
@@ -27,9 +27,17 @@ public final class MockOrigin implements Origin {
|
||||
|
||||
private final String value;
|
||||
|
||||
private MockOrigin(String value) {
|
||||
private final Origin parent;
|
||||
|
||||
private MockOrigin(String value, Origin parent) {
|
||||
Assert.notNull(value, "Value must not be null");
|
||||
this.value = value;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Origin getParent() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,7 +62,11 @@ public final class MockOrigin implements Origin {
|
||||
}
|
||||
|
||||
public static Origin of(String value) {
|
||||
return (value != null) ? new MockOrigin(value) : null;
|
||||
return of(value, null);
|
||||
}
|
||||
|
||||
public static Origin of(String value, Origin parent) {
|
||||
return (value != null) ? new MockOrigin(value, parent) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.boot.origin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -29,6 +31,13 @@ import static org.mockito.Mockito.mock;
|
||||
*/
|
||||
class OriginTests {
|
||||
|
||||
@Test
|
||||
void getParentWhenDefaultIsNull() {
|
||||
Origin origin = new Origin() {
|
||||
};
|
||||
assertThat(origin.getParent()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromWhenSourceIsNullReturnsNull() {
|
||||
assertThat(Origin.from(null)).isNull();
|
||||
@@ -68,6 +77,20 @@ class OriginTests {
|
||||
assertThat(Origin.from(exception)).isEqualTo(origin);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parentsFromWhenSourceIsNullReturnsEmptyList() {
|
||||
assertThat(Origin.parentsFrom(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parentsFromReturnsParents() {
|
||||
Origin o1 = MockOrigin.of("1");
|
||||
Origin o2 = MockOrigin.of("2", o1);
|
||||
Origin o3 = MockOrigin.of("3", o2);
|
||||
List<Origin> parents = Origin.parentsFrom(o3);
|
||||
assertThat(parents).containsExactly(o2, o1);
|
||||
}
|
||||
|
||||
static class TestException extends RuntimeException implements OriginProvider {
|
||||
|
||||
private final Origin origin;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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.origin;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.origin.OriginTrackedResource.OriginTrackedWritableResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.WritableResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link OriginTrackedResource}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class OriginTrackedResourceTests {
|
||||
|
||||
private Origin origin;
|
||||
|
||||
private WritableResource resource;
|
||||
|
||||
private OriginTrackedWritableResource tracked;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.origin = MockOrigin.of("test");
|
||||
this.resource = mock(WritableResource.class);
|
||||
this.tracked = OriginTrackedResource.of(this.resource, this.origin);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInputStreamDelegatesToResource() throws IOException {
|
||||
this.tracked.getInputStream();
|
||||
verify(this.resource).getInputStream();
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsDelegatesToResource() {
|
||||
this.tracked.exists();
|
||||
verify(this.resource).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isReadableDelegatesToResource() {
|
||||
this.tracked.isReadable();
|
||||
verify(this.resource).isReadable();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isOpenDelegatesToResource() {
|
||||
this.tracked.isOpen();
|
||||
verify(this.resource).isOpen();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isFileDelegatesToResource() {
|
||||
this.tracked.isFile();
|
||||
verify(this.resource).isFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getURLDelegatesToResource() throws IOException {
|
||||
this.tracked.getURL();
|
||||
verify(this.resource).getURL();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getURIDelegatesToResource() throws IOException {
|
||||
this.tracked.getURI();
|
||||
verify(this.resource).getURI();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFileDelegatesToResource() throws IOException {
|
||||
this.tracked.getFile();
|
||||
verify(this.resource).getFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
void readableChannelDelegatesToResource() throws IOException {
|
||||
this.tracked.readableChannel();
|
||||
verify(this.resource).readableChannel();
|
||||
}
|
||||
|
||||
@Test
|
||||
void contentLengthDelegatesToResource() throws IOException {
|
||||
this.tracked.contentLength();
|
||||
verify(this.resource).contentLength();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lastModifiedDelegatesToResource() throws IOException {
|
||||
this.tracked.lastModified();
|
||||
verify(this.resource).lastModified();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRelativeDelegatesToResource() throws IOException {
|
||||
this.tracked.createRelative("path");
|
||||
verify(this.resource).createRelative("path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFilenameDelegatesToResource() {
|
||||
this.tracked.getFilename();
|
||||
verify(this.resource).getFilename();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDescriptionDelegatesToResource() {
|
||||
this.tracked.getDescription();
|
||||
verify(this.resource).getDescription();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOutputStreamDelegatesToResource() throws IOException {
|
||||
this.tracked.getOutputStream();
|
||||
verify(this.resource).getOutputStream();
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringDelegatesToResource() {
|
||||
Resource resource = new ClassPathResource("test");
|
||||
Resource tracked = OriginTrackedResource.of(resource, this.origin);
|
||||
assertThat(tracked).hasToString(resource.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOriginReturnsOrigin() {
|
||||
assertThat(this.tracked.getOrigin()).isEqualTo(this.origin);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getResourceReturnsResource() {
|
||||
assertThat(this.tracked.getResource()).isEqualTo(this.resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsAndHashCode() {
|
||||
Origin o1 = MockOrigin.of("o1");
|
||||
Origin o2 = MockOrigin.of("o2");
|
||||
Resource r1 = mock(Resource.class);
|
||||
Resource r2 = mock(Resource.class);
|
||||
OriginTrackedResource r1o1a = OriginTrackedResource.of(r1, o1);
|
||||
OriginTrackedResource r1o1b = OriginTrackedResource.of(r1, o1);
|
||||
OriginTrackedResource r1o2 = OriginTrackedResource.of(r1, o2);
|
||||
OriginTrackedResource r2o1 = OriginTrackedResource.of(r2, o1);
|
||||
OriginTrackedResource r2o2 = OriginTrackedResource.of(r2, o2);
|
||||
assertThat(r1o1a).isEqualTo(r1o1a).isEqualTo(r1o1a).isNotEqualTo(r1o2).isNotEqualTo(r2o1).isNotEqualTo(r2o2);
|
||||
assertThat(r1o1a.hashCode()).isEqualTo(r1o1b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofReturnsOriginTrackedResource() {
|
||||
Resource resource = mock(Resource.class);
|
||||
Resource tracked = OriginTrackedResource.of(resource, this.origin);
|
||||
assertThat(tracked).isExactlyInstanceOf(OriginTrackedResource.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenWritableReturnsOriginTrackedWrtiableResource() {
|
||||
Resource resource = mock(WritableResource.class);
|
||||
Resource tracked = OriginTrackedResource.of(resource, this.origin);
|
||||
assertThat(tracked).isInstanceOf(WritableResource.class)
|
||||
.isExactlyInstanceOf(OriginTrackedWritableResource.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.origin.TextResourceOrigin.Location;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -54,7 +55,21 @@ class TextResourceOriginTests {
|
||||
Location location = new Location(1, 2);
|
||||
TextResourceOrigin origin = new TextResourceOrigin(null, location);
|
||||
assertThat(origin.getLocation()).isEqualTo(location);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getParentWhenResourceIsNotOriginTrackedReturnsNull() {
|
||||
ClassPathResource resource = new ClassPathResource("foo.txt");
|
||||
TextResourceOrigin origin = new TextResourceOrigin(resource, null);
|
||||
assertThat(origin.getParent()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getParentWhenResourceIsOriginTrackedReturnsResourceOrigin() {
|
||||
Origin resourceOrigin = MockOrigin.of("test");
|
||||
Resource resource = OriginTrackedResource.of(new ClassPathResource("foo.txt"), resourceOrigin);
|
||||
TextResourceOrigin origin = new TextResourceOrigin(resource, null);
|
||||
assertThat(origin.getParent()).isSameAs(resourceOrigin);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user