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}