]> gitweb.hamatoma.de Git - exhibition.git/commitdiff
Generator, i18n_text_parser, i18n, yaml_merger, rest_server
authorHamatoma <author.hamatoma.de>
Sun, 8 Aug 2021 12:45:08 +0000 (14:45 +0200)
committerHamatoma <author.hamatoma.de>
Sun, 8 Aug 2021 13:01:12 +0000 (15:01 +0200)
* new: Generator contains all meta data creation parts.
* new: i18n_text_parser parses Dart files for I18N texts.
* new: i18n allows multi language support.
* new: yaml_merger merges SQL statement definitions,
  normally generated files and hand made files.
* rest_server: scripts for simple installing on the backend.

22 files changed:
.gitignore
Meta [new file with mode: 0755]
bin/generator.dart [new file with mode: 0644]
bin/i18n_text_parser.dart [new file with mode: 0644]
bin/meta_tool.dart
bin/yaml_merger.dart [new file with mode: 0644]
lib/base/i18n.dart [new file with mode: 0644]
lib/meta/module_meta_data.dart
lib/meta/modules.dart
lib/meta/roles_meta.dart [new file with mode: 0644]
lib/meta/users_meta.dart
lib/page/r_data.dart [deleted file]
lib/page/users/user_list.dart
rest_server/CR [new file with mode: 0755]
rest_server/data/sql/users.sql.yaml
rest_server/lib/rest_server.dart
rest_server/tools/project.inc [new file with mode: 0644]
test/i18n_text_parser_test.dart [new file with mode: 0644]
test/yaml_merger_test.dart [new file with mode: 0644]
tools/CompileMerge [new file with mode: 0755]
tools/InitProject [new file with mode: 0755]
tools/PackRestServer [new file with mode: 0755]

index f8ae0fdef6501cc94cabac0901f519d14e461405..e75c7bdb5021119f4627828f974a64beee616dce 100644 (file)
@@ -50,3 +50,5 @@ ios/
 web/
 linux/
 pubspec.lock
+tools/meta_tool
+tools/yaml_merger
diff --git a/Meta b/Meta
new file mode 100755 (executable)
index 0000000..6668928
--- /dev/null
+++ b/Meta
@@ -0,0 +1,5 @@
+#! /bin/sh
+EXE=tools/meta_tool
+test ! -x $EXE && ./ReCreateMetaTool
+$EXE $*
+
diff --git a/bin/generator.dart b/bin/generator.dart
new file mode 100644 (file)
index 0000000..4cfb235
--- /dev/null
@@ -0,0 +1,339 @@
+import 'dart:io';
+
+import 'package:exhibition/meta/module_meta_data.dart';
+import 'package:exhibition/meta/modules.dart';
+import 'package:path/path.dart';
+
+/// Converts a [className] into a file name using Dart conventions.
+/// Example: "UserData" is converted to "user_data"
+String classToFilename(String className) {
+  String upperCase = className.toUpperCase();
+  String rc = '';
+  for (var ix = 0; ix < className.length; ix++) {
+    if (className[ix] == upperCase[ix] && ix > 0) {
+      rc += '_';
+    }
+    rc = className[ix];
+  }
+  return rc.toLowerCase();
+}
+
+/// Converts a [filename] into a class name using Dart conventions.
+/// Example: "user_data.dart" is converted to "UserData"
+String filenameToClass(String filename) {
+  final parts = basenameWithoutExtension(filename).split('_');
+  String rc = '';
+  for (var part in parts) {
+    if (part.isNotEmpty) {
+      rc += part[0].toUpperCase() + part.substring(1);
+    }
+  }
+  return rc;
+}
+
+class Generator {
+  /// Appends a [string] at the end of a [buffer].
+  /// If [buffer] is null a new instance is created.
+  /// If the last line in [buffer] has a length greater than [maxLength]
+  /// a newline is inserted and [indent] spaces.
+  /// If [separator] is not null that is inserted above the [string].
+  /// Returns the buffer (for chaining).
+  StringBuffer addToBuffer(String string,
+      {required int maxLength,
+      String? separator,
+      int indent = 0,
+      StringBuffer? buffer}) {
+    buffer ??= StringBuffer();
+    if (separator != null) {
+      buffer.write(separator);
+    }
+    final last = buffer.toString().lastIndexOf('\n');
+    if (buffer.length - (last < 0 ? 0 : last) + string.length > maxLength) {
+      buffer.write('\n' + (' ' * indent));
+    }
+    buffer.write(string);
+    return buffer;
+  }
+
+  ModuleMetaData? checkModule(List<String> args, int index) {
+    ModuleMetaData? rc;
+    if (index >= args.length) {
+      out('+++ too few arguments. Missing MODULE.');
+    } else if ((rc = moduleByName(args[index])) == null) {
+      out('+++ unknown module: ${args[index]}');
+      rc = null;
+    }
+    return rc;
+  }
+
+  /// Returns a DDL statement creating the database table of the [module].
+  String createDbTable(ModuleMetaData module) {
+    final tableName = module.tableName;
+    final list = module.list;
+    final buffer = StringBuffer();
+    buffer.write('-- DO NOT CHANGE. This file is created by the meta_tool\n');
+    buffer.write('CREATE TABLE $tableName (\n');
+    String? primary;
+    for (var item in list) {
+      if (item.options.contains('primary')) {
+        primary = item.columnName;
+      }
+      buffer.write(' ${item.columnName}');
+      buffer.write(' ' + module.mySqlType(item.dataType, item.size));
+      buffer.write(module.dbOptions(item) + ',\n');
+    }
+    buffer.write('  PRIMARY KEY ($primary)\n');
+    buffer.write(');\n');
+    return buffer.toString();
+  }
+
+  /// Returns a Dart class definition of the [module].
+  String createModuleData(ModuleMetaData module) {
+    final buffer = StringBuffer();
+    buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n');
+    buffer.write("import '../../base/defines.dart';\n");
+    buffer.write("import '../../base/helper.dart';\n");
+    buffer.write('class ${module.moduleNameSingular}Data{\n');
+    for (var item in module.list) {
+      buffer.write('  ' + module.dartType(item.dataType) + '? ${item.name};\n');
+    }
+    buffer.write('  ${module.moduleNameSingular}Data({');
+    String separator = '';
+    for (var item in module.list) {
+      buffer.write('$separator this.${item.name}');
+      separator = ',';
+    }
+    buffer.write('});\n');
+    buffer
+        .write('  ${module.moduleNameSingular}Data.fromYaml(YamlMap data) {\n');
+    for (var item in module.list) {
+      final name = item.columnName;
+      final type = item.dataType.toString();
+      //id = data.containsKey('id') ? int.tryParse(data['id']) : null;
+      buffer.write(
+          "    ${item.name} = data.containsKey('$name') ? valueOf($type, data['$name']) : null;\n");
+    }
+    buffer.write('  }\n');
+    buffer.write('  static DataType? dataTypeOf(String name) {\n');
+    buffer.write('    DataType? rc;\n');
+    buffer.write('    switch(name){\n');
+    for (var item in module.list) {
+      buffer.write("    case '${item.name}':\n");
+      buffer.write('      rc = ${item.dataType};\n');
+      buffer.write('      break;\n');
+    }
+    buffer.write('''    default:
+      break;
+    }
+    return rc;
+  }
+''');
+    buffer.write('}\n');
+
+    return buffer.toString();
+  }
+
+  /// Creates the file modules.dart.
+  String createModules() {
+    final buffer = StringBuffer();
+    buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n');
+    buffer.write("import 'dart:io';\n");
+    buffer.write("import 'module_meta_data.dart';\n");
+    final modules = <String>[];
+    final files = <String>[];
+    final fileOfModule = <String, String>{};
+
+    for (var item in Directory('lib/meta').listSync()) {
+      final name = basename(item.path);
+      if (name.endsWith('_meta.dart')) {
+        final moduleName = filenameToClass(name.replaceFirst('_meta.dart', ''));
+        fileOfModule[moduleName] = name;
+        modules.add(moduleName);
+        files.add(name);
+      }
+    }
+    modules.sort();
+    files.sort();
+    for (var file in files) {
+      buffer.write("import '$file';\n");
+    }
+    buffer.write('''
+/// Returns the meta data of the module given by [name].
+/// Returns null if not found.
+ModuleMetaData? moduleByName(String name) {
+  ModuleMetaData? rc;
+  switch (name) {
+''');
+    for (var module in modules) {
+      buffer.write("    case '$module':\n");
+      final className = findClass(fileOfModule[module]!);
+      buffer.write("      rc = $className();\n");
+      buffer.write("      break;\n");
+    }
+    buffer.write('''    default:
+      break;
+  }
+  return rc;
+}
+/// Returns the module names as string list.
+List<String> moduleNames(){
+  return [
+''');
+    for (var module in modules) {
+      buffer.write("    '$module',\n");
+    }
+    buffer.write('''  ];
+}
+''');
+    return buffer.toString();
+  }
+
+  /// Returns the SQL statements for insert, update, delete...
+  /// for a given [module].
+  /// This yaml file is a configuration for the rest_server.
+  String createSqlStatements(ModuleMetaData module) {
+    final moduleName = module.moduleName;
+    final tableName = module.tableName;
+    final list = module.list;
+    final buffer = StringBuffer();
+    buffer.write('---\n');
+    buffer.write('# DO NOT CHANGE. This file is created by the meta_tool\n');
+    buffer.write('''# SQL statements of the module "$moduleName":\n
+module: $moduleName
+list:
+  type: list
+  parameters: []
+  sql: "select * from $tableName;"
+byId:
+  type: record
+  parameters: [ "${list[0].name}" ]
+  sql: "select * from $tableName where ${list[0].columnName}=:${list[0].name};"
+delete:
+  type: delete
+  parameters: [ "${list[0].name}" ]
+  sql: "delete * from $tableName where ${list[0].columnName}=:${list[0].name};"
+update:
+  type: update
+''');
+    var items = module.standardColumns('changedBy');
+    var parameters = addToBuffer('  parameters: [', maxLength: 80);
+    var assignments = addToBuffer('    ', maxLength: 80);
+    var first = true;
+    for (var item in items) {
+      addToBuffer('":${item.name}"',
+          maxLength: 80,
+          separator: first ? null : ',',
+          indent: 4,
+          buffer: parameters);
+      addToBuffer('${item.columnName}=:${item.name}',
+          maxLength: 80,
+          separator: first ? null : ',',
+          indent: 4,
+          buffer: assignments);
+      first = false;
+    }
+    parameters.write(']');
+    var item = module.properties['changed']!;
+    addToBuffer('${item.columnName}=NOW()',
+        maxLength: 80, separator: ',', indent: 4, buffer: assignments);
+    // parameters: [":id", ":name", ":displayname", ":email", ":changedby"]
+    buffer.writeln(parameters);
+    buffer.write('  sql: "UPDATE $tableName SET\n');
+    buffer.writeln(assignments);
+    //user_name=:name, user_displayname=:displayname, user_email=:email, user_changed=NOW(), user_changedby=:changedby
+    buffer.write('    WHERE ${list[0].columnName}=:${list[0].name};"\n');
+
+    items = module.standardColumns('createdBy');
+    parameters = addToBuffer('  parameters: [', maxLength: 80);
+    final sql1 = addToBuffer('  sql: "INSERT INTO $tableName(', maxLength: 80);
+    final sql2 = addToBuffer('    VALUES(', maxLength: 80);
+    first = true;
+    for (var item in items) {
+      addToBuffer('":${item.name}"',
+          maxLength: 80,
+          separator: first ? null : ',',
+          indent: 4,
+          buffer: parameters);
+      addToBuffer('${item.columnName}',
+          maxLength: 80,
+          separator: first ? null : ',',
+          indent: 6,
+          buffer: sql1);
+      addToBuffer(':${item.name}',
+          maxLength: 80,
+          separator: first ? null : ',',
+          indent: 6,
+          buffer: sql2);
+      first = false;
+    }
+    parameters.write(']');
+    item = module.properties['created']!;
+    addToBuffer('${item.columnName})',
+        maxLength: 80, separator: ',', indent: 4, buffer: sql1);
+    addToBuffer('NOW());"',
+        maxLength: 80, separator: ',', indent: 4, buffer: sql2);
+    // parameters: [":id", ":name", ":displayname", ":email", ":changedby"]
+    buffer.write('''insert:
+  type: insert
+''');
+    buffer.writeln(parameters);
+    buffer.writeln(sql1);
+    buffer.writeln(sql2);
+    return buffer.toString();
+  }
+
+  /// Finds the class name of a module meta class given by the file [node].
+  String findClass(String node) {
+    final contents = File('lib/meta/$node').readAsStringSync();
+    RegExpMatch? match;
+    String rc;
+    if ((match = RegExp(r'class\s+(\w+)\s').firstMatch(contents)) != null) {
+      rc = match!.group(1)!;
+    } else {
+      print('+++ missing class in $node');
+      rc = filenameToClass(node.replaceFirst('.dart', ''));
+    }
+    return rc;
+  }
+
+  /// Replaces print().
+  void out(String line) {
+    stdout.write(line + '\n');
+  }
+  /// Generates all modules defined in the meta data.
+  void updateModules(Generator generator) {
+    writeFile('lib/meta/modules.dart', createModules());
+    final modules = moduleNames();
+    for (var name in modules) {
+      ModuleMetaData? module = moduleByName(name);
+      if (module == null) {
+        print('+++ unknown module: $name');
+      } else {
+        String filename =
+            classToFilename(module.moduleNameSingular) + '_data.dart';
+        final directory = name.toLowerCase();
+        writeFile('lib/page/$directory/$filename',
+            generator.createModuleData(module));
+      }
+    }
+  }
+  /// Generates the Sql statement file for each module defined in the meta data.
+  void updateSql(Generator generator) {
+    final modules = moduleNames();
+    for (var name in modules) {
+      ModuleMetaData? module = moduleByName(name);
+      if (module == null) {
+        print('+++ unknown module: $name');
+      } else {
+        String filename = 'rest_server/data/sql/${name.toLowerCase()}.sql.yaml';
+        writeFile(filename, generator.createSqlStatements(module));
+      }
+    }
+  }
+
+  void writeFile(String filename, String contents) {
+    out('creating $filename ...');
+    final file = File(filename);
+    file.writeAsStringSync(contents);
+  }
+}
diff --git a/bin/i18n_text_parser.dart b/bin/i18n_text_parser.dart
new file mode 100644 (file)
index 0000000..bbf660d
--- /dev/null
@@ -0,0 +1,194 @@
+import "dart:io";
+
+import 'package:dart_bones/dart_bones.dart';
+import "package:path/path.dart";
+
+void main(List<String> args) {}
+
+class I18nTextParser {
+  final BaseLogger logger;
+  final modules = <String, Map<String, String>>{};
+  final foundFiles = <String>{};
+  var regExpFiles = RegExp(r'.dart$');
+  String currentModule = '';
+  final moduleVariables = <String, String>{};
+  String currentFile = '';
+  List<String> lines = [];
+  int currentLineNo = 0;
+  // ...........................1.............1.........2
+  final regExpModule =
+      RegExp(r'''(\w+) = (I18N\(\)|i18N)\.module\(["'](.*?)['"]\);''');
+  final regExpDelimiter = RegExp(r'''["']''');
+  // ..........1.............1..2..............................2
+  final regExpText =
+      RegExp(r'(i18N|I18N\(\))\.(tr|trPlural|trMulti|trWithArgs)\(');
+  final regExpStringConstant = RegExp(r'''^\s*(r?)(["'])(.*?)\2''');
+  final regExpVariable = RegExp(r'^\w+');
+  final regExpEmptyString = RegExp(r'^\s*$');
+
+  I18nTextParser(this.logger);
+
+  /// Gets the text (multiple lines) from the [arguments].
+  /// [arguments] is the string behind the 'trMulti('.
+  void handleMultiText(String arguments) {
+    logger.error('not implemented: trMulti()');
+  }
+
+  /// Gets the text (multiple lines) from the [arguments] .
+  /// [arguments] is the string behind the 'trPlural('.
+  void handlePluralText(String arguments) {
+    logger.error('not implemented: trPlural()');
+  }
+
+  /// Gets the text (single line) from the [arguments] and store it.
+  /// [arguments] is the string behind the 'tr('.
+  void handleSimpleText(String arguments) {
+    RegExpMatch? match;
+    String module = '!global';
+    int lineNo = currentLineNo;
+    if (regExpEmptyString.firstMatch(arguments) != null) {
+      arguments = lineNo >= lines.length ? '' : lines[lineNo++].trimLeft();
+    }
+    String? key;
+    if ((match = regExpStringConstant.firstMatch(arguments)) != null) {
+      key = match?.group(3);
+      arguments = arguments.substring(match!.end).trimLeft();
+      if (arguments.startsWith(',')) {
+        arguments = arguments.substring(1).trimLeft();
+        if (arguments.isEmpty) {
+          arguments = lineNo >= lines.length ? '' : lines[lineNo++].trimLeft();
+        }
+        if ((match = regExpStringConstant.firstMatch(arguments)) != null) {
+          module = match!.group(3)!;
+        } else if ((match = regExpVariable.firstMatch(arguments)) != null) {
+          final variable = match!.group(0);
+          if (moduleVariables.containsKey(variable)) {
+            module = moduleVariables[variable]!;
+          } else {
+            logger.error(
+                '$currentFile-$currentLineNo: unknown module variable: $variable');
+          }
+        } else {
+          module = currentModule;
+        }
+      }
+    }
+    if (key == null) {
+      logger.error(
+          '$currentFile-$currentLineNo: cannot analyse parameters of tr()');
+    } else {
+      putText(key, currentFile, currentLineNo, module);
+    }
+  }
+
+  /// Gets the text (with placeholders) from the [arguments] and store it.
+  /// [arguments] is the string behind the 'trArgs('.
+  void handleTextWithArgs(String? arguments) {
+    logger.error('not implemented: trArgs()');
+  }
+
+  /// Stores the [key] in the [module] map with location info [filename] and
+  /// [lineNo].
+  void putText(String key, String filename, int lineNo, [String module = '']) {
+    if (module.isEmpty) {
+      module = currentModule;
+    }
+    if (!modules.containsKey(module)) {
+      modules[module] = <String, String>{};
+    }
+    if (modules[module]!.containsKey(key)) {
+      modules[module]![key] = modules[module]![key]! + '\n#: $filename:$lineNo';
+    } else {
+      modules[module]![key] = '#: $filename:$lineNo';
+    }
+  }
+
+  /// Scans recursively all dart files of the [directory].
+  void scanDirectory(
+    String directory,
+  ) {
+    final base = Directory(directory);
+    final subDirs = [];
+    try {
+      for (var file in base.listSync()) {
+        if (file is Directory) {
+          subDirs.add(file.path);
+          continue;
+        }
+        if (regExpFiles.firstMatch(file.path) == null) {
+          continue;
+        }
+        if (file is File) {
+          scanFile(file, directory);
+        } else {
+          logger.error('ignored: $directory/${file.path}');
+        }
+      }
+    } on FileSystemException catch (exc) {
+      logger.error('ignored: $directory: $exc');
+    }
+    for (var node in subDirs) {
+      scanDirectory(join(directory, node));
+    }
+  }
+
+  /// Scans the [file] for I18N texts.
+  /// [directory]: used for logging.
+  void scanFile(File file, String directory) {
+    try {
+      currentFile = join(directory, file.path);
+      lines = file.readAsLinesSync();
+      moduleVariables.clear();
+      currentLineNo = 0;
+      for (var line in lines) {
+        ++currentLineNo;
+        RegExpMatch? match;
+        if ((match = regExpModule.firstMatch(line)) != null) {
+          currentModule = match!.group(3)!;
+          moduleVariables[match.group(1)!] = currentModule;
+        } else {
+          for (match in regExpText.allMatches(line)) {
+            final restLine = line.substring(match.end);
+            switch (match.group(2)!) {
+              case 'tr':
+                handleSimpleText(restLine);
+                break;
+              case 'trMulti':
+                handleMultiText(restLine);
+                break;
+              case 'trPlural':
+                handlePluralText(restLine);
+                break;
+              case 'trArgs':
+                handleTextWithArgs(restLine);
+                break;
+            }
+          }
+        }
+      }
+    } on FileSystemException catch (exc) {
+      logger.error('ignored: $directory: $exc');
+    }
+  }
+
+  /// Writes the *.pot files to the [directory].
+  void writePot(String directory) {
+    FileSync().ensureDirectory(directory);
+    for (var module in modules.keys) {
+      final map = modules[module]!;
+      final keys = map.keys.toList();
+      keys.sort();
+      final buffer = StringBuffer('# Texts of module $module, parsed by i18n_text_parser\n');
+      for (var key in keys) {
+          buffer.writeln();
+        buffer.writeln(map[key]);
+        final key2 = key.replaceAll('"', r'\"');
+        buffer.writeln('msgid "$key2"');
+        buffer.writeln('msgstr ""');
+      }
+      final fn = join(directory, '$module.pot');
+      logger.log('writing $fn', LEVEL_DETAIL);
+      File(fn).writeAsStringSync(buffer.toString());
+    }
+  }
+}
index 5f0f97c0ab4227de83bea29e672d8f6f6ee905b6..13af6c5f64ae28513f97781d23ecccce3017d366 100644 (file)
-import 'dart:io';
-
+import 'package:exhibition/base/i18n.dart';
 import 'package:exhibition/meta/module_meta_data.dart';
 import 'package:exhibition/meta/modules.dart';
-import 'package:path/path.dart';
 
-void usage(){
-  out('''Usage: meta_tool MODE MODULE
-  Generate code specified by MODE for the module MODULE.
-MODE:
-  all-modules
-    Prints the module names.
-  print-modules
-    Prints the file lib/meta/modules.dart.
-  print-table MODULE
-    Prints the SQL table definition of MODULE.
-  print-data MODULE
-    Creates the data storage class of MODULE
-  update-modules
-    Generates all module files depending on the meta data.
-MODULE: 
-  The name of the module, e.g. "Users"
-Example:
-meta_tool print-table Users
-''');
-}
+import 'generator.dart';
+
 void main(List<String> args) {
+  final generator = Generator();
+  I18N.internal('data/i18n');
   if (args.isEmpty) {
-    usage();
-    out('+++ missing arguments.');
+    usage(generator);
+    generator.out('+++ missing arguments.');
   } else {
     ModuleMetaData? metaData;
     switch (args[0]) {
       case 'all-modules':
-        out(moduleNames().join('\n'));
+        generator.out(moduleNames().join('\n'));
         break;
       case 'print-modules':
-        out(createModules());
+        generator.out(generator.createModules());
         break;
       case 'print-table':
-        if ((metaData = checkModule(args, 1)) != null) {
-          out(metaData!.createDbTable());
+        if ((metaData = generator.checkModule(args, 1)) != null) {
+          generator.out(generator.createDbTable(metaData!));
+        }
+        break;
+      case 'print-sql':
+        if ((metaData = generator.checkModule(args, 1)) != null) {
+          generator.out(generator.createSqlStatements(metaData!));
         }
         break;
       case 'print-data':
-        if ((metaData = checkModule(args, 1)) != null) {
-          out(metaData!.createModuleData());
+        if ((metaData = generator.checkModule(args, 1)) != null) {
+          generator.out(generator.createModuleData(metaData!));
         }
         break;
       case 'update-modules':
-        updateModules();
+        generator.updateModules(generator);
+        break;
+      case 'update-sql':
+        generator.updateSql(generator);
         break;
       default:
-        usage();
-        out('+++ unknown MODE: ${args[0]}.');
+        usage(generator);
+        generator.out('+++ unknown MODE: ${args[0]}.');
         break;
     }
   }
 }
 
-ModuleMetaData? checkModule(List<String> args, int index) {
-  ModuleMetaData? rc;
-  if (index >= args.length) {
-    out('+++ too few arguments. Missing MODULE.');
-  } else {
-    rc = moduleByName(args[index]);
-  }
-  return rc;
-}
-
-String createModules() {
-  final buffer = StringBuffer();
-  buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n');
-  buffer.write("import 'dart:io';\n");
-  buffer.write("import 'module_meta_data.dart';\n");
-  final modules = <String>[];
-  final files = <String>[];
-  final fileOfModule = <String, String>{};
-
-  for (var item in Directory('lib/meta').listSync()) {
-    final name = basename(item.path);
-    if (name.endsWith('_meta.dart')) {
-      final moduleName = filenameToClass(name.replaceFirst('_meta.dart', ''));
-      fileOfModule[moduleName] = name;
-      modules.add(moduleName);
-      files.add(name);
-    }
-  }
-  modules.sort();
-  files.sort();
-  for (var file in files) {
-    buffer.write("import '$file';\n");
-  }
-  buffer.write('''
-/// Returns the meta data of the module given by [name].
-ModuleMetaData moduleByName(String name) {
-  ModuleMetaData rc;
-  switch (name) {
-''');
-  String? firstClass;
-  for (var module in modules) {
-    buffer.write("    case 'Users':\n");
-    final className = findClass(fileOfModule[module]!);
-    firstClass ??= className;
-    buffer.write("      rc = $className();\n");
-    buffer.write("      break;\n");
-  }
-  firstClass ??= 'MissingFirstClass';
-  buffer.write('''    default:
-      stdout.write('+++ unknown module: \$name. Using "$firstClass".');
-      rc = $firstClass();
-      break;
-  }
-  return rc;
-}
-/// Returns the module names as string list.
-List<String> moduleNames(){
-  return [
-''');
-  for (var module in modules) {
-    buffer.write("    '$module',\n");
-  }
-  buffer.write('''  ];
-}
+void usage(Generator generator) {
+  generator.out('''Usage: meta_tool MODE MODULE
+  Generate code specified by MODE for the module MODULE.
+MODE:
+  all-modules
+    Prints the module names.
+  print-modules
+    Prints the file lib/meta/modules.dart.
+  print-table MODULE
+    Prints the SQL table definition of MODULE.
+  print-data MODULE
+    Creates the data storage class of MODULE
+  print-sql MODULE
+    Creates the SQL statements (for the rest_server) of MODULE
+  update-modules
+    Generates all module files depending on the meta data.
+  update-sql
+    Generates all Sql statement files depending on the meta data.
+MODULE: 
+  The name of the module, e.g. "Users"
+Example:
+meta_tool print-table Users
 ''');
-  return buffer.toString();
 }
-
-/// Finds the class name of a module meta class given by the file [node].
-String findClass(String node) {
-  final contents = File('lib/meta/$node').readAsStringSync();
-  RegExpMatch? match;
-  String rc;
-  if ((match = RegExp(r'class\s+(\w+)\s').firstMatch(contents)) != null) {
-    rc = match!.group(1)!;
-  } else {
-    out('+++ missing class in $node');
-    rc = filenameToClass(node.replaceFirst('.dart', ''));
-  }
-  return rc;
-}
-
-/// Replaces print().
-void out(String line) {
-  stdout.write(line + '\n');
-}
-
-void writeFile(String filename, String contents){
-  out('creating $filename ...');
-  final file = File(filename);
-  file.writeAsStringSync(contents);
-}
-void updateModules(){
-  writeFile('lib/meta/modules.dart', createModules());
-  final modules = moduleNames();
-  for (var name in modules){
-    ModuleMetaData module = moduleByName(name);
-    String filename = classToFilename(module.moduleNameSingular) + '_data.dart';
-    final directory = name.toLowerCase();
-    writeFile('lib/page/$directory/$filename', module.createModuleData());
-  }
-}
\ No newline at end of file
diff --git a/bin/yaml_merger.dart b/bin/yaml_merger.dart
new file mode 100644 (file)
index 0000000..95f56e8
--- /dev/null
@@ -0,0 +1,163 @@
+import 'dart:io';
+
+import 'package:path/path.dart';
+import 'package:dart_bones/dart_bones.dart';
+
+/// Implements a merge tool for yaml files.
+/// @see merge() for description of merge.
+class Merger {
+  RegExp regExpKey = RegExp(r'^([a-zA-Z]\w+):\s*$');
+  RegExp regExpKeyValue = RegExp(r'^ ');
+  final BaseLogger logger;
+  final precedenceKeys = <String, String>{};
+  Merger(this.logger);
+
+  /// Parses the keys of a given [file] into [this.precedenceKeys].
+  void parseKeys(File file){
+    precedenceKeys.clear();
+    final lines = file.readAsLinesSync();
+    var state = 'undef';
+    var lastKey = '';
+    final keyValue = [];
+    for (var line in lines){
+      final match = regExpKey.firstMatch(line);
+      if (match != null){
+        if (keyValue.isNotEmpty){
+          precedenceKeys[lastKey] = keyValue.join('\n');
+          keyValue.clear();
+        }
+        state = 'inKey';
+        lastKey = match.group(1)!;
+      } else {
+        if (regExpKeyValue.firstMatch(line) != null){
+          if (state == 'inKey'){
+            keyValue.add(line);
+          }
+        }
+      }
+    }
+    if (keyValue.isNotEmpty){
+      precedenceKeys[lastKey] = keyValue.join('\n');
+    }
+  }
+  /// Merges the [fileSource] and [filePreference] into the [target] file.
+  void mergeOneFile(File fileSource, File filePreference, String target) {
+    parseKeys(filePreference);
+    final output = <String>[];
+    final foundKeys = <String>{};
+    final lines = fileSource.readAsLinesSync();
+    var state = 'undef';
+    var lastKey = '';
+    final keyValue = [];
+    for (var line in lines){
+      final match = regExpKey.firstMatch(line);
+      if (match != null) {
+        if (keyValue.isNotEmpty) {
+          output.add(lastKey + ':');
+          output.add(precedenceKeys.containsKey(lastKey)
+              ? precedenceKeys[lastKey]!
+              : keyValue.join('\n'));
+        }
+        keyValue.clear();
+        state = 'inKey';
+        lastKey = match.group(1)!;
+        if (foundKeys.contains(lastKey)){
+          logger.error('key $lastKey found multiple times');
+        }
+        foundKeys.add(lastKey);
+      } else {
+        if (regExpKeyValue.firstMatch(line) != null){
+          if (state == 'inKey'){
+            keyValue.add(line);
+          }
+        } else {
+          output.add(line);
+        }
+      }
+    }
+    if (keyValue.isNotEmpty){
+      output.add(lastKey + ':');
+      if (precedenceKeys.containsKey(lastKey)) {
+        output.add(precedenceKeys[lastKey]!);
+      } else {
+        output.add(keyValue.join('\n'));
+      }
+    }
+    for (var key in precedenceKeys.keys){
+      if (! foundKeys.contains(key)){
+        output.add(key + ':');
+        output.add(precedenceKeys[key]!);
+      }
+    }
+    File(target).writeAsStringSync(output.join('\n') + '\n');
+  }
+  /// Merges or copies files from two source directories into a target directory.
+  /// [pathSource] contains files with lower priorities.
+  /// [pathPreferences] contains files with higher priorities.
+  /// [pathTarget] is the target directory.
+  /// Only files with the same node will be merged.
+  /// Merging: the two source files contains keys. If a key is part of both
+  /// files the key content of of the higher priority file is taken.
+  void merge(String pathSource, String pathPreference, String pathTarget) {
+    final dirSource = Directory(pathSource);
+    final dirPreference = Directory(pathPreference);
+    final dirTarget = Directory(pathTarget);
+    if (!dirSource.existsSync()) {
+      logger.error('source not a directory: $pathSource');
+    } else if (!dirPreference.existsSync()) {
+      logger.error('source not a directory: $pathPreference');
+    } else if (!dirTarget.existsSync()) {
+      logger.error('source not a directory: $pathTarget');
+    } else {
+      /// Copy/merge the files from dirSource:
+      for (var file in dirSource.listSync()) {
+        final node = basename(file.path);
+        if (! node.endsWith('.yaml')){
+          continue;
+        }
+        final fnPreference = join(pathPreference, node);
+        final fnTarget = join(pathTarget, node);
+        final filePreference = File(fnPreference);
+        if (!(file is File)) {
+          logger.error('not a file: ${file.path} ignoring...');
+        } else if (filePreference.existsSync()) {
+          mergeOneFile(file, filePreference, fnTarget);
+        } else {
+          logger.log('copying ${file.path}', LEVEL_DETAIL);
+          file.copy(fnTarget);
+        }
+      }
+
+      /// Copy the "single" files from preferences:
+      for (var file in dirPreference.listSync()) {
+        final node = basename(file.path);
+        if (! node.endsWith('.yaml')){
+          continue;
+        }
+        final fnSource = join(pathSource, node);
+        final fnTarget = join(pathTarget, node);
+        File fileSource = File(fnSource);
+        if (file is File && !fileSource.existsSync()) {
+          logger.log('copying ${file.path}', LEVEL_DETAIL);
+          file.copy(fnTarget);
+        }
+      }
+    }
+  }
+}
+void main(List<String> args){
+  final logger = MemoryLogger(LEVEL_DETAIL);
+  if (args.length != 3){
+    print('''Usage: yaml_merger DIR_SOURCE DIR_PREFERENCE DIR_OUTPUT
+  Merges the yaml files from DIR_SOURCE (with lower priority) and DIR_PREFERENCE
+  (with higher priority) into files in DIR_OUTPUT.
+Version: 0.1.0
+Example:
+yaml_merger data/sql data/sql/precedence /tmp/sql
+''');
+    print('+++ wrong count of arguments: ${args.length} instead of 3');
+  } else {
+    final merger = Merger(logger);
+    merger.merge(args[0], args[1], args[2]);
+  }
+}
\ No newline at end of file
diff --git a/lib/base/i18n.dart b/lib/base/i18n.dart
new file mode 100644 (file)
index 0000000..0763460
--- /dev/null
@@ -0,0 +1,55 @@
+class I18N {
+  static I18N? instance;
+  Map<String, Map<String, String>> modules = {};
+  String locale = 'de';
+  factory I18N() {
+    return instance!;
+  }
+  I18N.internal(String directory) {
+    instance = this;
+  }
+  String module(String name) {
+    return name;
+  }
+
+  /// Translates the [key] into the local language using the namespace [module].
+  /// Returns the translation or (if not found) the key.
+  String tr(String key, [String module = '!global']) {
+    String? rc;
+    final mapModule = modules[module];
+    if (mapModule != null) {
+      rc = mapModule[key];
+      if (rc == null) {
+        if (module == '!global') {
+          rc = key;
+        } else {
+          rc = tr(key, '!global');
+        }
+      }
+    }
+    return rc!;
+  }
+
+  /// Translates the [key] into the local language using the namespace [module].
+  /// [key] contains placeholders "{0}", "{1}"... which will be replaced by
+  /// the matching entry in the array [args].
+  /// Returns the translation or (if not found) the key.
+  String trArgs(String key, List<Object> args, [String module = '!global']) {
+    String rc = tr(key, module);
+    for (var ix = 0; ix < args.length; ix++) {
+      rc = rc.replaceAll('{$ix}', args[ix].toString());
+    }
+    return rc;
+  }
+
+  /// Translates the [keySingular] or [keyPlural] into the local language
+  /// using the namespace [module].
+  /// If [count] is 1 the translation of [keySingular] is returned.
+  /// Otherwise the translation of [keyPlural] is returned.
+  /// Returns the translation or (if not found) the key.
+  String trPlural(String keySingular, String keyPlural, int count,
+      [String module = '!global']) {
+    String rc = tr(count == 1 ? keySingular : keyPlural, module);
+    return rc;
+  }
+}
index 46b9f63c85ca9e9abdcdd91caedd59f669b67abd..7d316128b2cc5f395b80655c3f4b317d71be6335 100644 (file)
@@ -1,10 +1,49 @@
-import 'package:path/path.dart';
 import '../base/defines.dart';
 
+/// Describes a button of a page.
+class ButtonMetaData extends WidgetMetaData {
+  ButtonMetaData(String name) : super(name, WidgetType.button) {
+    //@ToDo
+  }
+}
+/// Describes a field related to a database column.
+class DbFieldMetaData extends WidgetMetaData {
+  PropertyMetaData? reference;
+  DbFieldMetaData(String name, String reference)
+      : super(name, WidgetType.dbField) {
+    //@ToDo
+  }
+}
+
+enum DisplayType {
+  line,
+  multiLine,
+  combobox,
+}
+
+/// Describes a field of the page.
+class FieldMetaData extends WidgetMetaData {
+  DataType dataType;
+  DisplayType displayType;
+  final String options;
+  FieldMetaData(String name, this.options,
+      {this.dataType = DataType.string, this.displayType = DisplayType.line})
+      : super(name, WidgetType.field);
+}
+
 class MetaException extends FormatException {}
 
 /// Stores the meta data of a module.
 class ModuleMetaData {
+  static final metaColumns = [
+    'created',
+    'createdBy',
+    'changed',
+    'changedBy',
+    'deleted',
+    'deletedBy'
+  ];
+
   /// The module name, e.g. users
   final String moduleName;
 
@@ -35,9 +74,7 @@ class ModuleMetaData {
       item.module = this;
       properties[item.name] = item;
       if (item.columnName.isEmpty) {
-        final prefix = shortModifiedLabel &&
-                ['created', 'createdBy', 'changed', 'changedBy']
-                    .contains(item.name)
+        final prefix = shortModifiedLabel && metaColumns.contains(item.name)
             ? ''
             : columnPrefix + '_';
         item.columnName = prefix + item.name.toLowerCase();
@@ -45,70 +82,7 @@ class ModuleMetaData {
     }
   }
 
-  /// Returns a DDL statement creating the database table.
-  String createDbTable() {
-    final buffer = StringBuffer();
-    buffer.write('-- DO NOT CHANGE. This file is created by the meta_tool\n');
-    buffer.write('CREATE TABLE $tableName (\n');
-    String? primary;
-    for (var item in list) {
-      if (item.options.contains('primary')) {
-        primary = item.columnName;
-      }
-      buffer.write(' ${item.columnName}');
-      buffer.write(' ' + mySqlType(item.dataType, item.size));
-      buffer.write(dbOptions(item) + ',\n');
-    }
-    buffer.write('  PRIMARY KEY ($primary)\n');
-    buffer.write(');\n');
-    return buffer.toString();
-  }
-
-  /// Returns a Dart class definition of the module.
-  String createModuleData() {
-    final buffer = StringBuffer();
-    buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n');
-    buffer.write("import '../../base/defines.dart';\n");
-    buffer.write("import '../../base/helper.dart';\n");
-    buffer.write('class ${moduleNameSingular}Data{\n');
-    for (var item in list) {
-      buffer.write('  ' + dartType(item.dataType) + '? ${item.name};\n');
-    }
-    buffer.write('  ${moduleNameSingular}Data({');
-    String separator = '';
-    for (var item in list) {
-      buffer.write('$separator this.${item.name}');
-      separator = ',';
-    }
-    buffer.write('});\n');
-    buffer.write('  ${moduleNameSingular}Data.fromYaml(YamlMap data) {\n');
-    for (var item in list) {
-      final name = item.columnName;
-      final type = item.dataType.toString();
-      //id = data.containsKey('id') ? int.tryParse(data['id']) : null;
-      buffer.write(
-          "    ${item.name} = data.containsKey('$name') ? valueOf($type, data['$name']) : null;\n");
-    }
-    buffer.write('  }\n');
-    buffer.write('  static DataType? dataTypeOf(String name) {\n');
-    buffer.write('    DataType? rc;\n');
-    buffer.write('    switch(name){\n');
-    for (var item in list) {
-      buffer.write("    case '${item.name}':\n");
-      buffer.write('      rc = ${item.dataType};\n');
-      buffer.write('      break;\n');
-    }
-    buffer.write('''    default:
-      break;
-    }
-    return rc;
-  }
-''');
-    buffer.write('}\n');
-
-    return buffer.toString();
-  }
-
+  /// Returns a given [dataType] as string.
   String dartType(DataType dataType) {
     String rc;
     switch (dataType) {
@@ -200,13 +174,36 @@ class ModuleMetaData {
     }
     return rc;
   }
+
+  /// Returns the properties that are not in [metaColumns].
+  /// : if the name of the property is [included] the property is always part of
+  /// the result.
+  Iterable<PropertyMetaData> standardColumns([String included = '']) {
+    final rc = list.where(
+        (item) => item.name == included || !metaColumns.contains(item.name));
+    return rc;
+  }
 }
 
-/// Stores the meta data of a module property.
+class PageMetaData {
+  final String name;
+  final PageType pageType;
+  List<FieldMetaData>? fields;
+  PageMetaData(
+    this.name,
+    this.pageType,
+    this.fields,
+  );
+}
+
+enum PageType { create, custom, delete, edit, list }
+
+/// Stores the meta data of a module property stored as column in a database.
 class PropertyMetaData {
   /// Name of the property (Dart name).
   final String name;
   ModuleMetaData module = ModuleMetaData('', []);
+  final String label;
 
   /// A colon delimited list of options, e.g. ':notnull:unique:'.
   final String options;
@@ -221,29 +218,17 @@ class PropertyMetaData {
 
   /// The foreign key if dataType is DataType.reference, e.g. 'users.user_id'
   String? reference;
-  PropertyMetaData(this.name, this.dataType, this.options, this.widgetType,
+  PropertyMetaData(
+      this.name, this.dataType, this.options, this.widgetType, this.label,
       {this.columnName = '', this.size = 0, this.reference});
 }
 
-String filenameToClass(String filename) {
-  final parts = basenameWithoutExtension(filename).split('_');
-  String rc = '';
-  for (var part in parts) {
-    if (part.isNotEmpty) {
-      rc += part[0].toUpperCase() + part.substring(1);
-    }
-  }
-  return rc;
+/// Describes a widget used in the page.
+/// Base class of all other widgets.
+class WidgetMetaData {
+  final WidgetType widgetType;
+  final String name;
+  WidgetMetaData(this.name, this.widgetType);
 }
 
-String classToFilename(String className) {
-  String upperCase = className.toUpperCase();
-  String rc = '';
-  for (var ix = 0; ix < className.length; ix++) {
-    if (className[ix] == upperCase[ix] && ix > 0) {
-      rc += '_';
-    }
-    rc = className[ix];
-  }
-  return rc.toLowerCase();
-}
+enum WidgetType { button, field, dbField }
index e8e84252a3e6474cdaad467aab777da5201a5c46..bedf989d4ef5cd22d988bc0c0c2da49e43aff1fc 100644 (file)
@@ -3,15 +3,14 @@ import 'dart:io';
 import 'module_meta_data.dart';
 import 'users_meta.dart';
 /// Returns the meta data of the module given by [name].
-ModuleMetaData moduleByName(String name) {
-  ModuleMetaData rc;
+/// Returns null if not found.
+ModuleMetaData? moduleByName(String name) {
+  ModuleMetaData? rc;
   switch (name) {
     case 'Users':
       rc = UserMeta();
       break;
     default:
-      stdout.write('+++ unknown module: $name. Using "UserMeta".');
-      rc = UserMeta();
       break;
   }
   return rc;
@@ -22,3 +21,4 @@ List<String> moduleNames(){
     'Users',
   ];
 }
+
diff --git a/lib/meta/roles_meta.dart b/lib/meta/roles_meta.dart
new file mode 100644 (file)
index 0000000..ef454ba
--- /dev/null
@@ -0,0 +1,34 @@
+import '../base/defines.dart';
+import 'module_meta_data.dart';
+import '../base/i18n.dart';
+
+final i18N = I18N();
+final M = i18N.module("Roles");
+
+class RoleMeta extends ModuleMetaData {
+  static RoleMeta instance = RoleMeta.internal();
+  RoleMeta.internal()
+      : super(
+          'Roles',
+          [
+            PropertyMetaData('id', DataType.reference, ':primary:', 'combo',
+                i18N.tr('Id', M)),
+            PropertyMetaData(
+                'name', DataType.string, ':notnull:', '', i18N.tr('Name', M),
+                size: 64),
+            PropertyMetaData('created', DataType.datetime, ':hidden:', '',
+                i18N.tr('Created', M)),
+            PropertyMetaData('createdBy', DataType.string, ':hidden:', '',
+                i18N.tr('Created by', M),
+                size: 32),
+            PropertyMetaData('changed', DataType.datetime, ':hidden:', '',
+                i18N.tr('Changed', M)),
+            PropertyMetaData('changedBy', DataType.string, ':hidden:', '',
+                i18N.tr('Changed by', M),
+                size: 32),
+          ],
+        );
+  factory RoleMeta() {
+    return instance;
+  }
+}
index 8a6ec101ec0414bbd00bd2a84aac0eceb0c4fcbb..2d15ff94bcb20bc1e350b3d370ab414182c3f806 100644 (file)
@@ -1,5 +1,8 @@
 import '../base/defines.dart';
 import 'module_meta_data.dart';
+import '../base/i18n.dart';
+final i18N = I18N();
+final M = i18N.module("Users");
 
 class UserMeta extends ModuleMetaData {
   static UserMeta instance = UserMeta.internal();
@@ -7,22 +10,30 @@ class UserMeta extends ModuleMetaData {
       : super(
           'Users',
           [
-            PropertyMetaData('id', DataType.reference, ':primary:', 'combo'),
+            PropertyMetaData('id', DataType.reference, ':primary:', 'combo',
+                i18N.tr('Id', M)),
             PropertyMetaData('name', DataType.string, ':notnull:', '',
+                i18N.tr('Name', M),
                 size: 64),
-            PropertyMetaData(
-                'displayName', DataType.string, ':unique:notnull:', '',
+            PropertyMetaData('displayName', DataType.string, ':unique:notnull:',
+                '', i18N.tr('Display name', M),
                 size: 32),
             PropertyMetaData('email', DataType.string, ':unique:notnull:', '',
+                i18N.tr('EMail', M),
                 size: 255),
-            PropertyMetaData('role', DataType.reference, ':notnull:', ''),
-            PropertyMetaData('created', DataType.datetime, '', ''),
-            PropertyMetaData('createdBy', DataType.string, '', '', size: 32),
-            PropertyMetaData('changed', DataType.datetime, '', ''),
-            PropertyMetaData('changedBy', DataType.string, '', '', size: 32),
+            PropertyMetaData('role', DataType.reference, ':notnull:', '',
+                i18N.tr('Role', M), reference: 'roles.role_id'),
+            PropertyMetaData('created', DataType.datetime, ':hidden:', '',
+                i18N.tr('Created', M)),
+            PropertyMetaData('createdBy', DataType.string, ':hidden:', '',
+                i18N.tr('Created by', M),
+                size: 32),
+            PropertyMetaData('changed', DataType.datetime, ':hidden:', '',
+                i18N.tr('Changed', M)),
+            PropertyMetaData('changedBy', DataType.string, ':hidden:', '',
+                i18N.tr('Changed by', M),
+                size: 32),
           ],
-          tableName: 'loginusers',
-          shortModifiedLabel: true,
         );
   factory UserMeta() {
     return instance;
diff --git a/lib/page/r_data.dart b/lib/page/r_data.dart
deleted file mode 100644 (file)
index f35600e..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-// DO NOT CHANGE. This file is created by the meta_tool
-class User{
-  int? id;
-  String? name;
-  String? displayName;
-  String? email;
-  int? role;
-  DateTime? created;
-  String? createdBy;
-  DateTime? changed;
-  String? changedBy;
-  User({ this.id, this.name, this.displayName, this.email, this.role, this.created, this.createdBy, this.changed, this.changedBy});
-}
index 316e5294e09c1148e486ecde718976701b3c08fc..167505656e69caa190a01dc7c0ae85cfbdf13925 100644 (file)
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import '../../base/defines.dart';
 import 'user_state.dart';
-import 'user_data.dart';
 import 'package:equatable/equatable.dart';
 
 class User extends StatefulWidget {
@@ -15,7 +14,7 @@ class _UserState extends State<User> {
 
   @override
   void initState() {
-    context.read<UserBloc>().add(UserEvent(EventSource.undef, EventCardinality.undef));
+    //context.read<UserBloc>().add(UserEvent(EventSource.undef, EventCardinality.undef));
     super.initState();
   }
 
@@ -31,9 +30,9 @@ class _UserState extends State<User> {
       builder: (context, UserState state) {
         return Column(
           children: [
-            ResultDisplay(
-              text: _getDisplayText(state.calculationModel),
-            ),
+            //ResultDisplay(
+            //  text: _getDisplayText(state.calculationModel),
+            //),
             Row(
               children: [
                 _getButton(text: '7', onTap: () => numberPressed(7)),
@@ -86,8 +85,8 @@ class _UserState extends State<User> {
               ],
             ),
             Spacer(),
-            UserHistoryContainer(
-                calculations: state.history.reversed.toList())
+            //UserHistoryContainer(
+            //    calculations: state.history.reversed.toList())
           ],
         );
       },
@@ -99,6 +98,7 @@ class _UserState extends State<User> {
         required void Function() onTap,
         Color backgroundColor = Colors.white,
         Color textColor = Colors.black}) {
+    /*
     return CalculatorButton(
       label: text,
       onTap: onTap,
@@ -107,24 +107,26 @@ class _UserState extends State<User> {
       backgroundColor: backgroundColor,
       labelColor: textColor,
     );
+     */
+    return SizedBox(width: 10);
   }
 
   numberPressed(int number) {
-    context.read<UserBloc>().add(NumberPressed(number: number));
+    //context.read<UserBloc>().add(NumberPressed(number: number));
   }
 
   operatorPressed(String operator) {
-    context.read<UserBloc>().add(OperatorPressed(operator: operator));
+    //context.read<UserBloc>().add(OperatorPressed(operator: operator));
   }
 
   calculateResult() {
-    context.read<UserBloc>().add(CalculateResult());
+    //context.read<UserBloc>().add(CalculateResult());
   }
 
   clear() {
-    context.read<UserBloc>().add(ClearUser());
+    //context.read<UserBloc>().add(ClearUser());
   }
-
+  /*
   String _getDisplayText(UserModel model) {
     if (model.result != null) {
       return '${model.result}';
@@ -144,4 +146,5 @@ class _UserState extends State<User> {
 
     return "${model.result ?? 0}";
   }
+   */
 }
diff --git a/rest_server/CR b/rest_server/CR
new file mode 100755 (executable)
index 0000000..8420bde
--- /dev/null
@@ -0,0 +1,6 @@
+#! /bin/bash
+APP=rest_server
+TRG=tools/$APP
+pub get
+dart compile exe bin/$APP.dart -o $TRG
+ls -ld $TRG
index e87e0b8907af7c8a109a8d9c4a741b290a0d045f..d8848ab1b473aa06a4a72311ac0a6334c3b3a0bd 100644 (file)
@@ -9,6 +9,10 @@ byId:
   type: record
   parameters: [ ":id" ]
   sql: "select * from loginusers where user_id=:id;"
+delete:
+  type: delete
+  parameters: [ ":id" ]
+  sql: "delete * from loginusers where user_id=:id;"
 update:
   type: update
   parameters: [":id", ":name", ":displayname", ":email", ":changedby"]
index 44b95ad05324bfab080edc01062e074e6e631ed6..cacd2278c48dd99ceb02dab5ee6d708a26ac5346 100644 (file)
@@ -176,8 +176,8 @@ class RestServer {
 service:
   address: 0.0.0.0
   port: 58021
-  dataDirectory: /var/cache/rest_server/data
-  sqlDirectory: /etc/rest_server/sql.d
+  dataDirectory: /var/cache/exhibition/data
+  sqlDirectory: /usr/share/exhibition/rest_server/data/sql
   threads: 2
   watchDogPause: 60
   # logFile: /var/log/local/exhibition.log
@@ -226,7 +226,7 @@ clientSessionTimeout: 900
         starter: executable,
         user: appName,
         group: appName,
-        description: 'A REST server serving the POLLECTOR project',
+        description: 'A REST server serving the Exhibition project',
       );
     }
   }
@@ -243,7 +243,7 @@ clientSessionTimeout: 900
   static String version() => _version;
 }
 
-/// Datenklasse für eine Isolate-Instanz.
+/// Implements an isolate instance.
 class ServiceWorker {
   /// for unittests:
   static int maxRequests = 0;
@@ -490,14 +490,13 @@ class ServiceWorker {
     logger.log('thread $threadId stopped');
   }
 
-  /// Prüft, ob ein Isolate auf eine Watchdog-Anfrage antwortet.
-  /// Hintergrund: Wenn eine DB-Verbindung abbricht, tritt beim nächsten
-  /// DB-Zugriff eine SocketException auf, die nicht abgefangen werden kann.
-  /// Der Thread (Isolate) ist dann tot.
-  /// Daher wird vom Observer-Thread regelmäßig eine Anfrage verschickt.
-  /// Kommt keine Antwort, ist kein Empfangsthread mehr erreichbar, das Programm
-  /// wird mittels exit() abgebrochen, damit es von SystemD erneut gestartet
-  /// wird.
+  /// Checks whether an isolate instance is reponding to a request.
+  /// Background: If one DB connection breaks, a SocketException occurs
+  /// the next time the DB is accessed, which cannot be intercepted.
+  // The thread (Isolate) is then dead.
+  // Therefore, the observer thread regularly sends a request.
+  // f there is no response, the receiving thread can no longer be reached,
+  // the program is aborted with exit () so that it can be restarted by SystemD.
   Future observe() async {
     final duration = Duration(
         seconds:
@@ -518,7 +517,7 @@ class ServiceWorker {
     }
   }
 
-  /// Ermittelt die Anfragenklasse ("what"), die Teil der URI ist.
+  /// Determines the request class ("what") that is part of the URI.
   void prepareResource({bool withId = true}) {
     if (!(currentRequest?.requestedUri.path.contains('watchdog') ?? false)) {
       logger.log(currentRequest!.requestedUri.path, LEVEL_FINE);
@@ -612,11 +611,11 @@ class ServiceWorker {
     return buffer;
   }
 
-  /// Fordert per POST eine Aktion an.
-  /// [what] legt die Aktion fest.
-  /// [body] ist der Text, der mit der Anfrage mitgeliefert wird.
-  /// [headers] sind HTTP-Headerkomponenten.
-  /// Liefert die Antwort der Anfrage.
+  /// Requests an action via POST.
+  /// [what] defines the action.
+  /// [body] is the text that is supplied with the request.
+  /// [headers] are HTTP header components.
+  /// Returns the answer to the request.
   Future<String> runRequest(String what,
       {String? body, Map<String, String>? headers}) async {
     final port = configuration.asInt('port', section: 'service') ?? 58011;
@@ -694,7 +693,7 @@ class ServiceWorker {
     return rc;
   }
 
-  /// Speichert [content] im Verzeichnis [directory] unter dem Namen [node].
+  /// Stores [content] in the directory [directory] under the name [node].
   Future storeFile(String directory, String node, String content) async {
     if (directory.isEmpty) {
       logger.error('storeFile: missing data directory');
diff --git a/rest_server/tools/project.inc b/rest_server/tools/project.inc
new file mode 100644 (file)
index 0000000..3504b64
--- /dev/null
@@ -0,0 +1 @@
+PROJECT=exhibition
diff --git a/test/i18n_text_parser_test.dart b/test/i18n_text_parser_test.dart
new file mode 100644 (file)
index 0000000..3e854af
--- /dev/null
@@ -0,0 +1,85 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path/path.dart';
+
+import "../bin/i18n_text_parser.dart";
+
+void main() {
+  final logger = MemoryLogger(LEVEL_FINE);
+  FileSync.initialize(logger);
+  final fileSync = FileSync();
+  final baseDir = init(logger);
+  final targetDir = join(baseDir, nodeTarget);
+  test('parse', () {
+    final parser = I18nTextParser(logger);
+    parser.scanDirectory(baseDir);
+    parser.writePot(targetDir);
+    expect(parser.modules, isNotEmpty);
+    expect(parser.modules.containsKey('Example'), isTrue);
+    expect(parser.modules.containsKey('!global'), isTrue);
+    expect(parser.modules.containsKey('Sample'), isTrue);
+    expect(parser.modules['Example']!.containsKey('introduction'), isTrue);
+    expect(parser.modules['Sample']!.containsKey('info'), isTrue);
+    var fn = join(targetDir, 'Example.pot');
+    var content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module Example, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:5
+msgid "description"
+msgstr ""
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:5
+msgid "introduction"
+msgstr ""
+''');
+    fn = join(targetDir, 'Sample.pot');
+    content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module Sample, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/args.dart:5
+msgid "info"
+msgstr ""
+''');
+    fn = join(targetDir, '!global.pot');
+    content = fileSync.fileAsString(fn);
+    expect(content, '''# Texts of module !global, parsed by i18n_text_parser
+
+#: /tmp/unittest/i18n/lib/base/simple.dart:10
+#: /tmp/unittest/i18n/lib/base/args.dart:9
+msgid "Status line"
+msgstr ""
+''');
+  });
+}
+
+const nodeTarget = 'output';
+
+String init(MemoryLogger logger) {
+  final fileSync = FileSync();
+  final baseDir = fileSync.tempDirectory('i18n', subDirs: 'unittest');
+  final subDir = fileSync.tempDirectory('base', subDirs: 'unittest/i18n/lib');
+  fileSync.toFile(join(subDir, 'simple.dart'), r'''import 'dart:io';
+final i18N = I18N();
+final M = i18N.module('Example');
+String header(){
+  return i18N.tr('introduction', M) + " " + i18N.tr(
+    "description",
+    M);
+}
+String footer(){
+  return I18N().tr("Status line");
+}
+''');
+  fileSync.toFile(join(subDir, 'args.dart'), r'''import 'dart:io';
+final i18N = I18N();
+final M = i18N.module("Sample");
+String header(String user, String role){
+  return i18N.trArgs('Name: {0} Role: {1}', [user, role], M) + " " + i18N.tr(
+    r'info', M);
+}
+String footer(){
+  return I18N().tr('Status line');
+}
+''');
+  return baseDir;
+}
diff --git a/test/yaml_merger_test.dart b/test/yaml_merger_test.dart
new file mode 100644 (file)
index 0000000..f214344
--- /dev/null
@@ -0,0 +1,117 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path/path.dart';
+
+import "../bin/yaml_merger.dart";
+
+const nodePrecedence = 'precedence';
+const nodeTarget = 'output';
+void main() {
+  final logger = MemoryLogger(LEVEL_FINE);
+  FileSync.initialize(logger);
+  final fileSync = FileSync();
+  final baseDir = init(logger);
+  final targetDir = join(baseDir, nodeTarget);
+  test('merge', () {
+    final merger = Merger(logger);
+    merger.merge(
+        baseDir, join(baseDir, nodePrecedence), join(baseDir, nodeTarget));
+    var content = fileSync.fileAsString(join(targetDir, 'users.sql.yaml'));
+    expect(content, r'''---
+# SQL statements of the module "Users":
+module: Users
+list:
+  type: list
+  parameters: [":name", ":role"]
+  sql: "select * from loginusers where user_name like :filterName;
+    and (:role is null or user_role=:role);"
+byId:
+  type: record
+  parameters: [ ":id" ]
+  sql: "select * from loginusers where user_id=:id;"
+insert:
+  type: insert
+  parameters: [":name", ":displayname", ":email", ":createdby"]
+  sql: "INSERT INTO loginusers(user_name, user_displayname, user_email, user_changedby)
+    VALUES(:name, :displayname, :email, NOW(), :createdby);"
+byName:
+  type: record
+  parameters: [":name"]
+  sql: "select * from loginusers where user_name=:name;"
+''');
+    content = fileSync.fileAsString(join(targetDir, 'roles.sql.yaml'));
+    expect(content, r'''---
+# SQL statements of the module "Roles":
+module: Roles
+list:
+  type: list
+  parameters: [":name", ":role"]
+  sql: "select * from loginusers where user_name like :filterName;
+    and (:role is null or user_role=:role);"
+''');
+  content = fileSync.fileAsString(join(targetDir, 'misc.sql.yaml'));
+  expect(content, r'''---
+# SQL statements of the module "Misc":
+module: Misc
+tables:
+  type: list
+  parameters: []
+  sql: "show tables;"
+''');
+  });
+}
+
+String init(MemoryLogger logger) {
+  final fileSync = FileSync();
+  final baseDir = fileSync.tempDirectory('yaml_merger', subDirs: 'unittest');
+  final precedenceDir = join(baseDir, nodePrecedence);
+  fileSync.ensureDirectory(precedenceDir);
+  fileSync.ensureDirectory(join(baseDir, nodeTarget));
+  fileSync.toFile(join(baseDir, 'users.sql.yaml'), r'''---
+# SQL statements of the module "Users":
+module: Users
+list:
+  type: list
+  parameters: []
+  sql: "select * from loginusers;"
+byId:
+  type: record
+  parameters: [ ":id" ]
+  sql: "select * from loginusers where user_id=:id;"
+insert:
+  type: insert
+  parameters: [":name", ":displayname", ":email", ":createdby"]
+  sql: "INSERT INTO loginusers(user_name, user_displayname, user_email, user_changedby)
+    VALUES(:name, :displayname, :email, NOW(), :createdby);"
+''');
+  fileSync.toFile(join(baseDir, 'roles.sql.yaml'), r'''---
+# SQL statements of the module "Roles":
+module: Roles
+list:
+  type: list
+  parameters: []
+  sql: select * from loginusers;
+''');
+  fileSync.toFile(join(precedenceDir, 'users.sql.yaml'), r'''---
+# Overriding SQL statements of the module "Users":
+module: Users
+list:
+  type: list
+  parameters: [":name", ":role"]
+  sql: "select * from loginusers where user_name like :filterName;
+    and (:role is null or user_role=:role);"
+byName:
+  type: record
+  parameters: [":name"]
+  sql: "select * from loginusers where user_name=:name;"
+''');
+  fileSync.toFile(join(precedenceDir, 'misc.sql.yaml'), r'''---
+# SQL statements of the module "Misc":
+module: Misc
+tables:
+  type: list
+  parameters: []
+  sql: "show tables;"
+''');
+  return baseDir;
+}
diff --git a/tools/CompileMerge b/tools/CompileMerge
new file mode 100755 (executable)
index 0000000..e3e62dd
--- /dev/null
@@ -0,0 +1,5 @@
+#! /bin/bash
+APP=yaml_merger
+TRG=tools/$APP
+dart compile exe bin/$APP.dart -o $TRG
+ls -ld $TRG
diff --git a/tools/InitProject b/tools/InitProject
new file mode 100755 (executable)
index 0000000..10bd3e6
--- /dev/null
@@ -0,0 +1,92 @@
+#! /bin/bash
+APP=$1
+APP2=$(echo ${APP:0:1} | tr a-z A-Z)${APP:1}
+function Usage(){
+  echo "Usage: InitProject PROJECT"
+  echo "  Prepares the project PROJECT for using the framework exhibition"
+  echo "PROJECT: the name of the already existing project (created with 'flutter create')"
+  echo "Example:"
+  echo "InitProject myapp"
+  echo "+++ $*"
+}
+function MkDirs(){
+    for dir in tools rest_server/data; do
+      mkdir -p $dir
+    done
+}
+function EnsureDir(){
+    local dir=$1
+    if [ ! -d $dir ]; then
+      echo "creating $dir"
+      mkdir -p $dir
+    fi
+}
+function CopyFiles(){
+    local projDir=$(pwd)
+    cd ../exhibition
+    local srcDir=$(pwd)    
+    for file in lib/base/*.dart lib/meta/module_meta_data.dart lib/meta/modules.dart \
+    rest_server/pubspec.yaml \
+    tools/CompileMerge tools/PackRestServer \
+    ; do
+      cd $projDir
+      local dir=$(dirname $file)
+      EnsureDir $dir
+      echo "creating $file"
+      cp -a ../exhibition/$file $file
+      cd $srcDir
+    done
+    cd $projDir
+}
+function CopyAndReplace(){
+    local projDir=$(pwd)
+    cd ../exhibition
+    local srcDir=$(pwd)
+    for file in lib/setting/*.dart \
+      pubspec.yaml \
+      lib/persistence/*.dart \
+      lib/page/*.dart \
+      rest_server/lib/*.dart rest_server/bin/*.dart rest_server/tools/project.inc \
+      rest_server/CR \
+      lib/main.dart \
+      lib/meta/*_meta.dart \
+      bin/*.dart \
+    ; do
+        cd $projDir
+        local dir=$(dirname $file)
+        EnsureDir $dir
+        local file2=${file/exhibition/$APP}
+        echo "copy and replace $file -> $(basename $file2)"
+        sed <../exhibition/$file >$file2 -e "s/exhibition/$APP/g" -e "s/Exhibition/$APP2/g"
+        chmod --reference=../exhibition/$file $file2
+        cd $srcDir
+    done
+    cd $projDir
+}
+function SymbolicLinks(){
+  for links in ReCreateMetaTool:. Meta:. ; do
+    local src=../exhibition/${links%:*}
+    local trg=${links#*:}
+    local trg2=$trg
+    test $trg = . && trg2=$(basename $src)
+    echo "trg: $trg2"
+    if [ ! -L $trg2 ]; then
+      echo "linking $src -> $trg2"
+      ln -s $src $trg2
+    fi
+  done
+}
+function PrepareTools(){
+  test -e tools/yaml_merger || tools/CompileMerger
+}
+if [ -z "$APP" ]; then
+  Usage "missing PROJECT"
+elif [ ! -d ../$APP ]; then
+  Usage "wrong current directory: Please go into the base folder of the project."
+else
+  PrepareTools
+  MkDirs
+  CopyFiles
+  CopyAndReplace
+  SymbolicLinks
+fi
diff --git a/tools/PackRestServer b/tools/PackRestServer
new file mode 100755 (executable)
index 0000000..4fbc077
--- /dev/null
@@ -0,0 +1,43 @@
+#! /bin/bash
+BASE_DIR=/tmp/rest_server
+SCRIPT_INSTALL=/tmp/InstallRestServer
+TAR_NODE=rest_server.tgz
+TAR=/tmp/$TAR_NODE
+source rest_server/tools/project.inc
+
+function DoIt(){
+  test -d $BASE_DIR && rm -Rf $BASE_DIR
+  mkdir -p $BASE_DIR/data/sql
+  cd rest_server
+  ./CR
+  test -d data/sql/precedence || mkdir -p data/sql/precedence
+  cd ..
+  cp -av rest_server/tools/rest_server $BASE_DIR
+  tools/CompileMerge
+  tools/yaml_merge data/sql data/sql/precedence $BASE_DIR/data/sql
+  cat <<EOS >$BASE_DIR/INSTALL.TXT
+# ------------
+# Installation:
+./InstallRestServer
+# or manually:
+DIR=\$(pwd)
+mkdir -p /usr/share/$PROJECT && cd /usr/share/$PROJECT
+tar xzf \DIR/$TAR_NODE
+# ./rest_server install <executable> <service-name>
+./rest_server install /usr/share/$PROJECT/rest_server $PROJECT
+# see /usr/share/$PROJECT and /etc/$PROJECT
+EOS
+  tar -C $BASE_DIR -czf $TAR .
+  cat <<EOS >$SCRIPT_INSTALL
+#! /bin/bash
+DIR=\$(pwd)
+mkdir -p /usr/share/$PROJECT && cd /usr/share/$PROJECT
+tar xzf \$DIR/rest_server.tgz
+./rest_server install /usr/share/$PROJECT/rest_server $PROJECT
+EOS
+  chmod uog+x $SCRIPT_INSTALL
+  echo "================"
+  echo "= put $TAR and $SCRIPT_INSTALL to the backend server and..."
+  cat $BASE_DIR/INSTALL.TXT
+}
+DoIt