diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java index bb81ccaaea..ea6d3a0c43 100644 --- a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -16,20 +16,24 @@ package org.springframework.core; +import java.util.function.Function; + import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Interface defining a generic contract for attaching and accessing metadata * to/from arbitrary objects. * * @author Rob Harrop + * @author Sam Brannen * @since 2.0 */ public interface AttributeAccessor { /** * Set the attribute defined by {@code name} to the supplied {@code value}. - * If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}. + *

If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}. *

In general, users should take care to prevent overlaps with other * metadata attributes by using fully-qualified names, perhaps using * class or package names as prefix. @@ -40,16 +44,48 @@ public interface AttributeAccessor { /** * Get the value of the attribute identified by {@code name}. - * Return {@code null} if the attribute doesn't exist. + *

Return {@code null} if the attribute doesn't exist. * @param name the unique attribute key * @return the current value of the attribute, if any */ @Nullable Object getAttribute(String name); + /** + * Compute a new value for the attribute identified by {@code name} if + * necessary and {@linkplain #setAttribute set} the new value in this + * {@code AttributeAccessor}. + *

If a value for the attribute identified by {@code name} already exists + * in this {@code AttributeAccessor}, the existing value will be returned + * without applying the supplied compute function. + *

The default implementation of this method is not thread safe but can + * overridden by concrete implementations of this interface. + * @param the type of the attribute value + * @param name the unique attribute key + * @param computeFunction a function that computes a new value for the attribute + * name; the function must not return a {@code null} value + * @return the existing value or newly computed value for the named attribute + * @see #getAttribute(String) + * @see #setAttribute(String, Object) + * @since 5.3.3 + */ + @SuppressWarnings("unchecked") + default T computeAttribute(String name, Function computeFunction) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(computeFunction, "Compute function must not be null"); + Object value = getAttribute(name); + if (value == null) { + value = computeFunction.apply(name); + Assert.state(value != null, + () -> String.format("Compute function must not return null for attribute named '%s'", name)); + setAttribute(name, value); + } + return (T) value; + } + /** * Remove the attribute identified by {@code name} and return its value. - * Return {@code null} if no attribute under {@code name} is found. + *

Return {@code null} if no attribute under {@code name} is found. * @param name the unique attribute key * @return the last value of the attribute, if any */ @@ -58,7 +94,7 @@ public interface AttributeAccessor { /** * Return {@code true} if the attribute identified by {@code name} exists. - * Otherwise return {@code false}. + *

Otherwise return {@code false}. * @param name the unique attribute key */ boolean hasAttribute(String name); diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java index df9c391b05..f5c788d5f4 100644 --- a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-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. @@ -19,6 +19,7 @@ package org.springframework.core; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.Function; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -32,6 +33,7 @@ import org.springframework.util.StringUtils; * * @author Rob Harrop * @author Juergen Hoeller + * @author Sam Brannen * @since 2.0 */ @SuppressWarnings("serial") @@ -59,6 +61,17 @@ public abstract class AttributeAccessorSupport implements AttributeAccessor, Ser return this.attributes.get(name); } + @Override + @SuppressWarnings("unchecked") + public T computeAttribute(String name, Function computeFunction) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(computeFunction, "Compute function must not be null"); + Object value = this.attributes.computeIfAbsent(name, computeFunction); + Assert.state(value != null, + () -> String.format("Compute function must not return null for attribute named '%s'", name)); + return (T) value; + } + @Override @Nullable public Object removeAttribute(String name) { diff --git a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java index bd56c919ce..1b08361933 100644 --- a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java +++ b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -17,39 +17,61 @@ package org.springframework.core; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** + * Unit tests for {@link AttributeAccessorSupport}. + * * @author Rob Harrop * @author Sam Brannen * @since 2.0 */ class AttributeAccessorSupportTests { - private static final String NAME = "foo"; + private static final String NAME = "name"; - private static final String VALUE = "bar"; + private static final String VALUE = "value"; + + private final AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport(); - private AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport(); @Test - void setAndGet() throws Exception { + void setAndGet() { this.attributeAccessor.setAttribute(NAME, VALUE); assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo(VALUE); } @Test - void setAndHas() throws Exception { + void setAndHas() { assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); this.attributeAccessor.setAttribute(NAME, VALUE); assertThat(this.attributeAccessor.hasAttribute(NAME)).isTrue(); } @Test - void remove() throws Exception { + void computeAttribute() { + AtomicInteger atomicInteger = new AtomicInteger(); + Function computeFunction = name -> "computed-" + atomicInteger.incrementAndGet(); + + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + this.attributeAccessor.computeAttribute(NAME, computeFunction); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1"); + this.attributeAccessor.computeAttribute(NAME, computeFunction); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1"); + + this.attributeAccessor.removeAttribute(NAME); + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + this.attributeAccessor.computeAttribute(NAME, computeFunction); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-2"); + } + + @Test + void remove() { assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); this.attributeAccessor.setAttribute(NAME, VALUE); assertThat(this.attributeAccessor.removeAttribute(NAME)).isEqualTo(VALUE); @@ -57,13 +79,13 @@ class AttributeAccessorSupportTests { } @Test - void attributeNames() throws Exception { + void attributeNames() { this.attributeAccessor.setAttribute(NAME, VALUE); this.attributeAccessor.setAttribute("abc", "123"); String[] attributeNames = this.attributeAccessor.attributeNames(); Arrays.sort(attributeNames); - assertThat(Arrays.binarySearch(attributeNames, NAME) > -1).isTrue(); - assertThat(Arrays.binarySearch(attributeNames, "abc") > -1).isTrue(); + assertThat(Arrays.binarySearch(attributeNames, "abc")).isEqualTo(0); + assertThat(Arrays.binarySearch(attributeNames, NAME)).isEqualTo(1); } @SuppressWarnings("serial") diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java index 5cc2dbf912..0d0d0af7e1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -19,6 +19,7 @@ package org.springframework.test.context.support; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -200,6 +201,17 @@ public class DefaultTestContext implements TestContext { return this.attributes.get(name); } + @Override + @SuppressWarnings("unchecked") + public T computeAttribute(String name, Function computeFunction) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(computeFunction, "Compute function must not be null"); + Object value = this.attributes.computeIfAbsent(name, computeFunction); + Assert.state(value != null, + () -> String.format("Compute function must not return null for attribute named '%s'", name)); + return (T) value; + } + @Override @Nullable public Object removeAttribute(String name) {