Skip to content

Commit

Permalink
Merge pull request #2921 from agentgt/jstachio_pre_encode
Browse files Browse the repository at this point in the history
Add Rocker style pre-encoding for JStachio
  • Loading branch information
jknack committed May 19, 2023
2 parents 61dc762 + d556782 commit 12f3ba4
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.jstachio;

import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
* This is basically the same as Rockers byte buffer but as an OutputStream because JStachio wants
* that interface. Currently it is internal.
*
* @author agentgt
*/
class ByteBufferedOutputStream extends OutputStream {

/** Default buffer size: <code>4k</code>. */
public static final int BUFFER_SIZE = 4096;

/**
* The maximum size of array to allocate. Some VMs reserve some header words in an array. Attempts
* to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/** The buffer where data is stored. */
protected byte[] buf;

/** The number of valid bytes in the buffer. */
protected int count;

ByteBufferedOutputStream(int bufferSize) {
this.buf = new byte[bufferSize];
}

void reset() {
count = 0;
}

@Override
public void close() {
this.reset();
}

@Override
public void write(byte[] bytes) {
int len = bytes.length;
ensureCapacity(count + len);
System.arraycopy(bytes, 0, buf, count, len);
count += len;
}

public int size() {
return count;
}

/**
* Copy internal byte array into a new array.
*
* @return Byte array.
*/
public byte[] toByteArray() {
byte[] array = new byte[count];
System.arraycopy(buf, 0, array, 0, count);
return array;
}

/**
* Get a view of the byte buffer.
*
* @return Byte buffer.
*/
public ByteBuffer toBuffer() {
return ByteBuffer.wrap(buf, 0, count);
}

private void ensureCapacity(int minCapacity) {
// overflow-conscious code
if (minCapacity - buf.length > 0) {
grow(minCapacity);
}
}

/**
* Increases the capacity to ensure that it can hold at least the number of elements specified by
* the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = buf.length;
int newCapacity = oldCapacity << 1;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
buf = Arrays.copyOf(buf, 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 write(int b) {
throw new UnsupportedOperationException("expecting only write(byte[])");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.jstachio;

/**
* To provide Rocker like buffer support
*
* @author agentgt
*/
interface JStachioBuffer {

public ByteBufferedOutputStream acquire();

public void release(ByteBufferedOutputStream buffer);

static JStachioBuffer of(int bufferSize, boolean reuseBuffer) {
if (reuseBuffer) {
return new ReuseBuffer(bufferSize);
} else {
return new NoReuseBuffer(bufferSize);
}
}
}

record NoReuseBuffer(int bufferSize) implements JStachioBuffer {
@Override
public ByteBufferedOutputStream acquire() {
return new ByteBufferedOutputStream(bufferSize);
}

@Override
public void release(ByteBufferedOutputStream buffer) {}
}

class ReuseBuffer implements JStachioBuffer {
private final ThreadLocal<ByteBufferedOutputStream> localBuffer;

public ReuseBuffer(int bufferSize) {
super();
this.localBuffer = ThreadLocal.withInitial(() -> new ByteBufferedOutputStream(bufferSize));
}

@Override
public ByteBufferedOutputStream acquire() {
return localBuffer.get();
}

@Override
public void release(ByteBufferedOutputStream buffer) {
buffer.reset();
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,56 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2023 Edgar Espina
* Copyright 2014 Edgar Espina
*/
package io.jooby.jstachio;

import java.io.IOException;

import io.jooby.Context;
import io.jooby.MediaType;
import io.jooby.Route;
import io.jooby.Route.Handler;
import io.jstach.jstachio.JStachio;
import io.jstach.jstachio.Template;

class JStachioHandler extends JStachioRenderer<Context> implements Route.Filter {

class JStachioHandler implements Route.Filter {

private final JStachioMessageEncoder encoder;

public JStachioHandler(JStachioMessageEncoder encoder) {
super();
this.encoder = encoder;
public JStachioHandler(JStachio jstachio, JStachioBuffer buffer) {
super(jstachio, buffer);
}

@Override
public Handler apply(Handler next) {
return ctx -> {
try {
Object model = next.apply(ctx);
ctx.setResponseType(MediaType.html);
return ctx.send(encoder.render(model));
Object model = next.apply(ctx);
return render(ctx, model);
} catch (Throwable x) {
ctx.sendError(x);
return x;
}
};
}

@SuppressWarnings("unchecked")
@Override
Context render(
Context ctx,
@SuppressWarnings("rawtypes") Template template,
Object model,
ByteBufferedOutputStream stream)
throws IOException {
ctx.setResponseType(MediaType.html);
template.write(model, stream);
/*
* Rocker used a byte buffer here BUT it just wraps the internal buffer in the stream
* instead of copying.
*
* Which is good for performance but bad if the ctx.send call is not blocking aka
* hand the buffer off to another thread.
*/
ctx.send(stream.toBuffer());
return ctx;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,39 @@
*/
package io.jooby.jstachio;

import java.nio.charset.Charset;
import java.io.IOException;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.Context;
import io.jooby.MessageEncoder;
import io.jstach.jstachio.JStachio;
import io.jstach.jstachio.Template;

/*
* Should this be public?
*/
class JStachioMessageEncoder implements MessageEncoder {
class JStachioMessageEncoder extends JStachioRenderer<byte[]> implements MessageEncoder {

private final JStachio jstachio;
private final Charset charset;
private final int bufferSize;

public JStachioMessageEncoder(
@NonNull JStachio jstachio, @NonNull Charset charset, int bufferSize) {
super();
this.jstachio = jstachio;
this.charset = charset;
this.bufferSize = bufferSize;
public JStachioMessageEncoder(JStachio jstachio, JStachioBuffer buffer) {
super(jstachio, buffer);
}

@Override
public byte[] encode(Context ctx, Object value) throws Exception {
if (supportsType(value.getClass())) {
return render(value);
return render(ctx, value);
}
return null;
}

protected boolean supportsType(Class<?> modelClass) {
return jstachio.supportsType(modelClass);
}

protected byte[] render(Object value) {
StringBuilder b = acquireBuffer();
try {
jstachio.execute(value, b);
return b.toString().getBytes(charset);
} finally {
releaseBuffer(b);
}
}

/**
* Returns a new buffer. {@link #releaseBuffer(StringBuilder)) should be
* called when done.
*
* @return a buffer either from a pool or a new one.
*/
protected StringBuilder acquireBuffer() {
return new StringBuilder(bufferSize);
}

/**
* Releases the buffer. Override for pooling buffers.
*
* @param sb
*/
protected void releaseBuffer(StringBuilder sb) {
return;
@SuppressWarnings("unchecked")
@Override
byte[] render(
Context ctx,
@SuppressWarnings("rawtypes") Template template,
Object model,
ByteBufferedOutputStream stream)
throws IOException {
template.write(model, stream);
return stream.toByteArray();
}
}
Loading

0 comments on commit 12f3ba4

Please sign in to comment.