001/*
002 * Copyright 2024-2026 Revetware LLC.
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 */
016
017package com.soklet.servlet.javax;
018
019import com.soklet.servlet.javax.SokletServletPrintWriterEvent.CharAppended;
020import com.soklet.servlet.javax.SokletServletPrintWriterEvent.CharSequenceAppended;
021import com.soklet.servlet.javax.SokletServletPrintWriterEvent.CharWritten;
022import com.soklet.servlet.javax.SokletServletPrintWriterEvent.CharsWritten;
023import com.soklet.servlet.javax.SokletServletPrintWriterEvent.FormatPerformed;
024import com.soklet.servlet.javax.SokletServletPrintWriterEvent.NewlinePrinted;
025import com.soklet.servlet.javax.SokletServletPrintWriterEvent.PrintfPerformed;
026import com.soklet.servlet.javax.SokletServletPrintWriterEvent.StringWritten;
027import com.soklet.servlet.javax.SokletServletPrintWriterEvent.ValuePrinted;
028import com.soklet.servlet.javax.SokletServletPrintWriterEvent.ValueWithNewlinePrinted;
029import org.jspecify.annotations.NonNull;
030import org.jspecify.annotations.Nullable;
031
032import javax.annotation.concurrent.NotThreadSafe;
033import java.io.PrintWriter;
034import java.io.Writer;
035import java.util.Locale;
036import java.util.function.BiConsumer;
037import java.util.function.Consumer;
038
039import static java.util.Objects.requireNonNull;
040
041/**
042 * Special implementation of {@link PrintWriter}, designed for use with {@link SokletHttpServletResponse}.
043 *
044 * @author <a href="https://www.revetkn.com">Mark Allen</a>
045 */
046@NotThreadSafe
047public final class SokletServletPrintWriter extends PrintWriter {
048        @NonNull
049        private final BiConsumer<@NonNull SokletServletPrintWriter, @NonNull SokletServletPrintWriterEvent> onWriteOccurred;
050        @NonNull
051        private final Consumer<@NonNull SokletServletPrintWriter> onWriteFinalized;
052        @NonNull
053        private Boolean writeFinalized = false;
054
055        @NonNull
056        public static SokletServletPrintWriter fromWriter(@NonNull Writer writer) {
057                requireNonNull(writer);
058                return withWriter(writer).build();
059        }
060
061        @NonNull
062        public static Builder withWriter(@NonNull Writer writer) {
063                return new Builder(writer);
064        }
065
066        private SokletServletPrintWriter(@NonNull Builder builder) {
067                super(requireNonNull(builder.writer), true);
068                this.onWriteOccurred = builder.onWriteOccurred != null ? builder.onWriteOccurred : (ignored1, ignored2) -> {};
069                this.onWriteFinalized = builder.onWriteFinalized != null ? builder.onWriteFinalized : (ignored) -> {};
070        }
071
072        /**
073         * Builder used to construct instances of {@link SokletServletPrintWriter}.
074         * <p>
075         * This class is intended for use by a single thread.
076         *
077         * @author <a href="https://www.revetkn.com">Mark Allen</a>
078         */
079        @NotThreadSafe
080        public static class Builder {
081                @NonNull
082                private Writer writer;
083                @Nullable
084                private BiConsumer<@NonNull SokletServletPrintWriter, @NonNull SokletServletPrintWriterEvent> onWriteOccurred;
085                @Nullable
086                private Consumer<@NonNull SokletServletPrintWriter> onWriteFinalized;
087
088                @NonNull
089                private Builder(@NonNull Writer writer) {
090                        requireNonNull(writer);
091                        this.writer = writer;
092                }
093
094                @NonNull
095                public Builder writer(@NonNull Writer writer) {
096                        requireNonNull(writer);
097                        this.writer = writer;
098                        return this;
099                }
100
101                @NonNull
102                public Builder onWriteOccurred(
103                                @Nullable BiConsumer<@NonNull SokletServletPrintWriter, @NonNull SokletServletPrintWriterEvent> onWriteOccurred) {
104                        this.onWriteOccurred = onWriteOccurred;
105                        return this;
106                }
107
108                @NonNull
109                public Builder onWriteFinalized(@Nullable Consumer<@NonNull SokletServletPrintWriter> onWriteFinalized) {
110                        this.onWriteFinalized = onWriteFinalized;
111                        return this;
112                }
113
114                @NonNull
115                public SokletServletPrintWriter build() {
116                        return new SokletServletPrintWriter(this);
117                }
118        }
119
120        @NonNull
121        private Boolean getWriteFinalized() {
122                return this.writeFinalized;
123        }
124
125        private void setWriteFinalized(@NonNull Boolean writeFinalized) {
126                requireNonNull(writeFinalized);
127                this.writeFinalized = writeFinalized;
128        }
129
130        @NonNull
131        private BiConsumer<@NonNull SokletServletPrintWriter, @NonNull SokletServletPrintWriterEvent> getOnWriteOccurred() {
132                return this.onWriteOccurred;
133        }
134
135        @NonNull
136        private Consumer<@NonNull SokletServletPrintWriter> getOnWriteFinalized() {
137                return this.onWriteFinalized;
138        }
139
140        private void flushOutput() {
141                super.flush();
142        }
143
144// Implementation of PrintWriter methods below:
145
146        @Override
147        public void write(@NonNull char[] buf,
148                                                                                int off,
149                                                                                int len) {
150                requireNonNull(buf);
151
152                super.write(buf, off, len);
153                flushOutput();
154                getOnWriteOccurred().accept(this, new CharsWritten(buf, off, len));
155        }
156
157        @Override
158        public void write(@NonNull String s,
159                                                                                int off,
160                                                                                int len) {
161                requireNonNull(s);
162
163                super.write(s, off, len);
164                flushOutput();
165                getOnWriteOccurred().accept(this, new StringWritten(s, off, len));
166        }
167
168        @Override
169        public void write(int c) {
170                super.write(c);
171                flushOutput();
172                getOnWriteOccurred().accept(this, new CharWritten(c));
173        }
174
175        @Override
176        public void write(@NonNull char[] buf) {
177                requireNonNull(buf);
178
179                super.write(buf);
180                flushOutput();
181                getOnWriteOccurred().accept(this, new CharsWritten(buf, 0, buf.length));
182        }
183
184        @Override
185        public void write(@NonNull String s) {
186                requireNonNull(s);
187
188                super.write(s);
189                flushOutput();
190                getOnWriteOccurred().accept(this, new StringWritten(s, 0, s.length()));
191        }
192
193        @Override
194        public void print(boolean b) {
195                super.print(b);
196                flushOutput();
197                getOnWriteOccurred().accept(this, new ValuePrinted(b));
198        }
199
200        @Override
201        public void print(char c) {
202                super.print(c);
203                flushOutput();
204                getOnWriteOccurred().accept(this, new ValuePrinted(c));
205        }
206
207        @Override
208        public void print(int i) {
209                super.print(i);
210                flushOutput();
211                getOnWriteOccurred().accept(this, new ValuePrinted(i));
212        }
213
214        @Override
215        public void print(long l) {
216                super.print(l);
217                flushOutput();
218                getOnWriteOccurred().accept(this, new ValuePrinted(l));
219        }
220
221        @Override
222        public void print(float f) {
223                super.print(f);
224                flushOutput();
225                getOnWriteOccurred().accept(this, new ValuePrinted(f));
226        }
227
228        @Override
229        public void print(double d) {
230                super.print(d);
231                flushOutput();
232                getOnWriteOccurred().accept(this, new ValuePrinted(d));
233        }
234
235        @Override
236        public void print(@NonNull char[] s) {
237                requireNonNull(s);
238
239                super.print(s);
240                flushOutput();
241                getOnWriteOccurred().accept(this, new ValuePrinted(s));
242        }
243
244        @Override
245        public void print(@Nullable String s) {
246                super.print(s);
247                flushOutput();
248                getOnWriteOccurred().accept(this, new ValuePrinted(s));
249        }
250
251        @Override
252        public void print(@Nullable Object obj) {
253                super.print(obj);
254                flushOutput();
255                getOnWriteOccurred().accept(this, new ValuePrinted(obj));
256        }
257
258        @Override
259        public void println() {
260                super.println();
261                flush();
262                getOnWriteOccurred().accept(this, new NewlinePrinted());
263        }
264
265        @Override
266        public void println(boolean x) {
267                super.println(x);
268                flush();
269                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
270        }
271
272        @Override
273        public void println(char x) {
274                super.println(x);
275                flush();
276                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
277        }
278
279        @Override
280        public void println(int x) {
281                super.println(x);
282                flush();
283                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
284        }
285
286        @Override
287        public void println(long x) {
288                super.println(x);
289                flush();
290                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
291        }
292
293        @Override
294        public void println(float x) {
295                super.println(x);
296                flush();
297                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
298        }
299
300        @Override
301        public void println(double x) {
302                super.println(x);
303                flush();
304                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
305        }
306
307        @Override
308        public void println(char[] x) {
309                requireNonNull(x);
310
311                super.println(x);
312                flush();
313                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
314        }
315
316        @Override
317        public void println(@Nullable String x) {
318                super.println(x);
319                flush();
320                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
321        }
322
323        @Override
324        public void println(@Nullable Object x) {
325                super.println(x);
326                flush();
327                getOnWriteOccurred().accept(this, new ValueWithNewlinePrinted(x));
328        }
329
330        @Override
331        @NonNull
332        public PrintWriter printf(@NonNull String format,
333                                                                                                                @Nullable Object @Nullable ... args) {
334                requireNonNull(format);
335
336                @Nullable Object @NonNull [] normalizedArgs = args != null ? args : new Object[0];
337                PrintWriter printWriter = super.printf(format, normalizedArgs);
338                flush();
339                getOnWriteOccurred().accept(this, new PrintfPerformed(null, format, normalizedArgs));
340
341                return printWriter;
342        }
343
344        @Override
345        @NonNull
346        public PrintWriter printf(@Nullable Locale l,
347                                                                                                                @NonNull String format,
348                                                                                                                @Nullable Object @Nullable ... args) {
349                requireNonNull(format);
350
351                @Nullable Object @NonNull [] normalizedArgs = args != null ? args : new Object[0];
352                PrintWriter printWriter = super.printf(l, format, normalizedArgs);
353                flush();
354                getOnWriteOccurred().accept(this, new PrintfPerformed(l, format, normalizedArgs));
355                return printWriter;
356        }
357
358        @Override
359        @NonNull
360        public PrintWriter format(@NonNull String format,
361                                                                                                                @Nullable Object @Nullable ... args) {
362                requireNonNull(format);
363
364                @Nullable Object @NonNull [] normalizedArgs = args != null ? args : new Object[0];
365                PrintWriter printWriter = super.format(format, normalizedArgs);
366                flush();
367                getOnWriteOccurred().accept(this, new FormatPerformed(null, format, normalizedArgs));
368
369                return printWriter;
370        }
371
372        @Override
373        @NonNull
374        public PrintWriter format(@Nullable Locale l,
375                                                                                                                @NonNull String format,
376                                                                                                                @Nullable Object @Nullable ... args) {
377                requireNonNull(format);
378
379                @Nullable Object @NonNull [] normalizedArgs = args != null ? args : new Object[0];
380                PrintWriter printWriter = super.format(l, format, normalizedArgs);
381                flush();
382                getOnWriteOccurred().accept(this, new FormatPerformed(l, format, normalizedArgs));
383
384                return printWriter;
385        }
386
387        @Override
388        @NonNull
389        public PrintWriter append(@Nullable CharSequence csq) {
390                // JDK does this, we mirror it
391                if (csq == null)
392                        csq = "null";
393
394                PrintWriter printWriter = super.append(csq);
395                flushOutput();
396                getOnWriteOccurred().accept(this, new CharSequenceAppended(csq, 0, csq.length()));
397                return printWriter;
398        }
399
400        @Override
401        @NonNull
402        public PrintWriter append(@Nullable CharSequence csq,
403                                                                                                                int start,
404                                                                                                                int end) {
405                // JDK does this, we mirror it
406                if (csq == null)
407                        csq = "null";
408
409                PrintWriter printWriter = super.append(csq, start, end);
410                flushOutput();
411                getOnWriteOccurred().accept(this, new CharSequenceAppended(csq, start, end));
412                return printWriter;
413        }
414
415        @Override
416        @NonNull
417        public PrintWriter append(char c) {
418                PrintWriter printWriter = super.append(c);
419                flushOutput();
420                getOnWriteOccurred().accept(this, new CharAppended(c));
421                return printWriter;
422        }
423
424        @Override
425        public void flush() {
426                super.flush();
427
428                if (!getWriteFinalized()) {
429                        setWriteFinalized(true);
430                        getOnWriteFinalized().accept(this);
431                }
432        }
433
434        @Override
435        public void close() {
436                super.flush();
437                super.close();
438
439                if (!getWriteFinalized()) {
440                        setWriteFinalized(true);
441                        getOnWriteFinalized().accept(this);
442                }
443        }
444}