package org.acra.util; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; import org.acra.ACRA; import org.acra.ReportField; import org.acra.collector.CollectorUtil; import org.acra.collector.CrashReportData; import org.acra.sender.ReportSenderException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import static org.acra.ACRA.LOG_TAG; public class JSONReportBuilder { /** * <p> * Create a JSONObject containing the whole report data with the most * detailed possible structure depth. Fields are not just converted to a * single key=value pair. If a value can be decomposed into subobjects, it * is done. * </p> * * <p> * For example, a String containing: * * <pre> * some.key.name1=value1 * some.key.name2=value2 * some.other=value3 * any.other.key=value4 * key.without.value5 * </pre> * * is converted to * * <pre> * { * some : { * key : { * name1 : "value1", * name2 : "value2" * }, * other : "value3" * }, * any : { * other : { * key : "value4" * } * } * key.without.value : true * } * </pre> * * </p> * * @param errorContent * The ACRA report data structure. * @return A JSONObject containing all fields from the report converted to * JSON. * @throws ReportSenderException * @throws JSONReportException */ public static JSONObject buildJSONReport(CrashReportData errorContent) throws JSONReportException { JSONObject jsonReport = new JSONObject(); BufferedReader reader = null; for (ReportField key : errorContent.keySet()) { try { // Each ReportField can be identified as a substructure and not // a simple String value. if (key.containsKeyValuePairs()) { JSONObject subObject = new JSONObject(); String strContent = errorContent.getProperty(key); reader = new BufferedReader(new StringReader(strContent), 1024); String line = null; try { while ((line = reader.readLine()) != null) { addJSONFromProperty(subObject, line); } } catch (IOException e) { ACRA.log.e(LOG_TAG, "Error while converting " + key.name() + " to JSON.", e); } jsonReport.accumulate(key.name(), subObject); } else { // This field is a simple String value, store it as it is jsonReport.accumulate(key.name(), guessType(errorContent.getProperty(key))); } } catch (JSONException e) { throw new JSONReportException("Could not create JSON object for key " + key, e); } finally { CollectorUtil.safeClose(reader); } } return jsonReport; } /** * <p> * Given a String containing key=value pairs on each line, adds a detailed * JSON structure to an existing JSONObject, reusing intermediate subobjects * if available when keys are composed of a succession of subkeys delimited * by dots. * </p> * * <p> * For example, adding the string "metrics.xdpi=160.0" to an object * containing * * <pre> * { * "metrics" : { "ydpi" : "160.0"}, * "width" : "320", * "height" : "533" * } * </pre> * * results in * * <pre> * { * "metrics" : { "ydpi" : "160.0", "xdpi" : "160.0"}, * "width" : "320", * "height" : "533" * } * </pre> * * </p> * * @param destination * The JSONObject where the data must be inserted. * @param propertyString * A string containing "some.key.name=Any value" * @throws JSONException */ private static void addJSONFromProperty(JSONObject destination, String propertyString) throws JSONException { int equalsIndex = propertyString.indexOf('='); if (equalsIndex > 0) { JSONObject finalObject = destination; String currentKey = propertyString.substring(0, equalsIndex).trim(); String currentValue = propertyString.substring(equalsIndex + 1).trim(); Object value = guessType(currentValue); if(value instanceof String) { value = ((String) value).replaceAll("\\\\n","\n"); } String[] splitKey = currentKey.split("\\."); if (splitKey.length > 1) { addJSONSubTree(finalObject, splitKey, value); } else { finalObject.accumulate(currentKey, value); } } else { destination.put(propertyString.trim(), true); } } private static Object guessType(String value) { if (value.equalsIgnoreCase("true")) return true; if (value.equalsIgnoreCase("false")) return false; if (value.matches("(?:^|\\s)([1-9](?:\\d*|(?:\\d{0,2})(?:,\\d{3})*)(?:\\.\\d*[1-9])?|0?\\.\\d*[1-9]|0)(?:\\s|$)")) { NumberFormat format = NumberFormat.getInstance(Locale.US); try { Number number = format.parse(value); return number; } catch (ParseException e) { // never mind } } return value; } /** * Deep insert a value inside a JSONObject, reusing existing subobjects when * available or creating them when necessary. * * @param destination * The JSONObject which receives the additional subitem. * @param keys * An array containing the path keys leading to where the value * has to be inserted. * @param value * The value to be inserted. * @throws JSONException */ private static void addJSONSubTree(JSONObject destination, String[] keys, Object value) throws JSONException { for (int i = 0; i < keys.length; i++) { String subKey = keys[i]; if (i < keys.length - 1) { JSONObject intermediate = null; if (destination.isNull(subKey)) { intermediate = new JSONObject(); destination.accumulate(subKey, intermediate); } else { Object target = destination.get(subKey); if (target instanceof JSONObject) { intermediate = destination.getJSONObject(subKey); } else if (target instanceof JSONArray) { // Unexpected JSONArray, see issue #186 JSONArray wildCard = destination.getJSONArray(subKey); for (int j = 0; j < wildCard.length(); j++) { intermediate = wildCard.optJSONObject(j); if (intermediate != null) { // Found the original JSONObject we were looking for break; } } } if (intermediate == null) { ACRA.log.e(LOG_TAG, "Unknown json subtree type, see issue #186"); // We should never get here, but if we do, drop this value to still send the report return; } } destination = intermediate; } else { destination.accumulate(subKey, value); } } } public static class JSONReportException extends Exception { private static final long serialVersionUID = -694684023635442219L; public JSONReportException(String message, Throwable e) { super(message, e); } }; }