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
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
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
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
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
393
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
452 return "";
453 }
454 return name.substring(lastDot);
455 }
456 }