diff --git a/app/src/main/java/com/dano/test1/DatabaseDownloader.kt b/app/src/main/java/com/dano/test1/DatabaseDownloader.kt
index 107c37f..ad5b4b7 100644
--- a/app/src/main/java/com/dano/test1/DatabaseDownloader.kt
+++ b/app/src/main/java/com/dano/test1/DatabaseDownloader.kt
@@ -16,21 +16,24 @@ import javax.crypto.spec.SecretKeySpec
object DatabaseDownloader {
private const val DB_NAME = "questionnaire_database"
- private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
- private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php?token=$API_TOKEN"
+ private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php"
// AES-256 Key (muss exakt 32 Bytes lang sein)
private const val AES_KEY = "12345678901234567890123456789012"
private val client = OkHttpClient()
- fun downloadAndReplaceDatabase(context: Context) {
+ /**
+ * Startet den Download und Austausch der DB, benötigt gültiges Token
+ */
+ fun downloadAndReplaceDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
val request = Request.Builder()
.url(SERVER_DOWNLOAD_URL)
+ .header("Authorization", "Bearer $token")
.build()
val response = client.newCall(request).execute()
diff --git a/app/src/main/java/com/dano/test1/DatabaseUploader.kt b/app/src/main/java/com/dano/test1/DatabaseUploader.kt
index a8606b9..e82bee6 100644
--- a/app/src/main/java/com/dano/test1/DatabaseUploader.kt
+++ b/app/src/main/java/com/dano/test1/DatabaseUploader.kt
@@ -20,14 +20,32 @@ import kotlin.system.exitProcess
object DatabaseUploader {
private const val DB_NAME = "questionnaire_database"
- // TODO entferne uploadDeltaTest2.php
- private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest3.php"
+ private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest4.php"
private const val SERVER_CHECK_URL = "http://49.13.157.44/checkDatabaseExists.php"
- private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
private val client = OkHttpClient()
- fun uploadDatabase(context: Context) {
+ /**
+ * Startet den Upload mit Login über LoginManager.
+ * @param context Android Context
+ * @param password Vom User eingegebenes Passwort
+ */
+ fun uploadDatabaseWithLogin(context: Context, password: String) {
+ LoginManager.loginUser(context, password,
+ onSuccess = { token ->
+ Log.d("UPLOAD", "Login erfolgreich, Token erhalten")
+ uploadDatabase(context, token)
+ },
+ onError = { errorMsg ->
+ Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg")
+ }
+ )
+ }
+
+ /**
+ * Interner Upload, benötigt gültiges Token
+ */
+ private fun uploadDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val dbFile = context.getDatabasePath(DB_NAME)
@@ -45,9 +63,7 @@ object DatabaseUploader {
)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor ->
if (cursor.moveToFirst()) {
- try {
- Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}")
- } catch (_: Exception) {}
+ try { Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") } catch (_: Exception) {}
}
}
db.close()
@@ -59,12 +75,12 @@ object DatabaseUploader {
val exists = checkDatabaseExists()
if (exists) {
Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload")
- uploadPseudoDelta(context, dbFile)
} else {
Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload")
- uploadPseudoDelta(context, dbFile)
}
+ uploadPseudoDelta(context, dbFile, token)
+
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e)
}
@@ -97,15 +113,7 @@ object DatabaseUploader {
}
}
- /**
- * Wichtig: Diese Funktion wurde erweitert, sodass:
- * - die DB als JSON in eine temporäre Datei geschrieben wird,
- * - diese JSON-Datei AES-verschlüsselt wird (mit AES256Helper.encryptFile),
- * - die verschlüsselte Datei als Multipart 'file' an den Server gesendet wird.
- *
- * (Funktionalität: gleiche Signatur wie vorher behalten)
- */
- private fun uploadPseudoDelta(context: Context, file: File) {
+ private fun uploadPseudoDelta(context: Context, file: File, token: String) {
try {
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
@@ -125,25 +133,21 @@ object DatabaseUploader {
db.close()
- // Schreibe JSON in temporäre Datei
val tmpJson = File(context.cacheDir, "payload.json")
tmpJson.writeText(data.toString())
- // Verschlüssele JSON -> tmpEnc
val tmpEnc = File(context.cacheDir, "payload.enc")
try {
AES256Helper.encryptFile(tmpJson, tmpEnc)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e)
- // cleanup
tmpJson.delete()
return
}
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
- .addFormDataPart("token", API_TOKEN)
- // Datei-Feld "file" mit verschlüsselter Payload
+ .addFormDataPart("token", token) // Token vom Login
.addFormDataPart(
"file",
"payload.enc",
@@ -159,31 +163,19 @@ object DatabaseUploader {
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}")
- // cleanup
tmpJson.delete()
tmpEnc.delete()
}
override fun onResponse(call: Call, response: Response) {
- val body = try {
- response.body?.string() ?: "Keine Response"
- } catch (e: Exception) {
+ val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) {
"Fehler beim Lesen der Response: ${e.message}"
}
if (response.isSuccessful) {
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body")
- // Lösche Hauptdatenbank
- if (file.delete()) {
- Log.d("UPLOAD", "Lokale DB gelöscht.")
- } else {
- Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
- }
- // Lösche Journal-Datei
+ if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
val journalFile = File(file.parent, file.name + "-journal")
- if (journalFile.exists() && journalFile.delete()) {
- Log.d("UPLOAD", "Journal-Datei gelöscht.")
- }
- // cleanup temp files
+ if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.")
tmpJson.delete()
tmpEnc.delete()
exitProcess(0)
diff --git a/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt b/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt
index ba363d6..d56d276 100644
--- a/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt
+++ b/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt
@@ -69,10 +69,9 @@ class HandlerClientCoachCode(
val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString()
- // Prüfen, ob die Datenbank-Dateien vor dem Klick existieren
- val dbFile = layout.context.getDatabasePath("questionnaire_database")
- val dbJournalFile = layout.context.getDatabasePath("questionnaire_database-journal")
- val dbExisted = dbFile.exists() || dbJournalFile.exists()
+ // Prüfen, ob die DB-Datei vor dem Zugriff existiert
+ val dbPath = layout.context.getDatabasePath("questionnaire_database")
+ val dbExistedBefore = dbPath.exists()
// Check if client code already exists asynchronously
CoroutineScope(Dispatchers.IO).launch {
@@ -86,19 +85,21 @@ class HandlerClientCoachCode(
} else {
// Either no existing client or re-using previous code
saveAnswers(clientCode, coachCode)
-
- // Datenbank-Dateien löschen, wenn sie vorher NICHT existierten
- if (!dbExisted) {
- dbFile.delete()
- dbJournalFile.delete()
- }
-
goToNextQuestion()
+
+ // Lösche DB-Dateien nur, wenn sie vorher nicht existierten
+ if (!dbExistedBefore) {
+ MyApp.database.close()
+ dbPath.delete()
+ val journalFile = layout.context.getDatabasePath("questionnaire_database-journal")
+ journalFile.delete()
+ }
}
}
}
}
+
// Handle Previous button click
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
val clientCode = clientCodeField.text.toString()
diff --git a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt
index fb897df..66f03f9 100644
--- a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt
+++ b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt
@@ -13,7 +13,7 @@ import android.util.Log
import com.dano.test1.data.CompletedQuestionnaire
import java.io.File
-var INTEGRATION_INDEX_POINTS: Int? = null
+var RHS_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) {
@@ -342,13 +342,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
}
- // fülle buttonPoints & INTEGRATION_INDEX_POINTS
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
- if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) {
- INTEGRATION_INDEX_POINTS = entry.sumPoints
+ if (entry.questionnaireId.contains("questionnaire_2_rhs", ignoreCase = true)) {
+ RHS_POINTS = entry.sumPoints
}
}
}
@@ -682,39 +681,79 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
uploadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
+ if (clientCode.isBlank()) {
+ val message = LanguageManager.getText(languageID, "please_client_code")
+ Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
+ }
+
GlobalValues.LAST_CLIENT_CODE = clientCode
- Toast.makeText(activity, "Datenbank wird hochgeladen...", Toast.LENGTH_SHORT).show()
- DatabaseUploader.uploadDatabase(activity)
+ // Passwort-Eingabe-Popup
+ val input = EditText(activity).apply {
+ hint = "Server-Passwort"
+ }
+
+ android.app.AlertDialog.Builder(activity)
+ .setTitle("Login erforderlich")
+ .setView(input)
+ .setPositiveButton("OK") { _, _ ->
+ val password = input.text.toString()
+ if (password.isNotBlank()) {
+ Toast.makeText(activity, "Login wird überprüft...", Toast.LENGTH_SHORT).show()
+ // Login + Upload starten
+ DatabaseUploader.uploadDatabaseWithLogin(activity, password)
+ } else {
+ Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show()
+ }
+ }
+ .setNegativeButton("Abbrechen", null)
+ .show()
}
}
- // --- Füge diese Funktion in deine Klasse ein ---
- private fun isDatabasePopulated(): Boolean {
- return try {
- val db = MyApp.database.openHelper.readableDatabase
- val cursor = db.query(
- "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'"
- )
- cursor.use { it.count > 0 }
- } catch (e: Exception) {
- false
- }
- }
+
private fun setupDownloadButton() {
downloadButton.text = "Download"
downloadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
-
GlobalValues.LAST_CLIENT_CODE = clientCode
- Toast.makeText(activity, "Datenbank wird heruntergeladen...", Toast.LENGTH_SHORT).show()
- DatabaseDownloader.downloadAndReplaceDatabase(activity)
- updateMainButtonsState(true)
+ // Eingabe-Popup für Passwort anzeigen
+ val input = EditText(activity).apply {
+ hint = "Server-Passwort"
+ }
+
+ android.app.AlertDialog.Builder(activity)
+ .setTitle("Login erforderlich")
+ .setView(input)
+ .setPositiveButton("OK") { _, _ ->
+ val password = input.text.toString()
+ if (password.isNotBlank()) {
+ // Login starten
+ LoginManager.loginUser(
+ context = activity,
+ password = password,
+ onSuccess = { token ->
+ Toast.makeText(activity, "Login erfolgreich", Toast.LENGTH_SHORT).show()
+ DatabaseDownloader.downloadAndReplaceDatabase(activity, token)
+ updateMainButtonsState(true)
+ },
+ onError = { error ->
+ Toast.makeText(activity, error, Toast.LENGTH_LONG).show()
+ }
+ )
+ } else {
+ Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show()
+ }
+ }
+ .setNegativeButton("Abbrechen", null)
+ .show()
}
}
+
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
val buttons = listOf(buttonLoad, saveButton, editButton)
buttons.forEach { button ->
diff --git a/app/src/main/java/com/dano/test1/LanguageManager.kt b/app/src/main/java/com/dano/test1/LanguageManager.kt
index a61a6bb..e7bd420 100644
--- a/app/src/main/java/com/dano/test1/LanguageManager.kt
+++ b/app/src/main/java/com/dano/test1/LanguageManager.kt
@@ -17,7 +17,7 @@ object LanguageManager {
}
private fun injectDynamicValues(text: String): String {
- val points = INTEGRATION_INDEX_POINTS ?: 0
+ val points = RHS_POINTS ?: 0
val color = when (points) {
in 1..12 -> "#4CAF50" // Grün
in 13..36 -> "#FFEB3B" // Gelb
@@ -26,7 +26,7 @@ object LanguageManager {
}
val coloredPoints = "$points"
- return text.replace("INTEGRATION_INDEX_POINTS", coloredPoints)
+ return text.replace("RHS_POINTS", coloredPoints)
}
// Sprachdatenbank: Map>
@@ -285,7 +285,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Bitte wählen Sie eine Antwort pro Reihe aus!",
"no_next_question_defined" to "Keine Weiterleitungsseite definiert",
"date_consultation_health_interview_result" to "Datum Beratungsgespräch (zum Ergebnis Gesundheitsinterview grün/gelb/rot)",
- "consultation_decision" to "Beratungsentscheidung (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Beratungsentscheidung (RHS_POINTS)",
"consent_conversation_in_6_months" to "Einverständnis Gespräch in 6 Monaten:",
"participation_in_coaching" to "Teilnahme am Coaching",
"decision_after_reflection_period" to "Entscheidung am .............. (Datum) nach Bedenkzeit",
@@ -580,7 +580,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Please select one answer per row!",
"no_next_question_defined" to "No forwarding page defined",
"date_consultation_health_interview_result" to "Date of counseling interview (health interview result green/yellow/red)",
- "consultation_decision" to "Counseling decision (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Counseling decision (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consent for conversation in 6 months:",
"participation_in_coaching" to "Participation in coaching",
"decision_after_reflection_period" to "Decision on .............. (date) after reflection period",
@@ -878,7 +878,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Veuillez sélectionner une réponse par ligne !",
"no_next_question_defined" to "Aucune page de redirection définie",
"date_consultation_health_interview_result" to "Date de l’entretien de conseil (résultat de l’entretien santé vert/jaune/rouge)",
- "consultation_decision" to "Décision de conseil (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Décision de conseil (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consentement pour entretien dans 6 mois :",
"participation_in_coaching" to "Participation au coaching",
"decision_after_reflection_period" to "Décision le .............. (date) après période de réflexion",
@@ -1172,7 +1172,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Пожалуйста, выберите один ответ в каждой строке!",
"no_next_question_defined" to "Следующая страница не определена",
"date_consultation_health_interview_result" to "Дата консультации (результат медицинского интервью: зеленый/желтый/красный)",
- "consultation_decision" to "Решение по консультации (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Решение по консультации (RHS_POINTS)",
"consent_conversation_in_6_months" to "Согласие на разговор через 6 месяцев:",
"participation_in_coaching" to "Участие в коучинге",
"decision_after_reflection_period" to "Решение от .............. (дата) после периода раздумий",
@@ -1470,7 +1470,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Будь ласка, оберіть по одній відповіді в кожному рядку!",
"no_next_question_defined" to "Наступна сторінка не визначена",
"date_consultation_health_interview_result" to "Дата консультації (результат медичного інтерв’ю зелений/жовтий/червоний)",
- "consultation_decision" to "Рішення консультації (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Рішення консультації (RHS_POINTS)",
"consent_conversation_in_6_months" to "Згода на розмову через 6 місяців:",
"participation_in_coaching" to "Участь у коучингу",
"decision_after_reflection_period" to "Рішення .............. (дата) після періоду роздумів",
@@ -1768,7 +1768,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Lütfen her satır için bir cevap seçin!",
"no_next_question_defined" to "Bir yönlendirme sayfası tanımlanmadı",
"date_consultation_health_interview_result" to "Danışma görüşmesi tarihi (sağlık görüşmesi sonucu yeşil/sarı/kırmızı)",
- "consultation_decision" to "Danışma kararı (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Danışma kararı (RHS_POINTS)",
"consent_conversation_in_6_months" to "6 ay içinde görüşme onayı:",
"participation_in_coaching" to "Koçluğa katılım",
"decision_after_reflection_period" to "Karar .............. (tarih) tarihinde düşünme süresinden sonra verildi",
@@ -2066,7 +2066,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Proszę wybrać jedną odpowiedź w każdym wierszu!",
"no_next_question_defined" to "Nie zdefiniowano strony przekierowania",
"date_consultation_health_interview_result" to "Data rozmowy doradczej (wynik wywiadu zdrowotnego: zielony/żółty/czerwony)",
- "consultation_decision" to "Decyzja doradcza (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Decyzja doradcza (RHS_POINTS)",
"consent_conversation_in_6_months" to "Zgoda na rozmowę za 6 miesięcy:",
"participation_in_coaching" to "Udział w coachingu",
"decision_after_reflection_period" to "Decyzja dnia .............. (data) po czasie do namysłu",
@@ -2662,7 +2662,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Vă rugăm să selectați un răspuns pe rând!",
"no_next_question_defined" to "Nu este definită o pagină de redirecționare",
"date_consultation_health_interview_result" to "Data consilierii (rezultatul interviului de sănătate verde/galben/roșu)",
- "consultation_decision" to "Decizia consilierii (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Decizia consilierii (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consimțământ discuție peste 6 luni:",
"participation_in_coaching" to "Participare la coaching",
"decision_after_reflection_period" to "Decizie la .............. (dată) după perioada de reflecție",
@@ -2960,7 +2960,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Por favor, seleccione una respuesta por fila.",
"no_next_question_defined" to "No se definió ninguna pregunta de continuación",
"date_consultation_health_interview_result" to "Fecha de la entrevista de orientación (sobre el resultado de la entrevista de salud: verde/amarillo/rojo)",
- "consultation_decision" to "Decisión de orientación (INTEGRATION_INDEX_POINTS)",
+ "consultation_decision" to "Decisión de orientación (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consentimiento para entrevista en 6 meses:",
"participation_in_coaching" to "Participación en el coaching",
"decision_after_reflection_period" to "Decisión tomada el .............. (fecha) después del período de reflexión",
diff --git a/app/src/main/java/com/dano/test1/LoginManager.kt b/app/src/main/java/com/dano/test1/LoginManager.kt
new file mode 100644
index 0000000..34f8a0f
--- /dev/null
+++ b/app/src/main/java/com/dano/test1/LoginManager.kt
@@ -0,0 +1,72 @@
+package com.dano.test1
+
+import android.content.Context
+import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.json.JSONObject
+
+object LoginManager {
+
+ private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
+ private val client = OkHttpClient()
+
+ /**
+ * Startet den Login-Prozess.
+ *
+ * @param context Android Context
+ * @param password Vom User eingegebenes Passwort
+ * @param onSuccess Callback mit dem Token wenn Login erfolgreich
+ * @param onError Callback mit Fehlermeldung
+ */
+ fun loginUser(
+ context: Context,
+ password: String,
+ onSuccess: (String) -> Unit,
+ onError: (String) -> Unit
+ ) {
+ CoroutineScope(Dispatchers.IO).launch {
+ try {
+ val requestBody = """{"password":"$password"}"""
+ .toRequestBody("application/json".toMediaType())
+
+ val request = Request.Builder()
+ .url(SERVER_LOGIN_URL)
+ .post(requestBody)
+ .build()
+
+ val response = client.newCall(request).execute()
+ val responseText = response.body?.string()
+
+ if (response.isSuccessful && responseText != null) {
+ val json = JSONObject(responseText)
+ if (json.getBoolean("success")) {
+ val token = json.getString("token")
+ withContext(Dispatchers.Main) {
+ onSuccess(token)
+ }
+ } else {
+ withContext(Dispatchers.Main) {
+ onError("Login fehlgeschlagen")
+ }
+ }
+ } else {
+ withContext(Dispatchers.Main) {
+ onError("Fehler beim Login (${response.code})")
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("LOGIN", "Exception beim Login", e)
+ withContext(Dispatchers.Main) {
+ onError("Exception: ${e.message}")
+ }
+ }
+ }
+ }
+}