sclass HCRUD_Concepts extends HCRUD_Data { Concepts cc = db_mainConcepts(); Class cClass; new L> onCreateOrUpdate; new L> onCreate; IVF2 afterUpdate; // parameter 2: old values MapSO filters; // fields to filter by/add to new objects SS ciFilters; // case-insensitive filters IF1> customFilter; ValueConverterForField valueConverter; bool referencesBlockDeletion; bool trimAllSingleLineValues; Set fieldsToHideInCreationForm; bool lockDB; // lock DB while updating object bool verbose; bool dropEmptyListValues = true; bool lsMagic; bool convertConceptValuesToRefs; A currentConcept; // while editing bool useDynamicComboBoxes; // dynamically load concepts combo box entries for all fields IPred useDynamicComboBoxesForField; // activate dynamic combo boxes for selected fields int dynamicComboBoxesThreshold = 1000; // if there are this many entries or more, use a dynamic combo box *(Class *cClass) {} *(Concepts *cc, Class *cClass) {} // XXX - breaking change from just shortName() swappable S itemName() { ret humanizeShortName(cClass); } swappable S itemNamePlural() { ret super.itemNamePlural(); } //LS fields() { ret conceptFields(cClass); } L itemsForListing() { ret defaultSort(asList(listConcepts())); } @Override L list() { //ret lazyMap itemToMapForList(itemsForListing()); ret lazyMap(itemsForListing(), a -> new Item(str(a.id)) { public MapSO calcFullMap() { ret itemToMapForList(a); } }); } // more efficient version - convert items only after taking subList // TODO: this is never called by HCRUD; make lazy list instead? @Override L list(IntRange range) { ret lambdaMap itemToMapForList(subListOrFull(itemsForListing(), range)); } Cl listConcepts() { Cl l = listConcepts_firstStep(); ret postProcess(customFilter, l); } swappable Cl listConcepts_firstStep() { if (empty(ciFilters)) ret conceptsWhere(cc, cClass, mapToParams(filters)); else if (empty(filters)) ret conceptsWhereCI(cc, cClass, mapToParams(ciFilters)); else { // TODO: choose best index Cl l = conceptsWhere(cc, cClass, mapToParams(filters)); ret filterConceptsIC(l, mapToParams(ciFilters)); } } swappable Pair defaultSortField() { ret pair("id", true); } swappable L defaultSort(L l) { ret sortedByConceptIDDesc(l); } swappable A emptyConcept() { ret unlisted(cClass); } swappable MapSO emptyObject() { A c = emptyConcept(); // not actually necessary here, we do it on create /*cset(c, mapToParams(filters)); cset(c, mapToParams(ciFilters));*/ MapSO map = itemToMap(c); //printVars_str emptyObject(+cClass, +filters, +ciFilters, +map); ret mapMinusKeys(fieldsToHideInCreationForm, map); } MapSO itemToMap(A c) { if (c == null) null; ret putKeysFirst(getFieldOrder(c), conceptToMap_gen_withNullValues(c)); } MapSO itemToMapForList(A c) { if (c == null) null; MapSO map = itemToMap(c); massageItemMapForList(c, map); ret map; } swappable void massageItemMapForList(A c, MapSO map) { } swappable void massageItemMapForUpdate(A c, MapSO map) { // Concepts.RefL magic Cl refLFields = nonStaticNonTransientFieldObjectsOfType(Concept.RefL.class, c); printVars_str(+refLFields, +c); for (Field f : refLFields) { new TreeMap values; new Matches m; for (S key, O value : cloneMap(map)) if (startsWith(key, f.getName() + "_", m) && isInteger(m.rest())) { Concept concept = getConceptFromString((S) value); //printVars_str("RefL magic", +key, +conceptID, +concept); if (concept != null || !dropEmptyListValues) values.put(parseInt(m.rest()), concept); map.remove(key); } if (!dropEmptyListValues) while (nempty(values) && lastValue(values) == null) { if (verbose) print("Dropping value " + lastEntry(values)); removeLastKey(values); } map.put(f.getName(), valuesAsList(values)); } // process dynamic bool, concept fields for (S name : cloneKeys(map)) { S metaInfo = metaInfoFromForm(name); print("metaInfo for " + name + ": " + metaInfo); if (eqic(metaInfo, "concept")) replaceStringValueWithConcept(map, name); else if (eqic(metaInfo, "bool")) replaceStringValueWithBool(map, name); } // Concepts.Ref magic (look up concept) for (Field f : nonStaticNonTransientFieldObjectsOfType(Concept.Ref.class, c)) replaceStringValueWithConcept(map, f.getName()); // trim values if (trimAllSingleLineValues) for (Map.Entry e : map.entrySet()) { S val = optCastString(e.getValue()); if (val != null && isSingleLine(val) && isUntrimmed(val)) e.setValue(trim(val)); } // LS magic if (lsMagic) for (Field f : nonStaticNonTransientFieldObjectsOfType(L.class, c)) { if (eqOneOf(f.getName(), "refs", "backRefs")) continue; new TreeMap values; new Matches m; for (S key, O value : cloneMap(map)) { if (startsWith(key, f.getName() + "_", m) && isInteger(m.rest())) { if (!dropEmptyListValues || nempty((S) value)) { if (verbose) print("Adding value " + m.rest() + " / " + value); mapPut(values, parseInt(m.rest()), (S) value); } map.remove(key); } } if (!dropEmptyListValues) while (nempty(values) && empty(lastValue(values))) { if (verbose) print("Dropping value " + lastEntry(values)); removeLastKey(values); } map.put(f.getName(), valuesAsList(values)); } // don't set SecretValue fields for (Field f : nonStaticNonTransientFieldObjectsOfType(SecretValue, c)) map.remove(f.getName()); } void replaceStringValueWithConcept(MapSO map, S key) { O value = map.get(key); if (value cast S) { Concept concept = getConceptFromString(value); map.put(key, concept); } } void replaceStringValueWithBool(MapSO map, S key) { O value = map.get(key); if (value cast S) { map.put(key, englishStringToBool(value)); } } swappable MapSO getObject(O id) { ret itemToMap(conceptForID(id)); } MapSO getObjectForEdit(O id) { currentConcept = conceptForID(id); ret getObject(id); } O createObject(SS fullMap, S fieldPrefix) { rawFormValues = fullMap; try { SS map = extractFieldValues(fullMap, fieldPrefix); A c = cnew(cc, cClass); // make sure filters override setValues(c, mapMinusKeys(map, filteredFields()), true); cset(c, mapToParams(filters)); cset(c, mapToParams(ciFilters)); pcallFAll(onCreate, c); pcallFAll(onCreateOrUpdate, c); callOpt(c, "_onCreated"); // TODO: synchronize? ret c.id; } finally { rawFormValues = null; } } void setValues(A c, SS map, bool creating) { lock lockDB && !creating ? dbLock(cc) : null; MapSO map2 = (Map) cloneMap(map); massageItemMapForUpdate(c, map2); if (verbose) { print("setValues " + map); print("backRefs: " + c.backRefs); } MapSO oldValues = !creating && afterUpdate != null ? cgetAll_cloneLists(c, keys(map2)) : null; if (convertConceptValuesToRefs) convertAllConceptValuesToRefs(c, map2); if (valueConverter == null) cSmartSet(c, mapToParams(map2)); else cSmartSet_withConverter_pcall(verbose, valueConverter, c, mapToParams(map2)); if (oldValues != null) callF(afterUpdate, c, oldValues); if (verbose) print("backRefs: " + c.backRefs); } // for dynamic fields void convertAllConceptValuesToRefs(A c, MapSO map) { for (S key, O value : cloneMap(map)) { if (value cast Concept) if (!hasField(c, key)) { print("Converting value to ref: " + key); map.put(key, c.new Ref(value)); } } } A conceptForID(O id) { ret _getConcept(cc, cClass, toLong(id)); } S updateObject(O id, SS fullMap, S fieldPrefix) { rawFormValues = fullMap; try { SS map = extractFieldValues(fullMap, fieldPrefix); A c = conceptForID(id); if (c == null) ret "Object " + id + " not found"; try answer checkFilters(c); setValues(c, map, false); pcallFAll(onCreateOrUpdate, c); ret "Object " + id + " updated"; } finally { rawFormValues = null; } } S deleteObject(O id) { A c = conceptForID(id); if (c == null) ret "Object " + id + " not found"; try answer checkFilters(c); actuallyDeleteConcept(c); ret "Object " + id + " deleted"; } swappable void actuallyDeleteConcept(A c) { deleteConcept(c); } S checkFilters(A c) { if (!checkConceptFields(c, mapToParams(filters)) || !checkConceptFieldsIC(c, mapToParams(ciFilters))) ret "Object " + c.id + " not in view"; ret ""; } HCRUD_Concepts addFilters(MapSO map) { fOr (S field, O value : map) addFilter(field, value); this; } HCRUD_Concepts addFilter(S field, O value) { filters = orderedMapPutOrCreate(filters, field, value); this; } // TODO: do this the other way around (check for editable types) swappable bool isEditableValue(O value) { //if (value instanceof Concept.RefL) true; // subsumed in next line if (value instanceof L) true; if (value instanceof Cl) false; true; } Renderer getRenderer(S field) { if (!isEditableValue(currentValue)) ret new NotEditable; Class type = fieldType(or(currentConcept, cClass), field); S metaInfo = metaInfoFromForm(field); //printVars_str("getRenderer", +cClass, +field, +type, +rawFormValues); if (eq(type, bool.class)) ret new CheckBox; if (eq(type, Bool.class)) ret new ComboBox(ll("", "yes", "no"), b -> trueFalseNull((Bool) b, "yes", "no", "")); // show a Ref<> field as a combo box if (isSubtypeOf(type, Concept.Ref.class)) { Class c = fieldTypeArg(field); AbstractComboBox cb = makeConceptsComboBox(field, c); cb.metaInfo = "concept"; ret cb; } // show dynamic field with concept/ref value as combo box O val = deref(currentValue); if (val cast Concept) { Class c = val.getClass(); AbstractComboBox cb = makeConceptsComboBox(field, c); cb.metaInfo = "concept"; ret cb; } // show dynamic ref field from URL parameters if (eqic(metaInfo, "concept")) { printVars_str("metaInfo value", +field, +val); AbstractComboBox cb = makeConceptsComboBox(field, Concept); // TODO cb.metaInfo = "concept"; ret cb; } // show a RefL<> field as a list of combo boxes if (eq(type, Concept.RefL.class)) { Class c = fieldTypeArg(field); ret new FlexibleLengthList(makeConceptsComboBox(field, c)); } if (eq(type, L.class)) { //Class c = fieldTypeArg(field); ret new FlexibleLengthList(new TextField(80)); } if (val cast Bool) ret new CheckBox; ret super.getRenderer(field); } DynamicComboBox makeDynamicComboBox(S field, Class c) { DynamicComboBox cb = new(field); cb.valueToEntry = value -> { value = deref(value); //print("ComboBox: value type=" + _getClass(value); long id = 0; if (value cast Concept) id = conceptID(value); else if (value instanceof S && isInteger((S) value)) id = parseLong(value); if (id != 0) ret comboBoxItem(_getConcept(cc, id)); null; }; ret cb; } AbstractComboBox makeConceptsComboBox(S field, Class c) { if (c == null) fail("Null type for field \*field*/. currentConcept: " + currentConcept); if (useDynamicComboBoxes || useDynamicComboBoxesForField != null && useDynamicComboBoxesForField.get(field)) ret makeDynamicComboBox(field, c); Cl concepts = listConceptClass(c); if (l(concepts) >= dynamicComboBoxesThreshold) ret makeDynamicComboBox(field, c); LS entries = comboBoxItems(concepts); ComboBox cb = new(entries); cb.valueToEntry = value -> { value = deref(value); //print("ComboBox: value type=" + _getClass(value); long id = 0; if (value cast Concept) id = conceptID(value); else if (value instanceof S && isInteger((S) value)) id = parseLong(value); if (id != 0) { S entry = firstWhereFirstLongIs(entries, id); //print("combobox selected: " + id + " / " + entry); ret entry; } null; }; ret cb; } // TODO: filters? Cl listConceptClass(Class c) { ret cc.list(c); } LS comboBoxItemsForConceptClass(Class c) { ret comboBoxItems(listConceptClass(c)); } LS comboBoxItems(Cl l) { ret comboBoxItems_static(l); } static LS comboBoxItems_static(Cl l) { ret itemPlus("", lmap comboBoxItem_static(l)); } S comboBoxItem(Concept val) { ret comboBoxItem_static(val); } sS comboBoxItem_static(Concept val) { ret val == null ? null : shorten(val.id + ": " + val); } swappable bool objectCanBeDeleted(O id) { ret !referencesBlockDeletion || !hasBackRefs(conceptForID(id)); } Set filteredFields() { ret joinSets(keys(filters), keys(ciFilters)); } Class fieldTypeArg(S field) { ret getTypeArgumentAsClass(genericFieldType(or(currentConcept, cClass), field)); } swappable Class conceptClassForComboBoxSearch(S info, S query) { // info is the field name (Ref or RefL). get concept class if (!isIdentifier(info)) ret cClass; S field = info; Class c = fieldTypeArg(field); // simple hack to show list for dynamic/new fields // assuming it's the same as the main class // clients can improve on this if (c == null) c = cClass; ret c; } swappable LS comboBoxSearchBaseItems(S info, S query) { var c = conceptClassForComboBoxSearch(info, query); if (c == null) ret emptyList(); ret comboBoxItemsForConceptClass(c); } LS comboBoxSearch(S info, S query) { LS items = comboBoxSearchBaseItems(info, query); ret takeFirst(10, scoredSearch(query, items)); } // to find the concept e.g. within massageFormMatrix A conceptForMap(MapSO map) { if (map == null) null; long id = toLong(map.get(idField())); ret id == 0 ? null : (A) _getConcept(cc, id); } A getConcept(MapSO map) { ret conceptForMap(map); } SS extractFieldValues(SS fullMap, S fieldPrefix) { ret subMapStartingWith_dropPrefix(fullMap, fieldPrefix); } Concept getConceptFromString(S s) { long conceptID = parseFirstLong(s); ret _getConcept(cc, conceptID); } S metaInfoFromForm(S field) { ret mapGet(rawFormValues, "metaInfo_" + field); } void addCIFilter(S field, S value) { ciFilters = putOrCreate(ciFilters, field, value); } swappable S titleForObjectID(O id) { ret htmlEncode2(strOrNull(conceptForID(id))); } }