001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2023–2025 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License.  You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.reference;
015
016import java.util.IdentityHashMap;
017import java.util.LinkedHashSet;
018import java.util.Map;
019import java.util.Set;
020
021import java.util.concurrent.locks.Lock;
022import java.util.concurrent.locks.ReentrantLock;
023
024import org.microbean.reference.DestructorRegistry.Destructor;
025
026/**
027 * A straightforward {@link DestructorTree} implementation.
028 *
029 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
030 *
031 * @see DestructorTree
032 */
033public class DefaultDestructorTree implements DestructorTree {
034
035
036  /*
037   * Instance fields.
038   */
039
040
041  private final Lock lock;
042
043  // @GuardedBy("lock")
044  private Map<Object, Destructor> destructors; // identity hashmap when open for business
045
046
047  /*
048   * Constructors.
049   */
050
051
052  /**
053   * Creates a new {@link DefaultDestructorTree}.
054   */
055  public DefaultDestructorTree() {
056    super();
057    this.lock = new ReentrantLock();
058  }
059
060
061  /*
062   * Instance methods.
063   */
064
065
066  /*
067   * Returns a new {@link DefaultDestructorTree} instance that is not {@linkplain #close() closed}, has no {@linkplain
068   * #register(Object, Destructor) registrations} yet, and is itself {@linkplain #register(Object, Destructor) registered}
069   * as a destructor with this {@link DefaultDestructorTree}.
070   *
071   * @return a new, {@linkplain #close() unclosed} {@link DefaultDestructorTree} {@linkplain #register(Object, Destructor)
072   * registered} as a destructor with this {@link DefaultDestructorTree}
073   *
074   * @exception IllegalStateException if this {@link DefaultDestructorTree} is {@linkplain #close() closed}
075   *
076   * @microbean.nullability This method does not, and its overrides must not, return {@code null}.
077   *
078   * @microbean.idempotency Overrides of this method must return new, distinct {@link DefaultDestructorTree} instances.
079   *
080   * @microbean.threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads.
081   *
082   * @see #register(Object, Destructor)
083   *
084   * @see #close()
085   */
086  @Override // DestructorTree
087  public DefaultDestructorTree newChild() {
088    final DefaultDestructorTree child = new DefaultDestructorTree();
089    if (!this.register(child, child::close)) { // CRITICAL
090      throw new IllegalStateException();
091    }
092    return child;
093  }
094
095  /**
096   * Closes this {@link DefaultDestructorTree} and destroys its {@linkplain #register(Object, Destructor) registrants}
097   * by {@linkplain Destructor#destroy() running} their destructors {@linkplain #register(Object, Destructor) supplied
098   * at registration time}.
099   *
100   * <p>{@link Destructor#destroy()} is called on all {@linkplain #register(Object, Destructor) registrants}, even in the
101   * presence of exceptions. {@link RuntimeException}s consequently thrown may {@linkplain Throwable#getSuppressed()
102   * contain suppressed exceptions}.</p>
103   *
104   * <p>Overrides of this method wishing to add semantics to this behavior should perform that work before calling
105   * {@link #close() super.close()}.</p>
106   *
107   * <p>Overrides of this method must call {@link #close() super.close()} or undefined behavior may result.</p>
108   *
109   * <p>After any successful invocation of this method, this {@link DefaultDestructorTree} is deemed to be
110   * irrevocably closed. Invoking this method again will have no effect.</p>
111   *
112   * @microbean.idempotency This method is, and its overrides must be, idempotent.
113   *
114   * @microbean.threadsafety This method is, and its overrides must be, safe for concurrent use by multiple threads.
115   */
116  @Override // DestructorTree
117  public void close() {
118    Map<Object, Destructor> destructors;
119    lock.lock();
120    try {
121      destructors = this.destructors;
122      if (destructors == Map.<Object, Destructor>of()) {
123        // Already closed
124        return;
125      }
126      this.destructors = Map.of();
127    } finally {
128      lock.unlock();
129    }
130
131    if (destructors == null) {
132      // nothing to do
133      return;
134    }
135
136    RuntimeException re = null;
137    for (final Destructor d : destructors.values()) {
138      try {
139        d.destroy();
140      } catch (final RuntimeException e) {
141        if (re == null) {
142          re = e;
143        } else {
144          re.addSuppressed(e);
145        }
146      }
147    }
148
149    if (re != null) {
150      throw re;
151    }
152  }
153
154  /**
155   * If this {@link DefaultDestructorTree} is not closed, and if the supplied {@code reference} has not yet been
156   * registered, registers it such that it will be destroyed by the supplied {@code destructor} when this {@link
157   * DefaultDestructorTree} is {@linkplain #close() closed}, and returns {@code true}.
158   *
159   * <p>This method takes no action and returns {@code false} in all other cases.</p>
160   *
161   * @param reference a contextual reference that will be destroyed later; if {@code null} then no action will be taken
162   * and {@code false} will be returned
163   *
164   * @param destructor a {@link Destructor} that, when {@linkplain Destructor#destroy() run}, will destroy the supplied
165   * {@code reference} in some way; if {@code null} then no action will be taken and {@code false} will be returned; if
166   * non-{@code null} <strong>must be idempotent and safe for concurrent use by multiple threads</strong>
167   *
168   * @return {@code true} if and only if this {@link DefaultDestructorTree} is not closed, and the supplied {@code
169   * reference} is not already registered and registration completed successfully; {@code false} in all other cases
170   *
171   * @microbean.idempotency This method is idempotent.
172   *
173   * @microbean.threadsafety This method is safe for concurrent use by multiple threads.
174   */
175  @Override // DestructorRegistry
176  public final boolean register(final Object reference, final Destructor destructor) {
177    if (reference == null || destructor == null) {
178      return false;
179    }
180    lock.lock();
181    try {
182      if (this.destructors == null) {
183        this.destructors = new IdentityHashMap<>(); // critical that this is an IdentityHashMap
184      } else if (this.destructors == Map.<Object, Destructor>of()) {
185        // Already closed; register must therefore be a no-op.
186        return false;
187      }
188      return this.destructors.putIfAbsent(reference, destructor) == null;
189    } finally {
190      lock.unlock();
191    }
192  }
193
194  @Override // DestructorTree
195  public final Destructor remove(final Object reference) {
196    if (reference == null) {
197      return null;
198    }
199    lock.lock();
200    try {
201      return this.destructors == null ? null : this.destructors.remove(reference);
202    } finally {
203      lock.unlock();
204    }
205  }
206
207}