Driver.java
/*
* 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.esigate;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.util.EntityUtils;
import org.esigate.RequestExecutor.RequestExecutorBuilder;
import org.esigate.api.RedirectStrategy2;
import org.esigate.events.EventManager;
import org.esigate.events.impl.ProxyEvent;
import org.esigate.events.impl.RenderEvent;
import org.esigate.extension.ExtensionFactory;
import org.esigate.http.BasicCloseableHttpResponse;
import org.esigate.http.ContentTypeHelper;
import org.esigate.http.HeaderManager;
import org.esigate.http.HttpClientRequestExecutor;
import org.esigate.http.HttpResponseUtils;
import org.esigate.http.IncomingRequest;
import org.esigate.http.OutgoingRequest;
import org.esigate.http.ResourceUtils;
import org.esigate.impl.DriverRequest;
import org.esigate.impl.FragmentRedirectStrategy;
import org.esigate.impl.UrlRewriter;
import org.esigate.vars.VariablesResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main class used to retrieve data from a provider application using HTTP requests. Data can be retrieved as binary
* streams or as String for text data. To improve performance, the Driver uses a cache that can be configured depending
* on the needs.
*
* @author Francois-Xavier Bonnet
* @author Nicolas Richeton
* @author Sylvain Sicard
*/
public final class Driver {
private static final String CACHE_RESPONSE_PREFIX = "response_";
private static final Logger LOG = LoggerFactory.getLogger(Driver.class);
private static final int MAX_REDIRECTS = 50;
private DriverConfiguration config;
private EventManager eventManager;
private RequestExecutor requestExecutor;
private ContentTypeHelper contentTypeHelper;
private UrlRewriter urlRewriter;
private HeaderManager headerManager;
private final RedirectStrategy2 redirectStrategy = new FragmentRedirectStrategy();
public static class DriverBuilder {
private Driver driver = new Driver();
private String name;
private Properties properties;
private RequestExecutorBuilder requestExecutorBuilder;
public Driver build() {
if (name == null) {
throw new ConfigurationException("name is mandatory");
}
if (properties == null) {
throw new ConfigurationException("properties is mandatory");
}
if (requestExecutorBuilder == null) {
requestExecutorBuilder = HttpClientRequestExecutor.builder();
}
driver.eventManager = new EventManager(name);
driver.config = new DriverConfiguration(name, properties);
driver.contentTypeHelper = new ContentTypeHelper(properties);
// Load extensions.
ExtensionFactory.getExtensions(properties, Parameters.EXTENSIONS, driver);
UrlRewriter urlRewriter = new UrlRewriter();
driver.requestExecutor =
requestExecutorBuilder.setDriver(driver).setEventManager(driver.eventManager)
.setProperties(properties).setContentTypeHelper(driver.contentTypeHelper).build();
driver.urlRewriter = urlRewriter;
driver.headerManager = new HeaderManager(urlRewriter);
return driver;
}
public DriverBuilder setName(String n) {
this.name = n;
return this;
}
public DriverBuilder setProperties(Properties p) {
this.properties = p;
return this;
}
public DriverBuilder setRequestExecutorBuilder(RequestExecutorBuilder builder) {
this.requestExecutorBuilder = builder;
return this;
}
}
protected Driver() {
}
public static DriverBuilder builder() {
return new DriverBuilder();
}
/**
* Get current event manager for this driver instance.
*
* @return event manager.
*/
public EventManager getEventManager() {
return this.eventManager;
}
/**
* Perform rendering on a single url content, and append result to "writer". Automatically follows redirects
*
* @param pageUrl
* Address of the page containing the template
* @param incomingRequest
* originating request object
* @param renderers
* the renderers to use in order to transform the output
* @return The resulting response
* @throws IOException
* If an IOException occurs while writing to the writer
* @throws HttpErrorPage
* If an Exception occurs while retrieving the template
*/
public CloseableHttpResponse render(String pageUrl, IncomingRequest incomingRequest, Renderer... renderers)
throws IOException, HttpErrorPage {
DriverRequest driverRequest = new DriverRequest(incomingRequest, this, pageUrl);
// Replace ESI variables in URL
// TODO: should be performed in the ESI extension
String resultingPageUrl = VariablesResolver.replaceAllVariables(pageUrl, driverRequest);
String targetUrl = ResourceUtils.getHttpUrlWithQueryString(resultingPageUrl, driverRequest, false);
String currentValue;
CloseableHttpResponse response;
// Retrieve URL
// Get from cache to prevent multiple request to the same url if
// multiple fragments are used.
String cacheKey = CACHE_RESPONSE_PREFIX + targetUrl;
Pair<String, CloseableHttpResponse> cachedValue = incomingRequest.getAttribute(cacheKey);
// content and response were not in cache
if (cachedValue == null) {
OutgoingRequest outgoingRequest = requestExecutor.createOutgoingRequest(driverRequest, targetUrl, false);
headerManager.copyHeaders(driverRequest, outgoingRequest);
response = requestExecutor.execute(outgoingRequest);
int redirects = MAX_REDIRECTS;
try {
while (redirects > 0
&& this.redirectStrategy.isRedirected(outgoingRequest, response, outgoingRequest.getContext())) {
// Must consume the entity
EntityUtils.consumeQuietly(response.getEntity());
redirects--;
// Perform new request
outgoingRequest =
this.requestExecutor.createOutgoingRequest(
driverRequest,
this.redirectStrategy.getLocationURI(outgoingRequest, response,
outgoingRequest.getContext()).toString(), false);
this.headerManager.copyHeaders(driverRequest, outgoingRequest);
response = requestExecutor.execute(outgoingRequest);
}
} catch (ProtocolException e) {
throw new HttpErrorPage(HttpStatus.SC_BAD_GATEWAY, "Invalid response from server", e);
}
response = this.headerManager.copyHeaders(outgoingRequest, incomingRequest, response);
currentValue = HttpResponseUtils.toString(response, this.eventManager);
// Cache
cachedValue = new ImmutablePair<>(currentValue, response);
incomingRequest.setAttribute(cacheKey, cachedValue);
}
currentValue = cachedValue.getKey();
response = cachedValue.getValue();
logAction("render", pageUrl, renderers);
// Apply renderers
currentValue = performRendering(pageUrl, driverRequest, response, currentValue, renderers);
response.setEntity(new StringEntity(currentValue, HttpResponseUtils.getContentType(response)));
return response;
}
/**
* Log current provider, page and renderers that will be applied.
* <p>
* This methods log at the INFO level.
* <p>
* You should only call this method if INFO level is enabled.
*
* <pre>
* if (LOG.isInfoEnabled()) {
* logAction(pageUrl, renderers);
* }
* </pre>
*
* @param action
* Action name (eg. "proxy" or "render")
* @param onUrl
* current page url.
* @param renderers
* array of renderers
*
*/
private void logAction(String action, String onUrl, Renderer[] renderers) {
if (LOG.isInfoEnabled()) {
List<String> rendererNames = new ArrayList<>(renderers.length);
for (Renderer renderer : renderers) {
rendererNames.add(renderer.getClass().getName());
}
LOG.info("{} provider={} page= {} renderers={}", action, this.config.getInstanceName(), onUrl,
rendererNames);
}
}
/**
* Retrieves a resource from the provider application and transforms it using the Renderer passed as a parameter.
*
* @param relUrl
* the relative URL to the resource
* @param incomingRequest
* the request
* @param renderers
* the renderers to use to transform the output
* @return The resulting response.
* @throws IOException
* If an IOException occurs while writing to the response
* @throws HttpErrorPage
* If the page contains incorrect tags
*/
public CloseableHttpResponse proxy(String relUrl, IncomingRequest incomingRequest, Renderer... renderers)
throws IOException, HttpErrorPage {
DriverRequest driverRequest = new DriverRequest(incomingRequest, this, relUrl);
driverRequest.setCharacterEncoding(this.config.getUriEncoding());
// This is used to ensure EVENT_PROXY_POST is called once and only once.
// there are 3 different cases
// - Success -> the main code
// - Error page -> the HttpErrorPage exception
// - Unexpected error -> Other Exceptions
boolean postProxyPerformed = false;
// Create Proxy event
ProxyEvent e = new ProxyEvent(incomingRequest);
// Event pre-proxy
this.eventManager.fire(EventManager.EVENT_PROXY_PRE, e);
// Return immediately if exit is requested by extension
if (e.isExit()) {
return e.getResponse();
}
logAction("proxy", relUrl, renderers);
String url = ResourceUtils.getHttpUrlWithQueryString(relUrl, driverRequest, true);
OutgoingRequest outgoingRequest = requestExecutor.createOutgoingRequest(driverRequest, url, true);
headerManager.copyHeaders(driverRequest, outgoingRequest);
try {
CloseableHttpResponse response = requestExecutor.execute(outgoingRequest);
response = headerManager.copyHeaders(outgoingRequest, incomingRequest, response);
e.setResponse(response);
// Perform rendering
e.setResponse(performRendering(relUrl, driverRequest, e.getResponse(), renderers));
// Event post-proxy
// This must be done before calling sendResponse to ensure response
// can still be changed.
postProxyPerformed = true;
this.eventManager.fire(EventManager.EVENT_PROXY_POST, e);
// Send request to the client.
return e.getResponse();
} catch (HttpErrorPage errorPage) {
e.setErrorPage(errorPage);
// On error returned by the proxy request, perform rendering on the
// error page.
CloseableHttpResponse response = e.getErrorPage().getHttpResponse();
response = headerManager.copyHeaders(outgoingRequest, incomingRequest, response);
e.setErrorPage(new HttpErrorPage(performRendering(relUrl, driverRequest, response, renderers)));
// Event post-proxy
// This must be done before throwing exception to ensure response
// can still be changed.
postProxyPerformed = true;
this.eventManager.fire(EventManager.EVENT_PROXY_POST, e);
throw e.getErrorPage();
} finally {
if (!postProxyPerformed) {
this.eventManager.fire(EventManager.EVENT_PROXY_POST, e);
}
}
}
/**
* Performs rendering on an HttpResponse.
* <p>
* Rendering is only performed if page can be parsed.
*
* @param pageUrl
* The remove url from which the body was retrieved.
* @param originalRequest
* The request received by esigate.
* @param response
* The response which will be rendered.
* @param renderers
* list of renderers to apply.
* @return The rendered response, or the original response if if was not parsed.
* @throws HttpErrorPage
* @throws IOException
*/
private CloseableHttpResponse performRendering(String pageUrl, DriverRequest originalRequest,
CloseableHttpResponse response, Renderer[] renderers) throws HttpErrorPage, IOException {
if (!contentTypeHelper.isTextContentType(response)) {
LOG.debug("'{}' is binary on no transformation to apply: was forwarded without modification.", pageUrl);
return response;
}
LOG.debug("'{}' is text : will apply renderers.", pageUrl);
// Get response body
String currentValue = HttpResponseUtils.toString(response, this.eventManager);
// Perform rendering
currentValue = performRendering(pageUrl, originalRequest, response, currentValue, renderers);
// Generate the new response.
HttpEntity transformedHttpEntity = new StringEntity(currentValue, ContentType.get(response.getEntity()));
CloseableHttpResponse transformedResponse =
BasicCloseableHttpResponse.adapt(new BasicHttpResponse(response.getStatusLine()));
transformedResponse.setHeaders(response.getAllHeaders());
transformedResponse.setEntity(transformedHttpEntity);
return transformedResponse;
}
/**
* Performs rendering (apply a render list) on an http response body (as a String).
*
* @param pageUrl
* The remove url from which the body was retrieved.
* @param originalRequest
* The request received by esigate.
* @param response
* The Http Reponse.
* @param body
* The body of the Http Response which will be rendered.
* @param renderers
* list of renderers to apply.
* @return The rendered response body.
* @throws HttpErrorPage
* @throws IOException
*/
private String performRendering(String pageUrl, DriverRequest originalRequest, CloseableHttpResponse response,
String body, Renderer[] renderers) throws IOException, HttpErrorPage {
// Start rendering
RenderEvent renderEvent = new RenderEvent(pageUrl, originalRequest, response);
// Create renderer list from parameters.
renderEvent.getRenderers().addAll(Arrays.asList(renderers));
String currentBody = body;
this.eventManager.fire(EventManager.EVENT_RENDER_PRE, renderEvent);
for (Renderer renderer : renderEvent.getRenderers()) {
StringBuilderWriter stringWriter = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
renderer.render(originalRequest, currentBody, stringWriter);
stringWriter.close();
currentBody = stringWriter.toString();
}
this.eventManager.fire(EventManager.EVENT_RENDER_POST, renderEvent);
return currentBody;
}
/**
* Get current driver configuration.
* <p>
* This method is not intended to get a WRITE access to the configuration.
* <p>
* This may be supported in future versions (testing is needed). For the time being, changing configuration settings
* after getting access through this method is <b>UNSUPPORTED</b> and <b>SHOULD NOT</b> be used.
*
* @return current configuration
*/
public DriverConfiguration getConfiguration() {
return this.config;
}
public RequestExecutor getRequestExecutor() {
return requestExecutor;
}
@Override
public String toString() {
return "driver:" + config.getInstanceName();
}
public ContentTypeHelper getContentTypeHelper() {
return contentTypeHelper;
}
public UrlRewriter getUrlRewriter() {
return urlRewriter;
}
}