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}