diff --git a/assets/schema.json b/assets/schema.json new file mode 100644 index 0000000..429c926 --- /dev/null +++ b/assets/schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Schema for cabo game data", + "type": "array", + "items": { + "type": "object", + "properties": { + "gameTitle": { + "type": "string" + }, + "gameHasPointLimit": { + "type": "boolean" + }, + "players": { + "type": "array", + "items": { + "type": "string" + } + }, + "playerScores": { + "type": "array", + "items": { + "type": "number" + } + }, + "roundNumber": { + "type": "number" + }, + "isGameFinished": { + "type": "boolean" + }, + "winner": { + "type": "string" + }, + "roundList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "roundNum": { + "type": "number" + }, + "caboPlayerIndex": { + "type": "number" + }, + "kamikazePlayerIndex": {}, + "scores": { + "type": "array", + "items": { + "type": "number" + } + }, + "scoreUpdates": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "required": [ + "roundNum", + "caboPlayerIndex", + "kamikazePlayerIndex", + "scores", + "scoreUpdates" + ] + } + } + }, + "required": [ + "gameTitle", + "gameHasPointLimit", + "players", + "playerScores", + "roundNumber", + "isGameFinished", + "winner", + "roundList" + ] + } +} \ No newline at end of file diff --git a/lib/utility/local_storage_service.dart b/lib/utility/local_storage_service.dart index 661b139..f88e8e0 100644 --- a/lib/utility/local_storage_service.dart +++ b/lib/utility/local_storage_service.dart @@ -1,11 +1,12 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/utility/globals.dart'; import 'package:file_picker/file_picker.dart'; import 'package:file_saver/file_saver.dart'; +import 'package:flutter/services.dart'; +import 'package:json_schema/json_schema.dart'; import 'package:path_provider/path_provider.dart'; class LocalStorageService { @@ -38,31 +39,44 @@ class LocalStorageService { } /// Loads the game data from a local JSON file. - static Future loadGameSessions() async { + static Future loadGameSessions() async { print('Versuche, Daten zu laden...'); try { final file = await _getFilePath(); - if (await file.exists()) { - print('Es existiert bereits eine Datei mit Spieldaten'); - final jsonString = await file.readAsString(); - if (jsonString.isNotEmpty) { - print('Die gefundene Datei ist nicht leer'); - final jsonList = json.decode(jsonString) as List; - Globals.gameList = jsonList - .map((jsonItem) => - GameSession.fromJson(jsonItem as Map)) - .toList() - .cast(); - print('Die Daten wurden erfolgreich geladen'); - } else { - print('Die Datei ist leer'); - } - } else { - print('Es existiert bisher noch keine Datei mit Spieldaten'); + + if (!await file.exists()) { + print('Es existiert noch keine Datei mit Spieldaten'); + return false; } + + print('Es existiert bereits eine Datei mit Spieldaten'); + final jsonString = await file.readAsString(); + + if (jsonString.isEmpty) { + print('Die gefundene Datei ist leer'); + return false; + } + + if (!await validateJsonSchema(jsonString)) { + print('Die Datei konnte nicht validiert werden'); + Globals.gameList = []; + return false; + } + + print('Die gefundene Datei ist nicht leer und validiert'); + final jsonList = json.decode(jsonString) as List; + + Globals.gameList = jsonList + .map((jsonItem) => + GameSession.fromJson(jsonItem as Map)) + .toList(); + + print('Die Daten wurden erfolgreich geladen und verarbeitet'); + return true; } catch (e) { print('Fehler beim Laden der Spieldaten:\n$e'); Globals.gameList = []; + return false; } } @@ -87,34 +101,63 @@ class LocalStorageService { /// Opens the file picker to import a JSON file and loads the game data from it. static Future importJsonFile() async { + final result = await FilePicker.platform.pickFiles( + dialogTitle: 'Wähle eine Datei mit Spieldaten aus', + type: FileType.custom, + allowedExtensions: ['json'], + ); + + if (result == null) { + print('Der Dialog wurde abgebrochen'); + return false; + } + try { - final result = await FilePicker.platform.pickFiles( - dialogTitle: 'Wähle eine Datei mit Spieldaten aus', - type: FileType.custom, - allowedExtensions: ['json'], - ); - String jsonString = ''; - if (result != null) { - if (result.files.single.bytes != null) { - final Uint8List fileBytes = result.files.single.bytes!; - jsonString = utf8.decode(fileBytes); - } else if (result.files.single.path != null) { - final file = File(result.files.single.path!); - jsonString = await file.readAsString(); - } - final jsonList = json.decode(jsonString) as List; - print('JSON Inhalt: $jsonList'); - Globals.gameList = jsonList - .map((jsonItem) => - GameSession.fromJson(jsonItem as Map)) - .toList(); - return true; - } else { - print('Der Dialog wurde abgebrochen'); + final jsonString = await _readFileContent(result.files.single); + + if (!await validateJsonSchema(jsonString)) { + return false; + } + final jsonData = json.decode(jsonString) as List; + print('JSON Inhalt: $jsonData'); + Globals.gameList = jsonData + .map((jsonItem) => + GameSession.fromJson(jsonItem as Map)) + .toList(); + return true; + } on FormatException catch (e) { + print('Ungültiges JSON-Format: $e'); + return false; + } on Exception catch (e) { + print('Fehler beim Dateizugriff: $e'); + return false; + } + } + + /// Helper method to read file content from either bytes or path + static Future _readFileContent(PlatformFile file) async { + if (file.bytes != null) return utf8.decode(file.bytes!); + if (file.path != null) return await File(file.path!).readAsString(); + + throw Exception('Die Datei hat keinen lesbaren Inhalt'); + } + + /// Validates the JSON data against the schema. + static Future validateJsonSchema(String jsonString) async { + try { + final schemaString = await rootBundle.loadString('assets/schema.json'); + final schema = JsonSchema.create(json.decode(schemaString)); + final jsonData = json.decode(jsonString); + final result = schema.validate(jsonData); + + if (result.isValid) { + print('JSON ist erfolgreich validiert.'); return true; } + print('JSON ist nicht gültig: ${result.errors}'); + return false; } catch (e) { - print('Fehler beim Importieren: $e'); + print('Fehler beim Validieren des JSON-Schemas: $e'); return false; } } diff --git a/pubspec.yaml b/pubspec.yaml index ecbbc81..6141b6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: cabo_counter description: "Mobile app for the card game Cabo" publish_to: 'none' -version: 0.1.5+113 +version: 0.1.5+116 environment: sdk: ^3.5.4 @@ -19,6 +19,7 @@ dependencies: path_provider: ^2.1.1 typed_data: ^1.3.2 url_launcher: any + json_schema: ^5.2.1 dev_dependencies: flutter_test: