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