001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2024 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.webconlet.oidclogin;
020
021import java.nio.Buffer;
022import java.nio.ByteBuffer;
023import java.nio.CharBuffer;
024import java.nio.charset.Charset;
025import java.nio.charset.CharsetDecoder;
026import java.nio.charset.StandardCharsets;
027import java.util.function.Consumer;
028import org.jgrapes.io.util.InputConsumer;
029import org.jgrapes.io.util.ManagedBuffer;
030
031/**
032 * Collects character data from buffers and makes it available as
033 * a text.
034 */
035@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
036public class TextCollector implements InputConsumer {
037    private boolean isEof;
038    private CharsetDecoder decoder;
039    private Charset charset = StandardCharsets.UTF_8;
040    private CharBuffer collected;
041    private int maxLength = 8192;
042    private Consumer<String> consumer = text -> {
043    };
044
045    /**
046     * Sets the charset to be used if {@link #feed(ManagedBuffer)}
047     * is invoked with `ManagedBuffer<ByteBuffer>`. Defaults to UTF-8. 
048     * Must be set before the first invocation of 
049     * {@link #feed(ManagedBuffer)}.  
050     *
051     * @param charset the charset
052     * @return the managed buffer reader
053     */
054    public TextCollector charset(Charset charset) {
055        if (decoder != null) {
056            throw new IllegalStateException("Charset cannot be changed.");
057        }
058        this.charset = charset;
059        return this;
060    }
061
062    /**
063     * Sets the charset to be used if {@link #feed(ManagedBuffer)}
064     * is invoked with `ManagedBuffer<ByteBuffer>` to the charset
065     * specified as system property `native.encoding`. If this
066     * property does not specify a valid charset, 
067     * {@link Charset#defaultCharset()} is used.
068     *  
069     * Must be invoked before the first invocation of 
070     * {@link #feed(ManagedBuffer)}.  
071     *
072     * @return the managed buffer reader
073     */
074    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
075        "PMD.EmptyCatchBlock" })
076    public TextCollector nativeCharset() {
077        Charset toSet = Charset.defaultCharset();
078        var toCheck = System.getProperty("native.encoding");
079        if (toCheck != null) {
080            try {
081                toSet = Charset.forName(toCheck);
082            } catch (Exception e) {
083                // If this fails, simply use default
084            }
085        }
086        charset(toSet);
087        return this;
088    }
089
090    /**
091     * Configures the maximum length of the collected text. Input
092     * exceeding this size will be discarded.
093     *
094     * @param maximumLength the maximum length
095     * @return the maximum size
096     */
097    public TextCollector maximumSize(int maximumLength) {
098        this.maxLength = maximumLength;
099        return this;
100    }
101
102    /**
103     * Configures a consumer for the collected text. The consumer 
104     * is invoked once when the complete text is available.
105     *
106     * @param consumer the consumer
107     * @return the line collector
108     */
109    public TextCollector consumer(Consumer<String> consumer) {
110        this.consumer = consumer;
111        return this;
112    }
113
114    /**
115     * Feed data to the collector. 
116     * 
117     * Calling this method with `null` as argument closes the feed.
118     *
119     * @param buffer the buffer
120     */
121    public <W extends Buffer> void feed(W buffer) {
122        if (isEof) {
123            return;
124        }
125        if (buffer == null) {
126            isEof = true;
127            collected.flip();
128            consumer.accept(collected.toString());
129            collected = null;
130        } else {
131            copyToCollected(buffer);
132        }
133    }
134
135    /**
136     * Feed data to the collector. 
137     * 
138     * Calling this method with `null` as argument closes the feed.
139     *
140     * @param buffer the buffer
141     */
142    @Override
143    public <W extends Buffer> void feed(ManagedBuffer<W> buffer) {
144        if (buffer == null) {
145            feed((W) null);
146        } else {
147            feed(buffer.backingBuffer());
148        }
149    }
150
151    private <W extends Buffer> void copyToCollected(W buffer) {
152        try {
153            buffer.mark();
154            if (collected == null) {
155                int size = buffer.capacity();
156                if (size < maxLength && maxLength < 16_384) {
157                    size = maxLength;
158                }
159                collected = CharBuffer.allocate(size);
160            }
161            if (collected.position() >= maxLength) {
162                return;
163            }
164            if (buffer instanceof CharBuffer charBuf) {
165                if (collected.remaining() < charBuf.remaining()) {
166                    resizeCollected(charBuf);
167                }
168                collected.put(charBuf);
169                return;
170            }
171            if (decoder == null) {
172                decoder = charset.newDecoder();
173            }
174            while (true) {
175                var result
176                    = decoder.decode((ByteBuffer) buffer, collected, isEof);
177                if (!result.isOverflow()) {
178                    break;
179                }
180                // Need larger buffer
181                resizeCollected(buffer);
182            }
183        } finally {
184            buffer.reset();
185        }
186    }
187
188    private void resizeCollected(Buffer toAppend) {
189        var old = collected;
190        collected = CharBuffer.allocate(old.capacity() + toAppend.capacity());
191        old.flip();
192        collected.put(old);
193    }
194
195    /**
196     * Checks if more input may become available.
197     *
198     * @return true, if successful
199     */
200    public boolean eof() {
201        return isEof;
202    }
203}