Esempio n. 1
0
  @Override
  public int compareTo(E that) {
    if (that == null) throw new NullPointerException("You cannot compare with null");

    if (this.equals(that)) return 0;

    String thisId = this.getId();
    String thatId = that.getId();

    if (thisId == null) return -1;
    if (thatId == null) return +1;

    int ret = NaturalOrderComparator.get().compare(thisId, thatId);

    // The id's may be the same if these are objects from different collections
    // We avoid zero in an ugly way like this.
    // TODO: Improve by comparing collections and then databases.
    if (ret == 0) {
      ret = -1;
    }

    return ret;
  }
Esempio n. 2
0
public class Coll<E> implements CollInterface<E> {
  // -------------------------------------------- //
  // GLOBAL REGISTRY
  // -------------------------------------------- //

  public static final String TOTAL = "*total*";

  // All instances registered here are considered inited.
  private static Map<String, Coll<?>> name2instance =
      new ConcurrentSkipListMap<String, Coll<?>>(NaturalOrderComparator.get());

  private static Map<String, Coll<?>> umap = Collections.unmodifiableMap(name2instance);
  private static Set<String> unames = Collections.unmodifiableSet(name2instance.keySet());
  private static Collection<Coll<?>> uinstances =
      Collections.unmodifiableCollection(name2instance.values());

  public static Map<String, Coll<?>> getMap() {
    return umap;
  }

  public static Set<String> getNames() {
    return unames;
  }

  public static Collection<Coll<?>> getInstances() {
    return uinstances;
  }

  // -------------------------------------------- //
  // WHAT DO WE HANDLE?
  // -------------------------------------------- //

  protected final String name;

  @Override
  public String getName() {
    return this.name;
  }

  protected final String basename;

  @Override
  public String getBasename() {
    return this.basename;
  }

  protected final String universe;

  @Override
  public String getUniverse() {
    return this.universe;
  }

  protected final Class<E> entityClass;

  @Override
  public Class<E> getEntityClass() {
    return this.entityClass;
  }

  // -------------------------------------------- //
  // SUPPORTING SYSTEM
  // -------------------------------------------- //

  protected Plugin plugin;

  @Override
  public Plugin getPlugin() {
    return this.plugin;
  }

  public Gson getGson() {
    if (this.getPlugin() instanceof MPlugin) {
      return ((MPlugin) this.getPlugin()).gson;
    } else {
      return MCore.gson;
    }
  }

  protected Db db;

  @Override
  public Db getDb() {
    return this.db;
  }

  @Override
  public Driver getDriver() {
    return this.db.getDriver();
  }

  protected Object collDriverObject;

  @Override
  public Object getCollDriverObject() {
    return this.collDriverObject;
  }

  // -------------------------------------------- //
  // STORAGE
  // -------------------------------------------- //

  // All
  protected Set<String> ids;

  // Loaded
  protected Map<String, E> id2entity;
  protected Map<E, String> entity2id;

  @Override
  public Collection<String> getIds() {
    return Collections.unmodifiableCollection(this.ids);
  }

  @Override
  public Map<String, E> getId2entity() {
    return Collections.unmodifiableMap(this.id2entity);
  }

  @Override
  public E get(Object oid) {
    return this.get(oid, this.isCreative());
  }

  @Override
  public E get(Object oid, boolean creative) {
    return this.get(oid, creative, true);
  }

  protected E get(Object oid, boolean creative, boolean noteChange) {
    String id = this.fixId(oid);
    if (id == null) return null;
    E ret = this.id2entity.get(id);
    if (ret != null) return ret;
    if (!creative) return null;
    return this.create(id, noteChange);
  }

  @Override
  public Collection<String> getIdsLoaded() {
    return Collections.unmodifiableCollection(this.id2entity.keySet());
  }

  @Override
  public Collection<String> getIdsRemote() {
    return this.getDb().getDriver().getIds(this);
  }

  @Override
  public boolean containsId(Object oid) {
    String id = this.fixId(oid);
    if (id == null) return false;
    return this.ids.contains(id);
  }

  @Override
  public Map<E, String> getEntity2id() {
    return Collections.unmodifiableMap(this.entity2id);
  }

  @Override
  public String getId(Object entity) {
    return this.entity2id.get(entity);
  }

  @Override
  public boolean containsEntity(Object entity) {
    return this.entity2id.containsKey(entity);
  };

  @Override
  public Collection<E> getAll() {

    return Collections.unmodifiableCollection(this.entity2id.keySet());
  }

  @Override
  public Collection<E> getAll(Predictate<? super E> where) {
    return MStoreUtil.uglySQL(this.getAll(), where, null, null, null);
  }

  @Override
  public Collection<E> getAll(Predictate<? super E> where, Comparator<? super E> orderby) {
    return MStoreUtil.uglySQL(this.getAll(), where, orderby, null, null);
  }

  @Override
  public Collection<E> getAll(
      Predictate<? super E> where, Comparator<? super E> orderby, Integer limit) {
    return MStoreUtil.uglySQL(this.getAll(), where, orderby, limit, null);
  }

  @Override
  public Collection<E> getAll(
      Predictate<? super E> where, Comparator<? super E> orderby, Integer limit, Integer offset) {
    return MStoreUtil.uglySQL(this.getAll(), where, orderby, limit, offset);
  }

  @Override
  public String fixId(Object oid) {
    if (oid == null) return null;

    String ret = null;
    if (oid instanceof String) {
      ret = (String) oid;
    } else if (oid.getClass() == this.entityClass) {
      ret = this.entity2id.get(oid);
    }
    if (ret == null) return null;

    return this.isLowercasing() ? ret.toLowerCase() : ret;
  }

  // -------------------------------------------- //
  // BEHAVIOR
  // -------------------------------------------- //

  protected boolean lazy;

  @Override
  public boolean isLazy() {
    return this.lazy;
  }

  @Override
  public void setLazy(boolean lazy) {
    this.lazy = lazy;
  }

  protected boolean creative;

  @Override
  public boolean isCreative() {
    return this.creative;
  }

  @Override
  public void setCreative(boolean creative) {
    this.creative = creative;
  }

  // "Lowercasing" means that the ids are always converted to lower case when fixed.
  // This is highly recommended for sender colls.
  // The senderIds are case insensitive by nature and some times you simply can't know the correct
  // casing.
  protected boolean lowercasing;

  @Override
  public boolean isLowercasing() {
    return this.lowercasing;
  }

  @Override
  public void setLowercasing(boolean lowercasing) {
    this.lowercasing = lowercasing;
  }

  // Should that instance be saved or not?
  // If it is default it should not be saved.
  @SuppressWarnings("rawtypes")
  @Override
  public boolean isDefault(E entity) {
    if (entity instanceof Entity) {
      return ((Entity) entity).isDefault();
    } else {
      return false;
    }
  }

  // This is used in parallel with the isDefault.
  // Parallel usage is useful since we can then override isDeafult just like before.
  public static boolean isCustomDataDefault(Object entity) {
    if (!(entity instanceof Entity)) return true;
    JsonObject customData = ((Entity<?>) entity).getCustomData();
    if (customData == null) return true;
    if (customData.entrySet().size() == 0) return true;
    return false;
  }

  // -------------------------------------------- //
  // COPY AND CREATE
  // -------------------------------------------- //

  @SuppressWarnings({"rawtypes", "unchecked"})
  public void copy(Object ofrom, Object oto) {
    if (ofrom == null) throw new NullPointerException("ofrom");
    if (oto == null) throw new NullPointerException("oto");

    if (ofrom instanceof Entity) {
      Entity efrom = (Entity) ofrom;
      Entity eto = (Entity) oto;

      eto.load(efrom);
      eto.setCustomData(efrom.getCustomData());
    } else {
      Accessor.get(this.getEntityClass()).copy(ofrom, oto);
    }
  }

  // This simply creates and returns a new instance
  // It does not detach/attach or anything. Just creates a new instance.
  @Override
  public E createNewInstance() {
    try {
      return this.entityClass.newInstance();
    } catch (Exception e) {
      return null;
    }
  }

  // Create new instance with automatic id
  @Override
  public E create() {
    return this.create(null);
  }

  // Create new instance with the requested id
  @Override
  public synchronized E create(Object oid) {
    return this.create(oid, true);
  }

  public synchronized E create(Object oid, boolean noteChange) {
    E entity = this.createNewInstance();
    if (this.attach(entity, oid, noteChange) == null) return null;
    return entity;
  }

  // -------------------------------------------- //
  // ATTACH AND DETACH
  // -------------------------------------------- //

  @Override
  public String attach(E entity) {
    return this.attach(entity, null);
  }

  @Override
  public synchronized String attach(E entity, Object oid) {
    return this.attach(entity, oid, true);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  protected synchronized String attach(E entity, Object oid, boolean noteChange) {
    // Check entity
    if (entity == null) return null;
    String id = this.getId(entity);
    if (id != null) return id;

    // Check/Fix id
    if (oid == null) {
      id = MStore.createId();
    } else {
      id = this.fixId(oid);
      if (id == null) return null;
      if (this.id2entity.containsKey(id)) return null;
    }

    // PRE
    this.preAttach(entity, id);

    // Add entity reference info
    if (entity instanceof Entity) {
      ((Entity) entity).setColl(this);
      ((Entity) entity).setid(id);
    }

    // Attach
    this.ids.add(id);
    this.id2entity.put(id, entity);
    this.entity2id.put(entity, id);

    // Make note of the change
    if (noteChange) {
      this.localAttachIds.add(id);
      this.changedIds.add(id);
    }

    // POST
    this.postAttach(entity, id);

    return id;
  }

  @SuppressWarnings("unchecked")
  @Override
  public E detachEntity(Object entity) {
    E e = (E) entity;
    String id = this.getId(e);
    this.detach(e, id);
    return e;
  }

  @Override
  public E detachId(Object oid) {
    String id = this.fixId(oid);
    E e = this.get(id);
    this.detach(e, id);
    return e;
  }

  private void detach(E entity, String id) {
    // PRE
    this.preDetach(entity, id);

    // Remove @ local
    this.removeAtLocal(id);

    // Identify the change
    this.localDetachIds.add(id);
    this.changedIds.add(id);

    // POST
    this.postDetach(entity, id);
  }

  @Override
  public void preAttach(E entity, String id) {
    if (entity instanceof Entity) {
      ((Entity<?>) entity).preAttach(id);
    }
  }

  @Override
  public void postAttach(E entity, String id) {
    if (entity instanceof Entity) {
      ((Entity<?>) entity).postAttach(id);
    }
  }

  @Override
  public void preDetach(E entity, String id) {
    if (entity instanceof Entity) {
      ((Entity<?>) entity).preDetach(id);
    }
  }

  @Override
  public void postDetach(E entity, String id) {
    if (entity instanceof Entity) {
      ((Entity<?>) entity).postDetach(id);
    }
  }

  // -------------------------------------------- //
  // IDENTIFIED CHANGES
  // -------------------------------------------- //

  protected Set<String> localAttachIds;
  protected Set<String> localDetachIds;
  protected Set<String> changedIds;

  protected synchronized void clearIdentifiedChanges(Object oid) {
    String id = this.fixId(oid);
    this.localAttachIds.remove(id);
    this.localDetachIds.remove(id);
    this.changedIds.remove(id);
  }

  // -------------------------------------------- //
  // SYNCLOG
  // -------------------------------------------- //

  protected Map<String, Long> lastMtime;
  protected Map<String, JsonElement> lastRaw;
  protected Set<String> lastDefault;

  protected synchronized void clearSynclog(Object oid) {
    String id = this.fixId(oid);
    this.lastMtime.remove(id);
    this.lastRaw.remove(id);
    this.lastDefault.remove(id);
  }

  // Log database syncronization for display in the "/mcore mstore stats" command.
  private Map<String, Long> id2out = new TreeMap<String, Long>();
  private Map<String, Long> id2in = new TreeMap<String, Long>();

  public Map<String, Long> getSyncMap(boolean in) {
    return in ? this.id2in : this.id2out;
  }

  public long getSyncCount(String id, boolean in) {
    Long count = this.getSyncMap(in).get(id);
    if (count == null) return 0;
    return count;
  }

  public void addSyncCount(String id, boolean in) {
    long count = this.getSyncCount(id, in);
    count++;
    this.getSyncMap(in).put(id, count);
  }

  // -------------------------------------------- //
  // SYNC LOWLEVEL IO ACTIONS
  // -------------------------------------------- //

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Override
  public synchronized E removeAtLocal(Object oid) {
    String id = this.fixId(oid);
    this.clearIdentifiedChanges(id);
    this.clearSynclog(id);

    E entity = this.id2entity.remove(id);
    if (entity == null) return null;

    this.entity2id.remove(entity);

    this.ids.remove(id);

    // Remove entity reference info
    if (entity instanceof Entity) {
      ((Entity) entity).setColl(null);
      ((Entity) entity).setid(null);
    }

    return entity;
  }

  @Override
  public synchronized void removeAtRemote(Object oid) {
    String id = this.fixId(oid);

    this.clearIdentifiedChanges(id);
    this.clearSynclog(id);

    this.getDb().getDriver().delete(this, id);
  }

  @Override
  public synchronized void saveToRemote(Object oid) {
    String id = this.fixId(oid);

    this.clearIdentifiedChanges(id);
    this.clearSynclog(id);

    E entity = this.id2entity.get(id);
    if (entity == null) return;

    JsonElement raw = this.getGson().toJsonTree(entity, this.getEntityClass());
    this.lastRaw.put(id, raw);

    if (this.isDefault(entity) && isCustomDataDefault(entity)) {
      this.db.getDriver().delete(this, id);
      this.lastDefault.add(id);
    } else {
      Long mtime = this.db.getDriver().save(this, id, raw);
      if (mtime == null)
        return; // This fail should not happen often. We could handle it better though.
      this.lastMtime.put(id, mtime);
    }
  }

  @Override
  public synchronized void loadFromRemote(Object oid) {
    String id = this.fixId(oid);

    this.clearIdentifiedChanges(id);

    Entry<JsonElement, Long> entry = null;
    try {
      entry = this.getDriver().load(this, id);
    } catch (Exception e) {
      MCore.get()
          .log(
              Txt.parse(
                  "<b>Database could not load entity. You edited a file manually and made wrong JSON?"));
      MCore.get().log(Txt.parse("<k>Error: <v>%s", e.getMessage()));
      MCore.get().log(Txt.parse("<k>Entity: <v>%s", id));
      MCore.get().log(Txt.parse("<k>Collection: <v>%s", this.getName()));
      return;
    }
    if (entry == null) return;

    JsonElement raw = entry.getKey();
    if (raw == null) return;

    Long mtime = entry.getValue();
    if (mtime == null) return;

    E entity = this.get(id, false);
    if (entity != null) {
      // It did already exist
      this.copy(this.getGson().fromJson(raw, this.getEntityClass()), entity);
    } else {
      // Create first
      entity = this.createNewInstance();

      // Copy over data first
      this.copy(this.getGson().fromJson(raw, this.getEntityClass()), entity);

      // Then attach!
      this.attach(entity, oid, false);
    }

    this.lastRaw.put(id, raw);
    this.lastMtime.put(id, mtime);
    this.lastDefault.remove(id);
  }

  // -------------------------------------------- //
  // SYNC DECIDE AND BASIC DO
  // -------------------------------------------- //

  @Override
  public ModificationState examineId(Object oid) {
    String id = this.fixId(oid);
    return this.examineId(id, null, false);
  }

  @Override
  public ModificationState examineId(Object oid, Long remoteMtime) {
    String id = this.fixId(oid);
    return this.examineId(id, remoteMtime, true);
  }

  protected ModificationState examineId(Object oid, Long remoteMtime, boolean remoteMtimeSupplied) {
    String id = this.fixId(oid);

    if (this.localDetachIds.contains(id)) return ModificationState.LOCAL_DETACH;
    if (this.localAttachIds.contains(id)) return ModificationState.LOCAL_ATTACH;

    E localEntity = this.id2entity.get(id);
    if (!remoteMtimeSupplied) {
      remoteMtime = this.getDriver().getMtime(this, id);
    }

    boolean existsLocal = (localEntity != null);
    boolean existsRemote = (remoteMtime != null);

    if (!existsLocal && !existsRemote) return ModificationState.UNKNOWN;

    if (existsLocal && existsRemote) {
      Long lastMtime = this.lastMtime.get(id);
      if (remoteMtime.equals(lastMtime) == false) return ModificationState.REMOTE_ALTER;

      if (this.examineHasLocalAlter(id, localEntity)) return ModificationState.LOCAL_ALTER;
    } else if (existsLocal) {
      if (this.lastDefault.contains(id)) {
        if (this.examineHasLocalAlter(id, localEntity)) return ModificationState.LOCAL_ALTER;
      } else {
        return ModificationState.REMOTE_DETACH;
      }
    } else if (existsRemote) {
      return ModificationState.REMOTE_ATTACH;
    }

    return ModificationState.NONE;
  }

  protected boolean examineHasLocalAlter(String id, E entity) {
    JsonElement lastRaw = this.lastRaw.get(id);
    JsonElement currentRaw = null;

    try {
      currentRaw = this.getGson().toJsonTree(entity, this.getEntityClass());
    } catch (Exception e) {
      MCore.get()
          .log(
              Txt.parse(
                  "<b>Database examineHasLocalAlter failed convert current entity to JSON tree."));
      MCore.get().log(Txt.parse("<k>Error: <v>%s", e.getMessage()));
      MCore.get().log(Txt.parse("<k>Entity: <v>%s", id));
      MCore.get().log(Txt.parse("<k>Collection: <v>%s", this.getName()));
      throw e;
    }

    return !MStore.equal(lastRaw, currentRaw);
  }

  @Override
  public ModificationState syncId(Object oid) {
    String id = this.fixId(oid);

    ModificationState mstate = this.examineId(id);

    // mplugin.log("syncId: It seems", id, "has state", mstate);

    switch (mstate) {
      case LOCAL_ALTER:
      case LOCAL_ATTACH:
        this.saveToRemote(id);
        if (this.inited()) {
          this.addSyncCount(TOTAL, false);
          this.addSyncCount(id, false);
        }
        break;
      case LOCAL_DETACH:
        this.removeAtRemote(id);
        if (this.inited()) {
          this.addSyncCount(TOTAL, false);
          this.addSyncCount(id, false);
        }
        break;
      case REMOTE_ALTER:
      case REMOTE_ATTACH:
        this.loadFromRemote(id);
        if (this.inited()) {
          this.addSyncCount(TOTAL, true);
          this.addSyncCount(id, true);
        }
        break;
      case REMOTE_DETACH:
        this.removeAtLocal(id);
        if (this.inited()) {
          this.addSyncCount(TOTAL, true);
          this.addSyncCount(id, true);
        }
        break;
      default:
        this.clearIdentifiedChanges(id);
        break;
    }

    return mstate;
  }

  @Override
  public void syncSuspects() {
    for (String id : this.changedIds) {
      this.syncId(id);
    }
  }

  @Override
  public void syncAll() {
    // Find all ids
    Set<String> allids = new HashSet<String>(this.id2entity.keySet());
    allids.addAll(this.getDriver().getIds(this));
    for (String id : allids) {
      this.syncId(id);
    }
  }

  @Override
  public void findSuspects() {
    // Get remote id and mtime snapshot
    Map<String, Long> id2RemoteMtime = this.getDb().getDriver().getId2mtime(this);

    // Compile a list of all ids (both remote and local)
    Set<String> allids = new HashSet<String>();
    allids.addAll(id2RemoteMtime.keySet());
    allids.addAll(this.ids);

    // Check for modifications
    for (String id : allids) {
      Long remoteMtime = id2RemoteMtime.get(id);
      ModificationState state = this.examineId(id, remoteMtime);
      // mplugin.log("findSuspects: It seems", id, "has state", state);
      if (state.isModified()) {
        // System.out.println("It seems "+id+" has state "+state);
        this.changedIds.add(id);
      }
    }
  }

  // -------------------------------------------- //
  // SYNC RUNNABLES / SCHEDULING
  // -------------------------------------------- //

  protected Runnable tickTask;

  @Override
  public Runnable getTickTask() {
    return this.tickTask;
  }

  @Override
  public void onTick() {
    this.syncSuspects();
  }

  // -------------------------------------------- //
  // CONSTRUCT
  // -------------------------------------------- //

  public Coll(
      String name,
      Class<E> entityClass,
      Db db,
      Plugin plugin,
      boolean lazy,
      boolean creative,
      boolean lowercasing,
      Comparator<? super String> idComparator,
      Comparator<? super E> entityComparator) {
    // Setup the name and the parsed parts
    this.name = name;
    String[] nameParts = this.name.split("\\@");
    this.basename = nameParts[0];
    if (nameParts.length > 1) {
      this.universe = nameParts[1];
    } else {
      this.universe = null;
    }

    // WHAT DO WE HANDLE?
    this.entityClass = entityClass;
    this.lazy = lazy;
    this.creative = creative;
    this.lowercasing = lowercasing;

    // SUPPORTING SYSTEM
    this.plugin = plugin;
    this.db = db;
    this.collDriverObject = db.getCollDriverObject(this);

    // STORAGE
    this.ids = new ConcurrentSkipListSet<String>(idComparator);
    this.id2entity = new ConcurrentSkipListMap<String, E>(idComparator);
    this.entity2id = new ConcurrentSkipListMap<E, String>(entityComparator);

    // IDENTIFIED CHANGES
    this.localAttachIds = new ConcurrentSkipListSet<String>(idComparator);
    this.localDetachIds = new ConcurrentSkipListSet<String>(idComparator);
    this.changedIds = new ConcurrentSkipListSet<String>(idComparator);

    // SYNCLOG
    this.lastMtime = new ConcurrentSkipListMap<String, Long>(idComparator);
    this.lastRaw = new ConcurrentSkipListMap<String, JsonElement>(idComparator);
    this.lastDefault = new ConcurrentSkipListSet<String>(idComparator);

    final Coll<E> me = this;
    this.tickTask =
        new Runnable() {
          @Override
          public void run() {
            me.onTick();
          }
        };
  }

  public Coll(
      String name,
      Class<E> entityClass,
      Db db,
      Plugin plugin,
      boolean lazy,
      boolean creative,
      boolean lowercasing) {
    this(name, entityClass, db, plugin, lazy, creative, lowercasing, null, null);
  }

  public Coll(String name, Class<E> entityClass, Db db, Plugin plugin) {
    this(name, entityClass, db, plugin, false, false, false);
  }

  @Override
  public void init() {
    if (this.inited()) return;

    // TODO: Could this be more efficient by considering it's the first sync?
    this.syncAll();

    name2instance.put(this.getName(), this);
  }

  @Override
  public void deinit() {
    if (!this.inited()) return;

    // TODO: Save outwards only? We may want to avoid loads at this stage...
    this.syncAll();

    name2instance.remove(this.getName());
  }

  @Override
  public boolean inited() {
    return name2instance.containsKey(this.getName());
  }
}