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) {