001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.api.server;
021
022import jakarta.annotation.Nonnull;
023
024import java.util.Collection;
025import java.util.Map;
026import java.util.Set;
027import java.util.concurrent.ConcurrentHashMap;
028
029/**
030 * RequestDetails.getUserData() used a HashMap previously but that is not thread-safe.  It's not
031 * possible to simply replace HashMap with ConcurrentHashMap in RequestDetails since null
032 * values work differently. This class is a thin wrapper around ConcurrentHashMap that
033 * preserves the expected null value behaviour on get. Note that contains() and size() do
034 * behave differently. See UserDataMapTest for details.  We could use Collections.synchronizedMap to achieve
035 * the required thread-safety, but ConcurrentHashMap has better performance characteristics.
036 */
037public class UserDataMap implements Map<Object, Object> {
038        private final Map<Object, Object> myMap = new ConcurrentHashMap<>();
039
040        @Override
041        public int size() {
042                return myMap.size();
043        }
044
045        @Override
046        public boolean isEmpty() {
047                return myMap.isEmpty();
048        }
049
050        @Override
051        public boolean containsKey(Object key) {
052                return myMap.containsKey(key);
053        }
054
055        @Override
056        public boolean containsValue(Object value) {
057                if (value == null) {
058                        return false;
059                }
060                return myMap.containsValue(value);
061        }
062
063        @Override
064        public Object get(Object key) {
065                return myMap.get(key);
066        }
067
068        @Override
069        public Object put(Object key, Object value) {
070                if (value == null) {
071                        return myMap.remove(key);
072                }
073                return myMap.put(key, value);
074        }
075
076        @Override
077        public Object remove(Object key) {
078                return myMap.remove(key);
079        }
080
081        @Override
082        public void clear() {
083                myMap.clear();
084        }
085
086        @Override
087        public void putAll(@Nonnull Map<?, ?> m) {
088                // We can't delegate directly to putAll since null keys would throw an exception
089                for (Entry<?, ?> entry : m.entrySet()) {
090                        put(entry.getKey(), entry.getValue());
091                }
092        }
093
094        @Override
095        @Nonnull
096        public Set<Object> keySet() {
097                return myMap.keySet();
098        }
099
100        @Override
101        @Nonnull
102        public Collection<Object> values() {
103                return myMap.values();
104        }
105
106        @Override
107        @Nonnull
108        public Set<Entry<Object, Object>> entrySet() {
109                return myMap.entrySet();
110        }
111
112        @Override
113        public boolean equals(Object o) {
114                if (this == o) return true;
115                if (!(o instanceof Map)) return false;
116                return myMap.equals(o);
117        }
118
119        @Override
120        public int hashCode() {
121                return myMap.hashCode();
122        }
123}