package almonds;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
/**
* The <b>ParseObject</b> is a local representation of data that can be saved
* and retrieved from the Parse cloud.
*
* <p>
* The basic workflow for creating new data is to construct a new ParseObject,
* use put() to fill it with data, and then use save() to persist to the databa
*
* <p>
* The basic workflow for accessing existing data is to use a ParseQuery to
* specify which existing data to retrieve.
*
* @author js
*
*/
public class ParseObject {
private static final String FIELD_CREATED_AT = "createdAt";
private static final String FIELD_UPDATED_AT = "updatedAt";
private boolean userClass = false;
private String mClassName;
private Hashtable<String, Object> mData;
private ArrayList<String> mDataChangesKeys = new ArrayList<String>();
/**
* Creates a new ParseObject based upon a class name. If the class name is a
* special type (e.g. for ParseUser), then the appropriate type of
* ParseObject is returned.
*
* @param className
*
* @return
*/
public static ParseObject create(String className) {
return new ParseObject(className);
}
/**
* Constructs a new ParseObject with no data in it. A ParseObject
* constructed in this way will not have an objectId and will not persist to
* the database until save() is called.
*
* Class names must be alphanumerical plus underscore, and start with a
* letter. It is recommended to name classes in CamelCaseLikeThis.
*
* @param theClassName
* The className for this ParseObject.
*/
public ParseObject(String theClassName)
{
if(theClassName.equals("_User"))
userClass = true;
mClassName = theClassName;
mData = new Hashtable<String, Object>();
}
/**
* Used to support a query that returns an object from Parse encoded with
* JSON. This constructor will create itself from the JSON. This is probably
* a poor method, especially if the JSON is mal-formed. A better approach
* would be move the handling to ParseQuery, where it alone would
* understands query responses from Parse.
*
* @param theClassName
* The className for this ParseObject
* @param json
* JSON encoded response from Parse corresponding to a
* ParseObject
*/
ParseObject(String theClassName, JSONObject json)
{
mClassName = theClassName;
mData = new Hashtable<String, Object>();
for (String name : JSONObject.getNames(json))
{
try
{
/*
* Check for data types here. If the 'value' is JSONObject then
* there's additional data type information.
*/
if (json.get(name).getClass().getName().equals("org.json.JSONObject"))
{
JSONObject oType = (JSONObject) json.get(name);
if (oType.get("__type").equals("Pointer"))
{
System.out.println("Found Pointer");
put(name,
new ParsePointer(oType.getString("className"), oType
.getString("objectId")));
}
else if (oType.get("__type").equals("Date"))
{
throw new UnsupportedOperationException();
}
}
else
{
Object value = json.get(name); // the value associated with
// key 'name'
/*
* Check for special fields, createdAt and updatedAt, which
* will be formatted and stored as Dates.
*/
if (name.equals(FIELD_CREATED_AT) || name.equals(FIELD_UPDATED_AT))
{
value = javax.xml.bind.DatatypeConverter.parseDateTime((String) value)
.getTime();
}
put(name, value);
}
}
catch (JSONException e)
{
}
}
}
/**
* Whether this object has a particular key. Same as 'has'.
*
* @param key
* The key to check for
* @return Returns whether this object contains the key
*/
public boolean containsKey(String key)
{
return mData.containsKey(key);
}
/**
* A private helper class to facilitate running a ParseObject delete
* operation in the background.
*
* @author js
*
*/
class DeleteInBackgroundThread extends Thread
{
DeleteCallback mDeleteCallback;
/**
*
* @param callback
* A function object of type DeleteCallback, whose method
* done will be called upon completion
*/
DeleteInBackgroundThread(DeleteCallback callback)
{
mDeleteCallback = callback;
}
public void run()
{
ParseException exception = null;
try
{
delete();
}
catch (ParseException e)
{
exception = e;
}
if (mDeleteCallback != null)
{
mDeleteCallback.done(exception);
}
}
}
/**
* Deletes this object on the server in a background thread. Does nothing in
* particular when the save completes. Use this when you don't care if the
* delete works.
*/
public void deleteInBackground()
{
deleteInBackground(null);
}
/**
* Deletes this object on the server in a background thread. This is
* preferable to using delete(), unless your code is already running from a
* background thread.
*
* @param callback
* callback.done(e) is called when the save completes.
*/
public void deleteInBackground(DeleteCallback callback)
{
DeleteInBackgroundThread thread = new DeleteInBackgroundThread(callback);
thread.run();
}
/**
* Deletes this object on the server. This does not delete or destroy the
* object locally.
*
* @throws ParseException
* Throws an error if the object does not exist or if the
* internet fails.
*
*/
public void delete() throws ParseException
{
try
{
HttpClient httpclient = new DefaultHttpClient();
String path = Parse.getParseAPIUrlClasses() + mClassName + "/"
+ getObjectId();
if(userClass == true)
path = Parse.getParseAPIUrlUsers();
HttpDelete httpdelete = new HttpDelete(path);
httpdelete.addHeader("X-Parse-Application-Id", Parse.getApplicationId());
httpdelete.addHeader("X-Parse-REST-API-Key", Parse.getRestAPIKey());
HttpResponse httpresponse = httpclient.execute(httpdelete);
ParseResponse response = new ParseResponse(httpresponse);
if (!response.isFailed())
{
// delete was successful
}
else
{
throw response.getException();
}
}
catch (ClientProtocolException e)
{
throw ParseResponse.getConnectionFailedException(e.getMessage());
}
catch (IOException e)
{
throw ParseResponse.getConnectionFailedException(e.getMessage());
}
}
/**
* Accessor to the class name.
*
* @return
*/
public String getClassName()
{
return mClassName;
}
/**
* Accessor to the object id. An object id is assigned as soon as an object
* is saved to the server. The combination of a className and an objectId
* uniquely identifies an object in your application.
*
* @return The object id.
*/
public String getObjectId()
{
return (String) mData.get("objectId");
}
/**
* Setter for the object id. In general you do not need to use this.
* However, in some cases this can be convenient. For example, if you are
* serializing a ParseObject yourself and wish to recreate it, you can use
* this to recreate the ParseObject exactly.
*
* @param objectId
*/
public void setObjectId(String objectId) {
mData.put("objectId", objectId);
mDataChangesKeys.add("objectId");
}
public void setCreatedAt(String createdAt) {
mData.put("createdAt", createdAt);
mDataChangesKeys.add("objectId");
}
/**
* Access a ParsePointer value.
*
* @param key
* The key to access the value for.
* @return Returns null if there is no such key or if it is not a
* ParsePointer.
*/
public ParsePointer getParsePointer(String key)
{
Object value = get(key);
// test for no such key or not a ParsePointer
if (value == null || value.getClass() != ParsePointer.class)
return null;
return (ParsePointer) value;
}
/**
* Creates and returns a new ParsePointer object that points to the object
* it's called on. Use when other objects need to point to *this* object.
*
* @return A ParsePointer object set to point to this object.
*/
public ParsePointer getPointer()
{
return new ParsePointer(mClassName, getObjectId());
}
/**
* Access a string value.
*
* @param key
* The key to access the value for.
* @return Returns null if there is no such key or if it is not a String.
*/
public String getString(String key)
{
Object value = get(key);
// test for no such key or value not a string
if (value == null || value.getClass() != String.class)
return null;
return (String) value;
}
/**
* Access a boolean value.
*
* @param key
* The key to access the value for.
* @return Returns false if there is no such key or if it is not a boolean.
*/
public Boolean getBoolean(String key)
{
Object value = get(key);
// test for no such key or value not a string
if (value == null || value.getClass() != Boolean.class)
return null;
return (Boolean) value;
}
/**
* Encapsulates access to the HashTable that stores key/value pairs stored
* by this Object
*
* @param key
* The key to access the value for
* @return Returns null if there is no such key
*/
public Object get(String key) {
return mData.get(key);
}
/**
* Access a Date value.
*
* @param key
* The key to access the value for.
* @return Returns null if there is no such key or if it is not a Date.
*/
public Date getDate(String key) {
return getCalendarDate(key).getTime();
}
public Calendar getCalendarDate(String key){
JSONObject value = (JSONObject) get(key);
Object ob = null;
try {
ob = value.get("iso");
} catch (JSONException e) {
// XXX report error
return null;
}
return parseDateFromString(ob.toString());
}
private static Calendar parseDateFromString(String dateString){
int year = Integer.parseInt(dateString.substring(0, 4));
int month = Integer.parseInt(dateString.substring(5, 7)) - 1;
int day = Integer.parseInt(dateString.substring(8, 10));
int hour = Integer.parseInt(dateString.substring(11, 13));
int minute = Integer.parseInt(dateString.substring(14, 16));
int second = Integer.parseInt(dateString.substring(17, 19));
int milisecond = Integer.parseInt(dateString.substring(20, 23));
Calendar cal = Calendar.getInstance();
cal.set(year, month, day, hour, minute, second);
cal.set(Calendar.MILLISECOND, milisecond);
return cal;
}
private static String parseStringFromDate(Calendar cal){
String parsedDate = "";
parsedDate += cal.get(Calendar.YEAR) + "-";
parsedDate += repairIfShorter((cal.get(Calendar.MONTH)+1) + "") + "-";
parsedDate += repairIfShorter(cal.get(Calendar.DAY_OF_MONTH) + "") + "T";
parsedDate += repairIfShorter(cal.get(Calendar.HOUR_OF_DAY) + "") + ":";
parsedDate += repairIfShorter(cal.get(Calendar.MINUTE) + "") + ":";
parsedDate += repairIfShorter(cal.get(Calendar.SECOND) + "") + ".";
parsedDate += repairIfShorter(cal.get(Calendar.MILLISECOND) + "", 3) + "Z";
//return "{"+ '"' +"__type" + '"' + ":"+'"' + "Date" + '"' +","+ '"' + "iso"+ '"' + ":"+ '"' + parsedDate + '"' +"}";
return parsedDate;
}
private static String repairIfShorter(String str, int than){
while(str.length() < than)
str = "0"+str;
return str;
}
private static String repairIfShorter(String str){
return repairIfShorter(str, 2);
}
/**
* Access a long value.
*
* @param key
* The key to access the value for.
* @return Returns null if there is no such key or if it is not a long.
*/
public Long getLong(String key) {
Object value = get(key);
// test for no such key or value not a long
if (value == null || value.getClass() != Long.class)
return null;
return (Long) value;
}
/**
*
* @param key The key to access the value for.
* @return Returns null if there is no such key or if it is not an integer.
*/
public Integer getInt(String key){
Object value = get(key);
// test for no such key or value not a long
if (value == null || value.getClass() != Integer.class)
return null;
return (Integer) value;
}
/**
*
* @param key The key to access the value for.
* @return Returns null if there is no such key or if it is not a double.
*/
public Double getDouble(String key){
Object value = get(key);
try {
return (Double) value;
} catch (ClassCastException e) {
return null;
}
}
/**
* Add a key-value pair to this object. It is recommended to name keys in
* partialCamelCaseLikeThis.
*
* @param key
* Keys must be alphanumerical plus underscore, and start with a
* letter.
* @param value
* Values may be numerical, String, JSONObject, JSONArray,
* JSONObject.NULL, or other ParseObjects. value may not be null.
*/
public void put(String key, Object value) {
mData.put(key, value);
mDataChangesKeys.add(key);
}
protected void setSessionToken(String sessionToken){
}
/*
* TODO remove it from ParseObject
*/
public String getSessionToken(){
return null;
}
public void save(SaveCallback callback){
ParseException e = null;
try {
save();
} catch (ParseException e1) {
e = e1;
}
callback.done(e);
}
/**
* Saves this object to the server. Typically, you should use
* saveInBackground(com.parse.SaveCallback) instead of this, unless you are
* managing your own threading.
*
* @throws ParseException
* Throws an exception if the server is inaccessible.
*/
public void save() throws ParseException {
if(mData.get("objectId") == null)
saveAsNew();
else
update();
}
private void update() throws ParseException {
try {
HttpClient httpclient = new DefaultHttpClient();
String path = Parse.getParseAPIUrlClasses() + mClassName;
if(userClass == true)
path = Parse.getParseAPIUrlUsers();
path += "/" + mData.get("objectId");
HttpPut httpPut = new HttpPut(path);
httpPut.addHeader("X-Parse-Application-Id", Parse.getApplicationId());
httpPut.addHeader("X-Parse-REST-API-Key", Parse.getRestAPIKey());
if(userClass == true)
httpPut.addHeader("X-Parse-Session-Token", getSessionToken());
httpPut.addHeader("Content-Type", "application/json");
httpPut.setEntity(new StringEntity(toJSONObjectUpdateData().toString()));
HttpResponse httpresponse = httpclient.execute(httpPut);
ParseResponse response = new ParseResponse(httpresponse);
if (!response.isFailed()) {
JSONObject jsonResponse = response.getJsonObject();
if (jsonResponse == null)
throw response.getException();
try {
setCreatedAt(jsonResponse.getString("updatedAt"));
} catch (JSONException e) {
throw new ParseException(
ParseException.INVALID_JSON,
"Although Parse reports object successfully saved, the response was invalid.",
e);
}
} else {
throw response.getException();
}
} catch (IOException e) {
throw ParseResponse.getConnectionFailedException(e);
}
}
private void saveAsNew() throws ParseException {
try {
HttpClient httpclient = new DefaultHttpClient();
String path = Parse.getParseAPIUrlClasses() + mClassName;
if(userClass == true)
path = Parse.getParseAPIUrlUsers();
HttpPost httppost = new HttpPost(path);
httppost.addHeader("X-Parse-Application-Id", Parse.getApplicationId());
httppost.addHeader("X-Parse-REST-API-Key", Parse.getRestAPIKey());
httppost.addHeader("Content-Type", "application/json");
httppost.setEntity(new StringEntity(toJSONObject().toString()));
HttpResponse httpresponse = httpclient.execute(httppost);
ParseResponse response = new ParseResponse(httpresponse);
if (!response.isFailed()) {
JSONObject jsonResponse = response.getJsonObject();
if (jsonResponse == null)
throw response.getException();
try {
setObjectId(jsonResponse.getString("objectId"));
setCreatedAt(jsonResponse.getString("createdAt"));
try {
setSessionToken(jsonResponse.getString("sessionToken"));
} catch (Exception e) {
}
} catch (JSONException e) {
throw new ParseException(
ParseException.INVALID_JSON,
"Although Parse reports object successfully saved, the response was invalid.",
e);
}
} else {
throw response.getException();
}
} catch (ClientProtocolException e) {
throw ParseResponse.getConnectionFailedException(e);
} catch (IOException e) {
throw ParseResponse.getConnectionFailedException(e);
}
}
/**
* A private helper class to facilitate running a ParseObject save operation
* in the background.
*
* @author js
*
*/
class SaveInBackgroundThread extends Thread {
SaveCallback mSaveCallback;
/**
*
* @param callback
* A function object of type Savecallback, whose method done
* will be called upon completion
*/
SaveInBackgroundThread(SaveCallback callback) {
mSaveCallback = callback;
}
public void run() {
ParseException exception = null;
try {
save();
} catch (ParseException e) {
exception = e;
}
if (mSaveCallback != null) {
mSaveCallback.done(exception);
}
}
}
/**
* Saves this object to the server in a background thread. This is
* preferable to using save(), unless your code is already running from a
* background thread.
*
* @param callback
* callback.done(e) is called when the save completes.
*/
public void saveInBackground(SaveCallback callback) {
SaveInBackgroundThread t = new SaveInBackgroundThread(callback);
t.start();
}
/**
* Saves this object to the server in a background thread. Use this when you
* do not have code to run on completion of the push.
*/
public void saveInBackground() {
saveInBackground(null);
}
private JSONObject toJSONObject() {
JSONObject jo = new JSONObject();
for (String key : mData.keySet())
addElementToJSONObject(jo, key);
return jo;
}
private JSONObject toJSONObjectUpdateData(){
JSONObject jo = new JSONObject();
for(int i = 0; i < mDataChangesKeys.size(); i++)
addElementToJSONObject(jo, mDataChangesKeys.get(i));
mDataChangesKeys.clear();
return jo;
}
private void addElementToJSONObject(JSONObject jo, String key){
try {
Object obj = get(key);
if(obj instanceof Calendar){
obj = getDateJSONObject((Calendar) obj);
}else if(obj instanceof Date){
Calendar cal = Calendar.getInstance();
cal.setTime((Date) obj);
obj = getDateJSONObject(cal);
}
jo.put(key, obj);
} catch (JSONException e) {
}
}
static JSONObject getDateJSONObject(Calendar cal) throws JSONException{
JSONObject obj = new JSONObject();
obj.put("__type", "Date");
obj.put("iso", parseStringFromDate(cal));
return obj;
}
/**
* This reports time as the server sees it, so that if you make changes to a
* ParseObject, then wait a while, and then call save(), the updated time
* will be the time of the save() call rather than the time the object was
* changed locally.
*
* @return The last time this object was updated on the server.
*/
public Date getUpdatedAt() {
return getDate(FIELD_UPDATED_AT);
}
/**
* This reports time as the server sees it, so that if you create a
* ParseObject, then wait a while, and then call save(), the creation time
* will be the time of the first save() call rather than the time the object
* was created locally.
*
* @return The first time this object was saved on the server.
*/
public Date getCreatedAt() {
return getDate(FIELD_CREATED_AT);
}
/**
* Decides if the calling ParseObject and the parameter ParseObject have the
* same Parse Id.
*
* @param asThisObject
* Parse Object to compare with
* @return True if the Ids of the two objects are equal, false otherwise
*/
public boolean hasSameId(ParseObject asThisObject) {
return this.getObjectId().equals(asThisObject.getObjectId());
}
}