/*
* Copyright © 2014 Cask Data, 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 co.cask.cdap.internal.app.runtime.service.http;
import co.cask.cdap.api.Transactional;
import co.cask.cdap.api.TxRunnable;
import co.cask.cdap.api.metrics.MetricsContext;
import co.cask.cdap.api.service.http.HttpContentConsumer;
import co.cask.cdap.api.service.http.HttpContentProducer;
import co.cask.cdap.api.service.http.HttpServiceContext;
import co.cask.cdap.api.service.http.HttpServiceHandler;
import co.cask.cdap.api.service.http.HttpServiceRequest;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.lang.ClassLoaders;
import co.cask.cdap.common.lang.CombineClassLoader;
import co.cask.http.BodyConsumer;
import co.cask.http.BodyProducer;
import co.cask.http.HandlerContext;
import co.cask.http.HttpHandler;
import co.cask.http.HttpResponder;
import co.cask.tephra.TransactionContext;
import co.cask.tephra.TransactionFailureException;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import org.apache.twill.common.Cancellable;
import org.jboss.netty.handler.codec.http.HttpRequest;
/**
* An abstract base class for all {@link HttpHandler} generated through the {@link HttpHandlerGenerator}.
*
* @param <T> Type of the user {@link HttpServiceHandler}.
*/
public abstract class AbstractHttpHandlerDelegator<T extends HttpServiceHandler> implements HttpHandler {
private final DelegatorContext<T> context;
private MetricsContext metricsContext;
protected AbstractHttpHandlerDelegator(DelegatorContext<T> context, MetricsContext metricsContext) {
this.context = context;
this.metricsContext = metricsContext;
}
@Override
public void init(HandlerContext context) {
}
@Override
public void destroy(HandlerContext context) {
}
/**
* Returns the {@link HttpServiceHandler} associated with the current thread.
* This method is called from handler class generated by {@link HttpHandlerGenerator}.
*/
protected final T getHandler() {
return context.getHandler();
}
/**
* Returns a {@link TransactionContext} instance to be used for creating transaction.
* This method is called from handler class generated by {@link HttpHandlerGenerator}.
*/
@SuppressWarnings("unused")
protected final TransactionContext getTransactionContext() {
HttpServiceContext serviceContext = context.getServiceContext();
Preconditions.checkState(serviceContext instanceof TransactionalHttpServiceContext,
"This instance of HttpServiceContext does not support transactions.");
return ((TransactionalHttpServiceContext) serviceContext).newTransactionContext();
}
/**
* Returns a new instance of {@link HttpServiceRequest} that wraps around the given {@link HttpRequest} object.
* This method is called from handler class generated by {@link HttpHandlerGenerator}.
*/
@SuppressWarnings("unused")
protected final HttpServiceRequest wrapRequest(HttpRequest request) {
return new DefaultHttpServiceRequest(request);
}
/**
* Returns a new instance of {@link DelayedHttpServiceResponder} that wraps around the given {@link HttpResponder}
* object. This method is called from handler class generated by {@link HttpHandlerGenerator}.
*/
@SuppressWarnings("unused")
protected final DelayedHttpServiceResponder wrapResponder(HttpResponder responder,
final TransactionContext txContext) {
MetricsContext collector = this.metricsContext;
HttpServiceContext serviceContext = context.getServiceContext();
Preconditions.checkState(serviceContext instanceof TransactionalHttpServiceContext,
"This instance of HttpServiceContext does not support transactions.");
if (serviceContext.getSpecification() != null) {
collector = metricsContext.childContext(Constants.Metrics.Tag.HANDLER,
serviceContext.getSpecification().getName());
}
return new DelayedHttpServiceResponder(responder, new BodyProducerFactory() {
@Override
public BodyProducer create(HttpContentProducer contentProducer, TransactionalHttpServiceContext serviceContext) {
final ClassLoader programContextClassLoader = new CombineClassLoader(
null, ImmutableList.of(contentProducer.getClass().getClassLoader(), getClass().getClassLoader()));
Transactional transactional = createTransactional(txContext, programContextClassLoader, serviceContext);
// Capture the context since we need to keep it till the end of the content producing.
// We don't need to worry about double capturing of the context when HttpContentConsumer is used.
// This is because when HttpContentConsumer is used, the responder constructed here will get closed and this
// BodyProducerFactory won't be used.
final Cancellable contextReleaser = context.capture();
return new BodyProducerAdapter(contentProducer, transactional, programContextClassLoader,
serviceContext, contextReleaser);
}
}, (TransactionalHttpServiceContext) serviceContext, collector);
}
/**
* Returns a new instance of {@link BodyConsumer} that wraps around the given {@link HttpContentConsumer}
* and {@link DelayedHttpServiceResponder}.
*
* IMPORTANT: This method will also capture the context associated with the current thread, hence after
* this method is called, no other methods on this class should be called from the current thread.
*
* This method is called from handler class generated by {@link HttpHandlerGenerator}.
*/
@SuppressWarnings("unused")
protected final BodyConsumer wrapContentConsumer(HttpContentConsumer consumer,
DelayedHttpServiceResponder responder,
TransactionContext txContext) {
Preconditions.checkState(!responder.hasBufferedResponse(),
"HttpContentConsumer may not be used after a response has already been sent.");
// Close the provided responder since a new one will be created for the BodyConsumerAdapter to use.
responder.close();
HttpServiceContext serviceContext = context.getServiceContext();
Preconditions.checkState(serviceContext instanceof TransactionalHttpServiceContext,
"This instance of HttpServiceContext does not support transactions.");
final Cancellable contextReleaser = context.capture();
final ClassLoader programContextClassLoader = new CombineClassLoader(
null, ImmutableList.of(consumer.getClass().getClassLoader(), getClass().getClassLoader()));
final Transactional transactional = createTransactional(txContext, programContextClassLoader, serviceContext);
return new BodyConsumerAdapter(new DelayedHttpServiceResponder(responder, new BodyProducerFactory() {
@Override
public BodyProducer create(HttpContentProducer contentProducer, TransactionalHttpServiceContext serviceContext) {
// Transfer the captured context from the content consumer to the content producer
return new BodyProducerAdapter(contentProducer, transactional,
programContextClassLoader, serviceContext, contextReleaser);
}
}), consumer, transactional, programContextClassLoader, contextReleaser);
}
private Transactional createTransactional(final TransactionContext txContext,
final ClassLoader programContextClassLoader,
final HttpServiceContext serviceContext) {
return new Transactional() {
@Override
public void execute(final TxRunnable runnable) throws TransactionFailureException {
// This method maybe called from user code, hence the context classloader maybe the
// program context classloader (or whatever the user set to).
// We need to switch the classloader back to CDAP system classloader before calling txExecute
// since it starting of transaction should happens in CDAP system context, not program context
ClassLoader callerClassLoader = ClassLoaders.setContextClassLoader(getClass().getClassLoader());
try {
txContext.start();
try {
ClassLoader oldClassLoader = ClassLoaders.setContextClassLoader(programContextClassLoader);
try {
runnable.run(serviceContext);
} finally {
ClassLoaders.setContextClassLoader(oldClassLoader);
}
} catch (Throwable t) {
txContext.abort(new TransactionFailureException("Exception raised from TxRunnable.run()", t));
}
txContext.finish();
} finally {
ClassLoaders.setContextClassLoader(callerClassLoader);
}
}
};
}
}