package edu.mit.mobile.android.locast.data;
/*
* Copyright (C) 2010 MIT Mobile Experience Lab
*
* 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
* of the License, 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import java.io.IOException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import edu.mit.mobile.android.locast.accounts.Authenticator;
import edu.mit.mobile.android.locast.net.NetworkClient;
import edu.mit.mobile.android.locast.net.NetworkProtocolException;
import edu.mit.mobile.android.locast.sync.LocastSyncService;
import edu.mit.mobile.android.utils.ListUtils;
/**
* This type of object row can be serialized to/from JSON and synchronized to a server.
*
* @author stevep
*
*/
public abstract class JsonSyncableItem implements BaseColumns {
public static final String
_PUBLIC_URI = "uri",
_MODIFIED_DATE = "modified",
_SERVER_MODIFIED_DATE = "server_modified",
_CREATED_DATE = "created";
public static final String[] SYNC_PROJECTION = {
_ID,
_PUBLIC_URI,
_MODIFIED_DATE,
_SERVER_MODIFIED_DATE,
_CREATED_DATE,
};
/**
* @return the complete DB projection for the local object. Really only needs to
* contain all the fields that are used in the sync map.
*/
public abstract String[] getFullProjection();
/**
* @return The URI for a given content directory.
*/
public abstract Uri getContentUri();
private static String[] PUB_URI_PROJECTION = {_ID, _PUBLIC_URI};
/**
* Given a public Uri fragment, finds the local item representing it. If there isn't any such item, null is returned.
*
* @param context
* @param dirUri the base local URI to search.
* @param pubUri A public URI fragment that represents the given item. This must match the result from the API.
* @return a local URI matching the item or null if none were found.
*/
public static Uri getItemByPubIUri(Context context, Uri dirUri, String pubUri){
Uri uri = null;
final ContentResolver cr = context.getContentResolver();
final String[] selectionArgs = {pubUri};
final Cursor c = cr.query(dirUri, PUB_URI_PROJECTION, _PUBLIC_URI+"=?", selectionArgs, null);
if (c.moveToFirst()){
uri = ContentUris.withAppendedId(dirUri, c.getLong(c.getColumnIndex(_ID)));
}
c.close();
return uri;
}
/**
* @return A mapping of server↔local DB items.
*/
public SyncMap getSyncMap(){
return SYNC_MAP;
};
public static class ItemSyncMap extends SyncMap {
/**
*
*/
private static final long serialVersionUID = 1L;
public ItemSyncMap() {
super();
put(_PUBLIC_URI, new SyncFieldMap("uri", SyncFieldMap.STRING, SyncItem.SYNC_FROM));
put(_SERVER_MODIFIED_DATE, new SyncFieldMap("modified", SyncFieldMap.DATE, SyncItem.SYNC_FROM));
put(_CREATED_DATE, new SyncFieldMap("created", SyncFieldMap.DATE, SyncItem.SYNC_FROM | SyncItem.FLAG_OPTIONAL));
}
}
public static final ItemSyncMap SYNC_MAP = new ItemSyncMap();
public static final String LIST_DELIM = "|";
// the below splits "tag1|tag2" but not "tag1\|tag2"
public static final String LIST_SPLIT = "(?<!\\\\)\\|";
/**
* Gets a list for the current item in the cursor.
*
* @param column column number
* @param c cursor pointing to a row
* @return
* @see #getList(int, Cursor)
*/
public static List<String> getList(int column, Cursor c){
final String t = c.getString(column);
return getList(t);
}
/**
* Given a string made by {@link JsonSyncableItem#putList(String, ContentValues, Collection) putList()},
* return a List containing all the items.
*
* @param listString
* @return a new list representing all the items in the list
* @see #putList(String, ContentValues, Collection)
*/
public static List<String> getList(String listString){
if (listString != null && listString.length() > 0){
final String[] split = listString.split(LIST_SPLIT);
for (int i = 0; i < split.length; i++){
split[i] = split[i].replace("\\"+LIST_DELIM, LIST_DELIM);
}
return Arrays.asList(split);
}else{
return new Vector<String>();
}
}
/**
* @param columnName the name of the key in cv to store the resulting list
* @param cv a {@link ContentValues} to store the resulting list in
* @param list
* @return the same ContentValues that were passed in
* @see #toListString(Collection)
*/
public static ContentValues putList(String columnName, ContentValues cv, Collection<String> list){
cv.put(columnName, toListString(list));
return cv;
}
/**
* Turns a collection of strings into a delimited string
*
* @param list a list of strings
* @return a string representing the list, delimited by LIST_DELIM with any existing instances escaped.
* @see #getList(String)
*/
public static String toListString(Collection<String> list){
final List<String> tempList = new Vector<String>(list.size());
for (String s : list){
// escape all of the delimiters in the individual strings
s = s.replace(LIST_DELIM, "\\" + LIST_DELIM);
tempList.add(s);
}
return ListUtils.join(tempList, LIST_DELIM);
}
private static Pattern durationPattern = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
/**
* Given a JSON item and a sync map, create a ContentValues map to be inserted into the DB.
*
* @param context
* @param localItem will be null if item is new to mobile. If it's been sync'd before, will point to local entry.
* @param item incoming JSON item.
* @param mySyncMap A mapping between the JSON object and the content values.
* @return new ContentValues, ready to be inserted into the database.
* @throws JSONException
* @throws IOException
* @throws NetworkProtocolException
*/
public final static ContentValues fromJSON(Context context, Uri localItem, JSONObject item, SyncMap mySyncMap) throws JSONException, IOException,
NetworkProtocolException {
final ContentValues cv = new ContentValues();
for (final String propName: mySyncMap.keySet()){
final SyncItem map = mySyncMap.get(propName);
if (!map.isDirection(SyncItem.SYNC_FROM)){
continue;
}
if (map.isOptional() &&
(!item.has(map.remoteKey) || item.isNull(map.remoteKey))){
continue;
}
final ContentValues cv2 = map.fromJSON(context, localItem, item, propName);
if (cv2 != null){
cv.putAll(cv2);
}
}
return cv;
}
/**
* @param context
* @param localItem Will contain the URI of the local item being referenced in the cursor
* @param c active cursor with the item to sync selected.
* @param mySyncMap
* @return a new JSONObject representing the item
* @throws JSONException
* @throws NetworkProtocolException
* @throws IOException
*/
public final static JSONObject toJSON(Context context, Uri localItem, Cursor c, SyncMap mySyncMap) throws JSONException, NetworkProtocolException, IOException {
final JSONObject jo = new JSONObject();
for (final String lProp: mySyncMap.keySet()){
final SyncItem map = mySyncMap.get(lProp);
if (!map.isDirection(SyncItem.SYNC_TO)){
continue;
}
final int colIndex = c.getColumnIndex(lProp);
// if it's a real property that's optional and is null on the local side
if (!lProp.startsWith("_") && map.isOptional()){
if (colIndex == -1){
throw new RuntimeException("Programming error: Cursor does not have column '"+lProp+"', though sync map says it should. Sync Map: "+mySyncMap );
}
if (c.isNull(colIndex)){
continue;
}
}
final Object jsonObject = map.toJSON(context, localItem, c, lProp);
if (jsonObject instanceof MultipleJsonObjectKeys){
for (final Entry<String, Object> entry :((MultipleJsonObjectKeys) jsonObject).entrySet()){
jo.put(entry.getKey(), entry.getValue());
}
}else{
jo.put(map.remoteKey, jsonObject);
}
}
return jo;
}
/**
* Return this from the toJson() method in order to have the mapper insert multiple
* keys into the parent JSON object. Use the standard put() method to add keys.
*
* @author steve
*
*/
public static class MultipleJsonObjectKeys extends HashMap<String, Object>{
/**
*
*/
private static final long serialVersionUID = 6639058165035918704L;
}
public static abstract class SyncItem {
protected final String remoteKey;
public static final int SYNC_BOTH = 0x3,
SYNC_TO = 0x1,
SYNC_FROM = 0x2,
SYNC_NONE = 0x4,
FLAG_OPTIONAL = 0x10;
private static final int DEFAULT_SYNC_DIRECTION = SYNC_BOTH;
private final int flags;
public SyncItem(String remoteKey) {
this(remoteKey, DEFAULT_SYNC_DIRECTION);
}
public SyncItem(String remoteKey, int flags){
this.remoteKey = remoteKey;
this.flags = flags;
}
public String getRemoteKey(){
return remoteKey;
}
/**
* @return SYNC_BOTH, SYNC_NONE, SYNC_TO, or SYNC_FROM
*/
public int getDirection() {
final int directionFlags = flags & 0x3;
if ((flags & SYNC_NONE) != 0){
return SYNC_NONE;
}
if (directionFlags == 0){
return DEFAULT_SYNC_DIRECTION;
}
return directionFlags;
}
public boolean isDirection(int syncDirection){
return (getDirection() & syncDirection) != 0;
}
public boolean isOptional() {
return (flags & FLAG_OPTIONAL) != 0;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append('"');
sb.append(remoteKey);
sb.append('"');
sb.append("; direction: ");
switch (getDirection()){
case SYNC_BOTH:
sb.append("SYNC_BOTH");
break;
case SYNC_FROM:
sb.append("SYNC_FROM");
break;
case SYNC_TO:
sb.append("SYNC_TO");
break;
case SYNC_NONE:
sb.append("SYNC_NONE");
break;
}
sb.append(isOptional() ? "; optional ": "; not optional ");
sb.append(String.format("(flags %x)", flags));
return sb.toString();
}
/**
* Translate a local database entry into JSON.
* @param context Android context
* @param localItem uri of the local item
* @param c cursor pointing to the item
* @param lProp the local property where the data should be stored in the CV
* @return JSONObject, JSONArray or any other type that JSONObject.put() supports.
* @throws JSONException
* @throws NetworkProtocolException
* @throws IOException
*/
public abstract Object toJSON(Context context, Uri localItem, Cursor c, String lProp) throws JSONException, NetworkProtocolException, IOException;
/**
* @param context Android context
* @param localItem uri of the local item or null if it is new
* @param item the JSONObject of the item. It's your job to pull out the desired field(s) here.
* @param lProp the local property where the data should be stored in the CV
* @return a new ContentValues, that will be merged into the new ContentValues object
* @throws JSONException
* @throws NetworkProtocolException
* @throws IOException
*/
public abstract ContentValues fromJSON(Context context, Uri localItem, JSONObject item, String lProp) throws JSONException, NetworkProtocolException, IOException;
public void onPostSyncItem(Context context, Uri uri, JSONObject item, boolean updated)
throws SyncException, IOException {}
}
/**
* A custom sync item. Use this if the automatic field mappers aren't
* flexible enough to read/write from JSON.
*
* @author steve
*
*/
public static abstract class SyncCustom extends SyncItem {
public SyncCustom(String remoteKey) {
super(remoteKey);
}
public SyncCustom(String remoteKey, int flags){
super(remoteKey, flags);
}
}
/**
* A simple field mapper. This maps a JSON object key to a local DB field.
* @author steve
*
*/
public static class SyncFieldMap extends SyncItem {
private final int type;
public SyncFieldMap(String remoteKey, int type) {
this(remoteKey, type, SyncItem.SYNC_BOTH);
}
public SyncFieldMap(String remoteKey, int type, int flags) {
super(remoteKey, flags);
this.type = type;
}
public int getType(){
return type;
}
public final static int
STRING = 0,
INTEGER = 1,
BOOLEAN = 2,
LIST_STRING = 3,
DATE = 4,
DOUBLE = 5,
LIST_DOUBLE = 6,
LIST_INTEGER = 7,
LOCATION = 8,
DURATION = 9;
@Override
public ContentValues fromJSON(Context context, Uri localItem,
JSONObject item, String lProp) throws JSONException,
NetworkProtocolException, IOException {
final ContentValues cv = new ContentValues();
switch (getType()){
case SyncFieldMap.STRING:
cv.put(lProp, item.getString(remoteKey));
break;
case SyncFieldMap.INTEGER:
cv.put(lProp, item.getInt(remoteKey));
break;
case SyncFieldMap.DOUBLE:
cv.put(lProp, item.getDouble(remoteKey));
break;
case SyncFieldMap.BOOLEAN:
cv.put(lProp, item.getBoolean(remoteKey));
break;
case SyncFieldMap.LIST_INTEGER:
case SyncFieldMap.LIST_STRING:
case SyncFieldMap.LIST_DOUBLE:{
final JSONArray ar = item.getJSONArray(remoteKey);
final List<String> l = new Vector<String>(ar.length());
for (int i = 0; i < ar.length(); i++){
switch (getType()){
case SyncFieldMap.LIST_STRING:
l.add(ar.getString(i));
break;
case SyncFieldMap.LIST_DOUBLE:
l.add(String.valueOf(ar.getDouble(i)));
break;
case SyncFieldMap.LIST_INTEGER:
l.add(String.valueOf(ar.getInt(i)));
break;
}
}
cv.put(lProp, ListUtils.join(l, LIST_DELIM));
}
break;
case SyncFieldMap.DATE:
try {
cv.put(lProp, NetworkClient.parseDate(item.getString(remoteKey)).getTime());
} catch (final ParseException e) {
final NetworkProtocolException ne = new NetworkProtocolException("bad date format");
ne.initCause(e);
throw ne;
}
break;
case SyncFieldMap.DURATION:{
final Matcher m = durationPattern.matcher(item.getString(remoteKey));
if (! m.matches()){
throw new NetworkProtocolException("bad duration format");
}
final int durationSeconds = 1200 * Integer.parseInt(m.group(1)) + 60 * Integer.parseInt(m.group(2)) + Integer.parseInt(m.group(3));
cv.put(lProp, durationSeconds);
} break;
}
return cv;
}
@Override
public Object toJSON(Context context, Uri localItem, Cursor c, String lProp)
throws JSONException, NetworkProtocolException, IOException {
Object retval;
final int columnIndex = c.getColumnIndex(lProp);
switch (getType()){
case SyncFieldMap.STRING:
retval = c.getString(columnIndex);
break;
case SyncFieldMap.INTEGER:
retval = c.getInt(columnIndex);
break;
case SyncFieldMap.DOUBLE:
retval = c.getDouble(columnIndex);
break;
case SyncFieldMap.BOOLEAN:
retval = c.getInt(columnIndex) != 0;
break;
case SyncFieldMap.LIST_STRING:
case SyncFieldMap.LIST_DOUBLE:
case SyncFieldMap.LIST_INTEGER:
{
final JSONArray ar = new JSONArray();
final String joined = c.getString(columnIndex);
if (joined == null){
throw new NullPointerException("Local value for '" + lProp + "' cannot be null.");
}
if (joined.length() > 0){
for (final String s : joined.split(TaggableItem.LIST_SPLIT)){
switch (getType()){
case SyncFieldMap.LIST_STRING:
ar.put(s);
break;
case SyncFieldMap.LIST_DOUBLE:
ar.put(Double.valueOf(s));
break;
case SyncFieldMap.LIST_INTEGER:
ar.put(Integer.valueOf(s));
break;
}
}
}
retval = ar;
}
break;
case SyncFieldMap.DATE:
retval =
NetworkClient.dateFormat.format(new Date(c.getLong(columnIndex)));
break;
case SyncFieldMap.DURATION:{
final int durationSeconds = c.getInt(columnIndex);
// hh:mm:ss
retval = String.format("%02d:%02d:%02d", durationSeconds / 1200, (durationSeconds / 60) % 60, durationSeconds % 60);
}break;
default:
throw new IllegalArgumentException(this.toString() + " has an invalid type.");
}
return retval;
}
}
/**
* Given a URI as the value, resolves the item(s) and stores the relations in the database.
* Each URI represents a list of items that should be related to the parent object.
*
* The type of the child is defined by the type of the URI that is returned in the Relationship.
* Synchronization of this field simply stores the public URI in the given local field as a string
* and then calls a synchronization on that URI with the context of the destination URI (as defined
* by the Relationship).
*
* For example, one could define a simple comment system to allow commenting on a Cast. In the Cast's sync map,
* <pre>
* SYNC_MAP.put(_COMMENTS_URI, new SyncChildRelation("comments", new SyncChildRelation.SimpleRelationship("castcomments"), SyncFieldMap.SYNC_FROM | SyncFieldMap.FLAG_OPTIONAL));
* </pre>
*
* This stores the public URI that's in the JSON object's "comments" value in the Cast's _COMMENTS_URI field.
* Additionally, it starts a sync on that same public URI with the extra information of {@code content://casts/1/castcomments}
* so that the results of the public URI sync will be stored in the local URI provided. The type of the child object is entirely
* determined by the type of the local URI.
*
* @author steve
*
*/
public static class SyncChildRelation extends SyncItem {
private final Relationship mRelationship;
private final boolean mStartChildSync;
/**
*
* @param remoteKey the key in the JSON
* @param relationship how this field relates to the local database
* @param startChildSync if true, requests that the child be sync'd when the parent is sync'd. Defaults to true.
* @param flags standard {@link SyncItem} flags
*/
public SyncChildRelation(String remoteKey, Relationship relationship, boolean startChildSync, int flags) {
super(remoteKey, flags);
mRelationship = relationship;
mStartChildSync = startChildSync;
}
/**
* By default, starts a child sync.
*
* @param remoteKey the key in the JSON
* @param relationship how this field relates to the local database
* @param flags standard {@link SyncItem} flags
*/
public SyncChildRelation(String remoteKey, Relationship relationship, int flags) {
this(remoteKey, relationship, true, flags);
}
@Override
public Object toJSON(Context context, Uri localItem, Cursor c,
String lProp) throws JSONException, NetworkProtocolException,
IOException {
// This doesn't need to do anything, as the sync framework will automatically handle JSON creation.
return null;
}
@Override
public ContentValues fromJSON(Context context, Uri localItem,
JSONObject item, String lProp) throws JSONException,
NetworkProtocolException, IOException {
final ContentValues cv = new ContentValues();
final String childPubUri = item.getString(remoteKey);
// store the URI so we can refresh from it later
cv.put(lProp, childPubUri);
return cv;
}
@Override
public void onPostSyncItem(Context context, Uri uri, JSONObject item,
boolean updated) throws SyncException, IOException {
if (!mStartChildSync){
return;
}
final Uri childDir = mRelationship.getChildDirUri(uri);
try {
final String childPubUri = item.getString(remoteKey);
// TODO optimize so it doesn't need to create a whole new instance
// TODO this shouldn't be getting the first account here, it should somehow use the
// one that's active for the sync. Perhaps this needs to be revised to allow this
// method to get the sync context.
final NetworkClient nc = NetworkClient.getInstance(context, Authenticator.getFirstAccount(context));
final Uri serverUri = nc.getFullUrl(childPubUri);
LocastSyncService.startSync(context, serverUri, childDir, false);
} catch (final JSONException e) {
final IOException ioe = new IOException("JSON encoding error");
ioe.initCause(e);
throw ioe;
}
}
/**
* A simple relationship where the local URI path is the relationship name. eg.
*
* Given the parent {@code content://itinerary/1} with a relationship of "casts", the child items are all at {@code content://itinerary/1/casts}
*
* @author steve
*
*/
public static class SimpleRelationship extends Relationship {
/**
* Create a new simple relationship.
*
* @param relationship local uri path suffix for the item.
*/
public SimpleRelationship(String relationship) {
super(relationship);
}
@Override
public Uri getChildDirUri(Uri parent) {
return Uri.withAppendedPath(parent, getRelation());
}
}
/**
* Defines a relationship between one object and another.
* For most cases, you'll want to use {@link SimpleRelationship}.
*
* @author steve
*
*/
public static abstract class Relationship {
private final String mRelation;
public Relationship(String relationship) {
mRelation = relationship;
}
public abstract Uri getChildDirUri(Uri parent);
public String getRelation(){
return mRelation;
}
}
}
/**
* An item that recursively goes into a JSON object and can map
* properties from that. When outputting JSON, will create the object
* again.
*
* @author stevep
*
*/
public static class SyncMapChain extends SyncItem {
private final SyncMap chain;
public SyncMapChain(String remoteKey, SyncMap chain) {
super(remoteKey);
this.chain = chain;
}
public SyncMapChain(String remoteKey, SyncMap chain, int direction) {
super(remoteKey, direction);
this.chain = chain;
}
public SyncMap getChain() {
return chain;
}
@Override
public ContentValues fromJSON(Context context, Uri localItem,
JSONObject item, String lProp) throws JSONException,
NetworkProtocolException, IOException {
return JsonSyncableItem.fromJSON(context, localItem, item.getJSONObject(remoteKey), getChain());
}
@Override
public Object toJSON(Context context, Uri localItem, Cursor c,
String lProp) throws JSONException, NetworkProtocolException,
IOException {
return JsonSyncableItem.toJSON(context, localItem, c, getChain());
}
@Override
public void onPostSyncItem(Context context, Uri uri, JSONObject item,
boolean updated) throws SyncException, IOException {
super.onPostSyncItem(context, uri, item, updated);
chain.onPostSyncItem(context, uri, item, updated);
}
}
/**
* One-directional sync of a child item.
* @author steve
*
*/
public static class SyncChildField extends SyncMapChain {
public SyncChildField(String localKey, String remoteKey, String remoteField, int fieldType) {
this(localKey, remoteKey, remoteField, fieldType, 0);
}
public SyncChildField(String localKey, String remoteKey, String remoteField, int fieldType, int fieldFlags) {
super(remoteKey, new SyncMap(), SyncMapChain.SYNC_FROM);
getChain().put(localKey, new SyncFieldMap(remoteField, fieldType, fieldFlags));
}
}
/**
* Store multiple remote fields into one local field.
*
* @author steve
*
*/
public static abstract class SyncMapJoiner extends SyncItem {
private final SyncItem[] children;
/**
* @param children The SyncItems that you wish to join. These should probably be of the same type,
* but don't need to be. You'll have to figure out how to join them by defining your joinContentValues().
*/
public SyncMapJoiner(SyncItem ... children) {
super("_syncMapJoiner", SyncItem.SYNC_BOTH);
this.children = children;
}
public SyncItem getChild(int index){
return children[index];
}
public int getChildCount(){
return children.length;
}
@Override
public ContentValues fromJSON(Context context, Uri localItem,
JSONObject item, String lProp) throws JSONException,
NetworkProtocolException, IOException {
final ContentValues[] cvArray = new ContentValues[children.length];
for (int i = 0; i < children.length; i++){
if (children[i].isDirection(SYNC_FROM)){
cvArray[i] = children[i].fromJSON(context, localItem, item, lProp);
}
}
return joinContentValues(cvArray);
}
@Override
public Object toJSON(Context context, Uri localItem, Cursor c,
String lProp) throws JSONException, NetworkProtocolException,
IOException {
final Object[] jsonObjects = new Object[children.length];
for (int i = 0; i < children.length; i++){
if (children[i].isDirection(SYNC_TO)){
jsonObjects[i] = children[i].toJSON(context, localItem, c, lProp);
}
}
return joinJson(jsonObjects);
}
@Override
public void onPostSyncItem(Context context, Uri uri, JSONObject item,
boolean updated) throws SyncException, IOException {
super.onPostSyncItem(context, uri, item, updated);
for (final SyncItem child : children){
child.onPostSyncItem(context, uri, item, updated);
}
}
/**
* Implement this to tell the joiner how to join the result of the children's fromJson()
* into the same ContentValues object.
* @param cv all results from fromJson()
* @return a joined version of the ContentValues
*/
public abstract ContentValues joinContentValues(ContentValues[] cv);
public Object joinJson(Object[] jsonObjects) {
final MultipleJsonObjectKeys multKeys = new MultipleJsonObjectKeys();
for (int i = 0; i < getChildCount(); i++){
multKeys.put(getChild(i).remoteKey, jsonObjects[i]);
}
return multKeys;
}
}
/**
* Used for outputting a literal into a JSON object. If the format requires
* some strange literal, like
* "type": "point"
* this can add it.
*
* @author steve
*
*/
public static class SyncLiteral extends SyncItem {
private final Object literal;
public SyncLiteral(String remoteKey, Object literal) {
super(remoteKey);
this.literal = literal;
}
public Object getLiteral() {
return literal;
}
@Override
public ContentValues fromJSON(Context context, Uri localItem,
JSONObject item, String lProp) throws JSONException,
NetworkProtocolException, IOException {
return null;
}
@Override
public Object toJSON(Context context, Uri localItem, Cursor c,
String lProp) throws JSONException, NetworkProtocolException,
IOException {
return literal;
}
}
}