static public enum Handler { JSON, GEOJSON, RECORD, SIMPLEGEO }
 public class GeoJSONEncoder {

	private static Logger logger = Logger.getLogger(GeoJSONEncoder.class.getName());

	/**
	 * Converts a {@link org.json.JSONObject} into a 
	 * {@link com.simplegeo.client.model.DefaultRecord}. If
	 * a parse exception is thrown, the return value will be null.
	 * 
	 * @param jsonObject the {@link org.json.JSONObject} to convert
	 * @return a newly constructed {@link com.simplegeo.client.model.DefaultRecord}
	 */
	public static DefaultRecord getRecord(JSONObject jsonObject) {
		
		DefaultRecord defaultRecord = null;
		try {

			if (jsonObject == null)
				return null;

			GeoJSONObject geoJSONObject = new GeoJSONObject("Feature",
					jsonObject.toString());

			JSONObject properties = new JSONObject(geoJSONObject
					.getProperties().toString());
			defaultRecord = new DefaultRecord(geoJSONObject.optString("id"),
					geoJSONObject.optString("layer"), geoJSONObject
							.getProperties().optString("type"), geoJSONObject
							.getLongitude(), geoJSONObject.getLatitude());

			defaultRecord.setProperties(properties);

			// Don't create an exception as these are optional keys
			defaultRecord.setCreated(geoJSONObject.optLong("created"));
			defaultRecord.setExpiration(geoJSONObject.optLong("expires"));

		} catch (JSONException e) {

			logger.info(String.format("%s", e.getLocalizedMessage()));

		}

		return defaultRecord;
	}

	/**
	 * Converts a {@link com.simplegeo.client.geojson.GeoJSONObject} into an Array of 
	 * {@link com.simplegeo.client.model.DefaultRecord}. If
	 * a parse exception is thrown, the return value will be null. An array 
	 * is returned instead of a single object due to the fact that a single 
	 * GeoJSON object has the capacity to maintain a collection. 
	 * 
	 * @param geoJSONObject the {@link com.simplegeo.client.geojson.GeoJSONObject} to convert
	 * @return a newly constructed list of {@link com.simplegeo.client.model.DefaultRecord}
	 */
	public static List<DefaultRecord> getRecords(GeoJSONObject geoJSONObject) {

		List<DefaultRecord> defaultRecords = null;

		try {

			if (geoJSONObject.isFeatureCollection()) {

				defaultRecords = new ArrayList<DefaultRecord>();
				JSONArray features = geoJSONObject.getFeatures();
				int size = features.length();
				for (int index = 0; index < size; index++) {

					JSONObject feature = features.getJSONObject(index);
					DefaultRecord record = GeoJSONEncoder.getRecord(feature);
					if (record != null)
						defaultRecords.add(record);

				}

			} else if (geoJSONObject.getType().equals("Feature")) {

				defaultRecords = new ArrayList<DefaultRecord>();
				DefaultRecord record = GeoJSONEncoder.getRecord(geoJSONObject);
				if (record != null)
					defaultRecords.add(record);
			}

		} catch (JSONException e) {

			logger.info(String.format("%s for %s", e.getLocalizedMessage(), geoJSONObject.toString()));
		}

		return defaultRecords;
	}

	/**
	 * Converts a {@link com.simplegeo.client.model.IRecord} into a 
	 * {@link com.simplegeo.client.geojson.GeoJSONObject}. If
	 * a parse exception is thrown, the return value will be null.
	 * 
	 * @param record the {@link com.simplegeo.client.model.IRecord} to convert
	 * @return a newly constructed {@link com.simplegeo.client.geojson.GeoJSONObject}
	 */
	public static GeoJSONRecord getGeoJSONRecord(IRecord record) {

		if (record == null)
			return null;

		GeoJSONRecord geoJSONRecord = null;
		try {
			
			geoJSONRecord = new GeoJSONRecord(record.getRecordId(), record.getLayer(), record.getObjectType());

			JSONObject properties = record.getProperties();
			if (properties != null)
				properties = new JSONObject(properties.toString());
			else
				properties = new JSONObject();
			
			JSONObject gjrp = geoJSONRecord.getProperties();
			
			@SuppressWarnings("unchecked")
			Iterator<String> keys = (Iterator<String>)(gjrp.keys());
			while (keys.hasNext())
			{
				String s = keys.next();
				properties.put(s, gjrp.get(s));
			}
			
			geoJSONRecord.setLatitude(record.getLatitude());
			geoJSONRecord.setLongitude(record.getLongitude());

			geoJSONRecord.setProperties(properties);

			geoJSONRecord.setCreated(record.getCreated());
			geoJSONRecord.setExpiration(record.getExpiration());

		} catch (JSONException e) {

			logger.info(String.format("%s for %s", e.getLocalizedMessage(), record.toString()));

		}

		return geoJSONRecord;
	}

	/**
	 * @see GeoJSONEncoder#getGeoJSONRecord(com.simplegeo.client.model.IRecord)
	 */
	public static GeoJSONObject getGeoJSONRecord(List<IRecord> records) {

		if (records == null || records.isEmpty())
			return null;

		GeoJSONObject geoJSONObject = null;
		try {

			geoJSONObject = new GeoJSONObject("FeatureCollection");

			JSONArray features = new JSONArray();
			for (IRecord record : records) {

				GeoJSONObject jsonObject = GeoJSONEncoder
						.getGeoJSONRecord(record);

				if (jsonObject != null)
					features.put(jsonObject);
			}

			geoJSONObject.setFeatures(features);

		} catch (JSONException e) {

			logger.info(e.getLocalizedMessage());
		}

		return geoJSONObject;
	}

} public class GeoJSONObject extends JSONObject {
	
	private static Logger logger = Logger.getLogger(GeoJSONObject.class.getName());
	
	/**
	 * Initializes the GeoJSON object with the given JSON string
	 * representation.
	 * 
	 * @param type the type of GeoJSON object (ignored for now) 
	 * @param jsonString a string representation of a GeoJSON object
	 * @throws JSONException
	 */
	public GeoJSONObject(String type, String jsonString) throws JSONException {
		// Ignoring the type for the moment
		super(jsonString);		
	}
	
	
	/**
	 * @param type the type of GeoJSON object to initialize
	 * @see <a href="http://geojson.org/geojson-spec.html#feature-objects"</a>
	 */
	public GeoJSONObject(String type) {
		super();
		setupStructure(type);
	}
	
	/**
	 * Initializes a new object of type Feature.
	 */
	public GeoJSONObject() {
		super();
		setupStructure("Feature");
	}
	
	private void setupStructure(String type) {
		
		try {
			
			this.put("type", type);
			
			if(type.equals("Feature")) {
				
				JSONObject geometry = new JSONObject();
				geometry.putOpt("type", "Point");
				setGeometry(geometry);
				JSONArray coords = new JSONArray("[0.0,0.0]");
				setCoordinates(coords);
				setProperties(new JSONObject());
				
			} else if(type.equals("FeatureCollection")) {
				
				setFeatures(new JSONArray());
				
			}
			
		} catch (JSONException e) {
			
			logger.info("unable to initialize properly");
		}

	}
	
	/**
	 * @return true if the object is of type Feature; otherwise false
	 */
	public boolean isFeature() {
		return isType("Feature");
	}
	
	/**
	 * @return true if the object is of type FeatureCollection; otherwise false
	 */
	public boolean isFeatureCollection() {
		return isType("FeatureCollection");
	}
	
	/**
	 * @return true if the object is of type GeometryCollection; otherwise false
	 */
	public boolean isGeometryCollection() {
		return isType("GeometryCollection");
	}
	
	/**
	 * @return true if the object is of type MultiPolygon; otherwise false
	 */
	public boolean isMultiPolygon() {
		return isType("MultiPolygon");
	}

	/**
	 * @return true if the object is of type Polygon; otherwise false
	 */
	public boolean isPolygon() {
		return isType("Polygon");
	}

	private boolean isType(String type) {
		boolean isType = false;
		try {
			String objectType = this.getType();
			isType = objectType.equals(type);
		} catch (JSONException e) {
			e.printStackTrace();
		}
		
		return isType;		
	}
	
	/**
	 * @return the latitude of the record
	 * @throws JSONException
	 */
	public double getLatitude() throws JSONException {
		return this.getCoordinates().getDouble(1);
	}
	
	/**
	 * @param latitude the latitude of the record
	 * @throws JSONException
	 */
	public void setLatitude(double latitude) throws JSONException {
		this.getCoordinates().put(1, latitude);
	}
	
	/**
	 * @return the longitude of the record
	 * @throws JSONException
	 */
	public double getLongitude() throws JSONException {
		return this.getCoordinates().getDouble(0);
	}
	
	/**
	 * @param longitude the longitude of the record
	 * @throws JSONException
	 */
	public void setLongitude(double longitude) throws JSONException {
		this.getCoordinates().put(0, longitude);
	}
	
	/**
	 * @return @see <a href="http://geojson.org/geojson-spec.html#point"</a>
	 * @throws JSONException
	 */
	public JSONArray getCoordinates() throws JSONException {
		return this.getGeometry().getJSONArray("coordinates");
	}
	
	/**
	 * @param coordinates @see <a href="http://geojson.org/geojson-spec.html#point"</a>
	 * @throws JSONException
	 */
	public void setCoordinates(JSONArray coordinates) throws JSONException {
		this.getGeometry().put("coordinates", coordinates);
	}
	
	/**
	 * @return @see <a href="http://geojson.org/geojson-spec.html#geometry-objects"</a>
	 * @throws JSONException
	 */
	public JSONObject getGeometry() throws JSONException {
		return this.getJSONObject("geometry");
	}
	
	/**
	 * @param geometry
	 * @throws JSONException
	 * @see <a href="http://geojson.org/geojson-spec.html#geometry-objects"</a>
	 */
	public void setGeometry(JSONObject geometry) throws JSONException {
		this.put("geometry", geometry);
	}

	/**
	 * @return @see <a href="http://geojson.org/geojson-spec.html#geometry-collection"</a>
	 * @throws JSONException
	 */
	public JSONArray getGeometries() throws JSONException {
		return this.getJSONArray("geometries");
	}

	/**
	 * @param geometries
	 * @throws JSONException
	 * @see <a href="http://geojson.org/geojson-spec.html#geometry-collection"</a>
	 */
	public void setGeometries(JSONArray geometries) throws JSONException {
		this.put("geometries", geometries);
	}
	
	/**
	 * @return @see <a href="http://geojson.org/geojson-spec.html#geometry-objects"</a> 
	 * @throws JSONException
	 */
	public String getGeometryType() throws JSONException {
		return this.getGeometry().getString("type");
	}
	
	/**
	 * Only type "Point" is currently supported.
	 * 
	 * @see <a href="http://geojson.org/geojson-spec.html#point"</a>
	 * 
	 * @param type the type of geometry that is associated with the record
	 * @throws JSONException
	 */
	public void setGeometryType(String type) throws JSONException {
		this.getGeometry().put("type", type);
	}
	
	/**
	 * @return @see <a href="http://geojson.org/geojson-spec.html#feature-objects"</a>
	 * @throws JSONException
	 */
	public String getType() throws JSONException {
		return this.getString("type");
	}
	
	/**
	 * @param type either the value "Feature" or "FeatureCollection"
	 * @throws JSONException
	 * @see <a href="http://geojson.org/geojson-spec.html#feature-objects"</a>
	 */
	public void setType(String type) throws JSONException {
		this.put("type", type);
	}
	
	/**
	 * @return @see <a href="http://geojson.org/geojson-spec.html#feature-objects"</a> 
	 * @throws JSONException
	 */
	public JSONObject getProperties() throws JSONException {
		return this.getJSONObject("properties");
	}
	
	/**
	 * @param properties the JSONArray to set at key "properties"
	 * @throws JSONException
	 * @see <a href="http://geojson.org/geojson-spec.html#feature-objects"</a>
	 */
	public void setProperties(JSONObject properties) throws JSONException {
		this.put("properties", properties);
	}
	
	/**
	 * @param features the {@link org.json.JSONArray} to set at key "features"
	 * @throws JSONException
	 * @see <a href="http://geojson.org/geojson-spec.html#feature-collection-objects"</a>
	 */
	public void setFeatures(JSONArray features) throws JSONException {
		if(isFeatureCollection())
			this.putOpt("features", features);
		else
			throw new JSONException("GeoJSONObject is not of type FeatureCollection");
	}
	
	/**
	 * @return if the GeoJSON object is of type FeatureCollection, then this will
	 * be a {@link org.json.JSONArray}
	 * @throws JSONException
	 * @see <a href="http://geojson.org/geojson-spec.html#feature-collection-objects"</a>
	 */
	public JSONArray getFeatures() throws JSONException {
		if(isFeatureCollection())
			return (JSONArray)this.get("features");
		
		throw new JSONException("GeoJSONObject is not of type FeatureCollection");
	}
	
	/**
	 * Attempts to flatten the GeoJSON object if it is a FeatureCollection
	 * of FeatureCollections.
	 * 
	 * @throws JSONException
	 */
	public void flatten() throws JSONException {
		
		if(isFeatureCollection()) {
			
			List<GeoJSONObject> newFeatures = new ArrayList<GeoJSONObject>();
			JSONArray features = getFeatures();
			int length = features.length();
			for(int index = 0; index < length; index++) {
				
				GeoJSONObject geoJSONObject = (GeoJSONObject)features.get(index);
				if(geoJSONObject.isFeatureCollection()) {
					JSONArray subfeatures = geoJSONObject.getFeatures();
					int sublength = subfeatures.length();
					for(int subindex = 0; subindex < sublength; subindex++) {
						GeoJSONObject subGeoJSONObject = (GeoJSONObject)subfeatures.get(subindex);
						subGeoJSONObject.flatten();
						newFeatures.add(subGeoJSONObject);
					}
					
				}
			}
			
			if(!newFeatures.isEmpty()) {
				for(GeoJSONObject object : newFeatures)
					features.put(object);
			}
		}
		
	}
	
	public JSONArray geoJSONArray(JSONArray jsonArray) throws JSONException {
		
		JSONArray geoJSONArray = new JSONArray();
		int length = jsonArray.length();
		for(int index = 0; index < length; index++) {
			jsonArray.put(new GeoJSONObject("Feature", jsonArray.get(index).toString()));
		}
		
		return geoJSONArray;
	}
} public class GeoJSONHandler implements SimpleGeoJSONHandlerIfc {
	
	private static Logger logger = Logger.getLogger(GeoJSONHandler.class.getName());

	private String getLayerForGeoJSONRecord(JSONObject record) {
		
		String layer = "";
		JSONObject layerDict = (JSONObject)record.opt("layerLink");
		if(layerDict != null) {
			
			
			// THIS IS SOOOO BAD
			String hRef = (String)layerDict.opt("href");
			String[] substrings = hRef.split("/");
			int length = substrings.length;
			if(substrings.length > 0) {
				layer = substrings[length - 1];
				layer = layer.replaceAll(".json", "");
			}
			
		}
		
		return layer;
		
	}

	
	public Object parseResponse(String response) {
		GeoJSONObject topObject = null;
        try {
        	
        	// We need to do a little massaging of the data here
        	String jsonString = response;
        	if(jsonString != null && jsonString.length() > 1) {
        		
        		topObject = new GeoJSONRecord("Feature", jsonString);
        		
        		String geoJSONType = topObject.getType();
        		if(geoJSONType.equals("Feature")) {
        			
        			topObject.put("layer", getLayerForGeoJSONRecord(topObject));
        			
        		} else if(geoJSONType.equals("FeatureCollection")) {
        			
        			JSONArray features = topObject.getFeatures();
        			int length = features.length();
        			for(int index = 0; index < length; index++) {
        				
        				JSONObject record = (JSONObject)features.get(index);
        				record.put("layer", getLayerForGeoJSONRecord(record));
        		
        			}
        		}
        	}
        	
        } catch (JSONException e) {
			
			logger.info(e.getMessage());
		}
        
		return topObject;
	}
} public class JSONHandler implements SimpleGeoJSONHandlerIfc {

	private static Logger logger = Logger.getLogger(GeoJSONHandler.class.getName());

	
	public Object parseResponse(String response) {
		JSONArray topObject = null;
        try {

        	String jsonString = response;
        	if(jsonString != null && jsonString.length() > 1)
        		topObject = new JSONArray(jsonString);
        	
        } catch (JSONException e) {
			
			logger.info(e.getMessage());
		}
        
		return topObject;
	}

} public class RecordHandler extends GeoJSONHandler {

	public Object parseResponse(String response)
	{
		
		GeoJSONObject geoJSONObject = (GeoJSONObject)super.parseResponse(response);
		
		List<DefaultRecord> defaultRecords = null;
		
		if(geoJSONObject != null)
			defaultRecords = GeoJSONEncoder.getRecords(geoJSONObject);
		
		return defaultRecords;
	}

} public interface SimpleGeoJSONHandlerIfc {
	
	/**
	 * @param response
	 * @return the object contained in the response
	 */
	public Object parseResponse (String response);

}
 public class SimpleGeoHandler implements ResponseHandler<Object> {
	
	private static Logger logger = Logger.getLogger(SimpleGeoHandler.class.getName());
	
	private SimpleGeoJSONHandlerIfc handler;
	
	/* Status codes */
	public static final int GET_SUCCESS = 200;
	public static final int PUT_SUCCESS = 202;
	public static final int BAD_REQUEST = 400;
	public static final int NO_SUCH = 404;
	public static final int NOT_AUTHORIZED = 401;
	
	public SimpleGeoHandler (SimpleGeoJSONHandlerIfc handler)
	{
		super();
		this.handler = handler;
	}
	
	/* (non-Javadoc)
	 * @see org.apache.http.client.ResponseHandler#handleResponse(org.apache.http.HttpResponse)
	 */
	public Object handleResponse(HttpResponse response)
			throws ClientProtocolException, IOException {

		logger.info("recieved response " + response);

		StatusLine statusLine = response.getStatusLine();
		int statusCode = statusLine.getStatusCode();
		
		HttpEntity entity = response.getEntity();

		@SuppressWarnings("unused")
		HttpResponse validResponse = null;
		switch(statusCode) {
		
			case GET_SUCCESS:
			case PUT_SUCCESS:
				validResponse = response; 
				break;
			case BAD_REQUEST:
				throw APIException.createException(entity, statusLine);
			case NO_SUCH:
				throw NoSuchRecordException.createException(entity, statusLine);
			case NOT_AUTHORIZED:
				throw NotAuthorizedException.createException(entity, statusLine);
			default:
				throw APIException.createException(entity, statusLine);
		
		}
		
		//
		// Extract the string
		// 
		String jsonString = null;
		jsonString = EntityUtils.toString(response.getEntity());	
		
		return handler.parseResponse(jsonString);
	}
} @SuppressWarnings("serial")
public class APIException extends IOException { 

	private static Logger logger = Logger.getLogger(APIException.class.getName());
	
	/**
	 * The Http status code that generated the exception.
	 */
	public int statusCode;
	
	/**
	 * A static factory method that creates proper API exceptions from 
	 * a {@link org.apache.http.HttpEntity} and {@link org.apache.http.StatusLine}.
	 * The message is built using a combination of both the status code and the payload.
	 * 
	 * @param entity the entity retrieved from a Http response
	 * @param statusLine the {@link org.apache.http.StatusLine} that was retrieved
	 * from a Http response
	 * @return a new APIException object
	 */
	public static APIException createException(HttpEntity entity, StatusLine statusLine) {
		
		int statusCode = statusLine.getStatusCode();
		String reason = null;
		
		try {
			
			InputStream inputStream = entity.getContent();
			DataInputStream dis = new DataInputStream(inputStream);
			reason = dis.readUTF();
			
		} catch (EOFException e) {
			
			;
			
		} catch (IllegalStateException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		if(reason == null)
			reason = statusLine.getReasonPhrase();
	
		logger.info(String.format("(status %d) %s", statusCode, reason));
		
		return new APIException(statusCode, reason);
	}
		
	/**
	 * Creates an exception with the given status code and message. 
	 * 
	 * @param statusCode the Http status code that generated the exception
	 * @param reason the reason why the this exception is being created
	 */
	public APIException(int statusCode, String reason) {
		
		super(reason);
		this.statusCode = statusCode;
		
		logger.info(String.format("(status %d) %s", statusCode, reason));
	}

} @SuppressWarnings("serial")
public class UnsupportedHandlerException extends APIException {

	public UnsupportedHandlerException(int statusCode, String reason) {
		super(statusCode, reason);
	}

} public class DefaultRecord implements IRecord {
	
	private String recordId;
	private String layer;
	private String type;
	private long created;
	private long expiration;
	private double latitude;
	private double longitude;
	private JSONObject properties;

	/**
	 * Creates a new record.
	 * 
	 * @param recordId the id associated with the record
	 * @param layer the layer where the record is held
	 * @param type the type associated with the record 
	 * @param longitude
	 * @param latitude
	 * @see com.simplegeo.client.model.RecordType
	 */
	public DefaultRecord(String recordId, String layer, String type, double longitude, double latitude) {
		
		this.recordId = recordId;
		this.layer = layer;
		this.type = type;
		this.longitude = longitude;
		this.latitude = latitude;
		setProperties(new JSONObject());
	
		this.created = (long)(System.currentTimeMillis() / 1000);
		this.expiration = 0;
	}

	/**
	 * @param recordId the id associated with the record
	 * @param layer the layer where the record is held
	 * @param type the type associated with the record
	 * @see com.simplegeo.client.model.RecordType
	 */
	public DefaultRecord(String recordId, String layer, String type) {
		this(recordId, layer, type, 0.0, 0.0);
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getLayer()
	 */
	public String getLayer() {
		return layer;
	}

	/**
	 * @param layer the layer where the record is held
	 */
	public void setLayer(String layer) {
		this.layer = layer;
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getLatitude()
	 */
	public double getLatitude() {
		return latitude;
	}

	/**
	 * @param latitude
	 */
	public void setLatitude(double latitude) {
		this.latitude = latitude;
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getLongitude()
	 */
	public double getLongitude() {
		return longitude;
	}
	
	/**
	 * @param longitude
	 */
	public void setLongitude(double longitude) {
		this.longitude = longitude;
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getRecordId()
	 */
	public String getRecordId() {
		return recordId;
	}
	
	/**
	 * @param recordId the id associated with the record
	 */
	public void setRecordId(String recordId) {
		this.recordId = recordId;
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getObjectType()
	 */
	public String getObjectType() {
		return type;
	}
	
	/**
	 * @param type the type associated with the record
	 */
	public void setObjectType(String type) {
		this.type = type;
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getCreated()
	 */
	public long getCreated() {
		return created;
	}
	
	/**
	 * @param created the time at which this record was created in milliseconds
	 */
	public void setCreated(long created) {
		this.created = created;
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getExpiration()
	 */
	public long getExpiration() {
		return expiration;
	}
	
	/**
	 * @param expiration the time at which this record will expire in milliseconds
	 */
	public void setExpiration(long expiration) {
		this.expiration = expiration;
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getProperties()
	 */
	public JSONObject getProperties() {
		return this.properties;
	}
	
	/**
	 * @param properties other values associated with this record
	 */
	public void setProperties(JSONObject properties) {
		this.properties = properties;
	}
	
	/**
	 * @param key the key to look-up in the {@link org.json.JSONObject} properties 
	 * @return the integer value
	 */
	public int getIntProperty(String key) {
		int value = 0;
		try {
			value = getProperties().getInt(key);
		} catch (JSONException e) {
			;
		}
		
		return value;
	}
	
	/**
	 * @param key the key to use
	 * @param value the integer value that will be assigned
	 * to the key in the properties {@link org.json.JSONObject}.
	 */
	public void setIntProperty(String key, int value) {
		try {
			getProperties().put(key, value);
		} catch (JSONException e) {
			
		}
	}
	
	/**
	 * @param key the key to look-up in the {@link org.json.JSONObject} properties 
	 * @return the double value
	 */
	public double getDoubleProperty(String key) {
		double value = 0.0;
		try {
			value = getProperties().getDouble(key);
		} catch (JSONException e) {
			;
		}	
		
		return value;
	}
	
	/**
	 * @param key the key to use
	 * @param value the double value that will be assigned
	 * to the key in the properties {@link org.json.JSONObject}.
	 */
	public void setDoubleProperty(String key, double value) {
		try {
			getProperties().put(key, value);
		} catch (JSONException e) {
			
		}	
	}
	
	/**
	 * @param key the key to look-up in the {@link org.json.JSONObject} properties 
	 * @return the long value
	 */
	public long getLongProperty(String key) {
		long value = 0;
		try {
			value = getProperties().getLong(key);
		} catch (JSONException e) {
			;
		}	
		
		return value;
	}
	
	/**
	 * @param key the key to use
	 * @param value the long value that will be assigned
	 * to the key in the properties {@link org.json.JSONObject}.
	 */
	public void setLongProperty(String key, long value) {
		try {
			getProperties().put(key, value);
		} catch (JSONException e) {
			
		}
	}

	/**
	 * @param key the key to look-up in the {@link org.json.JSONObject} properties 
	 * @return the Object value
	 */
	public Object getObjectProperty(String key) {
		Object value = null;
		try {
			value = getProperties().get(key);
		} catch (JSONException e) {
			;
		}	
		
		return value;
	}
	
	/**
	 * @param key the key to use
	 * @param value the Object value that will be assigned
	 * to the key in the properties {@link org.json.JSONObject}.
	 */
	public void setObjectProperty(String key, Object value) {
		if(value == null)
			value = JSONObject.NULL;
		try {
			getProperties().put(key, value);
		} catch (JSONException e) {
			
		}
	}
	
	public Point getOrigin() {
		return new Point(this.latitude, this.longitude);
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		
		return String.format("<DefaultRecord id=%s, layer=%s, lat=%f, lon=%f, type=%s, created=%d>", 
				getRecordId(), getLayer(), getLatitude(), getLongitude(), getObjectType(), getCreated());
	}
	
} public class Envelope {
	
	private double south;
	private double west;
	private double north;
	private double east;
	
	public static Envelope fromJSON(JSONArray jsonArray) throws JSONException {
		Envelope envelope = null;
		if(jsonArray.length() == 4) {
			envelope = new Envelope(jsonArray.getDouble(0),
					jsonArray.getDouble(1),
					jsonArray.getDouble(2),
					jsonArray.getDouble(3)
					);
		}
		
		return envelope;
	}

	/**
	 * Creates a new Envelope object with the given latitude and longitude
	 * lines.
	 * 
	 * @param south the south latitude line of the bounding box
	 * @param west the west longitude line of the bounding box
	 * @param north the north latitude line of the bounding box
	 * @param east the east longitude line of the bounding box
	 */
	public Envelope(double south, double west, double north, double east) {
		
		this.south = south;
		this.west = west;
		this.north = north;
		this.east = east;
	}
	
	/**
	 * @return the south
	 */
	public double getSouth() {
		return south;
	}

	/**
	 * @param south the south to set
	 */
	public void setSouth(double south) {
		this.south = south;
	}

	/**
	 * @return the west
	 */
	public double getWest() {
		return west;
	}

	/**
	 * @param west the west to set
	 */
	public void setWest(double west) {
		this.west = west;
	}

	/**
	 * @return the north
	 */
	public double getNorth() {
		return north;
	}

	/**
	 * @param north the north to set
	 */
	public void setNorth(double north) {
		this.north = north;
	}

	/**
	 * @return the east
	 */
	public double getEast() {
		return east;
	}

	/**
	 * @param east the east to set
	 */
	public void setEast(double east) {
		this.east = east;
	}

	public String toString() {
		
		return String.format("%f,%f,%f,%f", south, west, north, east);
		
	}
} public class GeoJSONRecord extends GeoJSONObject implements IRecord {
	
	private static Logger logger = Logger.getLogger(GeoJSONRecord.class.getName());
	
	/**
	 * @param recordId
	 * @param layer
	 * @param type
	 * @param created
	 * @param expiration
	 */
	public GeoJSONRecord(String recordId, String layer, String type, long created, long expiration) {
		super();
		setProperties(new JSONObject());
		
		setRecordId(recordId);
		setLayer(layer);
		setObjectType(type);
		setCreated(created);
		setExpiration(expiration);
		setLatitude(0.0);
		setLongitude(0.0);
		
		try {
			
			setType("Feature");
			
		} catch (JSONException e) {
			
			logger.info("unable to locate type key");
			
		}
	}

	/**
	 * @param recordId
	 * @param layer
	 * @param type
	 */
	public GeoJSONRecord(String recordId, String layer, String type) {
		this(recordId, layer, type, System.currentTimeMillis() / 1000, 0);
	}
		
	public GeoJSONRecord(String type, String jsonString) throws JSONException {
		super(type, jsonString);
	}

	public GeoJSONRecord(String string) {
		super(string);
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getCreated()
	 */
	public long getCreated() {
		long created = 0;		
		try {
			created = super.getLong("created");
		} catch (JSONException e) {			
			logger.info("unable to locate created");
		}
		
		return created;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setCreated(long)
	 */
	public void setCreated(long created) {
		try {
			super.put("created", created);
		} catch (JSONException e) {			
			logger.info("unable to locate properties");
		}
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getExpiration()
	 */
	public long getExpiration() {
		long expires = 0;		
		try {
			expires = super.getLong("expiration");
		} catch (JSONException e) {			
			logger.info("unable to locate expiration");
		}
		
		return expires;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setExpiration(long)
	 */
	public void setExpiration(long expiration) {

		try {
			super.put("expiration", expiration);
		} catch (JSONException e) {			
			logger.info("unable to locate properties");
		}
		
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getLatitude()
	 */
	public double getLatitude() {
		double latitude = -1.0;		
		try {
			latitude = super.getLatitude();
		} catch (JSONException e) {			
			logger.info("unable to locate latitude");
		}
		
		return latitude;
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.GeoJSONObject#setLatitude(double)
	 */
	public void setLatitude(double lat) {
		try {
			super.setLatitude(lat);
		} catch (JSONException e) {
			logger.info("unable to locate latitude");
		}
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getLayer()
	 */
	public String getLayer() {
		String layer = null;		
		try {
			layer = super.getString("layer");
		} catch (JSONException e) {			
			logger.info("unable to locate layer");
		}
		
		return layer;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setLayer(String)
	 */
	public void setLayer(String layer) {
		try {
			super.put("layer", layer);
		} catch (JSONException e) {			
			logger.info("unable to locate properties");
		}		
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getLongitude()
	 */
	public double getLongitude() {
		double longitude = -1.0;		
		try {
			longitude = super.getLongitude();
		} catch (JSONException e) {			
			logger.info("unable to locate longitude");
		}
		
		return longitude;
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.GeoJSONObject#setLongitude(double)
	 */
	public void setLongitude(double lon) {
		try {
			super.setLongitude(lon);
		} catch (JSONException e) {
			logger.info("unable to locate longitude");
		}
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getProperties()
	 */
	public JSONObject getProperties() {
		JSONObject properties = null;		
		try {
			properties = super.getProperties();
		} catch (JSONException e) {			
			logger.info("unable to locate properties");
		}
		
		return properties;
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.GeoJSONObject#setProperties(org.json.JSONObject)
	 */
	public void setProperties(JSONObject properties) {
		
		try {
			super.setProperties(properties);
		} catch (JSONException e) {
			logger.info("unable to locate properties");
		}
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getRecordId()
	 */
	public String getRecordId() {
		String recordId = null;
		try {
			recordId = super.getString("id");
		} catch (JSONException e) {
			logger.info("unable to locate id");
		}
		
		return recordId;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setRecordId(String)
	 */
	public void setRecordId(String recordId) {
		try {
			super.put("id", recordId);
		} catch (JSONException e) {
			logger.info("unable to locate properties");
		}
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.model.IRecord#getObjectType()
	 */
	public String getObjectType() {
		
		String type = null;		
		try {
			type = super.getProperties().getString("type");
		} catch (JSONException e) {			
			logger.info("unable to locate type");
		}
		
		return type;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setObjectType(String)
	 */
	public void setObjectType(String type) {
		try {
			super.getProperties().put("type", type);
		} catch (JSONException e) {			
			logger.info("unable to locate properties");
		}
	}

	/**
	 * @see com.simplegeo.client.model.DefaultRecord#getIntProperty(String)
	 */
	public int getIntProperty(String key) {
		int value = 0;
		try {
			value = super.getProperties().getInt(key);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}
		
		return value;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setIntProperty(String, int)
	 */
	public void setIntProperty(String key, int value) {
		try {
			super.getProperties().put(key, value);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#getDoubleProperty(String)
	 */
	public double getDoubleProperty(String key) {
		double value = 0.0;
		try {
			value = super.getProperties().getDouble(key);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}	
		
		return value;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setDoubleProperty(String, double)
	 */
	public void setDoubleProperty(String key, double value) {
		try {
			super.getProperties().put(key, value);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}	
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#getLongProperty(String)
	 */
	public long getLongProperty(String key) {
		long value = 0;
		try {
			value = super.getProperties().getLong(key);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}	
		
		return value;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setLongProperty(String, long)
	 */
	public void setLongProperty(String key, long value) {
		try {
			super.getProperties().put(key, value);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}
	}

	/**
	 * @see com.simplegeo.client.model.DefaultRecord#getObjectProperty(String)
	 */
	public Object getObjectProperty(String key) {
		Object value = null;
		try {
			value = super.getProperties().get(key);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}	
		
		return value;
	}
	
	/**
	 * @see com.simplegeo.client.model.DefaultRecord#setObjectProperty(String, Object)
	 */
	public void setObjectProperty(String key, Object value) {
		if(value == null)
			value = JSONObject.NULL;
		try {
			super.getProperties().put(key, value);
		} catch (JSONException e) {
			logger.info(String.format("unable to locate %s", key));
		}
	}
		
} public interface IRecord {

	/**
	 * @return the id associated with the record
	 */
	public String getRecordId();
	
	/**
	 * @return the layer where the record is held
	 */
	public String getLayer();
	
	/**
	 * @return the type associated with the record
	 * @see com.simplegeo.client.model.RecordType
	 */
	public String getObjectType();
	
	/**
	 * @return the latitude for the record
	 */
	public double getLatitude();
	
	/**
	 * @return the longitude for the record
	 */
	public double getLongitude();
	
	/**
	 * @return the time at which this record was created in milliseconds
	 */
	public long getCreated();
	
	/**
	 * @return the time at which this record will expire in milliseconds
	 */
	public long getExpiration();
	
	/**
	 * @return other values associated with this record
	 */
	public JSONObject getProperties();

} public class HistoryQuery implements IQuery {

	private String cursor;
	private String layer;
	private String recordId;
	private int limit;
	
	/**
	 * @param recordId @see com.simplegeo.client.query.HistoryQuery#getRecordId()
	 * @param layer @see com.simplegeo.client.query.HistoryQuery#getLayer()
	 * @param limit @see com.simplegeo.client.query.HistoryQuery#getLimit()
	 * @param cursor @see com.simplegeo.client.query.IQuery#getCursor()
	 */
	public HistoryQuery(String recordId, String layer, int limit, String cursor) {
		this.recordId = recordId;
		this.layer = layer;
		this.cursor = cursor;
		this.limit = limit;
	}
	
	/**
	 * @param recordId @see com.simplegeo.client.query.HistoryQuery#getRecordId()
	 * @param layer @see com.simplegeo.client.query.HistoryQuery#getLayer()
	 * @param layer
	 */
	public HistoryQuery(String recordId, String layer, int limit) {
		this(recordId, layer, limit, null);
	}
	
	/**
	 * @see com.simplegeo.client.query.IQuery#getCursor()
	 */
	public String getCursor() {
		return cursor;
	}

	/**
	 * @see com.simplegeo.client.query.IQuery#getParams()
	 */
	public Map<String, String> getParams() {
		Map<String, String> params =  new HashMap<String, String>();
		if(cursor != null)
			params.put("cursor", cursor);
		
		if(limit > 0)
			params.put("limit", Integer.toString(limit));

		return params;
	}

	/**
	 * @see com.simplegeo.client.query.IQuery#getUri()
	 */
	public String getUri() {
		return String.format("/records/%s/%s/history.json", this.layer, this.recordId);
	}

	/**
	 * @return the layer
	 */
	public String getLayer() {
		return layer;
	}

	/**
	 * @param layer the layer to set
	 */
	public void setLayer(String layer) {
		this.layer = layer;
	}

	/**
	 * @return the recordId
	 */
	public String getRecordId() {
		return recordId;
	}

	/**
	 * @param recordId the recordId to set
	 */
	public void setRecordId(String recordId) {
		this.recordId = recordId;
	}

	/**
	 * @see com.simplegeo.client.query.IQuery#setCursor(java.lang.String)
	 */
	public void setCursor(String cursor) {
		this.cursor = cursor;
	}
	
	/**
	 * @return the limit
	 */
	public int getLimit() {
		return limit;
	}

	/**
	 * @param limit the limit to set
	 */
	public void setLimit(int limit) {
		this.limit = limit;
	}
} public abstract class NearbyQuery implements IQuery {
	
	private String cursor;
	private String layer;
	private List<String> types;
	private int limit;
	private double start;
	private double end;

	/**
	 * @param layer @see com.simplegeo.client.query.NearbyQuery#getLayer()
	 * @param types @see com.simplegeo.client.query.NearbyQuery#getTypes()
	 * @param limit @see com.simplegeo.client.query.NearbyQuery#getLimit()
	 * @param cursor @see com.simplegeo.client.query.IQuery#getCursor()
	 * @throws ValidLayerException
	 */
	public NearbyQuery(String layer, List<String> types, int limit, String cursor) throws ValidLayerException {
		this(layer, types, limit, cursor, -1, -1);
	}
	
	public NearbyQuery(String layer, List<String> types, int limit, String cursor, double start, double end) 
			throws ValidLayerException {

		this.types = types;
		this.cursor = cursor;
		this.limit = limit;
		
		if (layer == null || layer.equals(""))
			throw new ValidLayerException("");

		this.layer = layer;
		
		this.start = start;
		this.end = end;
	}
	
	/**
	 * @see com.simplegeo.client.query.IQuery#getParams()
	 */
	public Map<String, String> getParams() {
		Map<String, String> params = new HashMap<String, String>();
		if(limit > 0)
			params.put("limit", Integer.toString(limit));
		
		if(types != null && !types.isEmpty())			
			params.put("types", SimpleGeoUtilities.commaSeparatedString(types));
		
		if(cursor != null)
			params.put("cursor", cursor);
		
		if(start > 0 && end > 0) {
			params.put("start", Double.toString(start));
			params.put("end", Double.toString(end));
		}

		return params;
	}
	
	/**
	 * @throws UnsupportedEncodingException 
	 * @see com.simplegeo.client.query.IQuery#getUri()
	 */
	public String getUri() throws UnsupportedEncodingException {
		return String.format("/records/%s/nearby", URLEncoder.encode(layer, "UTF-8"));
	}
	
	/**
	 * @see com.simplegeo.client.query.IQuery#getCursor()
	 */
	public String getCursor() {
		return cursor;
	}

	/**
	 * @see com.simplegeo.client.query.IQuery#setCursor()
	 */
	public void setCursor(String cursor) {
		this.cursor = cursor;
	}

	/**
	 * @return the layer
	 */
	public String getLayer() {
		return layer;
	}

	/**
	 * @param layer the layer to search in
	 */
	public void setLayer(String layer) {
		this.layer = layer;
	}

	/**
	 * If this value is null, ALL types will be searched.
	 * 
	 * @return the types to look for
	 */
	public List<String> getTypes() {
		return types;
	}

	/**
	 * @param types the types to set
	 */
	public void setTypes(List<String> types) {
		this.types = types;
	}

	/**
	 * The default limit is 25.
	 * 
	 * @return the limit
	 */
	public int getLimit() {
		return limit;
	}

	/**
	 * @param limit the limit to set
	 */
	public void setLimit(int limit) {
		this.limit = limit;
	}
}
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.simplegeo.client.ISimpleGeoClient.Handler;
import com.simplegeo.client.encoder.GeoJSONEncoder;
import com.simplegeo.client.geojson.GeoJSONObject;
import com.simplegeo.client.handler.GeoJSONHandler;
import com.simplegeo.client.handler.JSONHandler;
import com.simplegeo.client.handler.RecordHandler;
import com.simplegeo.client.handler.SimpleGeoJSONHandlerIfc;
import com.simplegeo.client.http.SimpleGeoHandler;
import com.simplegeo.client.http.exceptions.APIException;
import com.simplegeo.client.http.exceptions.UnsupportedHandlerException;
import com.simplegeo.client.model.DefaultRecord;
import com.simplegeo.client.model.Envelope;
import com.simplegeo.client.model.GeoJSONRecord;
import com.simplegeo.client.model.IRecord;
import com.simplegeo.client.query.HistoryQuery;
import com.simplegeo.client.query.NearbyQuery;

	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#contains(string)
	 */
	public Object contains(String ipAddress) throws IOException {
		return contains(ipAddress, Handler.JSON);
	}

	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#contains(string, com.simplegeo.client.ISimpleGeoClient.Handler)
	 */
	public Object contains(String ipAddress, Handler type)
	throws IOException {
		if(type != Handler.JSON)
			throw new UnsupportedHandlerException(400, "The contains endpoint can only return JSON objects.");
		
		return executeGet(getURI(String.format("/contains/%s.json", ipAddress)), getHandler(type));
	}	
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#boundaries(java.lang.String)
	 */
	public Object boundaries(String featureId)
	throws IOException {
		return boundaries(featureId, Handler.GEOJSON);
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#boundaries(java.lang.String, com.simplegeo.client.ISimpleGeoClient.Handler)
	 */
	public Object boundaries(String featureId, Handler type)
	throws IOException {
		return executeGet(getURI(String.format("/boundary/%s.json", featureId)), getHandler(type));
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#overlaps(com.simplegeo.client.model.Envelope, int, java.lang.String)
	 */
	public Object overlaps(Envelope envelope, int limit, String featureType) 
	throws IOException {
		return overlaps(envelope, limit, featureType, Handler.JSON);
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#overlaps(com.simplegeo.client.model.Envelope, int, java.lang.String, com.simplegeo.client.ISimpleGeoClient.Handler)
	 */
	public Object overlaps(Envelope envelope, int limit, String featureType, Handler type)
	throws IOException {
		
		if(type != Handler.JSON)
			throw new UnsupportedHandlerException(400, "The contains endpoint can only return JSON objects.");
		
		Map<String, String> params = new HashMap<String, String>();
		
		if (limit > 0)
			params.put("limit", Integer.toString(limit));
		
		if (featureType != null) 			
			params.put("type", featureType);
		
		return executeGet(buildUrl(getURI(String.format("/overlaps/%s.json", envelope.toString())), params), getHandler(type));
	}
	
	/* (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#locate(java.lang.String)
	 */
	public Object locate(String ipAddress) 
	throws IOException {
		return locate(ipAddress, Handler.GEOJSON);
	}
	
	/*
	 * (non-Javadoc)
	 * @see com.simplegeo.client.ISimpleGeoClient#locate(java.lang.String, com.simplegeo.client.ISimpleGeoClient.Handler)
	 */
	public Object locate(String ipAddress, Handler type)
	throws IOException {
		
		if(type == Handler.RECORD)
			throw new UnsupportedHandlerException(400, "The contains endpoint can only return JSON and GeoJSON objects.");
		
		return executeGet(buildUrl(getURI(String.format("/locate/%s.json", ipAddress)), null), getHandler(type));
	}
	
	private String buildUrl(String url, Map<String, ?> parameters) {
		
		if(parameters == null)
			return url;
		
		StringBuilder sb = new StringBuilder(url);
		boolean first = true;
		for (Entry<String, ?> entry : parameters.entrySet()) {
			if (entry.getValue() != null) {
				sb.append(first ? "?" : "&");
				try {
					String key = URLEncoder.encode(entry.getKey(), DEFAULT_CONTENT_CHARSET);
					String value = URLEncoder.encode(entry.getValue().toString(), DEFAULT_CONTENT_CHARSET);
					sb.append(key).append("=").append(value);
				} catch (UnsupportedEncodingException e) {
					throw new RuntimeException(e);
				}
				first = false;
			}
		}
		return sb.toString();
	}
		
	private GeoJSONObject getGeoJSONObject(Object object) {
		
		GeoJSONObject jsonObject = null;
		if(JSONObject.class.isInstance(object)) {
			try {
				jsonObject = (GeoJSONObject)object;
				jsonObject.flatten();
			} catch (JSONException e) {
				logger.info("unable to flatten GeoJSONObject");
			}
			
		} else {

			jsonObject = GeoJSONEncoder.getGeoJSONRecord((DefaultRecord)object);
