1

Merge pull request #20 from ChUrl/dev

Update Master
This commit is contained in:
Christoph
2020-04-22 15:17:01 +02:00
committed by GitHub
183 changed files with 6782 additions and 4756 deletions

1
.gitignore vendored
View File

@ -34,3 +34,4 @@ out/
.flooignore
/mysql/db/storage/
/mysql/keycloak/

View File

@ -8,6 +8,43 @@ Private Gruppen kann man nur über einen Beitrittslink beitreten.
Öffentliche Gruppen kann man ohne diesen beitreten.
Man kann nach Öffentlichen Gruppen über eine Suchfunktion suchen.
== Stand
=== Probleme
* Momentan ist kein Controller getestet
* Das Styling ist inkonsistent und skaliert nicht gut auf kleineren screens
* Schlecht dokumentiert und Arc42 nicht aktuell
* Integrationen nicht implementiert
* Gruppenoptionen nicht implementiert
* Snapshotting nicht implementiert
* Caching funktioniert, aber teilweise nicht konsequent verwendet
* Gruppenbeschreibung mit markup nicht implementiert
* Invitelink kann nicht regeneriert werden
* Seit Implementierung eines Caches ist die API kaputt
=== Fertig
* Fast die komplette Logik + Templates + Controller überholt
* Verwendung eines Caches anstatt vieler Datenbankanfragen
* UI: Templates mit Fragmenten und auslagertem styling vereinfacht, einige Seiten zusammengefasst
* Services komplett umstrukturiert, ergeben inhaltlich mehr Sinn und sind kleiner
* Konsequenteres Logging, aspektorientiertes Logging
* Aktivere Gruppenobjekte mit mehr Validierung
* Extratabelle für Invitelinks entfernt, Datenbank vereinfacht
* Man kann Änderungen an Gruppen nachvollziehen mit Zeitstempeln und einer Übersichtsseite
* Gruppen und Teilnehmerlisten können exportiert werden
=== Heroku
Die letzte Version der Anwendung ist unter gruppenfindung.herokuapp.com zu erreichen.
Es existieren zwei Defaultuser:
* Username: orga, Passwort: orga
* Username: studentin, Passwort: studentin
== Ursprüngliche Readme
=== Problem
Die meisten Teilsysteme von MOPS arbeiten mit Gruppierungen von Studenten: Materialien für Lerngruppen/Veranstaltungen, Gruppenportfolios, Gruppenabstimmungen etc.

View File

@ -1,11 +1,9 @@
import com.github.spotbugs.SpotBugsTask
plugins {
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
id 'com.github.spotbugs' version '3.0.0'
id 'com.github.spotbugs' version '4.0.1'
id 'checkstyle'
id 'pmd'
}
@ -14,34 +12,43 @@ group = 'mops'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
maven {
url = 'https://s3.cs.hhu.de/public/mops/'
metadataSources {
artifact()
}
}
mavenCentral()
}
spotbugs {
toolVersion = '4.0.1'
ignoreFailures = false
reportLevel = "high"
effort = "max"
toolVersion = '4.0.0-RC1'
showProgress = true
}
tasks.withType(SpotBugsTask) {
spotbugsMain {
reports {
xml.enabled = false
html.enabled = true
html {
enabled = true
}
}
}
pmd {
consoleOutput = true
ignoreFailures = true
toolVersion = "6.21.0"
toolVersion = "6.22.0"
rulePriority = 5
ruleSets = ["category/java/errorprone.xml",
"category/java/bestpractices.xml",
"category/java/security.xml",
"category/java/performance.xml",
"category/java/design.xml"]
ruleSetFiles = files("config/pmd/ruleset.xml")
ruleSets = []
}
checkstyle {
toolVersion = "8.28"
toolVersion = "8.30"
configFile = file("${rootDir}/config/checkstyle/checkstyle.xml")
ignoreFailures = true
}
@ -56,25 +63,18 @@ configurations {
}
}
repositories {
maven {
url = 'https://s3.cs.hhu.de/public/mops/'
metadataSources {
artifact()
}
}
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.security.oauth:spring-security-oauth2:2.4.0.RELEASE'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.keycloak:keycloak-spring-boot-starter:9.0.0'
implementation 'org.keycloak.bom:keycloak-adapter-bom:9.0.0'
implementation 'mops:styleguide:2.1.0'
@ -82,10 +82,11 @@ dependencies {
implementation 'io.springfox:springfox-swagger-ui:2.9.2'
implementation 'com.github.javafaker:javafaker:1.0.2'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.10.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.3'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'

View File

@ -223,7 +223,7 @@
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="8"/>
<!--<property name="lineWrappingIndentation" value="8"/>-->
<property name="arrayInitIndent" value="4"/>
</module>
<!-- <module name="VariableDeclarationUsageDistance"/>-->

335
config/pmd/ruleset.xml Normal file
View File

@ -0,0 +1,335 @@
<ruleset name="quickstart"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>PMD Rules</description>
<!-- BEST PRACTICES -->
<!-- <rule ref="category/java/bestpractices.xml/AbstractClassWithoutAbstractMethod" /> -->
<!-- <rule ref="category/java/bestpractices.xml/AccessorClassGeneration" /> -->
<!-- <rule ref="category/java/bestpractices.xml/AccessorMethodGeneration" /> -->
<!-- <rule ref="category/java/bestpractices.xml/ArrayIsStoredDirectly" /> -->
<!-- <rule ref="category/java/bestpractices.xml/AvoidPrintStackTrace" /> -->
<!-- <rule ref="category/java/bestpractices.xml/AvoidReassigningParameters" /> -->
<rule ref="category/java/bestpractices.xml/AvoidStringBufferField"/>
<rule ref="category/java/bestpractices.xml/AvoidUsingHardCodedIP"/>
<rule ref="category/java/bestpractices.xml/CheckResultSet"/>
<rule ref="category/java/bestpractices.xml/ConstantsInInterface"/>
<rule ref="category/java/bestpractices.xml/DefaultLabelNotLastInSwitchStmt"/>
<rule ref="category/java/bestpractices.xml/ForLoopCanBeForeach"/>
<!-- <rule ref="category/java/bestpractices.xml/GuardLogStatement"/>-->
<!-- <rule ref="category/java/bestpractices.xml/JUnit4SuitesShouldUseSuiteAnnotation" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnit4TestShouldUseAfterAnnotation" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnit4TestShouldUseBeforeAnnotation" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnit4TestShouldUseTestAnnotation" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnitAssertionsShouldIncludeMessage" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnitTestContainsTooManyAsserts" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnitTestsShouldIncludeAssert" /> -->
<!-- <rule ref="category/java/bestpractices.xml/JUnitUseExpected" /> -->
<rule ref="category/java/bestpractices.xml/LooseCoupling"/>
<!-- <rule ref="category/java/bestpractices.xml/MethodReturnsInternalArray" /> -->
<rule ref="category/java/bestpractices.xml/MissingOverride"/>
<rule ref="category/java/bestpractices.xml/OneDeclarationPerLine"/>
<rule ref="category/java/bestpractices.xml/PositionLiteralsFirstInCaseInsensitiveComparisons"/>
<rule ref="category/java/bestpractices.xml/PositionLiteralsFirstInComparisons"/>
<rule ref="category/java/bestpractices.xml/PreserveStackTrace"/>
<!-- <rule ref="category/java/bestpractices.xml/ReplaceEnumerationWithIterator" /> -->
<!-- <rule ref="category/java/bestpractices.xml/ReplaceHashtableWithMap" /> -->
<!-- <rule ref="category/java/bestpractices.xml/ReplaceVectorWithList" /> -->
<rule ref="category/java/bestpractices.xml/SwitchStmtsShouldHaveDefault"/>
<!-- <rule ref="category/java/bestpractices.xml/SystemPrintln" /> -->
<rule ref="category/java/bestpractices.xml/UnusedFormalParameter"/>
<rule ref="category/java/bestpractices.xml/UnusedImports"/>
<rule ref="category/java/errorprone.xml/ImportFromSamePackage"/>
<rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
<rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
<rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>
<rule ref="category/java/bestpractices.xml/UseAssertEqualsInsteadOfAssertTrue"/>
<rule ref="category/java/bestpractices.xml/UseAssertNullInsteadOfAssertTrue"/>
<rule ref="category/java/bestpractices.xml/UseAssertSameInsteadOfAssertTrue"/>
<rule ref="category/java/bestpractices.xml/UseAssertTrueInsteadOfAssertEquals"/>
<rule ref="category/java/bestpractices.xml/UseCollectionIsEmpty"/>
<!-- <rule ref="category/java/bestpractices.xml/UseVarargs" /> -->
<!-- <rule ref="category/java/codestyle.xml/AtLeastOneConstructor" /> -->
<rule ref="category/java/codestyle.xml/AvoidDollarSigns"/>
<!-- <rule ref="category/java/codestyle.xml/AvoidFinalLocalVariable" /> -->
<rule ref="category/java/codestyle.xml/AvoidProtectedFieldInFinalClass"/>
<rule ref="category/java/codestyle.xml/AvoidProtectedMethodInFinalClassNotExtending"/>
<!-- NAMING CONVENTIONS -->
<rule ref="category/java/codestyle.xml/FormalParameterNamingConventions"/>
<rule ref="category/java/codestyle.xml/ClassNamingConventions"/>
<rule ref="category/java/codestyle.xml/LocalVariableNamingConventions"/>
<rule ref="category/java/codestyle.xml/MethodNamingConventions"/>
<rule ref="category/java/codestyle.xml/PackageCase"/>
<!-- UNIMPLEMENTED -->
<!-- <rule ref="category/java/codestyle.xml/FieldNamingConventions" /> -->
<rule ref="category/java/codestyle.xml/GenericsNaming"/>
<!-- <rule ref="category/java/codestyle.xml/LongVariable" /> -->
<rule ref="category/java/codestyle.xml/ShortClassName"/>
<rule ref="category/java/codestyle.xml/ShortMethodName"/>
<rule ref="category/java/codestyle.xml/ShortVariable"/>
<!-- OTHER -->
<!-- <rule ref="category/java/codestyle.xml/AvoidUsingNativeCode" /> -->
<!-- <rule ref="category/java/codestyle.xml/BooleanGetMethodName" /> -->
<!-- <rule ref="category/java/codestyle.xml/CallSuperInConstructor" /> -->
<!-- <rule ref="category/java/codestyle.xml/CommentDefaultAccessModifier" /> -->
<!-- <rule ref="category/java/codestyle.xml/ConfusingTernary" /> -->
<rule ref="category/java/codestyle.xml/ControlStatementBraces"/>
<!-- <rule ref="category/java/codestyle.xml/DefaultPackage" /> -->
<rule ref="category/java/codestyle.xml/DontImportJavaLang"/>
<rule ref="category/java/codestyle.xml/DuplicateImports"/>
<!-- <rule ref="category/java/codestyle.xml/EmptyMethodInAbstractClassShouldBeAbstract" /> -->
<rule ref="category/java/codestyle.xml/ExtendsObject"/>
<!-- <rule ref="category/java/codestyle.xml/FieldDeclarationsShouldBeAtStartOfClass" /> -->
<rule ref="category/java/codestyle.xml/ForLoopShouldBeWhileLoop"/>
<rule ref="category/java/codestyle.xml/IdenticalCatchBranches"/>
<!-- <rule ref="category/java/codestyle.xml/LocalVariableCouldBeFinal" /> -->
<!-- <rule ref="category/java/codestyle.xml/MethodArgumentCouldBeFinal" /> -->
<!-- <rule ref="category/java/codestyle.xml/MIsLeadingVariableName" /> -->
<rule ref="category/java/codestyle.xml/NoPackage"/>
<!-- <rule ref="category/java/codestyle.xml/OnlyOneReturn" /> -->
<!-- <rule ref="category/java/codestyle.xml/PrematureDeclaration" /> -->
<!-- <rule ref="category/java/codestyle.xml/SuspiciousConstantFieldName" /> -->
<!-- <rule ref="category/java/codestyle.xml/TooManyStaticImports" /> -->
<rule ref="category/java/codestyle.xml/UnnecessaryAnnotationValueElement"/>
<rule ref="category/java/codestyle.xml/UnnecessaryConstructor"/>
<rule ref="category/java/codestyle.xml/UnnecessaryFullyQualifiedName"/>
<rule ref="category/java/codestyle.xml/UnnecessaryLocalBeforeReturn"/>
<rule ref="category/java/codestyle.xml/UnnecessaryModifier"/>
<rule ref="category/java/codestyle.xml/UnnecessaryReturn"/>
<rule ref="category/java/codestyle.xml/UselessParentheses"/>
<rule ref="category/java/codestyle.xml/UselessQualifiedThis"/>
<rule ref="category/java/design.xml/AbstractClassWithoutAnyMethod"/>
<!-- <rule ref="category/java/design.xml/AvoidCatchingGenericException" /> -->
<!-- <rule ref="category/java/design.xml/AvoidDeeplyNestedIfStmts" /> -->
<!-- <rule ref="category/java/design.xml/AvoidRethrowingException" /> -->
<!-- <rule ref="category/java/design.xml/AvoidThrowingNewInstanceOfSameException" /> -->
<!-- <rule ref="category/java/design.xml/AvoidThrowingNullPointerException" /> -->
<!-- <rule ref="category/java/design.xml/AvoidThrowingRawExceptionTypes" /> -->
<rule ref="category/java/design.xml/ClassWithOnlyPrivateConstructorsShouldBeFinal"/>
<!-- <rule ref="category/java/design.xml/CollapsibleIfStatements" /> -->
<rule ref="category/java/design.xml/CouplingBetweenObjects"/>
<!-- <rule ref="category/java/design.xml/DataClass" /> -->
<!-- <rule ref="category/java/design.xml/DoNotExtendJavaLangError" /> -->
<!-- <rule ref="category/java/design.xml/ExceptionAsFlowControl" /> -->
<!-- <rule ref="category/java/design.xml/ExcessiveClassLength" /> -->
<!-- <rule ref="category/java/design.xml/ExcessiveImports" /> -->
<rule ref="category/java/design.xml/ExcessiveMethodLength"/>
<rule ref="category/java/design.xml/ExcessiveParameterList"/>
<rule ref="category/java/design.xml/ExcessivePublicCount"/>
<rule ref="category/java/design.xml/FinalFieldCouldBeStatic"/>
<!-- <rule ref="category/java/design.xml/GodClass" /> -->
<!-- <rule ref="category/java/design.xml/ImmutableField" /> -->
<!-- <rule ref="category/java/design.xml/LawOfDemeter"/>-->
<rule ref="category/java/design.xml/LogicInversion"/>
<rule ref="category/java/design.xml/CyclomaticComplexity"/>
<!-- <rule ref="category/java/design.xml/NcssCount" /> -->
<rule ref="category/java/design.xml/NPathComplexity"/>
<!-- <rule ref="category/java/design.xml/SignatureDeclareThrowsException" /> -->
<rule ref="category/java/design.xml/SimplifiedTernary"/>
<!-- <rule ref="category/java/design.xml/SimplifyBooleanAssertion" /> -->
<!-- <rule ref="category/java/design.xml/SimplifyBooleanExpressions" /> -->
<rule ref="category/java/design.xml/SimplifyBooleanReturns"/>
<rule ref="category/java/design.xml/SimplifyConditional"/>
<rule ref="category/java/design.xml/SingularField"/>
<!-- <rule ref="category/java/design.xml/SwitchDensity" /> -->
<rule ref="category/java/design.xml/TooManyFields"/>
<rule ref="category/java/design.xml/TooManyMethods"/>
<rule ref="category/java/design.xml/UselessOverridingMethod"/>
<!-- <rule ref="category/java/design.xml/UseObjectForClearerAPI" /> -->
<rule ref="category/java/design.xml/UseUtilityClass"/>
<!-- <rule ref="category/java/documentation.xml/CommentContent" /> -->
<!-- <rule ref="category/java/documentation.xml/CommentRequired" /> -->
<!-- <rule ref="category/java/documentation.xml/CommentSize" /> -->
<rule ref="category/java/documentation.xml/UncommentedEmptyConstructor"/>
<rule ref="category/java/documentation.xml/UncommentedEmptyMethodBody"/>
<rule ref="category/java/errorprone.xml/AssignmentInOperand">
<properties>
<property name="allowWhile" value="true"/>
</properties>
</rule>
<rule ref="category/java/errorprone.xml/AssignmentToNonFinalStatic"/>
<rule ref="category/java/errorprone.xml/AvoidAccessibilityAlteration"/>
<!-- <rule ref="category/java/errorprone.xml/AvoidAssertAsIdentifier" /> -->
<rule ref="category/java/errorprone.xml/AvoidBranchingStatementAsLastInLoop"/>
<!-- <rule ref="category/java/errorprone.xml/AvoidCallingFinalize" /> -->
<!-- <rule ref="category/java/errorprone.xml/AvoidCatchingNPE" /> -->
<rule ref="category/java/errorprone.xml/AvoidCatchingThrowable"/>
<rule ref="category/java/errorprone.xml/AvoidDecimalLiteralsInBigDecimalConstructor"/>
<!-- <rule ref="category/java/errorprone.xml/AvoidDuplicateLiterals" /> -->
<!-- <rule ref="category/java/errorprone.xml/AvoidEnumAsIdentifier" /> -->
<!-- <rule ref="category/java/errorprone.xml/AvoidFieldNameMatchingMethodName" /> -->
<!-- <rule ref="category/java/errorprone.xml/AvoidFieldNameMatchingTypeName" /> -->
<rule ref="category/java/errorprone.xml/AvoidInstanceofChecksInCatchClause"/>
<!-- <rule ref="category/java/errorprone.xml/AvoidLiteralsInIfCondition" /> -->
<!-- <rule ref="category/java/errorprone.xml/AvoidLosingExceptionInformation" /> -->
<rule ref="category/java/errorprone.xml/AvoidMultipleUnaryOperators"/>
<rule ref="category/java/errorprone.xml/AvoidUsingOctalValues"/>
<rule ref="category/java/errorprone.xml/BadComparison"/>
<!-- <rule ref="category/java/errorprone.xml/BeanMembersShouldSerialize" /> -->
<rule ref="category/java/errorprone.xml/BrokenNullCheck"/>
<!-- <rule ref="category/java/errorprone.xml/CallSuperFirst" /> -->
<!-- <rule ref="category/java/errorprone.xml/CallSuperLast" /> -->
<rule ref="category/java/errorprone.xml/CheckSkipResult"/>
<rule ref="category/java/errorprone.xml/ClassCastExceptionWithToArray"/>
<rule ref="category/java/errorprone.xml/CloneMethodMustBePublic"/>
<rule ref="category/java/errorprone.xml/CloneMethodMustImplementCloneable"/>
<rule ref="category/java/errorprone.xml/CloneMethodReturnTypeMustMatchClassName"/>
<rule ref="category/java/errorprone.xml/CloneThrowsCloneNotSupportedException"/>
<rule ref="category/java/errorprone.xml/CloseResource"/>
<rule ref="category/java/errorprone.xml/CompareObjectsWithEquals"/>
<!-- <rule ref="category/java/errorprone.xml/ConstructorCallsOverridableMethod" /> -->
<!-- <rule ref="category/java/errorprone.xml/DataflowAnomalyAnalysis" /> -->
<rule ref="category/java/errorprone.xml/DoNotCallGarbageCollectionExplicitly"/>
<!-- <rule ref="category/java/errorprone.xml/DoNotCallSystemExit" /> -->
<rule ref="category/java/errorprone.xml/DoNotExtendJavaLangThrowable"/>
<rule ref="category/java/design.xml/DoNotExtendJavaLangError"/>
<!-- <rule ref="category/java/errorprone.xml/DoNotHardCodeSDCard" /> -->
<!-- <rule ref="category/java/errorprone.xml/DoNotThrowExceptionInFinally" /> -->
<!-- <rule ref="category/java/errorprone.xml/DontImportSun" /> -->
<rule ref="category/java/errorprone.xml/DontUseFloatTypeForLoopIndices"/>
<rule ref="category/java/errorprone.xml/EmptyCatchBlock"/>
<!-- EMPTY RULES -->
<rule ref="category/java/errorprone.xml/EmptyFinalizer"/>
<rule ref="category/java/errorprone.xml/EmptyFinallyBlock"/>
<rule ref="category/java/errorprone.xml/EmptyIfStmt"/>
<rule ref="category/java/errorprone.xml/EmptyInitializer"/>
<rule ref="category/java/errorprone.xml/EmptyStatementBlock"/>
<rule ref="category/java/errorprone.xml/EmptyStatementNotInLoop"/>
<rule ref="category/java/errorprone.xml/EmptySwitchStatements"/>
<rule ref="category/java/errorprone.xml/EmptySynchronizedBlock"/>
<rule ref="category/java/errorprone.xml/EmptyTryBlock"/>
<rule ref="category/java/errorprone.xml/EmptyWhileStmt"/>
<rule ref="category/java/errorprone.xml/EqualsNull"/>
<!-- <rule ref="category/java/errorprone.xml/FinalizeDoesNotCallSuperFinalize" /> -->
<!-- <rule ref="category/java/errorprone.xml/FinalizeOnlyCallsSuperFinalize" /> -->
<!-- <rule ref="category/java/errorprone.xml/FinalizeOverloaded" /> -->
<!-- <rule ref="category/java/errorprone.xml/FinalizeShouldBeProtected" /> -->
<rule ref="category/java/errorprone.xml/IdempotentOperations"/>
<rule ref="category/java/errorprone.xml/InstantiationToGetClass"/>
<!-- <rule ref="category/java/errorprone.xml/InvalidSlf4jMessageFormat" /> -->
<rule ref="category/java/errorprone.xml/JumbledIncrementer"/>
<!-- <rule ref="category/java/errorprone.xml/JUnitSpelling" /> -->
<!-- <rule ref="category/java/errorprone.xml/JUnitStaticSuite" /> -->
<!-- <rule ref="category/java/errorprone.xml/LoggerIsNotStaticFinal" /> -->
<!-- <rule ref="category/java/errorprone.xml/MethodWithSameNameAsEnclosingClass" /> -->
<rule ref="category/java/errorprone.xml/MisplacedNullCheck"/>
<rule ref="category/java/errorprone.xml/MissingBreakInSwitch"/>
<!-- <rule ref="category/java/errorprone.xml/MissingSerialVersionUID" /> -->
<rule ref="category/java/errorprone.xml/MissingStaticMethodInNonInstantiatableClass"/>
<!-- <rule ref="category/java/errorprone.xml/MoreThanOneLogger" /> -->
<rule ref="category/java/errorprone.xml/NonCaseLabelInSwitchStatement"/>
<rule ref="category/java/errorprone.xml/NonStaticInitializer"/>
<!-- <rule ref="category/java/errorprone.xml/NullAssignment" /> -->
<rule ref="category/java/errorprone.xml/OverrideBothEqualsAndHashcode"/>
<rule ref="category/java/errorprone.xml/ProperCloneImplementation"/>
<rule ref="category/java/errorprone.xml/ProperLogger"/>
<rule ref="category/java/errorprone.xml/ReturnEmptyArrayRatherThanNull"/>
<rule ref="category/java/errorprone.xml/ReturnFromFinallyBlock"/>
<!-- <rule ref="category/java/errorprone.xml/SimpleDateFormatNeedsLocale" /> -->
<rule ref="category/java/errorprone.xml/SingleMethodSingleton"/>
<rule ref="category/java/errorprone.xml/SingletonClassReturningNewInstance"/>
<!-- <rule ref="category/java/errorprone.xml/StaticEJBFieldShouldBeFinal" /> -->
<!-- <rule ref="category/java/errorprone.xml/StringBufferInstantiationWithChar" /> -->
<rule ref="category/java/errorprone.xml/SuspiciousEqualsMethodName"/>
<rule ref="category/java/errorprone.xml/SuspiciousHashcodeMethodName"/>
<rule ref="category/java/errorprone.xml/SuspiciousOctalEscape"/>
<!-- <rule ref="category/java/errorprone.xml/TestClassWithoutTestCases" /> -->
<rule ref="category/java/errorprone.xml/UnconditionalIfStatement"/>
<!-- <rule ref="category/java/errorprone.xml/UnnecessaryBooleanAssertion" /> -->
<!-- <rule ref="category/java/errorprone.xml/UnnecessaryCaseChange" /> -->
<rule ref="category/java/errorprone.xml/UnnecessaryConversionTemporary"/>
<rule ref="category/java/errorprone.xml/UnusedNullCheckInEquals"/>
<!-- <rule ref="category/java/errorprone.xml/UseCorrectExceptionLogging" /> -->
<rule ref="category/java/errorprone.xml/UseEqualsToCompareStrings"/>
<rule ref="category/java/errorprone.xml/UselessOperationOnImmutable"/>
<rule ref="category/java/errorprone.xml/UseLocaleWithCaseConversions"/>
<!-- <rule ref="category/java/errorprone.xml/UseProperClassLoader" /> -->
<!-- <rule ref="category/java/multithreading.xml/AvoidSynchronizedAtMethodLevel" /> -->
<rule ref="category/java/multithreading.xml/AvoidThreadGroup"/>
<rule ref="category/java/multithreading.xml/AvoidUsingVolatile"/>
<!-- <rule ref="category/java/multithreading.xml/DoNotUseThreads" /> -->
<rule ref="category/java/multithreading.xml/DontCallThreadRun"/>
<rule ref="category/java/multithreading.xml/DoubleCheckedLocking"/>
<rule ref="category/java/multithreading.xml/NonThreadSafeSingleton"/>
<!-- <rule ref="category/java/multithreading.xml/UnsynchronizedStaticDateFormatter"/>-->
<!-- <rule ref="category/java/multithreading.xml/UseConcurrentHashMap" /> -->
<rule ref="category/java/multithreading.xml/UseNotifyAllInsteadOfNotify"/>
<!-- <rule ref="category/java/performance.xml/AddEmptyString" /> -->
<!-- <rule ref="category/java/performance.xml/AppendCharacterWithChar" /> -->
<!-- <rule ref="category/java/performance.xml/AvoidArrayLoops" /> -->
<!-- <rule ref="category/java/performance.xml/AvoidFileStream" /> -->
<!-- <rule ref="category/java/performance.xml/AvoidInstantiatingObjectsInLoops" /> -->
<!-- <rule ref="category/java/performance.xml/AvoidUsingShortType" /> -->
<rule ref="category/java/performance.xml/BigIntegerInstantiation"/>
<rule ref="category/java/performance.xml/BooleanInstantiation"/>
<!-- <rule ref="category/java/performance.xml/ByteInstantiation" /> -->
<!-- <rule ref="category/java/performance.xml/ConsecutiveAppendsShouldReuse" /> -->
<!-- <rule ref="category/java/performance.xml/ConsecutiveLiteralAppends" /> -->
<!-- <rule ref="category/java/performance.xml/InefficientEmptyStringCheck" /> -->
<!-- <rule ref="category/java/performance.xml/InefficientStringBuffering" /> -->
<!-- <rule ref="category/java/performance.xml/InsufficientStringBufferDeclaration" /> -->
<!-- <rule ref="category/java/performance.xml/IntegerInstantiation" /> -->
<!-- <rule ref="category/java/performance.xml/LongInstantiation" /> -->
<rule ref="category/java/performance.xml/OptimizableToArrayCall"/>
<!-- <rule ref="category/java/performance.xml/RedundantFieldInitializer" /> -->
<!-- <rule ref="category/java/performance.xml/SimplifyStartsWith" /> -->
<!-- <rule ref="category/java/performance.xml/ShortInstantiation" /> -->
<!-- <rule ref="category/java/performance.xml/StringInstantiation" /> -->
<!-- <rule ref="category/java/performance.xml/StringToString" /> -->
<rule ref="category/java/performance.xml/TooFewBranchesForASwitchStatement"/>
<!-- <rule ref="category/java/performance.xml/UnnecessaryWrapperObjectCreation" /> -->
<!-- <rule ref="category/java/performance.xml/UseArrayListInsteadOfVector" /> -->
<!-- <rule ref="category/java/performance.xml/UseArraysAsList" /> -->
<!-- <rule ref="category/java/performance.xml/UseIndexOfChar" /> -->
<!-- <rule ref="category/java/performance.xml/UselessStringValueOf" /> -->
<!-- <rule ref="category/java/performance.xml/UseStringBufferForStringAppends" /> -->
<!-- <rule ref="category/java/performance.xml/UseStringBufferLength" /> -->
</ruleset>

View File

@ -1,23 +1,50 @@
version: "3.7"
services:
dbmysql:
image: mysql:5.7
image: mysql:8.0
container_name: 'dbmysql'
environment:
MYSQL_DATABASE: 'gruppen2'
MYSQL_USER: 'root'
MYSQL_ROOT_PASSWORD: 'geheim'
MYSQL_DATABASE: 'gruppen'
MYSQL_USER: 'gruppen'
MYSQL_PASSWORD: 'password'
MYSQL_ROOT_PASSWORD: 'root'
restart: always
volumes:
- './mysql/db/storage:/var/lib/mysql'
- './mysql/db/entrypoint:/docker-entrypoint-initdb.d/'
ports:
- '3306:3306'
keymysql:
image: mysql:8.0
container_name: 'keymysql'
environment:
MYSQL_DATABASE: 'keycloak'
MYSQL_USER: 'keycloak'
MYSQL_PASSWORD: 'password'
MYSQL_ROOT_PASSWORD: 'root'
volumes:
- './mysql/keycloak/storage:/var/lib/mysql'
keycloak:
image: jboss/keycloak
container_name: 'keycloak'
depends_on:
- keymysql
environment:
DB_VENDOR: 'MYSQL'
DB_ADDR: 'keymysql'
DB_DATABASE: 'keycloak'
DB_USER: 'keycloak'
DB_PASSWORD: 'password'
KEYCLOAK_USER: 'admin'
KEYCLOAK_PASSWORD: 'admin'
ports:
- '8082:8080'
gruppenapp:
build: .
container_name: 'gruppenapp'
depends_on:
- dbmysql
- keycloak
command: ["/app/wait-for-it.sh", "dbmysql:3306", "--", "java", "-Dspring.profiles.active=docker", "-jar", "/app/gruppen2.jar"]
ports:
- '8081:8080'

View File

@ -1,20 +0,0 @@
# Bei "schicht" können Sie 'vormittags', 'nachmittags' oder 'egal' eintragen.
gruppe: IT-Bois
url: https://github.com/hhu-propra2/abschlussprojekt-it-bois
names:
- killerber4t
- tomvahl
- AndiBuls
- XXNitram
- LukasEttel
- Mahgs
- ChUrl
- kasch309
notebook: true
schicht: nachmittags
projektauswahl:
- Gruppenbildung
- Punkteübersicht
- Materialsammlung
- Korrektorinnen Bewerbung

2
lombok.config Normal file
View File

@ -0,0 +1,2 @@
lombok.anyConstructor.addConstructorProperties = true
lombok.equalsAndHashCode.callSuper = call

View File

@ -1,15 +1,10 @@
CREATE TABLE event
(
event_id INT PRIMARY KEY AUTO_INCREMENT,
group_id VARCHAR(36) NOT NULL,
user_id VARCHAR(50),
event_type VARCHAR(36),
event_payload JSON
);
CREATE TABLE invite
(
invite_id INT PRIMARY KEY AUTO_INCREMENT,
group_id VARCHAR(36) NOT NULL,
invite_link VARCHAR(36) NOT NULL
event_id INT PRIMARY KEY AUTO_INCREMENT,
group_id VARCHAR(36) NOT NULL,
group_version INT NOT NULL,
exec_id VARCHAR(32) NOT NULL,
target_id VARCHAR(32),
event_date DATETIME NOT NULL,
event_payload JSON NOT NULL
);

View File

@ -0,0 +1,12 @@
DROP TABLE IF EXISTS event;
CREATE TABLE event
(
event_id INT PRIMARY KEY AUTO_INCREMENT,
group_id VARCHAR(36) NOT NULL,
group_version INT NOT NULL,
exec_id VARCHAR(32) NOT NULL,
target_id VARCHAR(32),
event_date DATETIME NOT NULL,
event_payload TEXT NOT NULL
);

View File

@ -2,47 +2,11 @@ package mops.gruppen2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.Collections;
@SpringBootApplication
@EnableCaching
@EnableSwagger2
public class Gruppen2Application {
public static void main(String[] args) {
SpringApplication.run(Gruppen2Application.class, args);
}
@Bean
public Docket productAPI() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.ant("/gruppen2/api/**"))
.apis(RequestHandlerSelectors.basePackage("mops.gruppen2"))
.build()
.apiInfo(apiMetadata());
}
private ApiInfo apiMetadata() {
return new ApiInfo(
"Gruppenbildung API",
"API zum anfragen/aktualisieren der Gruppendaten.",
"0.0.1",
"Free to use",
new Contact("gruppen2", "https://github.com/hhu-propra2/abschlussprojekt-it-bois", ""),
"",
"",
Collections.emptyList()
);
}
}

View File

@ -0,0 +1,62 @@
package mops.gruppen2.aspect;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.Trace;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Log4j2
@Profile("dev")
@Aspect
@Component
public class LogAspect {
// ######################################### POINTCUT ########################################
@Pointcut("within(@mops.gruppen2.aspect.annotation.TraceMethodCalls *)")
public void beanAnnotatedWithMonitor() {}
@Pointcut("execution(public * *(..))")
public void publicMethod() {}
@Pointcut("publicMethod() && beanAnnotatedWithMonitor()")
public void logMethodCalls() {}
// ###################################### ANNOTATIONS ########################################
@Before("@annotation(mops.gruppen2.aspect.annotation.Trace)")
public static void logCustom(JoinPoint joinPoint) {
log.trace(((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Trace.class).value());
}
@Before("@annotation(mops.gruppen2.aspect.annotation.TraceMethodCall) || logMethodCalls()")
public static void logMethodCall(JoinPoint joinPoint) {
log.trace("Methodenaufruf: {} ({})",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName().replace("mops.gruppen2.", ""));
System.out.println();
}
@Around("@annotation(mops.gruppen2.aspect.annotation.TraceExecutionTime)")
public static Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
joinPoint.proceed();
long stop = System.currentTimeMillis();
log.trace("Ausführungsdauer: {} Millis", stop - start);
return joinPoint.proceed();
}
}

View File

@ -0,0 +1,16 @@
package mops.gruppen2.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Schreibt eine benutzerdefinierte Nachricht in den Trace-Stream bei Methodenaufruf.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
String value();
}

View File

@ -0,0 +1,15 @@
package mops.gruppen2.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Schreibt die Methodenausführdauer in den Trace-Stream.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceExecutionTime {
}

View File

@ -0,0 +1,14 @@
package mops.gruppen2.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Schreibt eine Nachricht bei Methodenausführung in den Trace-Stream.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceMethodCall {
}

View File

@ -0,0 +1,14 @@
package mops.gruppen2.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Schreibt eine Nachricht für jede ausgeführte Methode einer Klasse in den Trace-Stream.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceMethodCalls {
}

View File

@ -0,0 +1,15 @@
package mops.gruppen2.config;
import mops.gruppen2.config.converter.StringToLimitConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class FormatterConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToLimitConverter());
}
}

View File

@ -2,6 +2,7 @@ package mops.gruppen2.config;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -15,15 +16,16 @@ import org.springframework.web.client.RestTemplate;
*/
@Configuration
@KeycloakConfiguration
public class KeycloakConfig {
@Value("${keycloak.resource}")
private String clientId;
@Value("${keycloak.credentials.secret}")
@Value("2e2e5770-c454-4d31-be99-9d8c34c93089")
private String clientSecret;
@Value("${hhu_keycloak.token-uri}")
@Value("https://churl-keycloak.herokuapp.com/auth/realms/Gruppen/protocol/openid-connect/token")
private String tokenUri;
@Bean

View File

@ -29,7 +29,7 @@ import javax.servlet.http.HttpServletRequest;
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {

View File

@ -0,0 +1,43 @@
package mops.gruppen2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.Collections;
@Profile("dev")
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket productAPI() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.ant("/gruppen2/api/**"))
.apis(RequestHandlerSelectors.basePackage("mops.gruppen2"))
.build()
.apiInfo(apiMetadata());
}
private ApiInfo apiMetadata() {
return new ApiInfo(
"Gruppenbildung API",
"API zum anfragen/aktualisieren der Gruppendaten.",
"0.0.1",
"Free to use",
new Contact("gruppen2", "https://github.com/hhu-propra2/abschlussprojekt-it-bois", ""),
"",
"",
Collections.emptyList()
);
}
}

View File

@ -0,0 +1,12 @@
package mops.gruppen2.config.converter;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import org.springframework.core.convert.converter.Converter;
public class StringToLimitConverter implements Converter<String, Limit> {
@Override
public Limit convert(String value) {
return new Limit(Long.parseLong(value));
}
}

View File

@ -1,74 +0,0 @@
package mops.gruppen2.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.api.GroupRequestWrapper;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.service.APIFormatterService;
import mops.gruppen2.service.EventService;
import mops.gruppen2.service.GroupService;
import mops.gruppen2.service.UserService;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Api zum Datenabgleich mit Gruppenfindung.
*/
//TODO: API-Service?
@RestController
@RequestMapping("/gruppen2/api")
public class APIController {
private final EventService eventService;
private final GroupService groupService;
private final UserService userService;
public APIController(EventService eventService, GroupService groupService, UserService userService) {
this.eventService = eventService;
this.groupService = groupService;
this.userService = userService;
}
@GetMapping("/updateGroups/{lastEventId}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt alle Gruppen zurück, in denen sich etwas geändert hat")
public GroupRequestWrapper updateGroups(@ApiParam("Letzter Status des Anfragestellers") @PathVariable Long lastEventId) throws EventException {
List<Event> events = eventService.getNewEvents(lastEventId);
return APIFormatterService.wrap(eventService.getMaxEventId(), groupService.projectEventList(events));
}
@GetMapping("/getGroupIdsOfUser/{userId}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt alle Gruppen zurück, in denen sich ein Teilnehmer befindet")
public List<String> getGroupIdsOfUser(@ApiParam("Teilnehmer dessen groupIds zurückgegeben werden sollen") @PathVariable String userId) {
return userService.getUserGroups(userId).stream()
.map(group -> group.getId().toString())
.collect(Collectors.toList());
}
@GetMapping("/getGroup/{groupId}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt die Gruppe mit der als Parameter mitgegebenden groupId zurück")
public Group getGroupById(@ApiParam("GruppenId der gefordeten Gruppe") @PathVariable String groupId) throws EventException {
List<Event> eventList = eventService.getEventsOfGroup(UUID.fromString(groupId));
List<Group> groups = groupService.projectEventList(eventList);
if (groups.isEmpty()) {
return null;
}
return groups.get(0);
}
}

View File

@ -1,120 +0,0 @@
package mops.gruppen2.controller;
import mops.gruppen2.domain.Account;
import mops.gruppen2.service.ControllerService;
import mops.gruppen2.service.GroupService;
import mops.gruppen2.service.KeyCloakService;
import mops.gruppen2.service.ValidationService;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.annotation.SessionScope;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.security.RolesAllowed;
import java.util.UUID;
@Controller
@SessionScope
@RequestMapping("/gruppen2")
public class GroupCreationController {
private final GroupService groupService;
private final ControllerService controllerService;
private final ValidationService validationService;
public GroupCreationController(GroupService groupService, ControllerService controllerService, ValidationService validationService) {
this.groupService = groupService;
this.controllerService = controllerService;
this.validationService = validationService;
}
@RolesAllowed({"ROLE_orga", "ROLE_actuator"})
@GetMapping("/createOrga")
public String createGroupAsOrga(KeycloakAuthenticationToken token,
Model model) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
model.addAttribute("account", account);
model.addAttribute("lectures", groupService.getAllLecturesWithVisibilityPublic());
return "createOrga";
}
@RolesAllowed({"ROLE_orga", "ROLE_actuator"})
@PostMapping("/createOrga")
@CacheEvict(value = "groups", allEntries = true)
public String postCrateGroupAsOrga(KeycloakAuthenticationToken token,
@RequestParam("title") String title,
@RequestParam("description") String description,
@RequestParam(value = "visibility", required = false) Boolean visibility,
@RequestParam(value = "lecture", required = false) Boolean lecture,
@RequestParam("userMaximum") Long userMaximum,
@RequestParam(value = "maxInfiniteUsers", required = false) Boolean maxInfiniteUsers,
@RequestParam(value = "parent", required = false) String parent,
@RequestParam(value = "file", required = false) MultipartFile file) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
UUID parentUUID = controllerService.getUUID(parent);
validationService.checkFields(description, title, userMaximum, maxInfiniteUsers);
controllerService.createGroupAsOrga(account,
title,
description,
visibility,
lecture,
maxInfiniteUsers,
userMaximum,
parentUUID,
file);
return "redirect:/gruppen2";
}
@RolesAllowed("ROLE_studentin")
@GetMapping("/createStudent")
public String createGroupAsStudent(KeycloakAuthenticationToken token,
Model model) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
model.addAttribute("account", account);
model.addAttribute("lectures", groupService.getAllLecturesWithVisibilityPublic());
return "createStudent";
}
@RolesAllowed("ROLE_studentin")
@PostMapping("/createStudent")
@CacheEvict(value = "groups", allEntries = true)
public String postCreateGroupAsStudent(KeycloakAuthenticationToken token,
@RequestParam("title") String title,
@RequestParam("description") String description,
@RequestParam("userMaximum") Long userMaximum,
@RequestParam(value = "visibility", required = false) Boolean visibility,
@RequestParam(value = "maxInfiniteUsers", required = false) Boolean maxInfiniteUsers,
@RequestParam(value = "parent", required = false) String parent) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
UUID parentUUID = controllerService.getUUID(parent);
validationService.checkFields(description, title, userMaximum, maxInfiniteUsers);
controllerService.createGroup(account,
title,
description,
visibility,
null,
maxInfiniteUsers,
userMaximum,
parentUUID);
return "redirect:/gruppen2";
}
}

View File

@ -1,281 +0,0 @@
package mops.gruppen2.controller;
import mops.gruppen2.domain.Account;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.Role;
import mops.gruppen2.domain.User;
import mops.gruppen2.domain.Visibility;
import mops.gruppen2.service.ControllerService;
import mops.gruppen2.service.InviteService;
import mops.gruppen2.service.KeyCloakService;
import mops.gruppen2.service.UserService;
import mops.gruppen2.service.ValidationService;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.annotation.SessionScope;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
@Controller
@SessionScope
@RequestMapping("/gruppen2")
public class GroupDetailsController {
private final ControllerService controllerService;
private final UserService userService;
private final ValidationService validationService;
private final InviteService inviteService;
public GroupDetailsController(ControllerService controllerService, UserService userService, ValidationService validationService, InviteService inviteService) {
this.controllerService = controllerService;
this.userService = userService;
this.validationService = validationService;
this.inviteService = inviteService;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@GetMapping("/details/{id}")
public String showGroupDetails(KeycloakAuthenticationToken token,
Model model,
HttpServletRequest request,
@PathVariable("id") String groupId) {
Group group = userService.getGroupById(UUID.fromString(groupId));
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
UUID parentId = group.getParent();
String actualURL = request.getRequestURL().toString();
String serverURL = actualURL.substring(0, actualURL.indexOf("gruppen2/"));
Group parent = controllerService.getParent(parentId);
validationService.throwIfGroupNotExisting(group.getTitle());
model.addAttribute("account", account);
if (!validationService.checkIfUserInGroup(group, user)) {
validationService.throwIfNoAccessToPrivate(group, user);
model.addAttribute("group", group);
model.addAttribute("parentId", parentId);
model.addAttribute("parent", parent);
return "detailsNoMember";
}
model.addAttribute("parentId", parentId);
model.addAttribute("parent", parent);
model.addAttribute("group", group);
model.addAttribute("roles", group.getRoles());
model.addAttribute("user", user);
model.addAttribute("admin", Role.ADMIN);
model.addAttribute("public", Visibility.PUBLIC);
model.addAttribute("private", Visibility.PRIVATE);
if (validationService.checkIfAdmin(group, user)) {
model.addAttribute("link", serverURL + "gruppen2/acceptinvite/" + inviteService.getLinkByGroupId(group.getId()));
}
return "detailsMember";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@GetMapping("/details/changeMetadata/{id}")
public String changeMetadata(KeycloakAuthenticationToken token,
Model model,
@PathVariable("id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfNoAdmin(group, user);
model.addAttribute("account", account);
model.addAttribute("title", group.getTitle());
model.addAttribute("description", group.getDescription());
model.addAttribute("admin", Role.ADMIN);
model.addAttribute("roles", group.getRoles());
model.addAttribute("groupId", group.getId());
model.addAttribute("user", user);
return "changeMetadata";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/details/changeMetadata")
@CacheEvict(value = "groups", allEntries = true)
public String postChangeMetadata(KeycloakAuthenticationToken token,
@RequestParam("title") String title,
@RequestParam("description") String description,
@RequestParam("groupId") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfNoAdmin(group, user);
validationService.checkFields(title, description);
controllerService.changeMetaData(account, group, title, description);
return "redirect:/gruppen2/details/" + groupId;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@GetMapping("/details/members/{id}")
public String editMembers(KeycloakAuthenticationToken token,
Model model,
@PathVariable("id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
Group group = userService.getGroupById(UUID.fromString(groupId));
User user = new User(account);
validationService.throwIfNoAdmin(group, user);
model.addAttribute("account", account);
model.addAttribute("members", group.getMembers());
model.addAttribute("group", group);
model.addAttribute("admin", Role.ADMIN);
return "editMembers";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/details/members/changeRole")
@CacheEvict(value = "groups", allEntries = true)
public String changeRole(KeycloakAuthenticationToken token,
@RequestParam("group_id") String groupId,
@RequestParam("user_id") String userId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
Group group = userService.getGroupById(UUID.fromString(groupId));
User principle = new User(account);
User user = new User(userId, "", "", "");
validationService.throwIfNoAdmin(group, principle);
//TODO: checkIfAdmin checkt nicht, dass die rolle geändert wurde. oder die rolle wird nicht geändert
controllerService.changeRole(account, user, group);
if (!validationService.checkIfAdmin(group, principle)) {
return "redirect:/gruppen2/details/" + groupId;
}
return "redirect:/gruppen2/details/members/" + groupId;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/details/members/changeMaximum")
@CacheEvict(value = "groups", allEntries = true)
public String changeMaxSize(KeycloakAuthenticationToken token,
@RequestParam("maximum") Long maximum,
@RequestParam("group_id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfNewMaximumIsValid(maximum, group);
controllerService.updateMaxUser(account, UUID.fromString(groupId), maximum);
return "redirect:/gruppen2/details/members/" + groupId;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/details/members/deleteUser")
@CacheEvict(value = "groups", allEntries = true)
public String deleteUser(KeycloakAuthenticationToken token,
@RequestParam("group_id") String groupId,
@RequestParam("user_id") String userId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User principle = new User(account);
User user = new User(userId, "", "", "");
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfNoAdmin(group, principle);
controllerService.deleteUser(account, user, group);
if (!validationService.checkIfUserInGroup(group, principle)) {
return "redirect:/gruppen2";
}
return "redirect:/gruppen2/details/members/" + groupId;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/detailsBeitreten")
@CacheEvict(value = "groups", allEntries = true)
public String joinGroup(KeycloakAuthenticationToken token,
Model model,
@RequestParam("id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfUserAlreadyInGroup(group, user);
validationService.throwIfGroupFull(group);
controllerService.addUser(account, UUID.fromString(groupId));
model.addAttribute("account", account);
return "redirect:/gruppen2";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/leaveGroup")
@CacheEvict(value = "groups", allEntries = true)
public String leaveGroup(KeycloakAuthenticationToken token,
@RequestParam("group_id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
Group group = userService.getGroupById(UUID.fromString(groupId));
controllerService.deleteUser(account, user, group);
return "redirect:/gruppen2";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/deleteGroup")
@CacheEvict(value = "groups", allEntries = true)
public String deleteGroup(KeycloakAuthenticationToken token,
@RequestParam("group_id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfNoAdmin(group, user);
controllerService.deleteGroupEvent(user.getId(), UUID.fromString(groupId));
return "redirect:/gruppen2";
}
@RolesAllowed({"ROLE_orga", "ROLE_actuator"})
@PostMapping("/details/members/addUsersFromCsv")
@CacheEvict(value = "groups", allEntries = true)
public String addUsersFromCsv(KeycloakAuthenticationToken token,
@RequestParam("group_id") String groupId,
@RequestParam(value = "file", required = false) MultipartFile file) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
controllerService.addUsersFromCsv(account, file, groupId);
return "redirect:/gruppen2/details/members/" + groupId;
}
}

View File

@ -1,123 +0,0 @@
package mops.gruppen2.controller;
import mops.gruppen2.domain.Account;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.User;
import mops.gruppen2.domain.Visibility;
import mops.gruppen2.service.ControllerService;
import mops.gruppen2.service.InviteService;
import mops.gruppen2.service.KeyCloakService;
import mops.gruppen2.service.UserService;
import mops.gruppen2.service.ValidationService;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.annotation.SessionScope;
import javax.annotation.security.RolesAllowed;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Controller
@SessionScope
@RequestMapping("/gruppen2")
public class SearchAndInviteController {
private final ValidationService validationService;
private final InviteService inviteService;
private final UserService userService;
private final ControllerService controllerService;
public SearchAndInviteController(ValidationService validationService, InviteService inviteService, UserService userService, ControllerService controllerService) {
this.validationService = validationService;
this.inviteService = inviteService;
this.userService = userService;
this.controllerService = controllerService;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@GetMapping("/findGroup")
public String findGroup(KeycloakAuthenticationToken token,
Model model,
@RequestParam(value = "suchbegriff", required = false) String search) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
List<Group> groups = new ArrayList<>();
groups = validationService.checkSearch(search, groups, account);
model.addAttribute("account", account);
model.addAttribute("gruppen", groups);
model.addAttribute("inviteService", inviteService);
return "search";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@GetMapping("/detailsSearch")
public String showGroupDetailsNoMember(KeycloakAuthenticationToken token,
Model model,
@RequestParam("id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
Group group = userService.getGroupById(UUID.fromString(groupId));
UUID parentId = group.getParent();
Group parent = controllerService.getParent(parentId);
User user = new User(account);
model.addAttribute("account", account);
if (validationService.checkIfUserInGroup(group, user)) {
return "redirect:/gruppen2/details/" + groupId;
}
model.addAttribute("group", group);
model.addAttribute("parentId", parentId);
model.addAttribute("parent", parent);
return "detailsNoMember";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@GetMapping("/acceptinvite/{link}")
public String acceptInvite(KeycloakAuthenticationToken token,
Model model,
@PathVariable("link") String link) {
Group group = userService.getGroupById(inviteService.getGroupIdFromLink(link));
validationService.throwIfGroupNotExisting(group.getTitle());
model.addAttribute("account", KeyCloakService.createAccountFromPrincipal(token));
model.addAttribute("group", group);
if (group.getVisibility() == Visibility.PUBLIC) {
return "redirect:/gruppen2/details/" + group.getId();
}
return "joinprivate";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@PostMapping("/acceptinvite")
@CacheEvict(value = "groups", allEntries = true)
public String postAcceptInvite(KeycloakAuthenticationToken token,
@RequestParam("id") String groupId) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
Group group = userService.getGroupById(UUID.fromString(groupId));
validationService.throwIfUserAlreadyInGroup(group, user);
validationService.throwIfGroupFull(group);
controllerService.addUser(account, UUID.fromString(groupId));
return "redirect:/gruppen2/details/" + groupId;
}
}

View File

@ -1,10 +1,14 @@
package mops.gruppen2.domain;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import java.util.Set;
@Value
@AllArgsConstructor
public class Account {
String name; //user_id
@ -13,4 +17,14 @@ public class Account {
String givenname;
String familyname;
Set<String> roles;
public Account(KeycloakAuthenticationToken token) {
KeycloakPrincipal principal = (KeycloakPrincipal) token.getPrincipal();
name = principal.getName();
email = principal.getKeycloakSecurityContext().getIdToken().getEmail();
image = null;
givenname = principal.getKeycloakSecurityContext().getIdToken().getGivenName();
familyname = principal.getKeycloakSecurityContext().getIdToken().getFamilyName();
roles = token.getAccount().getRoles();
}
}

View File

@ -1,35 +0,0 @@
package mops.gruppen2.domain;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Repräsentiert den aggregierten Zustand einer Gruppe.
*/
@Getter
@Setter
public class Group {
//TODO: List to Hashmap
private final List<User> members;
private final Map<String, Role> roles;
private UUID id;
private String title;
private String description;
private Long userMaximum;
private GroupType type;
private Visibility visibility;
private UUID parent;
public Group() {
members = new ArrayList<>();
roles = new HashMap<>();
}
}

View File

@ -1,6 +0,0 @@
package mops.gruppen2.domain;
public enum GroupType {
SIMPLE,
LECTURE
}

View File

@ -1,6 +0,0 @@
package mops.gruppen2.domain;
public enum Role {
ADMIN,
MEMBER
}

View File

@ -1,25 +0,0 @@
package mops.gruppen2.domain;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(exclude = {"givenname", "familyname", "email"})
public class User {
private String id;
private String givenname;
private String familyname;
private String email;
public User(Account account) {
id = account.getName();
givenname = account.getGivenname();
familyname = account.getFamilyname();
email = account.getEmail();
}
}

View File

@ -1,6 +0,0 @@
package mops.gruppen2.domain;
public enum Visibility {
PUBLIC,
PRIVATE
}

View File

@ -1,17 +0,0 @@
package mops.gruppen2.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Table("invite")
@Getter
@AllArgsConstructor
public class InviteLinkDTO {
@Id
Long invite_id;
String group_id;
String invite_link;
}

View File

@ -0,0 +1,57 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.UUID;
/**
* Fügt einen einzelnen Nutzer einer Gruppe hinzu.
*/
@Log4j2
@Value
@AllArgsConstructor
public class AddMemberEvent extends Event {
@JsonProperty("user")
User user;
public AddMemberEvent(UUID groupId, String exec, String target, User user) throws IdMismatchException {
super(groupId, exec, target);
this.user = user;
if (!target.equals(user.getId())) {
throw new IdMismatchException("Der User passt nicht zur angegebenen userid.");
}
}
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.usersPut(target, group);
}
@Override
protected void applyEvent(Group group) throws UserExistsException, GroupFullException {
group.addMember(target, user);
log.trace("\t\t\t\t\tNeue Members: {}", group.getMembers());
}
@Override
public String format() {
return "Benutzer hinzugefügt: " + target + ".";
}
@Override
public String type() {
return EventType.ADDMEMBER.toString();
}
}

View File

@ -1,47 +0,0 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.Role;
import mops.gruppen2.domain.User;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.UserAlreadyExistsException;
import java.util.UUID;
/**
* Fügt einen einzelnen Nutzer einer Gruppe hinzu.
*/
@Getter
@NoArgsConstructor // For Jackson
public class AddUserEvent extends Event {
private String givenname;
private String familyname;
private String email;
public AddUserEvent(UUID groupId, String userId, String givenname, String familyname, String email) {
super(groupId, userId);
this.givenname = givenname;
this.familyname = familyname;
this.email = email;
}
@Override
protected void applyEvent(Group group) throws EventException {
User user = new User(userId, givenname, familyname, email);
if (group.getMembers().contains(user)) {
throw new UserAlreadyExistsException(getClass().toString());
}
if (group.getMembers().size() >= group.getUserMaximum()) {
throw new GroupFullException(getClass().toString());
}
group.getMembers().add(user);
group.getRoles().put(userId, Role.MEMBER);
}
}

View File

@ -1,36 +1,56 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.GroupType;
import mops.gruppen2.domain.Visibility;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@NoArgsConstructor // For Jackson
@Log4j2
@Value
@AllArgsConstructor// Value generiert den allArgsConstrucot nur, wenn keiner explizit angegeben ist
public class CreateGroupEvent extends Event {
private Visibility groupVisibility;
private UUID groupParent;
private GroupType groupType;
private Long groupUserMaximum;
@JsonProperty("date")
LocalDateTime date;
public CreateGroupEvent(UUID groupId, String userId, UUID parent, GroupType type, Visibility visibility, Long userMaximum) {
super(groupId, userId);
groupParent = parent;
groupType = type;
groupVisibility = visibility;
groupUserMaximum = userMaximum;
public CreateGroupEvent(UUID groupId, String exec, LocalDateTime date) {
super(groupId, exec, null);
this.date = date;
}
@Override
protected void applyEvent(Group group) {
group.setId(groupId);
group.setParent(groupParent);
group.setType(groupType);
group.setVisibility(groupVisibility);
group.setUserMaximum(groupUserMaximum);
protected void updateCache(GroupCache cache, Group group) {
cache.groupsPut(groupid, group);
cache.linksPut(group.getLink(), group);
}
@Override
protected void applyEvent(Group group) throws BadArgumentException {
group.setId(groupid);
group.setCreator(exec);
group.setCreationDate(date);
log.trace("\t\t\t\t\tNeue Gruppe: {}", group.toString());
}
@Override
public String format() {
return "Gruppe erstellt.";
}
@Override
public String type() {
return EventType.CREATEGROUP.toString();
}
@Override
public String toString() {
return "(" + version + "," + groupid + "," + date + ")";
}
}

View File

@ -1,28 +0,0 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import java.util.UUID;
@Getter
@NoArgsConstructor // For Jackson
public class DeleteGroupEvent extends Event {
public DeleteGroupEvent(UUID groupId, String userId) {
super(groupId, userId);
}
@Override
protected void applyEvent(Group group) {
group.getRoles().clear();
group.getMembers().clear();
group.setTitle(null);
group.setDescription(null);
group.setVisibility(null);
group.setType(null);
group.setParent(null);
group.setUserMaximum(0L);
}
}

View File

@ -1,34 +0,0 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.User;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import java.util.UUID;
/**
* Entfernt ein einzelnes Mitglied einer Gruppe.
*/
@Getter
@NoArgsConstructor // For Jackson
public class DeleteUserEvent extends Event {
public DeleteUserEvent(UUID groupId, String userId) {
super(groupId, userId);
}
@Override
protected void applyEvent(Group group) throws EventException {
for (User user : group.getMembers()) {
if (user.getId().equals(this.userId)) {
group.getMembers().remove(user);
group.getRoles().remove(user.getId());
return;
}
}
throw new UserNotFoundException(this.getClass().toString());
}
}

View File

@ -0,0 +1,42 @@
package mops.gruppen2.domain.event;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.UUID;
@Log4j2
@Value
@AllArgsConstructor
public class DestroyGroupEvent extends Event {
public DestroyGroupEvent(UUID groupId, String exec) {
super(groupId, exec, null);
}
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.groupsRemove(groupid, group);
}
@Override
protected void applyEvent(Group group) throws NoAccessException {
group.destroy(exec);
log.trace("\t\t\t\t\tGelöschte Gruppe: {}", group.toString());
}
@Override
public String format() {
return "Gruppe gelöscht.";
}
@Override
public String type() {
return EventType.DESTROYGROUP.toString();
}
}

View File

@ -1,51 +1,119 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.GroupIdMismatchException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.time.LocalDateTime;
import java.util.UUID;
import static com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = AddUserEvent.class, name = "AddUserEvent"),
@JsonSubTypes.Type(value = CreateGroupEvent.class, name = "CreateGroupEvent"),
@JsonSubTypes.Type(value = DeleteUserEvent.class, name = "DeleteUserEvent"),
@JsonSubTypes.Type(value = UpdateGroupDescriptionEvent.class, name = "UpdateGroupDescriptionEvent"),
@JsonSubTypes.Type(value = UpdateGroupTitleEvent.class, name = "UpdateGroupTitleEvent"),
@JsonSubTypes.Type(value = UpdateRoleEvent.class, name = "UpdateRoleEvent"),
@JsonSubTypes.Type(value = DeleteGroupEvent.class, name = "DeleteGroupEvent"),
@JsonSubTypes.Type(value = UpdateUserMaxEvent.class, name = "UpdateUserMaxEvent")
})
@Log4j2
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "class")
@JsonSubTypes({@Type(value = AddMemberEvent.class, name = "ADDMEMBER"),
@Type(value = CreateGroupEvent.class, name = "CREATEGROUP"),
@Type(value = DestroyGroupEvent.class, name = "DESTROYGROUP"),
@Type(value = KickMemberEvent.class, name = "KICKMEMBER"),
@Type(value = SetDescriptionEvent.class, name = "SETDESCRIPTION"),
@Type(value = SetInviteLinkEvent.class, name = "SETLINK"),
@Type(value = SetLimitEvent.class, name = "SETLIMIT"),
@Type(value = SetParentEvent.class, name = "SETPARENT"),
@Type(value = SetTitleEvent.class, name = "SETTITLE"),
@Type(value = SetTypeEvent.class, name = "SETTYPE"),
@Type(value = UpdateRoleEvent.class, name = "UPDATEROLE")})
@Getter
@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor // Lombok needs a default constructor in the base class
public abstract class Event {
protected UUID groupId;
protected String userId;
@JsonProperty("groupid")
protected UUID groupid;
public void apply(Group group) throws EventException {
checkGroupIdMatch(group.getId());
applyEvent(group);
@JsonProperty("version")
protected long version; // Group-Version
@JsonProperty("exec")
protected String exec;
@JsonProperty("target")
protected String target;
@JsonProperty("date")
protected LocalDateTime date;
//TODO: Eigentlich sollte die Gruppe aus dem Cache genommen werden, nicht übergeben
public Event(UUID groupid, String exec, String target) {
this.groupid = groupid;
this.exec = exec;
this.target = target;
}
private void checkGroupIdMatch(UUID groupId) {
if (groupId == null || this.groupId.equals(groupId)) {
public void init(long version) {
if (this.version != 0) {
throw new BadArgumentException("Event wurde schon initialisiert. (" + type() + ")");
}
date = LocalDateTime.now();
log.trace("Event wurde initialisiert. (" + type() + "," + version + ")");
this.version = version;
}
public void apply(Group group, GroupCache cache) throws EventException {
log.trace("Event wird angewendet:\t{}", this);
if (version == 0) {
throw new BadArgumentException("Event wurde nicht initialisiert.");
}
checkGroupIdMatch(group.getId());
group.updateVersion(version);
applyEvent(group);
updateCache(cache, group); // Update erst nachdem apply keine exception geworfen hat
}
public void apply(Group group) throws EventException {
log.trace("Event wird angewendet:\t{}", this);
if (version == 0) {
throw new BadArgumentException("Event wurde nicht initialisiert.");
}
checkGroupIdMatch(group.getId());
group.updateVersion(version);
applyEvent(group);
}
private void checkGroupIdMatch(UUID groupid) throws IdMismatchException {
// CreateGroupEvents müssen die Id erst initialisieren
if (this instanceof CreateGroupEvent) {
return;
}
throw new GroupIdMismatchException(getClass().toString());
if (!this.groupid.equals(groupid)) {
throw new IdMismatchException("Das Event gehört zu einer anderen Gruppe");
}
}
protected abstract void updateCache(GroupCache cache, Group group);
protected abstract void applyEvent(Group group) throws EventException;
@JsonIgnore
public abstract String format();
@JsonIgnore
public abstract String type();
}

View File

@ -0,0 +1,15 @@
package mops.gruppen2.domain.event;
public enum EventType {
ADDMEMBER,
CREATEGROUP,
DESTROYGROUP,
KICKMEMBER,
SETDESCRIPTION,
SETLINK,
SETLIMIT,
SETPARENT,
SETTITLE,
SETTYPE,
UPDATEROLE
}

View File

@ -0,0 +1,46 @@
package mops.gruppen2.domain.event;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.UUID;
/**
* Entfernt ein einzelnes Mitglied einer Gruppe.
*/
@Log4j2
@Value
@AllArgsConstructor
public class KickMemberEvent extends Event {
public KickMemberEvent(UUID groupId, String exec, String target) {
super(groupId, exec, target);
}
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.usersRemove(target, group);
}
@Override
protected void applyEvent(Group group) throws UserNotFoundException, LastAdminException {
group.kickMember(target);
log.trace("\t\t\t\t\tNeue Members: {}", group.getMembers());
}
@Override
public String format() {
return "Mitglied entfernt: " + target + ".";
}
@Override
public String type() {
return EventType.KICKMEMBER.toString();
}
}

View File

@ -0,0 +1,50 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.infrastructure.GroupCache;
import javax.validation.Valid;
import java.util.UUID;
/**
* Ändert nur die Gruppenbeschreibung.
*/
@Log4j2
@Value
@AllArgsConstructor
public class SetDescriptionEvent extends Event {
@JsonProperty("desc")
Description description;
public SetDescriptionEvent(UUID groupId, String exec, @Valid Description description) {
super(groupId, exec, null);
this.description = description;
}
@Override
protected void updateCache(GroupCache cache, Group group) {}
@Override
protected void applyEvent(Group group) throws NoAccessException {
group.setDescription(exec, description);
log.trace("\t\t\t\t\tNeue Beschreibung: {}", group.getDescription());
}
@Override
public String format() {
return "Beschreibung gesetzt: " + description + ".";
}
@Override
public String type() {
return EventType.SETDESCRIPTION.toString();
}
}

View File

@ -0,0 +1,50 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.infrastructure.GroupCache;
import javax.validation.Valid;
import java.util.UUID;
@Log4j2
@Value
@AllArgsConstructor
public class SetInviteLinkEvent extends Event {
@JsonProperty("link")
Link link;
public SetInviteLinkEvent(UUID groupId, String exec, @Valid Link link) {
super(groupId, exec, null);
this.link = link;
}
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.linksRemove(group.getLink());
cache.linksPut(link.getValue(), group);
}
@Override
protected void applyEvent(Group group) throws NoAccessException {
group.setLink(exec, link);
log.trace("\t\t\t\t\tNeuer Link: {}", group.getLink());
}
@Override
public String format() {
return "Einladungslink gesetzt: " + link + ".";
}
@Override
public String type() {
return EventType.SETLINK.toString();
}
}

View File

@ -0,0 +1,48 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.infrastructure.GroupCache;
import javax.validation.Valid;
import java.util.UUID;
@Log4j2
@Value
@AllArgsConstructor
public class SetLimitEvent extends Event {
@JsonProperty("limit")
Limit limit;
public SetLimitEvent(UUID groupId, String exec, @Valid Limit limit) {
super(groupId, exec, null);
this.limit = limit;
}
@Override
protected void updateCache(GroupCache cache, Group group) {}
@Override
protected void applyEvent(Group group) throws BadArgumentException, NoAccessException {
group.setLimit(exec, limit);
log.trace("\t\t\t\t\tNeues UserLimit: {}", group.getLimit());
}
@Override
public String format() {
return "Benutzerlimit gesetzt: " + limit + ".";
}
@Override
public String type() {
return EventType.SETLIMIT.toString();
}
}

View File

@ -0,0 +1,48 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.infrastructure.GroupCache;
import javax.validation.Valid;
import java.util.UUID;
@Log4j2
@Value
@AllArgsConstructor
public class SetParentEvent extends Event {
@JsonProperty("parent")
Parent parent;
public SetParentEvent(UUID groupId, String exec, @Valid Parent parent) {
super(groupId, exec, null);
this.parent = parent;
}
@Override
protected void updateCache(GroupCache cache, Group group) {}
@Override
protected void applyEvent(Group group) throws NoAccessException, BadArgumentException {
group.setParent(exec, parent);
log.trace("\t\t\t\t\tNeues Parent: {}", group.getParent());
}
@Override
public String format() {
return "Veranstaltungszugehörigkeit gesetzt: " + parent.getValue() + ".";
}
@Override
public String type() {
return EventType.SETPARENT.toString();
}
}

View File

@ -0,0 +1,51 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.infrastructure.GroupCache;
import javax.validation.Valid;
import java.util.UUID;
/**
* Ändert nur den Gruppentitel.
*/
@Log4j2
@Value
@AllArgsConstructor
public class SetTitleEvent extends Event {
@JsonProperty("title")
Title title;
public SetTitleEvent(UUID groupId, String exec, @Valid Title title) {
super(groupId, exec, null);
this.title = title;
}
@Override
protected void updateCache(GroupCache cache, Group group) {}
@Override
protected void applyEvent(Group group) throws NoAccessException {
group.setTitle(exec, title);
log.trace("\t\t\t\t\tNeuer Titel: {}", group.getTitle());
}
@Override
public String format() {
return "Titel gesetzt: " + title + ".";
}
@Override
public String type() {
return EventType.SETTITLE.toString();
}
}

View File

@ -0,0 +1,57 @@
package mops.gruppen2.domain.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.NonFinal;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.infrastructure.GroupCache;
import javax.validation.Valid;
import java.util.UUID;
@Log4j2
@Value
@AllArgsConstructor
public class SetTypeEvent extends Event {
@JsonProperty("type")
Type type;
//TODO: blöder hack, das soll eigentlich anders gehen
// Problem ist, dass die Gruppe vor dem Cache verändert wird, also kann der cache den alten Typ
// nicht mehr aus der Gruppe holen
@NonFinal
Type oldType;
public SetTypeEvent(UUID groupId, String exec, @Valid Type type) {
super(groupId, exec, null);
this.type = type;
}
@Override
protected void updateCache(GroupCache cache, Group group) {
cache.typesRemove(oldType, group);
cache.typesPut(type, group);
}
@Override
protected void applyEvent(Group group) throws EventException {
oldType = group.getType();
group.setType(exec, type);
}
@Override
public String format() {
return "Gruppentype gesetzt: " + type + ".";
}
@Override
public String type() {
return EventType.SETTYPE.toString();
}
}

View File

@ -1,32 +0,0 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.exception.BadParameterException;
import java.util.UUID;
/**
* Ändert nur die Gruppenbeschreibung.
*/
@Getter
@NoArgsConstructor // For Jackson
public class UpdateGroupDescriptionEvent extends Event {
private String newGroupDescription;
public UpdateGroupDescriptionEvent(UUID groupId, String userId, String newGroupDescription) {
super(groupId, userId);
this.newGroupDescription = newGroupDescription.trim();
}
@Override
protected void applyEvent(Group group) {
if (newGroupDescription.isEmpty()) {
throw new BadParameterException("Die Beschreibung ist leer.");
}
group.setDescription(newGroupDescription);
}
}

View File

@ -1,33 +0,0 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.exception.BadParameterException;
import java.util.UUID;
/**
* Ändert nur den Gruppentitel.
*/
@Getter
@NoArgsConstructor // For Jackson
public class UpdateGroupTitleEvent extends Event {
private String newGroupTitle;
public UpdateGroupTitleEvent(UUID groupId, String userId, String newGroupTitle) {
super(groupId, userId);
this.newGroupTitle = newGroupTitle.trim();
}
@Override
protected void applyEvent(Group group) {
if (newGroupTitle.isEmpty()) {
throw new BadParameterException("Der Titel ist leer.");
}
group.setTitle(newGroupTitle);
}
}

View File

@ -1,35 +1,51 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.Role;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.UUID;
/**
* Aktualisiert die Gruppenrolle eines Teilnehmers.
*/
@Getter
@NoArgsConstructor // For Jackson
@Log4j2
@Value
@AllArgsConstructor
public class UpdateRoleEvent extends Event {
private Role newRole;
@JsonProperty("role")
Role role;
public UpdateRoleEvent(UUID groupId, String userId, Role newRole) {
super(groupId, userId);
this.newRole = newRole;
public UpdateRoleEvent(UUID groupId, String exec, String target, Role role) {
super(groupId, exec, target);
this.role = role;
}
@Override
protected void applyEvent(Group group) throws UserNotFoundException {
if (group.getRoles().containsKey(userId)) {
group.getRoles().put(userId, newRole);
return;
}
protected void updateCache(GroupCache cache, Group group) {}
throw new UserNotFoundException(getClass().toString());
@Override
protected void applyEvent(Group group) throws UserNotFoundException, LastAdminException {
group.memberPutRole(target, role);
log.trace("\t\t\t\t\tNeue Admin: {}", group.getAdmins());
}
@Override
public String format() {
return "Mitgliedsrolle gesetzt: " + target + ": " + role + ".";
}
@Override
public String type() {
return EventType.UPDATEROLE.toString();
}
}

View File

@ -1,30 +0,0 @@
package mops.gruppen2.domain.event;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.Group;
import mops.gruppen2.domain.exception.BadParameterException;
import mops.gruppen2.domain.exception.EventException;
import java.util.UUID;
@Getter
@NoArgsConstructor
public class UpdateUserMaxEvent extends Event {
private Long userMaximum;
public UpdateUserMaxEvent(UUID groupId, String userId, Long userMaximum) {
super(groupId, userId);
this.userMaximum = userMaximum;
}
@Override
protected void applyEvent(Group group) throws EventException {
if (userMaximum <= 0 || userMaximum < group.getMembers().size()) {
throw new BadParameterException("Usermaximum zu klein.");
}
group.setUserMaximum(userMaximum);
}
}

View File

@ -0,0 +1,12 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class BadArgumentException extends EventException {
private static final long serialVersionUID = -6757742013238625595L;
public BadArgumentException(String info) {
super(HttpStatus.BAD_REQUEST, "Fehlerhafter Parameter.", info);
}
}

View File

@ -1,10 +0,0 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class BadParameterException extends EventException {
public BadParameterException(String info) {
super(HttpStatus.BAD_REQUEST, "Fehlerhafter Parameter angegeben!", info);
}
}

View File

@ -0,0 +1,13 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class BadPayloadException extends EventException {
private static final long serialVersionUID = -3978242017847155629L;
public BadPayloadException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Payload konnte nicht übersetzt werden.", info);
}
}

View File

@ -5,8 +5,10 @@ import org.springframework.web.server.ResponseStatusException;
public class EventException extends ResponseStatusException {
private static final long serialVersionUID = 6784052016028094340L;
public EventException(HttpStatus status, String msg, String info) {
super(status, msg + " (" + info + ")");
super(status, info.isBlank() ? "" : msg + " (" + info + ")");
}
}

View File

@ -4,8 +4,10 @@ import org.springframework.http.HttpStatus;
public class GroupFullException extends EventException {
private static final long serialVersionUID = -4011141160467668713L;
public GroupFullException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Die Gruppe hat die maximale Midgliederanzahl bereits erreicht!", info);
super(HttpStatus.INTERNAL_SERVER_ERROR, "Gruppe hat maximale Teilnehmeranzahl bereits erreicht.", info);
}
}

View File

@ -1,10 +0,0 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class GroupIdMismatchException extends EventException {
public GroupIdMismatchException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Falsche Gruppe für Event.", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class GroupNotFoundException extends EventException {
private static final long serialVersionUID = -4738218416842951106L;
public GroupNotFoundException(String info) {
super(HttpStatus.NOT_FOUND, "Gruppe wurde nicht gefunden.", info);
super(HttpStatus.NOT_FOUND, "Gruppe existiert nicht oder wurde gelöscht.", info);
}
}

View File

@ -0,0 +1,13 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class IdMismatchException extends EventException {
private static final long serialVersionUID = 7944077617758922089L;
public IdMismatchException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Ids stimmen nicht überein.", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class InvalidInviteException extends EventException {
private static final long serialVersionUID = 2643001101459427944L;
public InvalidInviteException(String info) {
super(HttpStatus.NOT_FOUND, "Der Einladungslink ist ungültig.", info);
super(HttpStatus.NOT_FOUND, "Einladungslink ist ungültig oder Gruppe wurde gelöscht.", info);
}
}

View File

@ -0,0 +1,12 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class LastAdminException extends EventException {
private static final long serialVersionUID = 9059481382346544288L;
public LastAdminException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Gruppe braucht mindestens einen Admin.", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class NoAccessException extends EventException {
private static final long serialVersionUID = 1696988497122834654L;
public NoAccessException(String info) {
super(HttpStatus.FORBIDDEN, "Hier hast du leider keinen Zugriff!", info);
super(HttpStatus.FORBIDDEN, "Kein Zugriff.", info);
}
}

View File

@ -1,10 +0,0 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class NoAdminAfterActionException extends EventException {
public NoAdminAfterActionException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Nach dieser Aktion hätte die Gruppe keinen Admin mehr", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class NoInviteExistException extends EventException {
private static final long serialVersionUID = -8092076461455840693L;
public NoInviteExistException(String info) {
super(HttpStatus.NOT_FOUND, "Für diese Gruppe existiert kein Link.", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class PageNotFoundException extends EventException {
private static final long serialVersionUID = 2374509005158710104L;
public PageNotFoundException(String info) {
super(HttpStatus.NOT_FOUND, "Die Seite wurde nicht gefunden!", info);
super(HttpStatus.NOT_FOUND, "Seite wurde nicht gefunden.", info);
}
}

View File

@ -1,10 +0,0 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class UserAlreadyExistsException extends EventException {
public UserAlreadyExistsException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "Der User existiert bereits.", info);
}
}

View File

@ -0,0 +1,13 @@
package mops.gruppen2.domain.exception;
import org.springframework.http.HttpStatus;
public class UserExistsException extends EventException {
private static final long serialVersionUID = -8150634358760194625L;
public UserExistsException(String info) {
super(HttpStatus.INTERNAL_SERVER_ERROR, "User existiert bereits.", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class UserNotFoundException extends EventException {
private static final long serialVersionUID = 8347442921199785291L;
public UserNotFoundException(String info) {
super(HttpStatus.NOT_FOUND, "Der User wurde nicht gefunden.", info);
super(HttpStatus.NOT_FOUND, "User existiert nicht.", info);
}
}

View File

@ -4,7 +4,10 @@ import org.springframework.http.HttpStatus;
public class WrongFileException extends EventException {
private static final long serialVersionUID = -166192514348555116L;
public WrongFileException(String info) {
super(HttpStatus.BAD_REQUEST, "Die entsprechende Datei ist keine valide CSV-Datei!", info);
super(HttpStatus.BAD_REQUEST, "Datei ist keine valide CSV-Datei.", info);
}
}

View File

@ -0,0 +1,357 @@
package mops.gruppen2.domain.model.group;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.wrapper.Body;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.helper.CommonHelper;
import mops.gruppen2.domain.service.helper.SortHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Repräsentiert den aggregierten Zustand einer Gruppe.
*
* <p>
* Muss beim Start gesetzt werden: groupid, meta
*/
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Group {
// Metainformationen
@EqualsAndHashCode.Include
private UUID groupid;
@Getter
private Type type = Type.PRIVATE;
private Parent parent = Parent.EMPTY();
private Limit limit = Limit.DEFAULT(); // Add initial user
private Link link = Link.RANDOM();
private GroupMeta meta = GroupMeta.EMPTY();
//TODO: UI set + use for options
private final GroupOptions options = GroupOptions.DEFAULT();
// Inhalt
private Title title = Title.EMPTY();
private Description description = Description.EMPTY();
//TODO: Asciidoc description
private Body body;
// Integrationen
// Teilnehmer
private final Map<String, Membership> memberships = new HashMap<>();
// ####################################### Members ###########################################
public List<User> getMembers() {
return SortHelper.sortByMemberRole(new ArrayList<>(memberships.values())).stream()
.map(Membership::getUser)
.collect(Collectors.toList());
}
public List<User> getRegulars() {
return memberships.values().stream()
.map(Membership::getUser)
.filter(member -> isRegular(member.getId()))
.collect(Collectors.toList());
}
public List<User> getAdmins() {
return memberships.values().stream()
.map(Membership::getUser)
.filter(member -> isAdmin(member.getId()))
.collect(Collectors.toList());
}
public Role getRole(String userid) {
return memberships.get(userid).getRole();
}
public void addMember(String target, User user) throws UserExistsException, GroupFullException {
ValidationHelper.throwIfMember(this, target);
ValidationHelper.throwIfGroupFull(this);
memberships.put(target, new Membership(user, Role.REGULAR));
}
public void kickMember(String target) throws UserNotFoundException, LastAdminException {
ValidationHelper.throwIfNoMember(this, target);
ValidationHelper.throwIfLastAdmin(this, target);
memberships.remove(target);
}
public boolean memberHasRole(String target, Role role) {
ValidationHelper.throwIfNoMember(this, target);
return memberships.get(target).getRole() == role;
}
public void memberPutRole(String target, Role role) throws UserNotFoundException, LastAdminException {
ValidationHelper.throwIfNoMember(this, target);
if (role == Role.REGULAR) {
ValidationHelper.throwIfLastAdmin(this, target);
}
memberships.put(target, memberships.get(target).setRole(role));
}
public boolean isMember(String target) {
return memberships.containsKey(target);
}
public boolean isAdmin(String target) throws UserNotFoundException {
ValidationHelper.throwIfNoMember(this, target);
return memberships.get(target).getRole() == Role.ADMIN;
}
public boolean isRegular(String target) throws UserNotFoundException {
ValidationHelper.throwIfNoMember(this, target);
return memberships.get(target).getRole() == Role.REGULAR;
}
// ######################################### Getters #########################################
public UUID getId() {
return groupid;
}
public UUID getParent() {
return parent.getValue();
}
public long getLimit() {
return limit.getValue();
}
public String getTitle() {
return title.toString();
}
public String getDescription() {
return description.getValue();
}
public String getLink() {
return link.getValue();
}
public String creator() {
return meta.getCreator();
}
public long version() {
return meta.getVersion();
}
public LocalDateTime creationDate() {
return meta.getCreationDate();
}
public int size() {
return memberships.size();
}
public boolean isFull() {
return size() >= limit.getValue();
}
public boolean isEmpty() {
return size() == 0;
}
public boolean exists() {
return groupid != null && !CommonHelper.uuidIsEmpty(groupid);
}
public boolean isPublic() {
return type == Type.PUBLIC;
}
public boolean isPrivate() {
return type == Type.PRIVATE;
}
public boolean isLecture() {
return type == Type.LECTURE;
}
public boolean hasParent() {
return !parent.isEmpty();
}
public boolean hasMaterial() {
return options.isHasMaterialIntegration();
}
public boolean hasForums() {
return options.isHasForumsIntegration();
}
public boolean hasCalendar() {
return options.isHasTermineIntegration();
}
public boolean hasModules() {
return options.isHasModulesIntegration();
}
public boolean hasPortfolios() {
return options.isHasPortfolioIntegration();
}
// ######################################## Setters ##########################################
public void setId(UUID groupid) throws BadArgumentException {
if (this.groupid != null) {
throw new BadArgumentException("GruppenId bereits gesetzt.");
}
this.groupid = groupid;
}
public void setType(String exec, Type type) throws NoAccessException {
ValidationHelper.throwIfNoAdmin(this, exec);
this.type = type;
}
public void setTitle(String exec, @Valid Title title) throws NoAccessException {
ValidationHelper.throwIfNoAdmin(this, exec);
this.title = title;
}
public void setDescription(String exec, @Valid Description description) throws NoAccessException {
ValidationHelper.throwIfNoAdmin(this, exec);
this.description = description;
}
public void setLimit(String exec, @Valid Limit limit) throws NoAccessException, BadArgumentException {
ValidationHelper.throwIfNoAdmin(this, exec);
if (size() > limit.getValue()) {
throw new BadArgumentException("Das Userlimit ist zu klein für die Gruppe.");
}
this.limit = limit;
}
public void setParent(String exec, @Valid Parent parent) throws NoAccessException, BadArgumentException {
ValidationHelper.throwIfNoAdmin(this, exec);
if (parent.getValue().equals(groupid)) {
throw new BadArgumentException("Die Gruppe kann nicht zu sich selbst gehören!");
}
this.parent = parent;
}
public void setLink(String exec, @Valid Link link) throws NoAccessException {
ValidationHelper.throwIfNoAdmin(this, exec);
if (link.getValue().equals(groupid.toString())) {
throw new BadArgumentException("Link kann nicht der GruppenID entsprechen.");
}
this.link = link;
}
public void updateVersion(long version) throws IdMismatchException {
meta = meta.setVersion(version);
}
public void setCreator(String target) throws BadArgumentException {
meta = meta.setCreator(target);
}
public void setCreationDate(LocalDateTime date) throws BadArgumentException {
meta = meta.setCreationDate(date);
}
// ######################################### Util ############################################
public void destroy(String userid) throws NoAccessException {
if (!isEmpty()) {
ValidationHelper.throwIfNoAdmin(this, userid);
}
groupid = null;
// Wenn man alles null setzt hat der cache mehr arbeit, weil dieser erst nach der löschung
// geupdated wird und sich link und mitgliedschaften selber heraussuchen muss
/*type = null;
parent = null;
limit = null;
link = null;
meta = null;
options = null;
title = null;
description = null;
body = null;
memberships = null;*/
}
public String format() {
return title + " - " + description;
}
@Override
public String toString() {
return "group("
+ (groupid == null ? "groupid: null" : groupid.toString())
+ ", "
+ (parent == null ? "parent: null" : parent.toString())
+ ", "
+ (meta == null ? "meta: null" : meta.toString())
+ ")";
}
public static Group EMPTY() {
return new Group();
}
public long getVersion() {
return meta.getVersion();
}
}

View File

@ -0,0 +1,50 @@
package mops.gruppen2.domain.model.group;
import lombok.ToString;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.IdMismatchException;
import java.time.LocalDateTime;
@Log4j2
@Value
@ToString
class GroupMeta {
long version;
String creator;
LocalDateTime creationDate;
GroupMeta setVersion(long version) throws IdMismatchException {
if (this.version >= version) {
throw new IdMismatchException("Die Gruppe ist bereits auf einem neueren Stand.");
}
if (this.version + 1 != version) {
throw new IdMismatchException("Es fehlen vorherige Events.");
}
return new GroupMeta(version, creator, creationDate);
}
GroupMeta setCreator(String userid) throws BadArgumentException {
if (creator != null) {
throw new BadArgumentException("Gruppe hat schon einen Ersteller.");
}
return new GroupMeta(version, userid, creationDate);
}
GroupMeta setCreationDate(LocalDateTime date) throws BadArgumentException {
if (creationDate != null) {
throw new BadArgumentException("Gruppe hat schon ein Erstellungsdatum.");
}
return new GroupMeta(version, creator, date);
}
static GroupMeta EMPTY() {
return new GroupMeta(0, null, null);
}
}

View File

@ -0,0 +1,40 @@
package mops.gruppen2.domain.model.group;
import lombok.Value;
//TODO: doooooodododo
@Value
class GroupOptions {
// Gruppe
boolean showClearname;
boolean hasBody;
boolean isLeavable;
boolean hasLink;
String customLogo;
String customBackground;
String customTitle;
// Integrations
boolean hasMaterialIntegration;
boolean hasTermineIntegration;
boolean hasPortfolioIntegration;
boolean hasForumsIntegration;
boolean hasModulesIntegration;
static GroupOptions DEFAULT() {
return new GroupOptions(true,
false,
true,
false,
null,
null,
null,
true,
true,
true,
true,
true);
}
}

View File

@ -0,0 +1,23 @@
package mops.gruppen2.domain.model.group;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Membership {
User user;
Role role;
// LocalDateTime age;
@Override
public String toString() {
return user.format() + ": " + role;
}
public Membership setRole(Role role) {
return new Membership(user, role);
}
}

View File

@ -0,0 +1,10 @@
package mops.gruppen2.domain.model.group;
public enum Role {
ADMIN,
REGULAR;
public Role toggle() {
return this == ADMIN ? REGULAR : ADMIN;
}
}

View File

@ -0,0 +1,7 @@
package mops.gruppen2.domain.model.group;
public enum Type {
PUBLIC,
PRIVATE,
LECTURE
}

View File

@ -0,0 +1,64 @@
package mops.gruppen2.domain.model.group;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Value;
import lombok.extern.log4j.Log4j2;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
@Log4j2
@Value
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@AllArgsConstructor
public class User {
@EqualsAndHashCode.Include
@Getter(AccessLevel.NONE)
@JsonProperty("id")
String userid;
@JsonProperty("givenname")
String givenname;
@JsonProperty("familyname")
String familyname;
@JsonProperty("email")
String email;
public User(KeycloakAuthenticationToken token) {
KeycloakPrincipal principal = (KeycloakPrincipal) token.getPrincipal();
userid = principal.getName();
givenname = principal.getKeycloakSecurityContext().getIdToken().getGivenName();
familyname = principal.getKeycloakSecurityContext().getIdToken().getFamilyName();
email = principal.getKeycloakSecurityContext().getIdToken().getEmail();
}
/**
* User identifizieren sich über die Id, mehr wird also manchmal nicht benötigt.
*
* @param userid Die User Id
*/
public User(String userid) {
this.userid = userid;
givenname = "";
familyname = "";
email = "";
}
public String getId() {
return userid;
}
public String format() {
return givenname + " " + familyname;
}
public boolean isMember(Group group) {
return group.getMembers().contains(this);
}
}

View File

@ -0,0 +1,8 @@
package mops.gruppen2.domain.model.group.wrapper;
import lombok.Value;
//TODO: do it
@Value
public class Body {
}

View File

@ -0,0 +1,27 @@
package mops.gruppen2.domain.model.group.wrapper;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Value
public class Description {
@NotNull
@Size(min = 4, max = 512)
@JsonProperty("value")
String value;
@Override
public String toString() {
return value;
}
@JsonIgnore
public static Description EMPTY() {
return new Description("EMPTY");
}
}

View File

@ -0,0 +1,27 @@
package mops.gruppen2.domain.model.group.wrapper;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@Value
public class Limit {
@NotNull
@Min(1)
@Max(999_999)
@JsonProperty("value")
long value;
public static Limit DEFAULT() {
return new Limit(1);
}
@Override
public String toString() {
return String.valueOf(value);
}
}

View File

@ -0,0 +1,27 @@
package mops.gruppen2.domain.model.group.wrapper;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.UUID;
@Value
public class Link {
@NotNull
@JsonProperty("value")
UUID value;
public Link(String value) {
this.value = UUID.fromString(value);
}
public static Link RANDOM() {
return new Link(UUID.randomUUID().toString());
}
public String getValue() {
return value.toString();
}
}

View File

@ -0,0 +1,36 @@
package mops.gruppen2.domain.model.group.wrapper;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.ToString;
import lombok.Value;
import mops.gruppen2.domain.service.helper.CommonHelper;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.beans.ConstructorProperties;
import java.util.UUID;
@Value
@ToString
public class Parent {
@NotNull
@JsonProperty("id")
UUID value;
@ConstructorProperties("id")
public Parent(@NotBlank @Size(min = 36, max = 36) String parentid) {
value = UUID.fromString(parentid);
}
public static Parent EMPTY() {
return new Parent("00000000-0000-0000-0000-000000000000");
}
@JsonIgnore
public boolean isEmpty() {
return CommonHelper.uuidIsEmpty(value);
}
}

View File

@ -0,0 +1,27 @@
package mops.gruppen2.domain.model.group.wrapper;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Value
public class Title {
@NotNull
@Size(min = 4, max = 128)
@JsonProperty("value")
String value;
@Override
public String toString() {
return value;
}
@JsonIgnore
public static Title EMPTY() {
return new Title("EMPTY");
}
}

View File

@ -0,0 +1,130 @@
package mops.gruppen2.domain.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.exception.BadPayloadException;
import mops.gruppen2.domain.service.helper.CommonHelper;
import mops.gruppen2.domain.service.helper.FileHelper;
import mops.gruppen2.persistance.EventRepository;
import mops.gruppen2.persistance.dto.EventDTO;
import org.springframework.stereotype.Service;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Log4j2
@RequiredArgsConstructor
@Service
public class EventStoreService {
private final EventRepository eventStore;
//########################################### SAVE ###########################################
/**
* Erzeugt ein DTO aus einem Event und speicher es.
*
* @param event Event, welches gespeichert wird
*/
public void saveEvent(Event event) {
eventStore.save(getDTOFromEvent(event));
}
public void saveAll(Event... events) {
for (Event event : events) {
eventStore.save(getDTOFromEvent(event));
}
}
//########################################### DTOs ###########################################
/**
* Erzeugt aus einem Event Objekt ein EventDTO Objekt.
*
* @param event Event, welches in DTO übersetzt wird
*
* @return EventDTO (Neues DTO)
*/
private static EventDTO getDTOFromEvent(Event event) {
try {
String payload = FileHelper.serializeEventJson(event);
return new EventDTO(null,
event.getGroupid().toString(),
event.getVersion(),
event.getExec(),
event.getTarget(),
Timestamp.valueOf(event.getDate()),
payload);
} catch (JsonProcessingException e) {
log.error("Event ({}) konnte nicht serialisiert werden!", event, e);
throw new BadPayloadException(EventStoreService.class.toString());
}
}
/**
* Erzeugt aus einer Liste von eventDTOs eine Liste von Events.
*
* @param eventDTOS Liste von DTOs
*
* @return Liste von Events
*/
private static List<Event> getEventsFromDTOs(List<EventDTO> eventDTOS) {
return eventDTOS.stream()
.map(EventStoreService::getEventFromDTO)
.collect(Collectors.toList());
}
private static Event getEventFromDTO(EventDTO dto) {
try {
return FileHelper.deserializeEventJson(dto.getEvent_payload());
} catch (JsonProcessingException e) {
log.error("Payload {} konnte nicht deserialisiert werden!", dto.getEvent_payload(), e);
throw new BadPayloadException(EventStoreService.class.toString());
}
}
// #################################### SIMPLE QUERIES #######################################
public List<Event> findAllEvents() {
return getEventsFromDTOs(eventStore.findAllEvents());
}
public List<Event> findGroupEvents(UUID groupId) {
return getEventsFromDTOs(eventStore.findGroupEvents(groupId.toString()));
}
public List<Event> findGroupEvents(List<UUID> ids) {
return ids.stream()
.map(id -> eventStore.findGroupEvents(id.toString()))
.map(EventStoreService::getEventsFromDTOs)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
public List<String> findGroupPayloads(UUID groupId) {
return eventStore.findGroupPayloads(groupId.toString());
}
public List<EventDTO> findGroupDTOs(UUID groupid) {
return eventStore.findGroupEvents(groupid.toString());
}
public List<UUID> findChangedGroups(long eventid) {
return CommonHelper.stringsToUUID(eventStore.findChangedGroupIds(eventid));
}
public long findMaxEventId() {
return eventStore.findMaxEventId();
}
}

View File

@ -0,0 +1,280 @@
package mops.gruppen2.domain.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.AddMemberEvent;
import mops.gruppen2.domain.event.CreateGroupEvent;
import mops.gruppen2.domain.event.DestroyGroupEvent;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.event.KickMemberEvent;
import mops.gruppen2.domain.event.SetDescriptionEvent;
import mops.gruppen2.domain.event.SetInviteLinkEvent;
import mops.gruppen2.domain.event.SetLimitEvent;
import mops.gruppen2.domain.event.SetParentEvent;
import mops.gruppen2.domain.event.SetTitleEvent;
import mops.gruppen2.domain.event.SetTypeEvent;
import mops.gruppen2.domain.event.UpdateRoleEvent;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Link;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.springframework.stereotype.Service;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Behandelt Aufgaben, welche sich auf eine Gruppe beziehen.
* Es werden übergebene Gruppen bearbeitet und dementsprechend Events erzeugt und gespeichert.
*/
@Log4j2
@RequiredArgsConstructor
@Service
public class GroupService {
private final GroupCache groupCache;
private final EventStoreService eventStoreService;
// ################################# GRUPPE ERSTELLEN ########################################
public Group createGroup(String exec) {
return createGroup(UUID.randomUUID(), exec, LocalDateTime.now());
}
public void initGroupMembers(Group group,
String exec,
String target,
User user,
Limit limit) {
addMember(group, exec, target, user);
updateRole(group, exec, target, Role.ADMIN);
setLimit(group, exec, limit);
}
public void initGroupMeta(Group group,
String exec,
Type type,
Parent parent) {
setType(group, exec, type);
setParent(group, exec, parent);
}
public void initGroupText(Group group,
String exec,
Title title,
Description description) {
setTitle(group, exec, title);
setDescription(group, exec, description);
}
// ################################### GRUPPEN ÄNDERN ########################################
/**
* Fügt eine Liste von Usern zu einer Gruppe hinzu.
* Duplikate werden übersprungen, die erzeugten Events werden gespeichert.
* Dabei wird das Teilnehmermaximum eventuell angehoben.
* Prüft, ob der User Admin ist.
*
* @param newUsers Userliste
* @param group Gruppe
* @param exec Ausführender User
*/
public void addUsersToGroup(Group group, String exec, List<User> newUsers) {
List<User> users = newUsers.stream().distinct().collect(Collectors.toUnmodifiableList());
setLimit(group, exec, getAdjustedUserLimit(users, group));
users.forEach(newUser -> addUserSilent(group, exec, newUser.getId(), newUser));
}
/**
* Ermittelt ein passendes Teilnehmermaximum.
* Reicht das alte Maximum, wird dieses zurückgegeben.
* Ansonsten wird ein erhöhtes Maximum zurückgegeben.
*
* @param newUsers Neue Teilnehmer
* @param group Bestehende Gruppe, welche verändert wird
*
* @return Das neue Teilnehmermaximum
*/
private static Limit getAdjustedUserLimit(List<User> newUsers, Group group) {
return new Limit(Math.max((long) group.size() + newUsers.size(), group.getLimit()));
}
/**
* Wechselt die Rolle eines Teilnehmers von Admin zu Member oder andersherum.
* Überprüft, ob der User Mitglied ist und ob er der letzte Admin ist.
*
* @param target Teilnehmer, welcher geändert wird
* @param group Gruppe, in welcher sih der Teilnehmer befindet
*
* @throws EventException Falls der User nicht gefunden wird
*/
public void toggleMemberRole(Group group, String exec, String target) {
ValidationHelper.throwIfNoMember(group, target);
updateRole(group, exec, target, group.getRole(target).toggle());
}
// ################################# SINGLE EVENTS ###########################################
// Spezifische Events werden erzeugt, validiert, auf die Gruppe angewandt und gespeichert
/**
* Erzeugt eine Gruppe, speichert diese und gibt diese zurück.
*/
private Group createGroup(UUID groupid, String exec, LocalDateTime date) {
Event event = new CreateGroupEvent(groupid,
exec,
date);
Group group = Group.EMPTY();
applyAndSave(group, event);
return group;
}
/**
* Dasselbe wie addUser(), aber exceptions werden abgefangen und nicht geworfen.
*/
private void addUserSilent(Group group, String exec, String target, User user) {
try {
addMember(group, exec, target, user);
} catch (Exception e) {
log.debug("Doppelter User {} wurde nicht zu Gruppe {} hinzugefügt!", user, group);
}
}
/**
* Erzeugt, speichert ein AddUserEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer schon Mitglied ist und ob Gruppe voll ist.
*/
public void addMember(Group group, String exec, String target, User user) {
applyAndSave(group, new AddMemberEvent(group.getId(), exec, target, user));
}
/**
* Erzeugt, speichert ein DeleteUserEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer Mitglied ist und ob er der letzte Admin ist.
*/
public void kickMember(Group group, String exec, String target) {
applyAndSave(group, new KickMemberEvent(group.getId(), exec, target));
if (group.isEmpty()) {
deleteGroup(group, exec);
}
}
/**
* Erzeugt, speichert ein DeleteGroupEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer Admin ist.
*/
public void deleteGroup(Group group, String exec) {
if (!group.exists()) {
return;
}
applyAndSave(group, new DestroyGroupEvent(group.getId(), exec));
}
/**
* Erzeugt, speichert ein UpdateTitleEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer Admin ist und ob der Titel valide ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
public void setTitle(Group group, String exec, @Valid Title title) {
if (group.getTitle().equals(title.getValue())) {
return;
}
applyAndSave(group, new SetTitleEvent(group.getId(), exec, title));
}
/**
* Erzeugt, speichert ein UpdateDescriptiopnEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer Admin ist und ob die Beschreibung valide ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
public void setDescription(Group group, String exec, @Valid Description description) {
if (group.getDescription().equals(description.getValue())) {
return;
}
applyAndSave(group, new SetDescriptionEvent(group.getId(), exec, description));
}
/**
* Erzeugt, speichert ein UpdateRoleEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer Mitglied ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
private void updateRole(Group group, String exec, String target, Role role) {
if (group.memberHasRole(target, role)) {
return;
}
applyAndSave(group, new UpdateRoleEvent(group.getId(), exec, target, role));
}
/**
* Erzeugt, speichert ein UpdateUserLimitEvent und wendet es auf eine Gruppe an.
* Prüft, ob der Nutzer Admin ist und ob das Limit valide ist.
* Bei keiner Änderung wird nichts erzeugt.
*/
public void setLimit(Group group, String exec, @Valid Limit userLimit) {
if (userLimit.getValue() == group.getLimit()) {
return;
}
applyAndSave(group, new SetLimitEvent(group.getId(), exec, userLimit));
}
public void setParent(Group group, String exec, Parent parent) {
if (parent.getValue() == group.getParent()) {
return;
}
applyAndSave(group, new SetParentEvent(group.getId(), exec, parent));
}
//TODO: UI Link regenerieren button
public void setLink(Group group, String exec, @Valid Link link) {
if (group.getLink().equals(link.getValue())) {
return;
}
applyAndSave(group, new SetInviteLinkEvent(group.getId(), exec, link));
}
private void setType(Group group, String exec, Type type) {
if (group.getType() == type) {
return;
}
applyAndSave(group, new SetTypeEvent(group.getId(), exec, type));
}
private void applyAndSave(Group group, Event event) throws EventException {
event.init(group.version() + 1);
event.apply(group, groupCache);
eventStoreService.saveEvent(event);
}
}

View File

@ -0,0 +1,68 @@
package mops.gruppen2.domain.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.infrastructure.GroupCache;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Log4j2
public class SearchService {
private final GroupCache groupCache;
/**
* Filtert alle öffentliche Gruppen nach dem Suchbegriff und gibt diese als sortierte Liste zurück.
* Groß- und Kleinschreibung wird nicht beachtet.
* Der Suchbegriff wird im Gruppentitel und in der Beschreibung gesucht.
*
* @param search Der Suchstring
*
* @return Liste von projizierten Gruppen
*
* @throws EventException Projektionsfehler
*/
public List<Group> searchString(String search, String principal) {
List<Group> groups = new ArrayList<>();
groups.addAll(groupCache.publics());
groups.addAll(groupCache.lectures());
groups = removeUserGroups(groups, principal);
if (search.isEmpty()) {
return groups;
}
log.debug("Es wurde gesucht nach: {}", search);
return groups.stream()
.filter(group -> group.format().toLowerCase().contains(search.toLowerCase()))
.collect(Collectors.toList());
}
public List<Group> searchType(Type type, String principal) {
log.debug("Es wurde gesucht nach: {}", type);
if (type == Type.LECTURE) {
return removeUserGroups(groupCache.lectures(), principal);
}
if (type == Type.PUBLIC) {
return removeUserGroups(groupCache.publics(), principal);
}
return Collections.emptyList();
}
private static List<Group> removeUserGroups(List<Group> groups, String principal) {
return groups.stream()
.filter(group -> !group.isMember(principal))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,26 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.api.GroupRequestWrapper;
import mops.gruppen2.infrastructure.api.GroupWrapper;
import java.util.List;
import java.util.stream.Collectors;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class APIHelper {
public static GroupRequestWrapper wrap(long status, List<Group> groupList) {
return new GroupRequestWrapper(status, wrap(groupList));
}
public static List<GroupWrapper> wrap(List<Group> groups) {
return groups.stream()
.map(GroupWrapper::new)
.collect(Collectors.toUnmodifiableList());
}
}

View File

@ -0,0 +1,24 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CommonHelper {
public static boolean uuidIsEmpty(UUID uuid) {
return "00000000-0000-0000-0000-000000000000".equals(uuid.toString());
}
public static List<UUID> stringsToUUID(List<String> changedGroupIds) {
return changedGroupIds.stream()
.map(UUID::fromString)
.collect(Collectors.toUnmodifiableList());
}
}

View File

@ -0,0 +1,148 @@
package mops.gruppen2.domain.service.helper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.exception.EventException;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.exception.WrongFileException;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.persistance.dto.EventDTO;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class FileHelper {
// ######################################## CSV #############################################
public static List<User> readCsvFile(MultipartFile file) throws EventException {
if (file == null || file.isEmpty()) {
return Collections.emptyList();
}
try {
List<User> userList = readCsv(file.getInputStream());
return userList.stream()
.distinct()
.collect(Collectors.toList()); //filter duplicates from list
} catch (IOException e) {
log.error("File konnte nicht gelesen werden!", e);
throw new WrongFileException(file.getOriginalFilename());
}
}
private static List<User> readCsv(InputStream stream) throws IOException {
CsvMapper mapper = new CsvMapper();
CsvSchema schema = mapper.schemaFor(User.class).withHeader().withColumnReordering(true);
ObjectReader reader = mapper.readerFor(User.class).with(schema);
return reader.<User>readValues(stream).readAll();
}
public static String writeCsvUserList(List<User> members) {
StringBuilder builder = new StringBuilder();
builder.append("id,givenname,familyname,email\n");
members.forEach(user -> builder.append(user.getId())
.append(",")
.append(user.getGivenname())
.append(",")
.append(user.getFamilyname())
.append(",")
.append(user.getEmail())
.append("\n"));
return builder.toString();
}
// ########################################## JSON ###########################################
/**
* Übersetzt eine Java-Event-Repräsentation zu einem JSON-Event-Payload.
*
* @param event Java-Event-Repräsentation
*
* @return JSON-Event-Payload als String
*
* @throws JsonProcessingException Bei JSON Fehler
*/
public static String serializeEventJson(Event event) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
String payload = mapper.writeValueAsString(event);
log.trace(payload);
return payload;
}
/**
* Übersetzt eine JSON-Event-Payload zu einer Java-Event-Repräsentation.
*
* @param json JSON-Event-Payload als String
*
* @return Java-Event-Repräsentation
*
* @throws JsonProcessingException Bei JSON Fehler
*/
public static Event deserializeEventJson(String json) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
Event event = mapper.readValue(json, Event.class);
log.trace(event);
return event;
}
// ############################################### TXT #######################################
public static String payloadsToPlain(List<String> payloads) {
return payloads.stream()
.map(payload -> payload + "\n")
.reduce((String payloadA, String payloadB) -> payloadA + payloadB)
.orElseThrow(() -> new GroupNotFoundException("Keine Payloads gefunden."));
}
public static String eventDTOsToSql(List<EventDTO> dtos) {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO event(group_id, group_version, exec_id, target_id, event_date, event_payload)\nVALUES\n");
dtos.forEach(dto -> builder.append("('")
.append(dto.getGroup_id())
.append("','")
.append(dto.getGroup_version())
.append("','")
.append(dto.getExec_id())
.append("','")
.append(dto.getTarget_id())
.append("','")
.append(dto.getEvent_date())
.append("','")
.append(dto.getEvent_payload())
.append("'),\n"));
builder.replace(builder.length() - 2, builder.length(), ";");
return builder.toString();
}
// ############################################### SQL #######################################
}

View File

@ -0,0 +1,67 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.event.Event;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.infrastructure.GroupCache;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Liefert verschiedene Projektionen auf Gruppen.
* Benötigt ausschließlich den EventStoreService.
*/
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ProjectionHelper {
public static List<Group> project(List<Event> events) {
Map<UUID, Group> groups = new HashMap<>();
if (events.isEmpty()) {
return Collections.emptyList();
}
log.trace(groups);
log.trace(events);
events.forEach(event -> event.apply(getOrCreateGroup(groups, event.getGroupid())));
return new ArrayList<>(groups.values());
}
public static void project(Map<UUID, Group> groups, List<Event> events, GroupCache cache) {
if (events.isEmpty()) {
return;
}
log.trace(groups);
log.trace(events);
events.forEach(event -> event.apply(getOrCreateGroup(groups, event.getGroupid()), cache));
}
/**
* Gibt die Gruppe mit der richtigen Id aus der übergebenen Map wieder, existiert diese nicht
* wird die Gruppe erstellt und der Map hizugefügt.
*
* @param groups Map aus GruppenIds und Gruppen
* @param groupId Die Id der Gruppe, die zurückgegeben werden soll
*
* @return Die gesuchte Gruppe
*/
private static Group getOrCreateGroup(Map<UUID, Group> groups, UUID groupId) {
if (!groups.containsKey(groupId)) {
groups.put(groupId, Group.EMPTY());
}
return groups.get(groupId);
}
}

View File

@ -0,0 +1,27 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import mops.gruppen2.domain.model.group.Membership;
import mops.gruppen2.domain.model.group.Role;
import java.util.List;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SortHelper {
public static List<Membership> sortByMemberRole(List<Membership> memberships) {
memberships.sort((Membership m1, Membership m2) -> {
if (m1.getRole() == Role.ADMIN) {
return -1;
}
if (m2.getRole() == Role.ADMIN) {
return 1;
}
return 0;
});
return memberships;
}
}

View File

@ -0,0 +1,84 @@
package mops.gruppen2.domain.service.helper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.BadArgumentException;
import mops.gruppen2.domain.exception.GroupFullException;
import mops.gruppen2.domain.exception.LastAdminException;
import mops.gruppen2.domain.exception.NoAccessException;
import mops.gruppen2.domain.exception.UserExistsException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
@Log4j2
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ValidationHelper {
public static boolean checkIfLastMember(Group group, String userid) {
return group.isMember(userid) && group.size() == 1;
}
/**
* Überprüft, ob ein User in einer Gruppe Admin ist.
*/
public static boolean checkIfAdmin(Group group, String userid) {
if (group.isMember(userid)) {
return group.isAdmin(userid);
}
return false;
}
public static boolean checkIfLastAdmin(Group group, String userid) {
return checkIfAdmin(group, userid) && group.getAdmins().size() == 1;
}
// ######################################## THROW ############################################
public static void throwIfMember(Group group, String userid) throws UserExistsException {
if (group.isMember(userid)) {
log.error("Benutzer {} ist schon in Gruppe {}", userid, group);
throw new UserExistsException(userid);
}
}
public static void throwIfNoMember(Group group, String userid) throws UserNotFoundException {
if (!group.isMember(userid)) {
log.error("Benutzer {} ist nicht in Gruppe {}!", userid, group);
throw new UserNotFoundException(userid);
}
}
public static void throwIfNoAdmin(Group group, String userid) throws NoAccessException {
if (!checkIfAdmin(group, userid)) {
log.error("User {} ist kein Admin in Gruppe {}!", userid, group);
throw new NoAccessException(group.getId().toString());
}
}
/**
* Schmeißt keine Exception, wenn der User der letzte User ist.
*/
public static void throwIfLastAdmin(Group group, String userid) throws LastAdminException {
if (!checkIfLastMember(group, userid) && checkIfLastAdmin(group, userid)) {
throw new LastAdminException("Du bist letzter Admin!");
}
}
public static void throwIfGroupFull(Group group) throws GroupFullException {
if (group.isFull()) {
log.error("Die Gruppe {} ist voll!", group);
throw new GroupFullException(group.getId().toString());
}
}
public static void validateCreateForm(KeycloakAuthenticationToken token, Type type) {
if (!token.getAccount().getRoles().contains("orga") && type == Type.LECTURE) {
throw new BadArgumentException("Nur Orga kann Veranstaltungen erstellen.");
}
}
}

View File

@ -0,0 +1,18 @@
package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class ApplicationInit {
private final GroupCache groupCache;
@EventListener(ApplicationReadyEvent.class)
public void init() {
groupCache.init();
}
}

View File

@ -0,0 +1,208 @@
package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.domain.exception.GroupNotFoundException;
import mops.gruppen2.domain.exception.IdMismatchException;
import mops.gruppen2.domain.exception.UserNotFoundException;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.helper.ProjectionHelper;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Cached alle existierenden Gruppen und einige Beziehungen.
* Gruppen können nach Typ angefragt werden, nach ID, nach Link oder nach User.
* Der Cache wird von den Events aktualisiert.
* Beim Aufruf der init() Methode werden alle bisherigen Events projiziert und die Gruppen gespeichert.
* Die Komplette Anwendung verwendet eine Instanz des Caches.
*/
@Log4j2
@RequiredArgsConstructor
@Component
@Scope("singleton")
public class GroupCache {
private final EventStoreService eventStoreService;
private final Map<UUID, Group> groups = new HashMap<>();
private final Map<String, Group> links = new HashMap<>();
private final Map<String, List<Group>> users = new HashMap<>(); // Wird vielleicht zu groß?
private final Map<Type, List<Group>> types = new EnumMap<>(Type.class);
// ######################################## CACHE ###########################################
void init() {
ProjectionHelper.project(groups, eventStoreService.findAllEvents(), this);
}
// ########################################### GETTERS #######################################
public Group group(UUID groupid) {
if (!groups.containsKey(groupid)) {
throw new GroupNotFoundException("Gruppe ist nicht im Cache.");
}
return groups.get(groupid);
}
public Group group(String link) {
if (!links.containsKey(link)) {
throw new GroupNotFoundException("Link ist nicht im Cache.");
}
return links.get(link);
}
public List<Group> groups() {
if (groups.isEmpty()) {
return Collections.emptyList();
}
return List.copyOf(groups.values());
}
public List<Group> userGroups(String userid) {
if (!users.containsKey(userid)) {
return Collections.emptyList();
}
return Collections.unmodifiableList(users.get(userid));
}
public List<Group> userLectures(String userid) {
return userGroups(userid).stream()
.filter(Group::isLecture)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> userPublics(String userid) {
return userGroups(userid).stream()
.filter(Group::isPublic)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> userPrivates(String userid) {
return userGroups(userid).stream()
.filter(Group::isPrivate)
.collect(Collectors.toUnmodifiableList());
}
public List<Group> publics() {
if (!types.containsKey(Type.PUBLIC)) {
return Collections.emptyList();
}
return Collections.unmodifiableList(types.get(Type.PUBLIC));
}
public List<Group> privates() {
if (!types.containsKey(Type.PRIVATE)) {
return Collections.emptyList();
}
return Collections.unmodifiableList(types.get(Type.PRIVATE));
}
public List<Group> lectures() {
if (!types.containsKey(Type.LECTURE)) {
return Collections.emptyList();
}
return Collections.unmodifiableList(types.get(Type.LECTURE));
}
// ######################################## SETTERS ##########################################
public void usersPut(String userid, Group group) {
if (!group.isMember(userid)) {
throw new UserNotFoundException("User ist kein Mitglied, Gruppe nicht gecached.");
}
if (!users.containsKey(userid)) {
users.put(userid, new ArrayList<>());
log.debug("Ein User wurde dem Cache hinzugefügt.");
}
users.get(userid).add(group);
}
public void usersRemove(String target, Group group) {
if (!users.containsKey(target)) {
return;
}
users.get(target).remove(group);
}
public void groupsPut(UUID groupid, Group group) {
if (group.getId() != groupid) {
throw new IdMismatchException("ID passt nicht zu Gruppe, Gruppe nicht gecached.");
}
groups.put(groupid, group);
}
public void groupsRemove(UUID groupid, Group group) {
if (!groups.containsKey(groupid)) {
return;
}
groups.remove(groupid);
links.remove(group.getLink());
group.getMembers().forEach(user -> users.get(user.getId()).removeIf(usergroup -> !usergroup.exists()));
types.get(group.getType()).removeIf(typegroup -> !typegroup.exists());
}
public void linksPut(String link, Group group) {
if (!link.equals(group.getLink())) {
throw new IdMismatchException("Link passt nicht zu Gruppe, Gruppe nicht gecached.");
}
links.put(link, group);
}
public void linksRemove(String link) {
if (!links.containsKey(link)) {
return;
}
links.remove(link);
}
public void typesPut(Type type, Group group) {
if (!types.containsKey(type)) {
types.put(type, new ArrayList<>());
log.debug("Ein Typ wurde dem Cache hinzugefügt.");
}
if (group.getType() != type) {
throw new IdMismatchException("Typ passt nicht zu Gruppe, Gruppe nicht gecached.");
}
types.get(type).add(group);
}
public void typesRemove(Type type, Group group) {
if (!types.containsKey(type)) {
return;
}
types.get(type).remove(group);
}
}

View File

@ -0,0 +1,36 @@
package mops.gruppen2.infrastructure;
import lombok.RequiredArgsConstructor;
import mops.gruppen2.domain.Account;
import mops.gruppen2.domain.model.group.Role;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@RequiredArgsConstructor
@ControllerAdvice
public class ModelAttributeControllerAdvice {
// Add modelAttributes before each @RequestMapping
@ModelAttribute
public void modelAttributes(KeycloakAuthenticationToken token,
Model model) {
// Prevent NullPointerException if not logged in
if (token != null) {
model.addAttribute("account", new Account(token));
model.addAttribute("principal", new User(token));
}
// Add enums
model.addAttribute("REGULAR", Role.REGULAR);
model.addAttribute("ADMIN", Role.ADMIN);
model.addAttribute("PUBLIC", Type.PUBLIC);
model.addAttribute("PRIVATE", Type.PRIVATE);
model.addAttribute("LECTURE", Type.LECTURE);
}
}

View File

@ -1,18 +1,17 @@
package mops.gruppen2.domain.api;
package mops.gruppen2.infrastructure.api;
import lombok.AllArgsConstructor;
import lombok.Getter;
import mops.gruppen2.domain.Group;
import java.util.List;
/**
* Kombiniert den Status und die Gruppenliste zur ausgabe über die API.
*/
@AllArgsConstructor
@Getter
@AllArgsConstructor
public class GroupRequestWrapper {
private final Long status;
private final List<Group> groupList;
private final long version;
private final List<GroupWrapper> groups;
}

View File

@ -0,0 +1,31 @@
package mops.gruppen2.infrastructure.api;
import lombok.Value;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import java.util.List;
import java.util.UUID;
@Value
public class GroupWrapper {
UUID groupid;
Type type;
UUID parent;
String title;
String description;
List<User> admins;
List<User> regulars;
public GroupWrapper(Group group) {
groupid = group.getId();
type = group.getType();
parent = group.getParent();
title = group.getTitle();
description = group.getDescription();
admins = group.getAdmins();
regulars = group.getRegulars();
}
}

View File

@ -0,0 +1,83 @@
package mops.gruppen2.infrastructure.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.helper.APIHelper;
import mops.gruppen2.domain.service.helper.ProjectionHelper;
import mops.gruppen2.infrastructure.GroupCache;
import mops.gruppen2.infrastructure.api.GroupRequestWrapper;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Api zum Datenabgleich.
*/
@Log4j2
@TraceMethodCalls
@RequiredArgsConstructor
@RestController
@RequestMapping("/gruppen2/api")
public class APIController {
private final GroupCache cache;
private final EventStoreService eventStoreService;
/**
* Erzeugt eine Liste aus Gruppen, welche sich seit einer übergebenen Event-Id geändert haben.
* Die Gruppen werden vollständig projiziert, enthalten also alle Informationen zum entsprechenden Zeitpunkt.
*
* @param eventId Die Event-ID, welche der Anfragesteller beim letzten Aufruf erhalten hat
*/
//TODO: sollte den cache benutzen, am besten wäre eine groupversion, welche der eventid
//TODO: entspricht, dann kann man leicht alle geänderten gruppen finden
@GetMapping("/update/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt veränderte Gruppen zurück")
public GroupRequestWrapper getApiUpdate(@ApiParam("Letzte gespeicherte EventId des Anfragestellers")
@PathVariable("id") long eventId) {
return APIHelper.wrap(eventStoreService.findMaxEventId(),
ProjectionHelper.project(eventStoreService.findGroupEvents(eventStoreService.findChangedGroups(eventId))));
}
/**
* Gibt die Gruppen-IDs von Gruppen, in welchen der übergebene Nutzer teilnimmt, zurück.
*/
@GetMapping("/usergroups/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt Gruppen zurück, in welchen ein Nutzer teilnimmt")
public List<String> getApiUserGroups(@ApiParam("Nutzer-Id")
@PathVariable("id") String userId) {
return cache.userGroups(userId).stream()
.map(Group::getId)
.map(UUID::toString)
.collect(Collectors.toUnmodifiableList());
}
/**
* Konstruiert eine einzelne, vollständige Gruppe.
*/
@GetMapping("/group/{id}")
@Secured("ROLE_api_user")
@ApiOperation("Gibt die Gruppe mit der als Parameter mitgegebenden groupId zurück")
public Group getApiGroup(@ApiParam("Gruppen-Id der gefordeten Gruppe")
@PathVariable("id") String groupId) {
return cache.group(UUID.fromString(groupId));
}
}

View File

@ -0,0 +1,75 @@
package mops.gruppen2.infrastructure.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.Type;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Parent;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.GroupService;
import mops.gruppen2.domain.service.helper.FileHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.security.RolesAllowed;
import javax.validation.Valid;
@SuppressWarnings("SameReturnValue")
@Log4j2
@TraceMethodCalls
@RequiredArgsConstructor
@Controller
@RequestMapping("/gruppen2")
public class GroupCreationController {
private final GroupCache groupCache;
private final GroupService groupService;
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/create")
public String getCreate(Model model) {
model.addAttribute("lectures", groupCache.lectures());
return "create";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/create")
public String postCreateOrga(KeycloakAuthenticationToken token,
@RequestParam("type") Type type,
@RequestParam("parent") @Valid Parent parent,
@RequestParam("title") @Valid Title title,
@RequestParam("description") @Valid Description description,
@RequestParam("limit") @Valid Limit limit,
@RequestParam(value = "file", required = false) MultipartFile file) {
// Zusätzlicher check: studentin kann keine lecture erstellen
ValidationHelper.validateCreateForm(token, type);
String principal = token.getName();
Group group = groupService.createGroup(principal);
groupService.initGroupMembers(group, principal, principal, new User(token), limit);
groupService.initGroupMeta(group, principal, type, parent);
groupService.initGroupText(group, principal, title, description);
// ROLE_studentin kann kein CSV importieren
if (token.getAccount().getRoles().contains("orga")) {
groupService.addUsersToGroup(group, principal, FileHelper.readCsvFile(file));
}
return "redirect:/gruppen2/details/" + group.getId();
}
}

View File

@ -0,0 +1,296 @@
package mops.gruppen2.infrastructure.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCalls;
import mops.gruppen2.domain.model.group.Group;
import mops.gruppen2.domain.model.group.User;
import mops.gruppen2.domain.model.group.wrapper.Description;
import mops.gruppen2.domain.model.group.wrapper.Limit;
import mops.gruppen2.domain.model.group.wrapper.Title;
import mops.gruppen2.domain.service.EventStoreService;
import mops.gruppen2.domain.service.GroupService;
import mops.gruppen2.domain.service.helper.FileHelper;
import mops.gruppen2.domain.service.helper.ValidationHelper;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.UUID;
@SuppressWarnings("SameReturnValue")
@Log4j2
@TraceMethodCalls
@RequiredArgsConstructor
@Controller
@RequestMapping("/gruppen2")
public class GroupDetailsController {
private final GroupCache groupCache;
private final GroupService groupService;
private final EventStoreService eventStoreService;
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/details/{id}")
public String getDetailsPage(KeycloakAuthenticationToken token,
Model model,
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
// Parent Badge
Group parent = Group.EMPTY();
if (group.hasParent()) {
parent = groupCache.group(group.getParent());
}
model.addAttribute("group", group);
model.addAttribute("parent", parent);
// Detailseite für nicht-Mitglieder
if (!group.isMember(principal)) {
return "preview";
}
return "details";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/join")
public String postDetailsJoin(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
if (group.isMember(principal)) {
return "redirect:/gruppen2/details/" + groupId;
}
groupService.addMember(group, principal, principal, new User(token));
return "redirect:/gruppen2/details/" + groupId;
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/leave")
public String postDetailsLeave(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
groupService.kickMember(group, principal, principal);
return "redirect:/gruppen2";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("details/{id}/history")
public String getDetailsHistory(KeycloakAuthenticationToken token,
Model model,
@PathVariable("id") String groupId) {
model.addAttribute("events",
eventStoreService.findGroupEvents(UUID.fromString(groupId)));
return "log";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping(value = "details/{id}/export/history/plain", produces = "text/plain;charset=UTF-8")
public void getDetailsExportHistoryPlain(HttpServletResponse response,
@PathVariable("id") String groupId) {
String filename = "eventlog-" + groupId + ".txt";
response.setContentType("text/txt;charset=UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"");
try {
response.getWriter()
.write(FileHelper.payloadsToPlain(
eventStoreService.findGroupPayloads(UUID.fromString(groupId))));
} catch (IOException e) {
log.error("Payloads konnten nicht geschrieben werden.", e);
}
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping(value = "details/{id}/export/history/sql", produces = "application/sql;charset=UTF-8")
public void getDetailsExportHistorySql(HttpServletResponse response,
@PathVariable("id") String groupId) {
String filename = "data.sql";
response.setContentType("application/sql;charset=UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"");
try {
response.getWriter()
.write(FileHelper.eventDTOsToSql(
eventStoreService.findGroupDTOs(UUID.fromString(groupId))));
} catch (IOException e) {
log.error("Payloads konnten nicht geschrieben werden.", e);
}
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping(value = "details/{id}/export/members", produces = "text/csv;charset=UTF-8")
public void getDetailsExportMembers(HttpServletResponse response,
@PathVariable("id") String groupId) {
String filename = "teilnehmer-" + groupId + ".csv";
response.setContentType("text/csv;charset=UTF-8");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"");
try {
response.getWriter()
.print(FileHelper.writeCsvUserList(groupCache.group(UUID.fromString(groupId)).getMembers()));
} catch (IOException e) {
log.error("Teilnehmerliste konnte nicht geschrieben werden.", e);
}
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/details/{id}/edit")
public String getDetailsEdit(KeycloakAuthenticationToken token,
Model model,
HttpServletRequest request,
@PathVariable("id") String groupId) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
// Invite Link
String actualURL = request.getRequestURL().toString();
String serverURL = actualURL.substring(0, actualURL.indexOf("gruppen2/"));
String link = serverURL + "gruppen2/join/" + group.getLink();
ValidationHelper.throwIfNoAdmin(group, principal);
model.addAttribute("group", group);
model.addAttribute("link", link);
return "edit";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/meta")
public String postDetailsEditMeta(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@Valid Title title,
@Valid Description description) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
groupService.setTitle(group, principal, title);
groupService.setDescription(group, principal, description);
return "redirect:/gruppen2/details/" + groupId + "/edit";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/userlimit")
public String postDetailsEditUserLimit(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@Valid Limit limit) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
groupService.setLimit(group, principal, limit);
return "redirect:/gruppen2/details/" + groupId + "/edit";
}
@RolesAllowed("ROLE_orga")
@PostMapping("/details/{id}/edit/csv")
public String postDetailsEditCsv(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@RequestParam(value = "file", required = false) MultipartFile file) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
groupService.addUsersToGroup(group, principal, FileHelper.readCsvFile(file));
return "redirect:/gruppen2/details/" + groupId + "/edit";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/role/{userid}")
public String postDetailsEditRole(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@PathVariable("userid") String target) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
ValidationHelper.throwIfNoAdmin(group, principal);
if (target.equals(principal)) {
ValidationHelper.throwIfLastAdmin(group, principal);
}
groupService.toggleMemberRole(group, principal, target);
// Falls sich der User selbst die Rechte genommen hat
if (!ValidationHelper.checkIfAdmin(group, principal)) {
return "redirect:/gruppen2/details/" + groupId;
}
return "redirect:/gruppen2/details/" + groupId + "/edit";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/delete/{userid}")
public String postDetailsEditDelete(KeycloakAuthenticationToken token,
@PathVariable("id") String groupId,
@PathVariable("userid") String target) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupId));
ValidationHelper.throwIfNoAdmin(group, principal);
// Der eingeloggte User kann sich nicht selbst entfernen (er kann aber verlassen)
if (!principal.equals(target)) {
groupService.kickMember(group, principal, target);
}
return "redirect:/gruppen2/details/" + groupId + "/edit";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@PostMapping("/details/{id}/edit/destroy")
public String postDetailsEditDestroy(KeycloakAuthenticationToken token,
@PathVariable("id") String groupid) {
String principal = token.getName();
Group group = groupCache.group(UUID.fromString(groupid));
groupService.deleteGroup(group, principal);
return "redirect:/gruppen2";
}
//TODO: Method + view for /details/{id}/member/{id}
}

View File

@ -1,10 +1,10 @@
package mops.gruppen2.controller;
package mops.gruppen2.infrastructure.controller;
import mops.gruppen2.domain.Account;
import mops.gruppen2.domain.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import mops.gruppen2.aspect.annotation.TraceMethodCall;
import mops.gruppen2.domain.exception.PageNotFoundException;
import mops.gruppen2.service.KeyCloakService;
import mops.gruppen2.service.UserService;
import mops.gruppen2.infrastructure.GroupCache;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
@ -14,31 +14,29 @@ import javax.annotation.security.RolesAllowed;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@SuppressWarnings("SameReturnValue")
@Log4j2
@RequiredArgsConstructor
@Controller
public class GruppenfindungController {
private final UserService userService;
public GruppenfindungController(UserService userService) {
this.userService = userService;
}
private final GroupCache groupCache;
// For convenience
@GetMapping("")
public String redirect() {
return "redirect:/gruppen2";
}
@RolesAllowed({"ROLE_orga", "ROLE_studentin", "ROLE_actuator"})
@TraceMethodCall
@RolesAllowed({"ROLE_orga", "ROLE_studentin"})
@GetMapping("/gruppen2")
public String index(KeycloakAuthenticationToken token,
Model model) {
public String getIndexPage(KeycloakAuthenticationToken token,
Model model) {
Account account = KeyCloakService.createAccountFromPrincipal(token);
User user = new User(account);
model.addAttribute("account", account);
model.addAttribute("gruppen", userService.getUserGroups(user));
model.addAttribute("user", user);
model.addAttribute("lectures", groupCache.userLectures(token.getName()));
model.addAttribute("publics", groupCache.userPublics(token.getName()));
model.addAttribute("privates", groupCache.userPrivates(token.getName()));
return "index";
}

Some files were not shown because too many files have changed in this diff Show More