001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2023 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.http;
020
021import java.net.URLDecoder;
022import java.nio.Buffer;
023import java.nio.ByteBuffer;
024import java.nio.CharBuffer;
025import java.nio.charset.Charset;
026import java.nio.charset.CharsetDecoder;
027import java.nio.charset.StandardCharsets;
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.List;
031import java.util.Map;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.function.BiConsumer;
034import org.jgrapes.io.events.Input;
035import org.jgrapes.io.util.InputConsumer;
036import org.jgrapes.io.util.ManagedBuffer;
037
038/**
039 * Decodes www-form-urlencoded data.
040 */
041@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
042public class WwwFormUrldecoder implements InputConsumer {
043    private boolean isEof;
044    private CharsetDecoder decoder;
045    private Charset charset = StandardCharsets.UTF_8;
046    private Charset formCharset = StandardCharsets.UTF_8;
047    private CharBuffer pending;
048    private CharBuffer rest;
049    private final Map<String, List<String>> result = new ConcurrentHashMap<>();
050    private BiConsumer<String, String> consumer = (k, v) -> {
051        result.computeIfAbsent(k,
052            key -> Collections.synchronizedList(new ArrayList<>())).add(v);
053    };
054
055    /**
056     * Sets the charset to be used if {@link #feed(ManagedBuffer)}
057     * is invoked with `ManagedBuffer<ByteBuffer>`. Defaults to UTF-8. 
058     * Must be set before the first invocation of {@link #feed(ManagedBuffer)}.
059     * 
060     * This is provided for edge cases. As "urlencoded" data may only
061     * contain ASCII characters, it does not make sense to specify
062     * a charset for media type `x-www-form-urlencoded`
063     *
064     * @param charset the charset
065     * @return the managed buffer reader
066     */
067    public WwwFormUrldecoder charset(Charset charset) {
068        if (decoder != null) {
069            throw new IllegalStateException("Charset cannot be changed.");
070        }
071        this.charset = charset;
072        return this;
073    }
074
075    /**
076     * The charset to be used with {@link URLDecoder#decode(String, Charset)}.
077     * Defaults to UTF-8.
078     *
079     * @param charset the charset
080     * @return the www form urldecoder
081     */
082    public WwwFormUrldecoder formCharset(Charset charset) {
083        formCharset = charset;
084        return this;
085    }
086
087    /**
088     * Configures a consumer for key/value pairs. The consumer is invoked
089     * when a pair has been decoded. If a consumer is configured,
090     * {@link #result()} must not be used (always returns an empty map).
091     *
092     * @param consumer the consumer
093     * @return the decoder
094     */
095    public WwwFormUrldecoder consumer(BiConsumer<String, String> consumer) {
096        this.consumer = consumer;
097        return this;
098    }
099
100    /**
101     * Feed data to the collector. 
102     * 
103     * Calling this method with `null` as argument closes the feed.
104     *
105     * @param buffer the buffer
106     */
107    public <W extends Buffer> void feed(ManagedBuffer<W> buffer) {
108        if (buffer == null) {
109            isEof = true;
110        } else {
111            copyToPending(buffer.backingBuffer());
112        }
113        processPending();
114    }
115
116    /**
117     * Calls {@link #feed(ManagedBuffer)} with the provided event's
118     * buffer. If {@link Input#isEndOfRecord()} returns `true`,
119     * no further input data is processed.
120     * 
121     * Calling this method with `null` indicates the end of the feed.
122     *
123     * @param <W> the generic type
124     * @param event the event
125     */
126    @Override
127    public <W extends Buffer> void feed(Input<W> event) {
128        if (event == null) {
129            feed((ManagedBuffer<W>) null);
130        } else {
131            feed(event.buffer());
132            if (event.isEndOfRecord()) {
133                isEof = true;
134            }
135        }
136    }
137
138    private <W extends Buffer> void copyToPending(W buffer) {
139        try {
140            buffer.mark();
141            if (pending == null) {
142                pending = CharBuffer.allocate(buffer.capacity());
143            }
144            if (buffer instanceof CharBuffer charBuf) {
145                if (pending.remaining() < charBuf.remaining()) {
146                    resizePending(charBuf);
147                }
148                pending.put(charBuf);
149                return;
150            }
151            if (decoder == null) {
152                decoder = charset.newDecoder();
153            }
154            while (true) {
155                var result
156                    = decoder.decode((ByteBuffer) buffer, pending, isEof);
157                if (!result.isOverflow()) {
158                    break;
159                }
160                // Need larger buffer
161                resizePending(buffer);
162            }
163        } finally {
164            buffer.reset();
165        }
166    }
167
168    private void resizePending(Buffer toAppend) {
169        var old = pending;
170        pending = CharBuffer.allocate(old.capacity() + toAppend.capacity());
171        old.flip();
172        pending.put(old);
173    }
174
175    @SuppressWarnings({ "PMD.AvoidReassigningLoopVariables",
176        "PMD.AvoidInstantiatingObjectsInLoops",
177        "PMD.AvoidLiteralsInIfCondition",
178        "PMD.AvoidBranchingStatementAsLastInLoop", "PMD.NcssCount",
179        "PMD.NPathComplexity" })
180    private void processPending() {
181        pending.flip();
182        if (!pending.hasRemaining()) {
183            pending.clear();
184            return;
185        }
186        int end = pending.limit();
187        while (pending.hasRemaining()) {
188            int start = pending.position();
189            for (int pos = start; pos < end;) {
190                if (pending.get(pos) != '&') {
191                    pos += 1;
192                    continue;
193                }
194                splitPair(new String(pending.array(), start, pos - start));
195                pos += 1;
196                pending.position(pos);
197                break;
198            }
199            if (pending.position() == start) {
200                // No '&' found
201                break;
202            }
203        }
204        if (!pending.hasRemaining()) {
205            pending.clear();
206            return;
207        }
208        if (isEof) {
209            // Remaining is last entry
210            splitPair(new String(pending.array(), pending.position(),
211                pending.remaining()).trim());
212            return;
213        }
214        if (pending.position() == 0) {
215            // Nothing consumed, continue to write into pending
216            var limit = pending.limit();
217            pending.clear();
218            pending.position(limit);
219            return;
220        }
221        // Transfer remaining to beginning of pending
222        if (rest == null || rest.capacity() < pending.remaining()) {
223            rest = CharBuffer.allocate(pending.capacity());
224        }
225        rest.put(pending);
226        rest.flip();
227        pending.clear();
228        pending.put(rest);
229        rest.clear();
230    }
231
232    private void splitPair(String pair) {
233        int sep = pair.indexOf('=');
234        String key = URLDecoder.decode(pair.substring(0, sep), formCharset);
235        String value = URLDecoder.decode(pair.substring(sep + 1), formCharset);
236        consumer.accept(key, value);
237    }
238
239    /**
240     * Checks if more input may become available.
241     *
242     * @return true, if successful
243     */
244    public boolean eof() {
245        return isEof;
246    }
247
248    /**
249     * Gets the result.
250     *
251     * @return the line
252     */
253    public Map<String, List<String>> result() {
254        return result;
255    }
256}