Java tutorial
/* * 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<MessageObserver> messageObservers = new ArrayList<MessageObserver>(); private List<Preprocessor> preprocessors = new ArrayList<Preprocessor>(); private List<Postprocessor> postprocessors = new ArrayList<Postprocessor>(); private List<Postprocessor> finallyProcessors = new ArrayList<Postprocessor>(); private ExceptionMapping exceptionMap = new DefaultExceptionMapper(); private List<Plugin> plugins = new ArrayList<Plugin>(); 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. * * <p/> * 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. * * <p/> * 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<MessageObserver> 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<Preprocessor> 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<Postprocessor> 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<Postprocessor> 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; } /** * Turns off the Netty HttpContentCompressor on binding so that output GZip and deflate encodings are not possible. * This is a speed optimization, per Issue #126 * * By default, compression is supported. Use this to turn it off. * * @return this RestExpress instance. */ public RestExpress noCompression() { serverSettings.setUseCompression(false); return this; } /** * Answers whether the service is setup to use response compression via the Netty HttpContentCompressor. * * @return true if the RestExpress server is configured to use response compression. Otherwise, false. */ public boolean isUsingCompression() { return serverSettings.shouldUseCompression(); } 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 <T extends Exception, U extends ServiceException> RestExpress mapException(Class<T> from, Class<U> 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<RouteBuilder> 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()) .setUseCompression(serverSettings.shouldUseCompression())); 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. * <p/> * 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. * <p/> * If the Base URL is set, it is included in the URL pattern. * <p/> * Only named routes are included in the output. * * @return a Map of Route Name/URL pairs. */ public Map<String, String> getRouteUrlsByName() { final Map<String, String> urlsByName = new HashMap<String, String>(); iterateRouteBuilders(new Callback<RouteBuilder>() { @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); } }