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:
Greg Wilkins 2023-09-27 01:29:15 +02:00 committed by GitHub
parent d2dff9a758
commit 1a207dbeea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1144 additions and 984 deletions

View File

@ -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())

View File

@ -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"));
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
/**

View File

@ -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);
}
}
}

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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
{

View File

@ -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);
}
}

View File

@ -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()
{

View File

@ -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));
}
}

View File

@ -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();

View File

@ -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();

View File

@ -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()

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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()

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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