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
pull/23127/head
Phillip Webb 4 years ago
parent bc5958c398
commit 960651c15a

@ -17,6 +17,11 @@
package org.springframework.boot.origin; package org.springframework.boot.origin;
import java.io.File; 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 * 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 * @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
* @see OriginProvider * @see OriginProvider
* @see TextResourceOrigin
*/ */
public interface Origin { 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 * 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} * @param source the source object or {@code null}
* @return an optional {@link Origin} * @return an optional {@link Origin}
*/ */
@ -53,4 +70,27 @@ public interface Origin {
return 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 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 Madhura Bhave
* @author Phillip Webb * @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
* @see OriginTrackedResource
*/ */
public class TextResourceOrigin implements Origin { public class TextResourceOrigin implements Origin {
@ -54,6 +57,11 @@ public class TextResourceOrigin implements Origin {
return this.location; return this.location;
} }
@Override
public Origin getParent() {
return Origin.from(this.resource);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (this == obj) {

@ -27,9 +27,17 @@ public final class MockOrigin implements Origin {
private final String value; 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"); Assert.notNull(value, "Value must not be null");
this.value = value; this.value = value;
this.parent = parent;
}
@Override
public Origin getParent() {
return this.parent;
} }
@Override @Override
@ -54,7 +62,11 @@ public final class MockOrigin implements Origin {
} }
public static Origin of(String value) { 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; package org.springframework.boot.origin;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -29,6 +31,13 @@ import static org.mockito.Mockito.mock;
*/ */
class OriginTests { class OriginTests {
@Test
void getParentWhenDefaultIsNull() {
Origin origin = new Origin() {
};
assertThat(origin.getParent()).isNull();
}
@Test @Test
void fromWhenSourceIsNullReturnsNull() { void fromWhenSourceIsNullReturnsNull() {
assertThat(Origin.from(null)).isNull(); assertThat(Origin.from(null)).isNull();
@ -68,6 +77,20 @@ class OriginTests {
assertThat(Origin.from(exception)).isEqualTo(origin); 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 { static class TestException extends RuntimeException implements OriginProvider {
private final Origin origin; 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.boot.origin.TextResourceOrigin.Location;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -54,7 +55,21 @@ class TextResourceOriginTests {
Location location = new Location(1, 2); Location location = new Location(1, 2);
TextResourceOrigin origin = new TextResourceOrigin(null, location); TextResourceOrigin origin = new TextResourceOrigin(null, location);
assertThat(origin.getLocation()).isEqualTo(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 @Test

Loading…
Cancel
Save