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 */
035public class TextCollector implements InputConsumer {
036    private boolean isEof;
037    private CharsetDecoder decoder;
038    private Charset charset = StandardCharsets.UTF_8;
039    private CharBuffer collected;
040    private int maxLength = 8192;
041    private Consumer<String> consumer = text -> {
042    };
043
044    /**
045     * Sets the charset to be used if {@link #feed(ManagedBuffer)}
046     * is invoked with `ManagedBuffer<ByteBuffer>`. Defaults to UTF-8. 
047     * Must be set before the first invocation of 
048     * {@link #feed(ManagedBuffer)}.  
049     *
050     * @param charset the charset
051     * @return the managed buffer reader
052     */
053    public TextCollector charset(Charset charset) {
054        if (decoder != null) {
055            throw new IllegalStateException("Charset cannot be changed.");
056        }
057        this.charset = charset;
058        return this;
059    }
060
061    /**
062     * Sets the charset to be used if {@link #feed(ManagedBuffer)}
063     * is invoked with `ManagedBuffer<ByteBuffer>` to the charset
064     * specified as system property `native.encoding`. If this
065     * property does not specify a valid charset, 
066     * {@link Charset#defaultCharset()} is used.
067     *  
068     * Must be invoked before the first invocation of 
069     * {@link #feed(ManagedBuffer)}.  
070     *
071     * @return the managed buffer reader
072     */
073    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
074        "PMD.EmptyCatchBlock" })
075    public TextCollector nativeCharset() {
076        Charset toSet = Charset.defaultCharset();
077        var toCheck = System.getProperty("native.encoding");
078        if (toCheck != null) {
079            try {
080                toSet = Charset.forName(toCheck);
081            } catch (Exception e) {
082                // If this fails, simply use default
083            }
084        }
085        charset(toSet);
086        return this;
087    }
088
089    /**
090     * Configures the maximum length of the collected text. Input
091     * exceeding this size will be discarded.
092     *
093     * @param maximumLength the maximum length
094     * @return the maximum size
095     */
096    public TextCollector maximumSize(int maximumLength) {
097        this.maxLength = maximumLength;
098        return this;
099    }
100
101    /**
102     * Configures a consumer for the collected text. The consumer 
103     * is invoked once when the complete text is available.
104     *
105     * @param consumer the consumer
106     * @return the line collector
107     */
108    public TextCollector consumer(Consumer<String> consumer) {
109        this.consumer = consumer;
110        return this;
111    }
112
113    /**
114     * Feed data to the collector. 
115     * 
116     * Calling this method with `null` as argument closes the feed.
117     *
118     * @param buffer the buffer
119     */
120    public <W extends Buffer> void feed(W buffer) {
121        if (isEof) {
122            return;
123        }
124        if (buffer == null) {
125            isEof = true;
126            collected.flip();
127            consumer.accept(collected.toString());
128            collected = null;
129        } else {
130            copyToCollected(buffer);
131        }
132    }
133
134    /**
135     * Feed data to the collector. 
136     * 
137     * Calling this method with `null` as argument closes the feed.
138     *
139     * @param buffer the buffer
140     */
141    @Override
142    public <W extends Buffer> void feed(ManagedBuffer<W> buffer) {
143        if (buffer == null) {
144            feed((W) null);
145        } else {
146            feed(buffer.backingBuffer());
147        }
148    }
149
150    private <W extends Buffer> void copyToCollected(W buffer) {
151        try {
152            buffer.mark();
153            if (collected == null) {
154                int size = buffer.capacity();
155                if (size < maxLength && maxLength < 16_384) {
156                    size = maxLength;
157                }
158                collected = CharBuffer.allocate(size);
159            }
160            if (collected.position() >= maxLength) {
161                return;
162            }
163            if (buffer instanceof CharBuffer charBuf) {
164                if (collected.remaining() < charBuf.remaining()) {
165                    resizeCollected(charBuf);
166                }
167                collected.put(charBuf);
168                return;
169            }
170            if (decoder == null) {
171                decoder = charset.newDecoder();
172            }
173            while (true) {
174                var result
175                    = decoder.decode((ByteBuffer) buffer, collected, isEof);
176                if (!result.isOverflow()) {
177                    break;
178                }
179                // Need larger buffer
180                resizeCollected(buffer);
181            }
182        } finally {
183            buffer.reset();
184        }
185    }
186
187    private void resizeCollected(Buffer toAppend) {
188        var old = collected;
189        collected = CharBuffer.allocate(old.capacity() + toAppend.capacity());
190        old.flip();
191        collected.put(old);
192    }
193
194    /**
195     * Checks if more input may become available.
196     *
197     * @return true, if successful
198     */
199    public boolean eof() {
200        return isEof;
201    }
202}