View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (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   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.ambient.commons;
20  
21  import java.io.BufferedInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.LineNumberReader;
25  import java.io.OutputStream;
26  import java.io.Reader;
27  import java.io.UnsupportedEncodingException;
28  import java.net.MalformedURLException;
29  import java.net.URL;
30  import java.net.URLDecoder;
31  import java.net.URLEncoder;
32  import java.util.Collections;
33  import java.util.Date;
34  import java.util.HashMap;
35  import java.util.Map;
36  import java.util.StringTokenizer;
37  
38  import javax.xml.parsers.ParserConfigurationException;
39  import javax.xml.parsers.SAXParserFactory;
40  
41  import org.xml.sax.ContentHandler;
42  import org.xml.sax.InputSource;
43  import org.xml.sax.SAXException;
44  import org.xml.sax.XMLReader;
45  
46  import android.util.Log;
47  
48  /***
49   * Contains IO and HTTP utility methods.
50   * 
51   * @author Martin Vysny
52   */
53  public final class IOUtils {
54  	private IOUtils() {
55  		throw new Error();
56  	}
57  
58  	/***
59  	 * Encodes given URL as per {@link URLEncoder#encode(String, String)}. Uses
60  	 * UTF-8 encoding. Spaces are correctly represented as <code>%20</code>.
61  	 * 
62  	 * @param url
63  	 *            the URL to encode
64  	 * @return URL-friendly string.
65  	 */
66  	public static String encodeURL(final String url) {
67  		try {
68  			final String result = URLEncoder.encode(url, "UTF-8");
69  			return result.replace("+", "%20");
70  		} catch (UnsupportedEncodingException e) {
71  			throw new Error(e);
72  		}
73  	}
74  
75  	/***
76  	 * Parses XML readable from given stream. Note that the parser is configured
77  	 * to be namespace-aware - Android bug prevents us to use
78  	 * non-namespace-aware parser.
79  	 * 
80  	 * @param in
81  	 *            the stream, always closed.
82  	 * @param handler
83  	 *            handles XML events.
84  	 * @throws SAXException
85  	 *             if parsing fails
86  	 * @throws IOException
87  	 *             if i/o error occurs
88  	 */
89  	public static void parseXML(final InputStream in,
90  			final ContentHandler handler) throws IOException, SAXException {
91  		final SAXParserFactory parser = SAXParserFactory.newInstance();
92  		parser.setValidating(false);
93  		parser.setNamespaceAware(true);
94  		final XMLReader reader;
95  		try {
96  			reader = parser.newSAXParser().getXMLReader();
97  		} catch (ParserConfigurationException e) {
98  			throw new RuntimeException(e);
99  		}
100 		reader.setContentHandler(handler);
101 		try {
102 			final InputSource source = new InputSource(in);
103 			reader.parse(source);
104 		} finally {
105 			MiscUtils.closeQuietly(in);
106 		}
107 	}
108 
109 	/***
110 	 * Reads a line from given stream. No charset decoding is performed - only
111 	 * ASCII characters are expected. The line is expected to be terminated by
112 	 * the \n character. All \r characters are ignored.
113 	 * 
114 	 * @param in
115 	 *            the stream to read from. This should be an
116 	 *            {@link BufferedInputStream} implementation for performance
117 	 *            reasons.
118 	 * @return the line. May return <code>null</code> on end-of-stream.
119 	 * @throws IOException
120 	 *             if i/o error occurs.
121 	 */
122 	public static String readLine(final InputStream in) throws IOException {
123 		final StringBuilder result = new StringBuilder();
124 		int charsRead = 0;
125 		while (true) {
126 			final int c = in.read();
127 			if (c < 0) {
128 				if (charsRead == 0) {
129 					// Log.e("READLINE", "EOF");
130 					return null;
131 				}
132 				break;
133 			}
134 			charsRead++;
135 			if (c > 127) {
136 				throw new IOException("Invalid character " + (char) c
137 						+ ": ordinal " + c);
138 			}
139 			if (c == '\n') {
140 				break;
141 			}
142 			if (c == '\r') {
143 				continue;
144 			}
145 			result.append((char) c);
146 		}
147 		// Log.e("READLINE", "Line: '" + result.toString() + "'");
148 		return result.toString();
149 	}
150 
151 	/***
152 	 * Writes a line to given stream. No charset encoding is performed - only
153 	 * ASCII characters are expected. An additional \r\n characters will be
154 	 * written.
155 	 * 
156 	 * @param string
157 	 *            the string to write. It must consist of ASCII characters only.
158 	 * @param out
159 	 *            the stream to write to.
160 	 * @throws IOException
161 	 *             if i/o error occurs.
162 	 */
163 	public static void writeLine(final String string, final OutputStream out)
164 			throws IOException {
165 		// Log.e("WRITELINE", "Line: '" + string + "'");
166 		for (int i = 0; i < string.length(); i++) {
167 			final char c = string.charAt(i);
168 			if ((c < 0) || (c > 127)) {
169 				throw new IllegalArgumentException(
170 						"Non ascii characters found in " + string);
171 			}
172 			out.write(c);
173 		}
174 		out.write('\r');
175 		out.write('\n');
176 	}
177 
178 	/***
179 	 * Writes an empty line to given stream. An additional \r\n characters will
180 	 * be written.
181 	 * 
182 	 * @param out
183 	 *            the stream to write to.
184 	 * @throws IOException
185 	 *             if i/o error occurs.
186 	 */
187 	public static void writeLine(final OutputStream out) throws IOException {
188 		writeLine("", out);
189 	}
190 
191 	/***
192 	 * Strips extension from given name. Does nothing if the file name does not
193 	 * have an extension.
194 	 * 
195 	 * @param name
196 	 *            the name to strip extension from
197 	 * @return stripped name.
198 	 */
199 	public static String stripExt(final String name) {
200 		final int lastDot = name.lastIndexOf('.');
201 		if (lastDot < 0)
202 			return name;
203 		final int lastSlash = name.lastIndexOf('/');
204 		if ((lastSlash >= 0) && (lastDot < lastSlash)) {
205 			// no dot in base name.
206 			return name;
207 		}
208 		return name.substring(0, lastDot);
209 	}
210 
211 	/***
212 	 * Copies input stream to the output stream.
213 	 * 
214 	 * @param in
215 	 *            the input stream. Always closed.
216 	 * @param out
217 	 *            the output stream. Always closed.
218 	 * @param bufferSize
219 	 *            the buffer size in bytes
220 	 * @throws IOException
221 	 *             thrown when i/o error occurs or when interrupted.
222 	 */
223 	public static void copy(final InputStream in, final OutputStream out,
224 			final int bufferSize) throws IOException {
225 		try {
226 			final byte[] buf = new byte[bufferSize];
227 			while (true) {
228 				final int read = in.read(buf);
229 				if (read < 0) {
230 					break;
231 				}
232 				if (read > 0) {
233 					out.write(buf, 0, read);
234 				}
235 				if (Thread.currentThread().isInterrupted()) {
236 					throw new IOException("Interrupted");
237 				}
238 			}
239 		} finally {
240 			MiscUtils.closeQuietly(in);
241 			MiscUtils.closeQuietly(out);
242 		}
243 	}
244 
245 	/***
246 	 * Removes trailing slash from given file name.
247 	 * 
248 	 * @param name
249 	 *            the file name.
250 	 * @return file name without the trailing slash.
251 	 */
252 	public static String removeTrailingSlash(final String name) {
253 		if (name.endsWith("/") || name.endsWith("//")) {
254 			return name.substring(0, name.length() - 1);
255 		}
256 		return name;
257 	}
258 
259 	/***
260 	 * Reads the request until an empty string is encountered.
261 	 * 
262 	 * @param in
263 	 *            the stream to read from.
264 	 * @throws IOException
265 	 *             if i/o error occurs.
266 	 */
267 	public static void readRequest(final InputStream in) throws IOException {
268 		while (true) {
269 			final String line = IOUtils.readLine(in);
270 			if (MiscUtils.isEmpty(line)) {
271 				break;
272 			}
273 		}
274 	}
275 
276 	/***
277 	 * The http request
278 	 * 
279 	 * @author Martin Vysny
280 	 */
281 	public static final class HttpRequest {
282 		/***
283 		 * The request string: GET, POST etc.
284 		 */
285 		public String method;
286 		/***
287 		 * The original request path.
288 		 */
289 		public String requestPath;
290 		/***
291 		 * The path without the query/anchor part.
292 		 */
293 		public String path;
294 		/***
295 		 * The query parameter values.
296 		 */
297 		public Map<String, String> query;
298 		/***
299 		 * 0 for HTTP/1.0, 1 for HTTP/1.1
300 		 */
301 		public byte version;
302 	}
303 
304 	/***
305 	 * Parses given HTTP request.
306 	 * 
307 	 * @param request
308 	 *            the request
309 	 * @return parsed request or <code>null</code> if the request is malformed.
310 	 * @throws ServerHttpException
311 	 *             on HTTP error.
312 	 */
313 	public static HttpRequest parseRequest(final String request)
314 			throws ServerHttpException {
315 		final String[] parts = request.split("//s+");
316 		if (parts.length != 3) {
317 			throw new ServerHttpException(400, "Invalid request: " + request);
318 		}
319 		final HttpRequest result = new HttpRequest();
320 		result.method = parts[0];
321 		result.requestPath = parts[1];
322 		final URL path;
323 		try {
324 			path = new URL("file://" + parts[1]);
325 		} catch (MalformedURLException e) {
326 			throw new ServerHttpException(400, e);
327 		}
328 		result.query = parseParams(path.getQuery());
329 		result.path = URLDecoder.decode(path.getPath());
330 		if (!parts[2].startsWith("HTTP/1.")) {
331 			throw new ServerHttpException(400, "Invalid HTTP: " + parts[2]);
332 		}
333 		result.version = (byte) (parts[2].charAt(7) - '0');
334 		return result;
335 	}
336 
337 	private static Map<String, String> parseParams(final String get)
338 			throws ServerHttpException {
339 		if (get == null) {
340 			return Collections.emptyMap();
341 		}
342 		final StringTokenizer st = new StringTokenizer(get, "&");
343 		final Map<String, String> result = new HashMap<String, String>();
344 		while (st.hasMoreTokens()) {
345 			final String token = st.nextToken();
346 			int equals = token.indexOf('=');
347 			if (equals < 0) {
348 				throw new ServerHttpException(400);
349 			}
350 			final String paramName = token.substring(0, equals);
351 			final String paramValue = URLDecoder.decode(token.substring(
352 					equals + 1, token.length()));
353 			result.put(paramName, paramValue);
354 		}
355 		return result;
356 	}
357 
358 	/***
359 	 * Writes HTTP response. The response is not terminated by an empty line.
360 	 * 
361 	 * @param httpVer
362 	 *            the HTTP version, 0 or 1.
363 	 * @param httpCode
364 	 *            the HTTP code.
365 	 * @param keepAlive
366 	 *            if <code>true</code> then "keep-alive" is written. If
367 	 *            <code>false</code> then "close" is written.
368 	 * @param size
369 	 *            the size of the content. -1 if not known.
370 	 * @param resultMime
371 	 *            response MIME type.
372 	 * @param out
373 	 *            write the response here.
374 	 * @throws IOException
375 	 *             if i/o error occurs
376 	 */
377 	public static void writeHttpResponse(final byte httpVer,
378 			final int httpCode, final boolean keepAlive, final long size,
379 			final String resultMime, final OutputStream out) throws IOException {
380 		IOUtils.writeLine(ServerHttpException
381 				.getResponseLine(httpVer, httpCode), out);
382 		IOUtils.writeLine("Date: " + MiscUtils.getRFC2822Date(new Date()), out);
383 		IOUtils.writeLine(
384 				"Connection: " + (keepAlive ? "keep-alive" : "close"), out);
385 		IOUtils.writeLine("Cache-Control: max-age=31104000", out);
386 		IOUtils.writeLine("Content-Type: " + resultMime, out);
387 		IOUtils.writeLine("Accept-Ranges: bytes", out);
388 		if (size >= 0) {
389 			IOUtils.writeLine("Content-Length: " + size, out);
390 		}
391 		IOUtils.writeLine("Server: Ambient", out);
392 		// if (keepAlive) {
393 		// IOUtils.writeLine("Keep-Alive: timeout=15, max=100", out);
394 		// }
395 	}
396 
397 	/***
398 	 * Cat given reader to log.
399 	 * 
400 	 * @param r
401 	 *            the reader to cat.
402 	 */
403 	public static void cat(final Reader r) {
404 		try {
405 			final LineNumberReader reader = new LineNumberReader(r);
406 			while (true) {
407 				final String line = reader.readLine();
408 				if (line == null) {
409 					break;
410 				}
411 				MiscUtils.sysout(line);
412 			}
413 		} catch (Exception ex) {
414 			Log.e("IOUtils", "Error: " + ex.getMessage(), ex);
415 		} finally {
416 			MiscUtils.closeQuietly(r);
417 		}
418 	}
419 
420 	/***
421 	 * Formats IP address contained in an integer and returns it as a string.
422 	 * 
423 	 * @param address
424 	 *            the IP to decode
425 	 * @return decoded IP.
426 	 */
427 	public static String formatIP(final int address) {
428 		byte[] addr = new byte[4];
429 		addr[3] = (byte) ((address >>> 24) & 0xFF);
430 		addr[2] = (byte) ((address >>> 16) & 0xFF);
431 		addr[1] = (byte) ((address >>> 8) & 0xFF);
432 		addr[0] = (byte) (address & 0xFF);
433 		return (addr[0] & 0xff) + "." + (addr[1] & 0xff) + "."
434 				+ (addr[2] & 0xff) + "." + (addr[3] & 0xff);
435 	}
436 
437 	/***
438 	 * Returns the extension of the file, starting with dot character.
439 	 * 
440 	 * @param name
441 	 *            the file name to get the extension from.
442 	 * @return extension or empty string if the file does not have an extension.
443 	 */
444 	public static String getExt(final String name) {
445 		final int lastDot = name.lastIndexOf('.');
446 		if (lastDot < 0) {
447 			return "";
448 		}
449 		final int lastSlash = name.lastIndexOf('/');
450 		if ((lastSlash >= 0) && (lastDot < lastSlash)) {
451 			// no dot in base name.
452 			return "";
453 		}
454 		return name.substring(lastDot);
455 	}
456 }