Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON handling improvements #609

Merged
merged 3 commits into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions media/common/src/main/java/io/helidon/media/common/CharBuffer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.media.common;

import java.io.Writer;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
* A character buffer that acts as a {@link Writer} and uses cached {@code char[]} arrays.
* <p>
* Instances of this class are <em>not</em> thread-safe.
*/
public class CharBuffer extends Writer {
private static final Pool POOL = new Pool(8192);
private char[] buffer;
private int count;

/**
* Constructor.
*/
public CharBuffer() {
buffer = POOL.acquire();
count = 0;
}

@Override
public void write(char[] cbuf, int off, int len) {
if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) - cbuf.length > 0)) {
throw new IndexOutOfBoundsException();
}
ensureCapacity(count + len);
System.arraycopy(cbuf, off, buffer, count, len);
count += len;
}

/**
* Returns the number of characters written.
*
* @return The count.
*/
int size() {
return count;
}

/**
* Returns the content encoded into the given character set.
*
* @param charset The character set.
* @return The encoded content.
*/
ByteBuffer encode(Charset charset) {
final ByteBuffer result = charset.encode(java.nio.CharBuffer.wrap(buffer, 0, count));
POOL.release(buffer);
buffer = null;
return result;
}

private void ensureCapacity(int minCapacity) {
if (minCapacity - buffer.length > 0) {
grow(minCapacity);
}
}

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
int oldCapacity = buffer.length;
int newCapacity = oldCapacity << 1;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
buffer = Arrays.copyOf(buffer, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}

@Override
public void flush() {
}

@Override
public void close() {
}

private static class Pool {
private volatile SoftReference<ConcurrentLinkedQueue<char[]>> reference;
private final int arraySize;

/**
* Constructor.
*
* @param arraySize The size array to allocate when required.
*/
Pool(final int arraySize) {
this.arraySize = arraySize;
}

/**
* Acquires an array from the pool if available or creates a new one.
*
* @return The array.
*/
char[] acquire() {
final char[] array = getQueue().poll();
return array == null ? new char[arraySize] : array;
}

/**
* Returns an array back to the pool.
*
* @param array The array to return.
*/
void release(final char[] array) {
getQueue().offer(array);
}

private ConcurrentLinkedQueue<char[]> getQueue() {
final SoftReference<ConcurrentLinkedQueue<char[]>> reference = this.reference;
if (reference != null) {
final ConcurrentLinkedQueue<char[]> queue = reference.get();
if (queue != null) {
return queue;
}
}
final ConcurrentLinkedQueue<char[]> queue = new ConcurrentLinkedQueue<>();
this.reference = new SoftReference<>(queue);
return queue;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.media.common;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import io.helidon.common.http.Http;
import io.helidon.common.http.MediaType;
import io.helidon.common.http.Parameters;

/**
* Accessor for the {@link Charset} specified by a content-type header.
*/
public class ContentTypeCharset {

/**
* Returns the {@link Charset} specified in the content-type header, using {@link StandardCharsets#UTF_8}
* as the default.
*
* @param headers The headers.
* @return The charset.
*/
public static Charset determineCharset(Parameters headers) {
return determineCharset(headers, StandardCharsets.UTF_8);
}

/**
* Returns the {@link Charset} specified in the content type header. If not provided or an error occurs on lookup,
* the given default is returned.
*
* @param headers The headers.
* @param defaultCharset The default.
* @return The charset.
*/
public static Charset determineCharset(Parameters headers, Charset defaultCharset) {
return headers.first(Http.Header.CONTENT_TYPE)
.map(MediaType::parse)
.flatMap(MediaType::charset)
.map(sch -> {
try {
return Charset.forName(sch);
} catch (Exception e) {
return null;
}
})
.orElse(defaultCharset);
}

private ContentTypeCharset() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,29 @@ public final class ContentWriters {
private static final ByteArrayWriter COPY_BYTE_ARRAY_WRITER = new ByteArrayWriter(true);
private static final ByteArrayWriter BYTE_ARRAY_WRITER = new ByteArrayWriter(false);

private static final Map<Charset, CharSequenceWriter> STRING_WRITERS = new HashMap<>();
private static final Map<Charset, CharSequenceWriter> CHAR_SEQUENCE_WRITERS = new HashMap<>();
private static final Map<Charset, CharBufferWriter> CHAR_BUFFER_WRITERS = new HashMap<>();

static {
addWriter(StandardCharsets.UTF_8);
addWriter(StandardCharsets.UTF_16);
addWriter(StandardCharsets.ISO_8859_1);
addWriter(StandardCharsets.US_ASCII);
addWriters(StandardCharsets.UTF_8);
addWriters(StandardCharsets.UTF_16);
addWriters(StandardCharsets.ISO_8859_1);
addWriters(StandardCharsets.US_ASCII);

// try to register another common charset readers
addWriter("cp1252");
addWriter("cp1250");
addWriter("ISO-8859-2");
addWriters("cp1252");
addWriters("cp1250");
addWriters("ISO-8859-2");
}

private static void addWriter(Charset charset) {
STRING_WRITERS.put(charset, new CharSequenceWriter(charset));
private static void addWriters(final Charset charset) {
CHAR_SEQUENCE_WRITERS.put(charset, new CharSequenceWriter(charset));
CHAR_BUFFER_WRITERS.put(charset, new CharBufferWriter(charset));
}

private static void addWriter(String charset) {
private static void addWriters(final String charset) {
try {
addWriter(Charset.forName(charset));
addWriters(Charset.forName(charset));
} catch (Exception ignored) {
// ignored
}
Expand Down Expand Up @@ -98,7 +100,20 @@ public static Function<byte[], Flow.Publisher<DataChunk>> byteArrayWriter(boolea
* @throws NullPointerException if parameter {@code charset} is {@code null}
*/
public static Function<CharSequence, Flow.Publisher<DataChunk>> charSequenceWriter(Charset charset) {
return STRING_WRITERS.computeIfAbsent(charset, key -> new CharSequenceWriter(charset));
return CHAR_SEQUENCE_WRITERS.computeIfAbsent(charset, key -> new CharSequenceWriter(charset));
}

/**
* Returns a writer function for {@link CharBuffer} using provided standard {@code charset}.
* <p>
* An instance is by default registered in {@code ServerResponse} for all standard charsets.
*
* @param charset a standard charset to use
* @return a {@link String} writer
* @throws NullPointerException if parameter {@code charset} is {@code null}
*/
public static Function<CharBuffer, Flow.Publisher<DataChunk>> charBufferWriter(Charset charset) {
return CHAR_BUFFER_WRITERS.computeIfAbsent(charset, key -> new CharBufferWriter(charset));
}

/**
Expand Down Expand Up @@ -185,4 +200,28 @@ public Flow.Publisher<DataChunk> apply(CharSequence s) {
}
}

private static class CharBufferWriter implements Function<CharBuffer, Flow.Publisher<DataChunk>> {

private final Charset charset;

/**
* Creates new instance.
*
* @param charset a charset to use
* @throws NullPointerException if parameter {@code charset} is {@code null}
*/
CharBufferWriter(Charset charset) {
Objects.requireNonNull(charset, "Parameter 'charset' is null!");
this.charset = charset;
}

@Override
public Flow.Publisher<DataChunk> apply(CharBuffer buffer) {
if (buffer == null || buffer.size() == 0) {
return ReactiveStreamsAdapter.publisherToFlow(Mono.empty());
}
final DataChunk chunk = DataChunk.create(false, buffer.encode(charset));
return ReactiveStreamsAdapter.publisherToFlow(Mono.just(chunk));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@
*/
package io.helidon.media.jackson.common;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.function.Function;

import io.helidon.common.http.DataChunk;
import io.helidon.common.http.MediaType;
import io.helidon.common.http.Reader;
import io.helidon.common.reactive.Flow;
import io.helidon.media.common.CharBuffer;
import io.helidon.media.common.ContentReaders;
import io.helidon.media.common.ContentWriters;

import com.fasterxml.jackson.databind.ObjectMapper;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* Media type support for Jackson.
*/
Expand Down Expand Up @@ -93,21 +96,20 @@ public static Reader<Object> reader(final ObjectMapper objectMapper) {
* of {@link DataChunk}s by using the supplied {@link ObjectMapper}.
*
* @param objectMapper the {@link ObjectMapper} to use; must not be {@code null}
* @param charset the charset to use; may be null
* @return created function
* @exception NullPointerException if {@code objectMapper} is {@code null}
*/
public static Function<Object, Flow.Publisher<DataChunk>> writer(final ObjectMapper objectMapper) {
public static Function<Object, Flow.Publisher<DataChunk>> writer(final ObjectMapper objectMapper, final Charset charset) {
Objects.requireNonNull(objectMapper);
return payload -> {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
CharBuffer buffer = new CharBuffer();
try {
objectMapper.writeValue(baos, payload);
objectMapper.writeValue(buffer, payload);
} catch (final IOException wrapMe) {
throw new JacksonRuntimeException(wrapMe.getMessage(), wrapMe);
}
return ContentWriters.byteArrayWriter(false)
.apply(baos.toByteArray());
return ContentWriters.charBufferWriter(charset == null ? UTF_8 : charset).apply(buffer);
};
}

}
12 changes: 12 additions & 0 deletions media/jackson/server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
Expand Down
Loading