Last active
August 30, 2015 17:12
-
-
Save MadMarty/a2639504c94d37a03632 to your computer and use it in GitHub Desktop.
last.fm java wrapper GET/POST fix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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