View Javadoc
1   /* 
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   *
14   */
15  
16  package org.esigate;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.List;
22  import java.util.Properties;
23  
24  import org.apache.commons.io.output.StringBuilderWriter;
25  import org.apache.commons.lang3.tuple.ImmutablePair;
26  import org.apache.commons.lang3.tuple.Pair;
27  import org.apache.http.HttpEntity;
28  import org.apache.http.HttpStatus;
29  import org.apache.http.ProtocolException;
30  import org.apache.http.client.methods.CloseableHttpResponse;
31  import org.apache.http.entity.ContentType;
32  import org.apache.http.entity.StringEntity;
33  import org.apache.http.message.BasicHttpResponse;
34  import org.apache.http.util.EntityUtils;
35  import org.esigate.RequestExecutor.RequestExecutorBuilder;
36  import org.esigate.api.RedirectStrategy2;
37  import org.esigate.events.EventManager;
38  import org.esigate.events.impl.ProxyEvent;
39  import org.esigate.events.impl.RenderEvent;
40  import org.esigate.extension.ExtensionFactory;
41  import org.esigate.http.BasicCloseableHttpResponse;
42  import org.esigate.http.ContentTypeHelper;
43  import org.esigate.http.HeaderManager;
44  import org.esigate.http.HttpClientRequestExecutor;
45  import org.esigate.http.HttpResponseUtils;
46  import org.esigate.http.IncomingRequest;
47  import org.esigate.http.OutgoingRequest;
48  import org.esigate.http.ResourceUtils;
49  import org.esigate.impl.DriverRequest;
50  import org.esigate.impl.FragmentRedirectStrategy;
51  import org.esigate.impl.UrlRewriter;
52  import org.esigate.vars.VariablesResolver;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  /**
57   * Main class used to retrieve data from a provider application using HTTP requests. Data can be retrieved as binary
58   * streams or as String for text data. To improve performance, the Driver uses a cache that can be configured depending
59   * on the needs.
60   * 
61   * @author Francois-Xavier Bonnet
62   * @author Nicolas Richeton
63   * @author Sylvain Sicard
64   */
65  public final class Driver {
66      private static final String CACHE_RESPONSE_PREFIX = "response_";
67      private static final Logger LOG = LoggerFactory.getLogger(Driver.class);
68      private static final int MAX_REDIRECTS = 50;
69      private DriverConfiguration config;
70      private EventManager eventManager;
71      private RequestExecutor requestExecutor;
72      private ContentTypeHelper contentTypeHelper;
73      private UrlRewriter urlRewriter;
74      private HeaderManager headerManager;
75      private final RedirectStrategy2 redirectStrategy = new FragmentRedirectStrategy();
76  
77      public static class DriverBuilder {
78          private Driver driver = new Driver();
79          private String name;
80          private Properties properties;
81          private RequestExecutorBuilder requestExecutorBuilder;
82  
83          public Driver build() {
84              if (name == null) {
85                  throw new ConfigurationException("name is mandatory");
86              }
87              if (properties == null) {
88                  throw new ConfigurationException("properties is mandatory");
89              }
90              if (requestExecutorBuilder == null) {
91                  requestExecutorBuilder = HttpClientRequestExecutor.builder();
92              }
93              driver.eventManager = new EventManager(name);
94              driver.config = new DriverConfiguration(name, properties);
95              driver.contentTypeHelper = new ContentTypeHelper(properties);
96              // Load extensions.
97              ExtensionFactory.getExtensions(properties, Parameters.EXTENSIONS, driver);
98              UrlRewriter urlRewriter = new UrlRewriter();
99              driver.requestExecutor =
100                     requestExecutorBuilder.setDriver(driver).setEventManager(driver.eventManager)
101                             .setProperties(properties).setContentTypeHelper(driver.contentTypeHelper).build();
102             driver.urlRewriter = urlRewriter;
103             driver.headerManager = new HeaderManager(urlRewriter);
104 
105             return driver;
106         }
107 
108         public DriverBuilder setName(String n) {
109             this.name = n;
110             return this;
111         }
112 
113         public DriverBuilder setProperties(Properties p) {
114             this.properties = p;
115             return this;
116         }
117 
118         public DriverBuilder setRequestExecutorBuilder(RequestExecutorBuilder builder) {
119             this.requestExecutorBuilder = builder;
120             return this;
121         }
122 
123     }
124 
125     protected Driver() {
126     }
127 
128     public static DriverBuilder builder() {
129         return new DriverBuilder();
130     }
131 
132     /**
133      * Get current event manager for this driver instance.
134      * 
135      * @return event manager.
136      */
137     public EventManager getEventManager() {
138         return this.eventManager;
139     }
140 
141     /**
142      * Perform rendering on a single url content, and append result to "writer". Automatically follows redirects
143      * 
144      * @param pageUrl
145      *            Address of the page containing the template
146      * @param incomingRequest
147      *            originating request object
148      * @param renderers
149      *            the renderers to use in order to transform the output
150      * @return The resulting response
151      * @throws IOException
152      *             If an IOException occurs while writing to the writer
153      * @throws HttpErrorPage
154      *             If an Exception occurs while retrieving the template
155      */
156     public CloseableHttpResponse render(String pageUrl, IncomingRequest incomingRequest, Renderer... renderers)
157             throws IOException, HttpErrorPage {
158         DriverRequest driverRequest = new DriverRequest(incomingRequest, this, pageUrl);
159 
160         // Replace ESI variables in URL
161         // TODO: should be performed in the ESI extension
162         String resultingPageUrl = VariablesResolver.replaceAllVariables(pageUrl, driverRequest);
163 
164         String targetUrl = ResourceUtils.getHttpUrlWithQueryString(resultingPageUrl, driverRequest, false);
165 
166         String currentValue;
167         CloseableHttpResponse response;
168 
169         // Retrieve URL
170         // Get from cache to prevent multiple request to the same url if
171         // multiple fragments are used.
172 
173         String cacheKey = CACHE_RESPONSE_PREFIX + targetUrl;
174         Pair<String, CloseableHttpResponse> cachedValue = incomingRequest.getAttribute(cacheKey);
175 
176         // content and response were not in cache
177         if (cachedValue == null) {
178             OutgoingRequest outgoingRequest = requestExecutor.createOutgoingRequest(driverRequest, targetUrl, false);
179             headerManager.copyHeaders(driverRequest, outgoingRequest);
180             response = requestExecutor.execute(outgoingRequest);
181             int redirects = MAX_REDIRECTS;
182             try {
183                 while (redirects > 0
184                         && this.redirectStrategy.isRedirected(outgoingRequest, response, outgoingRequest.getContext())) {
185 
186                     // Must consume the entity
187                     EntityUtils.consumeQuietly(response.getEntity());
188 
189                     redirects--;
190 
191                     // Perform new request
192                     outgoingRequest =
193                             this.requestExecutor.createOutgoingRequest(
194                                     driverRequest,
195                                     this.redirectStrategy.getLocationURI(outgoingRequest, response,
196                                             outgoingRequest.getContext()).toString(), false);
197                     this.headerManager.copyHeaders(driverRequest, outgoingRequest);
198                     response = requestExecutor.execute(outgoingRequest);
199                 }
200             } catch (ProtocolException e) {
201                 throw new HttpErrorPage(HttpStatus.SC_BAD_GATEWAY, "Invalid response from server", e);
202             }
203             response = this.headerManager.copyHeaders(outgoingRequest, incomingRequest, response);
204             currentValue = HttpResponseUtils.toString(response, this.eventManager);
205             // Cache
206             cachedValue = new ImmutablePair<>(currentValue, response);
207             incomingRequest.setAttribute(cacheKey, cachedValue);
208         }
209         currentValue = cachedValue.getKey();
210         response = cachedValue.getValue();
211 
212         logAction("render", pageUrl, renderers);
213 
214         // Apply renderers
215         currentValue = performRendering(pageUrl, driverRequest, response, currentValue, renderers);
216 
217         response.setEntity(new StringEntity(currentValue, HttpResponseUtils.getContentType(response)));
218 
219         return response;
220     }
221 
222     /**
223      * Log current provider, page and renderers that will be applied.
224      * <p>
225      * This methods log at the INFO level.
226      * <p>
227      * You should only call this method if INFO level is enabled.
228      * 
229      * <pre>
230      * if (LOG.isInfoEnabled()) {
231      *     logAction(pageUrl, renderers);
232      * }
233      * </pre>
234      * 
235      * @param action
236      *            Action name (eg. "proxy" or "render")
237      * @param onUrl
238      *            current page url.
239      * @param renderers
240      *            array of renderers
241      * 
242      */
243     private void logAction(String action, String onUrl, Renderer[] renderers) {
244         if (LOG.isInfoEnabled()) {
245             List<String> rendererNames = new ArrayList<>(renderers.length);
246             for (Renderer renderer : renderers) {
247                 rendererNames.add(renderer.getClass().getName());
248             }
249             LOG.info("{} provider={} page= {} renderers={}", action, this.config.getInstanceName(), onUrl,
250                     rendererNames);
251         }
252     }
253 
254     /**
255      * Retrieves a resource from the provider application and transforms it using the Renderer passed as a parameter.
256      * 
257      * @param relUrl
258      *            the relative URL to the resource
259      * @param incomingRequest
260      *            the request
261      * @param renderers
262      *            the renderers to use to transform the output
263      * @return The resulting response.
264      * @throws IOException
265      *             If an IOException occurs while writing to the response
266      * @throws HttpErrorPage
267      *             If the page contains incorrect tags
268      */
269     public CloseableHttpResponse proxy(String relUrl, IncomingRequest incomingRequest, Renderer... renderers)
270             throws IOException, HttpErrorPage {
271         DriverRequest driverRequest = new DriverRequest(incomingRequest, this, relUrl);
272         driverRequest.setCharacterEncoding(this.config.getUriEncoding());
273 
274         // This is used to ensure EVENT_PROXY_POST is called once and only once.
275         // there are 3 different cases
276         // - Success -> the main code
277         // - Error page -> the HttpErrorPage exception
278         // - Unexpected error -> Other Exceptions
279         boolean postProxyPerformed = false;
280 
281         // Create Proxy event
282         ProxyEvent e = new ProxyEvent(incomingRequest);
283 
284         // Event pre-proxy
285         this.eventManager.fire(EventManager.EVENT_PROXY_PRE, e);
286         // Return immediately if exit is requested by extension
287         if (e.isExit()) {
288             return e.getResponse();
289         }
290 
291         logAction("proxy", relUrl, renderers);
292 
293         String url = ResourceUtils.getHttpUrlWithQueryString(relUrl, driverRequest, true);
294         OutgoingRequest outgoingRequest = requestExecutor.createOutgoingRequest(driverRequest, url, true);
295         headerManager.copyHeaders(driverRequest, outgoingRequest);
296 
297         try {
298             CloseableHttpResponse response = requestExecutor.execute(outgoingRequest);
299 
300             response = headerManager.copyHeaders(outgoingRequest, incomingRequest, response);
301 
302             e.setResponse(response);
303 
304             // Perform rendering
305             e.setResponse(performRendering(relUrl, driverRequest, e.getResponse(), renderers));
306 
307             // Event post-proxy
308             // This must be done before calling sendResponse to ensure response
309             // can still be changed.
310             postProxyPerformed = true;
311             this.eventManager.fire(EventManager.EVENT_PROXY_POST, e);
312 
313             // Send request to the client.
314             return e.getResponse();
315 
316         } catch (HttpErrorPage errorPage) {
317             e.setErrorPage(errorPage);
318 
319             // On error returned by the proxy request, perform rendering on the
320             // error page.
321             CloseableHttpResponse response = e.getErrorPage().getHttpResponse();
322             response = headerManager.copyHeaders(outgoingRequest, incomingRequest, response);
323             e.setErrorPage(new HttpErrorPage(performRendering(relUrl, driverRequest, response, renderers)));
324 
325             // Event post-proxy
326             // This must be done before throwing exception to ensure response
327             // can still be changed.
328             postProxyPerformed = true;
329             this.eventManager.fire(EventManager.EVENT_PROXY_POST, e);
330 
331             throw e.getErrorPage();
332         } finally {
333             if (!postProxyPerformed) {
334                 this.eventManager.fire(EventManager.EVENT_PROXY_POST, e);
335             }
336         }
337     }
338 
339     /**
340      * Performs rendering on an HttpResponse.
341      * <p>
342      * Rendering is only performed if page can be parsed.
343      * 
344      * @param pageUrl
345      *            The remove url from which the body was retrieved.
346      * @param originalRequest
347      *            The request received by esigate.
348      * @param response
349      *            The response which will be rendered.
350      * @param renderers
351      *            list of renderers to apply.
352      * @return The rendered response, or the original response if if was not parsed.
353      * @throws HttpErrorPage
354      * @throws IOException
355      */
356     private CloseableHttpResponse performRendering(String pageUrl, DriverRequest originalRequest,
357             CloseableHttpResponse response, Renderer[] renderers) throws HttpErrorPage, IOException {
358 
359         if (!contentTypeHelper.isTextContentType(response)) {
360             LOG.debug("'{}' is binary on no transformation to apply: was forwarded without modification.", pageUrl);
361             return response;
362         }
363 
364         LOG.debug("'{}' is text : will apply renderers.", pageUrl);
365 
366         // Get response body
367         String currentValue = HttpResponseUtils.toString(response, this.eventManager);
368 
369         // Perform rendering
370         currentValue = performRendering(pageUrl, originalRequest, response, currentValue, renderers);
371 
372         // Generate the new response.
373         HttpEntity transformedHttpEntity = new StringEntity(currentValue, ContentType.get(response.getEntity()));
374         CloseableHttpResponse transformedResponse =
375                 BasicCloseableHttpResponse.adapt(new BasicHttpResponse(response.getStatusLine()));
376         transformedResponse.setHeaders(response.getAllHeaders());
377         transformedResponse.setEntity(transformedHttpEntity);
378         return transformedResponse;
379 
380     }
381 
382     /**
383      * Performs rendering (apply a render list) on an http response body (as a String).
384      * 
385      * @param pageUrl
386      *            The remove url from which the body was retrieved.
387      * @param originalRequest
388      *            The request received by esigate.
389      * @param response
390      *            The Http Reponse.
391      * @param body
392      *            The body of the Http Response which will be rendered.
393      * @param renderers
394      *            list of renderers to apply.
395      * @return The rendered response body.
396      * @throws HttpErrorPage
397      * @throws IOException
398      */
399     private String performRendering(String pageUrl, DriverRequest originalRequest, CloseableHttpResponse response,
400             String body, Renderer[] renderers) throws IOException, HttpErrorPage {
401         // Start rendering
402         RenderEvent renderEvent = new RenderEvent(pageUrl, originalRequest, response);
403         // Create renderer list from parameters.
404         renderEvent.getRenderers().addAll(Arrays.asList(renderers));
405 
406         String currentBody = body;
407 
408         this.eventManager.fire(EventManager.EVENT_RENDER_PRE, renderEvent);
409         for (Renderer renderer : renderEvent.getRenderers()) {
410             StringBuilderWriter stringWriter = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
411             renderer.render(originalRequest, currentBody, stringWriter);
412             stringWriter.close();
413             currentBody = stringWriter.toString();
414         }
415         this.eventManager.fire(EventManager.EVENT_RENDER_POST, renderEvent);
416 
417         return currentBody;
418     }
419 
420     /**
421      * Get current driver configuration.
422      * <p>
423      * This method is not intended to get a WRITE access to the configuration.
424      * <p>
425      * This may be supported in future versions (testing is needed). For the time being, changing configuration settings
426      * after getting access through this method is <b>UNSUPPORTED</b> and <b>SHOULD NOT</b> be used.
427      * 
428      * @return current configuration
429      */
430     public DriverConfiguration getConfiguration() {
431         return this.config;
432     }
433 
434     public RequestExecutor getRequestExecutor() {
435         return requestExecutor;
436     }
437 
438     @Override
439     public String toString() {
440         return "driver:" + config.getInstanceName();
441     }
442 
443     public ContentTypeHelper getContentTypeHelper() {
444         return contentTypeHelper;
445     }
446 
447     public UrlRewriter getUrlRewriter() {
448         return urlRewriter;
449     }
450 
451 }