View Javadoc

1   /* This file is part of the project "Hilbert II" - http://www.qedeq.org" target="alexandria_uri">http://www.qedeq.org
2    *
3    * Copyright 2000-2014,  Michael Meyling <mime@qedeq.org>.
4    *
5    * "Hilbert II" is free software; you can redistribute
6    * it and/or modify it under the terms of the GNU General Public
7    * License as published by the Free Software Foundation; either
8    * version 2 of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13   * GNU General Public License for more details.
14   */
15  
16  package org.qedeq.base.io;
17  
18  import java.io.File;
19  import java.io.FileNotFoundException;
20  import java.io.FileOutputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.UnsupportedEncodingException;
24  import java.lang.reflect.InvocationTargetException;
25  import java.net.HttpURLConnection;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.net.URLConnection;
29  import java.net.URLDecoder;
30  
31  import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
32  import org.apache.commons.httpclient.HttpClient;
33  import org.apache.commons.httpclient.HttpStatus;
34  import org.apache.commons.httpclient.methods.GetMethod;
35  import org.apache.commons.httpclient.params.HttpMethodParams;
36  import org.qedeq.base.trace.Trace;
37  import org.qedeq.base.utility.YodaUtility;
38  
39  
40  /**
41   * A collection of useful static methods for URL s.
42   *
43   * @author  Michael Meyling
44   */
45  public final class UrlUtility {
46  
47      /** This class, for debugging purpose. */
48      private static final Class CLASS = UrlUtility.class;
49  
50      /**
51       * Constructor, should never be called.
52       */
53      private UrlUtility() {
54          // don't call me
55      }
56  
57      /**
58       * Convert file in URL.
59       *
60       * @param   file    File.
61       * @return  URL.
62       */
63      public static URL toUrl(final File file) {
64          try {
65              return file.getAbsoluteFile().toURI().toURL();
66          } catch (MalformedURLException e) { // should only happen if there is a bug in the JDK
67              throw new RuntimeException(e);
68          }
69      }
70  
71      /**
72       * Convert URL path in file path.
73       *
74       * @param   url    Convert this URL path.
75       * @return  File path.
76       */
77      public static File transformURLPathToFilePath(final URL url) {
78          try {
79              return new File(URLDecoder.decode(url.getFile(), "UTF-8"));
80          } catch (UnsupportedEncodingException e) {
81              throw new RuntimeException(e);
82          }
83      }
84  
85      /**
86       * Create relative address from <code>origin</code> to <code>next</code>.
87       * The resulting file path has "/" as directory name separator.
88       * If the resulting file path is the same as origin specifies, we return "".
89       * Otherwise the result will always have an "/" as last character.
90       *
91       * @param   origin  This is the original location. Must be a directory.
92       * @param   next    This should be the next location. Must also be a directory.
93       * @return  Relative (or if necessary absolute) file path.
94       */
95      public static final String createRelativePath(final File origin, final File next) {
96          if (origin.equals(next)) {
97              return "";
98          }
99          final Path org = new Path(origin.getPath().replace(File.separatorChar, '/'), "");
100         final Path ne = new Path(next.getPath().replace(File.separatorChar, '/'), "");
101         return org.createRelative(ne.toString()).toString();
102     }
103 
104     /**
105      * Simplify file URL by returning a file path.
106      *
107      * @param   url     URL to simplify.
108      * @return  File path (if protocol is "file"). Otherwise just return <code>url</code>.
109      */
110     public static String easyUrl(final String url) {
111         String result = url;
112         try {
113             final URL u = new URL(url);
114             // is this a file URL?
115             if (u.getProtocol().equalsIgnoreCase("file")) {
116                 return transformURLPathToFilePath(u).getCanonicalPath();
117             }
118         } catch (RuntimeException e) {
119             //  ignore
120         } catch (IOException e) {
121             //  ignore
122         }
123         return result;
124     }
125 
126     /**
127      * Make local copy of an URL.
128      *
129      * @param   url             Save this URL.
130      * @param   f               Save into this file. An existing file is overwritten.
131      * @param   proxyHost       Use this proxy host.
132      * @param   proxyPort       Use this port at proxy host.
133      * @param   nonProxyHosts   This are hosts not to be proxied.
134      * @param   connectTimeout  Connection timeout.
135      * @param   readTimeout     Read timeout.
136      * @param   listener        Here completion events are fired.
137      * @throws  IOException     Saving failed.
138      */
139     public static void saveUrlToFile(final String url, final File f, final String proxyHost,
140             final String proxyPort, final String nonProxyHosts, final int connectTimeout,
141             final int readTimeout, final LoadingListener listener)
142             throws IOException {
143         final String method = "saveUrlToFile()";
144         Trace.begin(CLASS, method);
145 
146         // if we are not web started and running under Java 1.4 we use apache commons
147         // httpclient library (so we can set timeouts)
148         if (!isSetConnectionTimeOutSupported()
149                 && !IoUtility.isWebStarted()) {
150             saveQedeqFromWebToBufferApache(url, f, proxyHost, proxyPort, nonProxyHosts, connectTimeout,
151                 readTimeout, listener);
152             Trace.end(CLASS, method);
153             return;
154         }
155 
156         // set proxy properties according to kernel configuration (if not webstarted)
157         if (!IoUtility.isWebStarted()) {
158             if (proxyHost != null) {
159                 System.setProperty("http.proxyHost", proxyHost);
160             }
161             if (proxyPort != null) {
162                 System.setProperty("http.proxyPort", proxyPort);
163             }
164             if (nonProxyHosts != null) {
165                 System.setProperty("http.nonProxyHosts", nonProxyHosts);
166             }
167         }
168 
169         FileOutputStream out = null;
170         InputStream in = null;
171         try {
172             final URLConnection connection = new URL(url).openConnection();
173 
174             if (connection instanceof HttpURLConnection) {
175                 final HttpURLConnection httpConnection = (HttpURLConnection) connection;
176                 // if we are running at least under Java 1.5 the following code should be executed
177                 if (isSetConnectionTimeOutSupported()) {
178                     try {
179                         YodaUtility.executeMethod(httpConnection, "setConnectTimeout",
180                             new Class[] {Integer.TYPE}, new Object[] {new Integer(
181                             connectTimeout)});
182                     } catch (NoSuchMethodException e) {
183                         Trace.fatal(CLASS, method,
184                             "URLConnection.setConnectTimeout was previously found", e);
185                     } catch (InvocationTargetException e) {
186                         Trace.fatal(CLASS, method,
187                             "URLConnection.setConnectTimeout throwed an error", e);
188                     }
189                 }
190                 // if we are running at least under Java 1.5 the following code should be executed
191                 if (isSetReadTimeoutSupported()) {
192                     try {
193                         YodaUtility.executeMethod(httpConnection, "setReadTimeout",
194                             new Class[] {Integer.TYPE}, new Object[] {new Integer(
195                             readTimeout)});
196                     } catch (NoSuchMethodException e) {
197                         Trace.fatal(CLASS, method,
198                             "URLConnection.setReadTimeout was previously found", e);
199                     } catch (InvocationTargetException e) {
200                         Trace.fatal(CLASS, method,
201                             "URLConnection.setReadTimeout throwed an error", e);
202                     }
203                 }
204                 int responseCode = httpConnection.getResponseCode();
205                 if (responseCode == 200) {
206                     in = httpConnection.getInputStream();
207                 } else {
208                     in = httpConnection.getErrorStream();
209                     final String errorText = IoUtility.loadStreamWithoutException(in, 1000);
210                     throw new IOException("Response code from HTTP server was " + responseCode
211                         + (errorText.length() > 0 ? "\nResponse  text from HTTP server was:\n"
212                         + errorText : ""));
213                 }
214             } else {
215                 Trace.paramInfo(CLASS, method, "connection.getClass", connection.getClass()
216                     .toString());
217                 in = connection.getInputStream();
218             }
219 
220             if (!url.equals(connection.getURL().toString())) {
221                 throw new FileNotFoundException("\"" + url + "\" was substituted by "
222                     + "\"" + connection.getURL() + "\" from server");
223             }
224             final double maximum = connection.getContentLength();
225             IoUtility.createNecessaryDirectories(f);
226             out = new FileOutputStream(f);
227             final byte[] buffer = new byte[4096];
228             int bytesRead; // bytes read during one buffer read
229             int position = 0; // current reading position within the whole document
230             // continue writing
231             while ((bytesRead = in.read(buffer)) != -1) {
232                 position += bytesRead;
233                 out.write(buffer, 0, bytesRead);
234                 if (maximum > 0) {
235                     double completeness = position / maximum;
236                     if (completeness < 0) {
237                         completeness = 0;
238                     }
239                     if (completeness > 100) {
240                         completeness = 1;
241                     }
242                     listener.loadingCompletenessChanged(completeness);
243                 }
244             }
245             listener.loadingCompletenessChanged(1);
246         } finally {
247             IoUtility.close(out);
248             out = null;
249             IoUtility.close(in);
250             in = null;
251             Trace.end(CLASS, method);
252         }
253     }
254 
255     /**
256      * Make local copy of a http accessable URL. This method uses apaches HttpClient,
257      * but it dosn't work under webstart with proxy configuration. If we don't use this
258      * method, the apache commons-httpclient library can be removed
259      *
260      * @param   url             Save this URL.
261      * @param   f               Save into this file. An existing file is overwritten.
262      * @param   proxyHost       Use this proxy host.
263      * @param   proxyPort       Use this port at proxy host.
264      * @param   nonProxyHosts   This are hosts not to be proxied.
265      * @param   connectTimeout  Connection timeout.
266      * @param   readTimeout     Read timeout.
267      * @param   listener        Here completion events are fired.
268      * @throws  IOException     Saving failed.
269      */
270     private static void saveQedeqFromWebToBufferApache(final String url, final File f,
271             final String proxyHost, final String proxyPort,
272             final String nonProxyHosts, final int connectTimeout, final int readTimeout,
273             final LoadingListener listener)
274             throws IOException {
275         final String method = "saveQedeqFromWebToBufferApache()";
276         Trace.begin(CLASS, method);
277 
278         // Create an instance of HttpClient.
279         HttpClient client = new HttpClient();
280 
281         // set proxy properties according to kernel configuration (if not webstarted)
282         if (!IoUtility.isWebStarted() && proxyHost != null && proxyHost.length() > 0) {
283             final String pHost = proxyHost;
284             int pPort = 80;
285             if (proxyPort != null) {
286                 try {
287                     pPort = Integer.parseInt(proxyPort);
288                 } catch (RuntimeException e) {
289                     Trace.fatal(CLASS, method, "proxy port not numeric: "
290                         + proxyPort, e);
291                 }
292             }
293             if (pHost.length() > 0) {
294                 client.getHostConfiguration().setProxy(pHost, pPort);
295             }
296         }
297 
298         // Create a method instance.
299         GetMethod httpMethod = new GetMethod(url);
300 
301         try {
302             // Provide custom retry handler is necessary
303             httpMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
304                     new DefaultHttpMethodRetryHandler(3, false));
305 
306             httpMethod.getParams().setSoTimeout(connectTimeout);
307             // Throws IOException on TimeOut.
308 
309             int statusCode = client.executeMethod(httpMethod);
310 
311             if (statusCode != HttpStatus.SC_OK) {
312                 throw new FileNotFoundException("Problems loading: " + url + "\n"
313                     + httpMethod.getStatusLine());
314             }
315 
316             // Read the response body.
317             byte[] responseBody = httpMethod.getResponseBody();
318             IoUtility.saveFileBinary(f, responseBody);
319             listener.loadingCompletenessChanged(1);
320         } finally {
321             // Release the connection.
322             httpMethod.releaseConnection();
323             Trace.end(CLASS, method);
324         }
325     }
326 
327 
328     /**
329      * This class ist just for solving the lazy loading problem thread save.
330      * see <a href="http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom">
331      * Initialization_on_demand_holder_idiom</a>.
332      */
333     private static final class LazyHolderTimeoutMethods {
334 
335         /**
336          * Constructor, should never be called.
337          */
338         private LazyHolderTimeoutMethods() {
339             // don't call me
340         }
341 
342         /** Lazy initialized constant that knows about the existence of the method
343          * <code>URLConnection.setConnectTimeout</code>. This depends on the currently running
344          * JVM. */
345         private static final boolean IS_SET_CONNECTION_TIMEOUT_SUPPORTED = YodaUtility.existsMethod(
346             URLConnection.class, "setConnectTimeout",
347             new Class[] {Integer.TYPE});
348 
349         /** Lazy initialized constant that knows about the existence of the method
350          * <code>URLConnection.setReadTimeout</code>. This depends on the currently running
351          * JVM. */
352         private static final boolean IS_SET_READ_TIMEOUT_SUSPPORTED = YodaUtility.existsMethod(
353                 URLConnection.class, "setReadTimeout",
354                 new Class[] {Integer.TYPE});
355 
356     }
357 
358     /**
359      * Is setting of connection timeout supported in current environment?
360      *
361      * @return  Setting connection timeout supported?
362      */
363     public static boolean isSetConnectionTimeOutSupported() {
364         return LazyHolderTimeoutMethods.IS_SET_CONNECTION_TIMEOUT_SUPPORTED;
365     }
366 
367     /**
368      * Is setting of read timeout supported in current environment?
369      *
370      * @return  Setting read timeout supported?
371      */
372     public static boolean isSetReadTimeoutSupported() {
373         return LazyHolderTimeoutMethods.IS_SET_READ_TIMEOUT_SUSPPORTED;
374     }
375 
376 }