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}