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

org.restexpress.RestExpress Maven / Gradle / Ivy

There is a newer version: 0.11.3
Show newest version
/*
 * Copyright 2009-2012, Strategic Gains, Inc.
 *
 * 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 org.restexpress;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.ChannelGroupFuture;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
import org.restexpress.domain.metadata.ServerMetadata;
import org.restexpress.exception.ExceptionMapping;
import org.restexpress.exception.ServiceException;
import org.restexpress.pipeline.DefaultRequestHandler;
import org.restexpress.pipeline.MessageObserver;
import org.restexpress.pipeline.PipelineBuilder;
import org.restexpress.pipeline.Postprocessor;
import org.restexpress.pipeline.Preprocessor;
import org.restexpress.plugin.Plugin;
import org.restexpress.response.DefaultHttpResponseWriter;
import org.restexpress.route.RouteBuilder;
import org.restexpress.route.RouteDeclaration;
import org.restexpress.route.RouteResolver;
import org.restexpress.route.parameterized.ParameterizedRouteBuilder;
import org.restexpress.route.regex.RegexRouteBuilder;
import org.restexpress.serialization.DefaultSerializationProvider;
import org.restexpress.serialization.SerializationProvider;
import org.restexpress.settings.RouteDefaults;
import org.restexpress.settings.ServerSettings;
import org.restexpress.settings.SocketSettings;
import org.restexpress.util.Bootstraps;
import org.restexpress.util.Callback;
import org.restexpress.util.DefaultShutdownHook;

/**
 * Primary entry point to create a RestExpress service. All that's required is a
 * RouteDeclaration. By default: port is 8081, serialization format is JSON,
 * supported formats are JSON and XML.
 * 
 * @author toddf
 */
public class RestExpress
{
	private static final ChannelGroup allChannels = new DefaultChannelGroup("RestExpress");

	public static final String DEFAULT_NAME = "RestExpress";
	public static final int DEFAULT_PORT = 8081;

	private static SerializationProvider SERIALIZATION_PROVIDER = null;

	private ServerBootstrap bootstrap;
	private SocketSettings socketSettings = new SocketSettings();
	private ServerSettings serverSettings = new ServerSettings();
	private RouteDefaults routeDefaults = new RouteDefaults();
	private boolean enforceHttpSpec = false;
	private boolean useSystemOut;

	private List messageObservers = new ArrayList();
	private List preprocessors = new ArrayList();
	private List postprocessors = new ArrayList();
	private List finallyProcessors = new ArrayList();
	private ExceptionMapping exceptionMap = new ExceptionMapping();
	private List plugins = new ArrayList();
	private RouteDeclaration routeDeclarations = new RouteDeclaration();
	
	/**
	 * Change the default behavior for serialization.
	 * If no SerializationProcessor is set, default of DefaultSerializationProcessor is used,
	 * which uses Jackson for JSON, XStream for XML.
	 * 
	 * @param provider a SerializationProvider instance.
	 */
	public static void setSerializationProvider(SerializationProvider provider)
	{
		SERIALIZATION_PROVIDER = provider;
	}

	public static SerializationProvider getSerializationProvider()
	{
		if (SERIALIZATION_PROVIDER == null)
		{
			SERIALIZATION_PROVIDER = new DefaultSerializationProvider();
		}

		return SERIALIZATION_PROVIDER;
	}

	/**
	 * Create a new RestExpress service. By default, RestExpress uses port 8081.
	 * Supports JSON, and XML, providing JSEND-style wrapped responses. And
	 * displays some messages on System.out. These can be altered with the
	 * setPort(), noJson(), noXml(), noSystemOut(), and useRawResponses() DSL
	 * modifiers, respectively, as needed.
	 * 
	 * 

* The default input and output format for messages is JSON. To change that, * use the setDefaultFormat(String) DSL modifier, passing the format to use * by default. Make sure there's a corresponding SerializationProcessor for * that particular format. The Format class has the basics. * *

* This DSL was created as a thin veneer on Netty functionality. The bind() * method simply builds a Netty pipeline and uses this builder class to * create it. Underneath the covers, RestExpress uses Google GSON for JSON * handling and XStream for XML processing. However, both of those can be * swapped out using the putSerializationProcessor(String, * SerializationProcessor) method, creating your own instance of * SerializationProcessor as necessary. * * @param routes * a RouteDeclaration that declares the URL routes that this * service supports. */ public RestExpress() { super(); setName(DEFAULT_NAME); useSystemOut(); } public String getBaseUrl() { return routeDefaults.getBaseUrl(); } public RestExpress setBaseUrl(String baseUrl) { routeDefaults.setBaseUrl(baseUrl); return this; } /** * Get the name of this RestExpress service. * * @return a String representing the name of this service suite. */ public String getName() { return serverSettings.getName(); } /** * Set the name of this RestExpress service suite. * * @param name * the name. * @return the RestExpress instance to facilitate DSL-style method chaining. */ public RestExpress setName(String name) { serverSettings.setName(name); return this; } public int getPort() { return serverSettings.getPort(); } public RestExpress setPort(int port) { serverSettings.setPort(port); return this; } public RestExpress addMessageObserver(MessageObserver observer) { if (!messageObservers.contains(observer)) { messageObservers.add(observer); } return this; } public List getMessageObservers() { return Collections.unmodifiableList(messageObservers); } /** * Add a Preprocessor instance that gets called before an incoming message * gets processed. Preprocessors get called in the order in which they are * added. To break out of the chain, simply throw an exception. * * @param processor * @return */ public RestExpress addPreprocessor(Preprocessor processor) { if (!preprocessors.contains(processor)) { preprocessors.add(processor); } return this; } public List getPreprocessors() { return Collections.unmodifiableList(preprocessors); } /** * Add a Postprocessor instance that gets called after an incoming message is * processed. A Postprocessor is useful for augmenting or transforming the * results of a controller or adding headers, etc. Postprocessors get called * in the order in which they are added. * Note however, they do NOT get called in the case of an exception or error * within the route. * * @param processor * @return */ public RestExpress addPostprocessor(Postprocessor processor) { if (!postprocessors.contains(processor)) { postprocessors.add(processor); } return this; } public List getPostprocessors() { return Collections.unmodifiableList(postprocessors); } /** * Add a Postprocessor instance that gets called in a finally block after * the message is processed. Finally processors are Postprocessor instances * that are guaranteed to run even if an error is thrown from the controller * or somewhere else in the route. A Finally Processor is useful for adding * headers or transforming results even during error conditions. Finally * processors get called in the order in which they are added. * * If an exception is thrown during finally processor execution, the finally processors * following it are executed after printing a stack trace to the System.err stream. * * @param processor * @return RestExpress for method chaining. */ public RestExpress addFinallyProcessor(Postprocessor processor) { if (!postprocessors.contains(processor)) { postprocessors.add(processor); } return this; } public List getFinallyProcessors() { return Collections.unmodifiableList(finallyProcessors); } public boolean shouldUseSystemOut() { return useSystemOut; } public RestExpress setUseSystemOut(boolean useSystemOut) { this.useSystemOut = useSystemOut; return this; } public RestExpress setEnforceHttpSpec(boolean enforceHttpSpec) { this.enforceHttpSpec = enforceHttpSpec; return this; } public RestExpress enforceHttpSpec() { setEnforceHttpSpec(true); return this; } public RestExpress useSystemOut() { setUseSystemOut(true); return this; } public RestExpress noSystemOut() { setUseSystemOut(false); return this; } public boolean useTcpNoDelay() { return socketSettings.useTcpNoDelay(); } public RestExpress setUseTcpNoDelay(boolean useTcpNoDelay) { socketSettings.setUseTcpNoDelay(useTcpNoDelay); return this; } public boolean useKeepAlive() { return serverSettings.isKeepAlive(); } public RestExpress setKeepAlive(boolean useKeepAlive) { serverSettings.setKeepAlive(useKeepAlive); return this; } public boolean shouldReuseAddress() { return serverSettings.isReuseAddress(); } public RestExpress setReuseAddress(boolean reuseAddress) { serverSettings.setReuseAddress(reuseAddress); return this; } public int getSoLinger() { return socketSettings.getSoLinger(); } public RestExpress setSoLinger(int soLinger) { socketSettings.setSoLinger(soLinger); return this; } public int getReceiveBufferSize() { return socketSettings.getReceiveBufferSize(); } public RestExpress setReceiveBufferSize(int receiveBufferSize) { socketSettings.setReceiveBufferSize(receiveBufferSize); return this; } public int getConnectTimeoutMillis() { return socketSettings.getConnectTimeoutMillis(); } public RestExpress setConnectTimeoutMillis(int connectTimeoutMillis) { socketSettings.setConnectTimeoutMillis(connectTimeoutMillis); return this; } /** * * @param elementName * @param theClass * @return */ public RestExpress alias(String elementName, Class theClass) { routeDefaults.addXmlAlias(elementName, theClass); return this; } public RestExpress mapException( Class from, Class to) { exceptionMap.map(from, to); return this; } public RestExpress setExceptionMap(ExceptionMapping mapping) { this.exceptionMap = mapping; return this; } /** * Return the number of requested NIO/HTTP-handling worker threads. * * @return the number of requested worker threads. */ public int getIoThreadCount() { return serverSettings.getIoThreadCount(); } /** * Set the number of NIO/HTTP-handling worker threads. This * value controls the number of simultaneous connections the * application can handle. * * The default (if this value is not set, or set to zero) is * the Netty default, which is 2 times the number of processors * (or cores). * * @param value the number of desired NIO worker threads. * @return the RestExpress instance. */ public RestExpress setIoThreadCount(int value) { serverSettings.setIoThreadCount(value); return this; } /** * Returns the number of background request-handling (executor) threads. * * @return the number of executor threads. */ public int getExecutorThreadCount() { return serverSettings.getExecutorThreadPoolSize(); } /** * Set the number of background request-handling (executor) threads. * This value controls the number of simultaneous blocking requests that * the server can handle. For longer-running requests, a higher number * may be indicated. * * For VERY short-running requests, a value of zero will cause no * background threads to be created, causing all processing to occur in * the NIO (front-end) worker thread. * * @param value the number of executor threads to create. * @return the RestExpress instance. */ public RestExpress setExecutorThreadCount(int value) { serverSettings.setExecutorThreadPoolSize(value); return this; } /** * Set the maximum length of the content in a request. If the length of the content exceeds this value, * the server closes the connection immediately without sending a response. * * @param size the maximum size in bytes. * @return the RestExpress instance. */ public RestExpress setMaxContentSize(int size) { serverSettings.setMaxContentSize(size); return this; } /** * Can be called after routes are defined to augment or get data from * all the currently-defined routes. * * @param callback a Callback implementor. */ public void iterateRouteBuilders(Callback callback) { routeDeclarations.iterateRouteBuilders(callback); } public Channel bind() { return bind((getPort() > 0 ? getPort() : DEFAULT_PORT)); } /** * The last call in the building of a RestExpress server, bind() causes * Netty to bind to the listening address and process incoming messages. * * @return Channel */ public Channel bind(int port) { setPort(port); // Configure the server. if (getIoThreadCount() == 0) { bootstrap = Bootstraps.createServerNioBootstrap(); } else { bootstrap = Bootstraps.createServerNioBootstrap(getIoThreadCount()); } // Set up the event pipeline factory. DefaultRequestHandler requestHandler = new DefaultRequestHandler( createRouteResolver(), getSerializationProvider(), new DefaultHttpResponseWriter(), enforceHttpSpec); // Add MessageObservers to the request handler here, if desired... requestHandler.addMessageObserver(messageObservers.toArray(new MessageObserver[0])); requestHandler.setExceptionMap(exceptionMap); // Add pre/post processors to the request handler here... addPreprocessors(requestHandler); addPostprocessors(requestHandler); addFinallyProcessors(requestHandler); PipelineBuilder pf = new PipelineBuilder() .addRequestHandler(requestHandler) .setMaxContentLength(serverSettings.getMaxContentSize()); if (getExecutorThreadCount() > 0) { ExecutionHandler executionHandler = new ExecutionHandler( new OrderedMemoryAwareThreadPoolExecutor(getExecutorThreadCount(), 0, 0)); pf.setExecutionHandler(executionHandler); } bootstrap.setPipelineFactory(pf); setBootstrapOptions(); // Bind and start to accept incoming connections. if (shouldUseSystemOut()) { System.out.println(getName() + " server listening on port " + port); } Channel channel = bootstrap.bind(new InetSocketAddress(port)); allChannels.add(channel); bindPlugins(); return channel; } private void setBootstrapOptions() { bootstrap.setOption("child.tcpNoDelay", useTcpNoDelay()); bootstrap.setOption("child.keepAlive", serverSettings.isKeepAlive()); bootstrap.setOption("reuseAddress", shouldReuseAddress()); bootstrap.setOption("child.soLinger", getSoLinger()); bootstrap.setOption("connectTimeoutMillis", getConnectTimeoutMillis()); bootstrap.setOption("receiveBufferSize", getReceiveBufferSize()); } /** * Used in main() to install a default JVM shutdown hook and shut down the * server cleanly. Calls shutdown() when JVM termination detected. To * utilize your own shutdown hook(s), install your own shutdown hook(s) and * call shutdown() instead of awaitShutdown(). */ public void awaitShutdown() { Runtime.getRuntime().addShutdownHook(new DefaultShutdownHook(this)); boolean interrupted = false; do { try { Thread.sleep(300); } catch (InterruptedException e) { interrupted = true; } } while (!interrupted); } /** * Releases all resources associated with this server so the JVM can * shutdown cleanly. Call this method to finish using the server. To utilize * the default shutdown hook in main() provided by RestExpress, call * awaitShutdown() instead. */ public void shutdown() { ChannelGroupFuture future = allChannels.close(); future.awaitUninterruptibly(); shutdownPlugins(); bootstrap.getFactory().releaseExternalResources(); } /** * @return */ private RouteResolver createRouteResolver() { return new RouteResolver(routeDeclarations.createRouteMapping(routeDefaults)); } /** * @return */ public ServerMetadata getRouteMetadata() { ServerMetadata m = new ServerMetadata(); m.setName(getName()); m.setPort(getPort()); //TODO: create a good substitute for this... // m.setDefaultFormat(getDefaultFormat()); // m.addAllSupportedFormats(getResponseProcessors().keySet()); m.addAllRoutes(routeDeclarations.getMetadata()); return m; } public RestExpress registerPlugin(Plugin plugin) { if (!plugins.contains(plugin)) { plugins.add(plugin); plugin.register(this); } return this; } private void bindPlugins() { for (Plugin plugin : plugins) { plugin.bind(this); } } private void shutdownPlugins() { for (Plugin plugin : plugins) { plugin.shutdown(this); } } /** * @param requestHandler */ private void addPreprocessors(DefaultRequestHandler requestHandler) { for (Preprocessor processor : getPreprocessors()) { requestHandler.addPreprocessor(processor); } } /** * @param requestHandler */ private void addPostprocessors(DefaultRequestHandler requestHandler) { for (Postprocessor processor : getPostprocessors()) { requestHandler.addPostprocessor(processor); } } /** * @param requestHandler */ private void addFinallyProcessors(DefaultRequestHandler requestHandler) { for (Postprocessor processor : getFinallyProcessors()) { requestHandler.addFinallyProcessor(processor); } } // SECTION: ROUTE CREATION public ParameterizedRouteBuilder uri(String uriPattern, Object controller) { return routeDeclarations.uri(uriPattern, controller, routeDefaults); } public RegexRouteBuilder regex(String uriPattern, Object controller) { return routeDeclarations.regex(uriPattern, controller, routeDefaults); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy