--- /dev/null
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_bones/src/model/combo_base_model.dart';
+import 'package:meta/meta.dart';
+import 'persistence.dart';
+
+/// A cache for objects coming from a persistence layer.
+/// Uses the least recently used algorithm to avoid overflow.
+class PersistenceCache {
+ static PersistenceCache _instance;
+ //static final regExpCombobox = RegExp(r'^\w\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$');
+ static final regExpCombobox =
+ RegExp(r'^\w+\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$');
+ final leastReasentlyUsed = <CacheEntry>[];
+ final map = <String, CacheEntry>{};
+ final maxEntries;
+ factory PersistenceCache() {
+ return _instance;
+ }
+ final ApplicationData application;
+ BaseLogger logger;
+
+ PersistenceCache.internal(this.application, this.maxEntries) {
+ this.logger = this.application.logger;
+ }
+
+ /// True constructor (of the singleton instance).
+ /// [application]: offers all needed data: logger, persistence...
+ /// [maxEntries]: the maximal entries of the cache. If a new entry exceeds
+ /// that number the least recently used entry is removed before.
+ factory PersistenceCache.create(ApplicationData application,
+ {int maxEntries = 256}) {
+ _instance = PersistenceCache.internal(application, maxEntries);
+ return _instance;
+ }
+
+ /// Returns the data (text, values) of a combobox specified by [key].
+ /// If the data are unavailable (e.g. a asynchronous request is running) the
+ /// result is null.
+ /// [key] has the format:
+ /// "<id>.<module>.<sqlName>;<colText> <colValue>;<param1> <param2>..."
+ /// e.g. "x99.role.list;role_name role_id;:role_name=% :role_active=T"
+ /// If [hasUndef] is true the first combobox list entry is '-' with the value null.
+ ComboboxData combobox(String key, {bool hasUndef = false}) {
+ ComboboxData rc;
+ if (regExpCombobox.firstMatch(key) == null) {
+ logger.error('wrong key syntax: $key');
+ } else if (map.containsKey(key)) {
+ final entry = updateLRU(key);
+ if (entry.ready) {
+ rc = ComboboxData(entry.list[0], entry.list[1]);
+ }
+ } else {
+ final parts = key.split(';');
+ final idModuleName = parts[0].split('.');
+ final nameValue = parts[1].split(' ');
+ final params = parts[2].split(' ');
+ final params2 = <String, dynamic>{};
+ params.forEach((element) {
+ final keyValue = element.split('=');
+ params2[keyValue[0]] = keyValue[1];
+ });
+ final entry = CacheEntry(key, CacheEntryType.combobox, [
+ hasUndef ? ['-'] : <String>[],
+ hasUndef ? <dynamic>[null] : <dynamic>[]
+ ]);
+ insert(entry);
+ application.persistence
+ .list(
+ module: idModuleName[1],
+ sqlName: idModuleName[2],
+ params: params2)
+ .then((rows) {
+ rows.forEach((row) {
+ entry.list[0].add(row[nameValue[0]]);
+ entry.list[1].add(row[nameValue[1]]);
+ });
+ entry.ready = true;
+ rc = ComboboxData(entry.list[0], entry.list[1]);
+ });
+ }
+ return rc;
+ }
+
+ /// Inserts an [cacheEntry] into the cache.
+ void insert(CacheEntry cacheEntry) {
+ if (leastReasentlyUsed.length >= maxEntries) {
+ map.remove(leastReasentlyUsed[0].key);
+ leastReasentlyUsed.removeAt(0);
+ }
+ leastReasentlyUsed.add(cacheEntry);
+ map[cacheEntry.key] = cacheEntry;
+ }
+
+ /// Moves the entry specified by [key] at the top of the Least Recently Used
+ /// stack.
+ /// Returns the entry specified by [key].
+ CacheEntry updateLRU(String key) {
+ final entry = map[key];
+ leastReasentlyUsed.remove(entry);
+ leastReasentlyUsed.add(entry);
+ return entry;
+ }
+}
+
+/// Specifies an cache entry.
+class CacheEntry {
+ final String key;
+ final CacheEntryType entryType;
+ final List list;
+ bool ready;
+ CacheEntry(this.key, this.entryType, this.list, {this.ready = false});
+}
+
+enum CacheEntryType {
+ combobox,
+ record,
+ list,
+}
--- /dev/null
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility that Flutter provides. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+ final logger = MemoryLogger(LEVEL_FINE);
+ PersistenceCache cache;
+ setUpAll(() {
+ final configuration = BaseConfiguration({
+ 'client': {
+ 'host': 'localhost',
+ 'port': 58011,
+ 'schema': 'http',
+ 'application': 'unittest',
+ 'version': '1.0.0',
+ }
+ }, logger);
+ final persistence = RestPersistence.fromConfig(configuration, logger);
+ final appData =
+ ApplicationData(configuration, null, null, persistence, logger);
+ cache = PersistenceCache.create(appData, maxEntries: 2);
+ });
+ group('basics', () {
+ test('combobox', () async {
+ final cache2 = PersistenceCache();
+ expect(cache2, isNotNull);
+ const key = 'id1.role.list;role_name role_id;:role_name=%';
+ const key2 = 'id2.role.list;role_name role_id;:role_name=%';
+ var data = cache.combobox(key, hasUndef: true);
+ var data2 = cache.combobox(key2, hasUndef: false);
+ expect(data, isNull);
+ expect(data2, isNull);
+ await sleep(200);
+ data = cache.combobox(key);
+ await sleep(200);
+ data2 = cache.combobox(key2);
+ expect(logger.errors.length, equals(0));
+ expect(data, isNotNull);
+ expect(data.texts.length, greaterThan(4));
+ expect(data.texts.length, equals(data.values.length));
+ expect(data2, isNotNull);
+ expect(data2.texts.length, greaterThan(4));
+ expect(data2.texts.length, equals(data2.values.length));
+ expect(data.texts.length, equals(data2.values.length + 1));
+ expect(cache.leastReasentlyUsed.length, equals(2));
+ const key3 = 'id3.role.list;role_name role_id;:role_name=%';
+ var data3 = cache.combobox(key3, hasUndef: false);
+ expect(cache.leastReasentlyUsed.length, equals(2));
+ expect(cache.map.containsKey(key), isFalse);
+ expect(cache.map.containsKey(key2), isTrue);
+ });
+ test('error', () {
+ logger.clear();
+ const key = 'id1.role.list;role_name+role_id;:role_name=%';
+ expect(cache.combobox(key), isNull);
+ expect(logger.errors.length, equals(1));
+ expect(logger.contains('wrong key syntax: id1.role.list;role_name+role_id;:role_name=%'), isTrue);
+ });
+ });
+}
+
+Future sleep(int millisec) {
+ return Future.delayed(Duration(milliseconds: millisec), () => null);
+}