diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..234db15
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..8ff7fe0
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @CyB3RC0nN0R
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..2b978b3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: CyB3RC0nN0R, delvh, DieGurke, derharry333
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..8341438
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,22 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement, feature
+assignees: CyB3RC0nN0R, delvh, DieGurke
+project: Envoy
+milestones: Envoy v0.3-alpha
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md
new file mode 100644
index 0000000..2a918ad
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md
@@ -0,0 +1,11 @@
+---
+name: Bug fix
+title: Fixed Bug
+labels: bug
+assignees: CyB3RC0nN0R, delvh, DieGurke
+reviewers: CyB3RC0nN0R, delvh
+projects: Envoy
+milestone: Envoy v0.1-beta
+
+---
+Fixes #{issue}
diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_integration.md b/.github/PULL_REQUEST_TEMPLATE/feature_integration.md
new file mode 100644
index 0000000..7d5e167
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/feature_integration.md
@@ -0,0 +1,10 @@
+---
+name: Feature integration
+title: Added feature
+labels: feature
+assignees: CyB3RC0nN0R, delvh, DieGurke
+reviewers: CyB3RC0nN0R, delvh
+projects: Envoy
+milestone: Envoy v0.1-beta
+
+---
diff --git a/.github/PULL_REQUEST_TEMPLATE/javadoc_update.md b/.github/PULL_REQUEST_TEMPLATE/javadoc_update.md
new file mode 100644
index 0000000..68b0afd
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/javadoc_update.md
@@ -0,0 +1,10 @@
+---
+name: Updated Javadoc
+title: Updated Javadoc
+labels: documentation
+assignees: CyB3RC0nN0R, delvh
+reviewers: CyB3RC0nN0R, delvh
+projects: Envoy
+milestone: Envoy v0.1-beta
+
+---
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..e9f97db
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,28 @@
+name: Java CI
+
+on: [push]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+ - name: Cache Maven packages
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2
+ key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
+ - name: Stage build artifacts
+ run: mkdir staging && cp target/*.jar staging
+ - uses: actions/upload-artifact@v1
+ with:
+ name: envoy-client-artifacts
+ path: staging
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e01c619
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/target/
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
diff --git a/.project b/.project
new file mode 100644
index 0000000..6d84958
--- /dev/null
+++ b/.project
@@ -0,0 +1,38 @@
+
+
+ envoy-client
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.jboss.tools.jst.web.kb.kbbuilder
+
+
+
+
+ org.jboss.tools.cdi.core.cdibuilder
+
+
+
+
+ org.eclipse.wst.validation.validationbuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..04cfa2c
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..65c71af
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,491 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.doc.comment.support=enabled
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.invalidJavadoc=info
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=public
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=info
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=all_standard_tags
+org.eclipse.jdt.core.compiler.problem.missingJavadocTags=info
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=true
+org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=1
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=true
+org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=true
+org.eclipse.jdt.core.formatter.align_with_spaces=false
+org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=84
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=80
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=20
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0
+org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_module_statements=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=84
+org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0
+org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0
+org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=1
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=true
+org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=true
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=true
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
+org.eclipse.jdt.core.formatter.comment.indent_tag_description=false
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=true
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert
+org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=false
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_if_single_item
+org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=true
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_single_item
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always
+org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_if_single_item
+org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=true
+org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_if_empty
+org.eclipse.jdt.core.formatter.lineSplit=150
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=separate_lines_if_wrapped
+org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=tab
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.text_block_indentation=0
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false
+org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
+org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..f6a7cc1
--- /dev/null
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,9 @@
+eclipse.preferences.version=1
+formatter_profile=_KSKE
+formatter_settings_version=18
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=java;javax;javafx;org;com;envoy;
+org.eclipse.jdt.ui.javadoc=true
+org.eclipse.jdt.ui.ondemandthreshold=4
+org.eclipse.jdt.ui.staticondemandthreshold=2
+org.eclipse.jdt.ui.text.custom_code_templates=/**\n * @return the ${bare_field_name}\n * @since Envoy Client v0.1-beta\n *//**\n * @param ${param} the ${bare_field_name} to set\n * @since Envoy Client v0.1-beta\n *//**\n * ${tags}\n * @since Envoy Client v0.1-beta\n *//**\n * Project\: <strong>${project_name}</strong><br>\n * File\: <strong>${file_name}</strong><br>\n * Created\: <strong>${date}</strong><br>\n * \n * @author ${user}\n * @since Envoy Client v0.1-beta\n *//**\n * ${tags}\n * @since Envoy Client v0.1-beta\n *//**\n * @author ${user}\n *\n * ${tags}\n * @since Envoy Client v0.1-beta\n *//**\n * {@inheritDoc}\n *//**\n * ${tags}\n * ${see_to_target}\n * @since Envoy Client v0.1-beta\n */${filecomment}\n${package_declaration}\n\n${typecomment}\n${type_declaration}\n\n\n\n${exception_var}.printStackTrace();${body_statement}${body_statement}return ${field};${field} \= ${param};
diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..14b697b
--- /dev/null
+++ b/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/.settings/org.hibernate.eclipse.console.prefs b/.settings/org.hibernate.eclipse.console.prefs
new file mode 100644
index 0000000..21fefff
--- /dev/null
+++ b/.settings/org.hibernate.eclipse.console.prefs
@@ -0,0 +1,3 @@
+default.configuration=
+eclipse.preferences.version=1
+hibernate3.enabled=false
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..9d4ae77
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at kske@outlook.de. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..1d682be
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,161 @@
+# Contributing to Envoy
+
+Looking to contribute something to Envoy? **Here's how you can help.**
+
+Please take a moment to review this document in order to make the contribution
+process easy and effective for everyone involved.
+
+Following these guidelines helps to communicate that you respect the time of
+the developers managing and developing this open source project. In return,
+they should reciprocate that respect in addressing your issue or assessing
+patches and features.
+
+
+## Using the issue tracker
+
+The [issue tracker](https://github.com/informatik-ag-ngl/envoy-client/issues) is
+the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests)
+and [submitting pull requests](#pull-requests), but please respect the following
+restrictions:
+
+* Please **do not** derail or troll issues. Keep the discussion on topic and
+ respect the opinions of others.
+
+* Please **do not** post comments consisting solely of "+1" or ":thumbsup:".
+ Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/)
+ instead. We reserve the right to delete comments which violate this rule.
+
+ However, as we know, we are all software engineers that like being funny hence doing it on purpose. Please also refrain from that kind of behaviour.
+
+## Issues and labels
+
+Our bug tracker utilizes several labels to help organize and identify issues. Here's what they represent and how we use them:
+
+- `Documentation` & `Javadoc`- Issues regarding the documentation of Envoy
+- `Enhancement` & `Feature` - Issues suggesting a new feature
+- `Maven` - Issues concerned with Maven problems
+- `Bug` - Issues concerned with a general bug
+
+For a complete look at our labels, see the [project labels page](https://github.com/informatik-ag-ngl/envoy-client/labels).
+
+## Bug reports
+
+A bug is a _demonstrable problem_ that is caused by the code in the repository.
+Good bug reports are extremely helpful, so thanks!
+
+Guidelines for bug reports:
+
+0. **ensure your problem isn't caused by a simple error in your own code**.
+
+1. **Use the GitHub issue search** — check if the issue has already been
+ reported.
+
+2. **Check if the issue has been fixed** — try to reproduce it using the
+ latest `master` or development branch in the repository.
+
+3. **Isolate the problem** — ideally create a reduced test
+ case and a live example.
+
+
+A good bug report shouldn't leave others needing to chase you up for more
+information. Please try to be as detailed as possible in your report. What is
+your environment? What steps will reproduce the issue? These details will help people to fix
+any potential bugs.
+
+Example:
+
+> Short and descriptive example bug report title
+>
+> 1. This is the first step
+> 2. This is the second step
+> 3. Further steps, etc.
+>
+> Any other information you want to share that is relevant to the issue being
+> reported. This might include the lines of code that you have identified as
+> causing the bug, and potential solutions (and your opinions on their
+> merits).
+
+## Feature requests
+
+Feature requests are welcome. But take a moment to find out whether your idea
+fits with the scope and aims of the project. It's up to *you* to make a strong
+case to convince the project's developers of the merits of this feature. Please
+provide as much detail and context as possible.
+
+
+## Pull requests
+
+Good pull requests—patches, improvements, new features—are a fantastic
+help. They should remain focused in scope and avoid containing unrelated
+commits.
+
+**Please ask first** before embarking on any significant pull request (e.g.
+implementing features, refactoring code, porting to a different language),
+otherwise you risk spending a lot of time working on something that the
+project's developers might not want to merge into the project.
+
+Please adhere to the [coding guidelines](#code-guidelines) used throughout the
+project (indentation, accurate comments, etc.) and any other requirements
+(such as test coverage).
+
+Adhering to the following process is the best way to get your work
+included in the project:
+
+1. Download, clone or [Fork](https://help.github.com/articles/fork-a-repo/) the project, using [https://github.com/informatik-ag-ngl/envoy-client/](https://github.com/informatik-ag-ngl/envoy-client/)as Remote.
+
+2. If you cloned a while ago, get the latest changes from upstream:
+
+ ```bash
+ git checkout master
+ git pull upstream master
+ ```
+ Or, if your IDE of choice supports this, simply use `pull`
+
+3. Create a new topic branch (off the main project development branch) to
+ contain your feature, change, or fix:
+
+ ```bash
+ git checkout -b
+ ```
+ Or, simply use "New branch" if your IDE supports this
+
+4. Commit your changes in logical chunks. Please adhere to these [git commit
+ message guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+ or your code is unlikely be merged into the main project. Use Git's
+ [interactive rebase](https://help.github.com/articles/about-git-rebase/)
+ feature to tidy up your commits before making them public.
+
+5. Locally merge (or rebase) the upstream development branch into your topic branch:
+
+ ```bash
+ git pull [--rebase] upstream master
+ ```
+
+6. Push your topic branch up to your fork:
+
+ ```bash
+ git push origin
+ ```
+
+7. [Open a Pull Request](https://help.github.com/articles/about-pull-requests/)
+ with a clear title and description against the `master` branch.
+
+**IMPORTANT**: By submitting a patch, you agree to allow the project owners to
+license your work under the terms of the [MIT License](../LICENSE) (if it
+includes code changes) and under the terms of the
+[Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
+(if it includes documentation changes).
+
+
+## Code guidelines
+
+### Java
+
+Please use the formatter provided with this project. Especially before saving. For best results, select the option "format code" in the "Save Actions" tab in Preferences in Eclipse, so that you never accidentally forget it.
+Every public function (not annotated with `@Override`) must be delivered with Javadoc. For best project-appropriate Javadoc please take a look at the other functions which are all already equipped with Javadoc.
+
+
+## License
+
+By contributing your code, you agree to license your contribution under the [MIT License](../LICENSE).
+By contributing to the documentation, you agree to license your contribution under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..46da469
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Informatik-AG (NGL)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..48fa3a6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+# Envoy Client
+
+
+
+**Envoy Client** is one of two repositories needed to use the messenger Envoy.
+The other one is **Envoy Common**.
+
+
+## Features
+
+Envoy Client features a lot of things and many more are yet to come.
+Currently existing features are:
+
+* Users
+ * Saving and loading of messages
+ * Login via name
+ * Settings to change the behavior of _Envoy_
+* UI
+ * Appealing user interface
+ * Changeable themes that store the colors used in _Envoy_
+ * Possibility to run _Envoy_ in the Background once it has been started
+ * Possibility to exit _Envoy_
+* Connectivity
+ * Sending messages to another person via a predefined server
+ * Offline mode
+* Programming
+ * API to change default configuration
+ * Advanced logging possibilities
+ * Access without Admin rights possible via local message storage in the home folder
+ * Tons of Events to interact with
+ * Detailed Javadoc to improve readability of code
+
+## Resources
+
+* [API Reference (later on)](https://github.com/informatik-ag-ngl/envoy-client/wiki)
+* [Release Notes](https://github.com/informatik-ag-ngl/envoy-client/releases)
+* [Gallery (later on)](https://github.com/informatik-ag-ngl/envoy-client/wiki/Gallery)
+* [Wiki](https://github.com/informatik-ag-ngl/envoy-client/wiki)
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..92cf7df
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,99 @@
+
+ 4.0.0
+
+ informatik-ag-ngl
+ envoy-client
+ 0.1-beta
+
+ Envoy Client
+ https://github.com/informatik-ag-ngl/envoy-client
+
+
+ UTF-8
+ UTF-8
+ 11
+ 11
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+ com.github.informatik-ag-ngl
+ envoy-common
+ develop-SNAPSHOT
+
+
+ org.openjfx
+ javafx-controls
+ 11.0.2
+
+
+ org.openjfx
+ javafx-fxml
+ 11.0.2
+
+
+ org.openjfx
+ javafx-graphics
+ 11
+ win
+
+
+ org.openjfx
+ javafx-graphics
+ 11
+ linux
+
+
+
+
+ envoy-client
+
+
+ src/main/resources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+
+ package
+
+ shade
+
+
+ true
+ envoy
+
+
+ envoy.client.Main
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/envoy/client/Main.java b/src/main/java/envoy/client/Main.java
new file mode 100644
index 0000000..57f2d6d
--- /dev/null
+++ b/src/main/java/envoy/client/Main.java
@@ -0,0 +1,30 @@
+package envoy.client;
+
+import javafx.application.Application;
+
+import envoy.client.ui.Startup;
+
+/**
+ * Triggers application startup.
+ *
+ * To allow Maven shading, the main method has to be separated from the
+ * {@link Startup} class which extends {@link Application}.
+ *
+ * Project: envoy-client
+ * File: Main.java
+ * Created: 05.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public class Main {
+
+ /**
+ * Starts the application.
+ *
+ * @param args the command line arguments are processed by the
+ * client configuration
+ * @since Envoy Client v0.1-beta
+ */
+ public static void main(String[] args) { Application.launch(Startup.class, args); }
+}
diff --git a/src/main/java/envoy/client/data/Cache.java b/src/main/java/envoy/client/data/Cache.java
new file mode 100644
index 0000000..25b985e
--- /dev/null
+++ b/src/main/java/envoy/client/data/Cache.java
@@ -0,0 +1,65 @@
+package envoy.client.data;
+
+import java.io.Serializable;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import envoy.util.EnvoyLog;
+
+/**
+ * Stores elements in a queue to process them later.
+ *
+ * Project: envoy-client
+ * File: Cache.java
+ * Created: 6 Feb 2020
+ *
+ * @param the type of cached elements
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public final class Cache implements Consumer, Serializable {
+
+ private final Queue elements = new LinkedList<>();
+ private transient Consumer processor;
+
+ private static final Logger logger = EnvoyLog.getLogger(Cache.class);
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Adds an element to the cache.
+ *
+ * @param element the element to add
+ * @since Envoy Client v0.3-alpha
+ */
+ @Override
+ public void accept(T element) {
+ logger.log(Level.FINE, String.format("Adding element %s to cache", element));
+ elements.offer(element);
+ }
+
+ @Override
+ public String toString() { return String.format("Cache[elements=" + elements + "]"); }
+
+ /**
+ * Sets the processor to which cached elements are relayed.
+ *
+ * @param processor the processor to set
+ * @since Envoy Client v0.3-alpha
+ */
+ public void setProcessor(Consumer processor) { this.processor = processor; }
+
+ /**
+ * Relays all cached elements to the processor.
+ *
+ * @throws IllegalStateException if the processor is not initialized
+ * @since Envoy Client v0.3-alpha
+ */
+ public void relay() {
+ if (processor == null) throw new IllegalStateException("Processor is not defined");
+ elements.forEach(processor::accept);
+ elements.clear();
+ }
+}
diff --git a/src/main/java/envoy/client/data/CacheMap.java b/src/main/java/envoy/client/data/CacheMap.java
new file mode 100644
index 0000000..8c1fcb2
--- /dev/null
+++ b/src/main/java/envoy/client/data/CacheMap.java
@@ -0,0 +1,66 @@
+package envoy.client.data;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Stores a heterogeneous map of {@link Cache} objects with different type
+ * parameters.
+ *
+ * Project: envoy-client
+ * File: CacheMap.java
+ * Created: 09.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public final class CacheMap implements Serializable {
+
+ private final Map, Cache>> map = new HashMap<>();
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Adds a cache to the map.
+ *
+ * @param the type accepted by the cache
+ * @param key the class that maps to the cache
+ * @param cache the cache to store
+ * @since Envoy Client v0.1-beta
+ */
+ public void put(Class key, Cache cache) { map.put(key, cache); }
+
+ /**
+ * Returns a cache mapped by a class.
+ *
+ * @param the type accepted by the cache
+ * @param key the class that maps to the cache
+ * @return the cache
+ * @since Envoy Client v0.1-beta
+ */
+ public Cache get(Class key) { return (Cache) map.get(key); }
+
+ /**
+ * Returns a cache mapped by a class or any of its subclasses.
+ *
+ * @param the type accepted by the cache
+ * @param key the class that maps to the cache
+ * @return the cache
+ * @since Envoy Client v0.1-beta
+ */
+ public Cache super T> getApplicable(Class key) {
+ Cache super T> cache = get(key);
+ if (cache == null)
+ for (var e : map.entrySet())
+ if (e.getKey().isAssignableFrom(key))
+ cache = (Cache super T>) e.getValue();
+ return cache;
+ }
+
+ /**
+ * @return the map in which the caches are stored
+ * @since Envoy Client v0.1-beta
+ */
+ public Map, Cache>> getMap() { return map; }
+}
diff --git a/src/main/java/envoy/client/data/Chat.java b/src/main/java/envoy/client/data/Chat.java
new file mode 100644
index 0000000..b1c2beb
--- /dev/null
+++ b/src/main/java/envoy/client/data/Chat.java
@@ -0,0 +1,153 @@
+package envoy.client.data;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import envoy.client.net.WriteProxy;
+import envoy.data.*;
+import envoy.data.Message.MessageStatus;
+import envoy.event.MessageStatusChange;
+
+/**
+ * Represents a chat between two {@link User}s
+ * as a list of {@link Message} objects.
+ *
+ * Project: envoy-client
+ * File: Chat.java
+ * Created: 19 Oct 2019
+ *
+ * @author Maximilian Käfer
+ * @author Leon Hofmeister
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-alpha
+ */
+public class Chat implements Serializable {
+
+ protected final Contact recipient;
+ protected final List messages = new ArrayList<>();
+
+ protected int unreadAmount;
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Provides the list of messages that the recipient receives.
+ *
+ * Saves the Messages in the corresponding chat at that Point.
+ *
+ * @param recipient the user who receives the messages
+ * @since Envoy Client v0.1-alpha
+ */
+ public Chat(Contact recipient) {
+ this.recipient = recipient;
+ }
+
+ @Override
+ public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
+
+ /**
+ * Generates a hash code based on the recipient.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ @Override
+ public int hashCode() { return Objects.hash(recipient); }
+
+ /**
+ * Tests equality to another object based on the recipient.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof Chat)) return false;
+ Chat other = (Chat) obj;
+ return Objects.equals(recipient, other.recipient);
+ }
+
+ /**
+ * Sets the status of all chat messages received from the recipient to
+ * {@code READ} starting from the bottom and stopping once a read message is
+ * found.
+ *
+ * @param writeProxy the write proxy instance used to notify the server about
+ * the message status changes
+ * @throws IOException if a {@link MessageStatusChange} could not be
+ * delivered to the server
+ * @since Envoy Client v0.3-alpha
+ */
+ public void read(WriteProxy writeProxy) throws IOException {
+ for (int i = messages.size() - 1; i >= 0; --i) {
+ final Message m = messages.get(i);
+ if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
+ else {
+ m.setStatus(MessageStatus.READ);
+ writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
+ }
+ }
+ unreadAmount = 0;
+ }
+
+ /**
+ * @return {@code true} if the newest message received in the chat doesn't have
+ * the status {@code READ}
+ * @since Envoy Client v0.3-alpha
+ */
+ public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
+
+ /**
+ * Inserts a message at the correct place according to its creation date.
+ *
+ * @param message the message to insert
+ * @since Envoy Client v0.1-beta
+ */
+ public void insert(Message message) {
+ for (int i = messages.size() - 1; i >= 0; --i)
+ if (message.getCreationDate().isAfter(messages.get(i).getCreationDate())) {
+ messages.add(i + 1, message);
+ return;
+ }
+ messages.add(0, message);
+ }
+
+ /**
+ * Increments the amount of unread messages.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public void incrementUnreadAmount() { unreadAmount++; }
+
+ /**
+ * @return the amount of unread mesages in this chat
+ * @since Envoy Client v0.1-beta
+ */
+ public int getUnreadAmount() { return unreadAmount; }
+
+ /**
+ * @return all messages in the current chat
+ * @since Envoy Client v0.1-beta
+ */
+ public List getMessages() { return messages; }
+
+ /**
+ * @return the recipient of a message
+ * @since Envoy Client v0.1-alpha
+ */
+ public Contact getRecipient() { return recipient; }
+
+ /**
+ * @return whether this {@link Chat} points at a {@link User}
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isUserChat() { return recipient instanceof User; }
+
+ /**
+ * @return whether this {@link Chat} points at a {@link Group}
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isGroupChat() { return recipient instanceof Group; }
+}
diff --git a/src/main/java/envoy/client/data/ClientConfig.java b/src/main/java/envoy/client/data/ClientConfig.java
new file mode 100644
index 0000000..49e7892
--- /dev/null
+++ b/src/main/java/envoy/client/data/ClientConfig.java
@@ -0,0 +1,115 @@
+package envoy.client.data;
+
+import static java.util.function.Function.identity;
+
+import java.io.File;
+import java.util.logging.Level;
+
+import envoy.client.ui.Startup;
+import envoy.data.Config;
+import envoy.data.ConfigItem;
+import envoy.data.LoginCredentials;
+
+/**
+ * Implements a configuration specific to the Envoy Client with default values
+ * and convenience methods.
+ *
+ * Project: envoy-client
+ * File: ClientConfig.java
+ * Created: 01.03.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public class ClientConfig extends Config {
+
+ private static ClientConfig config;
+
+ /**
+ * @return the singleton instance of the client config
+ * @since Envoy Client v0.1-beta
+ */
+ public static ClientConfig getInstance() {
+ if (config == null) config = new ClientConfig();
+ return config;
+ }
+
+ private ClientConfig() {
+ items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
+ items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
+ items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
+ items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
+ items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
+ items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
+ items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
+ items.put("user", new ConfigItem<>("user", "u", identity()));
+ items.put("password", new ConfigItem<>("password", "pw", identity()));
+ }
+
+ /**
+ * @return the host name of the Envoy server
+ * @since Envoy Client v0.1-alpha
+ */
+ public String getServer() { return (String) items.get("server").get(); }
+
+ /**
+ * @return the port at which the Envoy server is located on the host
+ * @since Envoy Client v0.1-alpha
+ */
+ public Integer getPort() { return (Integer) items.get("port").get(); }
+
+ /**
+ * @return the local database specific to the client user
+ * @since Envoy Client v0.1-alpha
+ */
+ public File getLocalDB() { return (File) items.get("localDB").get(); }
+
+ /**
+ * @return {@code true} if the local database is to be ignored
+ * @since Envoy Client v0.3-alpha
+ */
+ public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
+
+ /**
+ * @return the directory in which all local files are saves
+ * @since Envoy Client v0.2-alpha
+ */
+ public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
+
+ /**
+ * @return the minimal {@link Level} to log inside the log file
+ * @since Envoy Client v0.2-alpha
+ */
+ public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
+
+ /**
+ * @return the minimal {@link Level} to log inside the console
+ * @since Envoy Client v0.2-alpha
+ */
+ public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
+
+ /**
+ * @return the user name
+ * @since Envoy Client v0.3-alpha
+ */
+ public String getUser() { return (String) items.get("user").get(); }
+
+ /**
+ * @return the password
+ * @since Envoy Client v0.3-alpha
+ */
+ public String getPassword() { return (String) items.get("password").get(); }
+
+ /**
+ * @return {@code true} if user name and password are set
+ * @since Envoy Client v0.3-alpha
+ */
+ public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
+
+ /**
+ * @return login credentials for the specified user name and password, without
+ * the registration option
+ * @since Envoy Client v0.3-alpha
+ */
+ public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
+}
diff --git a/src/main/java/envoy/client/data/GroupChat.java b/src/main/java/envoy/client/data/GroupChat.java
new file mode 100644
index 0000000..aafd3a0
--- /dev/null
+++ b/src/main/java/envoy/client/data/GroupChat.java
@@ -0,0 +1,55 @@
+package envoy.client.data;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+
+import envoy.client.net.WriteProxy;
+import envoy.data.Contact;
+import envoy.data.GroupMessage;
+import envoy.data.Message.MessageStatus;
+import envoy.data.User;
+import envoy.event.GroupMessageStatusChange;
+
+/**
+ * Represents a chat between a user and a group
+ * as a list of messages.
+ *
+ * Project: envoy-client
+ * File: GroupChat.java
+ * Created: 05.07.2020
+ *
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-beta
+ */
+public class GroupChat extends Chat {
+
+ private final User sender;
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param sender the user sending the messages
+ * @param recipient the group whose members receive the messages
+ * @since Envoy Client v0.1-beta
+ */
+ public GroupChat(User sender, Contact recipient) {
+ super(recipient);
+ this.sender = sender;
+ }
+
+ @Override
+ public void read(WriteProxy writeProxy) throws IOException {
+ for (int i = messages.size() - 1; i >= 0; --i) {
+ final GroupMessage gmsg = (GroupMessage) messages.get(i);
+ if (gmsg.getSenderID() != sender.getID()) {
+ if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
+ else {
+ gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
+ writeProxy
+ .writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
+ }
+ }
+ }
+ unreadAmount = 0;
+ }
+}
diff --git a/src/main/java/envoy/client/data/LocalDB.java b/src/main/java/envoy/client/data/LocalDB.java
new file mode 100644
index 0000000..421cb38
--- /dev/null
+++ b/src/main/java/envoy/client/data/LocalDB.java
@@ -0,0 +1,205 @@
+package envoy.client.data;
+
+import java.util.*;
+
+import envoy.data.*;
+import envoy.event.GroupResize;
+import envoy.event.MessageStatusChange;
+import envoy.event.NameChange;
+
+/**
+ * Stores information about the current {@link User} and their {@link Chat}s.
+ * For message ID generation a {@link IDGenerator} is stored as well.
+ *
+ * Project: envoy-client
+ * File: LocalDB.java
+ * Created: 3 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public abstract class LocalDB {
+
+ protected User user;
+ protected Map users = new HashMap<>();
+ protected List chats = new ArrayList<>();
+ protected IDGenerator idGenerator;
+ protected CacheMap cacheMap = new CacheMap();
+
+ {
+ cacheMap.put(Message.class, new Cache<>());
+ cacheMap.put(MessageStatusChange.class, new Cache<>());
+ }
+
+ /**
+ * Initializes a storage space for a user-specific list of chats.
+ *
+ * @since Envoy Client v0.3-alpha
+ */
+ public void initializeUserStorage() {}
+
+ /**
+ * Stores all users. If the client user is specified, their chats will be stored
+ * as well. The message id generator will also be saved if present.
+ *
+ * @throws Exception if the saving process failed
+ * @since Envoy Client v0.3-alpha
+ */
+ public void save() throws Exception {}
+
+ /**
+ * Loads all user data.
+ *
+ * @throws Exception if the loading process failed
+ * @since Envoy Client v0.3-alpha
+ */
+ public void loadUsers() throws Exception {}
+
+ /**
+ * Loads all data of the client user.
+ *
+ * @throws Exception if the loading process failed
+ * @since Envoy Client v0.3-alpha
+ */
+ public void loadUserData() throws Exception {}
+
+ /**
+ * Loads the ID generator. Any exception thrown during this process is ignored.
+ *
+ * @since Envoy Client v0.3-alpha
+ */
+ public void loadIDGenerator() {}
+
+ /**
+ * Synchronizes the contact list of the client user with the chat and user
+ * storage.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public void synchronize() {
+ user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
+ users.put(user.getName(), user);
+
+ // Synchronize user status data
+ for (Contact contact : users.values())
+ if (contact instanceof User)
+ getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
+
+ // Create missing chats
+ user.getContacts()
+ .stream()
+ .filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
+ .map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
+ .forEach(chats::add);
+ }
+
+ /**
+ * @return a {@code Map} of all users stored locally with their
+ * user names as keys
+ * @since Envoy Client v0.2-alpha
+ */
+ public Map getUsers() { return users; }
+
+ /**
+ * @return all saved {@link Chat} objects that list the client user as the
+ * sender
+ * @since Envoy Client v0.1-alpha
+ **/
+ public List getChats() { return chats; }
+
+ /**
+ * @param chats the chats to set
+ */
+ public void setChats(List chats) { this.chats = chats; }
+
+ /**
+ * @return the {@link User} who initialized the local database
+ * @since Envoy Client v0.2-alpha
+ */
+ public User getUser() { return user; }
+
+ /**
+ * @param user the user to set
+ * @since Envoy Client v0.2-alpha
+ */
+ public void setUser(User user) { this.user = user; }
+
+ /**
+ * @return the message ID generator
+ * @since Envoy Client v0.3-alpha
+ */
+ public IDGenerator getIDGenerator() { return idGenerator; }
+
+ /**
+ * @param idGenerator the message ID generator to set
+ * @since Envoy Client v0.3-alpha
+ */
+ public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
+
+ /**
+ * @return {@code true} if an {@link IDGenerator} is present
+ * @since Envoy Client v0.3-alpha
+ */
+ public boolean hasIDGenerator() { return idGenerator != null; }
+
+ /**
+ * @return the cache map for messages and message status changes
+ * @since Envoy Client v0.1-beta
+ */
+ public CacheMap getCacheMap() { return cacheMap; }
+
+ /**
+ * Searches for a message by ID.
+ *
+ * @param id the ID of the message to search for
+ * @return an optional containing the message
+ * @since Envoy Client v0.1-beta
+ */
+ public Optional getMessage(long id) {
+ return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
+ }
+
+ /**
+ * Searches for a chat by recipient ID.
+ *
+ * @param recipientID the ID of the chat's recipient
+ * @return an optional containing the chat
+ * @since Envoy Client v0.1-beta
+ */
+ public Optional getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
+
+ /**
+ * Performs a contact name change if the corresponding contact is present.
+ *
+ * @param event the {@link NameChange} to process
+ * @since Envoy Client v0.1-beta
+ */
+ public void replaceContactName(NameChange event) {
+ chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
+ }
+
+ /**
+ * Performs a group resize operation if the corresponding group is present.
+ *
+ * @param event the {@link GroupResize} to process
+ * @since Envoy Client v0.1-beta
+ */
+ public void updateGroup(GroupResize event) {
+ chats.stream()
+ .map(Chat::getRecipient)
+ .filter(Group.class::isInstance)
+ .filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
+ .map(Group.class::cast)
+ .findAny()
+ .ifPresent(group -> {
+ switch (event.getOperation()) {
+ case ADD:
+ group.getContacts().add(event.get());
+ break;
+ case REMOVE:
+ group.getContacts().remove(event.get());
+ break;
+ }
+ });
+ }
+}
diff --git a/src/main/java/envoy/client/data/PersistentLocalDB.java b/src/main/java/envoy/client/data/PersistentLocalDB.java
new file mode 100644
index 0000000..182a4c8
--- /dev/null
+++ b/src/main/java/envoy/client/data/PersistentLocalDB.java
@@ -0,0 +1,88 @@
+package envoy.client.data;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import envoy.data.IDGenerator;
+import envoy.util.SerializationUtils;
+
+/**
+ * Implements a {@link LocalDB} in a way that stores all information inside a
+ * folder on the local file system.
+ *
+ * Project: envoy-client
+ * File: PersistentLocalDB.java
+ * Created: 27.10.2019
+ *
+ * @author Kai S. K. Engelbart
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-alpha
+ */
+public final class PersistentLocalDB extends LocalDB {
+
+ private File dbDir, userFile, idGeneratorFile, usersFile;
+
+ /**
+ * Constructs an empty local database. To serialize any user-specific data to
+ * the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
+ * and then {@link PersistentLocalDB#save()}.
+ *
+ * @param dbDir the directory in which to persist data
+ * @throws IOException if {@code dbDir} is a file (and not a directory)
+ * @since Envoy Client v0.1-alpha
+ */
+ public PersistentLocalDB(File dbDir) throws IOException {
+ this.dbDir = dbDir;
+
+ // Test if the database directory is actually a directory
+ if (dbDir.exists() && !dbDir.isDirectory())
+ throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
+
+ // Initialize global files
+ idGeneratorFile = new File(dbDir, "id_gen.db");
+ usersFile = new File(dbDir, "users.db");
+ }
+
+ /**
+ * Creates a database file for a user-specific list of chats.
+ *
+ * @throws IllegalStateException if the client user is not specified
+ * @since Envoy Client v0.1-alpha
+ */
+ @Override
+ public void initializeUserStorage() {
+ if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
+ userFile = new File(dbDir, user.getID() + ".db");
+ }
+
+ @Override
+ public void save() throws IOException {
+ // Save users
+ SerializationUtils.write(usersFile, users);
+
+ // Save user data
+ if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
+
+ // Save id generator
+ if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
+ }
+
+ @Override
+ public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
+
+ @Override
+ public void loadUserData() throws ClassNotFoundException, IOException {
+ try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
+ chats = (ArrayList) in.readObject();
+ cacheMap = (CacheMap) in.readObject();
+ }
+ }
+
+ @Override
+ public void loadIDGenerator() {
+ try {
+ idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
+ } catch (ClassNotFoundException | IOException e) {}
+ }
+}
diff --git a/src/main/java/envoy/client/data/Settings.java b/src/main/java/envoy/client/data/Settings.java
new file mode 100644
index 0000000..fc16c26
--- /dev/null
+++ b/src/main/java/envoy/client/data/Settings.java
@@ -0,0 +1,146 @@
+package envoy.client.data;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.prefs.Preferences;
+
+import envoy.util.SerializationUtils;
+
+/**
+ * Manages all application settings, which are different objects that can be
+ * changed during runtime and serialized them by using either the file system or
+ * the {@link Preferences} API.
+ *
+ * Project: envoy-client
+ * File: Settings.java
+ * Created: 11 Nov 2019
+ *
+ * @author Leon Hofmeister
+ * @author Maximilian Käfer
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.2-alpha
+ */
+public class Settings {
+
+ // Actual settings accessible by the rest of the application
+ private Map> items;
+
+ /**
+ * Settings are stored in this file.
+ */
+ private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
+
+ /**
+ * Singleton instance of this class.
+ */
+ private static Settings settings = new Settings();
+
+ /**
+ * The way to instantiate the settings. Is set to private to deny other
+ * instances of that object.
+ *
+ * @since Envoy Client v0.2-alpha
+ */
+ private Settings() {
+ // Load settings from settings file
+ try {
+ items = SerializationUtils.read(settingsFile, HashMap.class);
+ } catch (ClassNotFoundException | IOException e) {
+ items = new HashMap<>();
+ }
+ supplementDefaults();
+ }
+
+ /**
+ * This method is used to ensure that there is only one instance of Settings.
+ *
+ * @return the instance of Settings
+ * @since Envoy Client v0.2-alpha
+ */
+ public static Settings getInstance() { return settings; }
+
+ /**
+ * Updates the preferences when the save button is clicked.
+ *
+ * @throws IOException if an error occurs while saving the themes
+ * @since Envoy Client v0.2-alpha
+ */
+ public void save() throws IOException {
+
+ // Save settings to settings file
+ SerializationUtils.write(settingsFile, items);
+ }
+
+ private void supplementDefaults() {
+ items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
+ items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
+ items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
+ }
+
+ /**
+ * @return the name of the currently active theme
+ * @since Envoy Client v0.2-alpha
+ */
+ public String getCurrentTheme() { return (String) items.get("currentTheme").get(); }
+
+ /**
+ * Sets the name of the current theme.
+ *
+ * @param themeName the name to set
+ * @since Envoy Client v0.2-alpha
+ */
+ public void setCurrentTheme(String themeName) { ((SettingsItem) items.get("currentTheme")).set(themeName); }
+
+ /**
+ * @return true if the currently used theme is one of the default themes
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isUsingDefaultTheme() {
+ final var theme = getCurrentTheme();
+ return theme.equals("dark") || theme.equals("light");
+ }
+
+ /**
+ * @return {@code true}, if pressing the {@code Enter} key suffices to send a
+ * message. Otherwise it has to be pressed in conjunction with the
+ * {@code Control} key.
+ * @since Envoy Client v0.2-alpha
+ */
+ public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
+
+ /**
+ * Changes the keystrokes performed by the user to send a message.
+ *
+ * @param enterToSend If set to {@code true} a message can be sent by pressing
+ * the {@code Enter} key. Otherwise it has to be pressed in
+ * conjunction with the {@code Control} key.
+ * @since Envoy Client v0.2-alpha
+ */
+ public void setEnterToSend(boolean enterToSend) { ((SettingsItem) items.get("enterToSend")).set(enterToSend); }
+
+ /**
+ * @return the current on close mode.
+ * @since Envoy Client v0.3-alpha
+ */
+ public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
+
+ /**
+ * Sets the current on close mode.
+ *
+ * @param currentOnCloseMode the on close mode that should be set.
+ * @since Envoy Client v0.3-alpha
+ */
+ public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem) items.get("onCloseMode")).set(currentOnCloseMode); }
+
+ /**
+ * @return the items
+ */
+ public Map> getItems() { return items; }
+
+ /**
+ * @param items the items to set
+ */
+ public void setItems(Map> items) { this.items = items; }
+}
diff --git a/src/main/java/envoy/client/data/SettingsItem.java b/src/main/java/envoy/client/data/SettingsItem.java
new file mode 100644
index 0000000..d74e222
--- /dev/null
+++ b/src/main/java/envoy/client/data/SettingsItem.java
@@ -0,0 +1,99 @@
+package envoy.client.data;
+
+import java.io.Serializable;
+import java.util.function.Consumer;
+
+import javax.swing.JComponent;
+
+/**
+ * Encapsulates a persistent value that is directly or indirectly mutable by the
+ * user.
+ *
+ * Project: envoy-client
+ * File: SettingsItem.java
+ * Created: 23.12.2019
+ *
+ * @param the type of this {@link SettingsItem}'s value
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public class SettingsItem implements Serializable {
+
+ private T value;
+ private String userFriendlyName, description;
+
+ private transient Consumer changeHandler;
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Initializes a {@link SettingsItem}. The default value's class will be mapped
+ * to a {@link JComponent} that can be used to display this {@link SettingsItem}
+ * to the user.
+ *
+ * @param value the default value
+ * @param userFriendlyName the user friendly name (short)
+ * @param description the description (long)
+ * @since Envoy Client v0.3-alpha
+ */
+ public SettingsItem(T value, String userFriendlyName, String description) {
+ this.value = value;
+ this.userFriendlyName = userFriendlyName;
+ this.description = description;
+ }
+
+ /**
+ * @return the value
+ * @since Envoy Client v0.3-alpha
+ */
+ public T get() { return value; }
+
+ /**
+ * Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
+ * defined, it will be invoked with this value.
+ *
+ * @param value the value to set
+ * @since Envoy Client v0.3-alpha
+ */
+ public void set(T value) {
+ if (changeHandler != null && value != this.value) changeHandler.accept(value);
+ this.value = value;
+ }
+
+ /**
+ * @return the userFriendlyName
+ * @since Envoy Client v0.3-alpha
+ */
+ public String getUserFriendlyName() { return userFriendlyName; }
+
+ /**
+ * @param userFriendlyName the userFriendlyName to set
+ * @since Envoy Client v0.3-alpha
+ */
+ public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
+
+ /**
+ * @return the description
+ * @since Envoy Client v0.3-alpha
+ */
+ public String getDescription() { return description; }
+
+ /**
+ * @param description the description to set
+ * @since Envoy Client v0.3-alpha
+ */
+ public void setDescription(String description) { this.description = description; }
+
+ /**
+ * Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be
+ * invoked with the current value once during the registration and every time
+ * when the value changes.
+ *
+ * @param changeHandler the changeHandler to set
+ * @since Envoy Client v0.3-alpha
+ */
+ public void setChangeHandler(Consumer changeHandler) {
+ this.changeHandler = changeHandler;
+ changeHandler.accept(value);
+ }
+}
diff --git a/src/main/java/envoy/client/data/TransientLocalDB.java b/src/main/java/envoy/client/data/TransientLocalDB.java
new file mode 100644
index 0000000..5c7d2a6
--- /dev/null
+++ b/src/main/java/envoy/client/data/TransientLocalDB.java
@@ -0,0 +1,15 @@
+package envoy.client.data;
+
+/**
+ * Implements a {@link LocalDB} in a way that does not persist any information
+ * after application shutdown.
+ *
+ * Project: envoy-client
+ * File: TransientLocalDB.java
+ * Created: 3 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public final class TransientLocalDB extends LocalDB {
+}
diff --git a/src/main/java/envoy/client/data/audio/AudioPlayer.java b/src/main/java/envoy/client/data/audio/AudioPlayer.java
new file mode 100644
index 0000000..61982db
--- /dev/null
+++ b/src/main/java/envoy/client/data/audio/AudioPlayer.java
@@ -0,0 +1,64 @@
+package envoy.client.data.audio;
+
+import javax.sound.sampled.*;
+
+import envoy.exception.EnvoyException;
+
+/**
+ * Plays back audio from a byte array.
+ *
+ * Project: envoy-client
+ * File: AudioPlayer.java
+ * Created: 05.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public final class AudioPlayer {
+
+ private final AudioFormat format;
+ private final DataLine.Info info;
+
+ private Clip clip;
+
+ /**
+ * Initializes the player with the default audio format.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); }
+
+ /**
+ * Initializes the player with a given audio format.
+ *
+ * @param format the audio format to use
+ * @since Envoy Client v0.1-beta
+ */
+ public AudioPlayer(AudioFormat format) {
+ this.format = format;
+ info = new DataLine.Info(Clip.class, format);
+ }
+
+ /**
+ * @return {@code true} if audio play back is supported
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isSupported() { return AudioSystem.isLineSupported(info); }
+
+ /**
+ * Plays back an audio clip.
+ *
+ * @param data the data of the clip
+ * @throws EnvoyException if the play back failed
+ * @since Envoy Client v0.1-beta
+ */
+ public void play(byte[] data) throws EnvoyException {
+ try {
+ clip = (Clip) AudioSystem.getLine(info);
+ clip.open(format, data, 0, data.length);
+ clip.start();
+ } catch (final LineUnavailableException e) {
+ throw new EnvoyException("Cannot play back audio", e);
+ }
+ }
+}
diff --git a/src/main/java/envoy/client/data/audio/AudioRecorder.java b/src/main/java/envoy/client/data/audio/AudioRecorder.java
new file mode 100644
index 0000000..85dafae
--- /dev/null
+++ b/src/main/java/envoy/client/data/audio/AudioRecorder.java
@@ -0,0 +1,122 @@
+package envoy.client.data.audio;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.sound.sampled.*;
+
+import envoy.exception.EnvoyException;
+
+/**
+ * Records audio and exports it as a byte array.
+ *
+ * Project: envoy-client
+ * File: AudioRecorder.java
+ * Created: 02.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public final class AudioRecorder {
+
+ /**
+ * The default audio format used for recording and play back.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false);
+
+ private final AudioFormat format;
+ private final DataLine.Info info;
+
+ private TargetDataLine line;
+ private Path tempFile;
+
+ /**
+ * Initializes the recorder with the default audio format.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); }
+
+ /**
+ * Initializes the recorder with a given audio format.
+ *
+ * @param format the audio format to use
+ * @since Envoy Client v0.1-beta
+ */
+ public AudioRecorder(AudioFormat format) {
+ this.format = format;
+ info = new DataLine.Info(TargetDataLine.class, format);
+ }
+
+ /**
+ * @return {@code true} if audio recording is supported
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isSupported() { return AudioSystem.isLineSupported(info); }
+
+ /**
+ * @return {@code true} if the recorder is active
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isRecording() { return line != null && line.isActive(); }
+
+ /**
+ * Starts the audio recording.
+ *
+ * @throws EnvoyException if starting the recording failed
+ * @since Envoy Client v0.1-beta
+ */
+ public void start() throws EnvoyException {
+ try {
+
+ // Open the line
+ line = (TargetDataLine) AudioSystem.getLine(info);
+ line.open(format);
+ line.start();
+
+ // Prepare temp file
+ tempFile = Files.createTempFile("recording", "wav");
+
+ // Start the recording
+ final var ais = new AudioInputStream(line);
+ AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
+ } catch (IOException | LineUnavailableException e) {
+ throw new EnvoyException("Cannot record voice", e);
+ }
+ }
+
+ /**
+ * Stops the recording.
+ *
+ * @return the finished recording
+ * @throws EnvoyException if finishing the recording failed
+ * @since Envoy Client v0.1-beta
+ */
+ public byte[] finish() throws EnvoyException {
+ try {
+ line.stop();
+ line.close();
+ final byte[] data = Files.readAllBytes(tempFile);
+ Files.delete(tempFile);
+ return data;
+ } catch (final IOException e) {
+ throw new EnvoyException("Cannot save voice recording", e);
+ }
+ }
+
+ /**
+ * Cancels the active recording.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public void cancel() {
+ line.stop();
+ line.close();
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException e) {}
+ }
+}
diff --git a/src/main/java/envoy/client/data/audio/package-info.java b/src/main/java/envoy/client/data/audio/package-info.java
new file mode 100644
index 0000000..3a172be
--- /dev/null
+++ b/src/main/java/envoy/client/data/audio/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * Contains classes related to recording and playing back audio clips.
+ *
+ * Project: envoy-client
+ * File: package-info.java
+ * Created: 05.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+package envoy.client.data.audio;
diff --git a/src/main/java/envoy/client/data/package-info.java b/src/main/java/envoy/client/data/package-info.java
new file mode 100644
index 0000000..3455f5d
--- /dev/null
+++ b/src/main/java/envoy/client/data/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * This package contains all data classes and classes related to persistence.
+ *
+ * @author Kai S. K. Engelbart
+ * @author Leon Hofmeister
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-beta
+ */
+package envoy.client.data;
diff --git a/src/main/java/envoy/client/event/MessageCreationEvent.java b/src/main/java/envoy/client/event/MessageCreationEvent.java
new file mode 100644
index 0000000..3238423
--- /dev/null
+++ b/src/main/java/envoy/client/event/MessageCreationEvent.java
@@ -0,0 +1,22 @@
+package envoy.client.event;
+
+import envoy.data.Message;
+import envoy.event.Event;
+
+/**
+ * Project: envoy-client
+ * File: MessageCreationEvent.java
+ * Created: 4 Dec 2019
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.2-alpha
+ */
+public class MessageCreationEvent extends Event {
+
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * @param message the {@link Message} that has been created
+ */
+ public MessageCreationEvent(Message message) { super(message); }
+}
diff --git a/src/main/java/envoy/client/event/MessageModificationEvent.java b/src/main/java/envoy/client/event/MessageModificationEvent.java
new file mode 100644
index 0000000..b2f114a
--- /dev/null
+++ b/src/main/java/envoy/client/event/MessageModificationEvent.java
@@ -0,0 +1,22 @@
+package envoy.client.event;
+
+import envoy.data.Message;
+import envoy.event.Event;
+
+/**
+ * Project: envoy-client
+ * File: MessageModificationEvent.java
+ * Created: 4 Dec 2019
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.2-alpha
+ */
+public class MessageModificationEvent extends Event {
+
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * @param message the {@link Message} that has been modified
+ */
+ public MessageModificationEvent(Message message) { super(message); }
+}
diff --git a/src/main/java/envoy/client/event/SendEvent.java b/src/main/java/envoy/client/event/SendEvent.java
new file mode 100644
index 0000000..8ea650b
--- /dev/null
+++ b/src/main/java/envoy/client/event/SendEvent.java
@@ -0,0 +1,22 @@
+package envoy.client.event;
+
+import envoy.event.Event;
+
+/**
+ * Project: envoy-client
+ * File: SendEvent.java
+ * Created: 11.02.2020
+ *
+ * @author: Maximilian Käfer
+ * @since Envoy Client v0.3-alpha
+ */
+public class SendEvent extends Event> {
+
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * @param value the event to send to the server
+ */
+ public SendEvent(Event> value) { super(value); }
+
+}
diff --git a/src/main/java/envoy/client/event/ThemeChangeEvent.java b/src/main/java/envoy/client/event/ThemeChangeEvent.java
new file mode 100644
index 0000000..572476b
--- /dev/null
+++ b/src/main/java/envoy/client/event/ThemeChangeEvent.java
@@ -0,0 +1,25 @@
+package envoy.client.event;
+
+import envoy.event.Event;
+
+/**
+ * Project: envoy-client
+ * File: ThemeChangeEvent.java
+ * Created: 15 Dec 2019
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.2-alpha
+ */
+public class ThemeChangeEvent extends Event {
+
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Initializes a {@link ThemeChangeEvent} conveying information about the change
+ * of the theme currently in use.
+ *
+ * @param theme the name of the new theme
+ * @since Envoy Client v0.2-alpha
+ */
+ public ThemeChangeEvent(String theme) { super(theme); }
+}
diff --git a/src/main/java/envoy/client/event/package-info.java b/src/main/java/envoy/client/event/package-info.java
new file mode 100644
index 0000000..6886d5c
--- /dev/null
+++ b/src/main/java/envoy/client/event/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * This package contains all client-sided events.
+ *
+ * @author Kai S. K. Engelbart
+ * @author Leon Hofmeister
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-beta
+ */
+package envoy.client.event;
diff --git a/src/main/java/envoy/client/net/Client.java b/src/main/java/envoy/client/net/Client.java
new file mode 100644
index 0000000..afa423a
--- /dev/null
+++ b/src/main/java/envoy/client/net/Client.java
@@ -0,0 +1,241 @@
+package envoy.client.net;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import envoy.client.data.*;
+import envoy.client.event.SendEvent;
+import envoy.data.*;
+import envoy.event.*;
+import envoy.event.contact.ContactOperation;
+import envoy.event.contact.ContactSearchResult;
+import envoy.util.EnvoyLog;
+import envoy.util.SerializationUtils;
+
+/**
+ * Establishes a connection to the server, performs a handshake and delivers
+ * certain objects to the server.
+ *
+ * Project: envoy-client
+ * File: Client.java
+ * Created: 28 Sep 2019
+ *
+ * @author Kai S. K. Engelbart
+ * @author Maximilian Käfer
+ * @author Leon Hofmeister
+ * @since Envoy Client v0.1-alpha
+ */
+public class Client implements Closeable {
+
+ // Connection handling
+ private Socket socket;
+ private Receiver receiver;
+ private boolean online;
+
+ // Asynchronously initialized during handshake
+ private volatile User sender;
+ private volatile boolean rejected;
+
+ // Configuration, logging and event management
+ private static final ClientConfig config = ClientConfig.getInstance();
+ private static final Logger logger = EnvoyLog.getLogger(Client.class);
+ private static final EventBus eventBus = EventBus.getInstance();
+
+ /**
+ * Enters the online mode by acquiring a user ID from the server. As a
+ * connection has to be established and a handshake has to be made, this method
+ * will block for up to 5 seconds. If the handshake does exceed this time limit,
+ * an exception is thrown.
+ *
+ * @param credentials the login credentials of the user
+ * @param cacheMap the map of all caches needed
+ * @throws TimeoutException if the server could not be reached
+ * @throws IOException if the login credentials could not be written
+ * @throws InterruptedException if the current thread is interrupted while
+ * waiting for the handshake response
+ */
+ public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
+ if (online) throw new IllegalStateException("Handshake has already been performed successfully");
+
+ // Establish TCP connection
+ logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
+ socket = new Socket(config.getServer(), config.getPort());
+ logger.log(Level.FINE, "Successfully established TCP connection to server");
+
+ // Create object receiver
+ receiver = new Receiver(socket.getInputStream());
+
+ // Register user creation processor, contact list processor and message cache
+ receiver.registerProcessor(User.class, sender -> this.sender = sender);
+ receiver.registerProcessors(cacheMap.getMap());
+ receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
+
+ rejected = false;
+
+ // Start receiver
+ receiver.start();
+
+ // Write login credentials
+ SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream());
+
+ // Wait for a maximum of five seconds to acquire the sender object
+ final long start = System.currentTimeMillis();
+ while (sender == null) {
+
+ // Quit immediately after handshake rejection
+ // This method can then be called again
+ if (rejected) {
+ socket.close();
+ receiver.removeAllProcessors();
+ return;
+ }
+
+ if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
+ Thread.sleep(500);
+ }
+
+ online = true;
+
+ logger.log(Level.INFO, "Handshake completed.");
+ }
+
+ /**
+ * Initializes the {@link Receiver} used to process data sent from the server to
+ * this client.
+ *
+ * @param localDB the local database used to persist the current
+ * {@link IDGenerator}
+ * @param cacheMap the map of all caches needed
+ * @throws IOException if no {@link IDGenerator} is present and none could be
+ * requested from the server
+ * @since Envoy Client v0.2-alpha
+ */
+ public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
+ checkOnline();
+
+ // Remove all processors as they are only used during the handshake
+ receiver.removeAllProcessors();
+
+ // Process incoming messages
+ final var receivedMessageProcessor = new ReceivedMessageProcessor();
+ final var receivedGroupMessageProcessor = new ReceivedGroupMessageProcessor();
+ final var messageStatusChangeProcessor = new MessageStatusChangeProcessor();
+ final var groupMessageStatusChangeProcessor = new GroupMessageStatusChangeProcessor();
+
+ receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor);
+ receiver.registerProcessor(Message.class, receivedMessageProcessor);
+ receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor);
+ receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor);
+
+ // Relay cached messages and message status changes
+ cacheMap.get(Message.class).setProcessor(receivedMessageProcessor);
+ cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor);
+ cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor);
+ cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor);
+
+ // Process user status changes
+ receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch);
+
+ // Process message ID generation
+ receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
+
+ // Process name changes
+ receiver.registerProcessor(NameChange.class, evt -> { localDB.replaceContactName(evt); eventBus.dispatch(evt); });
+
+ // Process contact searches
+ receiver.registerProcessor(ContactSearchResult.class, eventBus::dispatch);
+
+ // Process contact operations
+ receiver.registerProcessor(ContactOperation.class, eventBus::dispatch);
+
+ // Process group size changes
+ receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
+
+ // Send event
+ eventBus.register(SendEvent.class, evt -> {
+ try {
+ sendEvent(evt.get());
+ } catch (final IOException e) {
+ logger.log(Level.WARNING, "An error occurred when trying to send " + evt, e);
+ }
+ });
+
+ // Request a generator if none is present or the existing one is consumed
+ if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIdGenerator();
+
+ // Relay caches
+ cacheMap.getMap().values().forEach(Cache::relay);
+ }
+
+ /**
+ * Sends a message to the server. The message's status will be incremented once
+ * it was delivered successfully.
+ *
+ * @param message the message to send
+ * @throws IOException if the message does not reach the server
+ * @since Envoy Client v0.3-alpha
+ */
+ public void sendMessage(Message message) throws IOException {
+ writeObject(message);
+ message.nextStatus();
+ }
+
+ /**
+ * Sends an event to the server.
+ *
+ * @param evt the event to send
+ * @throws IOException if the event did not reach the server
+ */
+ public void sendEvent(Event> evt) throws IOException { writeObject(evt); }
+
+ /**
+ * Requests a new {@link IDGenerator} from the server.
+ *
+ * @throws IOException if the request does not reach the server
+ * @since Envoy Client v0.3-alpha
+ */
+ public void requestIdGenerator() throws IOException {
+ logger.log(Level.INFO, "Requesting new id generator...");
+ writeObject(new IDGeneratorRequest());
+ }
+
+ @Override
+ public void close() throws IOException { if (online) socket.close(); }
+
+ private void writeObject(Object obj) throws IOException {
+ checkOnline();
+ logger.log(Level.FINE, "Sending " + obj);
+ SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
+ }
+
+ private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); }
+
+ /**
+ * @return the {@link User} as which this client is logged in
+ * @since Envoy Client v0.1-alpha
+ */
+ public User getSender() { return sender; }
+
+ /**
+ * Sets the client user which is used to send messages.
+ *
+ * @param clientUser the client user to set
+ * @since Envoy Client v0.2-alpha
+ */
+ public void setSender(User clientUser) { sender = clientUser; }
+
+ /**
+ * @return the {@link Receiver} used by this {@link Client}
+ */
+ public Receiver getReceiver() { return receiver; }
+
+ /**
+ * @return {@code true} if a connection to the server could be established
+ * @since Envoy Client v0.2-alpha
+ */
+ public boolean isOnline() { return online; }
+}
diff --git a/src/main/java/envoy/client/net/GroupMessageStatusChangeProcessor.java b/src/main/java/envoy/client/net/GroupMessageStatusChangeProcessor.java
new file mode 100644
index 0000000..ce3dc73
--- /dev/null
+++ b/src/main/java/envoy/client/net/GroupMessageStatusChangeProcessor.java
@@ -0,0 +1,29 @@
+package envoy.client.net;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import envoy.data.Message.MessageStatus;
+import envoy.event.EventBus;
+import envoy.event.GroupMessageStatusChange;
+import envoy.util.EnvoyLog;
+
+/**
+ * Project: envoy-client
+ * File: GroupMessageStatusChangePocessor.java
+ * Created: 03.07.2020
+ *
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-beta
+ */
+public class GroupMessageStatusChangeProcessor implements Consumer {
+
+ private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class);
+
+ @Override
+ public void accept(GroupMessageStatusChange evt) {
+ if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt);
+ else EventBus.getInstance().dispatch(evt);
+ }
+
+}
diff --git a/src/main/java/envoy/client/net/MessageStatusChangeProcessor.java b/src/main/java/envoy/client/net/MessageStatusChangeProcessor.java
new file mode 100644
index 0000000..106cda4
--- /dev/null
+++ b/src/main/java/envoy/client/net/MessageStatusChangeProcessor.java
@@ -0,0 +1,35 @@
+package envoy.client.net;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import envoy.data.Message.MessageStatus;
+import envoy.event.EventBus;
+import envoy.event.MessageStatusChange;
+import envoy.util.EnvoyLog;
+
+/**
+ * Project: envoy-client
+ * File: MessageStatusChangeProcessor.java
+ * Created: 4 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public class MessageStatusChangeProcessor implements Consumer {
+
+ private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
+
+ /**
+ * Dispatches a {@link MessageStatusChange} if the status is
+ * {@code RECEIVED} or {@code READ}.
+ *
+ * @param evt the status change event
+ * @since Envoy Client v0.3-alpha
+ */
+ @Override
+ public void accept(MessageStatusChange evt) {
+ if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid message status change " + evt);
+ else EventBus.getInstance().dispatch(evt);
+ }
+}
diff --git a/src/main/java/envoy/client/net/ReceivedGroupMessageProcessor.java b/src/main/java/envoy/client/net/ReceivedGroupMessageProcessor.java
new file mode 100644
index 0000000..877d0f0
--- /dev/null
+++ b/src/main/java/envoy/client/net/ReceivedGroupMessageProcessor.java
@@ -0,0 +1,33 @@
+package envoy.client.net;
+
+import java.util.function.Consumer;
+import java.util.logging.Logger;
+
+import envoy.client.event.MessageCreationEvent;
+import envoy.data.GroupMessage;
+import envoy.data.Message.MessageStatus;
+import envoy.event.EventBus;
+import envoy.util.EnvoyLog;
+
+/**
+ * Project: envoy-client
+ * File: ReceivedGroupMessageProcessor.java
+ * Created: 13.06.2020
+ *
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-beta
+ */
+public class ReceivedGroupMessageProcessor implements Consumer {
+
+ private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class);
+
+ @Override
+ public void accept(GroupMessage groupMessage) {
+ if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ)
+ logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus());
+
+ // Dispatch event
+ EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage));
+ }
+
+}
diff --git a/src/main/java/envoy/client/net/ReceivedMessageProcessor.java b/src/main/java/envoy/client/net/ReceivedMessageProcessor.java
new file mode 100644
index 0000000..f83815c
--- /dev/null
+++ b/src/main/java/envoy/client/net/ReceivedMessageProcessor.java
@@ -0,0 +1,36 @@
+package envoy.client.net;
+
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import envoy.client.event.MessageCreationEvent;
+import envoy.data.Message;
+import envoy.data.Message.MessageStatus;
+import envoy.event.EventBus;
+import envoy.util.EnvoyLog;
+
+/**
+ * Project: envoy-client
+ * File: ReceivedMessageProcessor.java
+ * Created: 31.12.2019
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public class ReceivedMessageProcessor implements Consumer {
+
+ private static final Logger logger = EnvoyLog.getLogger(ReceivedMessageProcessor.class);
+
+ @Override
+ public void accept(Message message) {
+ if (message.getStatus() != MessageStatus.SENT) logger.log(Level.WARNING, "The message has the unexpected status " + message.getStatus());
+ else {
+ // Update status to RECEIVED
+ message.nextStatus();
+
+ // Dispatch event
+ EventBus.getInstance().dispatch(new MessageCreationEvent(message));
+ }
+ }
+}
diff --git a/src/main/java/envoy/client/net/Receiver.java b/src/main/java/envoy/client/net/Receiver.java
new file mode 100644
index 0000000..e325c4f
--- /dev/null
+++ b/src/main/java/envoy/client/net/Receiver.java
@@ -0,0 +1,118 @@
+package envoy.client.net;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.net.SocketException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import envoy.util.EnvoyLog;
+import envoy.util.SerializationUtils;
+
+/**
+ * Receives objects from the server and passes them to processor objects based
+ * on their class.
+ *
+ * Project: envoy-client
+ * File: Receiver.java
+ * Created: 30.12.2019
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public class Receiver extends Thread {
+
+ private final InputStream in;
+ private final Map, Consumer>> processors = new HashMap<>();
+
+ private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
+
+ /**
+ * Creates an instance of {@link Receiver}.
+ *
+ * @param in the {@link InputStream} to parse objects from
+ * @since Envoy Client v0.3-alpha
+ */
+ public Receiver(InputStream in) {
+ super("Receiver");
+ this.in = in;
+ }
+
+ /**
+ * Starts the receiver loop. When an object is read, it is passed to the
+ * appropriate processor.
+ *
+ * @since Envoy Client v0.3-alpha
+ */
+ @Override
+ public void run() {
+
+ while (true) {
+ try {
+ // Read object length
+ final byte[] lenBytes = new byte[4];
+ in.read(lenBytes);
+ final int len = SerializationUtils.bytesToInt(lenBytes, 0);
+ logger.log(Level.FINEST, "Expecting object of length " + len + ".");
+
+ // Read object into byte array
+ final byte[] objBytes = new byte[len];
+ final int bytesRead = in.read(objBytes);
+ logger.log(Level.FINEST, "Read " + bytesRead + " bytes.");
+
+ // Catch LV encoding errors
+ if (len != bytesRead) {
+ logger.log(Level.WARNING,
+ String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
+ continue;
+ }
+
+ try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
+ final Object obj = oin.readObject();
+ logger.log(Level.FINE, "Received " + obj);
+
+ // Get appropriate processor
+ @SuppressWarnings("rawtypes")
+ final Consumer processor = processors.get(obj.getClass());
+ if (processor == null)
+ logger.log(Level.WARNING, String.format("The received object has the %s for which no processor is defined.", obj.getClass()));
+ else processor.accept(obj);
+ }
+ } catch (final SocketException e) {
+ // Connection probably closed by client.
+ return;
+ } catch (final Exception e) {
+ logger.log(Level.SEVERE, "Error on receiver thread", e);
+ }
+ }
+ }
+
+ /**
+ * Adds an object processor to this {@link Receiver}. It will be called once an
+ * object of the accepted class has been received.
+ *
+ * @param processorClass the object class accepted by the processor
+ * @param processor the object processor
+ * @since Envoy Client v0.3-alpha
+ */
+ public void registerProcessor(Class processorClass, Consumer processor) { processors.put(processorClass, processor); }
+
+ /**
+ * Adds a map of object processors to this {@link Receiver}.
+ *
+ * @param processors the processors to add the processors to add
+ * @since Envoy Client v0.1-beta
+ */
+ public void registerProcessors(Map, ? extends Consumer>> processors) { this.processors.putAll(processors); }
+
+ /**
+ * Removes all object processors registered at this {@link Receiver}.
+ *
+ * @since Envoy Client v0.3-alpha
+ */
+ public void removeAllProcessors() { processors.clear(); }
+}
diff --git a/src/main/java/envoy/client/net/WriteProxy.java b/src/main/java/envoy/client/net/WriteProxy.java
new file mode 100644
index 0000000..0eccf6a
--- /dev/null
+++ b/src/main/java/envoy/client/net/WriteProxy.java
@@ -0,0 +1,100 @@
+package envoy.client.net;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import envoy.client.data.Cache;
+import envoy.client.data.LocalDB;
+import envoy.data.Message;
+import envoy.event.MessageStatusChange;
+import envoy.util.EnvoyLog;
+
+/**
+ * Implements methods to send {@link Message}s and
+ * {@link MessageStatusChange}s to the server or cache them inside a
+ * {@link LocalDB} depending on the online status.
+ *
+ * Project: envoy-client
+ * File: WriteProxy.java
+ * Created: 6 Feb 2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.3-alpha
+ */
+public class WriteProxy {
+
+ private final Client client;
+ private final LocalDB localDB;
+
+ private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
+
+ /**
+ * Initializes a write proxy using a client and a local database. The
+ * corresponding cache processors are injected into the caches.
+ *
+ * @param client the client used to send messages and message status change
+ * events
+ * @param localDB the local database used to cache messages and message status
+ * change events
+ * @since Envoy Client v0.3-alpha
+ */
+ public WriteProxy(Client client, LocalDB localDB) {
+ this.client = client;
+ this.localDB = localDB;
+
+ // Initialize cache processors for messages and message status change events
+ localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
+ try {
+ logger.log(Level.FINER, "Sending cached " + msg);
+ client.sendMessage(msg);
+ } catch (final IOException e) {
+ logger.log(Level.SEVERE, "Could not send cached message: ", e);
+ }
+ });
+ localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
+ logger.log(Level.FINER, "Sending cached " + evt);
+ try {
+ client.sendEvent(evt);
+ } catch (final IOException e) {
+ logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
+ }
+ });
+ }
+
+ /**
+ * Sends cached {@link Message}s and {@link MessageStatusChange}s to the
+ * server.
+ *
+ * @since Envoy Client v0.3-alpha
+ */
+ public void flushCache() {
+ localDB.getCacheMap().getMap().values().forEach(Cache::relay);
+ }
+
+ /**
+ * Delivers a message to the server if online. Otherwise the message is cached
+ * inside the local database.
+ *
+ * @param message the message to send
+ * @throws IOException if the message could not be sent
+ * @since Envoy Client v0.3-alpha
+ */
+ public void writeMessage(Message message) throws IOException {
+ if (client.isOnline()) client.sendMessage(message);
+ else localDB.getCacheMap().getApplicable(Message.class).accept(message);
+ }
+
+ /**
+ * Delivers a message status change event to the server if online. Otherwise the
+ * event is cached inside the local database.
+ *
+ * @param evt the event to send
+ * @throws IOException if the event could not be sent
+ * @since Envoy Client v0.3-alpha
+ */
+ public void writeMessageStatusChange(MessageStatusChange evt) throws IOException {
+ if (client.isOnline()) client.sendEvent(evt);
+ else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
+ }
+}
diff --git a/src/main/java/envoy/client/net/package-info.java b/src/main/java/envoy/client/net/package-info.java
new file mode 100644
index 0000000..9c2a79e
--- /dev/null
+++ b/src/main/java/envoy/client/net/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * This package contains all classes related to client-server communication.
+ *
+ * @author Kai S. K. Engelbart
+ * @author Leon Hofmeister
+ * @author Maximilian Käfer
+ * @since Envoy Client v0.1-beta
+ */
+package envoy.client.net;
diff --git a/src/main/java/envoy/client/ui/AudioControl.java b/src/main/java/envoy/client/ui/AudioControl.java
new file mode 100644
index 0000000..d82fd24
--- /dev/null
+++ b/src/main/java/envoy/client/ui/AudioControl.java
@@ -0,0 +1,49 @@
+package envoy.client.ui;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Button;
+import javafx.scene.layout.HBox;
+
+import envoy.client.data.audio.AudioPlayer;
+import envoy.exception.EnvoyException;
+import envoy.util.EnvoyLog;
+
+/**
+ * Enables the play back of audio clips through a button.
+ *
+ * Project: envoy-client
+ * File: AudioControl.java
+ * Created: 05.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public final class AudioControl extends HBox {
+
+ private AudioPlayer player = new AudioPlayer();
+
+ private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
+
+ /**
+ * Initializes the audio control.
+ *
+ * @param audioData the audio data to play.
+ * @since Envoy Client v0.1-beta
+ */
+ public AudioControl(byte[] audioData) {
+ var button = new Button("Play");
+ button.setOnAction(e -> {
+ try {
+ player.play(audioData);
+ } catch (EnvoyException ex) {
+ logger.log(Level.SEVERE, "Could not play back audio: ", ex);
+ new Alert(AlertType.ERROR, "Could not play back audio").showAndWait();
+ }
+ });
+ getChildren().add(button);
+ }
+}
diff --git a/src/main/java/envoy/client/ui/ClearableTextField.java b/src/main/java/envoy/client/ui/ClearableTextField.java
new file mode 100644
index 0000000..fbe6926
--- /dev/null
+++ b/src/main/java/envoy/client/ui/ClearableTextField.java
@@ -0,0 +1,169 @@
+package envoy.client.ui;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.StringProperty;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.*;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+
+/**
+ * This class offers a text field that is automatically equipped with a clear
+ * button.
+ *
+ * Project: envoy-client
+ * File: ClearableTextField.java
+ * Created: 25.06.2020
+ *
+ * @author Leon Hofmeister
+ * @since Envoy Client v0.1-beta
+ */
+public class ClearableTextField extends GridPane {
+
+ private final TextField textField;
+
+ private final Button clearButton;
+
+ /**
+ * Constructs a new {@code ClearableTextField} with no initial text and icon
+ * size 16.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public ClearableTextField() { this("", 16); }
+
+ /**
+ * Constructs a new {@code ClearableTextField} with initial text and a
+ * predetermined icon size.
+ *
+ * @param text the text that should be displayed by default
+ * @param size the size of the icon
+ * @since Envoy Client v0.1-beta
+ */
+ public ClearableTextField(String text, int size) {
+ // initializing the textField and the button
+ textField = new TextField(text);
+ clearButton = new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
+ clearButton.setOnAction(e -> textField.clear());
+ clearButton.setFocusTraversable(false);
+ clearButton.getStyleClass().clear();
+ clearButton.setBackground(Background.EMPTY);
+ // Adding the two elements to the GridPane
+ add(textField, 0, 0, 2, 1);
+ add(clearButton, 1, 0, 1, 1);
+ // Setting the percent - widths of the two columns.
+ // Used to locate the button on the right.
+ final var columnConstraints = new ColumnConstraints();
+ columnConstraints.setPercentWidth(90);
+ getColumnConstraints().add(columnConstraints);
+ final var columnConstraints2 = new ColumnConstraints();
+ columnConstraints2.setPercentWidth(10);
+ getColumnConstraints().add(columnConstraints2);
+ }
+
+ /**
+ * @return the underlying {@code textField}
+ * @since Envoy Client v0.1-beta
+ */
+ public TextField getTextField() { return textField; }
+
+ /**
+ * This method offers the freedom to perform custom actions when the
+ * {@code clearButton} has been pressed.
+ *
+ * The default is
+ * e -> {clearableTextField.getTextField().clear();}
+ *
+ * @param onClearButtonAction the action that should be performed
+ * @since Envoy Client v0.1-beta
+ */
+ public void setClearButtonListener(EventHandler onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); }
+
+ /**
+ * @return the current property of the prompt text
+ * @see javafx.scene.control.TextInputControl#promptTextProperty()
+ * @since Envoy Client v0.1-beta
+ */
+ public final StringProperty promptTextProperty() { return textField.promptTextProperty(); }
+
+ /**
+ * @return the current prompt text
+ * @see javafx.scene.control.TextInputControl#getPromptText()
+ * @since Envoy Client v0.1-beta
+ */
+ public final String getPromptText() { return textField.getPromptText(); }
+
+ /**
+ * @param value the prompt text to display
+ * @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String)
+ * @since Envoy Client v0.1-beta
+ */
+ public final void setPromptText(String value) { textField.setPromptText(value); }
+
+ /**
+ * @return the current property of the tooltip
+ * @see javafx.scene.control.Control#tooltipProperty()
+ * @since Envoy Client v0.1-beta
+ */
+ public final ObjectProperty tooltipProperty() { return textField.tooltipProperty(); }
+
+ /**
+ * @param value the new tooltip
+ * @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip)
+ * @since Envoy Client v0.1-beta
+ */
+ public final void setTooltip(Tooltip value) { textField.setTooltip(value); }
+
+ /**
+ * @return the current tooltip
+ * @see javafx.scene.control.Control#getTooltip()
+ * @since Envoy Client v0.1-beta
+ */
+ public final Tooltip getTooltip() { return textField.getTooltip(); }
+
+ /**
+ * @return the current property of the context menu
+ * @see javafx.scene.control.Control#contextMenuProperty()
+ * @since Envoy Client v0.1-beta
+ */
+ public final ObjectProperty contextMenuProperty() { return textField.contextMenuProperty(); }
+
+ /**
+ * @param value the new context menu
+ * @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu)
+ * @since Envoy Client v0.1-beta
+ */
+ public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); }
+
+ /**
+ * @return the current context menu
+ * @see javafx.scene.control.Control#getContextMenu()
+ * @since Envoy Client v0.1-beta
+ */
+ public final ContextMenu getContextMenu() { return textField.getContextMenu(); }
+
+ /**
+ * @param value whether this ClearableTextField should be editable
+ * @see javafx.scene.control.TextInputControl#setEditable(boolean)
+ * @since Envoy Client v0.1-beta
+ */
+ public final void setEditable(boolean value) { textField.setEditable(value); }
+
+ /**
+ * @return the current property whether this ClearableTextField is editable
+ * @see javafx.scene.control.TextInputControl#editableProperty()
+ * @since Envoy Client v0.1-beta
+ */
+ public final BooleanProperty editableProperty() { return textField.editableProperty(); }
+
+ /**
+ * @return whether this {@code ClearableTextField} is editable
+ * @see javafx.scene.control.TextInputControl#isEditable()
+ * @since Envoy Client v0.1-beta
+ */
+ public final boolean isEditable() { return textField.isEditable(); }
+}
diff --git a/src/main/java/envoy/client/ui/IconUtil.java b/src/main/java/envoy/client/ui/IconUtil.java
new file mode 100644
index 0000000..5324619
--- /dev/null
+++ b/src/main/java/envoy/client/ui/IconUtil.java
@@ -0,0 +1,160 @@
+package envoy.client.ui;
+
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.logging.Level;
+
+import javafx.scene.image.Image;
+
+import envoy.client.data.Settings;
+import envoy.util.EnvoyLog;
+
+/**
+ * Provides static utility methods for loading icons from the resource
+ * folder.
+ *
+ * Project: envoy-client
+ * File: IconUtil.java
+ * Created: 16.03.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public class IconUtil {
+
+ private IconUtil() {}
+
+ /**
+ * Loads an image from the resource folder.
+ *
+ * @param path the path to the icon inside the resource folder
+ * @return the loaded image
+ * @since Envoy Client v0.1-beta
+ */
+ public static Image load(String path) {
+ Image image = null;
+ try {
+ image = new Image(IconUtil.class.getResource(path).toExternalForm());
+ } catch (final NullPointerException e) {
+ EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
+ }
+ return image;
+ }
+
+ /**
+ * Loads an image from the resource folder and scales it to the given size.
+ *
+ * @param path the path to the icon inside the resource folder
+ * @param size the size to scale the icon to
+ * @return the scaled image
+ * @since Envoy Client v0.1-beta
+ */
+ public static Image load(String path, int size) {
+ Image image = null;
+ try {
+ image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
+ } catch (final NullPointerException e) {
+ EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
+ }
+ return image;
+ }
+
+ /**
+ * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
+ * resource folder.
+ * The suffix {@code .png} is automatically appended.
+ *
+ * @param name the image name without the .png suffix
+ * @return the loaded image
+ * @since Envoy Client v0.1-beta
+ * @apiNote let's load a sample image {@code /icons/abc.png}.
+ * To do that, we only have to call {@code IconUtil.loadIcon("abc")}
+ */
+ public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); }
+
+ /**
+ * Loads a {@code .png} image from the sub-folder {@code /icons/} of the
+ * resource folder and scales it to the given size.
+ * The suffix {@code .png} is automatically appended.
+ *
+ * @param name the image name without the .png suffix
+ * @param size the size of the image to scale to
+ * @return the loaded image
+ * @since Envoy Client v0.1-beta
+ * @apiNote let's load a sample image {@code /icons/abc.png} in size 16.
+ * To do that, we only have to call
+ * {@code IconUtil.loadIcon("abc", 16)}
+ */
+ public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); }
+
+ /**
+ * Loads a {@code .png} image whose design depends on the currently active theme
+ * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
+ * resource folder.
+ *
+ * The suffix {@code .png} is automatically appended.
+ *
+ * @param name the image name without the "black" or "white" suffix and without
+ * the .png suffix
+ * @return the loaded image
+ * @since Envoy Client v0.1-beta
+ * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
+ * {@code /icons/light/abc.png}, and load one of them.
+ * To do that theme sensitive, we only have to call
+ * {@code IconUtil.loadIconThemeSensitive("abc")}
+ */
+ public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); }
+
+ /**
+ * Loads a {@code .png} image whose design depends on the currently active theme
+ * from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
+ * resource folder and scales it to the given size.
+ *
+ * The suffix {@code .png} is automatically appended.
+ *
+ * @param name the image name without the .png suffix
+ * @param size the size of the image to scale to
+ * @return the loaded image
+ * @since Envoy Client v0.1-beta
+ * @apiNote let's take two sample images {@code /icons/dark/abc.png} and
+ * {@code /icons/light/abc.png}, and load one of them in size 16.
+ * To do that theme sensitive, we only have to call
+ * {@code IconUtil.loadIconThemeSensitive("abc", 16)}
+ */
+ public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); }
+
+ /**
+ *
+ * Loads images specified by an enum. The images have to be named like the
+ * lowercase enum constants with {@code .png} extension and be located inside a
+ * folder with the lowercase name of the enum, which must be contained inside
+ * the {@code /icons/} folder.
+ *
+ * @param the enum that specifies the images to load
+ * @param enumClass the class of the enum
+ * @param size the size to scale the images to
+ * @return a map containing the loaded images with the corresponding enum
+ * constants as keys
+ * @since Envoy Client v0.1-beta
+ */
+ public static > EnumMap loadByEnum(Class enumClass, int size) {
+ final var icons = new EnumMap(enumClass);
+ final var path = "/icons/" + enumClass.getSimpleName().toLowerCase() + "/";
+ for (final var e : EnumSet.allOf(enumClass))
+ icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
+ return icons;
+ }
+
+ /**
+ * This method should be called if the display of an image depends upon the
+ * currently active theme.
+ * In case of a default theme, the string returned will be
+ * ({@code dark/} or {@code light/}), otherwise it will be empty.
+ *
+ * @return the theme specific folder
+ * @since Envoy Client v0.1-beta
+ */
+ public static String themeSpecificSubFolder() {
+ return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
+ }
+}
diff --git a/src/main/java/envoy/client/ui/Restorable.java b/src/main/java/envoy/client/ui/Restorable.java
new file mode 100644
index 0000000..e9e40eb
--- /dev/null
+++ b/src/main/java/envoy/client/ui/Restorable.java
@@ -0,0 +1,25 @@
+package envoy.client.ui;
+
+/**
+ * This interface defines an action that should be performed when a scene gets
+ * restored from the scene stack in {@link SceneContext}.
+ *
+ * Project: envoy-client
+ * File: Restorable.java
+ * Created: 03.07.2020
+ *
+ * @author Leon Hofmeister
+ * @since Envoy Client v0.1-beta
+ */
+@FunctionalInterface
+public interface Restorable {
+
+ /**
+ * This method is getting called when a scene gets restored.
+ * Hence, it can contain anything that should be done when the underlying scene
+ * gets restored.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ void onRestore();
+}
diff --git a/src/main/java/envoy/client/ui/SceneContext.java b/src/main/java/envoy/client/ui/SceneContext.java
new file mode 100644
index 0000000..a027395
--- /dev/null
+++ b/src/main/java/envoy/client/ui/SceneContext.java
@@ -0,0 +1,179 @@
+package envoy.client.ui;
+
+import java.io.IOException;
+import java.util.Stack;
+import java.util.logging.Level;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+import envoy.client.data.Settings;
+import envoy.client.event.ThemeChangeEvent;
+import envoy.event.EventBus;
+import envoy.util.EnvoyLog;
+
+/**
+ * Manages a stack of scenes. The most recently added scene is displayed inside
+ * a stage. When a scene is removed from the stack, its predecessor is
+ * displayed.
+ *
+ * When a scene is loaded, the style sheet for the current theme is applied to
+ * it.
+ *
+ * Project: envoy-client
+ * File: SceneContext.java
+ * Created: 06.06.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+public final class SceneContext {
+
+ /**
+ * Contains information about different scenes and their FXML resource files.
+ *
+ * @author Kai S. K. Engelbart
+ * @since Envoy Client v0.1-beta
+ */
+ public enum SceneInfo {
+
+ /**
+ * The main scene in which the chat screen is displayed.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ CHAT_SCENE("/fxml/ChatScene.fxml"),
+
+ /**
+ * The scene in which the settings screen is displayed.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
+
+ /**
+ * The scene in which the contact search screen is displayed.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
+
+ /**
+ * The scene in which the group creation screen is displayed.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ GROUP_CREATION_SCENE("/fxml/GroupCreationScene.fxml"),
+
+ /**
+ * The scene in which the login screen is displayed.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ LOGIN_SCENE("/fxml/LoginScene.fxml"),
+
+ /**
+ * The scene in which the info screen is displayed.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml");
+
+ /**
+ * The path to the FXML resource.
+ */
+ public final String path;
+
+ SceneInfo(String path) { this.path = path; }
+ }
+
+ private final Stage stage;
+ private final FXMLLoader loader = new FXMLLoader();
+ private final Stack sceneStack = new Stack<>();
+ private final Stack