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

com.strategicgains.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 com.strategicgains.restexpress;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

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 com.strategicgains.restexpress.domain.metadata.ServerMetadata;
import com.strategicgains.restexpress.exception.ExceptionMapping;
import com.strategicgains.restexpress.exception.ServiceException;
import com.strategicgains.restexpress.pipeline.DefaultRequestHandler;
import com.strategicgains.restexpress.pipeline.MessageObserver;
import com.strategicgains.restexpress.pipeline.PipelineBuilder;
import com.strategicgains.restexpress.pipeline.Postprocessor;
import com.strategicgains.restexpress.pipeline.Preprocessor;
import com.strategicgains.restexpress.plugin.Plugin;
import com.strategicgains.restexpress.response.ResponseProcessor;
import com.strategicgains.restexpress.response.ResponseProcessorResolver;
import com.strategicgains.restexpress.route.RouteDeclaration;
import com.strategicgains.restexpress.route.RouteResolver;
import com.strategicgains.restexpress.route.parameterized.ParameterizedRouteBuilder;
import com.strategicgains.restexpress.route.regex.RegexRouteBuilder;
import com.strategicgains.restexpress.serialization.AliasingSerializationProcessor;
import com.strategicgains.restexpress.settings.RouteDefaults;
import com.strategicgains.restexpress.settings.ServerSettings;
import com.strategicgains.restexpress.settings.SocketSettings;
import com.strategicgains.restexpress.util.Bootstraps;
import com.strategicgains.restexpress.util.DefaultShutdownHook;
import com.strategicgains.restexpress.util.LogLevel;
import com.strategicgains.restexpress.util.Resolver;

/**
 * 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 ServerBootstrap bootstrap;
	private SocketSettings socketSettings = new SocketSettings();
	private ServerSettings serverSettings = new ServerSettings();
	private RouteDefaults routeDefaults = new RouteDefaults();
	private LogLevel logLevel = LogLevel.DEBUG; // Netty default
	private boolean useSystemOut;

	Map responseProcessors = new HashMap();
	private List messageObservers = new ArrayList();
	private List preprocessors = new ArrayList();
	private List postprocessors = new ArrayList();
	private List finallyProcessors = new ArrayList();
	private Resolver responseResolver;
	private ExceptionMapping exceptionMap = new ExceptionMapping();
	private List plugins = new ArrayList();
	private RouteDeclaration routeDeclarations = new RouteDeclaration();

	/**
	 * 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); supportJson(true); supportXml(); 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 putResponseProcessor(String format, ResponseProcessor processor) { responseProcessors.put(format, processor); return this; } /* package protected */Map getResponseProcessors() { return responseProcessors; } public Resolver getResponseResolver() { return responseResolver; } public RestExpress setResponseResolver(Resolver responseResolver) { this.responseResolver = responseResolver; return this; } public String getDefaultFormat() { return routeDefaults.getDefaultFormat(); } public RestExpress setDefaultFormat(String format) { if (format == null || format.trim().isEmpty()) return this; routeDefaults.setDefaultFormat(format.trim().toLowerCase()); return this; } /** * Tell RestExpress to support JSON in routes, incoming and outgoing. By * default RestExpress supports JSON and is the default. * * @param isDefault * true to make JSON the default format. * @return the RestExpress instance. */ public RestExpress supportJson(boolean isDefault) { if (!getResponseProcessors().containsKey(Format.JSON)) { responseProcessors.put(Format.JSON, ResponseProcessor.defaultJsonProcessor()); } if (isDefault) { setDefaultFormat(Format.JSON); } return this; } /** * Tell RestExpress to support JSON in routes, incoming and outgoing. By * default RestExpress supports JSON and is the default. * * @return the RestExpress instance. */ public RestExpress supportJson() { return supportJson(false); } /** * Tell RestExpress to not support JSON in routes, incoming or outgoing. * Client must call setDefaultFormat(String) to set the default format to * something else. * * @return the RestExpress instance. */ public RestExpress noJson() { responseProcessors.remove(Format.JSON); return this; } /** * Tell RestExpress to support XML in routes, incoming and outgoing. By * default RestExpress supports XML. * * @param isDefault * true to make XML the default format. * @return the RestExpress instance. */ public RestExpress supportXml(boolean isDefault) { if (!getResponseProcessors().containsKey(Format.XML)) { getResponseProcessors().put(Format.XML, ResponseProcessor.defaultXmlProcessor()); } if (isDefault) { setDefaultFormat(Format.XML); } return this; } /** * Tell RestExpress to support XML in routes, incoming and outgoing. By * default RestExpress supports XML. * * @param isDefault * true to make XML the default format. * @return the RestExpress instance. */ public RestExpress supportXml() { return supportXml(false); } /** * Tell RestExpress to not support XML in routes, incoming or outgoing. * * @return the RestExpress instance. */ public RestExpress noXml() { responseProcessors.remove(Format.XML); return this; } /** * Tell RestExpress to support TXT format specifiers in routes, outgoing * only at present. * * @param isDefault true to make TXT the default format. * @return the RestExpress instance. */ public RestExpress supportTxt(boolean isDefault) { if (!getResponseProcessors().containsKey(Format.TXT)) { getResponseProcessors().put(Format.TXT, ResponseProcessor.defaultTxtProcessor()); } if (isDefault) { setDefaultFormat(Format.TXT); } return this; } /** * Tell RestExpress to support TXT format specifier in routes, outgoing only * at present. * * @return the RestExpress instance. */ public RestExpress supportTxt() { return supportTxt(false); } 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 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 LogLevel getLogLevel() { return logLevel; } public RestExpress setLogLevel(LogLevel logLevel) { this.logLevel = logLevel; 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; } 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(), createResponseProcessorResolver()); // 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(new LoggingHandler(getLogLevel().getNettyLogLevel())) .addRequestHandler(requestHandler); 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("Starting " + getName() + " Server 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()); 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); } } /** * @return */ private ResponseProcessorResolver createResponseProcessorResolver() { ResponseProcessorResolver resolver = new ResponseProcessorResolver(); resolver.setDefaultFormat(getDefaultFormat()); for (Entry entry : getResponseProcessors().entrySet()) { if (entry.getKey().equals(Format.XML)) { setXmlAliases((AliasingSerializationProcessor) entry.getValue().getSerializer()); } resolver.put(entry.getKey(), entry.getValue()); } return resolver; } /** * @param processor */ private void setXmlAliases(AliasingSerializationProcessor processor) { routeDefaults.setXmlAliases(processor); } /** * @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