/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.waveprotocol.box.server.rpc; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; import com.google.inject.servlet.GuiceFilter; import com.google.inject.servlet.GuiceServletContextListener; import com.google.inject.servlet.ServletModule; import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors.MethodDescriptor; import com.google.protobuf.Message; import com.google.protobuf.RpcCallback; import com.google.protobuf.Service; import com.typesafe.config.Config; import org.apache.commons.lang.StringUtils; import org.atmosphere.cache.UUIDBroadcasterCache; import org.atmosphere.config.service.AtmosphereHandlerService; import org.atmosphere.cpr.*; import org.atmosphere.guice.AtmosphereGuiceServlet; import org.atmosphere.util.IOUtils; import org.eclipse.jetty.proxy.ProxyServlet; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.session.HashSessionManager; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.GzipFilter; import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.servlet.*; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolAuthenticate; import org.waveprotocol.box.common.comms.WaveClientRpc.ProtocolAuthenticationResult; import org.waveprotocol.box.server.authentication.SessionManager; import org.waveprotocol.box.server.executor.ExecutorAnnotations.ClientServerExecutor; import org.waveprotocol.box.server.persistence.file.FileUtils; import org.waveprotocol.box.server.rpc.atmosphere.AtmosphereChannel; import org.waveprotocol.box.server.rpc.atmosphere.AtmosphereClientInterceptor; import org.waveprotocol.box.server.util.NetUtils; import org.waveprotocol.box.stat.Timer; import org.waveprotocol.box.stat.Timing; import org.waveprotocol.wave.model.util.Pair; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.util.logging.Log; import javax.annotation.Nullable; import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.ServletContextListener; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpSession; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; /** * ServerRpcProvider can provide instances of type Service over an incoming * network socket and service incoming RPCs to these services and their methods. * * */ public class ServerRpcProvider { private static final Log LOG = Log.get(ServerRpcProvider.class); /** * The buffer size is passed to implementations of {@link WaveWebSocketServlet} as init * param. It defines the response buffer size. */ private static final int BUFFER_SIZE = 1024 * 1024; private final InetSocketAddress[] httpAddresses; private final Executor threadPool; private final SessionManager sessionManager; private final org.eclipse.jetty.server.SessionManager jettySessionManager; private Server httpServer = null; private final boolean sslEnabled; private final String sslKeystorePath; private final String sslKeystorePassword; // Mapping from incoming protocol buffer type -> specific handler. private final Map<Descriptors.Descriptor, RegisteredServiceMethod> registeredServices = Maps.newHashMap(); // List of webApp source directories ("./war", etc) private final String[] resourceBases; private final String sessionStoreDir; /** * Internal, static container class for any specific registered service * method. */ static class RegisteredServiceMethod { final Service service; final MethodDescriptor method; RegisteredServiceMethod(Service service, MethodDescriptor method) { this.service = service; this.method = method; } } static class WebSocketConnection extends Connection { private final WebSocketChannel socketChannel; WebSocketConnection(ParticipantId loggedInUser, ServerRpcProvider provider) { super(loggedInUser, provider); socketChannel = new WebSocketChannelImpl(this); LOG.info("New websocket connection set up for user " + loggedInUser); expectMessages(socketChannel); } @Override protected void sendMessage(int sequenceNo, Message message) { socketChannel.sendMessage(sequenceNo, message); } public WebSocketChannel getWebSocketServerChannel() { return socketChannel; } } static class AtmosphereConnection extends Connection { private final AtmosphereChannel atmosphereChannel; public AtmosphereConnection(ParticipantId loggedInUser, ServerRpcProvider provider) { super(loggedInUser, provider); atmosphereChannel = new AtmosphereChannel(this); expectMessages(atmosphereChannel); } @Override protected void sendMessage(int sequenceNo, Message message) { atmosphereChannel.sendMessage(sequenceNo, message); } public AtmosphereChannel getAtmosphereChannel() { return atmosphereChannel; } } static abstract class Connection implements ProtoCallback { private final Map<Integer, ServerRpcController> activeRpcs = new ConcurrentHashMap<>(); // The logged in user. // Note: Due to this bug: // http://code.google.com/p/wave-protocol/issues/detail?id=119, // the field may be null on first connect and then set later using an RPC. private ParticipantId loggedInUser; private final ServerRpcProvider provider; /** * @param loggedInUser The currently logged in user, or null if no user is * logged in. * @param provider the provider */ public Connection(ParticipantId loggedInUser, ServerRpcProvider provider) { this.loggedInUser = loggedInUser; this.provider = provider; } protected void expectMessages(MessageExpectingChannel channel) { synchronized (provider.registeredServices) { for (RegisteredServiceMethod serviceMethod : provider.registeredServices.values()) { channel.expectMessage(serviceMethod.service.getRequestPrototype(serviceMethod.method)); LOG.fine("Expecting: " + serviceMethod.method.getFullName()); } } channel.expectMessage(Rpc.CancelRpc.getDefaultInstance()); } protected abstract void sendMessage(int sequenceNo, Message message); private ParticipantId authenticate(String token) { HttpSession session = provider.sessionManager.getSessionFromToken(token); return provider.sessionManager.getLoggedInUser(session); } @Override public void message(final int sequenceNo, Message message) { final String messageName = "/" + message.getClass().getSimpleName(); final Timer profilingTimer = Timing.startRequest(messageName); if (message instanceof Rpc.CancelRpc) { final ServerRpcController controller = activeRpcs.get(sequenceNo); if (controller == null) { throw new IllegalStateException("Trying to cancel an RPC that is not active!"); } else { LOG.info("Cancelling open RPC " + sequenceNo); controller.cancel(); } } else if (message instanceof ProtocolAuthenticate) { // Workaround for bug: http://codereview.waveprotocol.org/224001/ // When we get this message, either the connection will not be logged in // (loggedInUser == null) or the connection will have been authenticated // via cookies // (in which case loggedInUser must match the authenticated user, and // this message has no // effect). ProtocolAuthenticate authMessage = (ProtocolAuthenticate) message; ParticipantId authenticatedAs = authenticate(authMessage.getToken()); Preconditions.checkArgument(authenticatedAs != null, "Auth token invalid"); Preconditions.checkState(loggedInUser == null || loggedInUser.equals(authenticatedAs), "Session already authenticated as a different user"); loggedInUser = authenticatedAs; LOG.info("Session authenticated as " + loggedInUser); sendMessage(sequenceNo, ProtocolAuthenticationResult.getDefaultInstance()); } else if (provider.registeredServices.containsKey(message.getDescriptorForType())) { if (activeRpcs.containsKey(sequenceNo)) { throw new IllegalStateException( "Can't invoke a new RPC with a sequence number already in use."); } else { final RegisteredServiceMethod serviceMethod = provider.registeredServices.get(message.getDescriptorForType()); // Create the internal ServerRpcController used to invoke the call. final ServerRpcController controller = new ServerRpcControllerImpl(message, serviceMethod.service, serviceMethod.method, loggedInUser, new RpcCallback<Message>() { @Override synchronized public void run(Message message) { if (message instanceof Rpc.RpcFinished || !serviceMethod.method.getOptions().getExtension(Rpc.isStreamingRpc)) { // This RPC is over - remove it from the map. boolean failed = message instanceof Rpc.RpcFinished && ((Rpc.RpcFinished) message).getFailed(); LOG.fine("RPC " + sequenceNo + " is now finished, failed = " + failed); if (failed) { LOG.info("error = " + ((Rpc.RpcFinished) message).getErrorText()); } activeRpcs.remove(sequenceNo); } sendMessage(sequenceNo, message); if (profilingTimer != null) { Timing.stop(profilingTimer); } } }); // Kick off a new thread specific to this RPC. activeRpcs.put(sequenceNo, controller); provider.threadPool.execute(controller); } } else { // Sent a message type we understand, but don't expect - erronous case! throw new IllegalStateException( "Got expected but unknown message (" + message + ") for sequence: " + sequenceNo); } } } /** * Construct a new ServerRpcProvider, hosting on the specified * WebSocket addresses. * * Also accepts an ExecutorService for spawning managing threads. */ public ServerRpcProvider(InetSocketAddress[] httpAddresses, String[] resourceBases, Executor threadPool, SessionManager sessionManager, org.eclipse.jetty.server.SessionManager jettySessionManager, String sessionStoreDir, boolean sslEnabled, String sslKeystorePath, String sslKeystorePassword) { this.httpAddresses = httpAddresses; this.resourceBases = resourceBases; this.threadPool = threadPool; this.sessionManager = sessionManager; this.jettySessionManager = jettySessionManager; this.sessionStoreDir = sessionStoreDir; this.sslEnabled = sslEnabled; this.sslKeystorePath = sslKeystorePath; this.sslKeystorePassword = sslKeystorePassword; } /** * Constructs a new ServerRpcProvider with a default ExecutorService. */ public ServerRpcProvider(InetSocketAddress[] httpAddresses, String[] resourceBases, SessionManager sessionManager, org.eclipse.jetty.server.SessionManager jettySessionManager, String sessionStoreDir, boolean sslEnabled, String sslKeystorePath, String sslKeystorePassword, Executor executor) { this(httpAddresses, resourceBases, executor, sessionManager, jettySessionManager, sessionStoreDir, sslEnabled, sslKeystorePath, sslKeystorePassword); } @Inject public ServerRpcProvider(Config config, SessionManager sessionManager, org.eclipse.jetty.server.SessionManager jettySessionManager, @ClientServerExecutor Executor executorService) { this(parseAddressList(config.getStringList("core.http_frontend_addresses"), config.getString("core.http_websocket_public_address")), config.getStringList("core.resource_bases").toArray(new String[0]), sessionManager, jettySessionManager, config.getString("core.sessions_store_directory"), config.getBoolean("security.enable_ssl"), config.getString("security.ssl_keystore_path"), config.getString("security.ssl_keystore_password"), executorService); } public void startWebSocketServer(final Injector injector) { httpServer = new Server(); List<Connector> connectors = getSelectChannelConnectors(httpAddresses); if (connectors.isEmpty()) { LOG.severe("No valid http end point address provided!"); } for (Connector connector : connectors) { httpServer.addConnector(connector); } final WebAppContext context = new WebAppContext(); context.setParentLoaderPriority(true); if (jettySessionManager != null) { // This disables JSessionIDs in URLs redirects // see: http://stackoverflow.com/questions/7727534/how-do-you-disable-jsessionid-for-jetty-running-with-the-eclipse-jetty-maven-plu // and: http://jira.codehaus.org/browse/JETTY-467?focusedCommentId=114884&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-114884 jettySessionManager.setSessionIdPathParameterName(null); context.getSessionHandler().setSessionManager(jettySessionManager); } final ResourceCollection resources = new ResourceCollection(resourceBases); context.setBaseResource(resources); addWebSocketServlets(); try { final ServletModule servletModule = getServletModule(); ServletContextListener contextListener = new GuiceServletContextListener() { private final Injector childInjector = injector.createChildInjector(servletModule); @Override protected Injector getInjector() { return childInjector; } }; context.addEventListener(contextListener); context.addFilter(GuiceFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); context.addFilter(GzipFilter.class, "/webclient/*", EnumSet.allOf(DispatcherType.class)); httpServer.setHandler(context); httpServer.start(); restoreSessions(); } catch (Exception e) { // yes, .start() throws "Exception" LOG.severe("Fatal error starting http server.", e); return; } LOG.fine("WebSocket server running."); } private void restoreSessions() { try { HashSessionManager hashSessionManager = (HashSessionManager) jettySessionManager; hashSessionManager.setStoreDirectory(FileUtils.createDirIfNotExists(sessionStoreDir, "Session persistence")); hashSessionManager.setSavePeriod(60); hashSessionManager.restoreSessions(); } catch (Exception e) { LOG.warning("Cannot restore sessions"); } } public void addWebSocketServlets() { // Servlet where the websocket connection is served from. ServletHolder wsholder = addServlet("/socket", WaveWebSocketServlet.class); // TODO(zamfi): fix to let messages span frames. wsholder.setInitParameter("bufferSize", "" + BUFFER_SIZE); // Atmosphere framework. Replacement of Socket.IO // See https://issues.apache.org/jira/browse/WAVE-405 ServletHolder atholder = addServlet("/atmosphere*", AtmosphereGuiceServlet.class); // Enable guice. See // https://github.com/Atmosphere/atmosphere/wiki/Configuring-Atmosphere%27s-Classes-Creation-and-Injection atholder.setInitParameter("org.atmosphere.cpr.objectFactory", "org.waveprotocol.box.server.rpc.atmosphere.GuiceAtmosphereFactory"); atholder.setAsyncSupported(true); atholder.setInitOrder(0); // Serve the static content and GWT web client with the default servlet // (acts like a standard file-based web server). addServlet("/static/*", DefaultServlet.class); addServlet("/webclient/*", DefaultServlet.class); } public ServletModule getServletModule() { return new ServletModule() { @Override protected void configureServlets() { // We add servlets here to override the DefaultServlet automatic registered by WebAppContext // in path "/" with our WaveClientServlet. Any other way to do this? // Related question (unanswered 08-Apr-2011) // http://web.archiveorange.com/archive/v/d0LdlXf1kN0OXyPNyQZp for (Pair<String, ServletHolder> servlet : servletRegistry) { String url = servlet.getFirst(); @SuppressWarnings("unchecked") Class<HttpServlet> clazz = (Class<HttpServlet>) servlet.getSecond().getHeldClass(); Map<String,String> params = servlet.getSecond().getInitParameters(); serve(url).with(clazz,params); bind(clazz).in(Singleton.class); } for (Pair<String, Class<? extends Filter>> filter : filterRegistry) { filter(filter.first).through(filter.second); } } }; } private static InetSocketAddress[] parseAddressList(List<String> addressList, String websocketAddress) { if (addressList == null || addressList.size() == 0) { return new InetSocketAddress[0]; } else { Set<InetSocketAddress> addresses = Sets.newHashSet(); // We add the websocketAddress as another listening address. ArrayList<String> mergedAddressList = new ArrayList<>(addressList); if (!StringUtils.isEmpty(websocketAddress)) { mergedAddressList.add(websocketAddress); } for (String str : mergedAddressList) { if (str.length() == 0) { LOG.warning("Encountered empty address in http addresses list."); } else { try { InetSocketAddress address = NetUtils.parseHttpAddress(str); if (!addresses.contains(address)) { addresses.add(address); } else { LOG.warning( "Ignoring duplicate address in http addresses list: Duplicate entry '" + str + "' resolved to " + address.getAddress().getHostAddress()); } } catch (IOException e) { LOG.severe("Unable to process address " + str, e); } } } return addresses.toArray(new InetSocketAddress[addresses.size()]); } } /** * @return a list of {@link Connector} each bound to a host:port * pair form the list addresses. */ private List<Connector> getSelectChannelConnectors( InetSocketAddress[] httpAddresses) { List<Connector> list = Lists.newArrayList(); String[] excludeCiphers = {"SSL_RSA_EXPORT_WITH_RC4_40_MD5", "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_RSA_WITH_DES_CBC_SHA", "SSL_DHE_RSA_WITH_DES_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA"}; SslContextFactory sslContextFactory = null; if (sslEnabled) { Preconditions.checkState(sslKeystorePath != null && !sslKeystorePath.isEmpty(), "SSL Keystore path left blank"); Preconditions.checkState(sslKeystorePassword != null && !sslKeystorePassword.isEmpty(), "SSL Keystore password left blank"); sslContextFactory = new SslContextFactory(sslKeystorePath); sslContextFactory.setKeyStorePassword(sslKeystorePassword); sslContextFactory.setRenegotiationAllowed(false); sslContextFactory.setExcludeCipherSuites(excludeCiphers); // Note: we only actually needed client auth for AuthenticationServlet. // Using Need instead of Want prevents web-sockets from working on // Chrome. sslContextFactory.setWantClientAuth(true); } for (InetSocketAddress address : httpAddresses) { ServerConnector connector; if (sslEnabled) { connector = new ServerConnector(httpServer, sslContextFactory); } else { connector = new ServerConnector(httpServer); } connector.setHost(address.getAddress().getHostAddress()); connector.setPort(address.getPort()); connector.setIdleTimeout(0); list.add(connector); } return list; } @SuppressWarnings("serial") @Singleton public static class WaveWebSocketServlet extends WebSocketServlet { final ServerRpcProvider provider; final int websocketMaxIdleTime; final int websocketMaxMessageSize; @Inject public WaveWebSocketServlet(ServerRpcProvider provider, Config config) { super(); this.provider = provider; this.websocketMaxIdleTime = config.getInt("network.websocket_max_idle_time"); this.websocketMaxMessageSize = config.getInt("network.websocket_max_message_size"); } @SuppressWarnings("cast") @Override public void configure(WebSocketServletFactory factory) { if (websocketMaxIdleTime == 0) { // Jetty does not allow to set infinite timeout. factory.getPolicy().setIdleTimeout(Integer.MAX_VALUE); } else { factory.getPolicy().setIdleTimeout(websocketMaxIdleTime); } factory.getPolicy().setMaxTextMessageSize(websocketMaxMessageSize * 1024 * 1024); factory.setCreator(new WebSocketCreator() { @Override public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) { ParticipantId loggedInUser = provider.sessionManager.getLoggedInUser(req.getSession()); return new WebSocketConnection(loggedInUser, provider).getWebSocketServerChannel(); } }); } } /** * Manange atmosphere connections and dispatch messages to * wave channels. * * @author pablojan@gmail.com <Pablo Ojanguren> * */ @Singleton @AtmosphereHandlerService(path = "/atmosphere", interceptors = {AtmosphereClientInterceptor.class}, broadcasterCache = UUIDBroadcasterCache.class) public static class WaveAtmosphereService implements AtmosphereHandler { private static final Log LOG = Log.get(WaveAtmosphereService.class); private static final String WAVE_CHANNEL_ATTRIBUTE = "WAVE_CHANNEL_ATTRIBUTE"; private static final String MSG_SEPARATOR = "|"; private static final String MSG_CHARSET = "UTF-8"; @Inject public ServerRpcProvider provider; @Override public void onRequest(AtmosphereResource resource) throws IOException { AtmosphereResourceSession resourceSession = AtmosphereResourceSessionFactory.getDefault().getSession(resource); AtmosphereChannel resourceChannel = resourceSession.getAttribute(WAVE_CHANNEL_ATTRIBUTE, AtmosphereChannel.class); if (resourceChannel == null) { ParticipantId loggedInUser = provider.sessionManager.getLoggedInUser(resource.getRequest().getSession(false)); AtmosphereConnection connection = new AtmosphereConnection(loggedInUser, provider); resourceChannel = connection.getAtmosphereChannel(); resourceSession.setAttribute(WAVE_CHANNEL_ATTRIBUTE, resourceChannel); resourceChannel.onConnect(resource); } resource.setBroadcaster(resourceChannel.getBroadcaster()); // on every // request if (resource.getRequest().getMethod().equalsIgnoreCase("GET")) { resource.suspend(); } if (resource.getRequest().getMethod().equalsIgnoreCase("POST")) { StringBuilder b = IOUtils.readEntirely(resource); resourceChannel.onMessage(b.toString()); } } @Override public void onStateChange(AtmosphereResourceEvent event) throws IOException { AtmosphereResponse response = event.getResource().getResponse(); AtmosphereResource resource = event.getResource(); if (event.isSuspended()) { // Set content type before do response.getWriter() // http://docs.oracle.com/javaee/5/api/javax/servlet/ServletResponse.html#setContentType(java.lang.String) response.setContentType("text/plain; charset=UTF-8"); response.setCharacterEncoding("UTF-8"); if (event.getMessage().getClass().isArray()) { LOG.fine("SEND MESSAGE ARRAY " + event.getMessage().toString()); List<Object> list = Collections.singletonList(event.getMessage()); response.getOutputStream().write(MSG_SEPARATOR.getBytes(MSG_CHARSET)); for (Object object : list) { String message = (String) object; message += MSG_SEPARATOR; response.getOutputStream().write(message.getBytes(MSG_CHARSET)); } } else if (event.getMessage() instanceof List) { LOG.fine("SEND MESSAGE LIST " + event.getMessage().toString()); @SuppressWarnings("unchecked") List<Object> list = List.class.cast(event.getMessage()); response.getOutputStream().write(MSG_SEPARATOR.getBytes(MSG_CHARSET)); for (Object object : list) { String message = (String) object; message += MSG_SEPARATOR; response.getOutputStream().write(message.getBytes(MSG_CHARSET)); } } else if (event.getMessage() instanceof String) { LOG.fine("SEND MESSAGE " + event.getMessage().toString()); String message = (String) event.getMessage(); response.getOutputStream().write(message.getBytes(MSG_CHARSET)); } try { response.flushBuffer(); switch (resource.transport()) { case JSONP: case LONG_POLLING: event.getResource().resume(); break; case WEBSOCKET: case STREAMING: case SSE: response.getOutputStream().flush(); break; default: LOG.info("Unknown transport"); break; } } catch (IOException e) { LOG.info("Error resuming resource response", e); } } else if (event.isResuming()) { LOG.fine("RESUMING"); } else if (event.isResumedOnTimeout()) { LOG.fine("RESUMED ON TIMEOUT"); } else if (event.isClosedByApplication() || event.isClosedByClient()) { LOG.fine("CONNECTION CLOSED"); AtmosphereResourceSession resourceSession = AtmosphereResourceSessionFactory.getDefault().getSession(resource); AtmosphereChannel resourceChannel = resourceSession.getAttribute(WAVE_CHANNEL_ATTRIBUTE, AtmosphereChannel.class); if (resourceChannel != null) { resourceChannel.onDisconnect(); } } } @Override public void destroy() { // Nothing to do } } /** * Returns the socket the WebSocket server is listening on. */ public SocketAddress getWebSocketAddress() { if (httpServer == null) { return null; } else { ServerConnector c = (ServerConnector)httpServer.getConnectors()[0]; return new InetSocketAddress(c.getHost(), c.getLocalPort()); } } /** * Stops this server. */ public void stopServer() throws IOException { try { httpServer.stop(); // yes, .stop() throws "Exception" } catch (Exception e) { LOG.warning("Fatal error stopping http server.", e); } LOG.fine("server shutdown."); } /** * Register all methods provided by the given service type. */ public void registerService(Service service) { synchronized (registeredServices) { for (MethodDescriptor methodDescriptor : service.getDescriptorForType().getMethods()) { registeredServices.put(methodDescriptor.getInputType(), new RegisteredServiceMethod(service, methodDescriptor)); } } } /** * List of servlets */ List<Pair<String, ServletHolder>> servletRegistry = Lists.newArrayList(); /** * List of filters */ List<Pair<String, Class<? extends Filter>>> filterRegistry = Lists.newArrayList(); /** * Add a servlet to the servlet registry. This servlet will be attached to the * specified URL pattern when the server is started up. * * @param urlPattern the URL pattern for paths. Eg, '/foo', '/foo/*'. * @param servlet the servlet class to bind to the specified paths. * @param initParams the map with init params, can be null or empty. * @return the {@link ServletHolder} that holds the servlet. */ public ServletHolder addServlet(String urlPattern, Class<? extends HttpServlet> servlet, @Nullable Map<String, String> initParams) { ServletHolder servletHolder = new ServletHolder(servlet); if (initParams != null) { servletHolder.setInitParameters(initParams); } servletRegistry.add(Pair.of(urlPattern, servletHolder)); return servletHolder; } /** * Add a servlet to the servlet registry. This servlet will be attached to the * specified URL pattern when the server is started up. * @param urlPattern the URL pattern for paths. Eg, '/foo', '/foo/*'. * @param servlet the servlet class to bind to the specified paths. * @return the {@link ServletHolder} that holds the servlet. */ public ServletHolder addServlet(String urlPattern, Class<? extends HttpServlet> servlet) { return addServlet(urlPattern, servlet, null); } /** * Add a filter to the filter registry. This filter will be attached to the * specified URL pattern when the server is started up. * * @param urlPattern the URL pattern for paths. Eg, '/foo', '/foo/*'. * */ public void addFilter(String urlPattern, Class<? extends Filter> filter) { filterRegistry.add(new Pair<String, Class<? extends Filter>>(urlPattern, filter)); } /** * Add a transparent proxy to the servlet registry. The servlet will proxy to the * specified URL pattern. * For example: addTransparentProxy("/gadgets/*","http://gmodules:80/gadgets", "/gadgets") * @param urlPattern the URL pattern for paths. Eg, '/foo', '/foo/*'. * @param proxyTo the URL to proxy to. * @param prefix the prefix that should be proxied. */ public void addTransparentProxy(String urlPattern, String proxyTo, String prefix) { Preconditions.checkNotNull(urlPattern); Preconditions.checkNotNull(proxyTo); Preconditions.checkNotNull(prefix); ServletHolder proxy = new ServletHolder(ProxyServlet.Transparent.class); proxy.setInitParameter("proxyTo", proxyTo); proxy.setInitParameter("prefix", prefix); servletRegistry.add(Pair.of(urlPattern, proxy)); } }