Package de.umass.lastfm

Source Code of de.umass.lastfm.Caller

/*
* Copyright (c) 2010, 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 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.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import de.umass.lastfm.Result.Status;
import de.umass.lastfm.cache.Cache;
import de.umass.lastfm.cache.FileSystemCache;

import static de.umass.util.StringUtilities.encode;
import static de.umass.util.StringUtilities.map;
import static de.umass.util.StringUtilities.md5;

/**
* 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 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;
  }

  /**
   * 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;
  }

  /**
   * 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.
   *
   * @param debugMode <code>true</code> to enable debug mode
   */
  public void setDebugMode(boolean debugMode) {
    this.debugMode = debugMode;
  }

  /**
   * 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;
  }

  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
    String cacheEntryName = Cache.createCacheEntryName(method, params);
    if (session == null && cache != null) {
      if (cache.contains(cacheEntryName) && !cache.isExpired(cacheEntryName)) {
        inputStream = cache.load(cacheEntryName);
      }
    }
    if (inputStream == null) {
      params.put(PARAM_API_KEY, apiKey);
      if (session != null) {
        params.put("sk", session.getKey());
        String sig = Authenticator.createSignature(method, params, session.getSecret());
        params.put("api_sig", sig);
      }
      try {
        HttpURLConnection urlConnection = openConnection(apiRootUrl);
        urlConnection.setRequestMethod("POST");
        urlConnection.setDoOutput(true);
        OutputStream outputStream = urlConnection.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
        String post = buildParameterQueue(method, params);
        if (debugMode) {
          System.out.println("body: " + post);
        }
        writer.write(post);
        writer.close();
        int responseCode = urlConnection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_FORBIDDEN || responseCode == HttpURLConnection.HTTP_BAD_REQUEST) {
          inputStream = urlConnection.getErrorStream();
        } else if (responseCode != HttpURLConnection.HTTP_OK) {
          this.lastResult = Result.createHttpErrorResult(responseCode, urlConnection.getResponseMessage());
          return lastResult;
        } else {
          inputStream = urlConnection.getInputStream();
          if (cache != null) {
            long expires = urlConnection.getHeaderFieldDate("Expires", -1);
            if (expires == -1) {
              long expirationTime = cache.getExpirationPolicy().getExpirationTime(method, params);
              if (expirationTime > 0) {
                if (expirationTime == Long.MAX_VALUE) {
                  expires = Long.MAX_VALUE;
                } else {
                  expires = System.currentTimeMillis() + expirationTime;
                }
              }
            }
            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 failed.");
            }
          }
        }
      } catch (IOException e) {
        throw new CallException(e);
      }
    }
    try {
      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) {
        if (cache != null)
          cache.remove(cacheEntryName); // if request was failed remove from cache
        Element error = (Element) root.getElementsByTagName("error").item(0);
        int errorCode = Integer.parseInt(error.getAttribute("code"));
        String message = error.getTextContent();
        if (debugMode)
          System.err.printf("Failed. Code: %d, Error: %s%n", errorCode, message);
        this.lastResult = Result.createRestErrorResult(errorCode, message);
      } else {
        this.lastResult = Result.createOkResult(document);
      }
      return this.lastResult;
    } catch (IOException e) {
      throw new CallException(e);
    } catch (SAXException e) {
      throw new CallException(e);
    }
  }

  /**
   * 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;
  }

  private DocumentBuilder newDocumentBuilder() {
    try {
      DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
      return builderFactory.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
      // better never happens
      throw new RuntimeException(e);
    }
  }

  /**
   * 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 {
    if (isDebugMode())
      System.out.println("open: " + 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 String buildParameterQueue(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();
  }

  private String createSignature(Map<String, String> params, String secret) {
    Set<String> sorted = new TreeSet<String>(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());
  }

  public Proxy getProxy() {
    return proxy;
  }

  public String getUserAgent() {
    return userAgent;
  }

  public boolean isDebugMode() {
    return debugMode;
  }
}
TOP

Related Classes of de.umass.lastfm.Caller

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.