001/*
002 * Copyright (C) 2022 - 2023, the original author or authors.
003 *
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 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
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 io.github.ascopes.jct.diagnostics;
017
018import static java.util.Objects.requireNonNull;
019
020import java.io.IOException;
021import java.io.Writer;
022import org.apiguardian.api.API;
023import org.apiguardian.api.API.Status;
024
025/**
026 * A writer that wraps an output stream and also writes any content to an in-memory buffer.
027 *
028 * <p>This is thread-safe.
029 *
030 * @author Ashley Scopes
031 * @since 0.0.1
032 */
033@API(since = "0.0.1", status = Status.STABLE)
034public final class TeeWriter extends Writer {
035
036  private final Object lock;
037
038  private volatile boolean closed;
039
040  private final Writer writer;
041
042  // We use a StringBuilder and manually synchronise it rather than
043  // a string buffer, as we want to manually synchronise the builder
044  // and the delegated output writer at the same time.
045  private final StringBuilder builder;
046
047  /**
048   * Initialise the writer.
049   *
050   * @param writer the underlying writer to "tee" to.
051   */
052  public TeeWriter(Writer writer) {
053    lock = new Object();
054    closed = false;
055
056    this.writer = requireNonNull(writer, "writer");
057    builder = new StringBuilder();
058    builder.ensureCapacity(512);
059  }
060
061  @Override
062  public void close() throws IOException {
063    // release to set and acquire to check ensures in-order operations to prevent
064    // a very minute chance of a race condition.
065    synchronized (lock) {
066      if (!closed) {
067        closed = true;
068        builder.trimToSize();
069        writer.flush();
070        writer.close();
071      }
072    }
073  }
074
075  @Override
076  public void flush() throws IOException {
077    synchronized (lock) {
078      ensureOpen();
079      writer.flush();
080    }
081  }
082
083  @Override
084  public String toString() {
085    synchronized (lock) {
086      return builder.toString();
087    }
088  }
089
090  @Override
091  public void write(char[] cbuf, int off, int len) throws IOException {
092    synchronized (lock) {
093      ensureOpen();
094
095      writer.write(cbuf, off, len);
096      // Only append to the buffer once we know that the writing
097      // operation has completed.
098      builder.append(cbuf, off, len);
099    }
100  }
101
102  private void ensureOpen() {
103    if (closed) {
104      throw new IllegalStateException("TeeWriter is closed");
105    }
106  }
107}