/*
* Copyright (c) 2012, the Last.fm Java Project and Committers All rights
* reserved. Redistribution and use of this software in source and binary forms,
* with or without modification, are permitted provided that the following
* conditions are met: - Redistributions of source code must retain the above
* copyright notice, this list of conditions and the following disclaimer. -
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution. THIS SOFTWARE IS
* PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.cyanogenmod.eleven.lastfm;
import static com.cyanogenmod.eleven.lastfm.StringUtilities.encode;
import static com.cyanogenmod.eleven.lastfm.StringUtilities.map;
import android.content.Context;
import android.util.Log;
import com.cyanogenmod.eleven.lastfm.Result.Status;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* The Caller
class handles the low-level communication between the
* client and last.fm.
* Direct usage of this class should be unnecessary since all method calls are
* available via the methods in the Artist
, Album
,
* User
, etc. classes. If specialized calls which are not covered
* by the Java API are necessary this class may be used directly.
* Supports the setting of a custom {@link Proxy} and a custom
* User-Agent
HTTP header.
*
* @author Janni Kovacs
*/
public class Caller {
private final static String TAG = "LastFm.Caller";
private final static String PARAM_API_KEY = "api_key";
private final static String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
private static Caller mInstance = null;
private final String apiRootUrl = DEFAULT_API_ROOT;
private final String userAgent = "Apollo";
private Result lastResult;
/**
* @param context The {@link Context} to use
*/
private Caller(final Context context) {
}
/**
* @param context The {@link Context} to use
* @return A new instance of this class
*/
public final static synchronized Caller getInstance(final Context context) {
if (mInstance == null) {
mInstance = new Caller(context.getApplicationContext());
}
return mInstance;
}
/**
* @param method
* @param apiKey
* @param params
* @return
* @throws CallException
*/
public Result call(final String method, final String apiKey, final String... params) {
return call(method, apiKey, map(params));
}
/**
* Performs the web-service call. If the session
parameter is
* non-null
then an authenticated call is made. If it's
* null
then an unauthenticated call is made.
* The apiKey
parameter is always required, even when a valid
* session is passed to this method.
*
* @param method The method to call
* @param apiKey A Last.fm API key
* @param params Parameters
* @param session A Session instance or null
* @return the result of the operation
*/
public Result call(final String method, final String apiKey, Map params) {
params = new WeakHashMap(params);
InputStream inputStream = null;
// no entry in cache, load from web
if (inputStream == null) {
// fill parameter map with apiKey and session info
params.put(PARAM_API_KEY, apiKey);
try {
final HttpURLConnection urlConnection = openPostConnection(method, params);
inputStream = getInputStreamFromConnection(urlConnection);
if (inputStream == null) {
lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(),
urlConnection.getResponseMessage());
return lastResult;
}
} catch (final IOException ioEx) {
// We will assume that the server is not ready
Log.e(TAG, "Failed to download data", ioEx);
lastResult = Result.createHttpErrorResult(HttpURLConnection.HTTP_UNAVAILABLE,
ioEx.getLocalizedMessage());
return lastResult;
}
}
try {
lastResult = createResultFromInputStream(inputStream);
} catch (final IOException ioEx) {
Log.e(TAG, "Failed to read document", ioEx);
lastResult = new Result(ioEx.getLocalizedMessage());
} catch (final SAXException saxEx) {
Log.e(TAG, "Failed to parse document", saxEx);
lastResult = new Result(saxEx.getLocalizedMessage());
}
return lastResult;
}
/**
* Creates a new {@link HttpURLConnection}, sets the proxy, if available,
* and sets the User-Agent property.
*
* @param url URL to connect to
* @return a new connection.
* @throws IOException if an I/O exception occurs.
*/
public HttpURLConnection openConnection(final String url) throws IOException {
final URL u = new URL(url);
HttpURLConnection urlConnection;
urlConnection = (HttpURLConnection)u.openConnection();
urlConnection.setRequestProperty("User-Agent", userAgent);
urlConnection.setUseCaches(true);
return urlConnection;
}
/**
* @param method
* @param params
* @return
* @throws IOException
*/
private HttpURLConnection openPostConnection(final String method,
final Map params) throws IOException {
final HttpURLConnection urlConnection = openConnection(apiRootUrl);
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
urlConnection.setUseCaches(true);
final OutputStream outputStream = urlConnection.getOutputStream();
final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
final String post = buildPostBody(method, params);
writer.write(post);
writer.close();
return urlConnection;
}
/**
* @param connection
* @return
* @throws IOException
*/
private InputStream getInputStreamFromConnection(final HttpURLConnection connection)
throws IOException {
final int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_FORBIDDEN
|| responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
return connection.getErrorStream();
} else if (responseCode == HttpURLConnection.HTTP_OK) {
return connection.getInputStream();
}
return null;
}
/**
* @param inputStream
* @return
* @throws SAXException
* @throws IOException
*/
private Result createResultFromInputStream(final InputStream inputStream) throws SAXException,
IOException {
final Document document = newDocumentBuilder().parse(
new InputSource(new InputStreamReader(inputStream, "UTF-8")));
final Element root = document.getDocumentElement(); // lfm element
final String statusString = root.getAttribute("status");
final Status status = "ok".equals(statusString) ? Status.OK : Status.FAILED;
if (status == Status.FAILED) {
final Element errorElement = (Element)root.getElementsByTagName("error").item(0);
final int errorCode = Integer.parseInt(errorElement.getAttribute("code"));
final String message = errorElement.getTextContent();
return Result.createRestErrorResult(errorCode, message);
} else {
return Result.createOkResult(document);
}
}
/**
* @return
*/
private DocumentBuilder newDocumentBuilder() {
try {
final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
return builderFactory.newDocumentBuilder();
} catch (final ParserConfigurationException e) {
// better never happens
throw new RuntimeException(e);
}
}
/**
* @param method
* @param params
* @param strings
* @return
*/
private String buildPostBody(final String method, final Map params,
final String... strings) {
final StringBuilder builder = new StringBuilder(100);
builder.append("method=");
builder.append(method);
builder.append('&');
for (final Iterator> it = params.entrySet().iterator(); it.hasNext();) {
final Entry entry = it.next();
builder.append(entry.getKey());
builder.append('=');
builder.append(encode(entry.getValue()));
if (it.hasNext() || strings.length > 0) {
builder.append('&');
}
}
int count = 0;
for (final String string : strings) {
builder.append(count % 2 == 0 ? string : encode(string));
count++;
if (count != strings.length) {
if (count % 2 == 0) {
builder.append('&');
} else {
builder.append('=');
}
}
}
return builder.toString();
}
}