001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.agentsflex.core.util;
017
018import java.io.Serializable;
019import java.util.Collections;
020import java.util.Map;
021import java.util.Objects;
022import java.util.concurrent.ConcurrentHashMap;
023
024/**
025 * A thread-safe, serializable metadata container for storing key-value pairs.
026 * <p>
027 * <strong>Key Features:</strong>
028 * <ul>
029 *   <li>Thread-safe by default using {@link ConcurrentHashMap}</li>
030 *   <li>Defensive copying to protect internal state</li>
031 *   <li>Null-safe: returns empty map instead of null</li>
032 *   <li>Type-safe generic getter with fallback default</li>
033 *   <li>Immutable view exposure via {@link #asMap()}</li>
034 * </ul>
035 * <p>
036 * <strong>Usage Example:</strong>
037 * <pre>{@code
038 *   Metadata meta = new Metadata();
039 *   meta.putMetadata("model", "gpt-4");
040 *   meta.putMetadata("temperature", 0.7);
041 *
042 *   String model = meta.getMetadata("model", String.class, "default");
043 *   Map<String, Object> snapshot = meta.asMap(); // unmodifiable view
044 * }</pre>
045 *
046 * @author Michael Yang (fuhai999@gmail.com)
047 * @since 2023
048 */
049public class Metadata implements Serializable {
050
051    private static final long serialVersionUID = 1L;
052
053    /**
054     * Internal storage: ConcurrentHashMap for thread-safety.
055     * Lazily initialized to save memory when unused.
056     */
057    protected Map<String, Object> metadataMap;
058
059    /**
060     * Gets the value associated with the specified key.
061     *
062     * @param key the metadata key
063     * @return the value, or {@code null} if not found
064     */
065    public Object getMetadata(String key) {
066        Map<String, Object> map = metadataMap;
067        return (map != null) ? map.get(key) : null;
068    }
069
070    /**
071     * Gets the value associated with the specified key, or returns defaultValue if not found.
072     *
073     * @param key          the metadata key
074     * @param defaultValue the value to return if key is absent
075     * @return the value, or defaultValue if not found
076     */
077    public Object getMetadata(String key, Object defaultValue) {
078        Object value = getMetadata(key);
079        return (value != null) ? value : defaultValue;
080    }
081
082    /**
083     * Type-safe getter with class check and fallback default.
084     *
085     * @param key          the metadata key
086     * @param type         the expected type of the value
087     * @param defaultValue the value to return if key is absent or type mismatch
088     * @param <T>          the expected type
089     * @return the casted value, or defaultValue if not found or type mismatch
090     */
091    @SuppressWarnings("unchecked")
092    public <T> T getMetadata(String key, Class<T> type, T defaultValue) {
093        Objects.requireNonNull(type, "type must not be null");
094        Object value = getMetadata(key);
095        return (type.isInstance(value)) ? (T) value : defaultValue;
096    }
097
098    /**
099     * Puts a metadata entry (replaces existing value if key exists).
100     *
101     * @param key   the metadata key
102     * @param value the metadata value (must be Serializable if serialization is used)
103     */
104    public void putMetadata(String key, Object value) {
105        getOrCreateMap().put(key, value);
106    }
107
108    /**
109     * Puts all entries from the given map (replaces existing values for duplicate keys).
110     *
111     * @param metadata the map of metadata to add
112     */
113    public void putMetadata(Map<String, ?> metadata) {
114        if (metadata == null || metadata.isEmpty()) {
115            return;
116        }
117        getOrCreateMap().putAll(metadata);
118    }
119
120    /**
121     * Puts a metadata entry only if the key is not already present.
122     *
123     * @param key   the metadata key
124     * @param value the metadata value
125     * @return the previous value associated with the key, or null if there was no mapping
126     */
127    public Object putMetadataIfAbsent(String key, Object value) {
128        return getOrCreateMap().putIfAbsent(key, value);
129    }
130
131    /**
132     * Removes the metadata entry for the specified key.
133     *
134     * @param key the metadata key
135     * @return the previous value associated with the key, or null if there was no mapping
136     */
137    public Object removeMetadata(String key) {
138        Map<String, Object> map = metadataMap;
139        return (map != null) ? map.remove(key) : null;
140    }
141
142    /**
143     * Checks if metadata contains the specified key.
144     *
145     * @param key the metadata key
146     * @return true if the key exists, false otherwise
147     */
148    public boolean containsMetadata(String key) {
149        Map<String, Object> map = metadataMap;
150        return (map != null) && map.containsKey(key);
151    }
152
153    /**
154     * Checks if this metadata container is empty.
155     *
156     * @return true if no entries exist, false otherwise
157     */
158    public boolean isEmpty() {
159        Map<String, Object> map = metadataMap;
160        return (map == null) || map.isEmpty();
161    }
162
163    /**
164     * Returns the number of metadata entries.
165     *
166     * @return the entry count
167     */
168    public int sizeOfMetadata() {
169        Map<String, Object> map = metadataMap;
170        return (map != null) ? map.size() : 0;
171    }
172
173    /**
174     * Removes all metadata entries.
175     */
176    public void clearMetadata() {
177        Map<String, Object> map = metadataMap;
178        if (map != null) {
179            map.clear();
180        }
181    }
182
183    /**
184     * Returns an unmodifiable view of the internal metadata map.
185     * <p>
186     * Changes to this Metadata instance will be reflected in the returned view,
187     * but attempts to modify the view directly will throw {@link UnsupportedOperationException}.
188     *
189     * @return an unmodifiable map view (never null)
190     */
191    public Map<String, Object> asMap() {
192        Map<String, Object> map = metadataMap;
193        return (map != null) ? Collections.unmodifiableMap(map) : Collections.emptyMap();
194    }
195
196    /**
197     * Replaces the internal metadata map with a defensive copy of the provided map.
198     * <p>
199     * This method does not merge; it replaces all existing entries.
200     *
201     * @param metadataMap the new metadata map (may be null to clear)
202     */
203    public void setMetadataMap(Map<String, ?> metadataMap) {
204        if (metadataMap == null || metadataMap.isEmpty()) {
205            this.metadataMap = null;
206        } else {
207            // Defensive copy to avoid external mutation
208            this.metadataMap = new ConcurrentHashMap<>(metadataMap);
209        }
210    }
211
212    /**
213     * Gets or creates the internal map with lazy initialization.
214     * Thread-safe via double-checked locking pattern.
215     *
216     * @return the internal map instance
217     */
218    private Map<String, Object> getOrCreateMap() {
219        Map<String, Object> map = metadataMap;
220        if (map == null) {
221            synchronized (this) {
222                map = metadataMap;
223                if (map == null) {
224                    map = new ConcurrentHashMap<>(8);
225                    metadataMap = map;
226                }
227            }
228        }
229        return map;
230    }
231
232    // ========== Object Overrides ==========
233
234    @Override
235    public String toString() {
236        return "Metadata{metadataMap=" + metadataMap + "}";
237    }
238
239    @Override
240    public boolean equals(Object o) {
241        if (this == o) return true;
242        if (!(o instanceof Metadata)) return false;
243        Metadata that = (Metadata) o;
244        // Compare content, not reference
245        return Objects.equals(this.asMap(), that.asMap());
246    }
247
248    @Override
249    public int hashCode() {
250        return Objects.hashCode(asMap());
251    }
252
253    // ========== Backward Compatibility Aliases ==========
254
255    /**
256     * @deprecated use {@link #putMetadata(String, Object)} for clearer semantics.
257     * This method is retained for backward compatibility and will be removed in a future major version.
258     */
259    @Deprecated
260    public void addMetadata(String key, Object value) {
261        putMetadata(key, value);
262    }
263
264    /**
265     * @deprecated use {@link #putMetadata(Map)} for clearer semantics.
266     * This method is retained for backward compatibility and will be removed in a future major version.
267     */
268    @Deprecated
269    public void addMetadata(Map<String, Object> metadata) {
270        putMetadata(metadata);
271    }
272
273    public Map<String, Object> getMetadataMap() {
274        return metadataMap;
275    }
276}