diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 237047fc5066..c663130beb10 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -28,6 +28,7 @@ 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 static Charset getKnownCharset(String charsetName) throws UnsupportedEnco } } + private static String nameOf(Charset charset) + { + return charset == null ? null : charset.name(); + } + protected final Map _mimeMap = new HashMap<>(); - protected final Map _inferredEncodings = new HashMap<>(); - protected final Map _assumedEncodings = new HashMap<>(); + protected final Map _inferredEncodings = new HashMap<>(); + protected final Map _assumedEncodings = new HashMap<>(); public MimeTypes() { @@ -314,11 +320,37 @@ public MimeTypes(MimeTypes defaults) 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 String getMimeForExtension(String extension) 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 getMimeMap() { return Collections.unmodifiableMap(_mimeMap); @@ -354,12 +396,12 @@ public Map getMimeMap() public Map getInferredMap() { - return Collections.unmodifiableMap(_inferredEncodings); + return _inferredEncodings.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().name())); } public Map 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 String addMimeMapping(String extension, String type) 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 Map getAssumedMap() 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 @@ else if (_mimeMap.size() < props.keySet().size()) { 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()) diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java index 291ad315265d..f299e8f09b31 100644 --- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java @@ -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.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 void testWrapper() 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 void testWrapper() 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 void testWrapper() 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")); } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java index 3d929d2d5f20..5670d08dc603 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferAggregator.java @@ -58,6 +58,15 @@ public ByteBufferAggregator(ByteBufferPool bufferPool, boolean direct, int start _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 diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ChunkAccumulator.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ChunkAccumulator.java new file mode 100644 index 000000000000..cb036274c09a --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ChunkAccumulator.java @@ -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 _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 readAll(Content.Source source) + { + return readAll(source, -1); + } + + public CompletableFuture readAll(Content.Source source, int maxSize) + { + CompletableTask 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 readAll(Content.Source source, ByteBufferPool pool, boolean direct, int maxSize) + { + CompletableTask task = new AccumulatorTask<>(source, maxSize) + { + @Override + protected RetainableByteBuffer take(ChunkAccumulator accumulator) + { + return accumulator.take(pool, direct); + } + }; + return task.start(); + } + + private abstract static class AccumulatorTask extends CompletableTask + { + 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); + } +} diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java index 02e86b47e538..9cc3a3f4cfde 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java @@ -162,6 +162,19 @@ static ByteBuffer asByteBuffer(Source source) throws IOException } } + /** + *

Reads, non-blocking, the whole content source into a {@code byte} array.

+ * + * @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 asByteArrayAsync(Source source, int maxSize) + { + return new ChunkAccumulator().readAll(source, maxSize); + } + /** *

Reads, non-blocking, the whole content source into a {@link ByteBuffer}.

* @@ -170,9 +183,34 @@ static ByteBuffer asByteBuffer(Source source) throws IOException */ static CompletableFuture asByteBufferAsync(Source source) { - Promise.Completable completable = new Promise.Completable<>(); - asByteBuffer(source, completable); - return completable; + return asByteBufferAsync(source, -1); + } + + /** + *

Reads, non-blocking, the whole content source into a {@link ByteBuffer}.

+ * + * @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 asByteBufferAsync(Source source, int maxSize) + { + return asByteArrayAsync(source, maxSize).thenApply(ByteBuffer::wrap); + } + + /** + *

Reads, non-blocking, the whole content source into a {@link RetainableByteBuffer}.

+ * + * @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 asRetainableByteBuffer(Source source, ByteBufferPool pool, boolean direct, int maxSize) + { + return new ChunkAccumulator().readAll(source, pool, direct, maxSize); } /** diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java index 2340e76372a2..c51f50eed113 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/QuietException.java @@ -53,4 +53,26 @@ public Exception(Throwable cause) 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); + } + } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java index c6ed33088dce..78e954833502 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/RetainableByteBuffer.java @@ -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); /** *

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

Returns a {@code RetainableByteBuffer} that wraps + * the given {@code ByteBuffer} and {@link Retainable}.

+ * + * @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 default boolean isDirect() /** * @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 default int remaining() /** * @return whether the {@code ByteBuffer} has remaining bytes */ - public default boolean hasRemaining() + default boolean hasRemaining() { return getByteBuffer().hasRemaining(); } @@ -101,7 +146,7 @@ public default boolean hasRemaining() /** * @return the {@code ByteBuffer} capacity */ - public default int capacity() + default int capacity() { return getByteBuffer().capacity(); } @@ -109,7 +154,7 @@ public default int capacity() /** * @see BufferUtil#clear(ByteBuffer) */ - public default void clear() + default void clear() { BufferUtil.clear(getByteBuffer()); } @@ -117,7 +162,7 @@ public default void clear() /** * 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) { diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/WriteThroughWriter.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/WriteThroughWriter.java new file mode 100644 index 000000000000..362f1104ed87 --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/WriteThroughWriter.java @@ -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; + +/** + *

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

+ *

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}

. + */ +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 ... + */ + 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; + } + } + + /** + *

Get a zero copy subsequence of a {@link String}.

+ *

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.

+ * @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); + } +} diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/BufferedContentSink.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/BufferedContentSink.java index d835716e7b15..acb85e2ddb60 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/BufferedContentSink.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/BufferedContentSink.java @@ -35,6 +35,12 @@ */ 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 void write(boolean last, ByteBuffer byteBuffer, Callback callback) } } + /** + * 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 @@ private void flush(boolean last, ByteBuffer currentBuffer, Callback callback) 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 void failed(Throwable x) } }); } + else + { + _flusher.offer(false, aggregatedBuffer.getByteBuffer(), Callback.from(aggregatedBuffer::release, callback)); + } } /** @@ -152,7 +171,9 @@ public void failed(Throwable x) 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 @@ private void aggregateAndFlush(boolean last, ByteBuffer currentBuffer, Callback _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)) + LOG.debug("writing aggregated buffer: {} bytes, then {}", aggregatedBuffer.remaining(), currentBuffer.remaining()); + + if (BufferUtil.hasContent(currentBuffer)) { - @Override - public void succeeded() + _flusher.offer(false, aggregatedBuffer.getByteBuffer(), new Callback.Nested(Callback.from(aggregatedBuffer::release)) { - 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 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); - } - }); + @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 { diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSinkOutputStream.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSinkOutputStream.java index 7484681f005f..c02498fa2a5a 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSinkOutputStream.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSinkOutputStream.java @@ -64,7 +64,7 @@ public void flush() throws IOException { 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 void close() throws IOException { try (Blocker.Callback callback = _blocking.callback()) { - sink.write(true, null, callback); + close(callback); callback.block(); } catch (Throwable x) @@ -86,4 +86,9 @@ public void close() throws IOException throw IO.rethrow(x); } } + + public void close(Callback callback) throws IOException + { + sink.write(true, null, callback); + } } diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/BufferedContentSinkTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/BufferedContentSinkTest.java index 2d1b2bbaf817..0731eff5d32b 100644 --- a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/BufferedContentSinkTest.java +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/BufferedContentSinkTest.java @@ -21,6 +21,8 @@ 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.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.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 void testSmallBuffer() } } + public static Stream> flushers() + { + return Stream.of( + BufferedContentSink::flush, + (b, callback) -> b.write(false, BufferedContentSink.FLUSH_BUFFER, callback) + ); + } + + @ParameterizedTest + @MethodSource("flushers") + public void testFlush(BiConsumer 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() { diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/HttpWriterTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/WriteThroughWriterTest.java similarity index 60% rename from jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/HttpWriterTest.java rename to jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/WriteThroughWriterTest.java index 67a336da5b69..f0d6ba185eb0 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/HttpWriterTest.java +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/WriteThroughWriterTest.java @@ -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 void testSimpleUTF8() throws Exception @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 void testUTF8() throws Exception @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 void testUTF16() throws Exception @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 void testNotCESU8() throws Exception @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 void testMultiByteOverflowUTF8() throws Exception @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 void testISO8859() throws Exception @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 void testUTF16x2() throws Exception @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 void testMultiByteOverflowUTF16x2() throws Exception @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 void testMultiByteOverflowUTF16X22() throws Exception 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 @@ private void assertArrayEquals(byte[] b1, byte[] b2) assertEquals(b1[i], b2[i], test); } } + + public static Stream 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)); + } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 963ccb1b0587..c8fa53efedd0 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -23,6 +23,7 @@ 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 @@ static List getLocales(Request request) }; } - // 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(); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java index acebc7ee9260..6c76433ab136 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java @@ -125,13 +125,16 @@ public void testExcludedByMime() throws Exception @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 void testFlushEmpty() throws Exception _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 boolean handle(Request request, Response response, Callback callback) thr { 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(); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java index c970f269a5dd..c04ca5aa0c36 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java @@ -35,6 +35,7 @@ // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck public class StringUtilTest { + @Test @SuppressWarnings("ReferenceEquality") public void testAsciiToLowerCase() diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/module-info.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/module-info.java index 1fcc6489c07d..4a38a1ac969e 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/module-info.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/module-info.java @@ -39,7 +39,6 @@ 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 diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java index d57452af7eef..6c2fb21921fb 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java @@ -321,13 +321,16 @@ public void softClose() } } + /** + * 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; diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/ResponseWriter.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResponseWriter.java similarity index 96% rename from jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/ResponseWriter.java rename to jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResponseWriter.java index a445fffdfd1c..3e1ff04c71e5 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/ResponseWriter.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResponseWriter.java @@ -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.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 void reopen() { _isClosed = false; clearError(); - out = _httpWriter; + out = _writer; } } @@ -99,7 +99,9 @@ private void setError(Throwable th) super.setError(); if (th instanceof IOException) + { _ioException = (IOException)th; + } else { _ioException = new IOException(String.valueOf(th)); @@ -165,13 +167,15 @@ public void close() } } - 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 diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index 0ca45502162c..b051b89cc34c 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -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 Enumeration getAttributeNames() @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 void setCharacterEncoding(String encoding) throws UnsupportedEncodingExce { if (_inputState != ServletContextRequest.INPUT_NONE) return; - MimeTypes.getKnownCharset(encoding); - _characterEncoding = encoding; + _charset = MimeTypes.getKnownCharset(encoding); } @Override @@ -1039,15 +1027,18 @@ public BufferedReader getReader() throws IOException 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 diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java index e5ed3e95b79f..5cd5865b596c 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java @@ -27,16 +27,12 @@ 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 PrintWriter getWriter() throws IOException 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. diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java index 5a391acf6554..65cf9358fc5c 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextHandler.java @@ -73,7 +73,6 @@ 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; diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java index 0fc1a7a350b8..6ed7aa8974a3 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletContextResponse.java @@ -23,7 +23,6 @@ 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 void included() 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) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/EncodingHttpWriter.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/EncodingHttpWriter.java deleted file mode 100644 index 667480e18584..000000000000 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/EncodingHttpWriter.java +++ /dev/null @@ -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; - } - } -} diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/HttpWriter.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/HttpWriter.java deleted file mode 100644 index 2c7bb821d253..000000000000 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/HttpWriter.java +++ /dev/null @@ -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(); - } -} diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/Iso88591HttpWriter.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/Iso88591HttpWriter.java deleted file mode 100644 index 08254de5342b..000000000000 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/Iso88591HttpWriter.java +++ /dev/null @@ -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; - } - } -} diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/Utf8HttpWriter.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/Utf8HttpWriter.java deleted file mode 100644 index 876efb83041e..000000000000 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/writer/Utf8HttpWriter.java +++ /dev/null @@ -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; - } - } -} diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/EncodingHttpWriter.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/EncodingHttpWriter.java deleted file mode 100644 index 7609f6d67a2d..000000000000 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/EncodingHttpWriter.java +++ /dev/null @@ -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; - } - } -} diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpWriter.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpWriter.java deleted file mode 100644 index f5a7d91ef1b7..000000000000 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpWriter.java +++ /dev/null @@ -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(); - } -} diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Iso88591HttpWriter.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Iso88591HttpWriter.java deleted file mode 100644 index 6186baeb0794..000000000000 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Iso88591HttpWriter.java +++ /dev/null @@ -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; - } - } -} diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java index 893e68726610..e6611b5ce269 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java @@ -51,6 +51,7 @@ 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 PrintWriter getWriter() throws IOException 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 void completeOutput() throws IOException public void completeOutput(Callback callback) { if (_outputType == OutputType.WRITER) - _writer.complete(callback); - else - _out.complete(callback); + _writer.markAsClosed(); + _out.complete(callback); } public long getLongContentLength() diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResponseWriter.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResponseWriter.java index 775ba628bf63..f0713a66cf69 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResponseWriter.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/ResponseWriter.java @@ -22,7 +22,7 @@ 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 @@ protected void reopen() { _isClosed = false; clearError(); - out = _httpWriter; + out = _writer; } } @@ -164,13 +164,15 @@ public void close() } } - 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 diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Utf8HttpWriter.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Utf8HttpWriter.java deleted file mode 100644 index 2b56c039899e..000000000000 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Utf8HttpWriter.java +++ /dev/null @@ -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; - } - } -} diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java index 438a458c3c22..ea48630b44de 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java @@ -318,7 +318,7 @@ public void testAssumedCharset() throws Exception 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 void testLocaleAndContentTypeEncoding() throws Exception 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