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.
22 package com.cyanogenmod.eleven.lastfm;
24 import static com.cyanogenmod.eleven.lastfm.StringUtilities.encode;
25 import static com.cyanogenmod.eleven.lastfm.StringUtilities.map;
27 import android.content.Context;
28 import android.util.Log;
30 import com.cyanogenmod.eleven.lastfm.Result.Status;
32 import org.w3c.dom.Document;
33 import org.w3c.dom.Element;
34 import org.xml.sax.InputSource;
35 import org.xml.sax.SAXException;
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;
46 import java.util.Iterator;
48 import java.util.Map.Entry;
49 import java.util.WeakHashMap;
51 import javax.xml.parsers.DocumentBuilder;
52 import javax.xml.parsers.DocumentBuilderFactory;
53 import javax.xml.parsers.ParserConfigurationException;
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.
65 * @author Janni Kovacs
69 private final static String TAG = "LastFm.Caller";
71 private final static String PARAM_API_KEY = "api_key";
73 private final static String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
75 private static Caller mInstance = null;
77 private final String apiRootUrl = DEFAULT_API_ROOT;
79 private final String userAgent = "Apollo";
81 private Result lastResult;
84 * @param context The {@link Context} to use
86 private Caller(final Context context) {
90 * @param context The {@link Context} to use
91 * @return A new instance of this class
93 public final static synchronized Caller getInstance(final Context context) {
94 if (mInstance == null) {
95 mInstance = new Caller(context.getApplicationContext());
105 * @throws CallException
107 public Result call(final String method, final String apiKey, final String... params) {
108 return call(method, apiKey, map(params));
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.
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
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;
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);
133 final HttpURLConnection urlConnection = openPostConnection(method, params);
134 inputStream = getInputStreamFromConnection(urlConnection);
136 if (inputStream == null) {
137 lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(),
138 urlConnection.getResponseMessage());
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());
151 final Result result = createResultFromInputStream(inputStream);
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());
164 * Creates a new {@link HttpURLConnection}, sets the proxy, if available,
165 * and sets the User-Agent property.
167 * @param url URL to connect to
168 * @return a new connection.
169 * @throws IOException if an I/O exception occurs.
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;
184 * @throws IOException
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);
197 return urlConnection;
203 * @throws IOException
205 private InputStream getInputStreamFromConnection(final HttpURLConnection connection)
207 final int responseCode = connection.getResponseCode();
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();
222 * @throws SAXException
223 * @throws IOException
225 private Result createResultFromInputStream(final InputStream inputStream) throws SAXException,
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);
238 return Result.createOkResult(document);
245 private DocumentBuilder newDocumentBuilder() {
247 final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
248 return builderFactory.newDocumentBuilder();
249 } catch (final ParserConfigurationException e) {
250 // better never happens
251 throw new RuntimeException(e);
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);
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());
271 builder.append(encode(entry.getValue()));
272 if (it.hasNext() || strings.length > 0) {
277 for (final String string : strings) {
278 builder.append(count % 2 == 0 ? string : encode(string));
280 if (count != strings.length) {
281 if (count % 2 == 0) {
288 return builder.toString();