6b75e48e94c2dd9d2230ef45fda013f761d282e1
[utils] / crawler / basic / src / main / java / org / wamblee / crawler / AbstractPageRequest.java
1 /*
2  * Copyright 2005 the original author or authors.
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  * 
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * 
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.wamblee.crawler;
18
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24
25 import javax.xml.transform.OutputKeys;
26 import javax.xml.transform.Transformer;
27 import javax.xml.transform.TransformerConfigurationException;
28 import javax.xml.transform.TransformerException;
29 import javax.xml.transform.TransformerFactory;
30 import javax.xml.transform.dom.DOMSource;
31 import javax.xml.transform.stream.StreamResult;
32
33 import org.apache.commons.httpclient.Header;
34 import org.apache.commons.httpclient.HttpClient;
35 import org.apache.commons.httpclient.HttpMethod;
36 import org.apache.commons.httpclient.HttpStatus;
37 import org.apache.commons.httpclient.NameValuePair;
38 import org.apache.commons.httpclient.methods.GetMethod;
39 import org.apache.commons.logging.Log;
40 import org.apache.commons.logging.LogFactory;
41 import org.apache.xml.serialize.OutputFormat;
42 import org.apache.xml.serialize.XMLSerializer;
43 import org.w3c.dom.Document;
44 import org.w3c.tidy.Tidy;
45 import org.wamblee.xml.DomUtils;
46 import org.wamblee.xml.XslTransformer;
47
48 /**
49  * General support claas for all kinds of requests.
50  *
51  * @author Erik Brakkee
52  */
53 public abstract class AbstractPageRequest implements PageRequest {
54
55     private static final Log LOG = LogFactory.getLog(AbstractPageRequest.class);
56
57     private static final String REDIRECT_HEADER = "Location";
58
59     private int _maxTries;
60
61     private int _maxDelay;
62
63     private NameValuePair[] _params;
64     
65     private NameValuePair[] _headers;
66
67     private String _xslt;
68     
69     private XslTransformer _transformer; 
70
71     /**
72      * Constructs the request.
73      * 
74      * @param aMaxTries
75      *            Maximum retries to perform.
76      * @param aMaxDelay
77      *            Maximum delay before executing a request.
78      * @param aParams
79      *            Request parameters to use.
80      * @param aHeaders
81      *            Request headers to use. 
82      * @param aXslt
83      *            XSLT used to convert the response.
84      */
85     protected AbstractPageRequest(int aMaxTries, int aMaxDelay,
86             NameValuePair[] aParams, NameValuePair[] aHeaders, String aXslt, XslTransformer aTransformer) {
87         if (aParams == null) {
88             throw new IllegalArgumentException("aParams is null");
89         }
90         if (aHeaders == null) {
91             throw new IllegalArgumentException("aHeaders is null");
92         }
93         if (aXslt == null) {
94             throw new IllegalArgumentException("aXslt is null");
95         }
96         _maxTries = aMaxTries;
97         _maxDelay = aMaxDelay;
98         _params = aParams;
99         _headers = aHeaders;
100         _xslt = aXslt;
101         _transformer = aTransformer;
102     }
103
104     /*
105      * (non-Javadoc)
106      * 
107      * @see org.wamblee.crawler.PageRequest#overrideXslt(java.lang.String)
108      */
109     public void overrideXslt(String aXslt) {
110         _xslt = aXslt;
111     }
112
113     /**
114      * Gets the parameters for the request.
115      * 
116      * @param aParams Additional parameters to use, obtained from another page, most likely as
117      *    hidden form fields. 
118      * @return Request parameters.
119      */
120     protected NameValuePair[] getParameters(NameValuePair[] aParams) {
121         List<NameValuePair> params = new ArrayList<NameValuePair>(); 
122         params.addAll(Arrays.asList(_params));
123         params.addAll(Arrays.asList(aParams));
124         return params.toArray(new NameValuePair[0]);
125     }
126     
127     /**
128      * Gets the headers for the request. 
129      * @return Request headers. 
130      */
131     protected NameValuePair[] getHeaders() { 
132         return _headers;
133     }
134
135     /**
136      * Executes the request with a random delay and with a maximum number of
137      * retries.
138      * 
139      * @param aClient
140      *            HTTP client to use.
141      * @param aMethod
142      *            Method representing the request.
143      * @return XML document describing the response.
144      * @throws IOException
145      *             In case of IO problems.
146      * @throws TransformerException
147      *             In case transformation of the HTML to XML fails.
148      */
149     protected Document executeMethod(HttpClient aClient, HttpMethod aMethod)
150             throws IOException, TransformerException {
151         
152         for (NameValuePair header: getHeaders()) { 
153             aMethod.setRequestHeader(header.getName(), header.getValue());
154         }
155         
156         int triesLeft = _maxTries;
157         while (triesLeft > 0) {
158             triesLeft--;
159             try {
160                 return executeMethodWithoutRetries(aClient, aMethod);
161             } catch (TransformerException e) {
162                 if (triesLeft == 0) {
163                     throw e;
164                 }
165             }
166         }
167         throw new RuntimeException("Code should never reach this point");
168     }
169
170     /**
171      * Executes the request without doing any retries in case XSLT
172      * transformation fails.
173      * 
174      * @param aClient
175      *            HTTP client to use.
176      * @param aMethod
177      *            Method to execute.
178      * @return XML document containing the result.
179      * @throws IOException
180      *             In case of IO problems.
181      * @throws TransformerException
182      *             In case transformation of the result to XML fails.
183      */
184     protected Document executeMethodWithoutRetries(HttpClient aClient,
185             HttpMethod aMethod) throws IOException, TransformerException {
186         try {
187             aMethod = executeWithRedirects(aClient, aMethod);
188             byte[] xhtmlData = getXhtml(aMethod);
189             
190      
191             Document transformed = _transformer.transform(xhtmlData,
192                     _transformer.resolve(_xslt));
193             ByteArrayOutputStream os = new ByteArrayOutputStream(); 
194             Transformer transformer = TransformerFactory.newInstance()
195                     .newTransformer();
196             transformer.setParameter(OutputKeys.INDENT, "yes");
197             transformer.setParameter(OutputKeys.METHOD, "xml");
198             transformer.transform(new DOMSource(transformed), new StreamResult(
199                     os));
200             LOG.debug("Transformed result is \n" + os.toString());
201             return transformed;
202         } catch (TransformerConfigurationException e) {
203             throw new TransformerException("Transformer configuration problem", e);
204         } finally {
205             // Release the connection.
206             aMethod.releaseConnection();
207         }
208     }
209
210     /**
211      * Gets the result of the HTTP method as an XHTML document.
212      * 
213      * @param aMethod
214      *            Method to invoke.
215      * @return XHTML as a byte array.
216      * @throws IOException
217      *             In case of problems obtaining the XHTML.
218      */
219     private byte[] getXhtml(HttpMethod aMethod) throws IOException {
220         // Transform the HTML into wellformed XML.
221         Tidy tidy = new Tidy();
222         tidy.setXHTML(true);
223         tidy.setQuiet(true);
224         tidy.setShowWarnings(false);
225       
226         // We write the jtidy output to XML since the DOM tree it produces is
227         // not namespace aware and namespace awareness is required by XSLT.
228         // An alternative is to configure namespace awareness of the XML parser
229         // in a system wide way.
230         ByteArrayOutputStream os = new ByteArrayOutputStream(); 
231         Document w3cDoc = tidy.parseDOM(aMethod.getResponseBodyAsStream(), os);
232         DomUtils.removeDuplicateAttributes(w3cDoc);
233         LOG.debug("Content of response is \n" + os.toString()); 
234
235         ByteArrayOutputStream xhtml = new ByteArrayOutputStream();
236         XMLSerializer serializer = new XMLSerializer(xhtml, new OutputFormat());
237         serializer.serialize(w3cDoc);
238         xhtml.flush();
239
240         return xhtml.toByteArray();
241     }
242
243     /**
244      * Sleeps for a random time but no more than the maximum delay.
245      * 
246      */
247     private void delay() {
248         try {
249             Thread.sleep((long) ((float) _maxDelay * Math.random()));
250         } catch (InterruptedException e) {
251             return; // to satisfy checkstyle
252         }
253     }
254
255     /**
256      * Executes the request and follows redirects if needed.
257      * 
258      * @param aClient
259      *            HTTP client to use.
260      * @param aMethod
261      *            Method to use.
262      * @return Final HTTP method used (differs from the parameter passed in in
263      *         case of redirection).
264      * @throws IOException
265      *             In case of network problems.
266      */
267     private HttpMethod executeWithRedirects(HttpClient aClient,
268             HttpMethod aMethod) throws IOException {
269         delay();
270         int statusCode = aClient.executeMethod(aMethod);
271
272         switch (statusCode) {
273         case HttpStatus.SC_OK: {
274             return aMethod;
275         }
276         case HttpStatus.SC_MOVED_PERMANENTLY:
277         case HttpStatus.SC_MOVED_TEMPORARILY:
278         case HttpStatus.SC_SEE_OTHER: {
279             aMethod.releaseConnection();
280             Header header = aMethod.getResponseHeader(REDIRECT_HEADER);
281             aMethod = new GetMethod(header.getValue());
282             return executeWithRedirects(aClient, aMethod); // TODO protect
283             // against infinite
284             // recursion.
285         }
286         default: {
287             throw new IOException("Method failed: "
288                     + aMethod.getStatusLine());
289         }
290         }
291     }
292 }