Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image Upload data

868
LINES

< > BotCompany Repo | #1004863 - "Concepts" (persistent Java objects, include)

JavaX fragment (include) [tags: use-pretranspiled]

Libraryless. Click here for Pure Java version (12144L/83K).

// A concept should be an object, not just a string.

// Functions that should always be there for child processes:
please include function dbLock.

static int concepts_internStringsLongerThan = 10;

static new ThreadLocal<Bool> concepts_unlisted;

static interface Derefable {
  Concept get();
}

static interface IConceptIndex {
  void update(Concept c); // also for adding
  void remove(Concept c);
}

static interface IFieldIndex<A extends Concept, Val> {
  L<A> getAll(Val val);
  L<Val> allValues(); // returns a cloned list
  MultiSet<Val> allValues_multiSet();
}

sclass Concepts {
  Map<Long, Concept> concepts = synchroTreeMap();
  new HashMap<Class, O> perClassData;
  Map miscMap;
  
  // set to "-" for non-persistent (possibly not implemented)
  // also, can include a case ID ("#123/1")
  // TODO: have an actual directory instead
  S programID;
  
  long idCounter;
  volatile long changes, changesWritten, lastChange;
  volatile java.util.Timer autoSaver;
  volatile bool savingConcepts, dontSave, noXFullGrab;
  bool vmBusSend = true;
  bool initialSave = true; // set to false to avoid initial useless saving
  int autoSaveInterval = -1000; // 1 second + wait logic
  bool useGZIP = true, quietSave;
  ReentrantLock lock = new ReentrantLock(true);
  ReentrantLock saverLock = new ReentrantLock(true);
  long lastSaveTook, lastSaveWas;
  float maxAutoSavePercentage = 10;
  L<IConceptIndex> conceptIndices;
  Map<Class<? extends Concept>, Map<S, IFieldIndex>> fieldIndices;
  Map<Class<? extends Concept>, Map<S, IFieldIndex>> ciFieldIndices;
  L saveActions = synchroList();
  O classFinder = _defaultClassFinder();
  L onAllChanged = synchroList(); // list of runnables
  transient O saveWrapper; // VF1<Runnable>, to profile saving
  
  *() {}
  *(S *programID) {}
  
  synchronized long internalID() {
    do {
      ++idCounter;
    } while (hasConcept(idCounter));
    ret idCounter;
  }
  
  void initProgramID() {
    if (programID == null)
      programID = getDBProgramID();
  }
  
  // Now tries to load from bot first, then go to disk.
  Concepts load() {
    ret load(false);
  }
  
  Concepts safeLoad() {
    ret load(true);
  }
  
  Concepts load(bool allDynamic) {
    initProgramID();
    try {
      if (tryToGrab(allDynamic)) ret this;
    } catch e {
      if (!exceptionMessageContains(e, "no xfullgrab"))
        printShortException(e);
      print("xfullgrab failed - loading DB of " + programID + " from disk");
    }
    ret loadFromDisk(allDynamic);
  }
  
  Concepts loadFromDisk() { ret loadFromDisk(false); }
  
  Concepts loadFromDisk(bool allDynamic) {
    if (nempty(concepts)) clearConcepts();
    //DynamicObject_loading.set(true); // now done in unstructure()
    //try {
      // minimal crash recovery
      restoreLatestBackupIfConceptsFileEmpty(programID, doIt := true);

      long time = now();
      Map<Long, Concept> _concepts = concepts; // empty map
      readLocally2_allDynamic.set(allDynamic);
      temp tempSetTL(readLocally2_classFinder, classFinder);
      readLocally2(this, programID, "concepts");
      Map<Long, Concept> __concepts = concepts;
      concepts = _concepts;
      concepts.putAll(__concepts);
      int l = readLocally_stringLength;
      int tokrefs = unstructure_tokrefs;
      assignConceptsToUs();
      done("Loaded " + n(l(concepts), "concepts"), time);
      if (fileSize(getProgramFile(programID, "idCounter.structure")) != 0)
        readLocally2(this, programID, "idCounter");
      else
        calcIdCounter();
    /*} finally {
      DynamicObject_loading.set(null);
    }*/
    if (initialSave) allChanged();
    ret this;
  }
  
  Concepts loadConcepts() { ret load(); }
  
  bool tryToGrab(bool allDynamic) {
    if (sameSnippetID(programID, getDBProgramID())) false;
    temp RemoteDB db = connectToDBOpt(programID);
    if (db != null) {
      loadGrab(db.fullgrab(), allDynamic);
      true;
    }
    false;
  }
  
  Concepts load(S grab) {
    ret loadGrab(grab, false);
  }
  
  Concepts safeLoad(S grab) {
    ret loadGrab(grab, true);
  }
  
  Concepts loadGrab(S grab, bool allDynamic) {
    clearConcepts();
    DynamicObject_loading.set(true);
    try {
      Map<Long, Concept> map = (Map) unstructure(grab, allDynamic, classFinder);
      concepts.putAll(map);
      assignConceptsToUs();
      for (long l : map.keySet())
        idCounter = max(idCounter, l);
    } finally {
      DynamicObject_loading.set(null);
    }
    allChanged();
    ret this;
  }
  
  void assignConceptsToUs() {
    for (Concept c : values(concepts)) c._concepts = this;
    for (Concept c : values(concepts))
      callOpt_noArgs(c, "_doneLoading2"); // doneLoading2 is called on all concepts after all concepts are loaded
  }

  S progID() {
    ret programID == null ? getDBProgramID() : programID;
  }
  
  Concept getConcept(S id) {
    ret empty(id) ? null : getConcept(parseLong(id));
  }
  
  Concept getConcept(long id) {
    ret (Concept) concepts.get((long) id);
  }
  
  Concept getConcept(RC ref) {
    ret ref == null ? null : getConcept(ref.longID());
  }
  
  bool hasConcept(long id) {
    ret concepts.containsKey((long) id);
  }
  
  void deleteConcept(long id) {
    Concept c = getConcept(id);
    if (c == null)
      print("Concept " + id + " not found");
    else
      c.delete();
  }
  
  void calcIdCounter() {
    long id_ = 0;
    for (long id : keys(concepts))
      id_ = max(id_, id);
    idCounter = id_+1;
    saveLocally2(this, programID, "idCounter");
  }
  
  void saveConceptsIfDirty() { saveConcepts(); }
  void save() { saveConcepts(); }

  void saveConcepts() {
    if (dontSave) ret;
    initProgramID();
    saverLock.lock();
    savingConcepts = true;
    long start = now(), time;
    try {
      S s = null;
      //synchronized(main.class) {
      long _changes = changes;
      if (_changes == changesWritten) ret;
      
      final File f = getProgramFile(programID, useGZIP ? "concepts.structure.gz" : "concepts.structure");
      
      lock.lock();
      long fullTime = now();
      try {
        saveLocally2(this, programID, "idCounter");
        
        if (useGZIP) {
          callRunnableWithWrapper(saveWrapper, r {
            saveGZStructureToFile(f, cloneMap(concepts));
          });
          getProgramFile(programID, "concepts.structure").delete();
        } else
          s = structure(cloneMap(concepts));
      } finally {
        lock.unlock();
      }
      
      while (nempty(saveActions))
        pcallF(popFirst(saveActions));

      changesWritten = _changes; // only update when structure didn't fail
      
      if (!useGZIP) {
        time = now()-start;
        if (!quietSave)
          print("Saving " + toM(l(s)) + "M chars (" /*+ changesWritten + ", "*/ + time + " ms)");
        start = now();
        saveTextFile(f, javaTokWordWrap(s));
        getProgramFile(programID, "concepts.structure.gz").delete();
      }
      
      copyFile(f, getProgramFile(programID, "backups/concepts.structure" + (useGZIP ? ".gz" : "") + ".backup" + ymd() + "-" + formatInt(hours(), 2)));
      time = now()-start;
      if (!quietSave)
        print(programID + ": Saved " + toK(f.length()) + " K, " + n(concepts, "concepts") + " (" + time + " ms)");
      lastSaveWas = fullTime;
      lastSaveTook = now()-fullTime;
    } finally {
      savingConcepts = false;
      saverLock.unlock();
    }
  }
  
  void _autoSaveConcepts() {
    if (autoSaveInterval < 0 && maxAutoSavePercentage != 0) {
      long pivotTime = Math.round(lastSaveWas+lastSaveTook*100.0/maxAutoSavePercentage);
      if (now() < pivotTime) {
        //print("Skipping auto-save (last save took " + lastSaveTook + ")");
        ret;
      }
    }
    try {
      saveConcepts();
    } catch e {
      print("Concept save failed, will try again: " + e);
    }
  }
  
  void clearConcepts() {
    concepts.clear();
    allChanged();
  }
  
  void allChanged() {
    synchronized(this) { ++changes; lastChange = sysNow(); }
    if (vmBusSend) vmBus_send('conceptsChanged, this);
    pcallFAll(onAllChanged);
  }
  
  // auto-save every second if dirty
  synchronized void autoSaveConcepts() {
    if (autoSaver == null) {
      if (isTransient()) fail("Can't persist transient database");
      autoSaver = doEvery_daemon(abs(autoSaveInterval), r { _autoSaveConcepts() });
      // print("Installed auto-saver (" + autoSaveInterval + " ms, " + progID() + ")");
    }
  }
  
  void cleanMeUp() {
    bool shouldSave = autoSaver != null;
    if (autoSaver != null) {
      autoSaver.cancel();
      autoSaver = null;
    }
    while (savingConcepts) sleepInCleanUp(10);
    if (shouldSave)
      saveConceptsIfDirty();
  }
  
  Map<Long, S> getIDsAndNames() {
    new Map<Long, S> map;
    Map<Long, Concept> cloned = cloneMap(concepts);
    for (long id : keys(cloned)) 
      map.put(id, cloned.get(id).className);
    ret map;
  }
  
  void deleteConcepts(L l) {
    if (l != null) for (O o : cloneList(l))
      if (o instanceof Long) {
        Concept c = concepts.get(o);
        if (c != null) c.delete();
      } else if (o cast Concept)
        o.delete();
      else
        warn("Can't delete " + getClassName(o));
  }
  
  <A extends Concept> A conceptOfType(Class<A> type) {
    IConceptCounter counter = conceptCounterForClass(type);
    if (counter != null) ret (A) first(counter.allConcepts());
    ret firstOfType(allConcepts(), type);
  }
  
  <A extends Concept> L<A> conceptsOfType(Class<A> type) {
    IConceptCounter counter = conceptCounterForClass(type);
    if (counter != null) ret (L<A>) cloneList(counter.allConcepts());
    ret filterByType(allConcepts(), type);
  }
  
  <A extends Concept> L<A> listConcepts(Class<A> type) {
    ret conceptsOfType(type);
  }
  
  <A extends Concept> L<A> list(Class<A> type) {
    ret conceptsOfType(type);
  }
  
  L<Concept> list(S type) {
    ret conceptsOfType(type);
  }
  
  L<Concept> conceptsOfType(S type) {
    ret filterByDynamicType(allConcepts(), "main$" + type);
  }
  
  bool hasConceptOfType(Class<? extends Concept> type) {
    ret hasType(allConcepts(), type);
  }
  
  void persistConcepts() {
    loadConcepts();
    autoSaveConcepts();
  }
  
  // We love synonyms
  void conceptPersistence() { persistConcepts(); }
  
  Concepts persist() { persistConcepts(); ret this; }
  void persist(Int interval) {
    if (interval != null) autoSaveInterval = interval;
    persist();
  }
    
  // Runs r if there is no concept of that type
  <A extends Concept> A ensureHas(Class<A> c, Runnable r) {
    A a = conceptOfType(c);
    if (a == null) {
      r.run();
      a = conceptOfType(c);
      if (a == null)
        fail("Concept not made by " + r + ": " + shortClassName(c));
    }
    ret a;
  }
  
  // Ensures that every concept of type c1 is ref'd by a concept of
  // type c2.
  // Type of func: voidfunc(concept)
  void ensureHas(Class<? extends Concept> c1, Class<? extends Concept> c2, O func) {
    for (Concept a : conceptsOfType(c1)) {
      Concept b = findBackRef(a, c2);
      if (b == null) {
        callF(func, a);
        b = findBackRef(a, c2);
        if (b == null)
          fail("Concept not made by " + func + ": " + shortClassName(c2));
      }
    }
  }
  
  // Type of func: voidfunc(concept)
  void forEvery(Class<? extends Concept> type, O func) {
    for (Concept c : conceptsOfType(type))
      callF(func, c);
  }
  
  int deleteAll(Class<? extends Concept> type) {
    L<Concept> l = (L) conceptsOfType(type);
    for (Concept c : l) c.delete();
    ret l(l);
  }
  
  Collection<Concept> allConcepts() {
    synchronized(concepts) {
      ret new L(values(concepts));
    }
  }
  
  IConceptCounter conceptCounterForClass(Class<? extends Concept> c) {
    for (IFieldIndex idx : values(mapGet(fieldIndices, c)))
      if (idx cast IConceptCounter) ret idx;
    for (IFieldIndex idx : values(mapGet(ciFieldIndices, c)))
      if (idx cast IConceptCounter) ret idx;
    null;
  }
  
  <A extends Concept> int countConcepts(Class<A> c, O... params) {
    if (empty(params)) {
      IConceptCounter counter = conceptCounterForClass(c);
      if (counter != null) ret counter.countConcepts();
      ret l(list(c));
    }
    int n = 0;
    for (A x : list(c)) if (checkConceptFields(x, params)) ++n;
    ret n;
  }

  int countConcepts(S c, O... params) {
    if (empty(params)) ret l(list(c));
    int n = 0;
    for (Concept x : list(c)) if (checkConceptFields(x, params)) ++n;
    ret n;
  }

  int countConcepts() {
    ret l(concepts);
  }
  
  synchronized void addConceptIndex(IConceptIndex index) {
    if (conceptIndices == null)
      conceptIndices = new L;
    conceptIndices.add(index);
  }
  
  synchronized void removeConceptIndex(IConceptIndex index) {
    if (conceptIndices == null) ret;
    conceptIndices.remove(index);
    if (empty(conceptIndices)) conceptIndices = null;
  }
  
  synchronized void addFieldIndex(Class<? extends Concept> c, S field, IFieldIndex index) {
    if (fieldIndices == null)
      fieldIndices = new HashMap;
    Map<S, IFieldIndex> map = fieldIndices.get(c);
    if (map == null)
      fieldIndices.put(c, map = new HashMap);
    map.put(field, index);
  }
  
  synchronized IFieldIndex getFieldIndex(Class<? extends Concept> c, S field) {
    if (fieldIndices == null) null;
    Map<S, IFieldIndex> map = fieldIndices.get(c);
    ret map == null ? null : map.get(field);
  }
  
  synchronized void addCIFieldIndex(Class<? extends Concept> c, S field, IFieldIndex index) {
    if (ciFieldIndices == null)
      ciFieldIndices = new HashMap;
    Map<S, IFieldIndex> map = ciFieldIndices.get(c);
    if (map == null)
      ciFieldIndices.put(c, map = new HashMap);
    map.put(field, index);
  }
  
  synchronized IFieldIndex getCIFieldIndex(Class<? extends Concept> c, S field) {
    if (ciFieldIndices == null) null;
    Map<S, IFieldIndex> map = ciFieldIndices.get(c);
    ret map == null ? null : map.get(field);
  }
  
  // inter-process methods
  
  RC xnew(S name, O... values) {
    ret new RC(cnew(name, values));
  }
  
  void xset(long id, S field, O value) {
    xset(new RC(id), field, value);
  }
  
  void xset(RC c, S field, O value) {
    if (value instanceof RC)
      value = getConcept((RC) value);
    cset(getConcept(c), field, value);
  }
  
  O xget(long id, S field) {
    ret xget(new RC(id), field);
  }
  
  O xget(RC c, S field) {
    ret xgetPost(cget(getConcept(c), field));
  }
  
  O xgetPost(O o) {
    o = deref(o);
    if (o instanceof Concept)
      ret new RC((Concept) o);
    ret o;
  }
  
  void xdelete(long id) {
    xdelete(new RC(id));
  }
  
  void xdelete(RC c) {
    getConcept(c).delete();
  }
  
  void xdelete(L<RC> l) {
    for (RC c : l)
      xdelete(c);
  }
  
  L<RC> xlist() {
    ret map("toPassRef", allConcepts());
  }
  
  L<RC> xlist(S className) {
    ret map("toPassRef", conceptsOfType(className));
  }
  
  bool isTransient() { ret eq(programID, "-"); }
  
  S xfullgrab() {
    if (noXFullGrab) fail("no xfullgrab (DB too large)");
    lock lock();
    if (changes == changesWritten && !isTransient())
      ret loadConceptsStructure(programID);
    ret structure(cloneMap(concepts));
  }
  
  /* dev.
  Either<File, byte[]> xfullgrabGZipped() {
    lock lock();
    if (changes == changesWritten && !isTransient())
      ret loadConceptsStructure(programID);
    ret structure(cloneMap(concepts));
  }*/
  
  void xshutdown() {
    // Killing whole VM if someone wants this DB to shut down
    cleanKillVM();
  }
  
  long xchangeCount() { ret changes; }
  int xcount() { ret countConcepts(); }
  
  void register(Concept c) {
    if (c._concepts == this) ret;
    if (c._concepts != null) fail("Can't re-register");
    c._concepts = this;
    c.id = internalID();
    c.created = now();
    concepts.put((long) c.id, c);
    c.change();
  }
  
  void conceptChanged(Concept c) {
    allChanged();
    if (conceptIndices != null)
      for (IConceptIndex index : conceptIndices)
        index.update(c);
  }
  
  bool hasUnsavedData() {
    ret changes != changesWritten || savingConcepts;
  }
} // end of Concepts

sclass Concept extends DynamicObject {
  transient Concepts _concepts; // Where we belong
  long id;
  long created, _modified;
  L<Ref> refs;
  L<Ref> backRefs;
  
  // used only internally (cnew)
  *(S className) {
    super(className);
    _created();
  }
  
  *() {
    if (!_loading()) {
      //className = shortClassName(this); // XXX - necessary?
      //print("New concept of type " + className);
      _created();
    }
  }
  
  *(bool unlisted) {
    if (!unlisted) _created();
  }
  
  static bool loading() { ret _loading(); }
  static bool _loading() { ret dynamicObjectIsLoading(); }

  void _created() {
    if (!isTrue(concepts_unlisted!))
      db_mainConcepts().register(this);
  }
  
  /*void put(S field, O value) {
    fieldValues.put(field, value);
    change();
  }
  
  O get(S field) {
    ret fieldValues.get(field);
  }*/
  
  class Ref<A extends Concept> {
    A value;
    
    *() {
      if (!dynamicObjectIsLoading()) refs = addDyn(refs, this);
    }
    
    *(A *value) {
      refs = addDyn(refs, this);
      index();
    }
    
    // get owning concept (source)
    Concept concept() {
      ret Concept.this;
    }
    
    // get target
    A get() { ret value; }
    bool has() { ret value != null; }
    
    void set(A a) {
      if (a == value) ret;
      unindex();
      value = a;
      index();
    }
    
    void set(Ref<A> ref) { set(ref.get()); }
    void clear() { set((A) null); }
    
    // TODO: sync all the indexing and unindexing!?
    void index() { 
      if (value != null)
        value._addBackRef(this);
      change();
    }
    
    void unindex() {
      if (value != null)
        value._removeBackRef(this);
    }
    
    void change() {
      Concept.this.change();
    }
  }
  
  class RefL<A extends Concept> extends AbstractList<A> {
    new L<Ref<A>> l;
    
    public A set(int i, A o) {
      A prev = l.get(i).get();
      l.get(i).set(o);
      ret prev;
    }
    
    public void add(int i, A o) {
      l.add(i, new Ref(o));
    }
    
    public A get(int i) {
      ret l.get(i).get();
    }
    
    public A remove(int i) {
      ret l.remove(i).get();
    }
    
    public int size() {
      ret l.size();
    }
    
    public bool contains(O o) {
      if (o instanceof Concept)
        for (Ref<A> r : l) if (eq(r!, o)) true;
      ret super.contains(o);
    }
  }
  
  void delete() {
    //name = "[defunct " + name + "]";
    //defunct = true;
    //energy = 0;
    
    // clean refs
    
    for (Ref r : unnull(refs))
      r.unindex();
    refs = null;
    
    // set back refs to null
    
    for (Ref r : cloneList(backRefs))
      r.set((Concept) null);
    backRefs = null;
    
    if (_concepts != null) {
      _concepts.concepts.remove((long) id);
      _concepts.allChanged();
      if (_concepts.conceptIndices != null)
        for (IConceptIndex index : _concepts.conceptIndices)
          index.remove(this);
      _concepts = null;
    }
    id = 0;
  }
  
  BaseXRef export() {
    ret new BaseXRef(_concepts.progID(), id);
  }
  
  // notice system of a change in this object
  void change() {
    _modified = now();
    _change_withoutUpdatingModifiedField();
  }
  
  void _change_withoutUpdatingModifiedField() {
    if (_concepts != null) _concepts.conceptChanged(this);
  }
  
  void _change() { change(); }
  
  S _programID() {
    ret _concepts == null ? getDBProgramID() : _concepts.progID();
  }
  
  // overridable
  
  void _addBackRef(Concept.Ref ref) {
    backRefs = addDyn(backRefs, ref);
  }
  
  void _removeBackRef(Concept.Ref ref) {
    backRefs = removeDyn(backRefs, ref);
  }
  
  // convenience methods
  
  void _setField(S field, O value) {
    cset(this, field, value);
  }
  
  void _setFields(O... values) {
    cset(this, values);
  }
} // class Concept

// remote reference (for inter-process communication or
// external databases). Formerly "PassRef".
// prepared for string ids if we do them later
sclass RC {
  transient O owner;
  S id;
  
  *() {} // make serialisation happy
  *(long id) { this.id = str(id); }
  *(O owner, long id) { this.id = str(id); this.owner = owner; }
  *(Concept c) { this(c.id); }
  long longID() { ret parseLong(id); }
  
  public S toString() {
    ret id;
  }
}

// Reference to a concept in another program
sclass BaseXRef {
  S programID;
  long id;
    
  *() {}
  *(S *programID, long *id) {}
  
  public bool equals(O o) {
    if (!(o instanceof BaseXRef)) false;
    BaseXRef r = cast o;
    ret eq(programID, r.programID) && eq(id, r.id);
  }
  
  public int hashCode() {
    ret programID.hashCode() + (int) id;
  }
}

// BaseXRef as a concept
sclass XRef extends Concept {
  BaseXRef ref;
  
  *() {}
  *(BaseXRef *ref) { _doneLoading2(); }
  
  // after we have been added to concepts
  void _doneLoading2() {
    getIndex().put(ref, this);
  }
    
  HashMap<BaseXRef, XRef> getIndex() {
    ret getXRefIndex(_concepts);
  }
}

static synchronized HashMap<BaseXRef, XRef> getXRefIndex(Concepts concepts) {
  HashMap cache = (HashMap) concepts.perClassData.get(XRef.class);
  if (cache == null)
    concepts.perClassData.put(XRef.class, cache = new HashMap);
  ret cache;
}

// uses mainConcepts
static XRef lookupOrCreateXRef(BaseXRef ref) {
  XRef xref = getXRefIndex(db_mainConcepts()).get(ref);
  if (xref == null)
    xref = new XRef(ref);
  ret xref;
}

// define standard concept functions to use main concepts

static void cleanMeUp_concepts() {
  if (db_mainConcepts() != null) db_mainConcepts().cleanMeUp();
  // mainConcepts = null; // TODO
}

svoid loadAndAutoSaveConcepts {
  db_mainConcepts().persist();
}

svoid loadAndAutoSaveConcepts(int interval) {
  db_mainConcepts().persist(interval);
}

static RC toPassRef(Concept c) {
  ret new RC(c);
}

// so we can instantiate the program to run as a bare DB bot
please include function bareDBMode.

Author comment

Began life as a copy of #1004681

download  show line numbers  debug dex   

Travelled to 20 computer(s): aoiabmzegqzx, ayivmpnvhhik, cbybwowwnfue, cfunsshuasjs, ddnzoavkxhuk, gwrvuhgaqvyk, imzmzdywqqli, irmadwmeruwu, ishqpsrjomds, jozkyjcghlvl, jtubtzbbkimh, lpdgvwnxivlt, lulzaavyztxj, mqqgnosmbjvj, onxytkatvevr, ppjhyzlbdabe, sawdedvomwva, tslmcundralx, tvejysmllsmz, wtqryiryparv

No comments. add comment

Snippet ID: #1004863
Snippet name: "Concepts" (persistent Java objects, include)
Eternal ID of this version: #1004863/136
Text MD5: fb14311ef57ffc7dbcce932cbe173c03
Transpilation MD5: 3e109e5d9531edda26bf289f33aa4245
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2019-08-13 18:08:10
Source code size: 22806 bytes / 868 lines
Pitched / IR pitched: No / No
Views / Downloads: 1015 / 18449
Version history: 135 change(s)
Referenced in: [show references]