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.OutputStream; 022import java.io.OutputStreamWriter; 023import java.io.Writer; 024import java.nio.charset.Charset; 025import org.apiguardian.api.API; 026import org.apiguardian.api.API.Status; 027 028/** 029 * A writer that wraps an output stream and also writes any content to an in-memory buffer. 030 * 031 * <p>This is thread-safe. 032 * 033 * @author Ashley Scopes 034 * @since 0.0.1 035 */ 036@API(since = "0.0.1", status = Status.STABLE) 037public final class TeeWriter extends Writer { 038 039 private final Object lock; 040 041 private volatile boolean closed; 042 043 private final Writer writer; 044 045 // We use a StringBuilder and manually synchronise it rather than 046 // a string buffer, as we want to manually synchronise the builder 047 // and the delegated output writer at the same time. 048 private final StringBuilder builder; 049 050 /** 051 * Initialise the writer. 052 * 053 * @param writer the underlying writer to "tee" to. 054 */ 055 public TeeWriter(Writer writer) { 056 lock = new Object(); 057 closed = false; 058 059 this.writer = requireNonNull(writer, "writer"); 060 builder = new StringBuilder(); 061 builder.ensureCapacity(512); 062 } 063 064 @Override 065 public void close() throws IOException { 066 // release to set and acquire to check ensures in-order operations to prevent 067 // a very minute chance of a race condition. 068 synchronized (lock) { 069 if (!closed) { 070 closed = true; 071 builder.trimToSize(); 072 writer.flush(); 073 writer.close(); 074 } 075 } 076 } 077 078 @Override 079 public void flush() throws IOException { 080 synchronized (lock) { 081 ensureOpen(); 082 writer.flush(); 083 } 084 } 085 086 /** 087 * Get the content of the internal buffer. 088 * 089 * @return the content. 090 * @since 0.2.1 091 */ 092 @API(since = "0.2.1", status = Status.STABLE) 093 public String getContent() { 094 synchronized (lock) { 095 return builder.toString(); 096 } 097 } 098 099 /** 100 * Get the content of the internal buffer. 101 * 102 * <p>This calls {@link #getContent()} internally as of 0.2.1. 103 * 104 * @return the content. 105 */ 106 @Override 107 public String toString() { 108 return getContent(); 109 } 110 111 @Override 112 public void write(char[] cbuf, int off, int len) throws IOException { 113 synchronized (lock) { 114 ensureOpen(); 115 116 writer.write(cbuf, off, len); 117 // Only append to the buffer once we know that the writing 118 // operation has completed. 119 builder.append(cbuf, off, len); 120 } 121 } 122 123 private void ensureOpen() { 124 if (closed) { 125 throw new IllegalStateException("TeeWriter is closed"); 126 } 127 } 128 129 /** 130 * Create a tee writer for the given output stream. 131 * 132 * <p>Remember you may need to manually flush the tee writer for all contents to be committed to 133 * the output stream. 134 * 135 * @param outputStream the output stream. 136 * @param charset the charset. 137 * @return the Tee Writer. 138 * @since 0.2.1 139 */ 140 @API(since = "0.2.1", status = Status.STABLE) 141 public static TeeWriter wrapOutputStream(OutputStream outputStream, Charset charset) { 142 requireNonNull(outputStream, "outputStream"); 143 requireNonNull(charset, "charset"); 144 var writer = new OutputStreamWriter(outputStream, charset); 145 return new TeeWriter(writer); 146 } 147}