OSDN Git Service

Eleven: lastFM: Replace HttpStatus use with HttpURLConnection
[android-x86/packages-apps-Eleven.git] / src / com / cyanogenmod / eleven / lastfm / Caller.java
1 /*
2  * Copyright (c) 2012, the Last.fm Java Project and Committers All rights
3  * reserved. Redistribution and use of this software in source and binary forms,
4  * with or without modification, are permitted provided that the following
5  * conditions are met: - Redistributions of source code must retain the above
6  * copyright notice, this list of conditions and the following disclaimer. -
7  * Redistributions in binary form must reproduce the above copyright notice,
8  * this list of conditions and the following disclaimer in the documentation
9  * and/or other materials provided with the distribution. THIS SOFTWARE IS
10  * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
11  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
13  * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
14  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
16  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
17  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
18  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
19  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
20  */
21
22 package com.cyanogenmod.eleven.lastfm;
23
24 import static com.cyanogenmod.eleven.lastfm.StringUtilities.encode;
25 import static com.cyanogenmod.eleven.lastfm.StringUtilities.map;
26
27 import android.content.Context;
28 import android.util.Log;
29
30 import com.cyanogenmod.eleven.lastfm.Result.Status;
31
32 import org.w3c.dom.Document;
33 import org.w3c.dom.Element;
34 import org.xml.sax.InputSource;
35 import org.xml.sax.SAXException;
36
37 import java.io.BufferedWriter;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.io.InputStreamReader;
41 import java.io.OutputStream;
42 import java.io.OutputStreamWriter;
43 import java.net.HttpURLConnection;
44 import java.net.Proxy;
45 import java.net.URL;
46 import java.util.Iterator;
47 import java.util.Map;
48 import java.util.Map.Entry;
49 import java.util.WeakHashMap;
50
51 import javax.xml.parsers.DocumentBuilder;
52 import javax.xml.parsers.DocumentBuilderFactory;
53 import javax.xml.parsers.ParserConfigurationException;
54
55 /**
56  * The <code>Caller</code> class handles the low-level communication between the
57  * client and last.fm.<br/>
58  * Direct usage of this class should be unnecessary since all method calls are
59  * available via the methods in the <code>Artist</code>, <code>Album</code>,
60  * <code>User</code>, etc. classes. If specialized calls which are not covered
61  * by the Java API are necessary this class may be used directly.<br/>
62  * Supports the setting of a custom {@link Proxy} and a custom
63  * <code>User-Agent</code> HTTP header.
64  * 
65  * @author Janni Kovacs
66  */
67 public class Caller {
68
69     private final static String TAG = "LastFm.Caller";
70
71     private final static String PARAM_API_KEY = "api_key";
72
73     private final static String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
74
75     private static Caller mInstance = null;
76
77     private final String apiRootUrl = DEFAULT_API_ROOT;
78
79     private final String userAgent = "Apollo";
80
81     private Result lastResult;
82
83     /**
84      * @param context The {@link Context} to use
85      */
86     private Caller(final Context context) {
87     }
88
89     /**
90      * @param context The {@link Context} to use
91      * @return A new instance of this class
92      */
93     public final static synchronized Caller getInstance(final Context context) {
94         if (mInstance == null) {
95             mInstance = new Caller(context.getApplicationContext());
96         }
97         return mInstance;
98     }
99
100     /**
101      * @param method
102      * @param apiKey
103      * @param params
104      * @return
105      * @throws CallException
106      */
107     public Result call(final String method, final String apiKey, final String... params) {
108         return call(method, apiKey, map(params));
109     }
110
111     /**
112      * Performs the web-service call. If the <code>session</code> parameter is
113      * <code>non-null</code> then an authenticated call is made. If it's
114      * <code>null</code> then an unauthenticated call is made.<br/>
115      * The <code>apiKey</code> parameter is always required, even when a valid
116      * session is passed to this method.
117      * 
118      * @param method The method to call
119      * @param apiKey A Last.fm API key
120      * @param params Parameters
121      * @param session A Session instance or <code>null</code>
122      * @return the result of the operation
123      */
124     public Result call(final String method, final String apiKey, Map<String, String> params) {
125         params = new WeakHashMap<String, String>(params);
126         InputStream inputStream = null;
127
128         // no entry in cache, load from web
129         if (inputStream == null) {
130             // fill parameter map with apiKey and session info
131             params.put(PARAM_API_KEY, apiKey);
132             try {
133                 final HttpURLConnection urlConnection = openPostConnection(method, params);
134                 inputStream = getInputStreamFromConnection(urlConnection);
135
136                 if (inputStream == null) {
137                     lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(),
138                             urlConnection.getResponseMessage());
139                     return lastResult;
140                 }
141             } catch (final IOException ioEx) {
142                 // We will assume that the server is not ready
143                 Log.e(TAG, "Failed to download data", ioEx);
144                 lastResult = Result.createHttpErrorResult(HttpURLConnection.HTTP_UNAVAILABLE,
145                         ioEx.getLocalizedMessage());
146                 return lastResult;
147             }
148         }
149
150         try {
151             final Result result = createResultFromInputStream(inputStream);
152             lastResult = result;
153         } catch (final IOException ioEx) {
154             Log.e(TAG, "Failed to read document", ioEx);
155             lastResult = new Result(ioEx.getLocalizedMessage());
156         } catch (final SAXException saxEx) {
157             Log.e(TAG, "Failed to parse document", saxEx);
158             lastResult = new Result(saxEx.getLocalizedMessage());
159         }
160         return lastResult;
161     }
162
163     /**
164      * Creates a new {@link HttpURLConnection}, sets the proxy, if available,
165      * and sets the User-Agent property.
166      * 
167      * @param url URL to connect to
168      * @return a new connection.
169      * @throws IOException if an I/O exception occurs.
170      */
171     public HttpURLConnection openConnection(final String url) throws IOException {
172         final URL u = new URL(url);
173         HttpURLConnection urlConnection;
174         urlConnection = (HttpURLConnection)u.openConnection();
175         urlConnection.setRequestProperty("User-Agent", userAgent);
176         urlConnection.setUseCaches(true);
177         return urlConnection;
178     }
179
180     /**
181      * @param method
182      * @param params
183      * @return
184      * @throws IOException
185      */
186     private HttpURLConnection openPostConnection(final String method,
187             final Map<String, String> params) throws IOException {
188         final HttpURLConnection urlConnection = openConnection(apiRootUrl);
189         urlConnection.setRequestMethod("POST");
190         urlConnection.setDoOutput(true);
191         urlConnection.setUseCaches(true);
192         final OutputStream outputStream = urlConnection.getOutputStream();
193         final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
194         final String post = buildPostBody(method, params);
195         writer.write(post);
196         writer.close();
197         return urlConnection;
198     }
199
200     /**
201      * @param connection
202      * @return
203      * @throws IOException
204      */
205     private InputStream getInputStreamFromConnection(final HttpURLConnection connection)
206             throws IOException {
207         final int responseCode = connection.getResponseCode();
208
209         if (responseCode == HttpURLConnection.HTTP_FORBIDDEN
210                 || responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
211             return connection.getErrorStream();
212         } else if (responseCode == HttpURLConnection.HTTP_OK) {
213             return connection.getInputStream();
214         }
215
216         return null;
217     }
218
219     /**
220      * @param inputStream
221      * @return
222      * @throws SAXException
223      * @throws IOException
224      */
225     private Result createResultFromInputStream(final InputStream inputStream) throws SAXException,
226             IOException {
227         final Document document = newDocumentBuilder().parse(
228                 new InputSource(new InputStreamReader(inputStream, "UTF-8")));
229         final Element root = document.getDocumentElement(); // lfm element
230         final String statusString = root.getAttribute("status");
231         final Status status = "ok".equals(statusString) ? Status.OK : Status.FAILED;
232         if (status == Status.FAILED) {
233             final Element errorElement = (Element)root.getElementsByTagName("error").item(0);
234             final int errorCode = Integer.parseInt(errorElement.getAttribute("code"));
235             final String message = errorElement.getTextContent();
236             return Result.createRestErrorResult(errorCode, message);
237         } else {
238             return Result.createOkResult(document);
239         }
240     }
241
242     /**
243      * @return
244      */
245     private DocumentBuilder newDocumentBuilder() {
246         try {
247             final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
248             return builderFactory.newDocumentBuilder();
249         } catch (final ParserConfigurationException e) {
250             // better never happens
251             throw new RuntimeException(e);
252         }
253     }
254
255     /**
256      * @param method
257      * @param params
258      * @param strings
259      * @return
260      */
261     private String buildPostBody(final String method, final Map<String, String> params,
262             final String... strings) {
263         final StringBuilder builder = new StringBuilder(100);
264         builder.append("method=");
265         builder.append(method);
266         builder.append('&');
267         for (final Iterator<Entry<String, String>> it = params.entrySet().iterator(); it.hasNext();) {
268             final Entry<String, String> entry = it.next();
269             builder.append(entry.getKey());
270             builder.append('=');
271             builder.append(encode(entry.getValue()));
272             if (it.hasNext() || strings.length > 0) {
273                 builder.append('&');
274             }
275         }
276         int count = 0;
277         for (final String string : strings) {
278             builder.append(count % 2 == 0 ? string : encode(string));
279             count++;
280             if (count != strings.length) {
281                 if (count % 2 == 0) {
282                     builder.append('&');
283                 } else {
284                     builder.append('=');
285                 }
286             }
287         }
288         return builder.toString();
289     }
290 }