/*
* Copyright 2013, The Sporting Exchange Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.betfair.cougar.transport.jetty;
import com.betfair.cougar.api.export.Protocol;
import com.betfair.cougar.core.api.*;
import com.betfair.cougar.core.api.exception.CougarFrameworkException;
import com.betfair.cougar.core.api.exception.PanicInTheCougar;
import com.betfair.cougar.core.api.transports.AbstractRegisterableTransport;
import com.betfair.cougar.logging.CougarLogger;
import com.betfair.cougar.logging.CougarLoggingUtils;
import com.betfair.cougar.transport.api.RequestLogger;
import com.betfair.cougar.transport.api.TransportCommandProcessorFactory;
import com.betfair.cougar.transport.api.protocol.ProtocolBinding;
import com.betfair.cougar.transport.api.protocol.ProtocolBindingRegistry;
import com.betfair.cougar.transport.api.protocol.http.GeoLocationDeserializer;
import com.betfair.cougar.transport.api.protocol.http.HttpCommandProcessor;
import com.betfair.cougar.transport.api.protocol.http.HttpServiceBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.jsonrpc.JsonRpcOperationBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.rescript.RescriptOperationBindingDescriptor;
import com.betfair.cougar.transport.jetty.jmx.JettyEndpoints;
import com.betfair.cougar.util.geolocation.GeoIPLocator;
import com.betfair.cougar.util.jmx.JMXControl;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import javax.servlet.ServletException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.logging.Level;
@ManagedResource
public class JettyHttpTransport extends AbstractRegisterableTransport implements GateListener {
private static final CougarLogger logger = CougarLoggingUtils.getLogger(JettyHttpTransport.class);
private JMXControl jmxControl;
private JettyServerWrapper server = new JettyServerWrapper();
private StaticContentServiceHandler wsdlStaticHandler;
private StaticContentServiceHandler htmlStaticHandler;
private AliasHandler aliasHandler;
private ContextHandlerCollection handlerCollection = new ContextHandlerCollection();
private TransportCommandProcessorFactory<HttpCommandProcessor> commandProcessorFactory;
private HttpCommandProcessor defaultCommandProcessor;
private ProtocolBindingRegistry protocolBindingRegistry;
private Map<String, JettyHandlerSpecification> handlerSpecificationMap = new HashMap<String, JettyHandlerSpecification>();
private Map<String, String> pathAliases = new HashMap<>();
// created in setApplicationContext
// set to empty initially so tests that don't set up channels and service binding descriptors will work OK
private Set<HttpServiceBindingDescriptor> serviceBindingDescriptors = new HashSet<HttpServiceBindingDescriptor>();
// The set of JSON-RPC context roots already bound.
private Set<String> rpcProtocolBindings = new HashSet<String>();
// populated by setter methods
private String wsdlContextPath;
private String wsdlRegex;
private String wsdlMediaType;
private String htmlContextPath;
private String htmlRegex;
private String htmlMediaType;
private GeoLocationDeserializer deserializer;
private String uuidHeader;
private int timeoutInSeconds;
private GeoIPLocator geoIPLocator;
private RequestLogger requestLogger;
private JettyEndpoints jettyEndPoints;
private boolean gzipEnabled;
private int gzipMinSize;
private int gzipBufferSize;
private String gzipExcludedAgents;
private int unknownCipherKeyLength;
private boolean suppressCommasInAccessLogForStaticHtml;
private boolean suppressCommasInAccessLogForCalls;
public JettyHttpTransport() {
}
@Override
public String getName() {
return "JettyHttpTransport";
}
@Override
public int getPriority() {
return Integer.MIN_VALUE;
}
@Override
public void onCougarStart() {
createJettyHandlers();
logOperationEndpoints(serviceBindingDescriptors);
try {
server.start();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to startup jetty", ex);
throw new PanicInTheCougar(ex);
}
}
/**
* By setting the starting gate property this IntroductionService will register
* itself with the CougarStartingGate
*
* @param startingGate the starting gate for the application
*/
public void setStartingGate(CougarStartingGate startingGate) {
startingGate.registerStartingListener(this);
}
@Override
public void notify(BindingDescriptor bindingDescriptor) {
if (bindingDescriptor.getServiceProtocol().underlyingTransportIsHttp()) {
serviceBindingDescriptors.add((HttpServiceBindingDescriptor) bindingDescriptor);
registerHandler((HttpServiceBindingDescriptor) bindingDescriptor);
}
}
public void start() throws Exception {
register();
initialiseStaticJettyConfig();
server.startThreadPool();
// Create a shutdown hook to close the jetty port cleanly
Runtime.getRuntime().addShutdownHook(new Thread("Jetty Shutdown Hook") {
@Override
public void run() {
logger.log(Level.INFO, "Gracefully shutting down Jetty Server");
try {
JettyHttpTransport.this.stop();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to shutdown jetty", e);
}
}
});
}
public void stop() throws Exception {
unregister();
server.stop();
}
public void initialiseStaticJettyConfig() throws Exception {
server.initialiseConnectors();
ErrorHandler errorHandler = new CougarErrorHandler();
wsdlStaticHandler = new StaticContentServiceHandler(
wsdlContextPath,
wsdlRegex,
wsdlMediaType,
uuidHeader,
deserializer,
geoIPLocator,
requestLogger,
true);
wsdlStaticHandler.setUnknownCipherKeyLength(unknownCipherKeyLength);
htmlStaticHandler = new StaticContentServiceHandler(
htmlContextPath,
htmlRegex,
htmlMediaType,
uuidHeader,
deserializer,
geoIPLocator,
requestLogger,
suppressCommasInAccessLogForStaticHtml);
htmlStaticHandler.setUnknownCipherKeyLength(unknownCipherKeyLength);
// aliasHandler = new AliasHandler(pathAliases);
StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setServer(server.getJettyServer());
handlerCollection.setServer(server.getJettyServer());
JettyHandler defaultJettyServiceHandler = new AliasHandler(defaultCommandProcessor, suppressCommasInAccessLogForCalls, pathAliases);
ContextHandler context = new ContextHandler();
context.setContextPath("");
context.setResourceBase(".");
context.setHandler(defaultJettyServiceHandler);
handlerCollection.addHandler(context);
handlerCollection.addHandler(wsdlStaticHandler);
handlerCollection.addHandler(htmlStaticHandler);
// handlerCollection.addHandler(aliasHandler);
statisticsHandler.setHandler(handlerCollection);
// Register the errorhandler with the server itself
server.addBean(errorHandler);
server.setHandler(statisticsHandler);
}
/**
* This method adds the service binding descriptor, and all the appropriate protocol binding combos
* into the handlerSpecMap, before binding the serviceBindingDescriptor to the appropriate
* command processor
*/
public void registerHandler(HttpServiceBindingDescriptor serviceBindingDescriptor) {
for (ProtocolBinding protocolBinding : protocolBindingRegistry.getProtocolBindings()) {
if (protocolBinding.getProtocol() == serviceBindingDescriptor.getServiceProtocol()) {
String contextPath = protocolBinding.getContextRoot() + serviceBindingDescriptor.getServiceContextPath();
JettyHandlerSpecification handlerSpec = handlerSpecificationMap.get(contextPath);
if (handlerSpec == null) {
handlerSpec = new JettyHandlerSpecification(protocolBinding.getContextRoot(),
protocolBinding.getProtocol(), serviceBindingDescriptor.getServiceContextPath());
handlerSpecificationMap.put(contextPath, handlerSpec);
}
if (protocolBinding.getIdentityTokenResolver() != null) {
handlerSpec.addServiceVersionToTokenResolverEntry(serviceBindingDescriptor.getServiceVersion(), protocolBinding.getIdentityTokenResolver());
}
}
}
commandProcessorFactory.getCommandProcessor(serviceBindingDescriptor.getServiceProtocol()).bind(serviceBindingDescriptor);
}
private void createJettyHandlers() {
for (Map.Entry<String, JettyHandlerSpecification> spec : handlerSpecificationMap.entrySet()) {
HttpCommandProcessor commandProcessor = commandProcessorFactory.getCommandProcessor(spec.getValue().getProtocol());
bindServiceContextRoot(spec.getValue(), commandProcessor);
}
}
private void bindServiceContextRoot(JettyHandlerSpecification spec, HttpCommandProcessor commandProcessor) {
Protocol serviceProtocol = spec.getProtocol();
String jettyContextRoot = spec.getJettyContextRoot();
if (jettyContextRoot.endsWith("/")) {
jettyContextRoot = jettyContextRoot.substring(0, jettyContextRoot.length()-1);
}
if (serviceProtocol == Protocol.JSON_RPC && jsonRpcContextAlreadyBound(jettyContextRoot)) {
logger.log(Level.INFO, "Not binding Jetty Handler for protocol: JSON-RPC on context root [" +
jettyContextRoot + "] - context already bound");
} else {
StringBuilder sb = new StringBuilder("Adding a new Jetty Handler on url [").append(jettyContextRoot).append("] ");
if (serviceProtocol == Protocol.JSON_RPC) {
sb.append("For all service/version combinations ");
} else {
sb.append("for versions [");
for (ServiceVersion version : spec.getVersionToIdentityTokenResolverMap().keySet()) {
sb.append(version.toString() + " ");
}
sb.append("] ");
}
sb.append("on protocol " + serviceProtocol);
logger.log(Level.INFO, sb.toString());
JettyHandler.IdentityTokenResolverLookup identityTokenResolverLookup = null;
if (!spec.getVersionToIdentityTokenResolverMap().isEmpty()) {
if (spec.getVersionToIdentityTokenResolverMap().size() == 1) {
identityTokenResolverLookup =
new JettyHandler.SingletonIdentityTokenResolverLookup(
spec.getVersionToIdentityTokenResolverMap().values().iterator().next());
} else {
identityTokenResolverLookup = new JettyHandler.GeneralHttpIdentityTokenResolverLookup(jettyContextRoot, spec);
}
}
JettyHandler jettyServiceHandler = new JettyHandler(commandProcessor, spec, identityTokenResolverLookup, suppressCommasInAccessLogForCalls);
jettyServiceHandler.setTimeoutInSeconds(timeoutInSeconds);
// Jetty stuff
ContextHandler context = new ContextHandler();
context.setServer(server.getJettyServer());
context.setContextPath(jettyContextRoot);
// Rescript is not allowed null paths as the operation must be appended to the end of the URI
if (serviceProtocol == Protocol.SOAP || serviceProtocol == Protocol.JSON_RPC) {
context.setAllowNullPathInfo(true);
} else {
context.setAllowNullPathInfo(false);
}
context.setResourceBase(".");
if (gzipEnabled) {
try {
context.setHandler(new GzipHandler(gzipBufferSize,gzipMinSize,gzipExcludedAgents, jettyServiceHandler));
}
catch (ServletException e) {
throw new CougarFrameworkException("Failed to create GZIP handler: [" + jettyContextRoot + "]", e);
}
}
else {
context.setHandler(jettyServiceHandler);
}
handlerCollection.addHandler(context);
try {
//If the server is already running, start this context up!
//If it isn't running, then it will be started when Jetty starts
if (server.isRunning()) {
context.start();
}
} catch (Exception ex) {
throw new CougarFrameworkException("Failed to register serviceBindingDescriptor: [" + jettyContextRoot + "]", ex);
}
}
}
private boolean jsonRpcContextAlreadyBound(String contextRoot) {
if (rpcProtocolBindings.contains(contextRoot)) {
return true;
}
rpcProtocolBindings.add(contextRoot);
return false;
}
private void logOperationEndpoints(
Collection<HttpServiceBindingDescriptor> services
) {
List<String> entries = new ArrayList<String>();
for (ProtocolBinding protocolBinding : protocolBindingRegistry.getProtocolBindings()) {
for (HttpServiceBindingDescriptor service : services) {
if (service.getServiceProtocol() == protocolBinding.getProtocol()) {
OperationBindingDescriptor[] operations = service.getOperationBindings();
for (OperationBindingDescriptor operation : operations) {
StringBuffer entry = new StringBuffer();
entry.append(String.format("%9s : ", service.getServiceProtocol()));
entry.append(String.valueOf(operation.getOperationKey()));
entry.append(" => ");
entry.append(endpointFor(protocolBinding.getContextRoot(), service, operation));
entries.add(entry.toString());
}
}
}
}
Collections.sort(entries);
jettyEndPoints.setEndPoints(entries);
}
private String endpointFor(String contextRoot, HttpServiceBindingDescriptor serviceDescriptor,
OperationBindingDescriptor operationDescriptor
) {
StringBuffer buf = new StringBuffer();
buf.append(getURISchemeAndAuthority());
buf.append(contextRoot);
buf.append(serviceDescriptor.getServiceContextPath());
if (serviceDescriptor.getServiceProtocol() != Protocol.JSON_RPC) {
buf.append("v").append(serviceDescriptor.getServiceVersion().getMajor());
}
if (operationDescriptor instanceof RescriptOperationBindingDescriptor) {
String path = ((RescriptOperationBindingDescriptor) operationDescriptor).getURI();
buf.append(path);
} else if (operationDescriptor instanceof JsonRpcOperationBindingDescriptor){
buf.append("/");
}
return buf.toString();
}
private String getURISchemeAndAuthority() {
try {
boolean http = server.isHttpEnabled();
return String.format(
"%s://%s:%d",
http ? "http" : "https",
InetAddress.getLocalHost().getHostAddress(),
http ? server.getHttpPort() : server.getHttpsPort()
);
} catch (UnknownHostException e) {
throw new RuntimeException("There was a problem determining this host's address", e);
}
}
public void setServerWrapper(JettyServerWrapper server) {
this.server = server;
}
public JettyServerWrapper getServerWrapper() {
return server;
}
public void setDefaultCommandProcessor(HttpCommandProcessor defaultCommandProcessor) {
this.defaultCommandProcessor = defaultCommandProcessor;
}
public void setProtocolBindingRegistry(ProtocolBindingRegistry protocolBindingRegistry) {
this.protocolBindingRegistry = protocolBindingRegistry;
}
public void setTimeoutInSeconds(int timeoutInSeconds) {
this.timeoutInSeconds = timeoutInSeconds;
}
public Server getJettyServer() {
return server.getJettyServer();
}
public StaticContentServiceHandler getWsdlStaticHandler() {
return wsdlStaticHandler;
}
public StaticContentServiceHandler getHtmlStaticHandler() {
return htmlStaticHandler;
}
public Map<String, JettyHandlerSpecification> getHandlerSpecificationMap() {
return handlerSpecificationMap;
}
public void setCommandProcessorFactory(TransportCommandProcessorFactory<HttpCommandProcessor> commandProcessorFactory) {
this.commandProcessorFactory = commandProcessorFactory;
}
public void setWsdlContextPath(String wsdlContextPath) {
this.wsdlContextPath = wsdlContextPath;
}
public void setWsdlRegex(String wsdlRegex) {
this.wsdlRegex = wsdlRegex;
}
public void setWsdlMediaType(String wsdlMediaType) {
this.wsdlMediaType = wsdlMediaType;
}
public void setHtmlContextPath(String htmlContextPath) {
this.htmlContextPath = htmlContextPath;
}
public void setHtmlRegex(String htmlRegex) {
this.htmlRegex = htmlRegex;
}
public void setHtmlMediaType(String htmlMediaType) {
this.htmlMediaType = htmlMediaType;
}
public void setGeoLocationDeserializer(GeoLocationDeserializer deserializer) {
this.deserializer = deserializer;
}
public void setRequestLogger(RequestLogger requestLogger) {
this.requestLogger = requestLogger;
}
public void setUuidHeader(String uuidHeader) {
this.uuidHeader = uuidHeader;
}
public void setGeoIPLocator(GeoIPLocator geoIPLocator) {
this.geoIPLocator = geoIPLocator;
}
public void setJettyEndPoints(JettyEndpoints jettyEndPoints) {
this.jettyEndPoints = jettyEndPoints;
}
public void setJmxControl(JMXControl jmxControl) {
this.jmxControl = jmxControl;
}
public void setPathAliases(Map<String, String> pathAliases) {
this.pathAliases = pathAliases;
}
@ManagedAttribute
public Map<String, String> getPathAliases() {
return pathAliases;
}
@ManagedAttribute
public boolean isHttpEnabled() {
return server.isHttpEnabled();
}
@ManagedAttribute
public boolean isHttpsEnabled() {
return server.isHttpsEnabled();
}
@ManagedAttribute
public int getRequestHeaderSize() {
return server.getRequestHeaderSize();
}
@ManagedAttribute
public int getResponseBufferSize() {
return server.getResponseBufferSize();
}
@ManagedAttribute
public int getResponseHeaderSize() {
return server.getResponseHeaderSize();
}
@ManagedAttribute
public int getHttpAcceptors() {
return server.getHttpAcceptors();
}
@ManagedAttribute
public int getHttpSelectors() {
return server.getHttpSelectors();
}
@ManagedAttribute
public int getHttpsAcceptors() {
return server.getHttpsAcceptors();
}
@ManagedAttribute
public int getHttpsSelectors() {
return server.getHttpsSelectors();
}
@ManagedAttribute
public boolean isHttpForwarded() {
return server.isHttpForwarded();
}
@ManagedAttribute
public boolean isHttpsForwarded() {
return server.isHttpsForwarded();
}
@ManagedAttribute
public boolean isGzipEnabled() {
return gzipEnabled;
}
public void setGzipEnabled(boolean gzipEnabled) {
this.gzipEnabled = gzipEnabled;
}
@ManagedAttribute
public String getExcludedAgents() {
return gzipExcludedAgents;
}
public void setGzipExcludedAgents(String excludedAgents) {
if (excludedAgents != null && !excludedAgents.isEmpty()) {
this.gzipExcludedAgents = excludedAgents;
}
}
@ManagedAttribute
public int getGzipBufferSize() {
return gzipBufferSize;
}
public void setGzipBufferSize(int bufferSize) {
this.gzipBufferSize = bufferSize;
}
@ManagedAttribute
public int getGzipMinSize() {
return gzipMinSize;
}
public void setGzipMinSize(int minSize) {
this.gzipMinSize = minSize;
}
@ManagedAttribute
public int getMaxFormContentSize() {
return getServerWrapper().getMaxFormContentSize();
}
@ManagedAttribute
public int getUnknownCipherKeyLength() {
return unknownCipherKeyLength;
}
public void setUnknownCipherKeyLength(int unknownCipherKeyLength) {
this.unknownCipherKeyLength = unknownCipherKeyLength;
}
@ManagedAttribute
public int getHttpAcceptQueueSize() {
return getServerWrapper().getHttpAcceptQueueSize();
}
@ManagedAttribute
public int getHttpsAcceptQueueSize() {
return getServerWrapper().getHttpsAcceptQueueSize();
}
@ManagedAttribute
public long getLowResourcesMaxMemory() {
return getServerWrapper().getLowResourcesMaxMemory();
}
@ManagedAttribute
public int getLowResourcesMaxConnections() {
return getServerWrapper().getLowResourcesMaxConnections();
}
@ManagedAttribute
public int getLowResourcesPeriod() {
return getServerWrapper().getLowResourcesPeriod();
}
@ManagedAttribute
public int getLowResourcesMaxTime() {
return getServerWrapper().getLowResourcesMaxTime();
}
@ManagedAttribute
public int getLowResourcesIdleTime() {
return getServerWrapper().getLowResourcesIdleTime();
}
@ManagedAttribute
public boolean isLowResourcesMonitorThreads() {
return getServerWrapper().isLowResourcesMonitorThreads();
}
@ManagedAttribute
public boolean isSuppressCommasInAccessLogForStaticHtml() {
return suppressCommasInAccessLogForStaticHtml;
}
public void setSuppressCommasInAccessLogForStaticHtml(boolean suppressCommasInAccessLogForStaticHtml) {
this.suppressCommasInAccessLogForStaticHtml = suppressCommasInAccessLogForStaticHtml;
}
@ManagedAttribute
public boolean isSuppressCommasInAccessLogForCalls() {
return suppressCommasInAccessLogForCalls;
}
public void setSuppressCommasInAccessLogForCalls(boolean suppressCommasInAccessLogForCalls) {
this.suppressCommasInAccessLogForCalls = suppressCommasInAccessLogForCalls;
}
}