Java tutorial
/* * Copyright (C) 2012-2013 Daniel Medina <http://danielme.com> * * This file is part of "Muspy for Android". * * "Muspy for Android" is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * "Muspy for Android" is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License version 3 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-3.0.html/> */ package com.danielme.muspyforandroid.services; import java.io.IOException; import java.net.HttpURLConnection; import java.net.Socket; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.entity.StringEntity; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.message.AbstractHttpMessage; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.Base64; import android.util.Log; import com.danielme.muspyforandroid.Constants; import com.danielme.muspyforandroid.exc.AuthorizationException; import com.danielme.muspyforandroid.model.Artist; import com.danielme.muspyforandroid.model.Release; import com.danielme.muspyforandroid.model.UserSettings; import com.danielme.muspyforandroid.utils.ArtistComparator; import com.danielme.muspyforandroid.utils.Utils; /** * WS client. * * @author Daniel Medina (danielme.com) * @since 1.0 * */ public final class MuspyClient { /* ************************************************************ */ /* FIELDS AND CONSTANTS */ /* ************************************************************ */ private static final String AUTHORIZATION = "Authorization"; private static final String APPLICATION_JSON = "application/json"; private static final String CONTENT_TYPE = "Content-Type"; private BasicHttpContext localContext; private DefaultHttpClient defaultHttpClient; private List<Artist> artists_cache; private boolean cache = true; private static final HttpHost TARGETHOST = new HttpHost(Constants.HOST, 443, "https"); private static MuspyClient muspyClient = null; private ArtistComparator artistComparator = new ArtistComparator(); // prevents the caller from creating objects private MuspyClient() { // not implemented } /** * Singleton */ public static MuspyClient getInstance() { if (muspyClient == null) { muspyClient = new MuspyClient(); } return muspyClient; } /* ************************************************************ */ /* PUBLIC */ /* ************************************************************ */ /** * Returns artists. */ public List<Artist> getArtists(Context context) throws Exception { if (artists_cache == null || !cache) { HttpGet httpGet = new HttpGet(Constants.API.ARTISTS + Utils.getUserId(context)); httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON); setAuthHeader(httpGet, context); HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext()); String responseString = EntityUtils.toString(response.getEntity()); if (!checkAuth(response)) { throw new AuthorizationException(); } JSONArray jsonArray = new JSONArray(responseString); artists_cache = new LinkedList<Artist>(); for (int i = 0; i < jsonArray.length(); i++) { artists_cache.add(populateArtist(jsonArray.getJSONObject(i))); } Collections.sort(artists_cache, artistComparator); } return artists_cache; } public List<Artist> refreshArtists(Context context) throws Exception { artists_cache = null; return getArtists(context); } public void clearArtists() { artists_cache = null; } public UserSettings getUserSettings(Context context) throws Exception { UserSettings userSettings = getUserSettings(Utils.getEmail(context), Utils.getPass(context)); if (userSettings == null) { throw new AuthorizationException(); } return userSettings; } public boolean updateUserSettings(Context context, String email, String password, boolean notifications, boolean filterAlbum, boolean filterSingle, boolean filterEP, boolean filterCompilation, boolean filterLive, boolean filterRemix, boolean filterOther) throws Exception { HttpPut httpPut = new HttpPut(Constants.API.USER + Utils.getUserId(context)); setAuthHeader(httpPut, context); List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>(); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY, getIntString(notifications))); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_ALBUM, getIntString(filterAlbum))); nameValuepairs.add( new BasicNameValuePair(Constants.SETTINGS.NOTIFY_COMPILATION, getIntString(filterCompilation))); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_EP, getIntString(filterEP))); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_LIVE, getIntString(filterLive))); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_OTHER, getIntString(filterOther))); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_REMIX, getIntString(filterRemix))); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.NOTIFY_SINGLE, getIntString(filterSingle))); // TODO password if (Utils.hasText(email) && !email.equals(Utils.getUserId(context))) { nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.EMAIL, email)); } httpPut.setEntity(new UrlEncodedFormEntity(nameValuepairs)); Log.d(MuspyClient.class.toString(), "updateUserSettings"); HttpResponse httpResponse = getDefaultHttpClient().execute(httpPut); String responseString = EntityUtils.toString(httpResponse.getEntity()); if (responseString.toUpperCase().contains(Constants.EMAIL_NON_UNIQUE.toUpperCase())) { return false; } return true; } public UserSettings getUserSettings(String email, String pass) throws Exception { UserSettings userSettings = null; HttpGet httpGet = new HttpGet(Constants.API.USER); httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON); httpGet.setHeader(AUTHORIZATION, "Basic " + Base64.encodeToString((email + ":" + pass).getBytes(), Base64.NO_WRAP)); HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext()); String responseString = EntityUtils.toString(response.getEntity()); if (checkAuth(response)) { userSettings = populateSettings(responseString); } return userSettings; } public void addArtist(Context context, String mbid) throws Exception { HttpPut httpPut = new HttpPut(Constants.API.ARTISTS + Utils.getUserId(context) + "/" + mbid); httpPut.setHeader(Constants.CONTENT_TYPE, APPLICATION_JSON); setAuthHeader(httpPut, context); JSONObject request = new JSONObject(); httpPut.setEntity(new StringEntity(request.toString())); Log.d(MuspyClient.class.toString(), "addArtist"); HttpResponse httpResponse = getDefaultHttpClient().execute(httpPut); if (!checkAuth(httpResponse)) { throw new AuthorizationException(); } artists_cache = null; } public void importLastFm(Context context, String username, String period, String top) throws Exception { HttpPut httpPut = new HttpPut(Constants.API.ARTISTS + Utils.getUserId(context) + "/"); setAuthHeader(httpPut, context); List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>(); nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.IMPORT, Constants.LASTFM.LASTFM)); nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.COUNT, top)); nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.PERIOD, period)); nameValuepairs.add(new BasicNameValuePair(Constants.LASTFM.USERNAME, username)); httpPut.setEntity(new UrlEncodedFormEntity(nameValuepairs)); Log.d(MuspyClient.class.toString(), "importLastFm"); HttpResponse httpResponse = getDefaultHttpClient().execute(httpPut); if (!checkAuth(httpResponse)) { throw new AuthorizationException(); } artists_cache = null; cache = false; } public List<Release> getReleases(Context context, int offset, String mbid, int size) throws Exception { StringBuilder getReleases = new StringBuilder(Constants.API.RELEASES); if (!Utils.hasText(mbid)) { getReleases.append(Utils.getUserId(context)); } getReleases.append("?" + Constants.API.PARAM_OFFSET + "=" + offset); getReleases.append("&" + Constants.API.PARAM_LIMIT + "=" + size); if (Utils.hasText(mbid)) { getReleases.append("&" + Constants.API.PARAM_MBID + "=" + mbid); } HttpGet httpGet = new HttpGet(getReleases.toString()); httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON); HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext()); String respString = EntityUtils.toString(response.getEntity()); JSONArray jsonArray = new JSONArray(respString); List<Release> releases = new ArrayList<Release>(jsonArray.length()); for (int i = 0; i < jsonArray.length(); i++) { releases.add(populateRelease(jsonArray.getJSONObject(i))); } return releases; } public Release getRelease(String mbid) throws Exception { HttpGet httpGet = new HttpGet(Constants.API.RELEASE + mbid); httpGet.setHeader(CONTENT_TYPE, APPLICATION_JSON); HttpResponse response = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext()); String respString = EntityUtils.toString(response.getEntity()); JSONObject jsonRelease = new JSONObject(respString); return populateRelease(jsonRelease); } public Drawable getCover(String mbid) { Drawable drawable = null; HttpURLConnection urlConnection = null; try { URL url = new URL(Constants.URL_COVER + mbid); urlConnection = (HttpURLConnection) url.openConnection(); drawable = Drawable.createFromStream(urlConnection.getInputStream(), mbid); } catch (IOException ex1) { //cover not found } catch (Exception ex2) { Log.e(MuspyClient.class.toString(), ex2.getMessage(), ex2); } finally { urlConnection.disconnect(); } return drawable; } public void deleteArtists(Context context, List<String> mbids) throws Exception { HttpDelete httpDelete = null; artists_cache = null; Log.d(MuspyClient.class.toString(), "deleting " + mbids.size() + " artists"); for (String mbid : mbids) { httpDelete = new HttpDelete(Constants.API.ARTISTS + Utils.getUserId(context) + "/" + mbid); httpDelete.setHeader(Constants.CONTENT_TYPE, APPLICATION_JSON); setAuthHeader(httpDelete, context); getDefaultHttpClient().execute(httpDelete); } } public UserSettings signUp(String email, String password) throws Exception { List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>(); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.EMAIL, email)); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.PASSWORD, password)); nameValuepairs.add(new BasicNameValuePair(Constants.SETTINGS.ACTIVATE, "1")); HttpPost httpPost = new HttpPost(Constants.API.USER); httpPost.setEntity(new UrlEncodedFormEntity(nameValuepairs)); Log.d(MuspyClient.class.toString(), "signUp"); HttpResponse httpResponse = getDefaultHttpClient().execute(httpPost); String responseString = EntityUtils.toString(httpResponse.getEntity()); if (responseString.toUpperCase().contains(Constants.USER_CREATED.toUpperCase())) { return getUserSettings(email, password); } else if (responseString.toUpperCase().contains(Constants.EMAIL_IN_USE_ERROR.toUpperCase())) { return null; } else { throw new Exception("unknown response in sign up"); } } /** * Resets password with jsoup. */ // TODO use api when available - probably never :( public boolean resetPassword(String email) throws Exception { boolean valid = true; HttpPost httpPost = new HttpPost(Constants.URL_RESET); List<BasicNameValuePair> nameValuepairs = new LinkedList<BasicNameValuePair>(); nameValuepairs.add(new BasicNameValuePair(Constants.EMAIL, email)); nameValuepairs.add(new BasicNameValuePair("csrfmiddlewaretoken", getCSRF())); httpPost.setHeader("Referer", Constants.URL_RESET); httpPost.setEntity(new UrlEncodedFormEntity(nameValuepairs)); Log.d(MuspyClient.class.toString(), "resetPassword"); HttpResponse httpResponse = getDefaultHttpClient().execute(httpPost); String responseString = EntityUtils.toString(httpResponse.getEntity()); int start = responseString.indexOf("<title>"); int end = responseString.indexOf("</title>"); if (responseString.substring(start, end).toLowerCase().contains("reset")) { valid = false; } return valid; } public void deleteAccount(Context context) throws Exception { HttpDelete httpDelete = null; Log.d(MuspyClient.class.toString(), "deleting account"); httpDelete = new HttpDelete(Constants.API.USER + Utils.getUserId(context)); // httpDelete.setHeader(Constants.CONTENT_TYPE, APPLICATION_JSON); setAuthHeader(httpDelete, context); HttpResponse httpResponse = getDefaultHttpClient().execute(httpDelete); if (!checkAuth(httpResponse)) { throw new AuthorizationException(); } artists_cache = null; } /* ************************************************************ */ /* RESPONSE MAPPING */ /* ************************************************************ */ private UserSettings populateSettings(String responseString) throws Exception { UserSettings userSettings = new UserSettings(); JSONObject responseJSON = new JSONObject(responseString); userSettings = new UserSettings(); userSettings.setId(responseJSON.getString(Constants.SETTINGS.USERID)); userSettings.setEmail(responseJSON.getString(Constants.SETTINGS.EMAIL)); userSettings.setNotifications(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY)); userSettings.setFilterAlbum(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_ALBUM)); userSettings.setFilterCompilation(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_COMPILATION)); userSettings.setFilterEP(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_EP)); userSettings.setFilterLive(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_LIVE)); userSettings.setFilterOther(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_OTHER)); userSettings.setFilterSingle(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_SINGLE)); userSettings.setFilterRemix(responseJSON.getBoolean(Constants.SETTINGS.NOTIFY_REMIX)); return userSettings; } private Release populateRelease(JSONObject jsonRelease) throws JSONException { Release release = new Release(); release.setMbid(jsonRelease.getString(Constants.RELEASE.MBID)); release.setName(jsonRelease.getString(Constants.RELEASE.NAME)); String type = jsonRelease.getString(Constants.RELEASE.TYPE); if (Locale.getDefault().getLanguage().equals(Constants.LOCALE_ES)) { type = Constants.TYPE.TYPES_ES.get(type.toUpperCase()); if (type == null) { type = ""; } } release.setType(type); String jsonDate = jsonRelease.getString(Constants.RELEASE.DATE); release.setYear(jsonDate.substring(0, 4)); if (jsonDate.length() > 4) { release.setMonth(jsonDate.substring(5, 7)); if (jsonDate.length() > 8) { release.setDay(jsonDate.substring(8)); } } JSONObject jsonArtist = jsonRelease.getJSONObject(Constants.RELEASE.ARTIST_OBJ); release.setArtist(populateArtist(jsonArtist)); return release; } private Artist populateArtist(JSONObject jsonArtist) throws JSONException { Artist artist = new Artist(); artist.setMbid(jsonArtist.getString(Constants.ARTIST.MBID)); artist.setDisambiguation(jsonArtist.getString(Constants.ARTIST.DISAMBIGUATION)); artist.setName(jsonArtist.getString(Constants.ARTIST.NAME)); artist.setSortname(jsonArtist.getString(Constants.ARTIST.SORTNAME)); return artist; } /* ************************************************************ */ /* PRIVATE */ /* ************************************************************ */ private void setAuthHeader(AbstractHttpMessage abstractHttpMessage, Context context) throws AuthorizationException { abstractHttpMessage.setHeader(AUTHORIZATION, "Basic " + Base64.encodeToString( (Utils.getEmail(context) + ":" + Utils.getPass(context)).getBytes(), Base64.NO_WRAP)); } // TODO This implementation accepts all certificates. This should be // improved in future versions in order to validate muspy.com // certificate. // http://stackoverflow.com/questions/2703161/how-to-ignore-ssl-certificate-errors-in-apache-httpclient-4-0 private class MySSLSocketFactory extends SSLSocketFactory { private SSLContext sslContext = SSLContext.getInstance("TLS"); public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { super(truststore); TrustManager trustManager = new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // not implemented } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // not implemented } public X509Certificate[] getAcceptedIssuers() { return null; } }; sslContext.init(null, new TrustManager[] { trustManager }, null); } @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); } @Override public Socket createSocket() throws IOException { return sslContext.getSocketFactory().createSocket(); } } private DefaultHttpClient getDefaultHttpClient() throws Exception { if (defaultHttpClient == null) { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null, null); SSLSocketFactory sslSocketFactory = new MySSLSocketFactory(trustStore); sslSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); registry.register(new Scheme("https", sslSocketFactory, 443)); defaultHttpClient = new DefaultHttpClient( new ThreadSafeClientConnManager(new BasicHttpParams(), registry), new BasicHttpParams()); } return defaultHttpClient; } private BasicHttpContext getBasicHttpContext() { if (localContext == null) { localContext = new BasicHttpContext(); BasicScheme basicAuth = new BasicScheme(); localContext.setAttribute("preemptive-auth", basicAuth); } return localContext; } private String getCSRF() throws Exception { HttpGet httpGet = new HttpGet(Constants.URL_RESET); HttpResponse httpResponse = getDefaultHttpClient().execute(TARGETHOST, httpGet, getBasicHttpContext()); String responseString = EntityUtils.toString(httpResponse.getEntity()); Document doc = Jsoup.parse(responseString); Elements elementsByTag = doc.getElementsByTag("input"); for (Element element : elementsByTag) { if ("csrfmiddlewaretoken".equals(element.attr("name"))) { return element.attr("value"); } } throw new Exception("csrf not found"); } private String getIntString(boolean booleanValue) { return booleanValue ? "1" : "0"; } private boolean checkAuth(HttpResponse response) { boolean auth = true; if (response.getStatusLine() != null && response.getStatusLine().getStatusCode() == 401) { auth = false; } return auth; } }