All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.leancloud.AVObject Maven / Gradle / Ivy

package cn.leancloud;

import cn.leancloud.core.AppConfiguration;
import cn.leancloud.network.NetworkingDetector;
import cn.leancloud.ops.*;
import cn.leancloud.types.AVDate;
import cn.leancloud.types.AVGeoPoint;
import cn.leancloud.core.PaasClient;
import cn.leancloud.types.AVNull;
import cn.leancloud.utils.AVUtils;
import cn.leancloud.utils.LogUtil;
import cn.leancloud.utils.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
import io.reactivex.Observer;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import retrofit2.HttpException;

@JSONType(deserializer = ObjectTypeAdapter.class, serializer = ObjectTypeAdapter.class)
public class AVObject {
  public static final String KEY_CREATED_AT = "createdAt";
  public static final String KEY_UPDATED_AT = "updatedAt";
  public static final String KEY_OBJECT_ID = "objectId";
  public static final String KEY_ACL = "ACL";

  public static final String KEY_CLASSNAME = "className";

  private static final String INTERNAL_PATTERN = "^[\\da-z][\\d-a-z]*$";
  private static final Set RESERVED_ATTRS = new HashSet(
          Arrays.asList(KEY_CREATED_AT, KEY_UPDATED_AT, KEY_OBJECT_ID, KEY_ACL));

  protected static final AVLogger logger = LogUtil.getLogger(AVObject.class);
  protected static final int UUID_LEN = UUID.randomUUID().toString().length();

  protected String className;
  protected String endpointClassName = null;

  protected String objectId = "";
  protected ConcurrentMap serverData = new ConcurrentHashMap();
  protected ConcurrentMap operations = new ConcurrentHashMap();
  protected AVACL acl = null;
  private String uuid = null;

  @JSONField(serialize = false)
  private volatile boolean fetchWhenSave = false;
  protected volatile boolean totallyOverwrite = false;

  public AVObject() {
    this.className = Transformer.getSubClassName(this.getClass());
  }

  public AVObject(String className) {
    Transformer.checkClassName(className);
    this.className = className;
  }

  public AVObject(AVObject other) {
    this.className = other.className;
    this.objectId = other.objectId;
    this.serverData.putAll(other.serverData);
    this.operations.putAll(other.operations);
    this.acl = other.acl;
    this.endpointClassName = other.endpointClassName;
  }

  public String getClassName() {
    return this.className;
  }
  public String internalClassName() {
    return this.getClassName();
  }
  public void setClassName(String name) {
    Transformer.checkClassName(name);
    this.className = name;
  }

  public String getCreatedAt() {
    return (String) this.serverData.get(KEY_CREATED_AT);
  }

  public String getUpdatedAt() {
    return (String) this.serverData.get(KEY_UPDATED_AT);
  }

  public String getObjectId() {
    if (this.serverData.containsKey(KEY_OBJECT_ID)) {
      return (String) this.serverData.get(KEY_OBJECT_ID);
    } else {
      return this.objectId;
    }
  }

  public void setObjectId(String objectId) {
    this.objectId = objectId;
    if (null != this.serverData && !StringUtil.isEmpty(objectId)) {
      this.serverData.put(KEY_OBJECT_ID, objectId);
    }
  }

  public boolean isFetchWhenSave() {
    return fetchWhenSave;
  }

  public void setFetchWhenSave(boolean fetchWhenSave) {
    this.fetchWhenSave = fetchWhenSave;
  }

  // Caution: public this method just for compatibility.
  public String getUuid() {
    if (StringUtil.isEmpty(this.uuid)) {
      this.uuid = UUID.randomUUID().toString().toLowerCase();
    }
    return this.uuid;
  }

  void setUuid(String uuid) {
    this.uuid = uuid;
  }

  protected static boolean verifyInternalId(String internalId) {
    return Pattern.matches(INTERNAL_PATTERN, internalId);
  }

  protected String internalId() {
    return StringUtil.isEmpty(getObjectId()) ? getUuid() : getObjectId();
  }

  /**
   * getter
   */
  public boolean containsKey(String key) {
    return serverData.containsKey(key);
  }

  public boolean has(String key) {
    return (this.get(key) != null);
  }

  public Object get(String key) {
    Object value = serverData.get(key);
    ObjectFieldOperation op = operations.get(key);
    if (null != op) {
      value = op.apply(value);
    }
    return value;
  }

  public boolean getBoolean(String key) {
    Boolean b = (Boolean) get(key);
    return b == null ? false : b;
  }

  public byte[] getBytes(String key) {
    return (byte[]) (get(key));
  }

  public Date getDate(String key) {
    Object res = get(key);
    if (res instanceof Date) {
      return (Date)res;
    }
    JSONObject rawData = (JSONObject) get(key);
    if (null == rawData) {
      return null;
    }
    AVDate date = new AVDate((JSONObject) get(key));
    return date.getDate();
  }

  public String getString(String key) {
    Object obj = get(key);
    if (obj instanceof String)
      return (String) obj;
    else
      return null;
  }

  public int getInt(String key) {
    Number v = (Number) get(key);
    if (v != null) return v.intValue();
    return 0;
  }

  public long getLong(String key) {
    Number v = (Number) get(key);
    if (v != null) return v.longValue();
    return 0l;
  }

  public double getDouble(String key) {
    Number number = (Number) get(key);
    if (number != null) return number.doubleValue();
    return 0f;
  }

  public Number getNumber(String key) {
    return (Number) get(key);
  }

  public JSONArray getJSONArray(String key) {
    Object list = get(key);
    if (list == null) {
      return null;
    }
    if (list instanceof JSONArray) {
      return (JSONArray) list;
    }
    if (list instanceof List) {
      return new JSONArray((List) list);
    }
    if (list instanceof Object[]) {
      JSONArray array = new JSONArray();
      for (Object obj : (Object[]) list) {
        array.add(obj);
      }
      return array;
    }
    return null;
  }

  public JSONObject getJSONObject(String key) {
    Object object = get(key);
    if (object instanceof JSONObject) {
      return (JSONObject) object;
    }
    String jsonString = JSON.toJSONString(object);
    JSONObject jsonObject = null;
    try {
      jsonObject = JSON.parseObject(jsonString);
    } catch (Exception exception) {
      throw new IllegalStateException("Invalid json string", exception);
    }
    return jsonObject;
  }

  public AVGeoPoint getAVGeoPoint(String key) {
    return (AVGeoPoint) get(key);
  }

  public AVFile getAVFile(String key) {
    return (AVFile) get(key);
  }

  public  T getAVObject(String key) {
    try {
      return (T) get(key);
    } catch (Exception ex) {
      logger.w("failed to convert Object.", ex);
      return null;
    }
  }

  public  AVRelation getRelation(String key) {
    validFieldName(key);
    Object object = get(key);
    if (object instanceof AVRelation) {
      ((AVRelation)object).setParent(this);
      ((AVRelation)object).setKey(key);
      return (AVRelation)object;
    } else {
      return new AVRelation<>(this, key);
    }
  }

  void addRelation(final AVObject object, final String key) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.AddRelation, key, object);
    addNewOperation(op);
  }

  void removeRelation(final AVObject object, final String key) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.RemoveRelation, key, object);
    addNewOperation(op);
  }

  public List getList(String key) {
    return (List) get(key);
  }

  public ConcurrentMap getServerData() {
    return this.serverData;
  }

  protected void validFieldName(String key) {
    if (StringUtil.isEmpty(key)) {
      throw new IllegalArgumentException("Blank key");
    }
    if (key.startsWith("_")) {
      throw new IllegalArgumentException("key should not start with '_'");
    }
    if (RESERVED_ATTRS.contains(key)) {
      throw new IllegalArgumentException("key(" + key + ") is reserved by LeanCloud");
    }
  }

  @JSONField(serialize = false)
  public boolean isDataAvailable() {
    return !StringUtil.isEmpty(this.objectId) && !this.serverData.isEmpty();
  }

  /**
   * changable operations.
   */
  public void add(String key, Object value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Add, key, value);
    addNewOperation(op);
  }
  public void addAll(String key, Collection values) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Add, key, values);
    addNewOperation(op);
  }

  public void addUnique(String key, Object value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.AddUnique, key, value);
    addNewOperation(op);
  }
  public void addAllUnique(String key, Collection values) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.AddUnique, key, values);
    addNewOperation(op);
  }

  public void put(String key, Object value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Set, key, value);
    addNewOperation(op);
  }

  public void remove(String key) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Delete, key, null);
    addNewOperation(op);
  }

  public void removeAll(String key, Collection values) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Remove, key, values);
    addNewOperation(op);
  }

  public void increment(String key) {
    this.increment(key, 1);
  }
  public void increment(String key, Number value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Increment, key, value);
    addNewOperation(op);
  }

  public void decrement(String key) {
    decrement(key, 1);
  }
  public void decrement(String key, Number value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Decrement, key, value);
    addNewOperation(op);
  }

  public void bitAnd(String key, long value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.BitAnd, key, value);
    addNewOperation(op);
  }
  public void bitOr(String key, long value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.BitOr, key, value);
    addNewOperation(op);
  }
  public void bitXor(String key, long value) {
    validFieldName(key);
    ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.BitXor, key, value);
    addNewOperation(op);
  }

  /**
   * abort all modify operations.
   * @Notice: this method doesn't work for AVInstallation.
   */
  public void abortOperations() {
    if (totallyOverwrite) {
      logger.w("Can't abort modify operations under TotalOverWrite mode.");
    }
    this.operations.clear();
  }

  protected void addNewOperation(ObjectFieldOperation op) {
    if (null == op) {
      return;
    }
    if (totallyOverwrite) {
      if ("Delete".equalsIgnoreCase(op.getOperation())) {
        this.serverData.remove(op.getField());
      } else {
        Object oldValue = this.serverData.get(op.getField());
        Object newValue = op.apply(oldValue);
        if (null == newValue) {
          this.serverData.remove(op.getField());
        } else {
          this.serverData.put(op.getField(), newValue);
        }
      }
    } else {
      ObjectFieldOperation previous = null;
      if (this.operations.containsKey(op.getField())) {
        previous = this.operations.get(op.getField());
      }
      this.operations.put(op.getField(), op.merge(previous));
    }
  }

  private boolean needBatchMode() {
    for (ObjectFieldOperation op : this.operations.values()) {
      if (op instanceof CompoundOperation) {
        return true;
      }
    }
    return false;
  }

  /**
   * save/update with server.
   */
  protected JSONObject generateChangedParam() {
    if (totallyOverwrite) {
      HashMap tmp = new HashMap<>();
      tmp.putAll(this.serverData);

      // createdAt, updatedAt, objectId is immutable.
      tmp.remove(KEY_CREATED_AT);
      tmp.remove(KEY_UPDATED_AT);
      tmp.remove(KEY_OBJECT_ID);
      return new JSONObject(tmp);
    }

    Map params = new HashMap();
    Set> entries = operations.entrySet();
    for (Map.Entry entry: entries) {
      //{"attr":{"__op":"Add", "objects":[obj1, obj2]}}
      Map oneOp = entry.getValue().encode();
      params.putAll(oneOp);
    }

    if (null != this.acl) {
      AVACL serverACL = generateACLFromServerData();
      if (!this.acl.equals(serverACL)) {
        // only append acl request when modified.
        ObjectFieldOperation op = OperationBuilder.gBuilder.create(OperationBuilder.OperationType.Set, KEY_ACL, acl);
        params.putAll(op.encode());
      }
    }

    if (!needBatchMode()) {
      return new JSONObject(params);
    }

    List> finalParams = new ArrayList>();
    Map topParams = Utils.makeCompletedRequest(getObjectId(), getRequestRawEndpoint(), getRequestMethod(), params);
    if (null != topParams) {
      finalParams.add(topParams);
    }

    for (ObjectFieldOperation ops : this.operations.values()) {
      if (ops instanceof CompoundOperation) {
        List> restParams = ((CompoundOperation)ops).encodeRestOp(this);
        if (null != restParams && !restParams.isEmpty()) {
          finalParams.addAll(restParams);
        }
      }
    }
    Map finalResult = new HashMap(1);
    finalResult.put("requests", finalParams);

    return new JSONObject(finalResult);
  }

  protected List extractCascadingObjects(Object o) {
    List result = new ArrayList<>();
    if (o instanceof AVObject && StringUtil.isEmpty(((AVObject)o).getObjectId())) {
      result.add((AVObject) o);
    } else if (o instanceof Collection) {
      for (Object secondO: ((Collection)o).toArray()) {
        List tmp = extractCascadingObjects(secondO);
        if (null != tmp && !tmp.isEmpty()) {
          result.addAll(tmp);
        }
      }
    }
    return result;
  }

  protected Observable> getCascadingSaveObjects() {
    List result = new ArrayList<>();
    for (ObjectFieldOperation ofo: operations.values()) {
      List operationValues = extractCascadingObjects(ofo.getValue());
      if (null != operationValues && !operationValues.isEmpty()) {
        result.addAll(operationValues);
      }
    }
    return Observable.just(result).subscribeOn(Schedulers.io());
  }

  protected void onSaveSuccess() {
    this.operations.clear();
  }

  protected void onSaveFailure() {
  }

  protected void onDataSynchronized() {
  }

  private Observable saveSelfOperations(AVSaveOption option) {
    final boolean needFetch = (null != option) ? option.fetchWhenSave : isFetchWhenSave();

    if (null != option && null != option.matchQuery) {
      String currentClass = getClassName();
      if (!StringUtil.isEmpty(currentClass) && !currentClass.equals(option.matchQuery.getClassName())) {
        return Observable.error(new AVException(0, "AVObject class inconsistant with AVQuery in AVSaveOption"));
      }
    }

    final JSONObject paramData = generateChangedParam();
    logger.i("saveObject param: " + paramData.toJSONString());

    final String currentObjectId = getObjectId();

    if (needBatchMode()) {
      logger.w("Caution: batch mode will ignore fetchWhenSave flag and matchQuery.");
      if (StringUtil.isEmpty(currentObjectId)) {
        logger.d("request payload: " + paramData.toJSONString());
        return PaasClient.getStorageClient().batchSave(paramData).map(new Function() {
          public AVObject apply(JSONArray object) throws Exception {
            if (null != object && !object.isEmpty()) {
              logger.d("batchSave result: " + object.toJSONString());

              Map lastResult = object.getObject(object.size() - 1, Map.class);
              if (null != lastResult) {
                AVUtils.mergeConcurrentMap(serverData, lastResult);
                AVObject.this.onSaveSuccess();
              }
            }
            return AVObject.this;
          }
        });
      } else {
        return PaasClient.getStorageClient().batchUpdate(paramData).map(new Function() {
          public AVObject apply(JSONObject object) throws Exception {
            if (null != object) {
              logger.d("batchUpdate result: " + object.toJSONString());
              Map lastResult = object.getObject(currentObjectId, Map.class);
              if (null != lastResult) {
                AVUtils.mergeConcurrentMap(serverData, lastResult);
                AVObject.this.onSaveSuccess();
              }
            }
            return AVObject.this;
          }
        });
      }
    } else {
      JSONObject whereCondition = null;
      if (null != option && null != option.matchQuery) {
        Map whereOperationMap = option.matchQuery.conditions.compileWhereOperationMap();
        whereCondition = new JSONObject(whereOperationMap);
      }
      if (totallyOverwrite) {
        return PaasClient.getStorageClient().saveWholeObject(this.getClass(), endpointClassName, currentObjectId,
                paramData, needFetch, whereCondition);
      } else if (StringUtil.isEmpty(currentObjectId)) {
        return PaasClient.getStorageClient().createObject(this.className, paramData, needFetch, whereCondition)
                .map(new Function() {
                  @Override
                  public AVObject apply(AVObject avObject) throws Exception {
                    AVObject.this.mergeRawData(avObject);
                    AVObject.this.onSaveSuccess();
                    return AVObject.this;
                  }
                });
      } else {
        return PaasClient.getStorageClient().saveObject(this.className, getObjectId(), paramData, needFetch, whereCondition)
                .map(new Function() {
                  @Override
                  public AVObject apply(AVObject avObject) throws Exception {
                    AVObject.this.mergeRawData(avObject);
                    AVObject.this.onSaveSuccess();
                    return AVObject.this;
                  }
                });
      }
    }
  }

  public Observable saveInBackground() {
    return saveInBackground(null);
  }

  public Observable saveInBackground(final AVSaveOption option) {
    Map markMap = new HashMap<>();
    if (hasCircleReference(markMap)) {
      return Observable.error(new AVException(AVException.CIRCLE_REFERENCE, "Found a circular dependency when saving."));
    }

    Observable> needSaveFirstly = getCascadingSaveObjects();
    return needSaveFirstly.flatMap(new Function, Observable>() {
      @Override
      public Observable apply(List objects) throws Exception {
        logger.d("First, try to execute save operations in thread: " + Thread.currentThread());
        for (AVObject o: objects) {
          o.save();
        }
        logger.d("Second, save object itself...");
        return saveSelfOperations(option);
      }
    });
  }

  /**
   * judge operations' value include circle reference or not.
   *
   * notice: internal used, pls not invoke it.
   *
   * @param markMap
   * @return
   */
  public boolean hasCircleReference(Map markMap) {
    if (null == markMap) {
      return false;
    }
    markMap.put(this, true);
    boolean rst = false;
    for (ObjectFieldOperation op: operations.values()) {
      rst = rst || op.checkCircleReference(markMap);
    }
    return rst;
  }

  public void save() {
    saveInBackground().blockingSubscribe();
  }

  public static void saveAll(Collection objects) throws AVException {
    saveAllInBackground(objects).blockingSubscribe();
  }

  public static Observable saveAllInBackground(final Collection objects) {
    if (null == objects || objects.isEmpty()) {
      JSONArray emptyResult = new JSONArray();
      return Observable.just(emptyResult);
    }
    JSONArray requests = new JSONArray();
    for (AVObject o : objects) {
      Map markMap = new HashMap<>();
      if (o.hasCircleReference(markMap)) {
        return Observable.error(new AVException(AVException.CIRCLE_REFERENCE, "Found a circular dependency when saving."));
      }
      JSONObject requestBody = o.generateChangedParam();
      JSONObject objectRequest = new JSONObject();
      objectRequest.put("method", o.getRequestMethod());
      objectRequest.put("path", o.getRequestRawEndpoint());
      objectRequest.put("body", requestBody);
      requests.add(objectRequest);
    }

    JSONObject requestTotal = new JSONObject();
    requestTotal.put("requests", requests);
    return PaasClient.getStorageClient().batchSave(requestTotal).map(new Function() {
      public JSONArray apply(JSONArray batchResults) throws Exception {

        if (null != batchResults && (objects.size() == batchResults.size())) {
          logger.d("batchSave result: " + batchResults.toJSONString());
          Iterator it = objects.iterator();

          for (int i = 0; i < batchResults.size() && it.hasNext(); i++) {
            JSONObject oneResult = batchResults.getJSONObject(i);
            AVObject originObject = (AVObject) it.next();
            if (oneResult.containsKey("success")) {
              AVUtils.mergeConcurrentMap(originObject.serverData, oneResult.getJSONObject("success"));
              originObject.onSaveSuccess();
            } else if (oneResult.containsKey("error")) {
              originObject.onSaveFailure();
            }
          }
        }
        return batchResults;
      }
    });
  }

  public void saveEventually() throws AVException {
    if (operations.isEmpty()) {
      return;
    }
    Map markMap = new HashMap<>();
    if (hasCircleReference(markMap)) {
      throw new AVException(AVException.CIRCLE_REFERENCE, "Found a circular dependency when saving.");
    }

    NetworkingDetector detector = AppConfiguration.getGlobalNetworkingDetector();
    if (null != detector && detector.isConnected()) {
      // network is fine, try to save object;
      this.saveInBackground().subscribe(new Observer() {
        @Override
        public void onSubscribe(Disposable disposable) {

        }

        @Override
        public void onNext(AVObject avObject) {
          logger.d("succeed to save directly");
        }

        @Override
        public void onError(Throwable throwable) {
          // failed, save data to local file first;
          add2ArchivedRequest(false);
        }

        @Override
        public void onComplete() {

        }
      });
    } else {
      // network down, save data to local file first;
      add2ArchivedRequest(false);
    }
  }
  private void add2ArchivedRequest(boolean isDelete) {
    ArchivedRequests requests = ArchivedRequests.getInstance();
    if (isDelete) {
      requests.deleteEventually(this);
    } else {
      requests.saveEventually(this);
    }
  }

  public void deleteEventually() {
    String objectId  = getObjectId();
    if (StringUtil.isEmpty(objectId)) {
      logger.w("objectId is empty, you couldn't delete a persistent object.");
      return;
    }
    NetworkingDetector detector = AppConfiguration.getGlobalNetworkingDetector();
    if (null != detector && detector.isConnected()) {
      this.deleteInBackground().subscribe(new Observer() {
        @Override
        public void onSubscribe(Disposable disposable) {

        }

        @Override
        public void onNext(AVNull avNull) {
          logger.d("succeed to delete directly.");
        }

        @Override
        public void onError(Throwable throwable) {
          add2ArchivedRequest(true);
        }

        @Override
        public void onComplete() {

        }
      });
    } else {
      add2ArchivedRequest(true);
    }
  }
  public Observable deleteInBackground() {
    if (totallyOverwrite) {
      return PaasClient.getStorageClient().deleteWholeObject(this.endpointClassName, getObjectId());
    }
    return PaasClient.getStorageClient().deleteObject(this.className, getObjectId());
  }

  public void delete() {
    deleteInBackground().blockingSubscribe();
  }

  public static void deleteAll(Collection objects) throws AVException {
    deleteAllInBackground(objects).blockingSubscribe();
  }

  public static Observable deleteAllInBackground(Collection objects) {
    if (null == objects || objects.isEmpty()) {
      return Observable.just(AVNull.getINSTANCE());
    }
    String className = null;
    StringBuilder sb = new StringBuilder();
    for (AVObject o : objects) {
      if (StringUtil.isEmpty(o.getObjectId()) || StringUtil.isEmpty(o.getClassName())) {
        return Observable.error(new IllegalArgumentException("Invalid AVObject, the class name or objectId is blank."));
      }
      if (className == null) {
        className = o.getClassName();
        sb.append(o.getObjectId());
      } else if (className.equals(o.getClassName())) {
        sb.append(",").append(o.getObjectId());
      } else {
        return Observable.error(new IllegalArgumentException("The objects class name must be the same."));
      }
    }
    return PaasClient.getStorageClient().deleteObject(className, sb.toString());
  }

  public void refresh() {
    refresh(null);
  }

  public void refresh(String includeKeys) {
    refreshInBackground(includeKeys).blockingSubscribe();
  }

  public Observable refreshInBackground() {
    return refreshInBackground(null);

  }

  public Observable refreshInBackground(final String includeKeys) {
    if (totallyOverwrite) {
      return PaasClient.getStorageClient().getWholeObject(this.endpointClassName, getObjectId())
              .map(new Function() {
                @Override
                public AVObject apply(AVObject avObject) throws Exception {
                  AVObject.this.serverData.clear();
                  AVObject.this.serverData.putAll(avObject.serverData);
                  AVObject.this.onDataSynchronized();
                  return AVObject.this;
                }
              });
    }
    return PaasClient.getStorageClient().fetchObject(this.className, getObjectId(), includeKeys)
            .map(new Function() {
              public AVObject apply(AVObject avObject) throws Exception {
                if (StringUtil.isEmpty(includeKeys)) {
                  if (className.equals(AVUser.CLASS_NAME) || AVObject.this instanceof AVUser) {
                    Object userSessionToken = AVObject.this.serverData.get(AVUser.ATTR_SESSION_TOKEN);
                    AVObject.this.serverData.clear();
                    if (null != userSessionToken){
                      AVObject.this.serverData.put(AVUser.ATTR_SESSION_TOKEN, userSessionToken);
                    }
                  } else {
                    AVObject.this.serverData.clear();
                  }
                }
                AVObject.this.serverData.putAll(avObject.serverData);
                AVObject.this.onDataSynchronized();
                return AVObject.this;
              }
            });
  }

  public AVObject fetch() {
    return fetch(null);
  }
  public AVObject fetch(String includeKeys) {
    refresh(includeKeys);
    return this;
  }
  public Observable fetchInBackground() {
    return refreshInBackground();
  }
  public Observable fetchInBackground(String includeKyes) {
    return refreshInBackground(includeKyes);
  }

  public Observable fetchIfNeededInBackground() {
    if (!StringUtil.isEmpty(getObjectId()) && this.serverData.size() > 1) {
      return Observable.just(this);
    } else {
      return refreshInBackground();
    }
  }

  public AVObject fetchIfNeeded() {
    fetchIfNeededInBackground().blockingSubscribe();
    return this;
  }

  protected void resetAll() {
    this.objectId = "";
    this.acl = null;
    this.serverData.clear();
    this.operations.clear();
  }

  protected void resetByRawData(AVObject avObject) {
    resetAll();
    if (null != avObject) {
      this.serverData.putAll(avObject.serverData);
      this.operations.putAll(avObject.operations);
    }
  }

  void mergeRawData(AVObject avObject) {
    if (null != avObject) {
      this.serverData.putAll(avObject.serverData);
    }
    this.operations.clear();
  }

  public void resetServerData(Map data) {
    this.serverData.clear();
    AVUtils.mergeConcurrentMap(this.serverData, data);
    this.operations.clear();
  }

  @JSONField(serialize = false)
  public String getRequestRawEndpoint() {
    if (StringUtil.isEmpty(getObjectId())) {
      return "/1.1/classes/" + this.getClassName();
    } else {
      return "/1.1/classes/" + this.getClassName() + "/" + getObjectId();
    }
  }

  @JSONField(serialize = false)
  public String getRequestMethod() {
    if (StringUtil.isEmpty(getObjectId())) {
      return "POST";
    } else {
      return "PUT";
    }
  }

  /**
   * Register subclass to AVOSCloud SDK.It must be invocated before AVOSCloud.initialize.
   *
   * @param clazz The subclass.
   * @since 1.3.6
   */
  public static  void registerSubclass(Class clazz) {
    Transformer.registerClass(clazz);
  }

  /**
   * ACL
   */
  public synchronized AVACL getACL() {
    if (null == this.acl) {
      this.acl = generateACLFromServerData();
    }
    return this.acl;
  }

  public synchronized void setACL(AVACL acl) {
    this.acl = acl;
  }

  protected AVACL generateACLFromServerData() {
    if (!this.serverData.containsKey(KEY_ACL)) {
      return new AVACL();
    } else {
      Object aclMap = this.serverData.get(KEY_ACL);
      if (aclMap instanceof HashMap) {
        return new AVACL((HashMap) aclMap);
      } else {
        return new AVACL();
      }
    }
  }

  public static  AVQuery getQuery(Class clazz) {
    return new AVQuery(Transformer.getSubClassName(clazz), clazz);
  }

  /**
   * common methods.
   */
  /**
   * generate a new json object with server data.
   * @return
   */
  public JSONObject toJSONObject() {
    return new JSONObject(this.serverData);
  }

  /**
   * generate a json string.
   * @return
   */
  public String toJSONString() {
    return JSON.toJSONString(this, ObjectValueFilter.instance,
          SerializerFeature.WriteClassName,
          SerializerFeature.DisableCircularReferenceDetect);
  }

  /**
   * create AVObject instance from json string which generated by AVObject.toString or AVObject.toJSONString.
   *
   * @param objectString
   * @return null if objectString is null
   */
  public static AVObject parseAVObject(String objectString) {
    if (StringUtil.isEmpty(objectString)) {
      return null;
    }
    // replace leading type name to compatible with v4.x android sdk serialized json string.
    objectString = objectString.replaceAll("^\\{\\s*\"@type\":\\s*\"[A-Za-z\\.]+\",", "{");
//    objectString = objectString.replaceAll("^\\{\\s*\"@type\":\\s*\"cn.leancloud.AV(Object|Installation|User|Status|Role|File)\",", "{");

    // replace old AVObject type name.
    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.AVObject\",", "\"@type\":\"cn.leancloud.AVObject\",");
    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.AVInstallation\",", "\"@type\":\"cn.leancloud.AVInstallation\",");
    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.AVUser\",", "\"@type\":\"cn.leancloud.AVUser\",");
    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.AVStatus\",", "\"@type\":\"cn.leancloud.AVStatus\",");
    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.AVRole\",", "\"@type\":\"cn.leancloud.AVRole\",");
    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.AVFile\",", "\"@type\":\"cn.leancloud.AVFile\",");

    objectString = objectString.replaceAll("\"@type\":\\s*\"com.avos.avoscloud.ops.[A-Za-z]+Op\",", "");

    return JSON.parseObject(objectString, AVObject.class, Feature.SupportAutoType);
  }

  /**
   * create a new instance with particular classname and objectId.
   * @param className class name
   * @param objectId  object id
   * @return
   */
  public static AVObject createWithoutData(String className, String objectId) {
    AVObject object = new AVObject(className);
    object.setObjectId(objectId);
    return object;
  }

  /**
   * create a new instance with particular class and objectId.
   * @param clazz     class info
   * @param objectId  object id
   * @param 
   * @return
   * @throws AVException
   */
  public static  T createWithoutData(Class clazz, String objectId) throws AVException{
    try {
      T obj = clazz.newInstance();
      obj.setClassName(Transformer.getSubClassName(clazz));
      obj.setObjectId(objectId);
      return obj;
    } catch (Exception ex) {
      throw new AVException(ex);
    }
  }

  protected static  T cast(AVObject object, Class clazz) throws Exception {
    if (clazz.getClass().isAssignableFrom(object.getClass())) {
      return (T) object;
    } else {
      T newItem = clazz.newInstance();
      newItem.className = object.className;
      newItem.objectId = object.objectId;
      newItem.serverData.putAll(object.serverData);
      newItem.operations.putAll(object.operations);
      newItem.acl = object.acl;
      newItem.endpointClassName = object.endpointClassName;
      return newItem;
    }
  }

  @Override
  public String toString() {
    return toJSONString();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof AVObject)) return false;
    AVObject avObject = (AVObject) o;
    return isFetchWhenSave() == avObject.isFetchWhenSave() &&
            Objects.equals(getClassName(), avObject.getClassName()) &&
            Objects.equals(getServerData(), avObject.getServerData()) &&
            Objects.equals(operations, avObject.operations) &&
            Objects.equals(acl, avObject.acl);
  }

  @Override
  public int hashCode() {

    return Objects.hash(getClassName(), getServerData(), operations, acl, isFetchWhenSave());
  }
}