All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.umass.lastfm.Caller Maven / Gradle / Ivy

The newest version!
/*
 * 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 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 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 Caller class. * * @return a Caller */ 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 null. * * @param proxy A Proxy or null. */ 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 null to disable caching. * * @param cache the new Cache or null */ public void setCache(Cache cache) { this.cache = cache; } /** * Sets the debugMode property. If debugMode is true all call() methods * will print debug information and error messages on failure to stdout and stderr respectively.
* Default is false. Set this to true while in development and for troubleshooting. * * @see de.umass.lastfm.Caller#getLogger() * @param debugMode true to enable debug mode * @deprecated Use the Logger instead */ 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 */ public boolean isDebugMode() { return debugMode; } public Logger getLogger() { return log; } /** * Returns the {@link Result} of the last operation, or null 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 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 params) { return call(method, session.getApiKey(), params, session); } /** * 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 */ private Result call(String method, String apiKey, Map params, Session session) { params = new HashMap(params); // create new Map in case params is an immutable Map InputStream inputStream = null; // try to load from cache String cacheEntryName = Cache.createCacheEntryName(method, params); if (session == null && cache != null) { inputStream = getStreamFromCache(cacheEntryName); } // 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 = openPostConnection(method, params); inputStream = getInputStreamFromConnection(urlConnection); if (inputStream == null) { this.lastResult = Result.createHttpErrorResult(urlConnection.getResponseCode(), urlConnection.getResponseMessage()); 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 (IOException e) { throw new CallException(e); } } try { Result result = createResultFromInputStream(inputStream); 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) { throw new CallException(e); } } 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 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 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) 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); } else { return Result.createOkResult(document); } } 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 params, String... strings) { StringBuilder builder = new StringBuilder(100); builder.append("method="); builder.append(method); builder.append('&'); for (Iterator> it = params.entrySet().iterator(); it.hasNext();) { 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 (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(); } private String createSignature(Map params, String secret) { Set sorted = new TreeSet(params.keySet()); StringBuilder builder = new StringBuilder(50); for (String s : sorted) { builder.append(s); builder.append(encode(params.get(s))); } builder.append(secret); return md5(builder.toString()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy