org.restexpress.RestExpress Maven / Gradle / Ivy
/*
* 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.
*/
public void serializationProvider(SerializationProvider provider)
{
this.serializationProvider = provider;
}
/**
* 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 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);
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 " + port);
}
Channel channel = bootstrap.bind(new InetSocketAddress(port)).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