2222import org .codehaus .groovy .util .ListHashMap ;
2323
2424import java .util .Collections ;
25+ import java .util .HashMap ;
2526import java .util .Map ;
2627import java .util .function .Function ;
2728
2829/**
2930 * An interface to mark a node being able to handle metadata.
31+ * <p>
32+ * The default {@link #newMetaDataMap()} returns a {@link ListHashMap} wrapped in
33+ * {@link Collections#synchronizedMap}, so concurrent compiles sharing an AST node
34+ * (e.g. built-in annotation {@code ClassNode}s cached by {@code ClassHelper})
35+ * cannot trip an {@code ArrayIndexOutOfBoundsException} during the
36+ * array-to-{@link HashMap} transition in {@code ListHashMap.put}. Implementers
37+ * that store the map in a field should declare it {@code volatile} so the
38+ * unsynchronized fast-path read in the default methods sees publish-time writes.
3039 *
3140 * @since 3.0.0
3241 */
@@ -57,12 +66,7 @@ default <T> T getNodeMetaData(Object key) {
5766 default <T > T getNodeMetaData (Object key , Function <?, ? extends T > valFn ) {
5867 if (key == null ) throw new GroovyBugError ("Tried to get/set meta data with null key on " + this + "." );
5968
60- Map metaDataMap = this .getMetaDataMap ();
61- if (metaDataMap == null ) {
62- metaDataMap = this .newMetaDataMap ();
63- this .setMetaDataMap (metaDataMap );
64- }
65- return (T ) metaDataMap .computeIfAbsent (key , valFn );
69+ return (T ) getOrCreateMetaDataMap ().computeIfAbsent (key , valFn );
6670 }
6771
6872 /**
@@ -75,13 +79,13 @@ default void copyNodeMetaData(NodeMetaDataHandler other) {
7579 if (otherMetaDataMap == null ) {
7680 return ;
7781 }
78- Map metaDataMap = this .getMetaDataMap ();
79- if (metaDataMap == null ) {
80- metaDataMap = this .newMetaDataMap ();
81- this .setMetaDataMap (metaDataMap );
82+ // snapshot under the source map's mutex to honour the synchronizedMap iteration contract
83+ Map snapshot ;
84+ synchronized (otherMetaDataMap ) {
85+ if (otherMetaDataMap .isEmpty ()) return ;
86+ snapshot = new HashMap <>(otherMetaDataMap );
8287 }
83-
84- metaDataMap .putAll (otherMetaDataMap );
88+ getOrCreateMetaDataMap ().putAll (snapshot );
8589 }
8690
8791 /**
@@ -107,15 +111,11 @@ default void setNodeMetaData(Object key, Object value) {
107111 default Object putNodeMetaData (Object key , Object value ) {
108112 if (key == null ) throw new GroovyBugError ("Tried to set meta data with null key on " + this + "." );
109113
110- Map metaDataMap = this .getMetaDataMap ();
111- if (metaDataMap == null ) {
112- if (value == null ) return null ;
113- metaDataMap = newMetaDataMap ();
114- this .setMetaDataMap (metaDataMap );
115- } else if (value == null ) {
116- return metaDataMap .remove (key );
114+ if (value == null ) {
115+ Map metaDataMap = this .getMetaDataMap ();
116+ return metaDataMap == null ? null : metaDataMap .remove (key );
117117 }
118- return metaDataMap .put (key , value );
118+ return getOrCreateMetaDataMap () .put (key , value );
119119 }
120120
121121 /**
@@ -135,7 +135,7 @@ default void removeNodeMetaData(Object key) {
135135 }
136136
137137 /**
138- * Returns an unmodifiable view of the current node metadata.
138+ * Returns an unmodifiable snapshot of the current node metadata.
139139 *
140140 * @return the node metadata. Always not null.
141141 */
@@ -144,18 +144,55 @@ default void removeNodeMetaData(Object key) {
144144 if (metaDataMap == null ) {
145145 return Collections .emptyMap ();
146146 }
147- return Collections .unmodifiableMap (metaDataMap );
147+ // snapshot under the map's mutex to honour the synchronizedMap iteration contract
148+ synchronized (metaDataMap ) {
149+ return Collections .unmodifiableMap (new HashMap <>(metaDataMap ));
150+ }
151+ }
152+
153+ /**
154+ * Returns the existing metadata map, creating one via {@link #newMetaDataMap()}
155+ * on first use. Lazy creation is guarded by a brief lock on {@code this} so
156+ * concurrent first-callers agree on a single map; subsequent callers see the
157+ * map via the (volatile) field read and skip the lock entirely.
158+ */
159+ private Map getOrCreateMetaDataMap () {
160+ Map metaDataMap = this .getMetaDataMap ();
161+ if (metaDataMap != null ) return metaDataMap ;
162+ synchronized (this ) {
163+ metaDataMap = this .getMetaDataMap ();
164+ if (metaDataMap == null ) {
165+ metaDataMap = this .newMetaDataMap ();
166+ this .setMetaDataMap (metaDataMap );
167+ }
168+ return metaDataMap ;
169+ }
148170 }
149171
150172 //--------------------------------------------------------------------------
151173
174+ /**
175+ * Returns the underlying metadata map. The map returned by the default
176+ * {@link #newMetaDataMap()} is internally synchronized, so individual
177+ * {@code get}/{@code put}/{@code remove} calls are thread-safe; however,
178+ * per the {@link Collections#synchronizedMap} contract, iteration over the
179+ * returned map (or any of its {@code keySet}, {@code values}, or
180+ * {@code entrySet} views) must be done inside a
181+ * {@code synchronized (map) { ... }} block to avoid
182+ * {@code ConcurrentModificationException}.
183+ */
152184 Map <?, ?> getMetaDataMap ();
153185
154186 /**
187+ * Creates the backing metadata map. The default returns a {@link ListHashMap}
188+ * wrapped in {@link Collections#synchronizedMap} for thread-safe per-entry
189+ * access; subclasses may override to supply an alternative map (e.g. for
190+ * different memory/concurrency trade-offs).
191+ *
155192 * @since 5.0.0
156193 */
157194 default Map <?, ?> newMetaDataMap () {
158- return new ListHashMap ();
195+ return Collections . synchronizedMap ( new ListHashMap () );
159196 }
160197
161198 void setMetaDataMap (Map <?, ?> metaDataMap );
0 commit comments