An omnibus PR for changes needed to support webfunctions (#10563)
Web functions are currently supported with servlets. These changes add/move utility classes to core to better support direct usage of core APIs * increase usage of Charset in request * Added flush mechanism to BufferedContentSink
This commit is contained in:
parent
d2dff9a758
commit
1a207dbeea
|
@ -28,6 +28,7 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.util.FileID;
|
||||
import org.eclipse.jetty.util.Index;
|
||||
|
@ -300,9 +301,14 @@ public class MimeTypes
|
|||
}
|
||||
}
|
||||
|
||||
private static String nameOf(Charset charset)
|
||||
{
|
||||
return charset == null ? null : charset.name();
|
||||
}
|
||||
|
||||
protected final Map<String, String> _mimeMap = new HashMap<>();
|
||||
protected final Map<String, String> _inferredEncodings = new HashMap<>();
|
||||
protected final Map<String, String> _assumedEncodings = new HashMap<>();
|
||||
protected final Map<String, Charset> _inferredEncodings = new HashMap<>();
|
||||
protected final Map<String, Charset> _assumedEncodings = new HashMap<>();
|
||||
|
||||
public MimeTypes()
|
||||
{
|
||||
|
@ -314,11 +320,37 @@ public class MimeTypes
|
|||
if (defaults != null)
|
||||
{
|
||||
_mimeMap.putAll(defaults.getMimeMap());
|
||||
_assumedEncodings.putAll(defaults.getAssumedMap());
|
||||
_inferredEncodings.putAll(defaults.getInferredMap());
|
||||
_assumedEncodings.putAll(defaults._assumedEncodings);
|
||||
_inferredEncodings.putAll(defaults._inferredEncodings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the explicit, assumed, or inferred Charset for a mime type
|
||||
* @param mimeType String form or a mimeType
|
||||
* @return A {@link Charset} or null;
|
||||
*/
|
||||
public Charset getCharset(String mimeType)
|
||||
{
|
||||
if (mimeType == null)
|
||||
return null;
|
||||
|
||||
MimeTypes.Type mime = MimeTypes.CACHE.get(mimeType);
|
||||
if (mime != null && mime.getCharset() != null)
|
||||
return mime.getCharset();
|
||||
|
||||
String charsetName = MimeTypes.getCharsetFromContentType(mimeType);
|
||||
if (charsetName != null)
|
||||
return Charset.forName(charsetName);
|
||||
|
||||
Charset charset = getAssumedCharset(mimeType);
|
||||
if (charset != null)
|
||||
return charset;
|
||||
|
||||
charset = getInferredCharset(mimeType);
|
||||
return charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME type by filename extension.
|
||||
*
|
||||
|
@ -337,16 +369,26 @@ public class MimeTypes
|
|||
return _mimeMap.get(extension);
|
||||
}
|
||||
|
||||
public String getCharsetInferredFromContentType(String contentType)
|
||||
public Charset getInferredCharset(String contentType)
|
||||
{
|
||||
return _inferredEncodings.get(contentType);
|
||||
}
|
||||
|
||||
public String getCharsetAssumedFromContentType(String contentType)
|
||||
public Charset getAssumedCharset(String contentType)
|
||||
{
|
||||
return _assumedEncodings.get(contentType);
|
||||
}
|
||||
|
||||
public String getCharsetInferredFromContentType(String contentType)
|
||||
{
|
||||
return nameOf(_inferredEncodings.get(contentType));
|
||||
}
|
||||
|
||||
public String getCharsetAssumedFromContentType(String contentType)
|
||||
{
|
||||
return nameOf(_assumedEncodings.get(contentType));
|
||||
}
|
||||
|
||||
public Map<String, String> getMimeMap()
|
||||
{
|
||||
return Collections.unmodifiableMap(_mimeMap);
|
||||
|
@ -354,12 +396,12 @@ public class MimeTypes
|
|||
|
||||
public Map<String, String> getInferredMap()
|
||||
{
|
||||
return Collections.unmodifiableMap(_inferredEncodings);
|
||||
return _inferredEncodings.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().name()));
|
||||
}
|
||||
|
||||
public Map<String, String> getAssumedMap()
|
||||
{
|
||||
return Collections.unmodifiableMap(_assumedEncodings);
|
||||
return _assumedEncodings.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().name()));
|
||||
}
|
||||
|
||||
public static class Mutable extends MimeTypes
|
||||
|
@ -390,12 +432,12 @@ public class MimeTypes
|
|||
|
||||
public String addInferred(String contentType, String encoding)
|
||||
{
|
||||
return _inferredEncodings.put(contentType, encoding);
|
||||
return nameOf(_inferredEncodings.put(contentType, Charset.forName(encoding)));
|
||||
}
|
||||
|
||||
public String addAssumed(String contentType, String encoding)
|
||||
{
|
||||
return _assumedEncodings.put(contentType, encoding);
|
||||
return nameOf(_assumedEncodings.put(contentType, Charset.forName(encoding)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -479,7 +521,7 @@ public class MimeTypes
|
|||
for (Type type : Type.values())
|
||||
{
|
||||
if (type.isCharsetAssumed())
|
||||
_assumedEncodings.put(type.asString(), type.getCharsetString());
|
||||
_assumedEncodings.put(type.asString(), type.getCharset());
|
||||
}
|
||||
|
||||
String resourceName = "mime.properties";
|
||||
|
@ -548,9 +590,9 @@ public class MimeTypes
|
|||
{
|
||||
String charset = props.getProperty(t);
|
||||
if (charset.startsWith("-"))
|
||||
_assumedEncodings.put(t, charset.substring(1));
|
||||
_assumedEncodings.put(t, Charset.forName(charset.substring(1)));
|
||||
else
|
||||
_inferredEncodings.put(t, props.getProperty(t));
|
||||
_inferredEncodings.put(t, Charset.forName(props.getProperty(t)));
|
||||
});
|
||||
|
||||
if (_inferredEncodings.isEmpty())
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -21,6 +22,7 @@ import org.junit.jupiter.params.provider.Arguments;
|
|||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalToIgnoringCase;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
@ -159,15 +161,15 @@ public class MimeTypesTest
|
|||
assertThat(wrapper.getAssumedMap().size(), is(0));
|
||||
|
||||
wrapper.addMimeMapping("txt", "text/plain");
|
||||
wrapper.addInferred("text/plain", "usascii");
|
||||
wrapper.addInferred("text/plain", "us-ascii");
|
||||
wrapper.addAssumed("json", "utf-8");
|
||||
|
||||
assertThat(wrapper.getMimeMap().size(), is(1));
|
||||
assertThat(wrapper.getInferredMap().size(), is(1));
|
||||
assertThat(wrapper.getAssumedMap().size(), is(1));
|
||||
assertThat(wrapper.getMimeByExtension("fee.txt"), is("text/plain"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), is("usascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), is("utf-8"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), equalToIgnoringCase("us-ascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), equalToIgnoringCase("utf-8"));
|
||||
|
||||
MimeTypes.Mutable wrapped = new MimeTypes.Mutable(null);
|
||||
wrapper.setWrapped(wrapped);
|
||||
|
@ -176,23 +178,23 @@ public class MimeTypesTest
|
|||
assertThat(wrapper.getInferredMap().size(), is(1));
|
||||
assertThat(wrapper.getAssumedMap().size(), is(1));
|
||||
assertThat(wrapper.getMimeByExtension("fee.txt"), is("text/plain"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), is("usascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), is("utf-8"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), equalToIgnoringCase("us-ascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), equalToIgnoringCase("utf-8"));
|
||||
|
||||
wrapped.addMimeMapping("txt", "overridden");
|
||||
wrapped.addInferred("text/plain", "overridden");
|
||||
wrapped.addAssumed("json", "overridden");
|
||||
wrapped.addMimeMapping("txt", StandardCharsets.UTF_16.name());
|
||||
wrapped.addInferred("text/plain", StandardCharsets.UTF_16.name());
|
||||
wrapped.addAssumed("json", StandardCharsets.UTF_16.name());
|
||||
|
||||
assertThat(wrapper.getMimeMap().size(), is(1));
|
||||
assertThat(wrapper.getInferredMap().size(), is(1));
|
||||
assertThat(wrapper.getAssumedMap().size(), is(1));
|
||||
assertThat(wrapper.getMimeByExtension("fee.txt"), is("text/plain"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), is("usascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), is("utf-8"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), equalToIgnoringCase("us-ascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), equalToIgnoringCase("utf-8"));
|
||||
|
||||
wrapped.addMimeMapping("xml", "text/xml");
|
||||
wrapped.addInferred("text/xml", "iso-8859-1");
|
||||
wrapped.addAssumed("text/xxx", "assumed");
|
||||
wrapped.addAssumed("text/xxx", StandardCharsets.UTF_16.name());
|
||||
assertThat(wrapped.getMimeMap().size(), is(2));
|
||||
assertThat(wrapped.getInferredMap().size(), is(2));
|
||||
assertThat(wrapped.getAssumedMap().size(), is(2));
|
||||
|
@ -201,10 +203,10 @@ public class MimeTypesTest
|
|||
assertThat(wrapper.getInferredMap().size(), is(2));
|
||||
assertThat(wrapper.getAssumedMap().size(), is(2));
|
||||
assertThat(wrapper.getMimeByExtension("fee.txt"), is("text/plain"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), is("usascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), is("utf-8"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/plain"), equalToIgnoringCase("us-ascii"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("json"), equalToIgnoringCase("utf-8"));
|
||||
assertThat(wrapper.getMimeByExtension("fee.xml"), is("text/xml"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/xml"), is("iso-8859-1"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("text/xxx"), is("assumed"));
|
||||
assertThat(wrapper.getCharsetInferredFromContentType("text/xml"), equalToIgnoringCase("iso-8859-1"));
|
||||
assertThat(wrapper.getCharsetAssumedFromContentType("text/xxx"), equalToIgnoringCase("utf-16"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,15 @@ public class ByteBufferAggregator
|
|||
_currentSize = startSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently aggregated length.
|
||||
* @return The current total aggregated bytes.
|
||||
*/
|
||||
public int length()
|
||||
{
|
||||
return _aggregatedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates the given ByteBuffer. This copies bytes up to the specified maximum size, at which
|
||||
* time this method returns {@code true} and {@link #takeRetainableByteBuffer()} must be called
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jetty.io.Content.Chunk;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.CompletableTask;
|
||||
|
||||
/**
|
||||
* An accumulator of {@link Content.Chunk}s used to facilitate minimal copy
|
||||
* aggregation of multiple chunks.
|
||||
*/
|
||||
public class ChunkAccumulator
|
||||
{
|
||||
private static final ByteBufferPool NON_POOLING = new ByteBufferPool.NonPooling();
|
||||
private final List<Chunk> _chunks = new ArrayList<>();
|
||||
private int _length;
|
||||
|
||||
/**
|
||||
* Add a {@link Chunk} to the accumulator.
|
||||
* @param chunk The {@link Chunk} to accumulate. If a reference is kept to the chunk (rather than a copy), it will be retained.
|
||||
* @return true if the {@link Chunk} had content and was added to the accumulator.
|
||||
* @throws ArithmeticException if more that {@link Integer#MAX_VALUE} bytes are added.
|
||||
* @throws IllegalArgumentException if the passed {@link Chunk} is a {@link Chunk#isFailure(Chunk) failure}.
|
||||
*/
|
||||
public boolean add(Chunk chunk)
|
||||
{
|
||||
if (chunk.hasRemaining())
|
||||
{
|
||||
_length = Math.addExact(_length, chunk.remaining());
|
||||
if (chunk.canRetain())
|
||||
{
|
||||
chunk.retain();
|
||||
return _chunks.add(chunk);
|
||||
}
|
||||
return _chunks.add(Chunk.from(BufferUtil.copy(chunk.getByteBuffer()), chunk.isLast(), () -> {}));
|
||||
}
|
||||
else if (Chunk.isFailure(chunk))
|
||||
{
|
||||
throw new IllegalArgumentException("chunk is failure");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total length of the accumulated {@link Chunk}s.
|
||||
* @return The total length in bytes.
|
||||
*/
|
||||
public int length()
|
||||
{
|
||||
return _length;
|
||||
}
|
||||
|
||||
public byte[] take()
|
||||
{
|
||||
if (_length == 0)
|
||||
return BufferUtil.EMPTY_BUFFER.array();
|
||||
byte[] bytes = new byte[_length];
|
||||
int offset = 0;
|
||||
for (Chunk chunk : _chunks)
|
||||
{
|
||||
offset += chunk.get(bytes, offset, chunk.remaining());
|
||||
chunk.release();
|
||||
}
|
||||
assert offset == _length;
|
||||
_chunks.clear();
|
||||
_length = 0;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public RetainableByteBuffer take(ByteBufferPool pool, boolean direct)
|
||||
{
|
||||
if (_length == 0)
|
||||
return RetainableByteBuffer.EMPTY;
|
||||
|
||||
if (_chunks.size() == 1)
|
||||
{
|
||||
Chunk chunk = _chunks.get(0);
|
||||
ByteBuffer byteBuffer = chunk.getByteBuffer();
|
||||
|
||||
if (direct == byteBuffer.isDirect())
|
||||
{
|
||||
_chunks.clear();
|
||||
_length = 0;
|
||||
return RetainableByteBuffer.wrap(byteBuffer, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
RetainableByteBuffer buffer = Objects.requireNonNullElse(pool, NON_POOLING).acquire(_length, direct);
|
||||
int offset = 0;
|
||||
for (Chunk chunk : _chunks)
|
||||
{
|
||||
offset += chunk.remaining();
|
||||
BufferUtil.append(buffer.getByteBuffer(), chunk.getByteBuffer());
|
||||
chunk.release();
|
||||
}
|
||||
assert offset == _length;
|
||||
_chunks.clear();
|
||||
_length = 0;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void close()
|
||||
{
|
||||
_chunks.forEach(Chunk::release);
|
||||
_chunks.clear();
|
||||
_length = 0;
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> readAll(Content.Source source)
|
||||
{
|
||||
return readAll(source, -1);
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> readAll(Content.Source source, int maxSize)
|
||||
{
|
||||
CompletableTask<byte[]> task = new AccumulatorTask<>(source, maxSize)
|
||||
{
|
||||
@Override
|
||||
protected byte[] take(ChunkAccumulator accumulator)
|
||||
{
|
||||
return accumulator.take();
|
||||
}
|
||||
};
|
||||
return task.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source The {@link Content.Source} to read
|
||||
* @param pool The {@link ByteBufferPool} to acquire the buffer from, or null for a non {@link Retainable} buffer
|
||||
* @param direct True if the buffer should be direct.
|
||||
* @param maxSize The maximum size to read, or -1 for no limit
|
||||
* @return A {@link CompletableFuture} that will be completed when the complete content is read or
|
||||
* failed if the max size is exceeded or there is a read error.
|
||||
*/
|
||||
public CompletableFuture<RetainableByteBuffer> readAll(Content.Source source, ByteBufferPool pool, boolean direct, int maxSize)
|
||||
{
|
||||
CompletableTask<RetainableByteBuffer> task = new AccumulatorTask<>(source, maxSize)
|
||||
{
|
||||
@Override
|
||||
protected RetainableByteBuffer take(ChunkAccumulator accumulator)
|
||||
{
|
||||
return accumulator.take(pool, direct);
|
||||
}
|
||||
};
|
||||
return task.start();
|
||||
}
|
||||
|
||||
private abstract static class AccumulatorTask<T> extends CompletableTask<T>
|
||||
{
|
||||
private final Content.Source _source;
|
||||
private final ChunkAccumulator _accumulator = new ChunkAccumulator();
|
||||
private final int _maxLength;
|
||||
|
||||
private AccumulatorTask(Content.Source source, int maxLength)
|
||||
{
|
||||
_source = source;
|
||||
_maxLength = maxLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Chunk chunk = _source.read();
|
||||
if (chunk == null)
|
||||
{
|
||||
_source.demand(this);
|
||||
break;
|
||||
}
|
||||
|
||||
if (Chunk.isFailure(chunk))
|
||||
{
|
||||
completeExceptionally(chunk.getFailure());
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_accumulator.add(chunk);
|
||||
|
||||
if (_maxLength > 0 && _accumulator._length > _maxLength)
|
||||
throw new IOException("accumulation too large");
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
chunk.release();
|
||||
_accumulator.close();
|
||||
_source.fail(t);
|
||||
completeExceptionally(t);
|
||||
break;
|
||||
}
|
||||
|
||||
chunk.release();
|
||||
|
||||
if (chunk.isLast())
|
||||
{
|
||||
complete(take(_accumulator));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract T take(ChunkAccumulator accumulator);
|
||||
}
|
||||
}
|
|
@ -162,6 +162,19 @@ public class Content
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Reads, non-blocking, the whole content source into a {@code byte} array.</p>
|
||||
*
|
||||
* @param source the source to read
|
||||
* @param maxSize The maximum size to read, or -1 for no limit
|
||||
* @return A {@link CompletableFuture} that will be completed when the complete content is read or
|
||||
* failed if the max size is exceeded or there is a read error.
|
||||
*/
|
||||
static CompletableFuture<byte[]> asByteArrayAsync(Source source, int maxSize)
|
||||
{
|
||||
return new ChunkAccumulator().readAll(source, maxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Reads, non-blocking, the whole content source into a {@link ByteBuffer}.</p>
|
||||
*
|
||||
|
@ -170,9 +183,34 @@ public class Content
|
|||
*/
|
||||
static CompletableFuture<ByteBuffer> asByteBufferAsync(Source source)
|
||||
{
|
||||
Promise.Completable<ByteBuffer> completable = new Promise.Completable<>();
|
||||
asByteBuffer(source, completable);
|
||||
return completable;
|
||||
return asByteBufferAsync(source, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Reads, non-blocking, the whole content source into a {@link ByteBuffer}.</p>
|
||||
*
|
||||
* @param source the source to read
|
||||
* @param maxSize The maximum size to read, or -1 for no limit
|
||||
* @return the {@link CompletableFuture} to notify when the whole content has been read
|
||||
*/
|
||||
static CompletableFuture<ByteBuffer> asByteBufferAsync(Source source, int maxSize)
|
||||
{
|
||||
return asByteArrayAsync(source, maxSize).thenApply(ByteBuffer::wrap);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Reads, non-blocking, the whole content source into a {@link RetainableByteBuffer}.</p>
|
||||
*
|
||||
* @param source The {@link Content.Source} to read
|
||||
* @param pool The {@link ByteBufferPool} to acquire the buffer from, or null for a non {@link Retainable} buffer
|
||||
* @param direct True if the buffer should be direct.
|
||||
* @param maxSize The maximum size to read, or -1 for no limit
|
||||
* @return A {@link CompletableFuture} that will be completed when the complete content is read or
|
||||
* failed if the max size is exceeded or there is a read error.
|
||||
*/
|
||||
static CompletableFuture<RetainableByteBuffer> asRetainableByteBuffer(Source source, ByteBufferPool pool, boolean direct, int maxSize)
|
||||
{
|
||||
return new ChunkAccumulator().readAll(source, pool, direct, maxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -53,4 +53,26 @@ public interface QuietException
|
|||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeException extends java.lang.RuntimeException implements QuietException
|
||||
{
|
||||
public RuntimeException()
|
||||
{
|
||||
}
|
||||
|
||||
public RuntimeException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RuntimeException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public RuntimeException(Throwable cause)
|
||||
{
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public interface RetainableByteBuffer extends Retainable
|
|||
/**
|
||||
* A Zero-capacity, non-retainable {@code RetainableByteBuffer}.
|
||||
*/
|
||||
public static RetainableByteBuffer EMPTY = wrap(BufferUtil.EMPTY_BUFFER);
|
||||
RetainableByteBuffer EMPTY = wrap(BufferUtil.EMPTY_BUFFER);
|
||||
|
||||
/**
|
||||
* <p>Returns a non-retainable {@code RetainableByteBuffer} that wraps
|
||||
|
@ -57,27 +57,72 @@ public interface RetainableByteBuffer extends Retainable
|
|||
* @return a non-retainable {@code RetainableByteBuffer}
|
||||
* @see ByteBufferPool.NonPooling
|
||||
*/
|
||||
public static RetainableByteBuffer wrap(ByteBuffer byteBuffer)
|
||||
static RetainableByteBuffer wrap(ByteBuffer byteBuffer)
|
||||
{
|
||||
return new NonRetainableByteBuffer(byteBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns a {@code RetainableByteBuffer} that wraps
|
||||
* the given {@code ByteBuffer} and {@link Retainable}.</p>
|
||||
*
|
||||
* @param byteBuffer the {@code ByteBuffer} to wrap
|
||||
* @param retainable the associated {@link Retainable}.
|
||||
* @return a {@code RetainableByteBuffer}
|
||||
* @see ByteBufferPool.NonPooling
|
||||
*/
|
||||
static RetainableByteBuffer wrap(ByteBuffer byteBuffer, Retainable retainable)
|
||||
{
|
||||
return new RetainableByteBuffer()
|
||||
{
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer()
|
||||
{
|
||||
return byteBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRetained()
|
||||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRetain()
|
||||
{
|
||||
return retainable.canRetain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void retain()
|
||||
{
|
||||
retainable.retain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release()
|
||||
{
|
||||
return retainable.release();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this instance is retained
|
||||
* @see ReferenceCounter#isRetained()
|
||||
*/
|
||||
public boolean isRetained();
|
||||
boolean isRetained();
|
||||
|
||||
/**
|
||||
* Get the wrapped, not {@code null}, {@code ByteBuffer}.
|
||||
* @return the wrapped, not {@code null}, {@code ByteBuffer}
|
||||
*/
|
||||
public ByteBuffer getByteBuffer();
|
||||
ByteBuffer getByteBuffer();
|
||||
|
||||
/**
|
||||
* @return whether the {@code ByteBuffer} is direct
|
||||
*/
|
||||
public default boolean isDirect()
|
||||
default boolean isDirect()
|
||||
{
|
||||
return getByteBuffer().isDirect();
|
||||
}
|
||||
|
@ -85,7 +130,7 @@ public interface RetainableByteBuffer extends Retainable
|
|||
/**
|
||||
* @return the number of remaining bytes in the {@code ByteBuffer}
|
||||
*/
|
||||
public default int remaining()
|
||||
default int remaining()
|
||||
{
|
||||
return getByteBuffer().remaining();
|
||||
}
|
||||
|
@ -93,7 +138,7 @@ public interface RetainableByteBuffer extends Retainable
|
|||
/**
|
||||
* @return whether the {@code ByteBuffer} has remaining bytes
|
||||
*/
|
||||
public default boolean hasRemaining()
|
||||
default boolean hasRemaining()
|
||||
{
|
||||
return getByteBuffer().hasRemaining();
|
||||
}
|
||||
|
@ -101,7 +146,7 @@ public interface RetainableByteBuffer extends Retainable
|
|||
/**
|
||||
* @return the {@code ByteBuffer} capacity
|
||||
*/
|
||||
public default int capacity()
|
||||
default int capacity()
|
||||
{
|
||||
return getByteBuffer().capacity();
|
||||
}
|
||||
|
@ -109,7 +154,7 @@ public interface RetainableByteBuffer extends Retainable
|
|||
/**
|
||||
* @see BufferUtil#clear(ByteBuffer)
|
||||
*/
|
||||
public default void clear()
|
||||
default void clear()
|
||||
{
|
||||
BufferUtil.clear(getByteBuffer());
|
||||
}
|
||||
|
@ -117,7 +162,7 @@ public interface RetainableByteBuffer extends Retainable
|
|||
/**
|
||||
* A wrapper for {@link RetainableByteBuffer} instances
|
||||
*/
|
||||
public class Wrapper extends Retainable.Wrapper implements RetainableByteBuffer
|
||||
class Wrapper extends Retainable.Wrapper implements RetainableByteBuffer
|
||||
{
|
||||
public Wrapper(RetainableByteBuffer wrapped)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,444 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jetty.util.ByteArrayOutputStream2;
|
||||
|
||||
/**
|
||||
* <p>An alternate to {@link java.io.OutputStreamWriter} that supports
|
||||
* several optimized implementation for well known {@link Charset}s,
|
||||
* specifically {@link StandardCharsets#UTF_8} and {@link StandardCharsets#ISO_8859_1}.</p>
|
||||
* <p>The implementations of this class will never buffer characters or bytes beyond a call to the
|
||||
* {@link #write(char[], int, int)} method, thus written characters will always be passed
|
||||
* as bytes to the passed {@link OutputStream}</p>.
|
||||
*/
|
||||
public abstract class WriteThroughWriter extends Writer
|
||||
{
|
||||
static final int DEFAULT_MAX_WRITE_SIZE = 1024;
|
||||
private final int _maxWriteSize;
|
||||
final OutputStream _out;
|
||||
final ByteArrayOutputStream2 _bytes;
|
||||
|
||||
protected WriteThroughWriter(OutputStream out)
|
||||
{
|
||||
this(out, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an {@link java.io.OutputStreamWriter}
|
||||
* @param out The {@link OutputStream} to write the converted bytes to.
|
||||
* @param maxWriteSize The maximum size in characters of a single conversion
|
||||
*/
|
||||
protected WriteThroughWriter(OutputStream out, int maxWriteSize)
|
||||
{
|
||||
_maxWriteSize = maxWriteSize <= 0 ? DEFAULT_MAX_WRITE_SIZE : maxWriteSize;
|
||||
_out = out;
|
||||
_bytes = new ByteArrayOutputStream2(_maxWriteSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a new {@link Writer} that converts characters written to bytes
|
||||
* written to an {@link OutputStream}.
|
||||
* @param outputStream The {@link OutputStream} to write to/
|
||||
* @param charset The {@link Charset} name.
|
||||
* @return A Writer that will
|
||||
* @throws IOException If there is a problem creating the {@link Writer}.
|
||||
*/
|
||||
public static WriteThroughWriter newWriter(OutputStream outputStream, String charset)
|
||||
throws IOException
|
||||
{
|
||||
if (StandardCharsets.ISO_8859_1.name().equalsIgnoreCase(charset))
|
||||
return new Iso88591Writer(outputStream);
|
||||
if (StandardCharsets.UTF_8.name().equalsIgnoreCase(charset))
|
||||
return new Utf8Writer(outputStream);
|
||||
return new EncodingWriter(outputStream, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a new {@link Writer} that converts characters written to bytes
|
||||
* written to an {@link OutputStream}.
|
||||
* @param outputStream The {@link OutputStream} to write to/
|
||||
* @param charset The {@link Charset}.
|
||||
* @return A Writer that will
|
||||
* @throws IOException If there is a problem creating the {@link Writer}.
|
||||
*/
|
||||
public static WriteThroughWriter newWriter(OutputStream outputStream, Charset charset)
|
||||
throws IOException
|
||||
{
|
||||
if (StandardCharsets.ISO_8859_1 == charset)
|
||||
return new Iso88591Writer(outputStream);
|
||||
if (StandardCharsets.UTF_8.equals(charset))
|
||||
return new Utf8Writer(outputStream);
|
||||
return new EncodingWriter(outputStream, charset);
|
||||
}
|
||||
|
||||
public int getMaxWriteSize()
|
||||
{
|
||||
return _maxWriteSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
_out.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException
|
||||
{
|
||||
_out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract WriteThroughWriter append(CharSequence sequence) throws IOException;
|
||||
|
||||
@Override
|
||||
public void write(String string, int offset, int length) throws IOException
|
||||
{
|
||||
while (length > _maxWriteSize)
|
||||
{
|
||||
append(subSequence(string, offset, _maxWriteSize));
|
||||
offset += _maxWriteSize;
|
||||
length -= _maxWriteSize;
|
||||
}
|
||||
|
||||
append(subSequence(string, offset, length));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] chars, int offset, int length) throws IOException
|
||||
{
|
||||
while (length > _maxWriteSize)
|
||||
{
|
||||
append(subSequence(chars, offset, _maxWriteSize));
|
||||
offset += _maxWriteSize;
|
||||
length -= _maxWriteSize;
|
||||
}
|
||||
|
||||
append(subSequence(chars, offset, length));
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link WriteThroughWriter} for
|
||||
* optimal ISO-8859-1 conversion.
|
||||
* The ISO-8859-1 encoding is done by this class and no additional
|
||||
* buffers or Writers are used.
|
||||
*/
|
||||
private static class Iso88591Writer extends WriteThroughWriter
|
||||
{
|
||||
private Iso88591Writer(OutputStream out)
|
||||
{
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteThroughWriter append(CharSequence charSequence) throws IOException
|
||||
{
|
||||
assert charSequence.length() <= getMaxWriteSize();
|
||||
|
||||
if (charSequence.length() == 1)
|
||||
{
|
||||
int c = charSequence.charAt(0);
|
||||
_out.write(c < 256 ? c : '?');
|
||||
return this;
|
||||
}
|
||||
|
||||
_bytes.reset();
|
||||
int bytes = 0;
|
||||
byte[] buffer = _bytes.getBuf();
|
||||
int length = charSequence.length();
|
||||
for (int offset = 0; offset < length; offset++)
|
||||
{
|
||||
int c = charSequence.charAt(offset);
|
||||
buffer[bytes++] = (byte)(c < 256 ? c : '?');
|
||||
}
|
||||
if (bytes >= 0)
|
||||
_bytes.setCount(bytes);
|
||||
_bytes.writeTo(_out);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link WriteThroughWriter} for
|
||||
* an optimal UTF-8 conversion.
|
||||
* The UTF-8 encoding is done by this class and no additional
|
||||
* buffers or Writers are used.
|
||||
* The UTF-8 code was inspired by <a href="http://javolution.org">...</a>
|
||||
*/
|
||||
private static class Utf8Writer extends WriteThroughWriter
|
||||
{
|
||||
int _surrogate = 0;
|
||||
|
||||
private Utf8Writer(OutputStream out)
|
||||
{
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteThroughWriter append(CharSequence charSequence) throws IOException
|
||||
{
|
||||
assert charSequence.length() <= getMaxWriteSize();
|
||||
int length = charSequence.length();
|
||||
int offset = 0;
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, getMaxWriteSize());
|
||||
|
||||
byte[] buffer = _bytes.getBuf();
|
||||
int bytes = _bytes.getCount();
|
||||
|
||||
if (bytes + chars > buffer.length)
|
||||
chars = buffer.length - bytes;
|
||||
|
||||
for (int i = 0; i < chars; i++)
|
||||
{
|
||||
int code = charSequence.charAt(offset + i);
|
||||
|
||||
// Do we already have a surrogate?
|
||||
if (_surrogate == 0)
|
||||
{
|
||||
// No - is this char code a surrogate?
|
||||
if (Character.isHighSurrogate((char)code))
|
||||
{
|
||||
_surrogate = code; // UCS-?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// else handle a low surrogate
|
||||
else if (Character.isLowSurrogate((char)code))
|
||||
{
|
||||
code = Character.toCodePoint((char)_surrogate, (char)code); // UCS-4
|
||||
}
|
||||
// else UCS-2
|
||||
else
|
||||
{
|
||||
code = _surrogate; // UCS-2
|
||||
_surrogate = 0; // USED
|
||||
i--;
|
||||
}
|
||||
|
||||
if ((code & 0xffffff80) == 0)
|
||||
{
|
||||
// 1b
|
||||
if (bytes >= buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(code);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((code & 0xfffff800) == 0)
|
||||
{
|
||||
// 2b
|
||||
if (bytes + 2 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xc0 | (code >> 6));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xffff0000) == 0)
|
||||
{
|
||||
// 3b
|
||||
if (bytes + 3 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xe0 | (code >> 12));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xff200000) == 0)
|
||||
{
|
||||
// 4b
|
||||
if (bytes + 4 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xf0 | (code >> 18));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xf4000000) == 0)
|
||||
{
|
||||
// 5b
|
||||
if (bytes + 5 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xf8 | (code >> 24));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0x80000000) == 0)
|
||||
{
|
||||
// 6b
|
||||
if (bytes + 6 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xfc | (code >> 30));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 24) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[bytes++] = (byte)('?');
|
||||
}
|
||||
|
||||
_surrogate = 0; // USED
|
||||
|
||||
if (bytes == buffer.length)
|
||||
{
|
||||
chars = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_bytes.setCount(bytes);
|
||||
|
||||
_bytes.writeTo(_out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link WriteThroughWriter} that internally
|
||||
* uses {@link java.io.OutputStreamWriter}.
|
||||
*/
|
||||
private static class EncodingWriter extends WriteThroughWriter
|
||||
{
|
||||
final Writer _converter;
|
||||
|
||||
public EncodingWriter(OutputStream out, String encoding) throws IOException
|
||||
{
|
||||
super(out);
|
||||
_converter = new OutputStreamWriter(_bytes, encoding);
|
||||
}
|
||||
|
||||
public EncodingWriter(OutputStream out, Charset charset) throws IOException
|
||||
{
|
||||
super(out);
|
||||
_converter = new OutputStreamWriter(_bytes, charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteThroughWriter append(CharSequence charSequence) throws IOException
|
||||
{
|
||||
assert charSequence.length() <= getMaxWriteSize();
|
||||
|
||||
_bytes.reset();
|
||||
_converter.append(charSequence);
|
||||
_converter.flush();
|
||||
_bytes.writeTo(_out);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Get a zero copy subsequence of a {@link String}.</p>
|
||||
* <p>Use of this is method can result in unforeseen GC consequences and can bypass
|
||||
* JVM optimizations available in {@link String#subSequence(int, int)}. It should only
|
||||
* be used in cases where there is a known benefit: large sub sequence of a larger string with no retained
|
||||
* references to the sub sequence beyond the life time of the string.</p>
|
||||
* @param string The {@link String} to take a subsequence of.
|
||||
* @param offset The offset in characters into the string to start the subsequence
|
||||
* @param length The length in characters of the substring
|
||||
* @return A new {@link CharSequence} containing the subsequence, backed by the passed {@link String}
|
||||
* or the original {@link String} if it is the same.
|
||||
*/
|
||||
static CharSequence subSequence(String string, int offset, int length)
|
||||
{
|
||||
Objects.requireNonNull(string);
|
||||
|
||||
if (offset == 0 && string.length() == length)
|
||||
return string;
|
||||
if (length == 0)
|
||||
return "";
|
||||
|
||||
int end = offset + length;
|
||||
if (offset < 0 || offset > end || end > string.length())
|
||||
throw new IndexOutOfBoundsException("offset and/or length out of range");
|
||||
|
||||
return new CharSequence()
|
||||
{
|
||||
@Override
|
||||
public int length()
|
||||
{
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index)
|
||||
{
|
||||
return string.charAt(offset + index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end)
|
||||
{
|
||||
return WriteThroughWriter.subSequence(string, offset + start, end - start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return string.substring(offset, offset + length);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a zero copy subsequence of a {@code char} array.
|
||||
* @param chars The characters to take a subsequence of. These character are not copied and the array should not be
|
||||
* modified for the life of the returned CharSequence.
|
||||
* @param offset The offset in characters into the string to start the subsequence
|
||||
* @param length The length in characters of the substring
|
||||
* @return A new {@link CharSequence} containing the subsequence.
|
||||
*/
|
||||
static CharSequence subSequence(char[] chars, int offset, int length)
|
||||
{
|
||||
// Needed to make bounds check of wrap the same as for string.substring
|
||||
if (length == 0)
|
||||
return "";
|
||||
return CharBuffer.wrap(chars, offset, length);
|
||||
}
|
||||
}
|
|
@ -35,6 +35,12 @@ import org.slf4j.LoggerFactory;
|
|||
*/
|
||||
public class BufferedContentSink implements Content.Sink
|
||||
{
|
||||
/**
|
||||
* An empty {@link ByteBuffer}, which if {@link #write(boolean, ByteBuffer, Callback) written}
|
||||
* will invoke a {@link #flush(Callback)} operation.
|
||||
*/
|
||||
public static final ByteBuffer FLUSH_BUFFER = ByteBuffer.wrap(new byte[0]);
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BufferedContentSink.class);
|
||||
|
||||
private static final int START_BUFFER_SIZE = 1024;
|
||||
|
@ -103,6 +109,15 @@ public class BufferedContentSink implements Content.Sink
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the buffered content.
|
||||
* @param callback Callback completed when the flush is complete
|
||||
*/
|
||||
public void flush(Callback callback)
|
||||
{
|
||||
flush(false, FLUSH_BUFFER, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the aggregated buffer if something was aggregated, then flushes the
|
||||
* given buffer, bypassing the aggregator.
|
||||
|
@ -119,7 +134,7 @@ public class BufferedContentSink implements Content.Sink
|
|||
LOG.debug("nothing aggregated, flushing current buffer {}", currentBuffer);
|
||||
_flusher.offer(last, currentBuffer, callback);
|
||||
}
|
||||
else
|
||||
else if (BufferUtil.hasContent(currentBuffer))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("flushing aggregated buffer {}", aggregatedBuffer);
|
||||
|
@ -144,6 +159,10 @@ public class BufferedContentSink implements Content.Sink
|
|||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_flusher.offer(false, aggregatedBuffer.getByteBuffer(), Callback.from(aggregatedBuffer::release, callback));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,7 +171,9 @@ public class BufferedContentSink implements Content.Sink
|
|||
private void aggregateAndFlush(boolean last, ByteBuffer currentBuffer, Callback callback)
|
||||
{
|
||||
boolean full = _aggregator.aggregate(currentBuffer);
|
||||
boolean complete = last && !currentBuffer.hasRemaining();
|
||||
boolean empty = !currentBuffer.hasRemaining();
|
||||
boolean flush = full || currentBuffer == FLUSH_BUFFER;
|
||||
boolean complete = last && empty;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("aggregated current buffer, full={}, complete={}, bytes left={}, aggregator={}", full, complete, currentBuffer.remaining(), _aggregator);
|
||||
if (complete)
|
||||
|
@ -171,34 +192,42 @@ public class BufferedContentSink implements Content.Sink
|
|||
_flusher.offer(true, BufferUtil.EMPTY_BUFFER, callback);
|
||||
}
|
||||
}
|
||||
else if (full)
|
||||
else if (flush)
|
||||
{
|
||||
RetainableByteBuffer aggregatedBuffer = _aggregator.takeRetainableByteBuffer();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("writing aggregated buffer: {} bytes", aggregatedBuffer.remaining());
|
||||
_flusher.offer(false, aggregatedBuffer.getByteBuffer(), new Callback.Nested(Callback.from(aggregatedBuffer::release))
|
||||
{
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
super.succeeded();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("written aggregated buffer, writing remaining of current: {} bytes{}", currentBuffer.remaining(), (last ? " (last write)" : ""));
|
||||
if (last)
|
||||
_flusher.offer(true, currentBuffer, callback);
|
||||
else
|
||||
aggregateAndFlush(false, currentBuffer, callback);
|
||||
}
|
||||
LOG.debug("writing aggregated buffer: {} bytes, then {}", aggregatedBuffer.remaining(), currentBuffer.remaining());
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
if (BufferUtil.hasContent(currentBuffer))
|
||||
{
|
||||
_flusher.offer(false, aggregatedBuffer.getByteBuffer(), new Callback.Nested(Callback.from(aggregatedBuffer::release))
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failure writing aggregated buffer", x);
|
||||
super.failed(x);
|
||||
callback.failed(x);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
super.succeeded();
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("written aggregated buffer, writing remaining of current: {} bytes{}", currentBuffer.remaining(), (last ? " (last write)" : ""));
|
||||
if (last)
|
||||
_flusher.offer(true, currentBuffer, callback);
|
||||
else
|
||||
aggregateAndFlush(false, currentBuffer, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("failure writing aggregated buffer", x);
|
||||
super.failed(x);
|
||||
callback.failed(x);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_flusher.offer(false, aggregatedBuffer.getByteBuffer(), Callback.from(aggregatedBuffer::release, callback));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -64,7 +64,7 @@ public class ContentSinkOutputStream extends OutputStream
|
|||
{
|
||||
try (Blocker.Callback callback = _blocking.callback())
|
||||
{
|
||||
sink.write(false, null, callback);
|
||||
sink.write(false, BufferedContentSink.FLUSH_BUFFER, callback);
|
||||
callback.block();
|
||||
}
|
||||
catch (Throwable x)
|
||||
|
@ -78,7 +78,7 @@ public class ContentSinkOutputStream extends OutputStream
|
|||
{
|
||||
try (Blocker.Callback callback = _blocking.callback())
|
||||
{
|
||||
sink.write(true, null, callback);
|
||||
close(callback);
|
||||
callback.block();
|
||||
}
|
||||
catch (Throwable x)
|
||||
|
@ -86,4 +86,9 @@ public class ContentSinkOutputStream extends OutputStream
|
|||
throw IO.rethrow(x);
|
||||
}
|
||||
}
|
||||
|
||||
public void close(Callback callback) throws IOException
|
||||
{
|
||||
sink.write(true, null, callback);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import java.util.concurrent.CountDownLatch;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.io.content.AsyncContent;
|
||||
import org.eclipse.jetty.io.content.BufferedContentSink;
|
||||
|
@ -29,6 +31,8 @@ import org.eclipse.jetty.util.Callback;
|
|||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
|
@ -39,6 +43,7 @@ import static org.hamcrest.Matchers.notNullValue;
|
|||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
@ -229,6 +234,45 @@ public class BufferedContentSinkTest
|
|||
}
|
||||
}
|
||||
|
||||
public static Stream<BiConsumer<BufferedContentSink, Callback>> flushers()
|
||||
{
|
||||
return Stream.of(
|
||||
BufferedContentSink::flush,
|
||||
(b, callback) -> b.write(false, BufferedContentSink.FLUSH_BUFFER, callback)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("flushers")
|
||||
public void testFlush(BiConsumer<BufferedContentSink, Callback> flusher) throws Exception
|
||||
{
|
||||
ByteBuffer accumulatingBuffer = BufferUtil.allocate(4096);
|
||||
BufferUtil.flipToFill(accumulatingBuffer);
|
||||
|
||||
try (AsyncContent async = new AsyncContent(); )
|
||||
{
|
||||
BufferedContentSink buffered = new BufferedContentSink(async, _bufferPool, false, 8192, 8192);
|
||||
|
||||
Callback.Completable callback = new Callback.Completable();
|
||||
buffered.write(false, BufferUtil.toBuffer("Hello "), callback);
|
||||
callback.get(5, TimeUnit.SECONDS);
|
||||
assertNull(async.read());
|
||||
|
||||
callback = new Callback.Completable();
|
||||
buffered.write(false, BufferUtil.toBuffer("World!"), callback);
|
||||
callback.get(5, TimeUnit.SECONDS);
|
||||
assertNull(async.read());
|
||||
|
||||
callback = new Callback.Completable();
|
||||
flusher.accept(buffered, callback);
|
||||
Content.Chunk chunk = async.read();
|
||||
assertThat(chunk.isLast(), is(false));
|
||||
assertThat(BufferUtil.toString(chunk.getByteBuffer()), is("Hello World!"));
|
||||
chunk.release();
|
||||
callback.get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxAggregationSizeExceeded()
|
||||
{
|
||||
|
|
|
@ -11,73 +11,47 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.nested;
|
||||
package org.eclipse.jetty.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Writer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.Utf8StringBuilder;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@Disabled // TODO
|
||||
public class HttpWriterTest
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
public class WriteThroughWriterTest
|
||||
{
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
private HttpOutput _httpOut;
|
||||
private OutputStream _out;
|
||||
private ByteBuffer _bytes;
|
||||
|
||||
@BeforeEach
|
||||
public void init() throws Exception
|
||||
{
|
||||
_bytes = BufferUtil.allocate(2048);
|
||||
|
||||
Server server = new Server();
|
||||
ContextHandler contextHandler = new ContextHandler(server);
|
||||
|
||||
HttpChannel channel = new HttpChannel(contextHandler, new MockConnectionMetaData())
|
||||
{
|
||||
@Override
|
||||
public boolean failAllContent(Throwable failure)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed(Throwable x)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean eof()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
_httpOut = new HttpOutput(channel)
|
||||
{
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException
|
||||
{
|
||||
BufferUtil.append(_bytes, b, off, len);
|
||||
}
|
||||
};
|
||||
_out = new ByteBufferOutputStream(_bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleUTF8() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
writer.write("Now is the time");
|
||||
assertArrayEquals("Now is the time".getBytes(StandardCharsets.UTF_8), BufferUtil.toArray(_bytes));
|
||||
}
|
||||
|
@ -85,7 +59,7 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testUTF8() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
writer.write("How now \uFF22rown cow");
|
||||
assertArrayEquals("How now \uFF22rown cow".getBytes(StandardCharsets.UTF_8), BufferUtil.toArray(_bytes));
|
||||
}
|
||||
|
@ -93,7 +67,7 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testUTF16() throws Exception
|
||||
{
|
||||
HttpWriter writer = new EncodingHttpWriter(_httpOut, MimeTypes.UTF16);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_16);
|
||||
writer.write("How now \uFF22rown cow");
|
||||
assertArrayEquals("How now \uFF22rown cow".getBytes(StandardCharsets.UTF_16), BufferUtil.toArray(_bytes));
|
||||
}
|
||||
|
@ -101,7 +75,7 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testNotCESU8() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
String data = "xxx\uD801\uDC00xxx";
|
||||
writer.write(data);
|
||||
byte[] b = BufferUtil.toArray(_bytes);
|
||||
|
@ -117,25 +91,20 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testMultiByteOverflowUTF8() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
int maxWriteSize = WriteThroughWriter.DEFAULT_MAX_WRITE_SIZE;
|
||||
final String singleByteStr = "a";
|
||||
final String multiByteDuplicateStr = "\uFF22";
|
||||
int remainSize = 1;
|
||||
|
||||
int multiByteStrByteLength = multiByteDuplicateStr.getBytes(StandardCharsets.UTF_8).length;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < HttpWriter.MAX_OUTPUT_CHARS - multiByteStrByteLength; i++)
|
||||
{
|
||||
sb.append(singleByteStr);
|
||||
}
|
||||
sb.append(singleByteStr.repeat(Math.max(0, maxWriteSize - multiByteStrByteLength)));
|
||||
sb.append(multiByteDuplicateStr);
|
||||
for (int i = 0; i < remainSize; i++)
|
||||
{
|
||||
sb.append(singleByteStr);
|
||||
}
|
||||
char[] buf = new char[HttpWriter.MAX_OUTPUT_CHARS * 3];
|
||||
sb.append(singleByteStr.repeat(remainSize));
|
||||
char[] buf = new char[maxWriteSize * 3];
|
||||
|
||||
int length = HttpWriter.MAX_OUTPUT_CHARS - multiByteStrByteLength + remainSize + 1;
|
||||
int length = maxWriteSize - multiByteStrByteLength + remainSize + 1;
|
||||
sb.toString().getChars(0, length, buf, 0);
|
||||
|
||||
writer.write(buf, 0, length);
|
||||
|
@ -146,7 +115,7 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testISO8859() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Iso88591HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.ISO_8859_1);
|
||||
writer.write("How now \uFF22rown cow");
|
||||
assertEquals(new String(BufferUtil.toArray(_bytes), StandardCharsets.ISO_8859_1), "How now ?rown cow");
|
||||
}
|
||||
|
@ -154,8 +123,7 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testUTF16x2() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
String source = "\uD842\uDF9F";
|
||||
|
||||
byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
|
||||
|
@ -177,24 +145,17 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testMultiByteOverflowUTF16x2() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
|
||||
final String singleByteStr = "a";
|
||||
int remainSize = 1;
|
||||
final String multiByteDuplicateStr = "\uD842\uDF9F";
|
||||
int adjustSize = -1;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < HttpWriter.MAX_OUTPUT_CHARS + adjustSize; i++)
|
||||
{
|
||||
sb.append(singleByteStr);
|
||||
}
|
||||
sb.append(multiByteDuplicateStr);
|
||||
for (int i = 0; i < remainSize; i++)
|
||||
{
|
||||
sb.append(singleByteStr);
|
||||
}
|
||||
String source = sb.toString();
|
||||
String source =
|
||||
singleByteStr.repeat(Math.max(0, WriteThroughWriter.DEFAULT_MAX_WRITE_SIZE + adjustSize)) +
|
||||
multiByteDuplicateStr +
|
||||
singleByteStr.repeat(remainSize);
|
||||
|
||||
byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
|
||||
writer.write(source.toCharArray(), 0, source.toCharArray().length);
|
||||
|
@ -215,24 +176,17 @@ public class HttpWriterTest
|
|||
@Test
|
||||
public void testMultiByteOverflowUTF16X22() throws Exception
|
||||
{
|
||||
HttpWriter writer = new Utf8HttpWriter(_httpOut);
|
||||
Writer writer = WriteThroughWriter.newWriter(_out, StandardCharsets.UTF_8);
|
||||
|
||||
final String singleByteStr = "a";
|
||||
int remainSize = 1;
|
||||
final String multiByteDuplicateStr = "\uD842\uDF9F";
|
||||
int adjustSize = -2;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < HttpWriter.MAX_OUTPUT_CHARS + adjustSize; i++)
|
||||
{
|
||||
sb.append(singleByteStr);
|
||||
}
|
||||
sb.append(multiByteDuplicateStr);
|
||||
for (int i = 0; i < remainSize; i++)
|
||||
{
|
||||
sb.append(singleByteStr);
|
||||
}
|
||||
String source = sb.toString();
|
||||
String source =
|
||||
singleByteStr.repeat(Math.max(0, WriteThroughWriter.DEFAULT_MAX_WRITE_SIZE + adjustSize)) +
|
||||
multiByteDuplicateStr +
|
||||
singleByteStr.repeat(remainSize);
|
||||
|
||||
byte[] bytes = source.getBytes(StandardCharsets.UTF_8);
|
||||
writer.write(source.toCharArray(), 0, source.toCharArray().length);
|
||||
|
@ -252,11 +206,12 @@ public class HttpWriterTest
|
|||
|
||||
private void myReportBytes(byte[] bytes) throws Exception
|
||||
{
|
||||
// for (int i = 0; i < bytes.length; i++)
|
||||
// {
|
||||
// System.err.format("%s%x",(i == 0)?"[":(i % (HttpWriter.MAX_OUTPUT_CHARS) == 0)?"][":",",bytes[i]);
|
||||
// }
|
||||
// System.err.format("]->%s\n",new String(bytes,StringUtil.__UTF8));
|
||||
if (LoggerFactory.getLogger(WriteThroughWriterTest.class).isDebugEnabled())
|
||||
{
|
||||
for (int i = 0; i < bytes.length; i++)
|
||||
System.err.format("%s%x", (i == 0) ? "[" : (i % 512 == 0) ? "][" : ",", bytes[i]);
|
||||
System.err.format("]->%s\n", new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertArrayEquals(byte[] b1, byte[] b2)
|
||||
|
@ -268,4 +223,46 @@ public class HttpWriterTest
|
|||
assertEquals(b1[i], b2[i], test);
|
||||
}
|
||||
}
|
||||
|
||||
public static Stream<Arguments> subSequenceTests()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of("", 0, 0, ""),
|
||||
Arguments.of("", 0, 1, null),
|
||||
Arguments.of("", 1, 0, ""),
|
||||
Arguments.of("", 1, 1, null),
|
||||
Arguments.of("hello", 0, 5, "hello"),
|
||||
Arguments.of("hello", 0, 4, "hell"),
|
||||
Arguments.of("hello", 1, 4, "ello"),
|
||||
Arguments.of("hello", 1, 3, "ell"),
|
||||
Arguments.of("hello", 5, 0, ""),
|
||||
Arguments.of("hello", 0, 6, null)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("subSequenceTests")
|
||||
public void testSubSequence(String source, int offset, int length, String expected)
|
||||
{
|
||||
if (expected == null)
|
||||
{
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> WriteThroughWriter.subSequence(source, offset, length));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> WriteThroughWriter.subSequence(source.toCharArray(), offset, length));
|
||||
return;
|
||||
}
|
||||
|
||||
CharSequence result = WriteThroughWriter.subSequence(source, offset, length);
|
||||
assertThat(result.toString(), equalTo(expected));
|
||||
|
||||
// check string optimization
|
||||
if (offset == 0 && length == source.length())
|
||||
{
|
||||
assertThat(result, sameInstance(source));
|
||||
assertThat(result.subSequence(offset, length), sameInstance(source));
|
||||
return;
|
||||
}
|
||||
|
||||
result = WriteThroughWriter.subSequence(source.toCharArray(), offset, length);
|
||||
assertThat(result.toString(), equalTo(expected));
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import java.security.Principal;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -495,12 +496,17 @@ public interface Request extends Attributes, Content.Source
|
|||
};
|
||||
}
|
||||
|
||||
// TODO: consider inline and remove.
|
||||
static InputStream asInputStream(Request request)
|
||||
{
|
||||
return Content.Source.asInputStream(request);
|
||||
}
|
||||
|
||||
static Charset getCharset(Request request)
|
||||
{
|
||||
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
|
||||
return Objects.requireNonNullElse(request.getContext().getMimeTypes(), MimeTypes.DEFAULTS).getCharset(contentType);
|
||||
}
|
||||
|
||||
static Fields extractQueryParameters(Request request)
|
||||
{
|
||||
String query = request.getHttpURI().getQuery();
|
||||
|
|
|
@ -125,13 +125,16 @@ public class BufferedResponseHandlerTest
|
|||
@Test
|
||||
public void testFlushed() throws Exception
|
||||
{
|
||||
_test._writes = 4;
|
||||
_test._flush = true;
|
||||
_test._bufferSize = 2048;
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response, containsString(" 200 OK"));
|
||||
assertThat(response, containsString("Write: 0"));
|
||||
assertThat(response, containsString("Write: 9"));
|
||||
assertThat(response, containsString("Written: true"));
|
||||
assertThat(response, containsString("Write: 1"));
|
||||
assertThat(response, containsString("Transfer-Encoding: chunked"));
|
||||
assertThat(response, not(containsString("Write: 3")));
|
||||
assertThat(response, not(containsString("Written: true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -181,10 +184,10 @@ public class BufferedResponseHandlerTest
|
|||
_test._content = new byte[0];
|
||||
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response, containsString(" 200 OK"));
|
||||
assertThat(response, containsString("Content-Length: "));
|
||||
assertThat(response, containsString("Transfer-Encoding: chunked"));
|
||||
assertThat(response, containsString("Write: 0"));
|
||||
assertThat(response, not(containsString("Write: 1")));
|
||||
assertThat(response, containsString("Written: true"));
|
||||
assertThat(response, not(containsString("Written: true")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -235,9 +238,11 @@ public class BufferedResponseHandlerTest
|
|||
{
|
||||
response.getHeaders().add("Write", Integer.toString(i));
|
||||
outputStream.write(_content);
|
||||
if (_flush)
|
||||
if (_flush && i % 2 == 1)
|
||||
outputStream.flush();
|
||||
}
|
||||
if (_flush)
|
||||
outputStream.flush();
|
||||
response.getHeaders().add("Written", "true");
|
||||
}
|
||||
callback.succeeded();
|
||||
|
|
|
@ -35,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
public class StringUtilTest
|
||||
{
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void testAsciiToLowerCase()
|
||||
|
|
|
@ -39,7 +39,6 @@ module org.eclipse.jetty.ee10.servlet
|
|||
exports org.eclipse.jetty.ee10.servlet.security;
|
||||
exports org.eclipse.jetty.ee10.servlet.security.authentication;
|
||||
exports org.eclipse.jetty.ee10.servlet.util;
|
||||
exports org.eclipse.jetty.ee10.servlet.writer;
|
||||
|
||||
|
||||
exports org.eclipse.jetty.ee10.servlet.jmx to
|
||||
|
|
|
@ -321,13 +321,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked for the COMPLETE action handling in
|
||||
* HttpChannel.handle. The callback passed typically will call completed
|
||||
* to finish the request cycle and so may need to asynchronously wait for:
|
||||
* a pending/blocked operation to finish and then either an async close or
|
||||
* wait for an application close to complete.
|
||||
* @param callback The callback to complete when writing the output is complete.
|
||||
*/
|
||||
public void complete(Callback callback)
|
||||
{
|
||||
// This method is invoked for the COMPLETE action handling in
|
||||
// HttpChannel.handle. The callback passed typically will call completed
|
||||
// to finish the request cycle and so may need to asynchronously wait for:
|
||||
// a pending/blocked operation to finish and then either an async close or
|
||||
// wait for an application close to complete.
|
||||
boolean succeeded = false;
|
||||
Throwable error = null;
|
||||
ByteBuffer content = null;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.servlet.writer;
|
||||
package org.eclipse.jetty.ee10.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
|
@ -23,7 +23,7 @@ import jakarta.servlet.ServletResponse;
|
|||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.RuntimeIOException;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.io.WriteThroughWriter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -41,17 +41,17 @@ public class ResponseWriter extends PrintWriter
|
|||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResponseWriter.class);
|
||||
|
||||
private final HttpWriter _httpWriter;
|
||||
private final WriteThroughWriter _writer;
|
||||
private final Locale _locale;
|
||||
private final String _encoding;
|
||||
private IOException _ioException;
|
||||
private boolean _isClosed = false;
|
||||
private Formatter _formatter;
|
||||
|
||||
public ResponseWriter(HttpWriter httpWriter, Locale locale, String encoding)
|
||||
public ResponseWriter(WriteThroughWriter writer, Locale locale, String encoding)
|
||||
{
|
||||
super(httpWriter, false);
|
||||
_httpWriter = httpWriter;
|
||||
super(writer, false);
|
||||
_writer = writer;
|
||||
_locale = locale;
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ public class ResponseWriter extends PrintWriter
|
|||
{
|
||||
_isClosed = false;
|
||||
clearError();
|
||||
out = _httpWriter;
|
||||
out = _writer;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,9 @@ public class ResponseWriter extends PrintWriter
|
|||
super.setError();
|
||||
|
||||
if (th instanceof IOException)
|
||||
{
|
||||
_ioException = (IOException)th;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ioException = new IOException(String.valueOf(th));
|
||||
|
@ -165,13 +167,15 @@ public class ResponseWriter extends PrintWriter
|
|||
}
|
||||
}
|
||||
|
||||
public void complete(Callback callback)
|
||||
/**
|
||||
* Used to mark this writer as closed during any asynchronous completion operation.
|
||||
*/
|
||||
void markAsClosed()
|
||||
{
|
||||
synchronized (lock)
|
||||
{
|
||||
_isClosed = true;
|
||||
}
|
||||
_httpWriter.complete(callback);
|
||||
}
|
||||
|
||||
@Override
|
|
@ -103,10 +103,10 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
private final ServletContextRequest _servletContextRequest;
|
||||
private final ServletChannel _servletChannel;
|
||||
private AsyncContextState _async;
|
||||
private String _characterEncoding;
|
||||
private Charset _charset;
|
||||
private Charset _readerCharset;
|
||||
private int _inputState = ServletContextRequest.INPUT_NONE;
|
||||
private BufferedReader _reader;
|
||||
private String _readerEncoding;
|
||||
private String _contentType;
|
||||
private boolean _contentParamsExtracted;
|
||||
private Fields _contentParameters;
|
||||
|
@ -717,24 +717,13 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
@Override
|
||||
public String getCharacterEncoding()
|
||||
{
|
||||
if (_characterEncoding == null)
|
||||
{
|
||||
if (getRequest().getContext() != null)
|
||||
_characterEncoding = getServletRequestInfo().getServletContext().getServletContext().getRequestCharacterEncoding();
|
||||
if (_charset == null)
|
||||
_charset = Request.getCharset(getRequest());
|
||||
|
||||
if (_characterEncoding == null)
|
||||
{
|
||||
String contentType = getContentType();
|
||||
if (contentType != null)
|
||||
{
|
||||
MimeTypes.Type mime = MimeTypes.CACHE.get(contentType);
|
||||
String charset = (mime == null || mime.getCharset() == null) ? MimeTypes.getCharsetFromContentType(contentType) : mime.getCharset().toString();
|
||||
if (charset != null)
|
||||
_characterEncoding = charset;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _characterEncoding;
|
||||
if (_charset == null)
|
||||
return getServletRequestInfo().getServletContext().getServletContext().getRequestCharacterEncoding();
|
||||
|
||||
return _charset.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -742,8 +731,7 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
{
|
||||
if (_inputState != ServletContextRequest.INPUT_NONE)
|
||||
return;
|
||||
MimeTypes.getKnownCharset(encoding);
|
||||
_characterEncoding = encoding;
|
||||
_charset = MimeTypes.getKnownCharset(encoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1039,15 +1027,18 @@ public class ServletApiRequest implements HttpServletRequest
|
|||
if (_inputState == ServletContextRequest.INPUT_READER)
|
||||
return _reader;
|
||||
|
||||
String encoding = getCharacterEncoding();
|
||||
if (encoding == null)
|
||||
encoding = MimeTypes.ISO_8859_1;
|
||||
if (_charset == null)
|
||||
_charset = Request.getCharset(getRequest());
|
||||
if (_charset == null)
|
||||
_charset = getRequest().getContext().getMimeTypes().getCharset(getServletRequestInfo().getServletContext().getServletContextHandler().getDefaultRequestCharacterEncoding());
|
||||
if (_charset == null)
|
||||
_charset = StandardCharsets.ISO_8859_1;
|
||||
|
||||
if (_reader == null || !encoding.equalsIgnoreCase(_readerEncoding))
|
||||
if (_reader == null || !_charset.equals(_readerCharset))
|
||||
{
|
||||
ServletInputStream in = getInputStream();
|
||||
_readerEncoding = encoding;
|
||||
_reader = new BufferedReader(new InputStreamReader(in, encoding))
|
||||
_readerCharset = _charset;
|
||||
_reader = new BufferedReader(new InputStreamReader(in, _charset))
|
||||
{
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
|
|
|
@ -27,16 +27,12 @@ import jakarta.servlet.http.Cookie;
|
|||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletRequestInfo;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletResponseInfo;
|
||||
import org.eclipse.jetty.ee10.servlet.writer.EncodingHttpWriter;
|
||||
import org.eclipse.jetty.ee10.servlet.writer.Iso88591HttpWriter;
|
||||
import org.eclipse.jetty.ee10.servlet.writer.ResponseWriter;
|
||||
import org.eclipse.jetty.ee10.servlet.writer.Utf8HttpWriter;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.io.WriteThroughWriter;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.session.ManagedSession;
|
||||
|
@ -318,12 +314,11 @@ public class ServletApiResponse implements HttpServletResponse
|
|||
writer.reopen();
|
||||
else
|
||||
{
|
||||
if (MimeTypes.ISO_8859_1.equalsIgnoreCase(encoding))
|
||||
getServletResponseInfo().setWriter(writer = new ResponseWriter(new Iso88591HttpWriter(getServletChannel().getHttpOutput()), locale, encoding));
|
||||
else if (MimeTypes.UTF8.equalsIgnoreCase(encoding))
|
||||
getServletResponseInfo().setWriter(writer = new ResponseWriter(new Utf8HttpWriter(getServletChannel().getHttpOutput()), locale, encoding));
|
||||
else
|
||||
getServletResponseInfo().setWriter(writer = new ResponseWriter(new EncodingHttpWriter(getServletChannel().getHttpOutput(), encoding), locale, encoding));
|
||||
// We must use an implementation of AbstractOutputStreamWriter here as we rely on the non cached characters
|
||||
// in the writer implementation for flush and completion operations.
|
||||
WriteThroughWriter outputStreamWriter = WriteThroughWriter.newWriter(getServletChannel().getHttpOutput(), encoding);
|
||||
getServletResponseInfo().setWriter(writer = new ResponseWriter(
|
||||
outputStreamWriter, locale, encoding));
|
||||
}
|
||||
|
||||
// Set the output type at the end, because setCharacterEncoding() checks for it.
|
||||
|
|
|
@ -73,7 +73,6 @@ import org.eclipse.jetty.ee10.servlet.ServletContextResponse.OutputType;
|
|||
import org.eclipse.jetty.ee10.servlet.security.ConstraintAware;
|
||||
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
|
||||
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.writer.ResponseWriter;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.pathmap.MatchedResource;
|
||||
import org.eclipse.jetty.security.SecurityHandler;
|
||||
|
|
|
@ -23,7 +23,6 @@ import jakarta.servlet.ServletResponse;
|
|||
import jakarta.servlet.ServletResponseWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.eclipse.jetty.ee10.servlet.writer.ResponseWriter;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
|
@ -206,9 +205,8 @@ public class ServletContextResponse extends ContextResponse implements ServletCo
|
|||
public void completeOutput(Callback callback)
|
||||
{
|
||||
if (_outputType == OutputType.WRITER)
|
||||
_writer.complete(callback);
|
||||
else
|
||||
getHttpOutput().complete(callback);
|
||||
_writer.markAsClosed();
|
||||
getHttpOutput().complete(callback);
|
||||
}
|
||||
|
||||
public boolean isAllContentWritten(long written)
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.servlet.writer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
|
||||
import org.eclipse.jetty.ee10.servlet.HttpOutput;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EncodingHttpWriter extends HttpWriter
|
||||
{
|
||||
final Writer _converter;
|
||||
|
||||
public EncodingHttpWriter(HttpOutput out, String encoding) throws IOException
|
||||
{
|
||||
super(out);
|
||||
_converter = new OutputStreamWriter(_bytes, encoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
HttpOutput out = _out;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, MAX_OUTPUT_CHARS);
|
||||
|
||||
_converter.write(s, offset, chars);
|
||||
_converter.flush();
|
||||
_bytes.writeTo(out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.servlet.writer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
import org.eclipse.jetty.ee10.servlet.HttpOutput;
|
||||
import org.eclipse.jetty.util.ByteArrayOutputStream2;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class HttpWriter extends Writer
|
||||
{
|
||||
public static final int MAX_OUTPUT_CHARS = 512; // TODO should this be configurable? super size is 1024
|
||||
|
||||
final HttpOutput _out;
|
||||
final ByteArrayOutputStream2 _bytes;
|
||||
final char[] _chars;
|
||||
|
||||
public HttpWriter(HttpOutput out)
|
||||
{
|
||||
_out = out;
|
||||
_chars = new char[MAX_OUTPUT_CHARS];
|
||||
_bytes = new ByteArrayOutputStream2(MAX_OUTPUT_CHARS); // TODO should this be pooled - or do we just recycle the writer?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
_out.close();
|
||||
}
|
||||
|
||||
public void complete(Callback callback)
|
||||
{
|
||||
_out.complete(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException
|
||||
{
|
||||
_out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String s, int offset, int length) throws IOException
|
||||
{
|
||||
while (length > MAX_OUTPUT_CHARS)
|
||||
{
|
||||
write(s, offset, MAX_OUTPUT_CHARS);
|
||||
offset += MAX_OUTPUT_CHARS;
|
||||
length -= MAX_OUTPUT_CHARS;
|
||||
}
|
||||
|
||||
s.getChars(offset, offset + length, _chars, 0);
|
||||
write(_chars, 0, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.servlet.writer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jetty.ee10.servlet.HttpOutput;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class Iso88591HttpWriter extends HttpWriter
|
||||
{
|
||||
|
||||
public Iso88591HttpWriter(HttpOutput out)
|
||||
{
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
HttpOutput out = _out;
|
||||
|
||||
if (length == 1)
|
||||
{
|
||||
int c = s[offset];
|
||||
out.write(c < 256 ? c : '?');
|
||||
return;
|
||||
}
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, MAX_OUTPUT_CHARS);
|
||||
|
||||
byte[] buffer = _bytes.getBuf();
|
||||
int bytes = _bytes.getCount();
|
||||
|
||||
if (chars > buffer.length - bytes)
|
||||
chars = buffer.length - bytes;
|
||||
|
||||
for (int i = 0; i < chars; i++)
|
||||
{
|
||||
int c = s[offset + i];
|
||||
buffer[bytes++] = (byte)(c < 256 ? c : '?');
|
||||
}
|
||||
if (bytes >= 0)
|
||||
_bytes.setCount(bytes);
|
||||
|
||||
_bytes.writeTo(out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.servlet.writer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jetty.ee10.servlet.HttpOutput;
|
||||
|
||||
/**
|
||||
* OutputWriter.
|
||||
* A writer that can wrap a {@link HttpOutput} stream and provide
|
||||
* character encodings.
|
||||
*
|
||||
* The UTF-8 encoding is done by this class and no additional
|
||||
* buffers or Writers are used.
|
||||
* The UTF-8 code was inspired by http://javolution.org
|
||||
*/
|
||||
public class Utf8HttpWriter extends HttpWriter
|
||||
{
|
||||
int _surrogate = 0;
|
||||
|
||||
public Utf8HttpWriter(HttpOutput out)
|
||||
{
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
HttpOutput out = _out;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, MAX_OUTPUT_CHARS);
|
||||
|
||||
byte[] buffer = _bytes.getBuf();
|
||||
int bytes = _bytes.getCount();
|
||||
|
||||
if (bytes + chars > buffer.length)
|
||||
chars = buffer.length - bytes;
|
||||
|
||||
for (int i = 0; i < chars; i++)
|
||||
{
|
||||
int code = s[offset + i];
|
||||
|
||||
// Do we already have a surrogate?
|
||||
if (_surrogate == 0)
|
||||
{
|
||||
// No - is this char code a surrogate?
|
||||
if (Character.isHighSurrogate((char)code))
|
||||
{
|
||||
_surrogate = code; // UCS-?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// else handle a low surrogate
|
||||
else if (Character.isLowSurrogate((char)code))
|
||||
{
|
||||
code = Character.toCodePoint((char)_surrogate, (char)code); // UCS-4
|
||||
}
|
||||
// else UCS-2
|
||||
else
|
||||
{
|
||||
code = _surrogate; // UCS-2
|
||||
_surrogate = 0; // USED
|
||||
i--;
|
||||
}
|
||||
|
||||
if ((code & 0xffffff80) == 0)
|
||||
{
|
||||
// 1b
|
||||
if (bytes >= buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(code);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((code & 0xfffff800) == 0)
|
||||
{
|
||||
// 2b
|
||||
if (bytes + 2 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xc0 | (code >> 6));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xffff0000) == 0)
|
||||
{
|
||||
// 3b
|
||||
if (bytes + 3 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xe0 | (code >> 12));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xff200000) == 0)
|
||||
{
|
||||
// 4b
|
||||
if (bytes + 4 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xf0 | (code >> 18));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xf4000000) == 0)
|
||||
{
|
||||
// 5b
|
||||
if (bytes + 5 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xf8 | (code >> 24));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0x80000000) == 0)
|
||||
{
|
||||
// 6b
|
||||
if (bytes + 6 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xfc | (code >> 30));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 24) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[bytes++] = (byte)('?');
|
||||
}
|
||||
|
||||
_surrogate = 0; // USED
|
||||
|
||||
if (bytes == buffer.length)
|
||||
{
|
||||
chars = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_bytes.setCount(bytes);
|
||||
|
||||
_bytes.writeTo(out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.nested;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EncodingHttpWriter extends HttpWriter
|
||||
{
|
||||
final Writer _converter;
|
||||
|
||||
public EncodingHttpWriter(HttpOutput out, String encoding) throws IOException
|
||||
{
|
||||
super(out);
|
||||
_converter = new OutputStreamWriter(_bytes, encoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
HttpOutput out = _out;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, MAX_OUTPUT_CHARS);
|
||||
|
||||
_converter.write(s, offset, chars);
|
||||
_converter.flush();
|
||||
_bytes.writeTo(out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.nested;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
import org.eclipse.jetty.util.ByteArrayOutputStream2;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class HttpWriter extends Writer
|
||||
{
|
||||
public static final int MAX_OUTPUT_CHARS = 512; // TODO should this be configurable? super size is 1024
|
||||
|
||||
final HttpOutput _out;
|
||||
final ByteArrayOutputStream2 _bytes;
|
||||
final char[] _chars;
|
||||
|
||||
public HttpWriter(HttpOutput out)
|
||||
{
|
||||
_out = out;
|
||||
_chars = new char[MAX_OUTPUT_CHARS];
|
||||
_bytes = new ByteArrayOutputStream2(MAX_OUTPUT_CHARS); // TODO should this be pooled - or do we just recycle the writer?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
_out.close();
|
||||
}
|
||||
|
||||
public void complete(Callback callback)
|
||||
{
|
||||
_out.complete(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException
|
||||
{
|
||||
_out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String s, int offset, int length) throws IOException
|
||||
{
|
||||
while (length > MAX_OUTPUT_CHARS)
|
||||
{
|
||||
write(s, offset, MAX_OUTPUT_CHARS);
|
||||
offset += MAX_OUTPUT_CHARS;
|
||||
length -= MAX_OUTPUT_CHARS;
|
||||
}
|
||||
|
||||
s.getChars(offset, offset + length, _chars, 0);
|
||||
write(_chars, 0, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.nested;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class Iso88591HttpWriter extends HttpWriter
|
||||
{
|
||||
|
||||
public Iso88591HttpWriter(HttpOutput out)
|
||||
{
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
HttpOutput out = _out;
|
||||
|
||||
if (length == 1)
|
||||
{
|
||||
int c = s[offset];
|
||||
out.write(c < 256 ? c : '?');
|
||||
return;
|
||||
}
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, MAX_OUTPUT_CHARS);
|
||||
|
||||
byte[] buffer = _bytes.getBuf();
|
||||
int bytes = _bytes.getCount();
|
||||
|
||||
if (chars > buffer.length - bytes)
|
||||
chars = buffer.length - bytes;
|
||||
|
||||
for (int i = 0; i < chars; i++)
|
||||
{
|
||||
int c = s[offset + i];
|
||||
buffer[bytes++] = (byte)(c < 256 ? c : '?');
|
||||
}
|
||||
if (bytes >= 0)
|
||||
_bytes.setCount(bytes);
|
||||
|
||||
_bytes.writeTo(out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,6 +51,7 @@ import org.eclipse.jetty.http.MimeTypes;
|
|||
import org.eclipse.jetty.http.PreEncodedHttpField;
|
||||
import org.eclipse.jetty.http.content.HttpContent;
|
||||
import org.eclipse.jetty.io.RuntimeIOException;
|
||||
import org.eclipse.jetty.io.WriteThroughWriter;
|
||||
import org.eclipse.jetty.server.Context;
|
||||
import org.eclipse.jetty.server.HttpCookieUtils;
|
||||
import org.eclipse.jetty.server.HttpCookieUtils.SetCookieHttpField;
|
||||
|
@ -867,15 +868,15 @@ public class Response implements HttpServletResponse
|
|||
String encoding = getCharacterEncoding(true);
|
||||
Locale locale = getLocale();
|
||||
if (_writer != null && _writer.isFor(locale, encoding))
|
||||
{
|
||||
_writer.reopen();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MimeTypes.ISO_8859_1.equalsIgnoreCase(encoding))
|
||||
_writer = new ResponseWriter(new Iso88591HttpWriter(_out), locale, encoding);
|
||||
else if (MimeTypes.UTF8.equalsIgnoreCase(encoding))
|
||||
_writer = new ResponseWriter(new Utf8HttpWriter(_out), locale, encoding);
|
||||
else
|
||||
_writer = new ResponseWriter(new EncodingHttpWriter(_out, encoding), locale, encoding);
|
||||
// We must use an specialized Writer here as we rely on the non cached characters
|
||||
// in the writer implementation for flush and completion operations.
|
||||
WriteThroughWriter outputStreamWriter = WriteThroughWriter.newWriter(_out, encoding);
|
||||
_writer = new ResponseWriter(outputStreamWriter, locale, encoding);
|
||||
}
|
||||
|
||||
// Set the output type at the end, because setCharacterEncoding() checks for it.
|
||||
|
@ -965,9 +966,8 @@ public class Response implements HttpServletResponse
|
|||
public void completeOutput(Callback callback)
|
||||
{
|
||||
if (_outputType == OutputType.WRITER)
|
||||
_writer.complete(callback);
|
||||
else
|
||||
_out.complete(callback);
|
||||
_writer.markAsClosed();
|
||||
_out.complete(callback);
|
||||
}
|
||||
|
||||
public long getLongContentLength()
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.util.Locale;
|
|||
import jakarta.servlet.ServletResponse;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.RuntimeIOException;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.io.WriteThroughWriter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -40,17 +40,17 @@ public class ResponseWriter extends PrintWriter
|
|||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResponseWriter.class);
|
||||
|
||||
private final HttpWriter _httpWriter;
|
||||
private final WriteThroughWriter _writer;
|
||||
private final Locale _locale;
|
||||
private final String _encoding;
|
||||
private IOException _ioException;
|
||||
private boolean _isClosed = false;
|
||||
private Formatter _formatter;
|
||||
|
||||
public ResponseWriter(HttpWriter httpWriter, Locale locale, String encoding)
|
||||
public ResponseWriter(WriteThroughWriter httpWriter, Locale locale, String encoding)
|
||||
{
|
||||
super(httpWriter, false);
|
||||
_httpWriter = httpWriter;
|
||||
_writer = httpWriter;
|
||||
_locale = locale;
|
||||
_encoding = encoding;
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ public class ResponseWriter extends PrintWriter
|
|||
{
|
||||
_isClosed = false;
|
||||
clearError();
|
||||
out = _httpWriter;
|
||||
out = _writer;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,13 +164,15 @@ public class ResponseWriter extends PrintWriter
|
|||
}
|
||||
}
|
||||
|
||||
public void complete(Callback callback)
|
||||
/**
|
||||
* Used to mark this writer as closed during any asynchronous completion operation.
|
||||
*/
|
||||
public void markAsClosed()
|
||||
{
|
||||
synchronized (lock)
|
||||
{
|
||||
_isClosed = true;
|
||||
}
|
||||
_httpWriter.complete(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.nested;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* OutputWriter.
|
||||
* A writer that can wrap a {@link HttpOutput} stream and provide
|
||||
* character encodings.
|
||||
*
|
||||
* The UTF-8 encoding is done by this class and no additional
|
||||
* buffers or Writers are used.
|
||||
* The UTF-8 code was inspired by http://javolution.org
|
||||
*/
|
||||
public class Utf8HttpWriter extends HttpWriter
|
||||
{
|
||||
int _surrogate = 0;
|
||||
|
||||
public Utf8HttpWriter(HttpOutput out)
|
||||
{
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] s, int offset, int length) throws IOException
|
||||
{
|
||||
HttpOutput out = _out;
|
||||
|
||||
while (length > 0)
|
||||
{
|
||||
_bytes.reset();
|
||||
int chars = Math.min(length, MAX_OUTPUT_CHARS);
|
||||
|
||||
byte[] buffer = _bytes.getBuf();
|
||||
int bytes = _bytes.getCount();
|
||||
|
||||
if (bytes + chars > buffer.length)
|
||||
chars = buffer.length - bytes;
|
||||
|
||||
for (int i = 0; i < chars; i++)
|
||||
{
|
||||
int code = s[offset + i];
|
||||
|
||||
// Do we already have a surrogate?
|
||||
if (_surrogate == 0)
|
||||
{
|
||||
// No - is this char code a surrogate?
|
||||
if (Character.isHighSurrogate((char)code))
|
||||
{
|
||||
_surrogate = code; // UCS-?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// else handle a low surrogate
|
||||
else if (Character.isLowSurrogate((char)code))
|
||||
{
|
||||
code = Character.toCodePoint((char)_surrogate, (char)code); // UCS-4
|
||||
}
|
||||
// else UCS-2
|
||||
else
|
||||
{
|
||||
code = _surrogate; // UCS-2
|
||||
_surrogate = 0; // USED
|
||||
i--;
|
||||
}
|
||||
|
||||
if ((code & 0xffffff80) == 0)
|
||||
{
|
||||
// 1b
|
||||
if (bytes >= buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(code);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((code & 0xfffff800) == 0)
|
||||
{
|
||||
// 2b
|
||||
if (bytes + 2 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xc0 | (code >> 6));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xffff0000) == 0)
|
||||
{
|
||||
// 3b
|
||||
if (bytes + 3 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xe0 | (code >> 12));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xff200000) == 0)
|
||||
{
|
||||
// 4b
|
||||
if (bytes + 4 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xf0 | (code >> 18));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0xf4000000) == 0)
|
||||
{
|
||||
// 5b
|
||||
if (bytes + 5 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xf8 | (code >> 24));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else if ((code & 0x80000000) == 0)
|
||||
{
|
||||
// 6b
|
||||
if (bytes + 6 > buffer.length)
|
||||
{
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
buffer[bytes++] = (byte)(0xfc | (code >> 30));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 24) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 18) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 12) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | ((code >> 6) & 0x3f));
|
||||
buffer[bytes++] = (byte)(0x80 | (code & 0x3f));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[bytes++] = (byte)('?');
|
||||
}
|
||||
|
||||
_surrogate = 0; // USED
|
||||
|
||||
if (bytes == buffer.length)
|
||||
{
|
||||
chars = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_bytes.setCount(bytes);
|
||||
|
||||
_bytes.writeTo(out);
|
||||
length -= chars;
|
||||
offset += chars;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -318,7 +318,7 @@ public class ResponseTest
|
|||
assertEquals("application/vnd.api+json", response.getContentType());
|
||||
response.getWriter();
|
||||
assertEquals("application/vnd.api+json", response.getContentType());
|
||||
assertEquals("utf-8", response.getCharacterEncoding());
|
||||
assertEquals("UTF-8", response.getCharacterEncoding());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -461,7 +461,7 @@ public class ResponseTest
|
|||
Response response = getResponse();
|
||||
|
||||
response.setContentType("text/html");
|
||||
assertEquals("iso-8859-1", response.getCharacterEncoding());
|
||||
assertEquals("ISO-8859-1", response.getCharacterEncoding());
|
||||
|
||||
// setLocale should change character encoding based on
|
||||
// locale-encoding-mapping-list
|
||||
|
|
Loading…
Reference in New Issue