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