(no commit message)
[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 public abstract class AbstractPageRequest implements PageRequest {
52
53     private static final Log LOG = LogFactory.getLog(AbstractPageRequest.class);
54
55     private static final String REDIRECT_HEADER = "Location";
56
57     private int _maxTries;
58
59     private int _maxDelay;
60
61     private NameValuePair[] _params;
62     
63     private NameValuePair[] _headers;
64
65     private String _xslt;
66     
67     private XslTransformer _transformer; 
68
69     /**
70      * Constructs the request.
71      * 
72      * @param aMaxTries
73      *            Maximum retries to perform.
74      * @param aMaxDelay
75      *            Maximum delay before executing a request.
76      * @param aParams
77      *            Request parameters to use.
78      * @param aHeaders
79      *            Request headers to use. 
80      * @param aXslt
81      *            XSLT used to convert the response.
82      */
83     protected AbstractPageRequest(int aMaxTries, int aMaxDelay,
84             NameValuePair[] aParams, NameValuePair[] aHeaders, String aXslt, XslTransformer aTransformer) {
85         if (aParams == null) {
86             throw new IllegalArgumentException("aParams is null");
87         }
88         if (aHeaders == null) {
89             throw new IllegalArgumentException("aHeaders is null");
90         }
91         if (aXslt == null) {
92             throw new IllegalArgumentException("aXslt is null");
93         }
94         _maxTries = aMaxTries;
95         _maxDelay = aMaxDelay;
96         _params = aParams;
97         _headers = aHeaders;
98         _xslt = aXslt;
99         _transformer = aTransformer;
100     }
101
102     /*
103      * (non-Javadoc)
104      * 
105      * @see org.wamblee.crawler.PageRequest#overrideXslt(java.lang.String)
106      */
107     public void overrideXslt(String aXslt) {
108         _xslt = aXslt;
109     }
110
111     /**
112      * Gets the parameters for the request.
113      * 
114      * @param aParams Additional parameters to use, obtained from another page, most likely as
115      *    hidden form fields. 
116      * @return Request parameters.
117      */
118     protected NameValuePair[] getParameters(NameValuePair[] aParams) {
119         List<NameValuePair> params = new ArrayList<NameValuePair>(); 
120         params.addAll(Arrays.asList(_params));
121         params.addAll(Arrays.asList(aParams));
122         return params.toArray(new NameValuePair[0]);
123     }
124     
125     /**
126      * Gets the headers for the request. 
127      * @return Request headers. 
128      */
129     protected NameValuePair[] getHeaders() { 
130         return _headers;
131     }
132
133     /**
134      * Executes the request with a random delay and with a maximum number of
135      * retries.
136      * 
137      * @param aClient
138      *            HTTP client to use.
139      * @param aMethod
140      *            Method representing the request.
141      * @return XML document describing the response.
142      * @throws IOException
143      *             In case of IO problems.
144      * @throws TransformerException
145      *             In case transformation of the HTML to XML fails.
146      */
147     protected Document executeMethod(HttpClient aClient, HttpMethod aMethod)
148             throws IOException, TransformerException {
149         
150         for (NameValuePair header: getHeaders()) { 
151             aMethod.setRequestHeader(header.getName(), header.getValue());
152         }
153         
154         int triesLeft = _maxTries;
155         while (triesLeft > 0) {
156             triesLeft--;
157             try {
158                 return executeMethodWithoutRetries(aClient, aMethod);
159             } catch (TransformerException e) {
160                 if (triesLeft == 0) {
161                     throw e;
162                 }
163             }
164         }
165         throw new RuntimeException("Code should never reach this point");
166     }
167
168     /**
169      * Executes the request without doing any retries in case XSLT
170      * transformation fails.
171      * 
172      * @param aClient
173      *            HTTP client to use.
174      * @param aMethod
175      *            Method to execute.
176      * @return XML document containing the result.
177      * @throws IOException
178      *             In case of IO problems.
179      * @throws TransformerException
180      *             In case transformation of the result to XML fails.
181      */
182     protected Document executeMethodWithoutRetries(HttpClient aClient,
183             HttpMethod aMethod) throws IOException, TransformerException {
184         try {
185             aMethod = executeWithRedirects(aClient, aMethod);
186             byte[] xhtmlData = getXhtml(aMethod);
187             
188      
189             Document transformed = _transformer.transform(xhtmlData,
190                     _transformer.resolve(_xslt));
191             ByteArrayOutputStream os = new ByteArrayOutputStream(); 
192             Transformer transformer = TransformerFactory.newInstance()
193                     .newTransformer();
194             transformer.setParameter(OutputKeys.INDENT, "yes");
195             transformer.setParameter(OutputKeys.METHOD, "xml");
196             transformer.transform(new DOMSource(transformed), new StreamResult(
197                     os));
198             LOG.debug("Transformed result is \n" + os.toString());
199             return transformed;
200         } catch (TransformerConfigurationException e) {
201             throw new TransformerException("Transformer configuration problem", e);
202         } finally {
203             // Release the connection.
204             aMethod.releaseConnection();
205         }
206     }
207
208     /**
209      * Gets the result of the HTTP method as an XHTML document.
210      * 
211      * @param aMethod
212      *            Method to invoke.
213      * @return XHTML as a byte array.
214      * @throws IOException
215      *             In case of problems obtaining the XHTML.
216      */
217     private byte[] getXhtml(HttpMethod aMethod) throws IOException {
218         // Transform the HTML into wellformed XML.
219         Tidy tidy = new Tidy();
220         tidy.setXHTML(true);
221         tidy.setQuiet(true);
222         tidy.setShowWarnings(false);
223       
224         // We write the jtidy output to XML since the DOM tree it produces is
225         // not namespace aware and namespace awareness is required by XSLT.
226         // An alternative is to configure namespace awareness of the XML parser
227         // in a system wide way.
228         ByteArrayOutputStream os = new ByteArrayOutputStream(); 
229         Document w3cDoc = tidy.parseDOM(aMethod.getResponseBodyAsStream(), os);
230         DomUtils.removeDuplicateAttributes(w3cDoc);
231         LOG.debug("Content of response is \n" + os.toString()); 
232
233         ByteArrayOutputStream xhtml = new ByteArrayOutputStream();
234         XMLSerializer serializer = new XMLSerializer(xhtml, new OutputFormat());
235         serializer.serialize(w3cDoc);
236         xhtml.flush();
237
238         return xhtml.toByteArray();
239     }
240
241     /**
242      * Sleeps for a random time but no more than the maximum delay.
243      * 
244      */
245     private void delay() {
246         try {
247             Thread.sleep((long) ((float) _maxDelay * Math.random()));
248         } catch (InterruptedException e) {
249             return; // to satisfy checkstyle
250         }
251     }
252
253     /**
254      * Executes the request and follows redirects if needed.
255      * 
256      * @param aClient
257      *            HTTP client to use.
258      * @param aMethod
259      *            Method to use.
260      * @return Final HTTP method used (differs from the parameter passed in in
261      *         case of redirection).
262      * @throws IOException
263      *             In case of network problems.
264      */
265     private HttpMethod executeWithRedirects(HttpClient aClient,
266             HttpMethod aMethod) throws IOException {
267         delay();
268         int statusCode = aClient.executeMethod(aMethod);
269
270         switch (statusCode) {
271         case HttpStatus.SC_OK: {
272             return aMethod;
273         }
274         case HttpStatus.SC_MOVED_PERMANENTLY:
275         case HttpStatus.SC_MOVED_TEMPORARILY:
276         case HttpStatus.SC_SEE_OTHER: {
277             aMethod.releaseConnection();
278             Header header = aMethod.getResponseHeader(REDIRECT_HEADER);
279             aMethod = new GetMethod(header.getValue());
280             return executeWithRedirects(aClient, aMethod); // TODO protect
281             // against infinite
282             // recursion.
283         }
284         default: {
285             throw new IOException("Method failed: "
286                     + aMethod.getStatusLine());
287         }
288         }
289     }
290 }