/*
* Copyright (C) 2005-2010 Team XBMC
* http://xbmc.org
*
* This Program 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; either version 2, or (at your option)
* any later version.
*
* This Program 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
* along with XBMC Remote; see the file license. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
package org.xbmc.jsonrpc;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import org.apache.http.HttpException;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import org.xbmc.api.business.INotifiableManager;
import org.xbmc.api.object.Host;
import org.xbmc.httpapi.NoSettingsException;
import org.xbmc.jsonrpc.client.Client;
import android.util.Log;
/**
* Singleton class. Will be instantiated only once and contains mostly help
*
* @author Team XBMC
*/
public class Connection {
private static final String TAG = "Connection-JsonRpc";
private static final String XBMC_JSONRPC_BOOTSTRAP = "/jsonrpc";
private static final String XBMC_THUMB_BOOTSTRAP = "/thumb";
//private static final int SOCKET_CONNECTION_TIMEOUT = 500000000;
private static final int SOCKET_CONNECTION_TIMEOUT = 9000;
/**
* Singleton class instance
*/
private static Connection sConnection;
/**
* Complete URL without any attached command parameters, for instance:
* <code>http://192.168.0.10:8080</code>
*/
private String mUrl;
/**
* Socket read timeout (connection timeout is default)
*/
private int mSocketReadTimeout = 0;
/**
* Performs HTTP Authentication
*/
private HttpAuthenticator mAuthenticator = null;
/**
* Use getInstance() for public class instantiation
* @param host XBMC host
* @param port Port
*/
private Connection(String host, int port) {
setHost(host, port);
}
/**
* Returns the singleton instance of this connection. Note that host and
* port settings are only looked at the first time. Use {@link setHost()}
* if you want to update these parameters.
* @param host XBMC host
* @param port HTTP API / JSON-RPC port (it's the same)
* @return Connection instance
*/
public static Connection getInstance(String host, int port) {
if (sConnection == null) {
sConnection = new Connection(host, port);
}
if (sConnection.mUrl == null) {
sConnection.setHost(host, port);
}
return sConnection;
}
/**
* Updates host info of the connection instance
* @param host
*/
public void setHost(Host host) {
if (host == null) {
setHost(null, 0);
} else {
setHost(host.addr, host.port);
setAuth(host.user, host.pass);
}
}
/**
* Updates host and port parameters of the connection instance.
* @param host Host or IP address of the host
* @param port HTTP port
*/
public void setHost(String host, int port) {
if (host == null || port <= 0) {
mUrl = null;
} else {
StringBuilder sb = new StringBuilder();
sb.append("http://");
sb.append(host);
sb.append(":");
sb.append(port);
mUrl = sb.toString();
}
}
/**
* Sets authentication info
* @param user Username
* @param pass Password
*/
public void setAuth(String user, String pass) {
if (pass != null && pass.length() > 0) {
mAuthenticator = new HttpAuthenticator(user, pass);
Authenticator.setDefault(mAuthenticator);
} else {
mAuthenticator = null;
Authenticator.setDefault(null);
}
}
/**
* Sets socket read timeout (connection timeout has constant value)
* @param timeout Read timeout in milliseconds.
*/
public void setTimeout(int timeout) {
if (timeout > 0) {
mSocketReadTimeout = timeout;
}
}
public InputStream getThumbInputStream(String url, INotifiableManager manager) throws FileNotFoundException {
try {
final URL u = new URL(mUrl + XBMC_THUMB_BOOTSTRAP + "/" + url + ".jpg");
Log.i(TAG, "Returning input stream for " + u.toString());
URLConnection uc;
uc = u.openConnection();
uc.setConnectTimeout(SOCKET_CONNECTION_TIMEOUT);
uc.setReadTimeout(mSocketReadTimeout);
return uc.getInputStream();
} catch (FileNotFoundException e) {
throw e;
} catch (IOException e) {
manager.onError(e);
}
return null;
}
/**
* Executes a query.
* @param command Name of the command to execute
* @param parameters Parameters
* @param manager Reference back to business layer
* @return Parsed JSON object, empty object on error.
*/
public JsonNode query(String command, JsonNode parameters, INotifiableManager manager) {
URLConnection uc = null;
try {
final ObjectMapper mapper = Client.MAPPER;
if (mUrl == null) {
throw new NoSettingsException();
}
if (mAuthenticator != null) {
mAuthenticator.resetCounter();
}
final URL url = new URL(mUrl + XBMC_JSONRPC_BOOTSTRAP);
uc = url.openConnection();
uc.setConnectTimeout(SOCKET_CONNECTION_TIMEOUT);
uc.setReadTimeout(mSocketReadTimeout);
uc.setDoOutput(true);
final ObjectNode data = Client.obj()
.put("jsonrpc", "2.0")
.put("method", command)
.put("id", "1");
if (parameters != null) {
data.put("params", parameters);
}
final JsonFactory jsonFactory = new JsonFactory();
final JsonGenerator jg = jsonFactory.createJsonGenerator(uc.getOutputStream(), JsonEncoding.UTF8);
jg.setCodec(mapper);
// POST data
jg.writeTree(data);
jg.flush();
final JsonParser jp = jsonFactory.createJsonParser(uc.getInputStream());
jp.setCodec(mapper);
final JsonNode ret = jp.readValueAs(JsonNode.class);
return ret;
} catch (MalformedURLException e) {
manager.onError(e);
} catch (IOException e) {
int responseCode = -1;
try {
responseCode = ((HttpURLConnection)uc).getResponseCode();
} catch (IOException e1) { } // do nothing, getResponse code failed so treat as default i/o exception.
if (uc != null && responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
manager.onError(new HttpException(Integer.toString(HttpURLConnection.HTTP_UNAUTHORIZED)));
} else {
manager.onError(e);
}
} catch (NoSettingsException e) {
manager.onError(e);
}
return new ObjectNode(null);
}
/**
* Executes a JSON-RPC command and returns the result as JSON object.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param parameters Parameters of the method
* @return Result
*/
public JsonNode getJson(INotifiableManager manager, String method, JsonNode parameters) {
try {
final JsonNode response = query(method, parameters, manager);
final JsonNode result = response.get(RESULT_FIELD);
if (result == null) {
if (response.get(ERROR_FIELD) == null) {
throw new Exception("Weird JSON response, could not parse error.");
} else {
throw new Exception(response.get(ERROR_FIELD).get("message").getTextValue());
}
} else {
return response.get(RESULT_FIELD);
}
} catch (Exception e) {
manager.onError(e);
}
return Client.obj();
}
public JsonNode getJson(INotifiableManager manager, String method, JsonNode parameters, String resultField) {
try {
final JsonNode response = getJson(manager, method, parameters);
final JsonNode result = response.get(resultField);
if (result == null) {
throw new Exception("Could not find field \"" + resultField + "\" as return value.");
} else {
return response.get(resultField);
}
} catch (Exception e) {
manager.onError(e);
}
return Client.obj();
}
/**
* Executes a JSON-RPC command without parameters and returns the result as
* JSON object.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @return Result
*/
public JsonNode getJson(INotifiableManager manager, String method) {
return query(method, null, manager).get(RESULT_FIELD);
}
/**
* Executes an JSON-RPC method and returns the result from a field as string.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param parameters Parameters of the method, separated by ";"
* @param returnField Name of the field to return
* @return Result
*/
public String getString(INotifiableManager manager, String method, ObjectNode parameters, String returnField) {
final JsonNode result = query(method, parameters, manager).get(RESULT_FIELD);
return result == null ? "" : result.get(returnField).getValueAsText();
}
/**
* Executes an JSON-RPC method without parameter and returns the result
* from a field as string.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param returnField Name of the field to return
* @return Result as string
*/
public String getString(INotifiableManager manager, String method, String returnField) {
return getString(manager, method, null, returnField);
}
/**
* Executes an JSON-RPC method and returns the result from a field as integer.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param parameters Parameters of the method, separated by ";"
* @param returnField Name of the field to return
* @return Result as integer
*/
public int getInt(INotifiableManager manager, String method, ObjectNode parameters, String returnField) {
try {
return Integer.parseInt(getString(manager, method, parameters, returnField));
} catch (NumberFormatException e) {
return 0;
}
}
/**
* Executes an JSON-RPC method without parameter and returns the result
* from a field as integer.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param returnField Name of the field to return
* @return Result as integer
*/
public int getInt(INotifiableManager manager, String method, String returnField) {
return getInt(manager, method, null, returnField);
}
/**
* Executes an JSON-RPC method and returns the result from a field as boolean.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param parameters Parameters of the method, separated by ";"
* @param returnField Name of the field to return
* @return Result as boolean
*/
public boolean getBoolean(INotifiableManager manager, String method, ObjectNode parameters, String returnField) {
return getString(manager, method, parameters, returnField).equals("true");
}
/**
* Executes an JSON-RPC method without parameters and returns the result
* from a field as boolean.
* @param manager Upper layer reference for error posting
* @param method Name of the method to run
* @param returnField Name of the field to return
* @return Result as boolean
*/
public boolean getBoolean(INotifiableManager manager, String method, String returnField) {
return getBoolean(manager, method, null, returnField);
}
/**
* HTTP Authenticator.
* @author Team XBMC
*/
public class HttpAuthenticator extends Authenticator {
public static final int MAX_RETRY = 5;
private final String mUser;
private final char[] mPass;
private int mRetryCount = 0;
public HttpAuthenticator(String user, String pass) {
mUser = user;
mPass = pass != null ? pass.toCharArray() : new char[0];
}
/**
* This method is called when a password-protected URL is accessed
*/
protected PasswordAuthentication getPasswordAuthentication() {
if (mRetryCount < MAX_RETRY) {
mRetryCount++;
return new PasswordAuthentication(mUser, mPass);
}
return null;
}
/**
* This method has to be called after each successful connection!!!
*/
public void resetCounter() {
mRetryCount = 0;
}
}
public static final String RESULT_FIELD = "result";
public static final String ERROR_FIELD = "error";
}