All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.peterphi.std.guice.web.rest.resteasy.GuicedResteasy Maven / Gradle / Ivy

There is a newer version: 10.1.5
Show newest version
package com.peterphi.std.guice.web.rest.resteasy;

import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;
import com.peterphi.std.guice.apploader.GuiceApplication;
import com.peterphi.std.guice.apploader.GuiceProperties;
import com.peterphi.std.guice.apploader.impl.GuiceRegistry;
import com.peterphi.std.guice.common.metrics.GuiceMetricNames;
import com.peterphi.std.guice.restclient.jaxb.RestFailure;
import com.peterphi.std.guice.restclient.resteasy.impl.JAXBContextResolver;
import com.peterphi.std.guice.serviceregistry.rest.RestResource;
import com.peterphi.std.guice.serviceregistry.rest.RestResourceRegistry;
import com.peterphi.std.guice.web.HttpCallContext;
import com.peterphi.std.guice.web.rest.jaxrs.exception.JAXRSExceptionMapper;
import com.peterphi.std.guice.web.rest.jaxrs.exception.RestFailureMarshaller;
import com.peterphi.std.guice.web.rest.pagewriter.TwitterBootstrapRestFailurePageRenderer;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
import org.jboss.resteasy.plugins.server.servlet.ListenerBootstrap;
import org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher;
import org.jboss.resteasy.spi.Registry;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.UnhandledException;

import javax.servlet.FilterConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.NotFoundException;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A resteasy dispatcher (that can be brought up/down) that brings a Guice-enabled environment to Resteasy
 */
class GuicedResteasy implements GuiceApplication
{
	private static final Logger log = Logger.getLogger(GuicedResteasy.class);

	private final GuiceRegistry registry;
	private final AtomicBoolean registered = new AtomicBoolean(false);
	private final FilterConfig filterConfig;
	private final ServletConfig servletConfig;
	private final ServletContext context;
	private final ListenerBootstrap bootstrap;
	private final boolean handleNotFoundException;

	// Lazy-created on first call
	private ServletContainerDispatcher dispatcher;

	@Inject
	private JAXRSExceptionMapper exceptionMapper;

	@Inject
	private JAXBContextResolver jaxbContextResolver;

	@Inject
	private Injector injector;

	private Timer httpCalls;
	private Meter httpExceptions;
	private Meter httpNotFoundExceptions;
	private Meter ignoredAborts;

	@Inject(optional = true)
	@Named(GuiceProperties.SUPPRESS_CLIENT_ABORT_EXCEPTIONS)
	boolean suppressClientAbortExceptions = true;

	@Inject(optional = true)
	@Named(GuiceProperties.HTTP_REQUESTS_DEFAULT_TO_UTF_8)
	boolean forceUTF8DefaultCharset = true;

	/**
	 * Allow to be overridden but have a default implementation
	 */
	@Inject(optional = true)
	DefaultHttpRequestCharsetHelper requestCharsetHelper = new DefaultHttpRequestCharsetHelper();


	public GuicedResteasy(final GuiceRegistry registry,
	                      final ServletConfig config,
	                      final ListenerBootstrap bootstrap,
	                      final boolean handleNotFoundException)
	{
		this.registry = registry;
		this.filterConfig = null;
		this.servletConfig = config;
		this.context = config.getServletContext();
		this.bootstrap = bootstrap;
		this.handleNotFoundException = handleNotFoundException;
	}


	public GuicedResteasy(final GuiceRegistry registry,
	                      final FilterConfig config,
	                      final ListenerBootstrap bootstrap,
	                      final boolean handleNotFoundException)
	{
		this.registry = registry;
		this.filterConfig = config;
		this.servletConfig = null;
		this.context = config.getServletContext();
		this.bootstrap = bootstrap;
		this.handleNotFoundException = handleNotFoundException;
	}


	/**
	 * Return the path of the local webapp (if known)
	 *
	 * @return
	 */
	public String getWebappPath()
	{
		return context.getContextPath();
	}


	public void initialise() throws ServletException
	{
		// Try to start guice (unless already started)
		getDispatcher();
	}


	private synchronized ServletContainerDispatcher getDispatcher() throws ServletException
	{
		if (!registered.get())
		{
			dispatcher = new ServletContainerDispatcher();

			configure(dispatcher);

			registered.set(true);
		}

		return dispatcher;
	}


	public void call(HttpServletRequest request,
	                 HttpServletResponse response) throws ServletException, IOException, NotFoundException
	{
		final HttpCallContext ctx = HttpCallContext.set(request, response, context);

		if (forceUTF8DefaultCharset && requestCharsetHelper != null)
			requestCharsetHelper.applyDefaultCharset(request);

		Timer.Context timer = null;

		if (httpCalls != null)
			timer = httpCalls.time();

		try
		{
			// Share the call id to log4j
			MDC.put("call.id", ctx.getLogId());
			MDC.put("remote_addr", ctx.getRequest().getRemoteAddr());
			MDC.put("context_path", ctx.getServletContext().getContextPath());

			// Add the session id (if present)
			final HttpSession session = ctx.getRequest().getSession(false);
			if (session != null)
			{
				MDC.put("session_id", session.getId());
			}

			try
			{
				// Optionally log the request
				if (log.isDebugEnabled())
					log.debug(ctx.getRequestInfo());

				// Get or create the dispatcher
				ServletContainerDispatcher dispatcher = getDispatcher();

				dispatcher.service(request.getMethod(), request, response, handleNotFoundException);
			}
			catch (NotFoundException e)
			{
				if (httpNotFoundExceptions != null)
					httpNotFoundExceptions.mark();

				throw e; // let the caller handle this
			}
			catch (ServletException | IOException | RuntimeException | Error e)
			{
				if (httpExceptions != null)
					httpExceptions.mark();

				tryHandleException(ctx, response, e); // try to pretty print, otherwise rethrow
			}
		}
		finally
		{
			if (timer != null)
				timer.stop();

			HttpCallContext.clear();
			MDC.clear();
		}
	}


	/**
	 * @param ctx
	 * @param response
	 * @param t
	 *
	 * @throws ServletException
	 * @throws IOException
	 * @throws RuntimeException
	 * @throws Error
	 */
	private void tryHandleException(HttpCallContext ctx,
	                                HttpServletResponse response,
	                                Throwable t) throws ServletException, IOException, RuntimeException, Error
	{
		if (suppressClientAbortExceptions &&
		    t instanceof UnhandledException &&
		    t.getCause() != null)
		{
			// Only look 20 exceptions deep
			int maxDepth = 20;
			Throwable cause = t.getCause();

			while (cause != null && --maxDepth > 0)
			{
				if (cause.getClass().getName().equals("org.apache.catalina.connector.ClientAbortException"))
				{
					ignoredAborts.mark();

					if (log.isTraceEnabled())
						log.trace("Client aborted during request. Ignoring. Detail: " + ctx.getRequestInfo(), t);

					return;
				}
				else
				{
					cause = cause.getCause();
				}
			}
		}

		log.warn("Failure during " + ctx.getRequestInfo(), t);

		// If the response is already committed we can't render the exception elegantly
		if (response.isCommitted())
			rethrow(t);

		try
		{
			RestFailureMarshaller marshaller = new RestFailureMarshaller();
			RestFailure failure = marshaller.renderFailure(t);

			TwitterBootstrapRestFailurePageRenderer renderer = new TwitterBootstrapRestFailurePageRenderer(failure);

			response.setStatus(500); // internal error

			// Render the HTML
			final StringBuilder sb = new StringBuilder(4096);
			renderer.writeHTML(sb);

			response.getWriter().print(sb);
		}
		catch (Throwable newException)
		{
			log.warn("Error trying to present exception elegantly: ", newException);

			// Rethrow the original exception, it's not our problem anymore
			rethrow(t);
		}
	}


	private void rethrow(Throwable t) throws ServletException, IOException, RuntimeException, Error
	{
		// Rethrow (ugly code, unfortunately)
		if (t instanceof RuntimeException)
			throw (RuntimeException) t;
		else if (t instanceof Error)
			throw (Error) t;
		else if (t instanceof IOException)
			throw (IOException) t;
		else if (t instanceof ServletException)
			throw (ServletException) t;
		else
			throw new RuntimeException(t);
	}


	/**
	 * Try to initialise a ServletContainerDispatcher with the connection to the Guice REST services
	 */
	protected void configure(ServletContainerDispatcher dispatcher) throws ServletException
	{
		// Make sure we are registered with the Guice registry
		registry.register(this, true);

		// Configure the dispatcher
		final Registry resteasyRegistry;
		final ResteasyProviderFactory providerFactory;
		{
			final ResteasyRequestResponseFactory converter = new ResteasyRequestResponseFactory(dispatcher);

			dispatcher.init(context, bootstrap, converter, converter);

			if (filterConfig != null)
				dispatcher.getDispatcher().getDefaultContextObjects().put(FilterConfig.class, filterConfig);
			if (servletConfig != null)
				dispatcher.getDispatcher().getDefaultContextObjects().put(ServletConfig.class, servletConfig);

			resteasyRegistry = dispatcher.getDispatcher().getRegistry();
			providerFactory = dispatcher.getDispatcher().getProviderFactory();
		}


		// Register the REST provider classes
		for (Class providerClass : ResteasyProviderRegistry.getClasses())
		{
			log.debug("Registering REST providers: " + providerClass.getName());
			providerFactory.registerProvider(providerClass);
		}

		// Register the REST provider singletons
		for (Object provider : ResteasyProviderRegistry.getSingletons())
		{
			log.debug("Registering REST provider singleton: " + provider);
			providerFactory.registerProviderInstance(provider);
		}

		// Register the JAXBContext provider
		providerFactory.registerProviderInstance(jaxbContextResolver);

		// Register the exception mapper
		{
			// Register the ExceptionMapper for ApplicationException
			providerFactory.register(this.exceptionMapper);

			log.trace("ExceptionMapper registered for ApplicationException");
		}

		// Register the REST resources
		for (RestResource resource : RestResourceRegistry.getResources())
		{
			log.debug("Registering REST resource: " + resource.getResourceClass().getName());

			resteasyRegistry.addResourceFactory(new ResteasyGuiceResource(injector, resource.getResourceClass()));
		}
	}


	@Override
	public void configured()
	{
		// Called when guice config is complete
		log.trace("Guice injector online");

		final MetricRegistry registry = injector.getInstance(MetricRegistry.class);
		this.httpNotFoundExceptions = registry.meter(GuiceMetricNames.HTTP_404_EXCEPTIONS_METER);
		this.httpExceptions = registry.meter(GuiceMetricNames.HTTP_EXCEPTIONS_METER);
		this.httpCalls = registry.timer(GuiceMetricNames.HTTP_CALLS_TIMER);
		this.ignoredAborts = registry.meter(GuiceMetricNames.HTTP_IGNORED_CLIENT_ABORTS);
	}


	@Override
	public void stopping()
	{
		log.trace("Guice injector stopping...");
		registered.set(false);

		if (dispatcher != null)
		{
			dispatcher.destroy();
			dispatcher = null;
		}
	}


	public void stop()
	{
		registry.stop();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy