1 /*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License").
5 * You may not use this file except in compliance with the License.
6 * A copy of the License is located at
7 *
8 * http://aws.amazon.com/apache2.0
9 *
10 * or in the "license" file accompanying this file. This file is distributed
11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 * express or implied. See the License for the specific language governing
13 * permissions and limitations under the License.
14 */
15
16 package software.amazon.awssdk.utils;
17
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.concurrent.ExecutorService;
21 import software.amazon.awssdk.annotations.Immutable;
22 import software.amazon.awssdk.annotations.SdkProtectedApi;
23 import software.amazon.awssdk.utils.builder.CopyableBuilder;
24 import software.amazon.awssdk.utils.builder.ToCopyableBuilder;
25
26 /**
27 * A map from {@code AttributeMap.Key<T>} to {@code T} that ensures the values stored with a key matches the type associated with
28 * the key. This does not implement {@link Map} because it has more strict typing requirements, but a {@link Map} can be
29 * converted
30 * to an {code AttributeMap} via the type-unsafe {@link AttributeMap} method.
31 *
32 * This can be used for storing configuration values ({@code OptionKey.LOG_LEVEL} to {@code Boolean.TRUE}), attaching
33 * arbitrary attributes to a request chain ({@code RequestAttribute.CONFIGURATION} to {@code ClientConfiguration}) or similar
34 * use-cases.
35 */
36 @SdkProtectedApi
37 @Immutable
38 public final class AttributeMap implements ToCopyableBuilder<AttributeMap.Builder, AttributeMap>, SdkAutoCloseable {
39 private final Map<Key<?>, Object> attributes;
40
41 private AttributeMap(Map<? extends Key<?>, ?> attributes) {
42 this.attributes = new HashMap<>(attributes);
43 }
44
45 /**
46 * Return true if the provided key is configured in this map. Useful for differentiating between whether the provided key was
47 * not configured in the map or if it is configured, but its value is null.
48 */
49 public <T> boolean containsKey(Key<T> typedKey) {
50 return attributes.containsKey(typedKey);
51 }
52
53 /**
54 * Get the value associated with the provided key from this map. This will return null if the value is not set or if the
55 * value
56 * stored is null. These cases can be disambiguated using {@link #containsKey(Key)}.
57 */
58 public <T> T get(Key<T> key) {
59 Validate.notNull(key, "Key to retrieve must not be null.");
60 return key.convertValue(attributes.get(key));
61 }
62
63 /**
64 * Merges two AttributeMaps into one. This object is given higher precedence then the attributes passed in as a parameter.
65 *
66 * @param lowerPrecedence Options to merge into 'this' AttributeMap object. Any attribute already specified in 'this' object
67 * will be left as is since it has higher precedence.
68 * @return New options with values merged.
69 */
70 public AttributeMap merge(AttributeMap lowerPrecedence) {
71 Map<Key<?>, Object> copiedConfiguration = new HashMap<>(attributes);
72 lowerPrecedence.attributes.forEach(copiedConfiguration::putIfAbsent);
73 return new AttributeMap(copiedConfiguration);
74 }
75
76 public static AttributeMap empty() {
77 return builder().build();
78 }
79
80 public AttributeMap copy() {
81 return toBuilder().build();
82 }
83
84 @Override
85 public void close() {
86 attributes.values().forEach(v -> IoUtils.closeIfCloseable(v, null));
87 attributes.values().forEach(this::shutdownIfExecutorService);
88 }
89
90 private void shutdownIfExecutorService(Object object) {
91 if (object instanceof ExecutorService) {
92 ExecutorService executor = (ExecutorService) object;
93 executor.shutdown();
94 }
95 }
96
97 /**
98 * An abstract class extended by pseudo-enums defining the key for data that is stored in the {@link AttributeMap}. For
99 * example, a {@code ClientOption<T>} may extend this to define options that can be stored in an {@link AttributeMap}.
100 */
101 public abstract static class Key<T> {
102
103 private final Class<?> valueType;
104
105 protected Key(Class<T> valueType) {
106 this.valueType = valueType;
107 }
108
109 protected Key(UnsafeValueType unsafeValueType) {
110 this.valueType = unsafeValueType.valueType;
111 }
112
113 /**
114 * Useful for parameterized types.
115 */
116 protected static class UnsafeValueType {
117 private final Class<?> valueType;
118
119 public UnsafeValueType(Class<?> valueType) {
120 this.valueType = valueType;
121 }
122 }
123
124 /**
125 * Validate the provided value is of the correct type.
126 */
127 final void validateValue(Object value) {
128 if (value != null) {
129 Validate.isAssignableFrom(valueType, value.getClass(),
130 "Invalid option: %s. Required value of type %s, but was %s.",
131 this, valueType, value.getClass());
132 }
133 }
134
135 /**
136 * Validate the provided value is of the correct type and convert it to the proper type for this option.
137 */
138 public final T convertValue(Object value) {
139 validateValue(value);
140
141 @SuppressWarnings("unchecked") // Only actually unchecked if UnsafeValueType is used.
142 T result = (T) valueType.cast(value);
143 return result;
144 }
145 }
146
147 @Override
148 public String toString() {
149 return attributes.toString();
150 }
151
152 @Override
153 public int hashCode() {
154 return attributes.hashCode();
155 }
156
157 @Override
158 public boolean equals(Object obj) {
159 return obj instanceof AttributeMap && attributes.equals(((AttributeMap) obj).attributes);
160 }
161
162 @Override
163 public Builder toBuilder() {
164 return builder().putAll(attributes);
165 }
166
167 public static Builder builder() {
168 return new Builder();
169 }
170
171 public static final class Builder implements CopyableBuilder<Builder, AttributeMap> {
172
173 private final Map<Key<?>, Object> configuration = new HashMap<>();
174
175 private Builder() {
176 }
177
178 public <T> T get(Key<T> key) {
179 Validate.notNull(key, "Key to retrieve must not be null.");
180 return key.convertValue(configuration.get(key));
181 }
182
183 /**
184 * Add a mapping between the provided key and value.
185 */
186 public <T> Builder put(Key<T> key, T value) {
187 Validate.notNull(key, "Key to set must not be null.");
188 configuration.put(key, value);
189 return this;
190 }
191
192 /**
193 * Adds all the attributes from the map provided. This is not type safe, and will throw an exception during creation if
194 * a value in the map is not of the correct type for its key.
195 */
196 public Builder putAll(Map<? extends Key<?>, ?> attributes) {
197 attributes.forEach((key, value) -> {
198 key.validateValue(value);
199 configuration.put(key, value);
200 });
201 return this;
202 }
203
204 @Override
205 public AttributeMap build() {
206 return new AttributeMap(configuration);
207 }
208 }
209
210 }
211