Libraryless. Click here for Pure Java version (21685L/133K).
1 | sclass HCRUD_Concepts<A extends Concept> extends HCRUD_Data { |
2 | Concepts cc = db_mainConcepts(); |
3 | Class<A> cClass; |
4 | new L<IVF1<A>> onCreateOrUpdate; |
5 | new L<IVF1<A>> onCreate; |
6 | IVF2<A, MapSO> afterUpdate; // parameter 2: old values |
7 | MapSO filters; // fields to filter by/add to new objects |
8 | SS ciFilters; // case-insensitive filters |
9 | IF1<Cl<A>> customFilter; |
10 | ValueConverterForField valueConverter; |
11 | bool referencesBlockDeletion; |
12 | bool trimAllSingleLineValues; |
13 | Set<S> fieldsToHideInCreationForm; |
14 | bool lockDB; // lock DB while updating object |
15 | bool verbose; |
16 | bool dropEmptyListValues = true; |
17 | bool lsMagic; |
18 | bool convertConceptValuesToRefs; |
19 | A currentConcept; // while editing |
20 | |
21 | bool useDynamicComboBoxes; // dynamically load concepts combo box entries for all fields |
22 | IPred<S> useDynamicComboBoxesForField; // activate dynamic combo boxes for selected fields |
23 | int dynamicComboBoxesThreshold = 1000; // if there are this many entries or more, use a dynamic combo box |
24 | |
25 | *(Class<A> *cClass) {} |
26 | *(Concepts *cc, Class<A> *cClass) {} |
27 | |
28 | // XXX - breaking change from just shortName() |
29 | swappable S itemName() { ret humanizeShortName(cClass); } |
30 | swappable S itemNamePlural() { ret super.itemNamePlural(); } |
31 | |
32 | //LS fields() { ret conceptFields(cClass); } |
33 | |
34 | L<A> itemsForListing() { |
35 | ret defaultSort(asList(listConcepts())); |
36 | } |
37 | |
38 | @Override |
39 | L<MapSO> list() { |
40 | //ret lazyMap itemToMapForList(itemsForListing()); |
41 | ret lazyMap(itemsForListing(), a -> new Item(str(a.id)) { |
42 | public MapSO calcFullMap() { ret itemToMapForList(a); } |
43 | }); |
44 | } |
45 | |
46 | // more efficient version - convert items only after taking subList |
47 | // TODO: this is never called by HCRUD; make lazy list instead? |
48 | @Override |
49 | L<MapSO> list(IntRange range) { |
50 | ret lambdaMap itemToMapForList(subListOrFull(itemsForListing(), range)); |
51 | } |
52 | |
53 | Cl<A> listConcepts() { |
54 | Cl<A> l = listConcepts_firstStep(); |
55 | ret postProcess(customFilter, l); |
56 | } |
57 | |
58 | swappable Cl<A> listConcepts_firstStep() { |
59 | if (empty(ciFilters)) |
60 | ret conceptsWhere(cc, cClass, mapToParams(filters)); |
61 | else if (empty(filters)) |
62 | ret conceptsWhereCI(cc, cClass, mapToParams(ciFilters)); |
63 | else { |
64 | // TODO: choose best index |
65 | Cl<A> l = conceptsWhere(cc, cClass, mapToParams(filters)); |
66 | ret filterConceptsIC(l, mapToParams(ciFilters)); |
67 | } |
68 | } |
69 | |
70 | swappable Pair<S, Bool> defaultSortField() { ret pair("id", true); } |
71 | |
72 | swappable L<A> defaultSort(L<A> l) { ret sortedByConceptIDDesc(l); } |
73 | |
74 | swappable A emptyConcept() { |
75 | ret unlisted(cClass); |
76 | } |
77 | |
78 | swappable MapSO emptyObject() { |
79 | A c = emptyConcept(); |
80 | |
81 | // not actually necessary here, we do it on create |
82 | /*cset(c, mapToParams(filters)); |
83 | cset(c, mapToParams(ciFilters));*/ |
84 | |
85 | MapSO map = itemToMap(c); |
86 | //printVars_str emptyObject(+cClass, +filters, +ciFilters, +map); |
87 | |
88 | ret mapMinusKeys(fieldsToHideInCreationForm, map); |
89 | } |
90 | |
91 | MapSO itemToMap(A c) { |
92 | if (c == null) null; |
93 | ret putKeysFirst(getFieldOrder(c), conceptToMap_gen_withNullValues(c)); |
94 | } |
95 | |
96 | MapSO itemToMapForList(A c) { |
97 | if (c == null) null; |
98 | MapSO map = itemToMap(c); |
99 | massageItemMapForList(c, map); |
100 | ret map; |
101 | } |
102 | |
103 | swappable void massageItemMapForList(A c, MapSO map) { |
104 | } |
105 | |
106 | swappable void massageItemMapForUpdate(A c, MapSO map) { |
107 | // Concepts.RefL magic |
108 | |
109 | Cl<Field> refLFields = nonStaticNonTransientFieldObjectsOfType(Concept.RefL.class, c); |
110 | printVars_str(+refLFields, +c); |
111 | for (Field f : refLFields) { |
112 | new TreeMap<Int, O> values; |
113 | new Matches m; |
114 | for (S key, O value : cloneMap(map)) |
115 | if (startsWith(key, f.getName() + "_", m) && isInteger(m.rest())) { |
116 | Concept concept = getConceptFromString((S) value); |
117 | //printVars_str("RefL magic", +key, +conceptID, +concept); |
118 | if (concept != null || !dropEmptyListValues) |
119 | values.put(parseInt(m.rest()), concept); |
120 | map.remove(key); |
121 | } |
122 | |
123 | if (!dropEmptyListValues) |
124 | while (nempty(values) && lastValue(values) == null) { |
125 | if (verbose) print("Dropping value " + lastEntry(values)); |
126 | removeLastKey(values); |
127 | } |
128 | |
129 | map.put(f.getName(), valuesAsList(values)); |
130 | } |
131 | |
132 | // process dynamic bool, concept fields |
133 | |
134 | for (S name : cloneKeys(map)) { |
135 | S metaInfo = metaInfoFromForm(name); |
136 | print("metaInfo for " + name + ": " + metaInfo); |
137 | if (eqic(metaInfo, "concept")) |
138 | replaceStringValueWithConcept(map, name); |
139 | else if (eqic(metaInfo, "bool")) |
140 | replaceStringValueWithBool(map, name); |
141 | } |
142 | |
143 | // Concepts.Ref magic (look up concept) |
144 | |
145 | for (Field f : nonStaticNonTransientFieldObjectsOfType(Concept.Ref.class, c)) |
146 | replaceStringValueWithConcept(map, f.getName()); |
147 | |
148 | // trim values |
149 | |
150 | if (trimAllSingleLineValues) |
151 | for (Map.Entry<S, O> e : map.entrySet()) { |
152 | S val = optCastString(e.getValue()); |
153 | if (val != null && isSingleLine(val) && isUntrimmed(val)) |
154 | e.setValue(trim(val)); |
155 | } |
156 | |
157 | // LS magic |
158 | |
159 | if (lsMagic) |
160 | for (Field f : nonStaticNonTransientFieldObjectsOfType(L.class, c)) { |
161 | if (eqOneOf(f.getName(), "refs", "backRefs")) continue; |
162 | new TreeMap<Int, S> values; |
163 | new Matches m; |
164 | |
165 | for (S key, O value : cloneMap(map)) { |
166 | if (startsWith(key, f.getName() + "_", m) && isInteger(m.rest())) { |
167 | if (!dropEmptyListValues || nempty((S) value)) { |
168 | if (verbose) print("Adding value " + m.rest() + " / " + value); |
169 | mapPut(values, parseInt(m.rest()), (S) value); |
170 | } |
171 | map.remove(key); |
172 | } |
173 | } |
174 | |
175 | if (!dropEmptyListValues) |
176 | while (nempty(values) && empty(lastValue(values))) { |
177 | if (verbose) print("Dropping value " + lastEntry(values)); |
178 | removeLastKey(values); |
179 | } |
180 | |
181 | map.put(f.getName(), valuesAsList(values)); |
182 | } |
183 | |
184 | // don't set SecretValue fields |
185 | |
186 | for (Field f : nonStaticNonTransientFieldObjectsOfType(SecretValue, c)) |
187 | map.remove(f.getName()); |
188 | } |
189 | |
190 | void replaceStringValueWithConcept(MapSO map, S key) { |
191 | O value = map.get(key); |
192 | if (value cast S) { |
193 | Concept concept = getConceptFromString(value); |
194 | map.put(key, concept); |
195 | } |
196 | } |
197 | |
198 | void replaceStringValueWithBool(MapSO map, S key) { |
199 | O value = map.get(key); |
200 | if (value cast S) { |
201 | map.put(key, englishStringToBool(value)); |
202 | } |
203 | } |
204 | |
205 | swappable MapSO getObject(O id) { |
206 | ret itemToMap(conceptForID(id)); |
207 | } |
208 | |
209 | MapSO getObjectForEdit(O id) { |
210 | currentConcept = conceptForID(id); |
211 | ret getObject(id); |
212 | } |
213 | |
214 | O createObject(SS fullMap, S fieldPrefix) { |
215 | rawFormValues = fullMap; |
216 | try { |
217 | SS map = extractFieldValues(fullMap, fieldPrefix); |
218 | A c = cnew(cc, cClass); |
219 | // make sure filters override |
220 | setValues(c, mapMinusKeys(map, filteredFields()), true); |
221 | cset(c, mapToParams(filters)); |
222 | cset(c, mapToParams(ciFilters)); |
223 | pcallFAll(onCreate, c); |
224 | pcallFAll(onCreateOrUpdate, c); |
225 | callOpt(c, "_onCreated"); // TODO: synchronize? |
226 | ret c.id; |
227 | } finally { |
228 | rawFormValues = null; |
229 | } |
230 | } |
231 | |
232 | void setValues(A c, SS map, bool creating) { |
233 | lock lockDB && !creating ? dbLock(cc) : null; |
234 | MapSO map2 = (Map) cloneMap(map); |
235 | massageItemMapForUpdate(c, map2); |
236 | if (verbose) { |
237 | print("setValues " + map); |
238 | print("backRefs: " + c.backRefs); |
239 | } |
240 | MapSO oldValues = !creating && afterUpdate != null |
241 | ? cgetAll_cloneLists(c, keys(map2)) : null; |
242 | if (convertConceptValuesToRefs) |
243 | convertAllConceptValuesToRefs(c, map2); |
244 | if (valueConverter == null) |
245 | cSmartSet(c, mapToParams(map2)); |
246 | else |
247 | cSmartSet_withConverter_pcall(verbose, valueConverter, c, mapToParams(map2)); |
248 | if (oldValues != null) |
249 | callF(afterUpdate, c, oldValues); |
250 | if (verbose) |
251 | print("backRefs: " + c.backRefs); |
252 | } |
253 | |
254 | // for dynamic fields |
255 | void convertAllConceptValuesToRefs(A c, MapSO map) { |
256 | for (S key, O value : cloneMap(map)) { |
257 | if (value cast Concept) |
258 | if (!hasField(c, key)) { |
259 | print("Converting value to ref: " + key); |
260 | map.put(key, c.new Ref(value)); |
261 | } |
262 | } |
263 | } |
264 | |
265 | A conceptForID(O id) { |
266 | ret _getConcept(cc, cClass, toLong(id)); |
267 | } |
268 | |
269 | S updateObject(O id, SS fullMap, S fieldPrefix) { |
270 | rawFormValues = fullMap; |
271 | try { |
272 | SS map = extractFieldValues(fullMap, fieldPrefix); |
273 | A c = conceptForID(id); |
274 | if (c == null) ret "Object " + id + " not found"; |
275 | try answer checkFilters(c); |
276 | setValues(c, map, false); |
277 | pcallFAll(onCreateOrUpdate, c); |
278 | ret "Object " + id + " updated"; |
279 | } finally { |
280 | rawFormValues = null; |
281 | } |
282 | } |
283 | |
284 | S deleteObject(O id) { |
285 | A c = conceptForID(id); |
286 | if (c == null) ret "Object " + id + " not found"; |
287 | try answer checkFilters(c); |
288 | actuallyDeleteConcept(c); |
289 | ret "Object " + id + " deleted"; |
290 | } |
291 | |
292 | swappable void actuallyDeleteConcept(A c) { |
293 | deleteConcept(c); |
294 | } |
295 | |
296 | S checkFilters(A c) { |
297 | if (!checkConceptFields(c, mapToParams(filters)) |
298 | || !checkConceptFieldsIC(c, mapToParams(ciFilters))) |
299 | ret "Object " + c.id + " not in view"; |
300 | ret ""; |
301 | } |
302 | |
303 | HCRUD_Concepts<A> addFilters(MapSO map) { |
304 | fOr (S field, O value : map) |
305 | addFilter(field, value); |
306 | this; |
307 | } |
308 | |
309 | HCRUD_Concepts<A> addFilter(S field, O value) { |
310 | filters = orderedMapPutOrCreate(filters, field, value); |
311 | this; |
312 | } |
313 | |
314 | // TODO: do this the other way around (check for editable types) |
315 | swappable bool isEditableValue(O value) { |
316 | //if (value instanceof Concept.RefL) true; // subsumed in next line |
317 | if (value instanceof L) true; |
318 | if (value instanceof Cl) false; |
319 | true; |
320 | } |
321 | |
322 | Renderer getRenderer(S field) { |
323 | if (!isEditableValue(currentValue)) |
324 | ret new NotEditable; |
325 | |
326 | Class type = fieldType(or(currentConcept, cClass), field); |
327 | S metaInfo = metaInfoFromForm(field); |
328 | //printVars_str("getRenderer", +cClass, +field, +type, +rawFormValues); |
329 | |
330 | if (eq(type, bool.class)) |
331 | ret new CheckBox; |
332 | |
333 | if (eq(type, Bool.class)) |
334 | ret new ComboBox(ll("", "yes", "no"), |
335 | b -> trueFalseNull((Bool) b, "yes", "no", "")); |
336 | |
337 | // show a Ref<> field as a combo box |
338 | |
339 | if (isSubtypeOf(type, Concept.Ref.class)) { |
340 | Class<? extends Concept> c = fieldTypeArg(field); |
341 | AbstractComboBox cb = makeConceptsComboBox(field, c); |
342 | cb.metaInfo = "concept"; |
343 | ret cb; |
344 | } |
345 | |
346 | // show dynamic field with concept/ref value as combo box |
347 | |
348 | O val = deref(currentValue); |
349 | if (val cast Concept) { |
350 | Class<? extends Concept> c = val.getClass(); |
351 | AbstractComboBox cb = makeConceptsComboBox(field, c); |
352 | cb.metaInfo = "concept"; |
353 | ret cb; |
354 | } |
355 | |
356 | // show dynamic ref field from URL parameters |
357 | |
358 | if (eqic(metaInfo, "concept")) { |
359 | printVars_str("metaInfo value", +field, +val); |
360 | AbstractComboBox cb = makeConceptsComboBox(field, Concept); // TODO |
361 | cb.metaInfo = "concept"; |
362 | ret cb; |
363 | } |
364 | |
365 | // show a RefL<> field as a list of combo boxes |
366 | |
367 | if (eq(type, Concept.RefL.class)) { |
368 | Class<? extends Concept> c = fieldTypeArg(field); |
369 | ret new FlexibleLengthList(makeConceptsComboBox(field, c)); |
370 | } |
371 | |
372 | if (eq(type, L.class)) { |
373 | //Class c = fieldTypeArg(field); |
374 | ret new FlexibleLengthList(new TextField(80)); |
375 | } |
376 | |
377 | if (val cast Bool) |
378 | ret new CheckBox; |
379 | |
380 | ret super.getRenderer(field); |
381 | } |
382 | |
383 | DynamicComboBox makeDynamicComboBox(S field, Class<? extends Concept> c) { |
384 | DynamicComboBox cb = new(field); |
385 | cb.valueToEntry = value -> { |
386 | value = deref(value); |
387 | //print("ComboBox: value type=" + _getClass(value); |
388 | long id = 0; |
389 | if (value cast Concept) id = conceptID(value); |
390 | else if (value instanceof S && isInteger((S) value)) id = parseLong(value); |
391 | if (id != 0) |
392 | ret comboBoxItem(_getConcept(cc, id)); |
393 | null; |
394 | }; |
395 | ret cb; |
396 | } |
397 | |
398 | AbstractComboBox makeConceptsComboBox(S field, Class<? extends Concept> c) { |
399 | if (c == null) fail("Null type for field \*field*/. currentConcept: " + currentConcept); |
400 | |
401 | if (useDynamicComboBoxes || useDynamicComboBoxesForField != null && useDynamicComboBoxesForField.get(field)) |
402 | ret makeDynamicComboBox(field, c); |
403 | |
404 | Cl concepts = listConceptClass(c); |
405 | if (l(concepts) >= dynamicComboBoxesThreshold) |
406 | ret makeDynamicComboBox(field, c); |
407 | LS entries = comboBoxItems(concepts); |
408 | |
409 | ComboBox cb = new(entries); |
410 | cb.valueToEntry = value -> { |
411 | value = deref(value); |
412 | //print("ComboBox: value type=" + _getClass(value); |
413 | long id = 0; |
414 | if (value cast Concept) id = conceptID(value); |
415 | else if (value instanceof S && isInteger((S) value)) id = parseLong(value); |
416 | if (id != 0) { |
417 | S entry = firstWhereFirstLongIs(entries, id); |
418 | //print("combobox selected: " + id + " / " + entry); |
419 | ret entry; |
420 | } |
421 | null; |
422 | }; |
423 | ret cb; |
424 | } |
425 | |
426 | // TODO: filters? |
427 | <A extends Concept> Cl<A> listConceptClass(Class<A> c) { |
428 | ret cc.list(c); |
429 | } |
430 | |
431 | LS comboBoxItemsForConceptClass(Class<? extends Concept> c) { |
432 | ret comboBoxItems(listConceptClass(c)); |
433 | } |
434 | |
435 | LS comboBoxItems(Cl<? extends Concept> l) { |
436 | ret comboBoxItems_static(l); |
437 | } |
438 | |
439 | static LS comboBoxItems_static(Cl<? extends Concept> l) { |
440 | ret itemPlus("", lmap comboBoxItem_static(l)); |
441 | } |
442 | |
443 | S comboBoxItem(Concept val) { |
444 | ret comboBoxItem_static(val); |
445 | } |
446 | |
447 | sS comboBoxItem_static(Concept val) { |
448 | ret val == null ? null : shorten(val.id + ": " + val); |
449 | } |
450 | |
451 | swappable bool objectCanBeDeleted(O id) { |
452 | ret !referencesBlockDeletion || !hasBackRefs(conceptForID(id)); |
453 | } |
454 | |
455 | Set<S> filteredFields() { ret joinSets(keys(filters), keys(ciFilters)); } |
456 | |
457 | Class<? extends Concept> fieldTypeArg(S field) { |
458 | ret getTypeArgumentAsClass(genericFieldType(or(currentConcept, cClass), field)); |
459 | } |
460 | |
461 | swappable Class<? extends Concept> conceptClassForComboBoxSearch(S info, S query) { |
462 | // info is the field name (Ref or RefL). get concept class |
463 | if (!isIdentifier(info)) ret cClass; |
464 | |
465 | S field = info; |
466 | Class<? extends Concept> c = fieldTypeArg(field); |
467 | |
468 | // simple hack to show list for dynamic/new fields |
469 | // assuming it's the same as the main class |
470 | // clients can improve on this |
471 | if (c == null) c = cClass; |
472 | |
473 | ret c; |
474 | } |
475 | |
476 | swappable LS comboBoxSearchBaseItems(S info, S query) { |
477 | var c = conceptClassForComboBoxSearch(info, query); |
478 | if (c == null) ret emptyList(); |
479 | ret comboBoxItemsForConceptClass(c); |
480 | } |
481 | |
482 | LS comboBoxSearch(S info, S query) { |
483 | LS items = comboBoxSearchBaseItems(info, query); |
484 | ret takeFirst(10, scoredSearch(query, items)); |
485 | } |
486 | |
487 | // to find the concept e.g. within massageFormMatrix |
488 | A conceptForMap(MapSO map) { |
489 | if (map == null) null; |
490 | long id = toLong(map.get(idField())); |
491 | ret id == 0 ? null : (A) _getConcept(cc, id); |
492 | } |
493 | |
494 | A getConcept(MapSO map) { |
495 | ret conceptForMap(map); |
496 | } |
497 | |
498 | SS extractFieldValues(SS fullMap, S fieldPrefix) { |
499 | ret subMapStartingWith_dropPrefix(fullMap, fieldPrefix); |
500 | } |
501 | |
502 | Concept getConceptFromString(S s) { |
503 | long conceptID = parseFirstLong(s); |
504 | ret _getConcept(cc, conceptID); |
505 | } |
506 | |
507 | S metaInfoFromForm(S field) { |
508 | ret mapGet(rawFormValues, "metaInfo_" + field); |
509 | } |
510 | |
511 | void addCIFilter(S field, S value) { |
512 | ciFilters = putOrCreate(ciFilters, field, value); |
513 | } |
514 | |
515 | swappable S titleForObjectID(O id) { |
516 | ret htmlEncode2(strOrNull(conceptForID(id))); |
517 | } |
518 | } |
download show line numbers debug dex old transpilations
Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1026002 |
Snippet name: | HCRUD_Concepts |
Eternal ID of this version: | #1026002/201 |
Text MD5: | 54c548eee75fe00ee9b2422e9ba511c3 |
Transpilation MD5: | c29b4ffe951e292ffb159455b14be011 |
Author: | stefan |
Category: | javax |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2021-11-15 23:08:51 |
Source code size: | 16155 bytes / 518 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 918 / 2135 |
Version history: | 200 change(s) |
Referenced in: | [show references] |