/* * Copyright (C) 2005-2015 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.android.jsonrpc.io; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ObjectNode; import org.codehaus.jackson.node.TextNode; import android.util.Log; /** * Performs HTTP POST requests on the XBMC JSON API and handles the parsing from * and to {@link ObjectNode}. * <p/> * <i>Note</i>: All in here is synchronous. * * @author Joel Stemmer <stemmertech@gmail.com> * @author freezy <freezy@xbmc.org> */ public class JsonApiRequest { private static final String TAG = JsonApiRequest.class.getSimpleName(); private static final int REQUEST_TIMEOUT = 5000; // 5 sec private static final ObjectMapper OM = new ObjectMapper(); /** * Executes a POST request to the URL using the JSON Object as request body * and returns a JSON Object if the response was successful. * * @param url Complete URL with schema, host, port if not default and path. * @param entity Object being serialized as message body * @return JSON Object of the JSON-RPC response. * @throws ApiException */ public static ObjectNode execute(String url, String user, String pass, ObjectNode entity) throws ApiException { try { String response = postRequest(new URL(url), user, pass, entity.toString()); return parseResponse(response); } catch (MalformedURLException e) { throw new ApiException(ApiException.MALFORMED_URL, e.getMessage(), e); } } /** * Execute a POST request on URL using entity as request body. * * @param url * @param entity * @return The response as a string * @throws ApiException * @throws IOException */ private static String postRequest(URL url, String user, String pass, String entity) throws ApiException { try { final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); // http basic authorization if (user != null && !user.isEmpty() && pass != null && !pass.isEmpty()) { final String token = Base64.encodeToString((user + ":" + pass).getBytes(), false); conn.setRequestProperty("Authorization", "Basic " + token); } conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("User-Agent", buildUserAgent()); conn.setConnectTimeout(REQUEST_TIMEOUT); conn.setReadTimeout(REQUEST_TIMEOUT); conn.setDoOutput(true); try { OutputStreamWriter output = new OutputStreamWriter(conn.getOutputStream(), "UTF-8"); output.write(entity); output.close(); } catch (UnsupportedEncodingException e) { throw new ApiException(ApiException.UNSUPPORTED_ENCODING, "Unable to convert request to UTF-8", e); } Log.i(TAG, "POST request: " + conn.getURL()); Log.i(TAG, "POST entity:" + entity); StringBuilder response = new StringBuilder(); BufferedReader reader = null; final int code = conn.getResponseCode(); if (code == 200) { try { reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"), 8192); String line; while ((line = reader.readLine()) != null) { response.append(line); } } catch (UnsupportedEncodingException e) { throw new ApiException(ApiException.UNSUPPORTED_ENCODING, "Unable to convert HTTP response to UTF-8", e); } finally { if (reader != null) { reader.close(); } } Log.i(TAG, "POST response: " + response.toString()); return response.toString(); } else { switch (code) { case 400: throw new ApiException(ApiException.HTTP_BAD_REQUEST, "Server says \"400 Bad HTTP request\"."); case 401: throw new ApiException(ApiException.HTTP_UNAUTHORIZED, "Server says \"401 Unauthorized\"."); case 403: throw new ApiException(ApiException.HTTP_FORBIDDEN, "Server says \"403 Forbidden\"."); case 404: throw new ApiException(ApiException.HTTP_NOT_FOUND, "Server says \"404 Not Found\"."); default: if (code >= 100 && code < 200) { throw new ApiException(ApiException.HTTP_INFO, "Server returned informational code " + code + " instead of 200."); } else if (code >= 200 && code < 300) { throw new ApiException(ApiException.HTTP_SUCCESS, "Server returned success code " + code + " instead of 200."); } else if (code >= 300 && code < 400) { throw new ApiException(ApiException.HTTP_REDIRECTION, "Server returned redirection code " + code + " instead of 200."); } else if (code >= 400 && code < 500) { throw new ApiException(ApiException.HTTP_CLIENT_ERROR, "Server returned client error " + code + "."); } else if (code >= 500 && code < 600) { throw new ApiException(ApiException.HTTP_SERVER_ERROR, "Server returned server error " + code + "."); } else { throw new ApiException(ApiException.HTTP_UNKNOWN, "Server returned unspecified code " + code + "."); } } } } catch (SocketTimeoutException e) { throw new ApiException(ApiException.IO_SOCKETTIMEOUT, e.getMessage(), e); } catch (IOException e) { throw new ApiException(ApiException.IO_EXCEPTION, e.getMessage(), e); } } /** * Parses the JSON response string and returns a {@link ObjectNode}. * * If the response is not valid JSON, contained an error message or did not * include a result then a HandlerException is thrown. * * @param response * @return ObjectNode Root node of the server response, unserialized as * ObjectNode. * @throws ApiException */ private static ObjectNode parseResponse(String response) throws ApiException { try { final ObjectNode node = (ObjectNode) OM.readTree(response.toString()); if (node.has("error")) { if (node.get("error").isTextual()) { final TextNode error = (TextNode) node.get("error"); Log.e(TAG, "[JSON-RPC] " + error.getTextValue()); Log.e(TAG, "[JSON-RPC] " + response); throw new ApiException(ApiException.API_ERROR, "Error: " + error.getTextValue(), null); } else { final ObjectNode error = (ObjectNode) node.get("error"); Log.e(TAG, "[JSON-RPC] " + error.get("message").getTextValue()); Log.e(TAG, "[JSON-RPC] " + response); throw new ApiException(ApiException.API_ERROR, "Error " + error.get("code").getIntValue() + ": " + error.get("message").getTextValue(), null); } } if (!node.has("result")) { Log.e(TAG, "[JSON-RPC] " + response); throw new ApiException(ApiException.RESPONSE_ERROR, "Neither result nor error object found in response.", null); } if (node.get("result").isNull()) { return null; } return node; } catch (JsonProcessingException e) { throw new ApiException(ApiException.JSON_EXCEPTION, "Parse error: " + e.getMessage(), e); } catch (IOException e) { throw new ApiException(ApiException.JSON_EXCEPTION, "Parse error: " + e.getMessage(), e); } } /** * Build user agent used for the HTTP requests * * TODO: include version information * * @return String containing the user agent */ private static String buildUserAgent() { return "xbmc-jsonrpclib-android"; } }