View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2026 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix;
19  
20  import java.io.Closeable;
21  import java.io.IOException;
22  import java.lang.ref.Cleaner;
23  import java.util.Objects;
24  
25  import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
26  
27  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
28  
29  /**
30   * This wrapper (along with the Java 8-specific counterpart in src/main/java8) allows us to
31   * implement cleanup logic for objects that are garbage-collectable/no longer reachable.
32   *
33   * <p>
34   * Usage:
35   * <ol>
36   * <li>Create a subclass of CleanableState and attach it as a private field to the object you want
37   * to be cleaned up. You may call that field {@code cleanableState}.</li>
38   * <li>Define all resources that need to be cleaned up in this subclass (instead of the observed
39   * object itself).</li>
40   * <li>Make sure to not refer the observed object instance itself, as that will create a reference
41   * cycle and prevent proper cleanup.</li>
42   * <li>Implement the {@link #doClean()} method to perform all the necessary cleanup steps.</li>
43   * <li>If the observed class implements {@code close()}, it's a good practice to have it just call
44   * {@code cleanableState.runCleaner()}.</li>
45   * </ol>
46   * <p>
47   * Exceptions thrown upon doClean() are either thrown via {@link #close()} when invoked directly,
48   * or, if cleaned during Garbage collection, to the exception handler specified in the constructor
49   * or (if none specified) to the default uncaught exception handler.
50   * <p>
51   * Implementation details:
52   * <ul>
53   * <li>In Java 9 or later, {@link Cleaner} is used under the hood.</li>
54   * <li>In Java 8 or earlier, {@link #finalize()} calls {@link #doClean()} directly.</li>
55   * </ul>
56   *
57   * @author Christian Kohlschütter
58   */
59  @IgnoreJRERequirement // see src/main/java8
60  public abstract class CleanableState implements Closeable {
61    /**
62     * A default exception handler: Calls {@link Thread#getDefaultUncaughtExceptionHandler()} with the
63     * stacktrace and information about the current thread.
64     */
65    public static final AFConsumer<Throwable> DEFAULT_EXCEPTION_HANDLER = (t) -> Thread
66        .getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), t);
67  
68    private static final Cleaner CLEANER = Cleaner.create();
69    private final Cleaner.Cleanable cleanable;
70    private AFConsumer<Throwable> exceptionHandler;
71    private AFConsumer<Throwable> exceptionHandlerCurrent;
72    private IOException exceptionUponClose = null;
73  
74    /**
75     * Creates a state object to be used as an implementation detail of the specified observed
76     * instance, using the {@link #DEFAULT_EXCEPTION_HANDLER}.
77     *
78     * @param observed The observed instance (the outer class referencing this
79     *          {@link CleanableState}).
80     */
81    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
82    protected CleanableState(Object observed) {
83      this(observed, DEFAULT_EXCEPTION_HANDLER);
84    }
85  
86    /**
87     * Creates a state object to be used as an implementation detail of the specified observed
88     * instance, using a custom exception handler.
89     *
90     * @param observed The observed instance (the outer class referencing this
91     *          {@link CleanableState}).
92     * @param exceptionHandler The exception handler.
93     */
94    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
95    protected CleanableState(Object observed, AFConsumer<Throwable> exceptionHandler) {
96      Objects.requireNonNull(exceptionHandler);
97  
98      this.exceptionHandler = exceptionHandler;
99      this.exceptionHandlerCurrent = exceptionHandler;
100     this.cleanable = CLEANER.register(observed, () -> doClean1());
101   }
102 
103   /**
104    * Explicitly the cleanup code defined in {@link #doClean()}. This is best be called from a
105    * {@code close()} method in the observed class.
106    */
107   public final void runCleaner() {
108     cleanable.clean();
109   }
110 
111   private void doClean1() {
112     try {
113       doClean();
114     } catch (Throwable t) { // NOPMD
115       exceptionHandlerCurrent.accept(t);
116     }
117   }
118 
119   /**
120    * Performs the actual cleanup. Be sure to always clean up whenever possible, either by tracking
121    * potential exceptions or by using try-finally to ensure proper cleanup.
122    *
123    * @throws IOException on error.
124    */
125   protected abstract void doClean() throws IOException;
126 
127   /**
128    * Checks if we're being called from within {@link #close()}.
129    *
130    * @return {@code true} if being called from within {@link #close()}.
131    */
132   protected boolean inClose() {
133     return exceptionHandlerCurrent != exceptionHandler; // NOPMD
134   }
135 
136   @Override
137   public final void close() throws IOException {
138     this.exceptionHandlerCurrent = (t) -> {
139       IOException exc = exceptionUponClose;
140       if (exc != null) {
141         exc.addSuppressed(t);
142       } else if (t instanceof IOException) {
143         exceptionUponClose = (IOException) t;
144       } else {
145         exceptionUponClose = new IOException();
146         exceptionUponClose.addSuppressed(t);
147       }
148     };
149     runCleaner();
150     this.exceptionHandlerCurrent = exceptionHandler;
151 
152     if (exceptionUponClose != null) {
153       throw exceptionUponClose;
154     }
155   }
156 }