1 | /* This file is part of the project "Hilbert II" - 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 | } |