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}