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