UrlUtility.java
001 /* This file is part of the project "Hilbert II" - http://www.qedeq.org
002  *
003  * Copyright 2000-2013,  Michael Meyling <mime@qedeq.org>.
004  *
005  * "Hilbert II" is free software; you can redistribute
006  * it and/or modify it under the terms of the GNU General Public
007  * License as published by the Free Software Foundation; either
008  * version 2 of the License, or (at your option) any later version.
009  *
010  * This program is distributed in the hope that it will be useful,
011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013  * GNU General Public License for more details.
014  */
015 
016 package org.qedeq.base.io;
017 
018 import java.io.File;
019 import java.io.FileNotFoundException;
020 import java.io.FileOutputStream;
021 import java.io.IOException;
022 import java.io.InputStream;
023 import java.io.UnsupportedEncodingException;
024 import java.lang.reflect.InvocationTargetException;
025 import java.net.HttpURLConnection;
026 import java.net.MalformedURLException;
027 import java.net.URL;
028 import java.net.URLConnection;
029 import java.net.URLDecoder;
030 
031 import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
032 import org.apache.commons.httpclient.HttpClient;
033 import org.apache.commons.httpclient.HttpStatus;
034 import org.apache.commons.httpclient.methods.GetMethod;
035 import org.apache.commons.httpclient.params.HttpMethodParams;
036 import org.qedeq.base.trace.Trace;
037 import org.qedeq.base.utility.YodaUtility;
038 
039 
040 /**
041  * A collection of useful static methods for URL s.
042  *
043  @author  Michael Meyling
044  */
045 public final class UrlUtility {
046 
047     /** This class, for debugging purpose. */
048     private static final Class CLASS = UrlUtility.class;
049 
050     /**
051      * Constructor, should never be called.
052      */
053     private UrlUtility() {
054         // don't call me
055     }
056 
057     /**
058      * Convert file in URL.
059      *
060      @param   file    File.
061      @return  URL.
062      */
063     public static URL toUrl(final File file) {
064         try {
065             return file.getAbsoluteFile().toURI().toURL();
066         catch (MalformedURLException e) { // should only happen if there is a bug in the JDK
067             throw new RuntimeException(e);
068         }
069     }
070 
071     /**
072      * Convert URL path in file path.
073      *
074      @param   url    Convert this URL path.
075      @return  File path.
076      */
077     public static File transformURLPathToFilePath(final URL url) {
078         try {
079             return new File(URLDecoder.decode(url.getFile()"UTF-8"));
080         catch (UnsupportedEncodingException e) {
081             throw new RuntimeException(e);
082         }
083     }
084 
085     /**
086      * Create relative address from <code>origin</code> to <code>next</code>.
087      * The resulting file path has "/" as directory name separator.
088      * If the resulting file path is the same as origin specifies, we return "".
089      * Otherwise the result will always have an "/" as last character.
090      *
091      @param   origin  This is the original location. Must be a directory.
092      @param   next    This should be the next location. Must also be a directory.
093      @return  Relative (or if necessary absolute) file path.
094      */
095     public static final String createRelativePath(final File origin, final File next) {
096         if (origin.equals(next)) {
097             return "";
098         }
099         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 = (HttpURLConnectionconnection;
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() "\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(3false));
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.createNecessaryDirectories(f);
319             IoUtility.saveFileBinary(f, responseBody);
320             listener.loadingCompletenessChanged(1);
321         finally {
322             // Release the connection.
323             httpMethod.releaseConnection();
324             Trace.end(CLASS, method);
325         }
326     }
327 
328 
329     /**
330      * This class ist just for solving the lazy loading problem thread save.
331      * see <a href="http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom">
332      * Initialization_on_demand_holder_idiom</a>.
333      */
334     private static final class LazyHolderTimeoutMethods {
335 
336         /** Lazy initialized constant that knows about the existence of the method
337          <code>URLConnection.setConnectTimeout</code>. This depends on the currently running
338          * JVM. */
339         private static final boolean IS_SET_CONNECTION_TIMEOUT_SUPPORTED = YodaUtility.existsMethod(
340             URLConnection.class, "setConnectTimeout",
341             new Class[] {Integer.TYPE});
342 
343         /** Lazy initialized constant that knows about the existence of the method
344          <code>URLConnection.setReadTimeout</code>. This depends on the currently running
345          * JVM. */
346         private static final boolean IS_SET_READ_TIMEOUT_SUSPPORTED = YodaUtility.existsMethod(
347                 URLConnection.class, "setReadTimeout",
348                 new Class[] {Integer.TYPE});
349 
350         /**
351          * Hidden constructor.
352          */
353         private LazyHolderTimeoutMethods() {
354             // nothing to do
355         }
356 
357     }
358 
359     /**
360      * Is setting of connection timeout supported in current environment?
361      *
362      @return  Setting connection timeout supported?
363      */
364     public static boolean isSetConnectionTimeOutSupported() {
365         return LazyHolderTimeoutMethods.IS_SET_CONNECTION_TIMEOUT_SUPPORTED;
366     }
367 
368     /**
369      * Is setting of read timeout supported in current environment?
370      *
371      @return  Setting read timeout supported?
372      */
373     public static boolean isSetReadTimeoutSupported() {
374         return LazyHolderTimeoutMethods.IS_SET_READ_TIMEOUT_SUSPPORTED;
375     }
376 
377 }