com.jetdrone.vertx.yoke.extras.middleware.Jersey Maven / Gradle / Ivy
package com.jetdrone.vertx.yoke.extras.middleware;
import static org.vertx.java.core.http.HttpHeaders.CONTENT_LENGTH;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.core.SecurityContext;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.internal.MapPropertiesDelegate;
import org.glassfish.jersey.internal.inject.ReferencingFactory;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.process.internal.RequestScoped;
import org.glassfish.jersey.server.ApplicationHandler;
import org.glassfish.jersey.server.ContainerException;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ContainerResponse;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.internal.ConfigHelper;
import org.glassfish.jersey.server.spi.Container;
import org.glassfish.jersey.server.spi.ContainerLifecycleListener;
import org.glassfish.jersey.server.spi.ContainerResponseWriter;
import org.glassfish.jersey.server.spi.RequestScopedInitializer;
import org.vertx.java.core.Handler;
import org.vertx.java.core.Vertx;
import org.vertx.java.core.buffer.Buffer;
import org.vertx.java.core.http.HttpHeaders;
import org.vertx.java.core.logging.Logger;
import org.vertx.java.core.logging.impl.LoggerFactory;
import com.jetdrone.vertx.yoke.Middleware;
import com.jetdrone.vertx.yoke.Yoke;
import com.jetdrone.vertx.yoke.middleware.YokeRequest;
import com.jetdrone.vertx.yoke.middleware.YokeResponse;
/**
*
* A middleware that can forward requests to JAX-RS annotated resources.
*
*
* Use it as the last middleware and do not use it with a body consuming middleware!
*
*
* Don't forget to add Jersey to your project's classpath! For Maven, this would be done with
* this addition to the POM:
*
*
*
* {@code
*
* org.glassfish.jersey.core
* jersey-server
* 2.11
*
* }
*
*
* Example usage:
*
*
*
* {@code
* yoke.use(new ErrorHandler(false))
* .use(new Timeout(10000L))
* .use(new Logger())
* .use(new Favicon())
* .use(new Router().get("/other/", r -> handleGetOther(r)))
* .use(new Jersey()
* .withPackages("com.acme.jaxrs.resources")
* .withInjectables(dependency1, dependency2));
* }
*
*
* The context classes available in the JAX-RS resources are: {@link YokeRequest},
* {@link YokeResponse}, {@link Vertx} and {@link org.vertx.java.platform.Container} (only if
* provided with {@link #withVertxContainer(org.vertx.java.platform.Container)}).
*
*
* Any object provided with {@link #withInjectables(Object...)} will be available via a standard
* {@link javax.inject.Inject} annotation.
*
*
* Implementation inspired by Jersey's SimpleContainer and Englishtown's Jersey Mod.
*
*/
public class Jersey extends Middleware implements Container
{
private static final Logger LOGGER = LoggerFactory.getLogger(Jersey.class);
private static class YokeOutputStream extends OutputStream
{
final YokeResponse response;
Buffer buffer = new Buffer();
boolean isClosed;
private YokeOutputStream(final YokeResponse response)
{
this.response = response;
}
@Override
public void write(final int b) throws IOException
{
checkState();
buffer.appendByte((byte) b);
}
@Override
public void write(final byte[] b) throws IOException
{
checkState();
buffer.appendBytes(b);
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException
{
checkState();
if (off == 0 && len == b.length)
{
buffer.appendBytes(b);
}
else
{
buffer.appendBytes(Arrays.copyOfRange(b, off, off + len));
}
}
@Override
public void flush() throws IOException
{
checkState();
// Only flush to underlying very.x response if the content-length has been set
if (buffer.length() > 0 && response.headers().contains(CONTENT_LENGTH))
{
response.write(buffer);
buffer = new Buffer();
}
}
@Override
public void close() throws IOException
{
// Write any remaining buffer to the vert.x response
// Set content-length if not set yet
if (buffer != null && buffer.length() > 0)
{
if (!response.headers().contains(HttpHeaders.CONTENT_LENGTH))
{
response.headers().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(buffer.length()));
}
response.write(buffer);
}
buffer = null;
isClosed = true;
}
void checkState()
{
if (isClosed)
{
throw new IllegalStateException("Stream is closed");
}
}
}
private static class YokeChunkedOutputStream extends OutputStream
{
private final YokeResponse response;
private boolean isClosed;
private YokeChunkedOutputStream(final YokeResponse response)
{
this.response = response;
}
@Override
public void write(final int b) throws IOException
{
checkState();
final Buffer buffer = new Buffer();
buffer.appendByte((byte) b);
response.write(buffer);
}
@Override
public void write(final byte[] b) throws IOException
{
checkState();
response.write(new Buffer(b));
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException
{
checkState();
final Buffer buffer = new Buffer();
if (off == 0 && len == b.length)
{
buffer.appendBytes(b);
}
else
{
buffer.appendBytes(Arrays.copyOfRange(b, off, off + len));
}
response.write(buffer);
}
@Override
public void close() throws IOException
{
isClosed = true;
}
void checkState()
{
if (isClosed)
{
throw new IllegalStateException("Stream is closed");
}
}
}
private static class YokePrincipal implements Principal
{
private final String user;
public YokePrincipal(final String user)
{
if (user == null)
{
throw new IllegalArgumentException("user can't be null");
}
this.user = user;
}
@Override
public String getName()
{
return user;
}
@Override
public String toString()
{
return getClass().getSimpleName() + ":" + user;
}
@Override
public int hashCode()
{
return 31 + ((user == null) ? 0 : user.hashCode());
}
@Override
public boolean equals(final Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final YokePrincipal other = (YokePrincipal) obj;
if (user == null)
{
if (other.user != null) return false;
}
else if (!user.equals(other.user)) return false;
return true;
}
}
private static final Type YOKE_REQUEST_TYPE = (new TypeLiteral>()
{
}).getType();
private static final Type YOKE_RESPONSE_TYPE = (new TypeLiteral>()
{
}).getType();
private static final Type VERTX_TYPE = (new TypeLiteral>()
{
}).getType();
private static final Type CONTAINER_TYPE = (new TypeLiteral>()
{
}).getType();
private static class YokeRequestReferencingFactory extends ReferencingFactory
{
@Inject
public YokeRequestReferencingFactory(final Provider> referenceFactory)
{
super(referenceFactory);
}
}
private static class YokeResponseReferencingFactory extends ReferencingFactory
{
@Inject
public YokeResponseReferencingFactory(final Provider> referenceFactory)
{
super(referenceFactory);
}
}
private static class VertxReferencingFactory extends ReferencingFactory
{
@Inject
public VertxReferencingFactory(final Provider> referenceFactory)
{
super(referenceFactory);
}
}
private static class ContainerReferencingFactory extends
ReferencingFactory
{
@Inject
public ContainerReferencingFactory(final Provider> referenceFactory)
{
super(referenceFactory);
}
}
private static class YokeBinder extends AbstractBinder
{
@Override
protected void configure()
{
bindFactory(YokeRequestReferencingFactory.class).to(YokeRequest.class)
.proxy(true)
.proxyForSameScope(false)
.in(RequestScoped.class);
bindFactory(ReferencingFactory. referenceFactory()).to(
new TypeLiteral>()
{
}).in(RequestScoped.class);
bindFactory(YokeResponseReferencingFactory.class).to(YokeResponse.class)
.proxy(true)
.proxyForSameScope(false)
.in(RequestScoped.class);
bindFactory(ReferencingFactory. referenceFactory()).to(
new TypeLiteral>()
{
}).in(RequestScoped.class);
bindFactory(VertxReferencingFactory.class).to(Vertx.class)
.proxy(true)
.proxyForSameScope(false)
.in(RequestScoped.class);
bindFactory(ReferencingFactory. referenceFactory()).to(new TypeLiteral>()
{
}).in(RequestScoped.class);
bindFactory(ContainerReferencingFactory.class).to(org.vertx.java.platform.Container.class)
.proxy(true)
.proxyForSameScope(false)
.in(RequestScoped.class);
bindFactory(ReferencingFactory. referenceFactory()).to(
new TypeLiteral>()
{
}).in(RequestScoped.class);
}
}
private static final class YokeResponseWriter implements ContainerResponseWriter
{
private final YokeResponse response;
private final Vertx vertx;
private TimeoutHandler timeoutHandler;
private long suspendTimerId;
public YokeResponseWriter(final YokeResponse response, final Vertx vertx)
{
this.response = response;
this.vertx = vertx;
this.suspendTimerId = 0;
}
@Override
public OutputStream writeResponseStatusAndHeaders(final long contentLength,
final ContainerResponse responseContext)
throws ContainerException
{
response.setStatusCode(responseContext.getStatus());
response.setStatusMessage(responseContext.getStatusInfo().getReasonPhrase());
if (contentLength != -1)
{
response.putHeader(CONTENT_LENGTH, Long.toString(contentLength));
}
for (final Entry> header : responseContext.getStringHeaders().entrySet())
{
for (final String value : header.getValue())
{
response.putHeader(header.getKey(), value);
}
}
if (responseContext.isChunked())
{
response.setChunked(true);
return new YokeChunkedOutputStream(response);
}
else
{
return new YokeOutputStream(response);
}
}
@Override
public void commit()
{
endResponse(response);
}
@Override
public void failure(final Throwable t)
{
LOGGER.error(t.getMessage(), t);
try
{
response.setStatusCode(500);
response.setStatusMessage("Internal Server Error");
response.end();
}
catch (final Exception e)
{
LOGGER.error("Failed to write failure response", e);
}
}
@Override
public boolean suspend(final long timeOut,
final TimeUnit timeUnit,
final TimeoutHandler timeoutHandler)
{
if (timeoutHandler == null)
{
throw new IllegalArgumentException("TimeoutHandler can't be null");
}
// if already suspended should return false according to documentation
if (this.timeoutHandler != null)
{
return false;
}
this.timeoutHandler = timeoutHandler;
doSuspend(timeOut, timeUnit);
return true;
}
@Override
public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit)
throws IllegalStateException
{
if (timeoutHandler == null)
{
throw new IllegalStateException("Request not currently suspended");
}
if (suspendTimerId != 0)
{
vertx.cancelTimer(suspendTimerId);
}
doSuspend(timeOut, timeUnit);
}
private void doSuspend(final long timeOut, final TimeUnit timeUnit)
{
// if timeout <= 0, then it suspends indefinitely
if (timeOut <= 0)
{
return;
}
final long ms = timeUnit.toMillis(timeOut);
suspendTimerId = vertx.setTimer(ms, new Handler()
{
@Override
public void handle(final Long $)
{
YokeResponseWriter.this.timeoutHandler.onTimeout(YokeResponseWriter.this);
}
});
}
@Override
public boolean enableResponseBuffering()
{
return false;
}
}
private final ResourceConfig resourceConfig;
private org.vertx.java.platform.Container vertxContainer;
private ApplicationHandler applicationHandler;
private ContainerLifecycleListener containerListener;
public Jersey()
{
resourceConfig = new ResourceConfig();
}
public Jersey withVertxContainer(final org.vertx.java.platform.Container vertxContainer)
{
this.vertxContainer = vertxContainer;
return this;
}
public Jersey withPackages(final String... packages)
{
resourceConfig.packages(packages);
return this;
}
public Jersey withClasses(final Class>... classes)
{
resourceConfig.registerClasses(classes);
return this;
}
public Jersey withInstances(final Object... instances)
{
resourceConfig.registerInstances(instances);
return this;
}
public Jersey withName(final String name)
{
resourceConfig.setApplicationName(name);
return this;
}
public Jersey withProperty(final String name, final Object value)
{
resourceConfig.property(name, value);
return this;
}
public Jersey withInjectables(final Object... instances)
{
resourceConfig.register(new AbstractBinder()
{
@SuppressWarnings("unchecked")
@Override
protected void configure()
{
for (final Object instance : instances)
{
bind(instance).to((Class