org.restexpress.RestExpress Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of RestExpress Show documentation
Show all versions of RestExpress Show documentation
Internet scale, high-performance RESTful Services in Java
The 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 io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelOption;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.ChannelGroupFuture;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
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 javax.net.ssl.SSLContext;
import org.restexpress.domain.metadata.RouteMetadata;
import org.restexpress.domain.metadata.ServerMetadata;
import org.restexpress.exception.DefaultExceptionMapper;
import org.restexpress.exception.ExceptionMapping;
import org.restexpress.exception.ServiceException;
import org.restexpress.pipeline.DefaultRequestHandler;
import org.restexpress.pipeline.MessageObserver;
import org.restexpress.pipeline.PipelineInitializer;
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.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
{
// static
// {
// ResourceLeakDetector.setLevel(Level.DISABLED);
// }
private static final ChannelGroup allChannels = new DefaultChannelGroup("RestExpress", GlobalEventExecutor.INSTANCE);
public static final String DEFAULT_NAME = "RestExpress";
public static final int DEFAULT_PORT = 8081;
private static SerializationProvider DEFAULT_SERIALIZATION_PROVIDER = null;
private SocketSettings socketSettings = new SocketSettings();
private ServerSettings serverSettings = new ServerSettings();
private RouteDefaults routeDefaults = new RouteDefaults();
private boolean enforceHttpSpec = false;
private boolean useSystemOut;
private ServerBootstrapFactory bootstrapFactory = new ServerBootstrapFactory();
private List messageObservers = new ArrayList();
private List preprocessors = new ArrayList();
private List postprocessors = new ArrayList();
private List finallyProcessors = new ArrayList();
private ExceptionMapping exceptionMap = new DefaultExceptionMapper();
private List plugins = new ArrayList();
private RouteDeclaration routeDeclarations = new RouteDeclaration();
private SSLContext sslContext = null;
private SerializationProvider serializationProvider = null;
/**
* 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.
* @deprecated use setDefaultSerializationProvider()
*/
public static void setSerializationProvider(SerializationProvider provider)
{
setDefaultSerializationProvider(provider);
}
/**
* @return the default serialization provider.
* @deprecated Use getDefaultSerializationProvider()
*/
public static SerializationProvider getSerializationProvider()
{
return getDefaultSerializationProvider();
}
/**
* Change the default behavior for serialization.
* If no SerializationProvider is set, default of DefaultSerializationProvider is used,
* which uses Jackson for JSON, XStream for XML.
*
* @param provider a SerializationProvider instance.
*/
public static void setDefaultSerializationProvider(SerializationProvider provider)
{
DEFAULT_SERIALIZATION_PROVIDER = provider;
}
/**
* Get the default serialization provider for RestExpress. If the value is
* unset DefaultSerializationProcessor is set as the default and returned.
* Otherwise, the previously-set value for the default is returned.
*
* @return the default serialization provider.
*/
public static SerializationProvider getDefaultSerializationProvider()
{
if (DEFAULT_SERIALIZATION_PROVIDER == null)
{
DEFAULT_SERIALIZATION_PROVIDER = new DefaultSerializationProvider();
}
return DEFAULT_SERIALIZATION_PROVIDER;
}
/**
* Change the serialization provider for this server instance.
* If no SerializationProcessor is set, default of DefaultSerializationProcessor is used,
* which uses Jackson for JSON, XStream for XML.
*
* @param provider a SerializationProvider instance.
* @return this RestExpress server instance.
*/
public RestExpress serializationProvider(SerializationProvider provider)
{
this.serializationProvider = provider;
return this;
}
/**
* Get the serialization provider for this server instance. If none has
* been set, it is set to the default serialization processor and returned.
* Otherwise, the setting for this server is returned.
*
* @return the SerializationProvider for this instance, or the default.
*/
public SerializationProvider serializationProvider()
{
if (serializationProvider == null)
{
serializationProvider(getDefaultSerializationProvider());
}
return serializationProvider;
}
/**
* 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.
*/
public RestExpress()
{
super();
setName(DEFAULT_NAME);
useSystemOut();
}
public RestExpress setSSLContext(SSLContext sslContext)
{
this.sslContext = sslContext;
return this;
}
public SSLContext getSSLContext()
{
return sslContext;
}
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 String getHostname()
{
return serverSettings.getHostname();
}
public boolean hasHostname()
{
return serverSettings.hasHostname();
}
/**
* Set the hostname or IP address that the server will listen on.
*
* @param hostname hostname or IP address.
*/
public void setHostname(String hostname)
{
serverSettings.setHostname(hostname);
}
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 right before the serialized
* message is sent to the client, or in a finally block after the message is
* processed, if an error occurs. 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 (!finallyProcessors.contains(processor))
{
finallyProcessors.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));
}
/**
* Build a default request handler. Used instead of bind() so it may be used
* injected into any existing Netty pipeline.
*
* @return ChannelHandler
*/
public ChannelHandler buildRequestHandler()
{
// Set up the event pipeline factory.
DefaultRequestHandler requestHandler = new DefaultRequestHandler(
createRouteResolver(), serializationProvider(),
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);
return requestHandler;
}
/**
* 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);
if (hasHostname())
{
return bind(new InetSocketAddress(getHostname(), port));
}
return bind(new InetSocketAddress(port));
}
/**
* Bind to a particular hostname or IP address and port.
*
* @param hostname
* @param port
* @return
*/
public Channel bind(String hostname, int port)
{
setPort(port);
return bind(new InetSocketAddress(hostname, port));
}
public Channel bind(InetSocketAddress ipAddress)
{
ServerBootstrap bootstrap = bootstrapFactory.newServerBootstrap(getIoThreadCount());
bootstrap.childHandler(new PipelineInitializer()
.setExecutionHandler(initializeExecutorGroup())
.addRequestHandler(buildRequestHandler())
.setSSLContext(sslContext)
.setMaxContentLength(serverSettings.getMaxContentSize()));
setBootstrapOptions(bootstrap);
// Bind and start to accept incoming connections.
if (shouldUseSystemOut())
{
System.out.println(getName() + " server listening on port " + ipAddress.toString());
}
Channel channel = bootstrap.bind(ipAddress).channel();
allChannels.add(channel);
bindPlugins();
return channel;
}
private EventExecutorGroup initializeExecutorGroup()
{
if (getExecutorThreadCount() > 0)
{
return new DefaultEventExecutorGroup(getExecutorThreadCount());
}
return null;
}
private void setBootstrapOptions(ServerBootstrap bootstrap)
{
bootstrap.option(ChannelOption.SO_KEEPALIVE, useKeepAlive());
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
bootstrap.option(ChannelOption.TCP_NODELAY, useTcpNoDelay());
bootstrap.option(ChannelOption.SO_KEEPALIVE, serverSettings.isKeepAlive());
bootstrap.option(ChannelOption.SO_REUSEADDR, shouldReuseAddress());
bootstrap.option(ChannelOption.SO_LINGER, getSoLinger());
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeoutMillis());
bootstrap.option(ChannelOption.SO_RCVBUF, getReceiveBufferSize());
bootstrap.option(ChannelOption.MAX_MESSAGES_PER_READ, Integer.MAX_VALUE);
bootstrap.childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator(true));
bootstrap.childOption(ChannelOption.MAX_MESSAGES_PER_READ, Integer.MAX_VALUE);
bootstrap.childOption(ChannelOption.SO_RCVBUF, getReceiveBufferSize());
bootstrap.childOption(ChannelOption.SO_REUSEADDR, shouldReuseAddress());
}
/**
* 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.
*
* Same as shutdown(false);
*/
public void shutdown()
{
shutdown(false);
}
/**
* 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.
*
* @param shouldWait true if shutdown() should wait for the shutdown of each thread group.
*/
public void shutdown(boolean shouldWait)
{
ChannelGroupFuture channelFuture = allChannels.close();
bootstrapFactory.shutdownGracefully(shouldWait);
channelFuture.awaitUninterruptibly();
shutdownPlugins();
}
/**
* @return
*/
private RouteResolver createRouteResolver()
{
return new RouteResolver(routeDeclarations.createRouteMapping(routeDefaults));
}
/**
* Retrieve metadata about the routes in this RestExpress server.
*
* @return ServerMetadata instance.
*/
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;
}
/**
* Retrieve the named routes in this RestExpress server, creating a Map of
* them by name, with the value portion being populated with the URL
* pattern. Any '.{format}' portion of the URL pattern is omitted.
*
* If the Base URL is set, it is included in the URL pattern.
*
* Only named routes are included in the output.
*
* @return a Map of Route Name/URL pairs.
*/
public Map getRouteUrlsByName()
{
final Map urlsByName = new HashMap();
iterateRouteBuilders(new Callback()
{
@Override
public void process(RouteBuilder routeBuilder)
{
RouteMetadata route = routeBuilder.asMetadata();
if (route.getName() != null)
{
urlsByName.put(route.getName(), getBaseUrl()
+ route.getUri().getPattern().replace(".{format}", ""));
}
}
});
return urlsByName;
}
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