JSON Flattener

2012/01/21

I recently ran into the problem of needing to store a JSON object in a key, value store. I pasted a little utility class that does the job below. Here’s an example run against some nasty JSON,

ORIGINAL:

{ string: 'aString', integer: -1, nested: { x: 1, y: 2, z: 3}, moreNested: { a: [1,2,3], b: [4,5,6], c: [7,8,9]}, arrayFromHell: [{ innerArray: [{ innerInnerArray: [1,2,3]}]}]}

ENCODED:

{"arrayFromHell.0.innerArray.0.innerInnerArray.0":1,"arrayFromHell.0.innerArray.0.innerInnerArray.1":2,"arrayFromHell.0.innerArray.0.innerInnerArray.2":3,"nested.z":3,"nested.y":2,"nested.x":1,"integer":-1,"string":"aString","moreNested.b.0":4,"moreNested.b.1":5,"moreNested.b.2":6,"moreNested.c.0":7,"moreNested.c.1":8,"moreNested.c.2":9,"moreNested.a.0":1,"moreNested.a.1":2,"moreNested.a.2":3}

DECODED:

{"nested":{"z":3,"y":2,"x":1},"arrayFromHell":[{"innerArray":[{"innerInnerArray":[1,2,3]}]}],"integer":-1,"string":"aString","moreNested":{"b":[4,5,6],"c":[7,8,9],"a":[1,2,3]}}

As you can see, it simply looks at every leaf value (string, int) and derives the key path and stores the value there. For example,

{ x: { y: { z: 1 } } }

becomes,

{ x.y.z: 1 }

Arrays are a special case. Each value in an array becomes the path so far to the array appended with the array index. For example,

{ anArray: [1,2,3] }

becomes,

{ anArray.0: 1, anArray.1: 2, anArray.2: 3 }
Of course this isn’t foolproof. If you have dots in your key names, it will break. If you use numbers as keys, it will break as it makes an assumption that a numbered key path element indicates an array. Here’s the source.
package org.test.flatjson;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class JsonFlattener {
	public static String encode(JSONObject jo) throws JSONException {
		String s = "{" + encode(null, jo) + "}";
		return s;

	}

	public static String encode(String json) throws JSONException {
		JSONObject jo = new JSONObject(json);
		return encode(jo);
	}

	private static String encode(String parent, Object val)
			throws JSONException {
		StringBuilder sb = new StringBuilder();
		if (val instanceof JSONObject) {
			JSONObject jo = (JSONObject) val;
			for (Iterator<String> i = jo.keys(); i.hasNext();) {
				String key = i.next();
				String hkey = (parent == null) ? key : parent + "." + key;
				Object jval = jo.get(key);
				String json = encode(hkey, jval);
				sb.append(json);
				if (i.hasNext()) {
					sb.append(",");
				}
			}
		} else if (val instanceof JSONArray) {
			JSONArray ja = (JSONArray) val;
			for (int i = 0; i < ja.length(); i++) {
				String hkey = (parent == null) ? "" + i : parent + "." + i;
				Object aval = ja.get(i);
				String json = encode(hkey, aval);
				sb.append(json);
				if (i < ja.length() - 1) {
					sb.append(",");
				}
			}
		} else if (val instanceof String) {
			sb.append("\"").append(parent).append("\"").append(":");
			String s = (String) val;
			sb.append(JSONObject.quote(s));
		} else if (val instanceof Integer) {
			sb.append("\"").append(parent).append("\"").append(":");
			Integer integer = (Integer) val;
			sb.append(integer);
		}

		return sb.toString();
	}

	public static String decode(String flatJson) throws JSONException {
		JSONObject encoded = new JSONObject(flatJson);
		return decodeToString(encoded);
	}

	public static String decodeToString(JSONObject encoded)
			throws JSONException {
		return decodeToObject(encoded).toString();
	}

	public static JSONObject decodeToObject(JSONObject encoded)
			throws JSONException {
		JSONObject decoded = new JSONObject();

		for (Iterator<String> i = encoded.keys(); i.hasNext();) {
			String hkey = i.next();
			String[] keys = hkey.split("\\.");

			Object json = decoded;

			for (int j = 0; j < keys.length; j++) {
				if (j == keys.length - 1) {
					Object val = encoded.get(hkey);
					if (json instanceof JSONObject) {
						JSONObject jo = (JSONObject)json;
						jo.put(keys[j], val);
					} else if (json instanceof JSONArray) {
						JSONArray ja = (JSONArray)json;
						int index = Integer.parseInt(keys[j]);
						ja.put(index, val);
					}
				} else {
					// we're NOT at a leaf key

					if (!isNumber(keys[j + 1])) {
						// next index is an object

						JSONObject joChild;

						if (json instanceof JSONObject) {
							// last index was an object
							// we're creating an object in an object
							JSONObject jo = (JSONObject)json;
							if (jo.has(keys[j])) {
								joChild = jo.getJSONObject(keys[j]);
							} else {
								joChild = new JSONObject();
								jo.put(keys[j], joChild);
							}
						} else if (json instanceof JSONArray) {
							// last index was an array
							// we're creating an object in an array
							JSONArray ja = (JSONArray)json;
							int index = Integer.parseInt(keys[j]);
							if (!ja.isNull(index)) {
								joChild = ja.getJSONObject(index);
							} else {
								joChild = new JSONObject();
								ja.put(index, joChild);
							}
						} else {
							throw new AssertionError("unhandled object type");
						}
						json = joChild;
					} else {
						// next index is an array element

						JSONArray jaChild;

						if (json instanceof JSONObject) {
							// last index was an object,
							// we're creating an array in an object
							JSONObject jo = (JSONObject)json;
							if (jo.has(keys[j])) {
								jaChild = jo.getJSONArray(keys[j]);
							} else {
								jaChild = new JSONArray();
								jo.put(keys[j], jaChild);
							}
						} else if (json instanceof JSONArray) {
							// last index was an array
							// we're creating an array in an array
							JSONArray ja = (JSONArray)json;
							int index = Integer.parseInt(keys[j + 1]);
							if (!ja.isNull(index)) {
								jaChild = ja.getJSONArray(index);
							} else {
								jaChild = new JSONArray();
								ja.put(index, jaChild);
							}
						} else {
							throw new AssertionError("unhandled object type");
						}
						json = jaChild;
					}
				}
			}
		}
		return decoded;
	}

	private static boolean isNumber(String s) {
		try {
			Integer.parseInt(s);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}

	}
}
Advertisements

Android Bitmap Scaling

2011/01/27

Scaling images is a common task for many applications including those written for Android. The most obvious approach, as described on this page, won’t work in most cases. The problem as you would soon find out if you tried it is that your phone doesn’t have enough memory. Luckily we can get around that, but not easily.

The trick ends up being a two step process. First we decode the bitmap, setting the sampling level as high as we can while avoiding loss of quality, then we scale this smaller bitmap in memory down to the exact specified size. I’ve included this in a nicely packaged class BitmapScaler, included at the end of this post.The class itself looks quite long but this is only because it has the flexibility of scaling bitmaps from resources, assets, and files.

BitmapScaler preserves the aspect ratio. You may only pass a new width. The resulting height will be scaled equal to the amount the width was scaled.

For example,

BitmapScaler scaler = new BitmapScaler(getResources(), R.drawable.moorwen, newWidth);
imageView.setImageBitmap(scaler.getScaled());

BitmapScaler class follows,

class BitmapScaler {
	private static class Size {
		int sample;
		float scale;
	}

	private Bitmap scaled;

	BitmapScaler(Resources resources, int resId, int newWidth)
			throws IOException {
		Size size = getRoughSize(resources, resId, newWidth);
		roughScaleImage(resources, resId, size);
		scaleImage(newWidth);
	}

	BitmapScaler(File file, int newWidth) throws IOException {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			Size size = getRoughSize(is, newWidth);
			try {
				is = new FileInputStream(file);
				roughScaleImage(is, size);
				scaleImage(newWidth);
			} finally {
				is.close();
			}
		} finally {
			is.close();
		}
	}

	BitmapScaler(AssetManager manager, String assetName, int newWidth)
			throws IOException {
		InputStream is = null;
		try {
			is = manager.open(assetName);
			Size size = getRoughSize(is, newWidth);
			try {
				is = manager.open(assetName);
				roughScaleImage(is, size);
				scaleImage(newWidth);
			} finally {
				is.close();
			}
		} finally {
			is.close();
		}
	}

	Bitmap getScaled() {
		return scaled;
	}

	private void scaleImage(int newWidth) {
		int width = scaled.getWidth();
		int height = scaled.getHeight();

		float scaleWidth = ((float) newWidth) / width;
		float ratio = ((float) scaled.getWidth()) / newWidth;
		int newHeight = (int) (height / ratio);
		float scaleHeight = ((float) newHeight) / height;

		Matrix matrix = new Matrix();
		matrix.postScale(scaleWidth, scaleHeight);

		scaled = Bitmap.createBitmap(scaled, 0, 0, width, height, matrix, true);
	}

	private void roughScaleImage(InputStream is, Size size) {
		Matrix matrix = new Matrix();
		matrix.postScale(size.scale, size.scale);

		BitmapFactory.Options scaledOpts = new BitmapFactory.Options();
		scaledOpts.inSampleSize = size.sample;
		scaled = BitmapFactory.decodeStream(is, null, scaledOpts);
	}

	private void roughScaleImage(Resources resources, int resId, Size size) {
		Matrix matrix = new Matrix();
		matrix.postScale(size.scale, size.scale);

		BitmapFactory.Options scaledOpts = new BitmapFactory.Options();
		scaledOpts.inSampleSize = size.sample;
		scaled = BitmapFactory.decodeResource(resources, resId, scaledOpts);
	}

	private Size getRoughSize(InputStream is, int newWidth) {
		BitmapFactory.Options o = new BitmapFactory.Options();
		o.inJustDecodeBounds = true;
		BitmapFactory.decodeStream(is, null, o);

		Size size = getRoughSize(o.outWidth, o.outHeight, newWidth);
		return size;
	}

	private Size getRoughSize(Resources resources, int resId, int newWidth) {
		BitmapFactory.Options o = new BitmapFactory.Options();
		o.inJustDecodeBounds = true;
		BitmapFactory.decodeResource(resources, resId, o);

		Size size = getRoughSize(o.outWidth, o.outHeight, newWidth);
		return size;
	}

	private Size getRoughSize(int outWidth, int outHeight, int newWidth) {
		Size size = new Size();
		size.scale = outWidth / newWidth;
		size.sample = 1;

		int width = outWidth;
		int height = outHeight;

		int newHeight = (int) (outHeight / size.scale);

		while (true) {
			if (width / 2 < newWidth || height / 2 < newHeight) {
				break;
			}
			width /= 2;
			height /= 2;
			size.sample *= 2;
		}
		return size;
	}
}

Or, try out the Eclipse test project.