Skip to content

Instantly share code, notes, and snippets.

@MadMarty
Last active August 30, 2015 17:12
Show Gist options
  • Save MadMarty/a2639504c94d37a03632 to your computer and use it in GitHub Desktop.
Save MadMarty/a2639504c94d37a03632 to your computer and use it in GitHub Desktop.
last.fm java wrapper GET/POST fix
/*
* 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 de.umass.lastfm;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.umass.lastfm.Result.Status;
import de.umass.lastfm.cache.Cache;
import de.umass.lastfm.cache.FileSystemCache;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import static de.umass.util.StringUtilities.*;
/**
* The <code>Caller</code> class handles the low-level communication between the client and last.fm.<br/>
* Direct usage of this class should be unnecessary since all method calls are available via the methods in
* the <code>Artist</code>, <code>Album</code>, <code>User</code>, etc. classes.
* If specialized calls which are not covered by the Java API are necessary this class may be used directly.<br/>
* Supports the setting of a custom {@link Proxy} and a custom <code>User-Agent</code> HTTP header.
*
* @author Janni Kovacs
*/
public class Caller {
private static final String PARAM_API_KEY = "api_key";
private static final String PARAM_METHOD = "method";
private static final String DEFAULT_API_ROOT = "http://ws.audioscrobbler.com/2.0/";
private static final Caller instance = new Caller();
private final Logger log = Logger.getLogger("de.umass.lastfm.Caller");
private String apiRootUrl = DEFAULT_API_ROOT;
private Proxy proxy;
private String userAgent = "tst";
private boolean debugMode = false;
private Cache cache;
private Result lastResult;
private Caller() {
cache = new FileSystemCache();
}
/**
* Returns the single instance of the <code>Caller</code> class.
*
* @return a <code>Caller</code>
*/
public static Caller getInstance() {
return instance;
}
/**
* Set api root url.
*
* @param apiRootUrl new api root url
*/
public void setApiRootUrl(String apiRootUrl) {
this.apiRootUrl = apiRootUrl;
}
/**
* Sets a {@link Proxy} instance this Caller will use for all upcoming HTTP requests. May be <code>null</code>.
*
* @param proxy A <code>Proxy</code> or <code>null</code>.
*/
public void setProxy(Proxy proxy) {
this.proxy = proxy;
}
public Proxy getProxy() {
return proxy;
}
/**
* Sets a User Agent this Caller will use for all upcoming HTTP requests. For testing purposes use "tst".
* If you distribute your application use an identifiable User-Agent.
*
* @param userAgent a User-Agent string
*/
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getUserAgent() {
return userAgent;
}
/**
* Returns the current {@link Cache}.
*
* @return the Cache
*/
public Cache getCache() {
return cache;
}
/**
* Sets the active {@link Cache}. May be <code>null</code> to disable caching.
*
* @param cache the new Cache or <code>null</code>
*/
public void setCache(Cache cache) {
this.cache = cache;
}
/**
* Sets the <code>debugMode</code> property. If <code>debugMode</code> is <code>true</code> all call() methods
* will print debug information and error messages on failure to stdout and stderr respectively.<br/>
* Default is <code>false</code>. Set this to <code>true</code> while in development and for troubleshooting.
*
* @see de.umass.lastfm.Caller#getLogger()
* @param debugMode <code>true</code> to enable debug mode
* @deprecated Use the Logger instead
*/
@Deprecated
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
log.setLevel(debugMode ? Level.ALL : Level.OFF);
}
/**
* @see de.umass.lastfm.Caller#getLogger()
* @return the debugMode property
* @deprecated Use the Logger instead
*/
@Deprecated
public boolean isDebugMode() {
return debugMode;
}
public Logger getLogger() {
return log;
}
/**
* Returns the {@link Result} of the last operation, or <code>null</code> if no call operation has been
* performed yet.
*
* @return the last Result object
*/
public Result getLastResult() {
return lastResult;
}
public Result call(String method, String apiKey, String... params) throws CallException {
return call(method, apiKey, map(params));
}
public Result call(String method, String apiKey, Map<String, String> params) throws CallException {
return call(method, apiKey, params, null);
}
public Result call(String method, Session session, String... params) {
return call(method, session.getApiKey(), map(params), session);
}
public Result call(String method, Session session, Map<String, String> params) {
return call(method, session.getApiKey(), params, session);
}
/**
* Performs the web-service call. If the <code>session</code> parameter is <code>non-null</code> then an
* authenticated call is made. If it's <code>null</code> then an unauthenticated call is made.<br/>
* The <code>apiKey</code> 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 <code>null</code>
* @return the result of the operation
*/
private Result call(String method, String apiKey, Map<String, String> params, Session session) {
params = new HashMap<String, String>(params); // create new Map in case params is an immutable Map
InputStream inputStream = null;
// try to load from cache
boolean fromCache = false;
String cacheEntryName = Cache.createCacheEntryName(method, params);
if (session == null && cache != null) {
inputStream = getStreamFromCache(cacheEntryName);
if (inputStream != null) {
fromCache = true;
}
}
// no entry in cache, load from web
if (inputStream == null) {
// fill parameter map with apiKey and session info
params.put(PARAM_API_KEY, apiKey);
if (session != null) {
params.put("sk", session.getKey());
params.put("api_sig", Authenticator.createSignature(method, params, session.getSecret()));
}
try {
HttpURLConnection urlConnection = openGetConnection(method, params);
inputStream = getInputStreamFromConnection(urlConnection);
if (inputStream == null) {
this.lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(), urlConnection.getResponseMessage(), false);
return lastResult;
} else {
if (cache != null) {
long expires = urlConnection.getHeaderFieldDate("Expires", -1);
if (expires == -1) {
expires = cache.findExpirationDate(method, params);
}
if (expires != -1) {
cache.store(cacheEntryName, inputStream, expires); // if data wasn't cached store new result
inputStream = cache.load(cacheEntryName);
if (inputStream == null)
throw new CallException("Caching/Reloading failed");
}
}
}
} catch (Exception e) {
throw new CallException(e);
}
}
try {
Result result = createResultFromInputStream(inputStream, fromCache);
if (!result.isSuccessful()) {
log.warning(String.format("API call failed with result: %s%n", result));
if (cache != null) {
cache.remove(cacheEntryName);
}
}
this.lastResult = result;
return result;
} catch (IOException e) {
throw new CallException(e);
} catch (SAXException e) {
//TODO: handle ex
// throw new CallException(e);
}
return lastResult;
}
private InputStream getStreamFromCache(String cacheEntryName) {
if (cache != null && cache.contains(cacheEntryName) && !cache.isExpired(cacheEntryName)) {
return cache.load(cacheEntryName);
}
return null;
}
/**
* 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(String url) throws IOException {
log.info("Open connection: " + url);
URL u = new URL(url);
HttpURLConnection urlConnection;
if (proxy != null)
urlConnection = (HttpURLConnection) u.openConnection(proxy);
else
urlConnection = (HttpURLConnection) u.openConnection();
urlConnection.setRequestProperty("User-Agent", userAgent);
return urlConnection;
}
private HttpURLConnection openPostConnection(String method, Map<String, String> params) throws IOException {
HttpURLConnection urlConnection = openConnection(apiRootUrl);
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
OutputStream outputStream = urlConnection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
String post = buildPostBody(method, params);
log.info("POST body: " + post);
writer.write(post);
writer.close();
return urlConnection;
}
private HttpURLConnection openGetConnection(String method, Map<String, String> params) throws IOException {
String get = buildPostBody(method, params);
log.info("GET body: " + get);
HttpURLConnection urlConnection = openConnection(apiRootUrl + "?" + get);
urlConnection.setRequestMethod("GET");
urlConnection.setDoOutput(true);
urlConnection.connect();
return urlConnection;
}
private InputStream getInputStreamFromConnection(HttpURLConnection connection) throws IOException {
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;
}
private Result createResultFromInputStream(InputStream inputStream, boolean fromCache) throws SAXException, IOException {
Document document = newDocumentBuilder().parse(new InputSource(new InputStreamReader(inputStream, "UTF-8")));
Element root = document.getDocumentElement(); // lfm element
String statusString = root.getAttribute("status");
Status status = "ok".equals(statusString) ? Status.OK : Status.FAILED;
if (status == Status.FAILED) {
Element errorElement = (Element) root.getElementsByTagName("error").item(0);
int errorCode = Integer.parseInt(errorElement.getAttribute("code"));
String message = errorElement.getTextContent();
return Result.createRestErrorResult(errorCode, message, fromCache);
} else {
return Result.createOkResult(document, fromCache);
}
}
private DocumentBuilder newDocumentBuilder() {
try {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
return builderFactory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
// better never happens
throw new RuntimeException(e);
}
}
private String buildPostBody(String method, Map<String, String> params, String... strings) {
StringBuilder builder = new StringBuilder(100);
builder.append("method=");
builder.append(method);
builder.append('&');
for (Iterator<Entry<String, String>> it = params.entrySet().iterator(); it.hasNext();) {
Entry<String, String> 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 (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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment