public class ConflictResolverFactory {

    public static ConflictResolutionStrategy getConflictResolver(ConflictResolutionType conflictResolutionStrategy) {

        switch(conflictResolutionStrategy) {
            case SERVER_WINS:
                return new ServerWinsResolver();
            case CLIENT_WINS:
                return new ClientWinsResolver();
            case MANUAL:
                return new ManualResolver();
            default:
                throw new UnsupportedOperationException("unknown conflict resolution strategy:" + conflictResolutionStrategy);
        }
    }

    public static Resolution getResolverAndResolveConflicts(ConflictResolutionType conflictResolutionType, List<Conflict> conflicts) {
        return getConflictResolver(conflictResolutionType).resolveConflicts(conflicts);
    }


} public class Resolution {
    private List<Conflict> unresolvedConflicts;
    private List<SauceEntity> resolvedConflicts;

    public static Resolution getUnresolvedResolution(List<Conflict> unresolvedConflicts) {
        Resolution resolution = new Resolution();
        resolution.unresolvedConflicts = unresolvedConflicts;
        return resolution;
    }

    public static Resolution getResolvedResolution(List<SauceEntity> resolvedConflicts) {
        Resolution resolution = new Resolution();
        resolution.resolvedConflicts = resolvedConflicts;
        return resolution;
    }

    public List<SauceEntity> getResolvedConflicts() {
        return resolvedConflicts;
    }

    public List<Conflict> getUnresolvedConflicts() {
        return unresolvedConflicts;
    }

    public boolean resolved() {
        if(unresolvedConflicts != null && !unresolvedConflicts.isEmpty()) {
            return false;
        }
        return true;
    }
} public class Conflict {
    private SauceEntity clientEntity;
    private SauceEntity serverEntity;
    public Conflict(SauceEntity clientEntity, SauceEntity serverEntity) {
        this.clientEntity = clientEntity;
        this.serverEntity = serverEntity;
    }

    public SauceEntity getClientEntity() {
        return clientEntity;
    }

    public SauceEntity getServerEntity() {
        return serverEntity;
    }

    @Override
    public String toString() {
        return "Conflict{" +
                "clientEntity=" + clientEntity +
                ", serverEntity=" + serverEntity +
                '}';
    }
} public class Reference {
    private String key;
    private Integer arrayIndex;

    public Reference() {/*for endpoints bean*/}
    public Reference(String key, Integer arrayIndex) {
        this.key = key;
        this.arrayIndex = arrayIndex;
    }

    public String getKey() { return key; }
    public Integer getArrayIndex() { return arrayIndex; }
} @Entity
public class SauceEntity {
    //indexed fields and IDs
    @Id private long id;
    @Parent private Key<SauceUser> userKey;
    @Index(IfNotNull.class) private String type;
    @Index private long lastSyncedId;
    @Index(IfNotNull.class) private Map<String, Object> indexedData;
    //we use a timestamp so the reaper can query > deleted at and we only index when its not null to save 2 writes per entitiy that isnt deleted
    @Index(IfNotNull.class) private Long deletedAt;

    //unindexed fields stored in datastore
    //@Serialize
    private Map<String, Object> unindexedData;
    private long lastSyncedTime;

    //Ignored in datastore
    @Ignore private List<Reference> references;
    @Ignore private boolean deleted = false;
    @Ignore private Map<String, Object> data;
    @Ignore private Set<String> indexes;
    @Ignore private Set<String> ignored;

    //static stuff isnt serialized
    public static final String lastSyncedIdColumn = "lastSyncedId";
    public static final String deletedColumn = "deletedAt";
    public static final String indexedDataPrefix = "indexedData" + '.';
    private static final Logger log = Logger.getLogger(SauceEntity.class.getName());

    //public endpoints API
    public String getSyncId() {
        return lastSyncedId + "." + lastSyncedTime;
    }

    public void setSyncId(String syncId) {
        lastSyncedTime = SyncIdUtils.getLastSyncedTime(syncId);
        lastSyncedId = SyncIdUtils.getLastSyncedId(syncId);
    }

    public boolean isDeleted() {
        if(deletedAt != null && deletedAt > 0) {
            deleted = true;
        }
        return deleted;
    }

    public void setData(Map<String, Object> data) {
        log.info("data:" + data);
        this.data = data;
        dataToIndexedAndUnindexed();
    }

    public void setIgnored(Set<String> ignored) {
        this.ignored = ignored;
        dataToIndexedAndUnindexed();
    }

    public Set<String> getIgnored() {
        return ignored;
    }

    public void setIndexes(Set<String> indexes) {
        this.indexes = indexes;
        log.info("indexes:" + indexes);
        dataToIndexedAndUnindexed();
    }

    public void setDeleted(boolean deleted) {
        if(deleted) {
            deletedAt = System.currentTimeMillis();
        }
        this.deleted = deleted;
    }

    public String getEntityId() {
        return getKey()==null? null : getKey().toWebSafeString();
    }

    public void setEntityId(String entityId) {
        Key<SauceEntity> key = Key.create(entityId);
        this.id = key.getId();
        this.userKey = key.getParent();
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }

    public void setReferences(List<Reference> references) { this.references = references; }
    public List<Reference> getReferences() { return references; }

    public Set<String> getIndexes() {
        if(indexes != null) {
            return indexes;
        }
        //on queries we need to infer the data from the @Ignore the indexed data fields
        Set<String> inferredIndexes = new HashSet<String>();
        if(indexedData != null) {
            inferredIndexes.addAll(indexedData.keySet());
        }
        return inferredIndexes;
    }

    public Map<String, Object> getData() {
        if(data != null) {
            return data;
        }
        log.info("indexedData: " + indexedData);
        log.info("unindexedData: " + unindexedData);

        //on queries we need to infer the data from the @Ignore indexed and unindexed data fields
        Map<String, Object> newData = new HashMap<String,Object>();
        if(indexedData != null) {
            newData.putAll(indexedData);
        }
        if(unindexedData != null) {
            newData.putAll(unindexedData);
        }
        log.info("data:" + newData);
        return newData;
    }

    //private endpoints api
    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getLastSyncedId() { return lastSyncedId; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public void setLastSyncedId(long lastSyncedId) { this.lastSyncedId = lastSyncedId; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public void setLastSyncedTime(long lastSyncedTime) { this.lastSyncedTime = lastSyncedTime; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getLastSyncedTime() { return lastSyncedId; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public void setIndexedData(Map<String, Object> indexedData) { this.indexedData = indexedData; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public Map<String, Object> getIndexedData(){ return indexedData; };

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public Map<String, Object> getUnindexedData() { return unindexedData; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public void setUnindexedData(Map<String, Object> unindexedData) { this.unindexedData = unindexedData; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public void setUserKey(Key<SauceUser> userKey) { this.userKey = userKey;}

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public Key<SauceEntity> getKey() {
        if(userKey == null || id == 0) {
            return null;
        }
        return Key.create(userKey, SauceEntity.class, id);
    }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getId() {
        return id;
    }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public void setId(long id) { this.id = id; }

    /* helper gets called by setIndexedData and setUnindexedData needs to be idempotent since
       setIndexedData, setData, setIndexes, and setIgnored all set this property, should probably
       see if I can do this in a slightly cleaner way
     */
    private void dataToIndexedAndUnindexed() {
        if(data == null) {
            //we cant do anything if data is null
            return;
        }
        if(indexes == null) {
            indexes = new HashSet<String>();
        }

        if (!indexes.isEmpty() && indexedData == null) {
            indexedData = new HashMap<String, Object>();
        }
        Map<String, Object> dataShallowCopy = new HashMap<String, Object>(data);
        log.info("shallow:" + dataShallowCopy);

        if(ignored != null) {
            for(String ignore: ignored) {
                dataShallowCopy.remove(ignore);
            }
        }

        if (indexes != null) {
            for (String index : indexes) {
                Object removedValue = dataShallowCopy.remove(index);
                if (removedValue != null) {
                    indexedData.put(index, removedValue);
                }
            }
        }

        if(!data.isEmpty()) {
            unindexedData = new HashMap<String,Object>(dataShallowCopy);
        }
    }


    /*
        So the problem here is two fold: first, Google Datastore upcasts all ints into a 64 bit number
        when storing them and second, since Cloud Endpoints sends data over the wire as json,
        all 64 bit numbers are turned into strings since [js loses precision after 2^53]()
        see https://cloud.google.com/appengine/docs/java/datastore/entities#Java_Properties_and_value_types
        and https://code.google.com/p/googleappengine/issues/detail?id=9173
    */
    @OnLoad
    public void onLoad() {
        fixLongsThatWillBeUpcastedToJSONStrings(indexedData);
        fixLongsThatWillBeUpcastedToJSONStrings(unindexedData);
    }

    private void fixLongsThatWillBeUpcastedToJSONStrings(Map<String, Object> map) {
        if(map == null) {
            return;
        }

        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if(value instanceof Long ) {
                try {
                    int intValue = Ints.checkedCast((Long) value);
                    entry.setValue(intValue);
                } catch (IllegalArgumentException e) {
                    //do nothing, the long is actually a long, GAE is gonna cast this to a string in
                    //the response, since JSON can only support 2^53 bit integers
                }
            } else if(value instanceof List) {
                //check for lists that contain numbers
                List list = (List)value;
                for(int c = 0; c < list.size(); c++) {
                    if(list.get(c) instanceof Long) {
                        try {
                            int intValue = Ints.checkedCast((Long) list.get(c));
                            list.set(c, intValue);
                        } catch (IllegalArgumentException e) {
                            //do nothing, the long is actually a long, GAE is gonna cast this to a string in
                            //the response, since JSON can only support 2^53 bit integers
                        }
                    }
                }
            }
        }
    }

    @Override
    public String toString() {
        return "SauceEntity{" +
                "id=" + id +
                ", userKey=" + userKey +
                ", deleted=" + deleted +
                ", lastSyncedId=" + lastSyncedId +
                ", indexedData=" + indexedData +
                ", unindexedData=" + unindexedData +
                ", references=" + references +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        SauceEntity that = (SauceEntity) o;

        if (id != that.id) return false;
        return lastSyncedId == that.lastSyncedId;

    }

    @Override
    public int hashCode() {
        int result = (int) (id ^ (id >>> 32));
        result = 31 * result + (int) (lastSyncedId ^ (lastSyncedId >>> 32));
        return result;
    }

} @Entity
public class SauceUser {
    @Id private String userId;
    @Index private String email;
    private long lastSyncedId;
    private long lastSyncedTime;
    private String syncId;

    public SauceUser() {}
    public SauceUser(String userId, String email) {
        this.userId = userId;
        this.email = email;
    }

    public String getUserId() { return userId; }
    public String getEmail() { return email; }
    public String getSyncId() { return lastSyncedId + "." + lastSyncedTime; }
    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getLastSyncedId() { return lastSyncedId; }
    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getLastSyncedTime() { return lastSyncedTime; }

    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public Key<SauceUser> getKey() {
        return Key.create(SauceUser.class, userId);
    }

    public long incrementLastSyncedId(long lastSyncedTime) {
        this.lastSyncedTime = lastSyncedTime;
        lastSyncedId++;
        return lastSyncedId;
    }

    public long setLastSyncedTime(long lastSyncedTime) {
        this.lastSyncedTime = lastSyncedTime;
        return this.lastSyncedTime;
    }
} public class SyncConflictException extends ServiceException {
    private List<Conflict> conflicts = new ArrayList<Conflict>();

    public SyncConflictException(Exception e) {
        super(409, e);
    }

    public SyncConflictException setConflicts(List<Conflict> conflicts) {
        this.conflicts = conflicts;
        return this;
    }

    public List<Conflict> getConflicts() {
        return conflicts;
    }
} public class OAuthTokenInfoResponse {
    private String user_id;

    public String getUserId() {
        return user_id;
    }

    @Override
    public String toString() {
        return "OAuthTokenInfoResponse{" +
                "user_id='" + user_id + '\'' +
                '}';
    }
} public class QueryRequest {

    private List<Filter> filters = new ArrayList<Filter>();
    private long limit = 0;

    public List<Filter> getFilters() {
        return filters;
    }

    public long getLimit() {
        return limit;
    }

    //inject a type filter
    public void setType(String type) {
        if(type == null) {
            return;
        }
        if(filters == null) {
            filters = new ArrayList<Filter>();
        }

        Filter filter = new Filter();
        filter.field = "type";
        filter.operator = "=";
        filter.value = type;
        filters.add(filter);
    }

    public void setFilters(List<Filter> filters) {
        //add the indexed prefix to each filter before adding to the list.  the "Type" filter is unchanged
        for(Filter filter: filters) {
            filter.field = SauceEntity.indexedDataPrefix + '.' + filter.field;
        }

        if(this.filters == null) {
            this.filters = filters;
        } else {
            this.filters.addAll(filters);
        }
    }

    @Override
    public String toString() {
        return "QueryRequest{" +
                "filters=" + filters +
                ", limit=" + limit +
                '}';
    }
    public static class Filter {
        private String field;
        private String operator;
        private Object value;
        private boolean descending = false;

        public String getField() {
            return field;
        }

        public String getOperator() {
            return operator;
        }

        public Object getValue() {
            return value;
        }

        public boolean isDescending() {
            return descending;
        }

        @Override
        public String toString() {
            return "Filter{" +
                    "field='" + field + '\'' +
                    ", operator='" + operator + '\'' +
                    ", value='" + value + '\'' +
                    ", descending=" + descending +
                    '}';
        }
    }
} public class SaveRequest {
    private List<SauceEntity> objectsToSave;

    public List<SauceEntity> getObjectsToSave() { return objectsToSave; }

    public SyncRequest toSyncRequest() {
        if(objectsToSave == null || objectsToSave.isEmpty()) {
            throw new IllegalArgumentException("objectsToSave cant be null or empty");
        }
        SyncRequest syncRequest = new SyncRequest(0, System.currentTimeMillis(), objectsToSave, null, ConflictResolutionType.CLIENT_WINS);
        return syncRequest;
    }
} public class SyncRequest {
    private List<SauceEntity> objectsToSync;
    private Long lastSyncedId = 0l;
    private Long lastSyncedTime = 0l;
    private String syncId = "0.0";
    private ConflictResolutionType conflictResolutionType;
    private QueryRequest deltaQuery;
    private String senderId;

    private static final Logger log = Logger.getLogger(SyncRequest.class.getName());

    public SyncRequest() {}

    public SyncRequest(long lastSyncedId, long lastSyncedTime, List<SauceEntity> objectsToSync, QueryRequest deltaQuery, ConflictResolutionType conflictResolutionType) {
        this.objectsToSync = objectsToSync;
        this.lastSyncedId = lastSyncedId;
        this.lastSyncedTime = lastSyncedTime;
        this.conflictResolutionType = conflictResolutionType;
        this.deltaQuery = deltaQuery;
    }

    public SyncRequest(long lastSyncedId, long lastSyncedTime) {
        this.lastSyncedId = lastSyncedId;
        this.lastSyncedTime = lastSyncedTime;
    }

    public List<SauceEntity> getObjectsToSync() { return objectsToSync; }
    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getLastSyncedId() { return lastSyncedId; }
    @ApiResourceProperty(ignored = AnnotationBoolean.TRUE)
    public long getLastSyncedTime() { return lastSyncedTime; }
    public ConflictResolutionType getConflictResolutionType() { return conflictResolutionType; }
    public QueryRequest getDeltaQuery() { return deltaQuery; }
    public String getSyncId() { return syncId;}
    public void setSyncId(String syncId) {
        if(syncId == null) {
            syncId = "0.0";
        }
        int indexOfPeriod = syncId.indexOf('.');
        if(indexOfPeriod == -1) {
            throw new IllegalArgumentException("invalid syncId");
        }

        lastSyncedId = Long.valueOf(syncId.substring(0,indexOfPeriod));
        lastSyncedTime = Long.valueOf(syncId.substring(indexOfPeriod+1));
    }
    public String getSenderId() { return senderId; }
    public void setSenderId(String senderId) { this.senderId = senderId; }

    public void validateRequest() {
        List<String>  errorMessages = new ArrayList<String>();

        if(syncId == null) {
            errorMessages.add("syncId cant be null");
        }

        if(!errorMessages.isEmpty()){
            throw new IllegalArgumentException(errorMessages.toString());
        }

        //loop to check objectsToSync
        if(objectsToSync != null) {
            for(SauceEntity objectToSync: objectsToSync) {
                //
            }
        }

        //no more validation from here on, just trusted operations, since endpoints serializes empty as null
        //we can populate some defaults here
        if(conflictResolutionType == null) {
            conflictResolutionType = Constants.DEFAULT_CONFLICT_RESOLUTION_STRATEGY;
        }

        if(objectsToSync == null) {
            objectsToSync = new ArrayList<SauceEntity>();
        }

        return;
    }

} public class QueryResponse {
    private List<SauceEntity> results = new ArrayList<SauceEntity>();

    public QueryResponse(List<SauceEntity> results) {
        this.results = results;
    }

    public List<SauceEntity> getResults() {
        return results;
    }

} public class SaveResponse {
    private List<SauceEntity> savedEntities;

    public List<SauceEntity> getSavedEntities() { return savedEntities; }

    public static SaveResponse syncResponseToSavedResponse(SyncResponse syncResponse) {
        SaveResponse response = new SaveResponse();
        response.savedEntities = syncResponse.getSyncedEntities();
        return response;
    }


} public class SyncResponse {
    private List<SauceEntity> syncedEntities;
    private List<SauceEntity> syncedDelta;
    private List<Conflict> conflicts;
    private List<SauceEntity> tooFarOutOfSyncEntities;
    private String syncId;

    public List<SauceEntity> getSyncedEntities() { return syncedEntities; }
    public List<SauceEntity> getSyncedDelta() { return syncedDelta; }
    public List<SauceEntity> getTooFarOutOfSyncEntities() { return tooFarOutOfSyncEntities; }
    public List<Conflict> getConflicts() { return conflicts; }
    public String getSyncId()  { return syncId; }
    public boolean success() { return conflicts != null && !conflicts.isEmpty(); }

    private static final Logger log = Logger.getLogger(SyncResponse.class.getName());

    public static SyncResponse getConflictSyncResponse(List<Conflict> conflicts, String syncId) {
        SyncResponse response = new SyncResponse();
        response.conflicts = conflicts;
        response.syncId = syncId;
        return response;
    }

    public static SyncResponse getSuccessSyncResponse(List<SauceEntity> syncedEntities, List<SauceEntity> syncedDelta, String syncId) {
        SyncResponse response = new SyncResponse();
        response.syncedEntities = syncedEntities;
        response.syncedDelta = syncedDelta;
        response.syncId = syncId;
        return response;
    }

    public static SyncResponse getTooFarOutOfSyncResponse(List<SauceEntity> tooFarOutOfSyncEntities, String syncId) {
        SyncResponse response = new SyncResponse();
        response.tooFarOutOfSyncEntities = tooFarOutOfSyncEntities;
        response.syncId = syncId;

        log.info("tooFarOutOfSyncResponse:" + response);
        return response;
    }

    @Override
    public String toString() {
        return "SyncResponse{" +
                "syncedEntities=" + syncedEntities +
                ", syncedDelta=" + syncedDelta +
                ", conflicts=" + conflicts +
                ", tooFarOutOfSyncEntities=" + tooFarOutOfSyncEntities +
                ", syncId=" + syncId +
                '}';
    }
} public static Objectify ofy() {
    return ObjectifyService.ofy();
}
import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;
import com.google.appengine.api.oauth.OAuthRequestException;
import com.google.appengine.api.users.User;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.KeyRange;
import com.googlecode.objectify.Work;
import com.sauce.sync.conflicts.ConflictResolverFactory;
import com.sauce.sync.conflicts.Resolution;
import com.sauce.sync.models.Conflict;
import com.sauce.sync.models.Reference;
import com.sauce.sync.models.SauceEntity;
import com.sauce.sync.models.SauceUser;
import com.sauce.sync.exceptions.SyncConflictException;
import com.sauce.sync.json.OAuthTokenInfoResponse;
import com.sauce.sync.requests.QueryRequest;
import com.sauce.sync.requests.SaveRequest;
import com.sauce.sync.requests.SyncRequest;
import com.sauce.sync.responses.QueryResponse;
import com.sauce.sync.responses.SaveResponse;
import com.sauce.sync.responses.SyncResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import static com.sauce.sync.OfyService.ofy;
/*
   For step-by-step instructions on connecting your Android application to this backend module,
   see "App Engine Java Endpoints Module" template documentation at
   https://github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/HelloEndpoints
*/

package com.sauce.sync;





/**
 * google cloud endpoint Api class
 */
@Api(
        name = "sauce",
        description = "Sauce Sync lightweight BaaS, used to sync per-user data",
        documentationLink = "https://sauce-sync.appspot.com",
        version = "v1",
        namespace = @ApiNamespace(ownerDomain = "backend.sync.sauce.com", ownerName = "backend.sync.sauce.com", packagePath = ""),
        scopes = {
                Constants.EMAIL_SCOPE,
                Constants.PLUS_ME_SCOPE,
                Constants.PLUS_LOGIN_SCOPE
        },
        clientIds = {
                Constants.WEB_CLIENT_ID,
                Constants.ANDROID_CLIENT_ID,
                Constants.IOS_CLIENT_ID,
                com.google.api.server.spi.Constant.API_EXPLORER_CLIENT_ID // so you can use the google api explorer
        },
        audiences = {Constants.ANDROID_AUDIENCE}
)
public class SauceSyncEndpoint {

    private static final Logger log = Logger.getLogger(SauceSyncEndpoint.class.getName());

    @ApiMethod(name = "userInfo", path = "userinfo", httpMethod = ApiMethod.HttpMethod.GET)
    public SauceUser getUserInfo(User user, HttpServletRequest request) throws OAuthRequestException, IOException {
        final String userId = authenticate(user, request);
        SauceUser sauceUser = registerOrGetUser(userId, user.getEmail());
        return sauceUser;
    }

    @ApiMethod(name = "sync", path = "sync", httpMethod = ApiMethod.HttpMethod.POST)
    public SyncResponse sync(final SyncRequest syncRequest, final User user, final HttpServletRequest request) throws OAuthRequestException, SyncConflictException, IOException {
        final String userId = authenticate(user, request);
        syncRequest.validateRequest();


        //in order to make the transaction idempotent we generate the IDs outside of a transaction
        //this will also allow us to implement the 'refs' feature
        List<SauceEntity> entitiesThatNeedIds = new ArrayList<SauceEntity>();
        //first find all entities that are null and generateIds for them

        for(SauceEntity entity: syncRequest.getObjectsToSync()) {
            if(entity.getId() == 0) {
                entitiesThatNeedIds.add(entity);
            }
        }

        log.info("entitiesThatNeedIds:" + entitiesThatNeedIds);
        if(entitiesThatNeedIds.size()> 0) {
            KeyRange<SauceEntity> keys = ofy().factory().allocateIds(SauceEntity.class, entitiesThatNeedIds.size());
            Iterator<Key<SauceEntity>>  keysIterator = keys.iterator();
            for (SauceEntity entity : entitiesThatNeedIds) {
                entity.setId(keysIterator.next().getId());
            }
        }
        log.info("entities with ids now:" + entitiesThatNeedIds);

        SyncResponse syncResponse = ofy().transact(new Work<SyncResponse>() {
            public SyncResponse run() {

                //first check to see if we are too far out of sync
                SauceUser sauceUser = registerOrGetUser(userId, user.getEmail());
                if (!isUserInSync(sauceUser, syncRequest.getLastSyncedTime())) {
                    //the user is out of sync, they have to blow away their cache and resync

                    com.googlecode.objectify.cmd.Query<SauceEntity> outOfSyncQuery
                            = ofy().load().type(SauceEntity.class).ancestor(sauceUser.getKey());

                    //if you're selectively syncing, we have to only return the entities you're interested in
                    if (syncRequest.getDeltaQuery() != null) {
                        outOfSyncQuery = mutateObjectifyObjectBySyncQuery(outOfSyncQuery, syncRequest.getDeltaQuery().getFilters());
                    }

                    List<SauceEntity> outOfSyncEntities = outOfSyncQuery.list();

                    /*Lists.newArrayList will resolve the proxy by iterating through the proxy objects
                      if we don't do this we get a horrible serialization error which results in a 503 from GAE
                      https://groups.google.com/forum/#!topic/objectify-appengine/q61rUzwrufI */
                    return SyncResponse.getTooFarOutOfSyncResponse(Lists.newArrayList(outOfSyncEntities), sauceUser.getSyncId());
                }

                List<SauceEntity> entitiesToSave = null;

                //then check for conflits, if there are any return the conflict response
                if (!syncRequest.getObjectsToSync().isEmpty()) {
                    Optional<Resolution> conflictResponse = checkForConflicts(syncRequest);

                    //todo redo this
                    if (conflictResponse.isPresent() && !conflictResponse.get().resolved()) {
                        return SyncResponse.getConflictSyncResponse(conflictResponse.get().getUnresolvedConflicts(), sauceUser.getSyncId());
                    } else if (conflictResponse.isPresent()) {
                        entitiesToSave = conflictResponse.get().getResolvedConflicts();
                    } else {
                        entitiesToSave = syncRequest.getObjectsToSync();
                    }
                    log.info("entitiesToSave:" + entitiesToSave);
                    long newSyncedTime = System.currentTimeMillis();
                    long newLastSyncedId = sauceUser.incrementLastSyncedId(newSyncedTime);
                    //get and set new lastSyncedId
                    for (SauceEntity objectToSave : entitiesToSave) {
                        objectToSave.setLastSyncedId(newLastSyncedId);
                        objectToSave.setLastSyncedTime(newSyncedTime);
                        objectToSave.setUserKey(sauceUser.getKey());
                    }
                    //now that we have the user key and no conflicts we need resolve references
                    resolveSauceEntityReferences(entitiesToSave);

                    Map<Key<SauceEntity>, SauceEntity> entitiesStored = ofy().save().entities(entitiesToSave).now();
                    ofy().save().entities(sauceUser).now(); //save the updated sync id
                } else {
                    // sync only, not saving any entities. Carry on
                }


                com.googlecode.objectify.cmd.Query<SauceEntity> deltaQuery
                        = ofy().load().type(SauceEntity.class).ancestor(sauceUser.getKey());

                // selective sync feature
                if (syncRequest.getDeltaQuery() != null) {
                    deltaQuery = mutateObjectifyObjectBySyncQuery(deltaQuery, syncRequest.getDeltaQuery().getFilters());
                }

                deltaQuery = deltaQuery.filter(SauceEntity.lastSyncedIdColumn + " >", syncRequest.getLastSyncedId());

                List<SauceEntity> delta = deltaQuery.list();
                //queries are run frozen in time, so these entities might get returned from 'saved' and 'delta' responses
                //so we need to prune entities that came in the request.
                if (entitiesToSave != null) {
                    delta.removeAll(entitiesToSave);
                }
                log.info("delta: " + delta);
                /*Lists.newArrayList will resolve the proxy by iterating through the proxy objects
                  if we dont do this we get a horrible serialization error which results in a 503 from GAE
                  https://groups.google.com/forum/#!topic/objectify-appengine/q61rUzwrufI */
                SyncResponse response = SyncResponse.getSuccessSyncResponse(entitiesToSave, Lists.newArrayList(delta), sauceUser.getSyncId());
                return response;
            }
        });

        //no need to fire GCM on empty syncs
        if (!syncRequest.getObjectsToSync().isEmpty()) {
            Networker.sendGCMSync(userId, syncRequest.getSenderId());
        }
        return syncResponse;
    }

    @ApiMethod(name = "save", path = "save", httpMethod = ApiMethod.HttpMethod.POST)  //should never throw a sync conflict exception though
    public SaveResponse save(SaveRequest saveRequest, User user, HttpServletRequest request) throws OAuthRequestException, IOException, SyncConflictException {
        SyncResponse response = sync(saveRequest.toSyncRequest(), user, request);
        return SaveResponse.syncResponseToSavedResponse(response);
    }

    @ApiMethod(name = "query", path = "query", httpMethod = ApiMethod.HttpMethod.POST)
    public QueryResponse query(QueryRequest query, User user, HttpServletRequest request) throws OAuthRequestException, IOException {