Introduce computeAttribute() in AttributeAccessor

This commit introduces computeAttribute() as an interface default method
in the AttributeAccessor API. This serves as a convenience analogous to
the computeIfAbsent() method in java.util.Map.

Closes gh-26281
This commit is contained in:
Sam Brannen
2020-12-15 22:40:23 +01:00
parent ef6a582c78
commit 1292947f78
4 changed files with 100 additions and 17 deletions

View File

@@ -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}.
* <p>If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}.
* <p>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.
* <p>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}.
* <p>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.
* <p>The default implementation of this method is not thread safe but can
* overridden by concrete implementations of this interface.
* @param <T> 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> T computeAttribute(String name, Function<String, T> 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.
* <p>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}.
* <p>Otherwise return {@code false}.
* @param name the unique attribute key
*/
boolean hasAttribute(String name);

View File

@@ -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> T computeAttribute(String name, Function<String, T> 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) {

View File

@@ -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<String, String> 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")

View File

@@ -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> T computeAttribute(String name, Function<String, T> 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) {