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 extends Concept> 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 extends Concept> 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 extends Concept> 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 extends Concept> 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 extends Concept> 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 extends Concept> c) {
ret comboBoxItems(listConceptClass(c));
}
LS comboBoxItems(Cl extends Concept> l) {
ret comboBoxItems_static(l);
}
static LS comboBoxItems_static(Cl extends Concept> 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 extends Concept> fieldTypeArg(S field) {
ret getTypeArgumentAsClass(genericFieldType(or(currentConcept, cClass), field));
}
swappable Class extends Concept> 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 extends Concept> 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)));
}
}