diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..84ac697a
--- /dev/null
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific dialect governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 00000000..2cc7d4a5
Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..642d572c
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/mvnw b/mvnw
new file mode 100644
index 00000000..41c0f0c2
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 00000000..f0081db3
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
index 4d5a6463..047b723e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,10 +31,17 @@
http://github.com/vlingo/xoom-auth/tree/master
+ 1.8
+ ${jdk.version}
+ ${jdk.version}
UTF-8
- 1.8
- 1.8
+ UTF-8
+ io.vlingo.xoom.auth.infrastructure.XoomInitializer
+ 2.22.2
+ 2.22.2
+ 1.9.1-SNAPSHOT
21.1.0
+ 5.8.1
@@ -75,36 +82,322 @@
+
+ io.vlingo.xoom
+ xoom-build-plugins
+ ${vlingo.xoom.version}
+
+
+
+ push
+
+
+
+
+ push-schema
+
+
+ ${basedir}/src/main/vlingo/schemata
+
+ http://xoom-schemata:9019
+ vlingo
+ xoom
+ true
+ true
+
+
+
+ [vlingo:xoom:auth:Credential:1.0.0]
+ Credential.vss
+
+
+ [vlingo:xoom:auth:PersonName:1.0.0]
+ PersonName.vss
+
+
+ [vlingo:xoom:auth:Constraint:1.0.0]
+ Constraint.vss
+
+
+ [vlingo:xoom:auth:Profile:1.0.0]
+ Profile.vss
+
+
+ [vlingo:xoom:auth:RoleProvisioned:1.0.0]
+ RoleProvisioned.vss
+
+
+ [vlingo:xoom:auth:RoleDescriptionChanged:1.0.0]
+ RoleDescriptionChanged.vss
+
+
+ [vlingo:xoom:auth:GroupAssignedToRole:1.0.0]
+ GroupAssignedToRole.vss
+
+
+ [vlingo:xoom:auth:GroupUnassignedFromRole:1.0.0]
+ GroupUnassignedFromRole.vss
+
+
+ [vlingo:xoom:auth:UserUnassignedFromRole:1.0.0]
+ UserUnassignedFromRole.vss
+
+
+ [vlingo:xoom:auth:UserAssignedToRole:1.0.0]
+ UserAssignedToRole.vss
+
+
+ [vlingo:xoom:auth:RolePermissionAttached:1.0.0]
+ RolePermissionAttached.vss
+
+
+ [vlingo:xoom:auth:RolePermissionDetached:1.0.0]
+ RolePermissionDetached.vss
+
+
+ [vlingo:xoom:auth:TenantActivated:1.0.0]
+ TenantActivated.vss
+
+
+ [vlingo:xoom:auth:TenantDeactivated:1.0.0]
+ TenantDeactivated.vss
+
+
+ [vlingo:xoom:auth:TenantDescriptionChanged:1.0.0]
+ TenantDescriptionChanged.vss
+
+
+ [vlingo:xoom:auth:TenantNameChanged:1.0.0]
+ TenantNameChanged.vss
+
+
+ [vlingo:xoom:auth:TenantSubscribed:1.0.0]
+ TenantSubscribed.vss
+
+
+ [vlingo:xoom:auth:GroupProvisioned:1.0.0]
+ GroupProvisioned.vss
+
+
+ [vlingo:xoom:auth:GroupDescriptionChanged:1.0.0]
+ GroupDescriptionChanged.vss
+
+
+ [vlingo:xoom:auth:GroupAssignedToGroup:1.0.0]
+ GroupAssignedToGroup.vss
+
+
+ [vlingo:xoom:auth:GroupUnassignedFromGroup:1.0.0]
+ GroupUnassignedFromGroup.vss
+
+
+ [vlingo:xoom:auth:UserAssignedToGroup:1.0.0]
+ UserAssignedToGroup.vss
+
+
+ [vlingo:xoom:auth:UserUnassignedFromGroup:1.0.0]
+ UserUnassignedFromGroup.vss
+
+
+ [vlingo:xoom:auth:UserRegistered:1.0.0]
+ UserRegistered.vss
+
+
+ [vlingo:xoom:auth:UserActivated:1.0.0]
+ UserActivated.vss
+
+
+ [vlingo:xoom:auth:UserDeactivated:1.0.0]
+ UserDeactivated.vss
+
+
+ [vlingo:xoom:auth:UserCredentialAdded:1.0.0]
+ UserCredentialAdded.vss
+
+
+ [vlingo:xoom:auth:UserCredentialRemoved:1.0.0]
+ UserCredentialRemoved.vss
+
+
+ [vlingo:xoom:auth:UserCredentialReplaced:1.0.0]
+ UserCredentialReplaced.vss
+
+
+ [vlingo:xoom:auth:UserProfileReplaced:1.0.0]
+ UserProfileReplaced.vss
+
+
+ [vlingo:xoom:auth:PermissionProvisioned:1.0.0]
+ PermissionProvisioned.vss
+
+
+ [vlingo:xoom:auth:PermissionConstraintEnforced:1.0.0]
+ PermissionConstraintEnforced.vss
+
+
+ [vlingo:xoom:auth:PermissionConstraintReplacementEnforced:1.0.0]
+ PermissionConstraintReplacementEnforced.vss
+
+
+ [vlingo:xoom:auth:PermissionConstraintForgotten:1.0.0]
+ PermissionConstraintForgotten.vss
+
+
+ [vlingo:xoom:auth:PermissionDescriptionChanged:1.0.0]
+ PermissionDescriptionChanged.vss
+
+
+
+
+
+
+ pull
+
+ pull-schema
+
+
+
+ http://xoom-schemata:9019
+ Inform the client organization
+ Inform the client unit
+
+
+
+
+
+
+
+
+
+ maven-shade-plugin
+ 3.1.0
+
+
+ package
+
+ shade
+
+
+
+
+ ${exec.mainClass}
+
+
+
+ .SF
+
+
+ .DSA
+
+
+ .RSA
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+ java
+
+ -classpath
+
+ -noverify
+ -XX:TieredStopAtLevel=1
+ -Dcom.sun.management.jmxremote
+ ${exec.mainClass}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+ true
+
+ %regex[.*]
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-failsafe-plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.7.0
+
+
+ -parameters
+
+
+
+ io.vlingo.xoom
+ xoom-turbo
+ ${vlingo.xoom.version}
+
+
+
+
+
+ test-compile
+
+ testCompile
+
+
+
+ -parameters
+
+
+
+ io.vlingo.xoom
+ xoom-turbo
+ ${vlingo.xoom.version}
+
+
+
+
+
+
+
+
-
- junit
- junit
- 4.13.2
- test
-
-
- ch.qos.logback
- logback-classic
- 1.2.3
- test
-
io.vlingo.xoom
- xoom-actors
- 1.9.1-SNAPSHOT
+ xoom-turbo
+ ${vlingo.xoom.version}
+ compile
io.vlingo.xoom
- xoom-cluster
- 1.9.1-SNAPSHOT
+ xoom-build-plugins
+ ${vlingo.xoom.version}
+ compile
- io.vlingo.xoom
- xoom-http
- 1.9.1-SNAPSHOT
+ net.java.dev.jna
+ jna
+ 5.9.0
org.graalvm.sdk
@@ -112,20 +405,48 @@
${graalvm.version}
provided
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit-jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-jupiter.version}
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ ${junit-jupiter.version}
+ test
+
+
+ io.rest-assured
+ rest-assured
+ 4.4.0
+ test
+
ossrh-snapshots
https://oss.sonatype.org/content/repositories/snapshots/
-
- false
-
-
- true
-
+ false
+ true
+
+
+ ossrh-snapshots
+ https://oss.sonatype.org/content/repositories/snapshots/
+ false
+ true
+
+
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/Bootstrap.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/Bootstrap.java
new file mode 100644
index 00000000..0e2dcfea
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/Bootstrap.java
@@ -0,0 +1,23 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.turbo.annotation.initializer.ResourceHandlers;
+
+import io.vlingo.xoom.http.resource.SinglePageApplicationConfiguration;
+import io.vlingo.xoom.lattice.grid.Grid;
+import io.vlingo.xoom.turbo.XoomInitializationAware;
+import io.vlingo.xoom.turbo.annotation.initializer.Xoom;
+
+@Xoom(name = "xoom-auth")
+@ResourceHandlers(packages = "io.vlingo.xoom.auth.infrastructure.resource")
+public class Bootstrap implements XoomInitializationAware {
+
+ @Override
+ public void onInit(final Grid grid) {
+ }
+
+ @Override
+ public SinglePageApplicationConfiguration singlePageApplicationResource() {
+ return SinglePageApplicationConfiguration.defineWith("/frontend", "/app");
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/ConstraintData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/ConstraintData.java
new file mode 100644
index 00000000..9dd45014
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/ConstraintData.java
@@ -0,0 +1,67 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.value.Constraint;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+public class ConstraintData {
+
+ public final String type;
+ public final String name;
+ public final String value;
+ public final String description;
+
+ public static ConstraintData from(final String type, final String name, final String value, final String description) {
+ return new ConstraintData(type, name, value, description);
+ }
+
+ private ConstraintData(final String type, final String name, final String value, final String description) {
+ this.description = description;
+ this.name = name;
+ this.type = type;
+ this.value = value;
+ }
+
+ public Constraint toConstraint() {
+ return Constraint.from(Constraint.Type.valueOf(type), name, value, description);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(31, 17)
+ .append(description)
+ .append(name)
+ .append(type)
+ .append(value)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ ConstraintData another = (ConstraintData) other;
+ return new EqualsBuilder()
+ .append(this.description, another.description)
+ .append(this.name, another.name)
+ .append(this.type, another.type)
+ .append(this.value, another.value)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("description", description)
+ .append("name", name)
+ .append("type", type)
+ .append("value", value)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/CredentialData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/CredentialData.java
new file mode 100644
index 00000000..930f9d86
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/CredentialData.java
@@ -0,0 +1,97 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.crypto.AuthHasher;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import io.vlingo.xoom.auth.model.value.*;
+
+public class CredentialData {
+
+ public final String authority;
+ public final String id;
+ public final String secret;
+ public final String type;
+
+ public static CredentialData from(final Credential credential) {
+ if (credential == null) {
+ return CredentialData.empty();
+ } else {
+ return from(credential.authority, credential.id, credential.secret, credential.type.name());
+ }
+ }
+
+ public static CredentialData from(final String authority, final String id, final String secret) {
+ return from(authority, id, secret, null);
+ }
+
+ public static CredentialData from(final String authority, final String id, final String secret, final String type) {
+ return new CredentialData(authority, id, secret, type);
+ }
+
+ public static Set fromAll(final Set correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(CredentialData::from).collect(Collectors.toSet());
+ }
+
+ public static List fromAll(final List correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptyList() : correspondingObjects.stream().map(CredentialData::from).collect(Collectors.toList());
+ }
+
+ private CredentialData(final String authority, final String id, final String secret, final String type) {
+ this.authority = authority;
+ this.id = id;
+ this.secret = secret;
+ this.type = type;
+ }
+
+ public Credential toCredential() {
+ final String cryptoSecret = AuthHasher.defaultHasher().hash(secret);
+ return Credential.from(authority, id, cryptoSecret, type);
+ }
+
+ public static CredentialData empty() {
+ return new CredentialData(null, null, null, null);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(31, 17)
+ .append(authority)
+ .append(id)
+ .append(secret)
+ .append(type)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ CredentialData another = (CredentialData) other;
+ return new EqualsBuilder()
+ .append(this.authority, another.authority)
+ .append(this.id, another.id)
+ .append(this.secret, another.secret)
+ .append(this.type, another.type)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("authority", authority)
+ .append("id", id)
+ .append("secret", secret)
+ .append("type", type)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/Events.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/Events.java
new file mode 100644
index 00000000..58c0f16c
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/Events.java
@@ -0,0 +1,35 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+public enum Events {
+ RolePermissionAttached,
+ UserUnassignedFromRole,
+ UserRegistered,
+ TenantDeactivated,
+ PermissionProvisioned,
+ TenantDescriptionChanged,
+ UserAssignedToRole,
+ UserAssignedToGroup,
+ UserCredentialRemoved,
+ PermissionConstraintEnforced,
+ GroupAssignedToGroup,
+ TenantNameChanged,
+ UserCredentialReplaced,
+ GroupAssignedToRole,
+ GroupUnassignedFromRole,
+ GroupDescriptionChanged,
+ GroupUnassignedFromGroup,
+ RoleProvisioned,
+ UserUnassignedFromGroup,
+ GroupProvisioned,
+ TenantActivated,
+ PermissionConstraintForgotten,
+ UserCredentialAdded,
+ UserProfileReplaced,
+ PermissionDescriptionChanged,
+ UserDeactivated,
+ RoleDescriptionChanged,
+ TenantSubscribed,
+ UserActivated,
+ RolePermissionDetached,
+ PermissionConstraintReplacementEnforced,
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/GroupData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/GroupData.java
new file mode 100644
index 00000000..81015917
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/GroupData.java
@@ -0,0 +1,60 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.group.GroupState;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("all")
+public class GroupData {
+ public final String id;
+ public final String name;
+ public final String description;
+ public final String tenantId;
+
+ public static GroupData from(final TenantId tenantId, final String name, final String description) {
+ return new GroupData(tenantId, name, description);
+ }
+
+ private GroupData(final TenantId tenantId, final String name, final String description) {
+ this.id = null;
+ this.name = name;
+ this.description = description;
+ this.tenantId = tenantId.idString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ GroupData another = (GroupData) other;
+ return new EqualsBuilder()
+ .append(this.id, another.id)
+ .append(this.name, another.name)
+ .append(this.description, another.description)
+ .append(this.tenantId, another.tenantId)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("name", name)
+ .append("description", description)
+ .append("tenantId", tenantId)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/MinimalUserData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/MinimalUserData.java
new file mode 100644
index 00000000..20dad788
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/MinimalUserData.java
@@ -0,0 +1,37 @@
+// Copyright © 2012-2021 VLINGO LABS. All rights reserved.
+//
+// This Source Code Form is subject to the terms of the
+// Mozilla Public License, v. 2.0. If a copy of the MPL
+// was not distributed with this file, You can obtain
+// one at https://mozilla.org/MPL/2.0/.
+
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.infrastructure.persistence.UserView;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+
+public final class MinimalUserData {
+ public final boolean active;
+ public final PersonNameData name;
+ public final String tenantId;
+ public final String username;
+
+ public static MinimalUserData from(final TenantId tenantId, final String username, final PersonNameData name, final boolean active) {
+ return new MinimalUserData(tenantId, username, name, active);
+ }
+
+ public static MinimalUserData from(final UserView user) {
+ return new MinimalUserData(user.tenantId, user.username, PersonNameData.from(user.profile.name), user.active);
+ }
+
+ private MinimalUserData(final TenantId tenantId, final String username, final PersonNameData name, final boolean active) {
+ this(tenantId.idString(), username, name, active);
+ }
+
+ private MinimalUserData(final String tenantId, final String username, final PersonNameData name, final boolean active) {
+ this.tenantId = tenantId;
+ this.username = username;
+ this.name = name;
+ this.active = active;
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/PermissionData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/PermissionData.java
new file mode 100644
index 00000000..0fb07284
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/PermissionData.java
@@ -0,0 +1,60 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@SuppressWarnings("all")
+public class PermissionData {
+ public final String id;
+ public final Set constraints = new HashSet<>();
+ public final String name;
+ public final String description;
+ public final String tenantId;
+
+ public static PermissionData from(final TenantId tenantId, final Set constraints, final String name, final String description) {
+ return new PermissionData(tenantId, constraints, name, description);
+ }
+
+ private PermissionData(final TenantId tenantId, final Set constraints, final String name, final String description) {
+ this.id = null;
+ this.constraints.addAll(constraints);
+ this.name = name;
+ this.description = description;
+ this.tenantId = tenantId.idString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ PermissionData another = (PermissionData) other;
+ return new EqualsBuilder()
+ .append(this.id, another.id)
+ .append(this.constraints, another.constraints)
+ .append(this.description, another.description)
+ .append(this.name, another.name)
+ .append(this.tenantId, another.tenantId)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("constraints", constraints)
+ .append("description", description)
+ .append("name", name)
+ .append("tenantId", tenantId)
+ .toString();
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/PersonNameData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/PersonNameData.java
new file mode 100644
index 00000000..ce8d2438
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/PersonNameData.java
@@ -0,0 +1,96 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.infrastructure.persistence.UserView;
+import io.vlingo.xoom.auth.model.value.PersonName;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class PersonNameData {
+
+ public final String given;
+ public final String family;
+ public final String second;
+
+ public static PersonNameData from(final PersonName personName) {
+ if (personName == null) {
+ return PersonNameData.empty();
+ } else {
+ return from(personName.given, personName.family, personName.second);
+ }
+ }
+
+ public static PersonNameData from(final UserView.PersonNameView name) {
+ return new PersonNameData(name.given, name.family, name.second);
+ }
+
+ public static PersonNameData from(final String given, final String family, final String second) {
+ return new PersonNameData(given, family, second);
+ }
+
+ public static Set fromAll(final Set correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(PersonNameData::from).collect(Collectors.toSet());
+ }
+
+ public static List fromAll(final List correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptyList() : correspondingObjects.stream().map(PersonNameData::from).collect(Collectors.toList());
+ }
+
+ public static PersonNameData of(String given, String second, String family) {
+ return from(given, family, second);
+ }
+
+ private PersonNameData (final String given, final String family, final String second) {
+ this.given = given;
+ this.family = family;
+ this.second = second;
+ }
+
+ public PersonName toPersonName() {
+ return PersonName.from(given, family, second);
+ }
+
+ public static PersonNameData empty() {
+ return new PersonNameData(null, null, null);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(31, 17)
+ .append(given)
+ .append(family)
+ .append(second)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ PersonNameData another = (PersonNameData) other;
+ return new EqualsBuilder()
+ .append(this.given, another.given)
+ .append(this.family, another.family)
+ .append(this.second, another.second)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("given", given)
+ .append("family", family)
+ .append("second", second)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/ProfileData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/ProfileData.java
new file mode 100644
index 00000000..4d0fb023
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/ProfileData.java
@@ -0,0 +1,92 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import io.vlingo.xoom.auth.model.value.*;
+
+public class ProfileData {
+
+ public final String emailAddress;
+ public final PersonNameData name;
+ public final String phone;
+
+ public static ProfileData from(final Profile profile) {
+ if (profile == null) {
+ return ProfileData.empty();
+ } else {
+ final PersonNameData name = profile.name != null ? PersonNameData.from(profile.name) : null;
+ return from(name, profile.emailAddress, profile.phone);
+ }
+ }
+
+ public static ProfileData from(final PersonNameData name, final String emailAddress, final String phone) {
+ return new ProfileData(emailAddress, name, phone);
+ }
+
+ public static ProfileData from(final String emailAddress, final PersonNameData name, final String phone) {
+ return new ProfileData(emailAddress, name, phone);
+ }
+
+ public static Set fromAll(final Set correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(ProfileData::from).collect(Collectors.toSet());
+ }
+
+ public static List fromAll(final List correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptyList() : correspondingObjects.stream().map(ProfileData::from).collect(Collectors.toList());
+ }
+
+ private ProfileData (final String emailAddress, final PersonNameData name, final String phone) {
+ this.emailAddress = emailAddress;
+ this.name = name;
+ this.phone = phone;
+ }
+
+ public Profile toProfile() {
+ final PersonName name = PersonName.from(this.name.given, this.name.family, this.name.second);
+ return Profile.from(emailAddress, name, phone);
+ }
+
+ public static ProfileData empty() {
+ return new ProfileData(null, null, null);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(31, 17)
+ .append(emailAddress)
+ .append(name)
+ .append(phone)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ ProfileData another = (ProfileData) other;
+ return new EqualsBuilder()
+ .append(this.emailAddress, another.emailAddress)
+ .append(this.name, another.name)
+ .append(this.phone, another.phone)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("emailAddress", emailAddress)
+ .append("name", name)
+ .append("phone", phone)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/RoleData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/RoleData.java
new file mode 100644
index 00000000..15b93c98
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/RoleData.java
@@ -0,0 +1,58 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.role.RoleState;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("all")
+public class RoleData {
+ public final String id;
+ public final String tenantId;
+ public final String name;
+ public final String description;
+
+ public static RoleData from(final TenantId tenantId, final String name, final String description) {
+ return new RoleData(tenantId, name, description);
+ }
+
+ private RoleData(final TenantId tenantId, final String name, final String description) {
+ this.id = null;
+ this.tenantId = tenantId.idString();
+ this.name = name;
+ this.description = description;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ RoleData another = (RoleData) other;
+ return new EqualsBuilder()
+ .append(this.id, another.id)
+ .append(this.tenantId, another.tenantId)
+ .append(this.name, another.name)
+ .append(this.description, another.description)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("tenantId", tenantId)
+ .append("name", name)
+ .append("description", description)
+ .toString();
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/TenantData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/TenantData.java
new file mode 100644
index 00000000..04a22111
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/TenantData.java
@@ -0,0 +1,79 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import java.util.stream.Collectors;
+import io.vlingo.xoom.auth.model.tenant.TenantState;
+import java.util.*;
+
+@SuppressWarnings("all")
+public class TenantData {
+ public final String tenantId;
+ public final String name;
+ public final String description;
+ public final boolean active;
+
+ public static TenantData from(final TenantState tenantState) {
+ return from(tenantState.tenantId, tenantState.name, tenantState.description, tenantState.active);
+ }
+
+ public static TenantData from(final TenantId tenantId, final String name, final String description, final boolean active) {
+ return new TenantData(tenantId, name, description, active);
+ }
+
+ public static TenantData from(final String name, final String description, final boolean active) {
+ return from(TenantId.from(UUID.randomUUID().toString()), name, description, active);
+ }
+
+ public static List fromAll(final List states) {
+ return states.stream().map(TenantData::from).collect(Collectors.toList());
+ }
+
+ public static TenantData empty() {
+ return from(TenantState.identifiedBy(TenantId.from("")));
+ }
+
+ private TenantData(final TenantId tenantId, final String name, final String description, final boolean active) {
+ this.tenantId = tenantId.idString();
+ this.name = name;
+ this.description = description;
+ this.active = active;
+ }
+
+ public TenantState toTenantState() {
+ return new TenantState(TenantId.from(tenantId), name, description, active);
+ }
+
+ public TenantData withTenantId(String tenantId) {
+ return from(TenantId.from(tenantId), this.name, this.description, this.active);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ TenantData another = (TenantData) other;
+ return new EqualsBuilder()
+ .append(this.tenantId, another.tenantId)
+ .append(this.name, another.name)
+ .append(this.description, another.description)
+ .append(this.active, another.active)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", tenantId)
+ .append("name", name)
+ .append("description", description)
+ .append("active", active)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/UserRegistrationData.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/UserRegistrationData.java
new file mode 100644
index 00000000..8a1aef74
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/UserRegistrationData.java
@@ -0,0 +1,86 @@
+package io.vlingo.xoom.auth.infrastructure;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.auth.model.user.UserState;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@SuppressWarnings("all")
+public class UserRegistrationData {
+ public final String id;
+ public final String tenantId;
+ public final String username;
+ public final boolean active;
+ public final Set credentials = new HashSet<>();
+ public final ProfileData profile;
+
+ public static UserRegistrationData from(final UserState userState) {
+ final Set credentials = userState.credentials != null ? userState.credentials.stream().map(CredentialData::from).collect(java.util.stream.Collectors.toSet()) : new HashSet<>();
+ final ProfileData profile = userState.profile != null ? ProfileData.from(userState.profile) : null;
+ return from(userState.userId, userState.username, profile, credentials, userState.active);
+ }
+
+ public static UserRegistrationData from(final UserId userId, final String username, final ProfileData profile, final Set credentials, final boolean active) {
+ return new UserRegistrationData(userId, username, profile, credentials, active);
+ }
+
+ public static UserRegistrationData from(final UserId userId, final String username, final ProfileData profile, final CredentialData credential, final boolean active) {
+ return new UserRegistrationData(userId, username, profile, Stream.of(credential).collect(Collectors.toSet()), active);
+ }
+
+ private UserRegistrationData(final UserId userId, final String username, final ProfileData profile, final Set credentials, final boolean active) {
+ this.id = userId.idString();
+ this.tenantId = userId.tenantId.idString();
+ this.username = username;
+ this.active = active;
+ this.credentials.addAll(credentials);
+ this.profile = profile;
+ }
+
+ public CredentialData credentialOf(final String authority) {
+ for (final CredentialData credential : credentials) {
+ if (credential.authority.equals(authority)) {
+ return credential;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ UserRegistrationData another = (UserRegistrationData) other;
+ return new EqualsBuilder()
+ .append(this.id, another.id)
+ .append(this.tenantId, another.tenantId)
+ .append(this.username, another.username)
+ .append(this.active, another.active)
+ .append(this.credentials, another.credentials)
+ .append(this.profile, another.profile)
+ .isEquals();
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("tenantId", tenantId)
+ .append("username", username)
+ .append("active", active)
+ .append("credentials", credentials)
+ .append("profile", profile)
+ .toString();
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionActor.java
new file mode 100644
index 00000000..0f8cd008
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupProjectionActor.java
@@ -0,0 +1,146 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.infrastructure.Events;
+import io.vlingo.xoom.auth.model.group.*;
+import io.vlingo.xoom.auth.model.role.GroupAssignedToRole;
+import io.vlingo.xoom.auth.model.role.GroupUnassignedFromRole;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.lattice.model.projection.Projectable;
+import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor;
+import io.vlingo.xoom.symbio.Source;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * See
+ *
+ * StateStoreProjectionActor
+ *
+ */
+public class GroupProjectionActor extends StateStoreProjectionActor {
+
+ private static final GroupView Empty = GroupView.empty();
+
+ public GroupProjectionActor() {
+ this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store);
+ }
+
+ public GroupProjectionActor(final StateStore stateStore) {
+ super(stateStore);
+ }
+
+ @Override
+ protected GroupView currentDataFor(final Projectable projectable) {
+ return Empty;
+ }
+
+ @Override
+ protected GroupView merge(final GroupView previousData, final int previousVersion, final GroupView currentData, final int currentVersion) {
+
+ if (previousVersion == currentVersion) return currentData;
+
+ GroupView merged = previousData;
+
+ for (final Source> event : sources()) {
+ switch (Events.valueOf(event.typeName())) {
+ case GroupProvisioned: {
+ final GroupProvisioned typedEvent = typed(event);
+ merged = GroupView.from(typedEvent.groupId, typedEvent.name, typedEvent.description);
+ break;
+ }
+
+ case GroupDescriptionChanged: {
+ final GroupDescriptionChanged typedEvent = typed(event);
+ merged = GroupView.from(typedEvent.groupId, previousData.name, typedEvent.description, previousData.groups, previousData.users, previousData.roles);
+ break;
+ }
+
+ case GroupAssignedToGroup: {
+ final GroupAssignedToGroup typedEvent = typed(event);
+ final Set> innerGroups = concat(previousData.groups, Relation.groupWithMember(typedEvent.groupId, typedEvent.innerGroupId));
+ merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, innerGroups, previousData.users, previousData.roles);
+ break;
+ }
+
+ case GroupUnassignedFromGroup: {
+ final GroupUnassignedFromGroup typedEvent = typed(event);
+ final Set> innerGroups = filter(previousData.groups, g -> !g.equals(Relation.groupWithMember(typedEvent.groupId, typedEvent.innerGroupId)));
+ merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, innerGroups, previousData.users, previousData.roles);
+ break;
+ }
+
+ case UserAssignedToGroup: {
+ final UserAssignedToGroup typedEvent = typed(event);
+ final Set> users = concat(previousData.users, Relation.userAssignedToGroup(typedEvent.userId, typedEvent.groupId));
+ merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, users, previousData.roles);
+ break;
+ }
+
+ case UserUnassignedFromGroup: {
+ final UserUnassignedFromGroup typedEvent = typed(event);
+ final Set> users = filter(previousData.users, u -> !u.equals(Relation.userAssignedToGroup(typedEvent.userId, typedEvent.groupId)));
+ merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, users, previousData.roles);
+ break;
+ }
+
+ case GroupAssignedToRole: {
+ final GroupAssignedToRole typedEvent = typed(event);
+ final Set> roles = concat(previousData.roles, Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId));
+ merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, previousData.users, roles);
+ break;
+ }
+
+ case GroupUnassignedFromRole: {
+ final GroupUnassignedFromRole typedEvent = typed(event);
+ final Set> roles = filter(previousData.roles, r -> !r.equals(Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId)));
+ merged = GroupView.from(typedEvent.groupId, previousData.name, previousData.description, previousData.groups, previousData.users, roles);
+ break;
+ }
+
+ default:
+ logger().warn("Event of type " + event.typeName() + " was not matched.");
+ break;
+ }
+ }
+
+ return merged;
+ }
+
+ @Override
+ protected String dataIdFor(final Projectable projectable) {
+ final Source> firstEvent = sources().get(0);
+ switch (Events.valueOf(firstEvent.typeName())) {
+ case GroupAssignedToRole:
+ return this.typed(firstEvent).groupId.idString();
+ case GroupUnassignedFromRole:
+ return this.typed(firstEvent).groupId.idString();
+ default:
+ return super.dataIdFor(projectable);
+ }
+ }
+
+ @Override
+ protected int currentDataVersionFor(final Projectable projectable, final GroupView previousData, final int previousVersion) {
+ switch (Events.valueOf(sources().get(0).typeName())) {
+ case GroupAssignedToRole:
+ case GroupUnassignedFromRole:
+ return previousVersion + 1;
+ default:
+ return super.currentDataVersionFor(projectable, previousData, previousVersion);
+ }
+ }
+
+ private Set filter(final Set items, final Predicate predicate) {
+ return items.stream().filter(predicate).collect(Collectors.toSet());
+ }
+
+ private Set concat(final Set items, final T item) {
+ return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueries.java
new file mode 100644
index 00000000..682e9fbc
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueries.java
@@ -0,0 +1,12 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.common.Completes;
+
+@SuppressWarnings("all")
+public interface GroupQueries {
+ Completes groupOf(GroupId groupId);
+ Completes> groups();
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesActor.java
new file mode 100644
index 00000000..1a024d4a
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupQueriesActor.java
@@ -0,0 +1,33 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.query.StateStoreQueryActor;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+
+import io.vlingo.xoom.auth.infrastructure.GroupData;
+
+/**
+ * See Querying a StateStore
+ */
+@SuppressWarnings("all")
+public class GroupQueriesActor extends StateStoreQueryActor implements GroupQueries {
+
+ public GroupQueriesActor(StateStore store) {
+ super(store);
+ }
+
+ @Override
+ public Completes groupOf(GroupId groupId) {
+ return queryStateFor(groupId.idString(), GroupView.class, GroupView.empty());
+ }
+
+ @Override
+ public Completes> groups() {
+ return streamAllOf(GroupView.class, new ArrayList<>());
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupView.java
new file mode 100644
index 00000000..bf406b13
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/GroupView.java
@@ -0,0 +1,94 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.group.GroupState;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+public class GroupView {
+ public final String id;
+ public final String tenantId;
+ public final String name;
+ public final String description;
+ public final Set> groups;
+ public final Set> users;
+ public final Set> roles;
+
+ public static GroupView empty() {
+ return new GroupView("", "", "", "", Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
+ }
+
+ public static GroupView from(GroupState groupState) {
+ return from(groupState.id, groupState.name, groupState.description);
+ }
+
+ public static GroupView from(final GroupId groupId, final String name, String description) {
+ return from(groupId, name, description, Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
+ }
+
+ public static GroupView from(final GroupId groupId, final String name, final String description, final Set> groups, final Set> users, final Set> roles) {
+ return new GroupView(
+ groupId.idString(),
+ groupId.tenantId.idString(),
+ name,
+ description,
+ groups,
+ users,
+ roles
+ );
+ }
+
+ private GroupView(final String id, final String tenantId, final String name, final String description, final Set> groups, final Set> users, final Set> roles) {
+ this.id = id;
+ this.tenantId = tenantId;
+ this.name = name;
+ this.description = description;
+ this.groups = groups;
+ this.users = users;
+ this.roles = roles;
+ }
+
+ public boolean isInRole(final RoleId roleId) {
+ return roles.stream().filter(r -> r.right.equals(roleId)).findFirst().isPresent();
+ }
+
+ public boolean hasMember(final GroupId groupId) {
+ return groups.stream().filter(g -> g.right.equals(groupId)).findFirst().isPresent();
+ }
+
+ public boolean hasMember(final UserId userId) {
+ return users.stream().filter(u -> u.left.equals(userId)).findFirst().isPresent();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof GroupView)) return false;
+ GroupView groupView = (GroupView) o;
+ return id.equals(groupView.id) && tenantId.equals(groupView.tenantId) && name.equals(groupView.name) && description.equals(groupView.description) && groups.equals(groupView.groups) && users.equals(groupView.users) && roles.equals(groupView.roles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, tenantId, name, description, groups, users, roles);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("tenantId", tenantId)
+ .append("name", name)
+ .append("description", description)
+ .append("groups", groups)
+ .append("users", users)
+ .append("roles", roles)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionActor.java
new file mode 100644
index 00000000..2a8d6be4
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionProjectionActor.java
@@ -0,0 +1,142 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.infrastructure.Events;
+import io.vlingo.xoom.auth.infrastructure.persistence.PermissionView.ConstraintView;
+import io.vlingo.xoom.auth.model.permission.*;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.role.RolePermissionAttached;
+import io.vlingo.xoom.auth.model.role.RolePermissionDetached;
+import io.vlingo.xoom.lattice.model.projection.Projectable;
+import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor;
+import io.vlingo.xoom.symbio.Source;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * See
+ *
+ * StateStoreProjectionActor
+ *
+ */
+public class PermissionProjectionActor extends StateStoreProjectionActor {
+
+ private static final PermissionView Empty = PermissionView.empty();
+
+ public PermissionProjectionActor() {
+ this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store);
+ }
+
+ public PermissionProjectionActor(final StateStore stateStore) {
+ super(stateStore);
+ }
+
+ @Override
+ protected PermissionView currentDataFor(final Projectable projectable) {
+ return Empty;
+ }
+
+ @Override
+ protected PermissionView merge(final PermissionView previousData, final int previousVersion, final PermissionView currentData, final int currentVersion) {
+
+ if (previousVersion == currentVersion) return currentData;
+
+ PermissionView merged = previousData;
+
+ for (final Source> event : sources()) {
+ switch (Events.valueOf(event.typeName())) {
+ case PermissionProvisioned: {
+ final PermissionProvisioned typedEvent = typed(event);
+ merged = PermissionView.from(typedEvent.permissionId, new HashSet<>(), typedEvent.name, typedEvent.description, new HashSet<>());
+ break;
+ }
+
+ case PermissionConstraintEnforced: {
+ final PermissionConstraintEnforced typedEvent = typed(event);
+ final ConstraintView constraint = ConstraintView.from(typedEvent.constraint);
+ merged = PermissionView.from(typedEvent.permissionId, concat(previousData.constraints, constraint), previousData.name, previousData.description, previousData.roles);
+ break;
+ }
+
+ case PermissionConstraintReplacementEnforced: {
+ final PermissionConstraintReplacementEnforced typedEvent = typed(event);
+ final ConstraintView constraint = ConstraintView.from(typedEvent.constraint);
+ final Set constraints = concat(filter(previousData.constraints, c -> !c.name.equals(typedEvent.constraintName)), constraint);
+ merged = PermissionView.from(typedEvent.permissionId, constraints, previousData.name, previousData.description, previousData.roles);
+ break;
+ }
+
+ case PermissionConstraintForgotten: {
+ final PermissionConstraintForgotten typedEvent = typed(event);
+ final Set constraints = filter(previousData.constraints, c -> !c.name.equals(typedEvent.constraintName));
+ merged = PermissionView.from(typedEvent.permissionId, constraints, previousData.name, previousData.description, previousData.roles);
+ break;
+ }
+
+ case PermissionDescriptionChanged: {
+ final PermissionDescriptionChanged typedEvent = typed(event);
+ merged = PermissionView.from(typedEvent.permissionId, previousData.constraints, previousData.name, typedEvent.description, previousData.roles);
+ break;
+ }
+
+ case RolePermissionAttached: {
+ final RolePermissionAttached typedEvent = typed(event);
+ final Set> roles = concat(previousData.roles, Relation.roleWithPermission(typedEvent.roleId, typedEvent.permissionId));
+ merged = PermissionView.from(typedEvent.permissionId, previousData.constraints, previousData.name, previousData.description, roles);
+ break;
+ }
+
+ case RolePermissionDetached: {
+ final RolePermissionDetached typedEvent = typed(event);
+ final Set> roles = filter(previousData.roles, r -> !r.equals(Relation.roleWithPermission(typedEvent.roleId, typedEvent.permissionId)));
+ merged = PermissionView.from(typedEvent.permissionId, previousData.constraints, previousData.name, previousData.description, roles);
+ break;
+ }
+
+ default:
+ logger().warn("Event of type " + event.typeName() + " was not matched.");
+ break;
+ }
+ }
+
+ return merged;
+ }
+
+ @Override
+ protected String dataIdFor(final Projectable projectable) {
+ final Source> firstEvent = sources().get(0);
+ switch (Events.valueOf(firstEvent.typeName())) {
+ case RolePermissionAttached:
+ return this.typed(firstEvent).permissionId.idString();
+ case RolePermissionDetached:
+ return this.typed(firstEvent).permissionId.idString();
+ default:
+ return super.dataIdFor(projectable);
+ }
+ }
+
+ @Override
+ protected int currentDataVersionFor(Projectable projectable, PermissionView previousData, int previousVersion) {
+ final Source> firstEvent = sources().get(0);
+ switch (Events.valueOf(firstEvent.typeName())) {
+ case RolePermissionAttached:
+ case RolePermissionDetached:
+ return previousVersion + 1;
+ default:
+ return super.currentDataVersionFor(projectable, previousData, previousVersion);
+ }
+ }
+
+ private Set filter(final Set items, final Predicate predicate) {
+ return items.stream().filter(predicate).collect(Collectors.toSet());
+ }
+
+ private Set concat(final Set items, final T item) {
+ return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueries.java
new file mode 100644
index 00000000..a4c1eaa9
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueries.java
@@ -0,0 +1,12 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.common.Completes;
+
+@SuppressWarnings("all")
+public interface PermissionQueries {
+ Completes permissionOf(PermissionId permissionId);
+ Completes> permissions();
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesActor.java
new file mode 100644
index 00000000..119c8927
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionQueriesActor.java
@@ -0,0 +1,33 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.query.StateStoreQueryActor;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+
+import io.vlingo.xoom.auth.infrastructure.PermissionData;
+
+/**
+ * See Querying a StateStore
+ */
+@SuppressWarnings("all")
+public class PermissionQueriesActor extends StateStoreQueryActor implements PermissionQueries {
+
+ public PermissionQueriesActor(StateStore store) {
+ super(store);
+ }
+
+ @Override
+ public Completes permissionOf(PermissionId permissionId) {
+ return queryStateFor(permissionId.idString(), PermissionView.class, PermissionView.empty());
+ }
+
+ @Override
+ public Completes> permissions() {
+ return streamAllOf(PermissionView.class, new ArrayList<>());
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionView.java
new file mode 100644
index 00000000..0e2b3aeb
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PermissionView.java
@@ -0,0 +1,135 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.permission.PermissionState;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.auth.model.value.Constraint;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class PermissionView {
+ public final String id;
+ public final String tenantId;
+ public final String name;
+ public final String description;
+ public final Set constraints;
+ public final Set> roles;
+
+ public static PermissionView empty() {
+ return from(PermissionId.from(TenantId.from(""), ""), new HashSet<>(), "", "", new HashSet<>());
+ }
+
+ public static PermissionView from(final PermissionState permissionState) {
+ final Set constraints = permissionState.constraints != null ? permissionState.constraints.stream().map(ConstraintView::from).collect(java.util.stream.Collectors.toSet()) : new HashSet<>();
+ return from(permissionState.id, constraints, permissionState.name, permissionState.description, new HashSet<>());
+ }
+
+ public static PermissionView from(final TenantId tenantId, final Set constraints, final String name, final String description, final Set> roles) {
+ return new PermissionView(null, tenantId.idString(), constraints, name, description, roles);
+ }
+
+ public static PermissionView from(final PermissionId permissionId, final Set constraints, final String name, final String description, final Set> roles) {
+ return new PermissionView(permissionId.idString(), permissionId.tenantId.idString(), constraints, name, description, roles);
+ }
+
+ private PermissionView(final String id, final String tenantId, final Set constraints, final String name, final String description, final Set> roles) {
+ this.id = id;
+ this.tenantId = tenantId;
+ this.name = name;
+ this.description = description;
+ this.constraints = constraints;
+ this.roles = roles;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof PermissionView)) return false;
+ PermissionView that = (PermissionView) o;
+ return id.equals(that.id) && tenantId.equals(that.tenantId) && name.equals(that.name) && description.equals(that.description) && constraints.equals(that.constraints) && roles.equals(that.roles);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, tenantId, name, description, constraints, roles);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("tenantId", tenantId)
+ .append("constraints", constraints)
+ .append("description", description)
+ .append("name", name)
+ .append("roles", roles)
+ .toString();
+ }
+
+ public boolean isAttachedTo(final RoleId roleId) {
+ return roles.stream().filter(r -> r.left.equals(roleId)).findFirst().isPresent();
+ }
+
+ public boolean isAttachedTo(final UserId userId) {
+ return false;
+ }
+
+ public static class ConstraintView {
+ public final String type;
+ public final String name;
+ public final String value;
+ public final String description;
+
+ public static ConstraintView from(final Constraint constraint) {
+ if (constraint == null) {
+ return ConstraintView.empty();
+ } else {
+ return from(constraint.type.name(), constraint.name, constraint.value, constraint.description);
+ }
+ }
+
+ public static ConstraintView from(final String type, final String name, final String value, final String description) {
+ return new ConstraintView(type, name, value, description);
+ }
+
+ public static ConstraintView empty() {
+ return new ConstraintView(null, null, null, null);
+ }
+
+ private ConstraintView(String type, String name, String value, String description) {
+ this.type = type;
+ this.name = name;
+ this.value = value;
+ this.description = description;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ConstraintView)) return false;
+ ConstraintView that = (ConstraintView) o;
+ return type.equals(that.type) && name.equals(that.name) && Objects.equals(value, that.value) && Objects.equals(description, that.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, name, value, description);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("description", description)
+ .append("name", name)
+ .append("type", type)
+ .append("value", value)
+ .toString();
+ }
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PersistenceSetup.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PersistenceSetup.java
new file mode 100644
index 00000000..83ec6b29
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/PersistenceSetup.java
@@ -0,0 +1,65 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.infrastructure.*;
+import io.vlingo.xoom.auth.model.group.*;
+import io.vlingo.xoom.auth.model.permission.*;
+import io.vlingo.xoom.auth.model.role.*;
+import io.vlingo.xoom.auth.model.tenant.*;
+import io.vlingo.xoom.auth.model.user.*;
+import io.vlingo.xoom.turbo.annotation.persistence.*;
+import io.vlingo.xoom.turbo.annotation.persistence.Persistence.StorageType;
+
+@SuppressWarnings("unused")
+@Persistence(basePackage = "io.vlingo.xoom.auth", storageType = StorageType.JOURNAL, cqrs = true)
+@Projections(value = {
+ @Projection(actor = RoleProjectionActor.class, becauseOf = {UserAssignedToRole.class, RolePermissionAttached.class, GroupUnassignedFromRole.class, RoleDescriptionChanged.class, RoleProvisioned.class, UserUnassignedFromRole.class, RolePermissionDetached.class, GroupAssignedToRole.class}),
+ @Projection(actor = TenantProjectionActor.class, becauseOf = {TenantSubscribed.class, TenantDeactivated.class, TenantNameChanged.class, TenantDescriptionChanged.class, TenantActivated.class}),
+ @Projection(actor = GroupProjectionActor.class, becauseOf = {GroupDescriptionChanged.class, UserAssignedToGroup.class, GroupAssignedToGroup.class, GroupUnassignedFromGroup.class, UserUnassignedFromGroup.class, GroupProvisioned.class, GroupAssignedToRole.class, GroupUnassignedFromRole.class}),
+ @Projection(actor = UserProjectionActor.class, becauseOf = {UserCredentialAdded.class, UserProfileReplaced.class, UserCredentialRemoved.class, UserDeactivated.class, UserRegistered.class, UserActivated.class, UserCredentialReplaced.class, UserAssignedToRole.class, UserUnassignedFromRole.class}),
+ @Projection(actor = PermissionProjectionActor.class, becauseOf = {PermissionConstraintEnforced.class, PermissionDescriptionChanged.class, PermissionProvisioned.class, PermissionConstraintReplacementEnforced.class, PermissionConstraintForgotten.class, RolePermissionAttached.class, RolePermissionDetached.class})
+}, type = ProjectionType.EVENT_BASED)
+@Adapters({
+ RolePermissionAttached.class,
+ UserUnassignedFromRole.class,
+ UserRegistered.class,
+ TenantDeactivated.class,
+ PermissionProvisioned.class,
+ TenantDescriptionChanged.class,
+ UserAssignedToRole.class,
+ UserAssignedToGroup.class,
+ UserCredentialRemoved.class,
+ PermissionConstraintEnforced.class,
+ GroupAssignedToGroup.class,
+ TenantNameChanged.class,
+ UserCredentialReplaced.class,
+ GroupAssignedToRole.class,
+ GroupUnassignedFromRole.class,
+ GroupDescriptionChanged.class,
+ GroupUnassignedFromGroup.class,
+ RoleProvisioned.class,
+ UserUnassignedFromGroup.class,
+ GroupProvisioned.class,
+ TenantActivated.class,
+ PermissionConstraintForgotten.class,
+ UserCredentialAdded.class,
+ UserProfileReplaced.class,
+ PermissionDescriptionChanged.class,
+ UserDeactivated.class,
+ RoleDescriptionChanged.class,
+ TenantSubscribed.class,
+ UserActivated.class,
+ RolePermissionDetached.class,
+ PermissionConstraintReplacementEnforced.class
+})
+@EnableQueries({
+ @QueriesEntry(protocol = RoleQueries.class, actor = RoleQueriesActor.class),
+ @QueriesEntry(protocol = GroupQueries.class, actor = GroupQueriesActor.class),
+ @QueriesEntry(protocol = TenantQueries.class, actor = TenantQueriesActor.class),
+ @QueriesEntry(protocol = UserQueries.class, actor = UserQueriesActor.class),
+ @QueriesEntry(protocol = PermissionQueries.class, actor = PermissionQueriesActor.class),
+})
+@DataObjects({CredentialData.class, ConstraintData.class, UserView.class, TenantData.class, PersonNameData.class, ProfileData.class, PermissionView.class, RoleView.class, GroupView.class})
+public class PersistenceSetup {
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/Relation.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/Relation.java
new file mode 100644
index 00000000..df741fc1
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/Relation.java
@@ -0,0 +1,117 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.HashMap;
+import java.util.Objects;
+
+import static io.vlingo.xoom.auth.infrastructure.persistence.Relation.TYPE.*;
+
+public class Relation {
+ public final L left;
+ public final TYPE type;
+ public final R right;
+
+ public static Relation groupWithMember(final GroupId parentGroup, final GroupId childGroup) {
+ return new Relation<>(parentGroup, GROUP_HAS_MEMBER, childGroup);
+ }
+
+ public static Relation groupWithParent(final GroupId childGroup, final GroupId parentGroup) {
+ return new Relation<>(childGroup, GROUP_HAS_PARENT, parentGroup);
+ }
+
+ public static Relation roleWithPermission(final RoleId role, final PermissionId permission) {
+ return new Relation<>(role, ROLE_HAS_PERMISSION, permission);
+ }
+
+ public static Relation permissionAttachedToRole(final PermissionId permission, final RoleId role) {
+ return new Relation<>(permission, PERMISSION_IS_ATTACHED_TO_ROLE, role);
+ }
+
+ public static Relation roleWithGroup(final RoleId role, final GroupId group) {
+ return new Relation<>(role, ROLE_WITH_GROUP, group);
+ }
+
+ public static Relation groupAssignedToRole(final GroupId group, final RoleId role) {
+ return new Relation<>(group, GROUP_ASSIGNED_TO_ROLE, role);
+ }
+
+ public static Relation userAssignedToRole(final UserId user, final RoleId role) {
+ return new Relation<>(user, USER_ASSIGNED_TO_ROLE, role);
+ }
+
+ public static Relation roleWithUser(final RoleId role, final UserId user) {
+ return new Relation<>(role, ROLE_WITH_USER, user);
+ }
+
+ public static Relation userAssignedToGroup(final UserId user, final GroupId group) {
+ return new Relation<>(user, USER_ASSIGNED_TO_GROUP, group);
+ }
+
+ public static Relation groupWithMember(final GroupId group, final UserId user) {
+ return new Relation<>(group, GROUP_HAS_USER_MEMBER, user);
+ }
+
+ enum TYPE {
+ GROUP_HAS_MEMBER,
+ GROUP_HAS_PARENT,
+ ROLE_HAS_PERMISSION,
+ PERMISSION_IS_ATTACHED_TO_ROLE,
+ GROUP_ASSIGNED_TO_ROLE,
+ ROLE_WITH_GROUP,
+ USER_ASSIGNED_TO_ROLE,
+ ROLE_WITH_USER,
+ USER_ASSIGNED_TO_GROUP,
+ GROUP_HAS_USER_MEMBER
+ }
+
+ private Relation(final L left, final TYPE type, final R right) {
+ this.left = left;
+ this.type = type;
+ this.right = right;
+ }
+
+ public Relation invert() {
+ final HashMap inversions = new HashMap() {{
+ put(GROUP_HAS_MEMBER, GROUP_HAS_PARENT);
+ put(GROUP_HAS_PARENT, GROUP_HAS_MEMBER);
+ put(ROLE_HAS_PERMISSION, PERMISSION_IS_ATTACHED_TO_ROLE);
+ put(PERMISSION_IS_ATTACHED_TO_ROLE, ROLE_HAS_PERMISSION);
+ put(GROUP_ASSIGNED_TO_ROLE, ROLE_WITH_GROUP);
+ put(ROLE_WITH_GROUP, GROUP_ASSIGNED_TO_ROLE);
+ put(USER_ASSIGNED_TO_ROLE, ROLE_WITH_USER);
+ put(ROLE_WITH_USER, USER_ASSIGNED_TO_ROLE);
+ put(USER_ASSIGNED_TO_GROUP, GROUP_HAS_USER_MEMBER);
+ put(GROUP_HAS_USER_MEMBER, USER_ASSIGNED_TO_GROUP);
+ }};
+
+ return new Relation<>(right, inversions.get(type), left);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Relation)) return false;
+ Relation, ?> relation = (Relation, ?>) o;
+ return left.equals(relation.left) && type == relation.type && right.equals(relation.right);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(left, type, right);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("left", left)
+ .append("type", type.name())
+ .append("right", right)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionActor.java
new file mode 100644
index 00000000..ca9d6303
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleProjectionActor.java
@@ -0,0 +1,122 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.infrastructure.Events;
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.role.*;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.lattice.model.projection.Projectable;
+import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor;
+import io.vlingo.xoom.symbio.Source;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * See
+ *
+ * StateStoreProjectionActor
+ *
+ */
+public class RoleProjectionActor extends StateStoreProjectionActor {
+
+ private static final RoleView Empty = RoleView.empty();
+
+ public RoleProjectionActor() {
+ this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store);
+ }
+
+ public RoleProjectionActor(final StateStore stateStore) {
+ super(stateStore);
+ }
+
+ @Override
+ protected RoleView currentDataFor(final Projectable projectable) {
+ return Empty;
+ }
+
+ @Override
+ protected RoleView merge(final RoleView previousData, final int previousVersion, final RoleView currentData, final int currentVersion) {
+
+ if (previousVersion == currentVersion) return currentData;
+
+ RoleView merged = previousData;
+
+ for (final Source> event : sources()) {
+ switch (Events.valueOf(event.typeName())) {
+ case RoleProvisioned: {
+ final RoleProvisioned typedEvent = typed(event);
+ merged = RoleView.from(typedEvent.roleId, typedEvent.name, typedEvent.description);
+ break;
+ }
+
+ case RoleDescriptionChanged: {
+ final RoleDescriptionChanged typedEvent = typed(event);
+ merged = RoleView.from(typedEvent.roleId, previousData.name, typedEvent.description, previousData.permissions, previousData.groups, previousData.users);
+ break;
+ }
+
+ case GroupAssignedToRole: {
+ final GroupAssignedToRole typedEvent = typed(event);
+ final Set> groups = concat(previousData.groups, Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId));
+ merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, groups, previousData.users);
+ break;
+ }
+
+ case GroupUnassignedFromRole: {
+ final GroupUnassignedFromRole typedEvent = typed(event);
+ final Set> groups = filter(previousData.groups, g -> !g.equals(Relation.groupAssignedToRole(typedEvent.groupId, typedEvent.roleId)));
+ merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, groups, previousData.users);
+ break;
+ }
+
+ case UserAssignedToRole: {
+ final UserAssignedToRole typedEvent = typed(event);
+ final Set> users = concat(previousData.users, Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId));
+ merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, previousData.groups, users);
+ break;
+ }
+
+ case UserUnassignedFromRole: {
+ final UserUnassignedFromRole typedEvent = typed(event);
+ final Set> users = filter(previousData.users, u -> !u.equals(Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId)));
+ merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, previousData.permissions, previousData.groups, users);
+ break;
+ }
+
+ case RolePermissionAttached: {
+ final RolePermissionAttached typedEvent = typed(event);
+ final Set> permissions = concat(previousData.permissions, Relation.permissionAttachedToRole(typedEvent.permissionId, typedEvent.roleId));
+ merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, permissions, previousData.groups, previousData.users);
+ break;
+ }
+
+ case RolePermissionDetached: {
+ final RolePermissionDetached typedEvent = typed(event);
+ final Set> permissions = filter(previousData.permissions, p -> !p.equals(Relation.permissionAttachedToRole(typedEvent.permissionId, typedEvent.roleId)));
+ merged = RoleView.from(typedEvent.roleId, previousData.name, previousData.description, permissions, previousData.groups, previousData.users);
+ break;
+ }
+
+ default:
+ logger().warn("Event of type " + event.typeName() + " was not matched.");
+ break;
+ }
+ }
+
+ return merged;
+ }
+
+
+ private Set filter(final Set items, final Predicate predicate) {
+ return items.stream().filter(predicate).collect(Collectors.toSet());
+ }
+
+ private Set concat(final Set items, final T item) {
+ return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueries.java
new file mode 100644
index 00000000..8fcdde89
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueries.java
@@ -0,0 +1,12 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.common.Completes;
+
+@SuppressWarnings("all")
+public interface RoleQueries {
+ Completes roleOf(RoleId roleId);
+ Completes> roles();
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesActor.java
new file mode 100644
index 00000000..c1cdaa00
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleQueriesActor.java
@@ -0,0 +1,33 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.query.StateStoreQueryActor;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+
+import io.vlingo.xoom.auth.infrastructure.RoleData;
+
+/**
+ * See Querying a StateStore
+ */
+@SuppressWarnings("all")
+public class RoleQueriesActor extends StateStoreQueryActor implements RoleQueries {
+
+ public RoleQueriesActor(StateStore store) {
+ super(store);
+ }
+
+ @Override
+ public Completes roleOf(RoleId roleId) {
+ return queryStateFor(roleId.idString(), RoleView.class, RoleView.empty());
+ }
+
+ @Override
+ public Completes> roles() {
+ return streamAllOf(RoleView.class, new ArrayList<>());
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleView.java
new file mode 100644
index 00000000..259b7626
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/RoleView.java
@@ -0,0 +1,91 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.role.RoleState;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class RoleView {
+ public final String id;
+ public final String tenantId;
+ public final String name;
+ public final String description;
+ public final Set> permissions;
+ public final Set> groups;
+ public final Set> users;
+
+ public static RoleView empty() {
+ return from(RoleId.from(TenantId.from(""), ""), "", "");
+ }
+
+ public static RoleView from(final RoleState roleState) {
+ return from(roleState.roleId, roleState.name, roleState.description);
+ }
+
+ public static RoleView from(final TenantId tenantId, final String name, final String description) {
+ return new RoleView(tenantId, name, description, new HashSet<>(), new HashSet<>(), new HashSet<>());
+ }
+
+ public static RoleView from(final RoleId roleId, final String name, final String description) {
+ return new RoleView(roleId, name, description, new HashSet<>(), new HashSet<>(), new HashSet<>());
+ }
+
+ public static RoleView from(final RoleId roleId, final String name, final String description, final Set> permissions, final Set> groups, final Set> users) {
+ return new RoleView(roleId, name, description, permissions, groups, users);
+ }
+
+ private RoleView(final TenantId tenantId, final String name, final String description, final Set> permissions, final Set> groups, final Set> users) {
+ this.id = null;
+ this.tenantId = tenantId.idString();
+ this.name = name;
+ this.description = description;
+ this.permissions = permissions;
+ this.groups = groups;
+ this.users = users;
+ }
+
+ private RoleView(final RoleId roleId, final String name, final String description, final Set> permissions, final Set> groups, final Set> users) {
+ this.id = roleId.idString();
+ this.tenantId = roleId.tenantId.idString();
+ this.name = name;
+ this.description = description;
+ this.permissions = permissions;
+ this.groups = groups;
+ this.users = users;
+ }
+
+ public boolean isInRole(final UserId userId) {
+ return users.stream().filter(u -> u.left.equals(userId)).findFirst().isPresent();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof RoleView)) return false;
+ RoleView roleView = (RoleView) o;
+ return id.equals(roleView.id) && tenantId.equals(roleView.tenantId) && name.equals(roleView.name) && description.equals(roleView.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, tenantId, name, description);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("tenantId", tenantId)
+ .append("name", name)
+ .append("description", description)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionActor.java
new file mode 100644
index 00000000..8ef46e3c
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantProjectionActor.java
@@ -0,0 +1,82 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.tenant.*;
+import io.vlingo.xoom.auth.infrastructure.*;
+
+import io.vlingo.xoom.lattice.model.projection.Projectable;
+import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor;
+import io.vlingo.xoom.symbio.Source;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+/**
+ * See
+ *
+ * StateStoreProjectionActor
+ *
+ */
+public class TenantProjectionActor extends StateStoreProjectionActor {
+
+ private static final TenantData Empty = TenantData.empty();
+
+ public TenantProjectionActor() {
+ this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store);
+ }
+
+ public TenantProjectionActor(final StateStore stateStore) {
+ super(stateStore);
+ }
+
+ @Override
+ protected TenantData currentDataFor(final Projectable projectable) {
+ return Empty;
+ }
+
+ @Override
+ protected TenantData merge(final TenantData previousData, final int previousVersion, final TenantData currentData, final int currentVersion) {
+
+ if (previousVersion == currentVersion) return currentData;
+
+ TenantData merged = previousData;
+
+ for (final Source> event : sources()) {
+ switch (Events.valueOf(event.typeName())) {
+ case TenantSubscribed: {
+ final TenantSubscribed typedEvent = typed(event);
+ merged = TenantData.from(typedEvent.tenantId, typedEvent.name, typedEvent.description, typedEvent.active);
+ break;
+ }
+
+ case TenantActivated: {
+ final TenantActivated typedEvent = typed(event);
+ merged = TenantData.from(typedEvent.tenantId, previousData.name, previousData.description, true);
+ break;
+ }
+
+ case TenantDeactivated: {
+ final TenantDeactivated typedEvent = typed(event);
+ merged = TenantData.from(typedEvent.tenantId, previousData.name, previousData.description, false);
+ break;
+ }
+
+ case TenantNameChanged: {
+ final TenantNameChanged typedEvent = typed(event);
+ merged = TenantData.from(typedEvent.tenantId, typedEvent.name, previousData.description, previousData.active);
+ break;
+ }
+
+ case TenantDescriptionChanged: {
+ final TenantDescriptionChanged typedEvent = typed(event);
+ merged = TenantData.from(typedEvent.tenantId, previousData.name, typedEvent.description, previousData.active);
+ break;
+ }
+
+ default:
+ logger().warn("Event of type " + event.typeName() + " was not matched.");
+ break;
+ }
+ }
+
+ return merged;
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueries.java
new file mode 100644
index 00000000..9efd1088
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueries.java
@@ -0,0 +1,14 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.common.Completes;
+
+import io.vlingo.xoom.auth.infrastructure.TenantData;
+
+@SuppressWarnings("all")
+public interface TenantQueries {
+ Completes tenantOf(TenantId tenantId);
+ Completes> tenants();
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesActor.java
new file mode 100644
index 00000000..d22b9d29
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/TenantQueriesActor.java
@@ -0,0 +1,33 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.query.StateStoreQueryActor;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+
+import io.vlingo.xoom.auth.infrastructure.TenantData;
+
+/**
+ * See Querying a StateStore
+ */
+@SuppressWarnings("all")
+public class TenantQueriesActor extends StateStoreQueryActor implements TenantQueries {
+
+ public TenantQueriesActor(StateStore store) {
+ super(store);
+ }
+
+ @Override
+ public Completes tenantOf(TenantId tenantId) {
+ return queryStateFor(tenantId.idString(), TenantData.class, TenantData.empty());
+ }
+
+ @Override
+ public Completes> tenants() {
+ return streamAllOf(TenantData.class, new ArrayList<>());
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionActor.java
new file mode 100644
index 00000000..906afe2d
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserProjectionActor.java
@@ -0,0 +1,163 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.infrastructure.Events;
+import io.vlingo.xoom.auth.infrastructure.persistence.UserView.CredentialView;
+import io.vlingo.xoom.auth.infrastructure.persistence.UserView.ProfileView;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.role.UserAssignedToRole;
+import io.vlingo.xoom.auth.model.role.UserUnassignedFromRole;
+import io.vlingo.xoom.auth.model.user.*;
+import io.vlingo.xoom.lattice.model.projection.Projectable;
+import io.vlingo.xoom.lattice.model.projection.StateStoreProjectionActor;
+import io.vlingo.xoom.symbio.Source;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * See
+ *
+ * StateStoreProjectionActor
+ *
+ */
+public class UserProjectionActor extends StateStoreProjectionActor {
+
+ private static final UserView Empty = UserView.empty();
+
+ public UserProjectionActor() {
+ this(ComponentRegistry.withType(QueryModelStateStoreProvider.class).store);
+ }
+
+ public UserProjectionActor(final StateStore stateStore) {
+ super(stateStore);
+ }
+
+ @Override
+ protected UserView currentDataFor(final Projectable projectable) {
+ return Empty;
+ }
+
+ @Override
+ protected UserView merge(final UserView previousData, final int previousVersion, final UserView currentData, final int currentVersion) {
+
+ if (previousVersion == currentVersion) return currentData;
+
+ UserView merged = previousData;
+
+ for (final Source> event : sources()) {
+ switch (Events.valueOf(event.typeName())) {
+ case UserRegistered: {
+ final UserRegistered typedEvent = typed(event);
+ final ProfileView profile = ProfileView.from(typedEvent.profile);
+ final Set credentials = typedEvent.credentials.stream().map(c -> CredentialView.from(c)).collect(Collectors.toSet());
+ merged = UserView.from(typedEvent.userId, typedEvent.username, profile, credentials, typedEvent.active, Collections.emptySet());
+ break;
+ }
+
+ case UserActivated: {
+ final UserActivated typedEvent = typed(event);
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, true, previousData.roles);
+ break;
+ }
+
+ case UserDeactivated: {
+ final UserDeactivated typedEvent = typed(event);
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, false, previousData.roles);
+ break;
+ }
+
+ case UserCredentialAdded: {
+ final UserCredentialAdded typedEvent = typed(event);
+ final CredentialView credential = CredentialView.from(typedEvent.credential);
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, includeCredential(previousData.credentials, credential), previousData.active, previousData.roles);
+ break;
+ }
+
+ case UserCredentialRemoved: {
+ final UserCredentialRemoved typedEvent = typed(event);
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, removeCredential(previousData.credentials, typedEvent.authority), previousData.active, previousData.roles);
+ break;
+ }
+
+ case UserCredentialReplaced: {
+ final UserCredentialReplaced typedEvent = typed(event);
+ final CredentialView credential = CredentialView.from(typedEvent.credential);
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, includeCredential(removeCredential(previousData.credentials, typedEvent.authority), credential), previousData.active, previousData.roles);
+ break;
+ }
+
+ case UserProfileReplaced: {
+ final UserProfileReplaced typedEvent = typed(event);
+ final ProfileView profile = ProfileView.from(typedEvent.profile);
+ merged = UserView.from(typedEvent.userId, previousData.username, profile, previousData.credentials, previousData.active, previousData.roles);
+ break;
+ }
+
+ case UserAssignedToRole: {
+ final UserAssignedToRole typedEvent = typed(event);
+ final Set> roles = concat(previousData.roles, Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId));
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, previousData.active, roles);
+ break;
+ }
+
+ case UserUnassignedFromRole: {
+ final UserUnassignedFromRole typedEvent = typed(event);
+ final Set> roles = filter(previousData.roles, r -> !r.equals(Relation.userAssignedToRole(typedEvent.userId, typedEvent.roleId)));
+ merged = UserView.from(typedEvent.userId, previousData.username, previousData.profile, previousData.credentials, previousData.active, roles);
+ break;
+ }
+
+ default:
+ logger().warn("Event of type " + event.typeName() + " was not matched.");
+ break;
+ }
+ }
+
+ return merged;
+ }
+
+ @Override
+ protected String dataIdFor(final Projectable projectable) {
+ final Source> firstEvent = sources().get(0);
+ switch (Events.valueOf(firstEvent.typeName())) {
+ case UserAssignedToRole:
+ return this.typed(firstEvent).userId.idString();
+ case UserUnassignedFromRole:
+ return this.typed(firstEvent).userId.idString();
+ default:
+ return super.dataIdFor(projectable);
+ }
+ }
+
+ @Override
+ protected int currentDataVersionFor(final Projectable projectable, final UserView previousData, final int previousVersion) {
+ switch (Events.valueOf(sources().get(0).typeName())) {
+ case UserAssignedToRole:
+ case UserUnassignedFromRole:
+ return previousVersion + 1;
+ default:
+ return super.currentDataVersionFor(projectable, previousData, previousVersion);
+ }
+ }
+
+ private Set includeCredential(final Set credentials, final CredentialView credential) {
+ return concat(credentials, credential);
+ }
+
+ private Set removeCredential(final Set credentials, final String authority) {
+ return filter(credentials, c -> !c.authority.equals(authority));
+ }
+
+ private Set filter(final Set items, final Predicate predicate) {
+ return items.stream().filter(predicate).collect(Collectors.toSet());
+ }
+
+ private Set concat(final Set items, final T item) {
+ return Stream.concat(items.stream(), Stream.of(item)).collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueries.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueries.java
new file mode 100644
index 00000000..9b869b34
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueries.java
@@ -0,0 +1,12 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import java.util.Collection;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.Completes;
+
+@SuppressWarnings("all")
+public interface UserQueries {
+ Completes userOf(UserId userId);
+ Completes> users();
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesActor.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesActor.java
new file mode 100644
index 00000000..35ae87c8
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserQueriesActor.java
@@ -0,0 +1,31 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.query.StateStoreQueryActor;
+import io.vlingo.xoom.symbio.store.state.StateStore;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * See Querying a StateStore
+ */
+@SuppressWarnings("all")
+public class UserQueriesActor extends StateStoreQueryActor implements UserQueries {
+
+ public UserQueriesActor(StateStore store) {
+ super(store);
+ }
+
+ @Override
+ public Completes userOf(UserId userId) {
+ return queryStateFor(userId.idString(), UserView.class, UserView.empty());
+ }
+
+ @Override
+ public Completes> users() {
+ return streamAllOf(UserView.class, new ArrayList<>());
+ }
+
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserView.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserView.java
new file mode 100644
index 00000000..d85396c4
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/persistence/UserView.java
@@ -0,0 +1,247 @@
+package io.vlingo.xoom.auth.infrastructure.persistence;
+
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.auth.model.value.Credential;
+import io.vlingo.xoom.auth.model.value.PersonName;
+import io.vlingo.xoom.auth.model.value.Profile;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class UserView {
+ public final String id;
+ public final String tenantId;
+ public final String username;
+ public final ProfileView profile;
+ public final boolean active;
+ public final Set credentials;
+ public final Set> roles;
+
+ public static UserView empty() {
+ return from(UserId.from(TenantId.from(""), ""), "", null, Collections.emptySet(), false, Collections.emptySet());
+ }
+
+ public static UserView from(final UserId userId, final String username, final ProfileView profile, final Set credentials, final boolean active, final Set> roles) {
+ return new UserView(userId, username, profile, credentials, active, roles);
+ }
+
+ public static UserView from(final UserId userId, final String username, final ProfileView profile, final CredentialView credential, final boolean active, final Set> roles) {
+ return new UserView(userId, username, profile, Stream.of(credential).collect(Collectors.toSet()), active, roles);
+ }
+
+ private UserView(String id, String tenantId, String username, ProfileView profile, Set credentials, boolean active, final Set> roles) {
+ this.id = id;
+ this.tenantId = tenantId;
+ this.username = username;
+ this.profile = profile;
+ this.active = active;
+ this.credentials = credentials;
+ this.roles = roles;
+ }
+
+ private UserView(UserId userId, String username, ProfileView profile, Set credentials, boolean active, final Set> roles) {
+ this(userId.idString(), userId.tenantId.id, username, profile, credentials, active, roles);
+ }
+
+ public boolean isInRole(final RoleId roleId) {
+ return roles.stream().filter(r -> r.right.equals(roleId)).findFirst().isPresent();
+ }
+
+ public static class CredentialView {
+ public final String authority;
+ public final String id;
+ public final String secret;
+ public final String type;
+
+ public static CredentialView empty() {
+ return new CredentialView(null, null, null, null);
+ }
+
+ public static CredentialView from(final Credential credential) {
+ if (credential == null) {
+ return CredentialView.empty();
+ } else {
+ return from(credential.authority, credential.id, credential.secret, credential.type.name());
+ }
+ }
+
+ public static CredentialView from(final String authority, final String id, final String secret) {
+ return from(authority, id, secret, null);
+ }
+
+ public static CredentialView from(final String authority, final String id, final String secret, final String type) {
+ return new CredentialView(authority, id, secret, type);
+ }
+
+ public static Set fromAll(final Set correspondingObjects) {
+ return correspondingObjects == null ? Collections.emptySet() : correspondingObjects.stream().map(CredentialView::from).collect(Collectors.toSet());
+ }
+
+ private CredentialView(String authority, String id, String secret, String type) {
+ this.authority = authority;
+ this.id = id;
+ this.secret = secret;
+ this.type = type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CredentialView)) return false;
+ CredentialView that = (CredentialView) o;
+ return Objects.equals(authority, that.authority) && Objects.equals(id, that.id) && Objects.equals(secret, that.secret) && Objects.equals(type, that.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(authority, id, secret, type);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("authority", authority)
+ .append("secret", secret)
+ .append("type", type)
+ .toString();
+ }
+ }
+
+ public static class ProfileView {
+ public final String emailAddress;
+ public final PersonNameView name;
+ public final String phone;
+
+ public static ProfileView empty() {
+ return new ProfileView(null, null, null);
+ }
+
+ public static ProfileView from(final Profile profile) {
+ if (profile == null) {
+ return ProfileView.empty();
+ } else {
+ final PersonNameView name = profile.name != null ? PersonNameView.from(profile.name) : null;
+ return from(name, profile.emailAddress, profile.phone);
+ }
+ }
+
+ public static ProfileView from(final PersonNameView name, final String emailAddress, final String phone) {
+ return new ProfileView(emailAddress, name, phone);
+ }
+
+ public static ProfileView from(final String emailAddress, final PersonNameView name, final String phone) {
+ return new ProfileView(emailAddress, name, phone);
+ }
+
+ private ProfileView(final String emailAddress, final PersonNameView name, final String phone) {
+ this.emailAddress = emailAddress;
+ this.name = name;
+ this.phone = phone;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ProfileView)) return false;
+ ProfileView that = (ProfileView) o;
+ return Objects.equals(emailAddress, that.emailAddress) && Objects.equals(name, that.name) && Objects.equals(phone, that.phone);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(emailAddress, name, phone);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("emailAddress", emailAddress)
+ .append("name", name)
+ .append("phone", phone)
+ .toString();
+ }
+ }
+
+ public static class PersonNameView {
+ public final String given;
+ public final String family;
+ public final String second;
+
+ public static PersonNameView empty() {
+ return new PersonNameView(null, null, null);
+ }
+
+ public static PersonNameView from(final PersonName personName) {
+ if (personName == null) {
+ return PersonNameView.empty();
+ } else {
+ return from(personName.given, personName.family, personName.second);
+ }
+ }
+
+ public static PersonNameView from(final String given, final String family, final String second) {
+ return new PersonNameView(given, family, second);
+ }
+
+ private PersonNameView(final String given, final String family, final String second) {
+ this.given = given;
+ this.family = family;
+ this.second = second;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof PersonNameView)) return false;
+ PersonNameView that = (PersonNameView) o;
+ return Objects.equals(given, that.given) && Objects.equals(family, that.family) && Objects.equals(second, that.second);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(given, family, second);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("given", given)
+ .append("family", family)
+ .append("second", second)
+ .toString();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof UserView)) return false;
+ UserView userView = (UserView) o;
+ return active == userView.active && Objects.equals(id, userView.id) && Objects.equals(tenantId, userView.tenantId) && Objects.equals(username, userView.username) && Objects.equals(profile, userView.profile) && Objects.equals(credentials, userView.credentials);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, tenantId, username, profile, active, credentials);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("id", id)
+ .append("tenantId", tenantId)
+ .append("username", username)
+ .append("profile", profile)
+ .append("active", active)
+ .append("credentials", credentials)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddress.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddress.java
new file mode 100644
index 00000000..706fba6a
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/ComplexAddress.java
@@ -0,0 +1,60 @@
+package io.vlingo.xoom.auth.infrastructure.resource;
+
+import io.vlingo.xoom.actors.Address;
+
+import java.util.function.Function;
+
+public class ComplexAddress implements Address {
+ private final ID id;
+ private final Function transformer;
+
+ public ComplexAddress(ID id, Function transformer) {
+ this.id = id;
+ this.transformer = transformer;
+ }
+
+ @Override
+ public long id() {
+ byte[] idBytes = transformer.apply(id).getBytes();
+ long outcome = 0;
+ for (int i = 0; i < idBytes.length; i++) {
+ outcome = ((outcome << 8) | (idBytes[i] & 0xFF));
+ }
+ return outcome;
+ }
+
+ @Override
+ public long idSequence() {
+ return id();
+ }
+
+ @Override
+ public String idSequenceString() {
+ return idString();
+ }
+
+ @Override
+ public String idString() {
+ return String.valueOf(id());
+ }
+
+ @Override
+ public T idTyped() {
+ return (T) id;
+ }
+
+ @Override
+ public String name() {
+ return "complex";
+ }
+
+ @Override
+ public boolean isDistributable() {
+ return true;
+ }
+
+ @Override
+ public int compareTo(Address o) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResource.java
new file mode 100644
index 00000000..010dbf82
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/GroupResource.java
@@ -0,0 +1,176 @@
+package io.vlingo.xoom.auth.infrastructure.resource;
+
+import io.vlingo.xoom.actors.Address;
+import io.vlingo.xoom.actors.Definition;
+import io.vlingo.xoom.auth.infrastructure.GroupData;
+import io.vlingo.xoom.auth.infrastructure.persistence.GroupQueries;
+import io.vlingo.xoom.auth.infrastructure.persistence.GroupView;
+import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider;
+import io.vlingo.xoom.auth.model.group.Group;
+import io.vlingo.xoom.auth.model.group.GroupEntity;
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.http.ContentType;
+import io.vlingo.xoom.http.Response;
+import io.vlingo.xoom.http.ResponseHeader;
+import io.vlingo.xoom.http.resource.DynamicResourceHandler;
+import io.vlingo.xoom.http.resource.Resource;
+import io.vlingo.xoom.lattice.grid.Grid;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import java.nio.CharBuffer;
+import java.util.UUID;
+
+import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized;
+import static io.vlingo.xoom.http.Response.Status.*;
+import static io.vlingo.xoom.http.ResponseHeader.Location;
+import static io.vlingo.xoom.http.resource.ResourceBuilder.resource;
+
+/**
+ * See @ResourceHandlers
+ */
+public class GroupResource extends DynamicResourceHandler {
+ private final Grid grid;
+ private final GroupQueries $queries;
+
+ public GroupResource(final Grid grid) {
+ super(grid.world().stage());
+ this.grid = grid;
+ this.$queries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).groupQueries;
+ }
+
+ public Completes provisionGroup(final GroupData data) {
+ return create(data.tenantId, data.name)
+ .provisionGroup(data.name, data.description)
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.id))), serialized(GroupView.from(state))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())));
+ }
+
+ public Completes changeDescription(final String tenantId, final String groupName, final GroupData data) {
+ return resolve(tenantId, groupName)
+ .andThenTo(group -> group.changeDescription(data.description))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes assignGroup(final String tenantId, final String groupName, final String innerGroupName, final GroupData data) {
+ return resolve(tenantId, groupName)
+ .andThenTo(group -> group.assignGroup(GroupId.from(TenantId.from(tenantId), innerGroupName)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes unassignGroup(final String tenantId, final String groupName, final String innerGroupName, final GroupData data) {
+ return resolve(tenantId, groupName)
+ .andThenTo(group -> group.unassignGroup(GroupId.from(TenantId.from(tenantId), innerGroupName)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes assignUser(final String tenantId, final String groupName, final String username, final GroupData data) {
+ return resolve(tenantId, groupName)
+ .andThenTo(group -> group.assignUser(UserId.from(TenantId.from(data.tenantId), username)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes unassignUser(final String tenantId, final String groupName, final String username, final GroupData data) {
+ return resolve(tenantId, groupName)
+ .andThenTo(group -> group.unassignUser(UserId.from(TenantId.from(data.tenantId), username)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(GroupView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes groups() {
+ return $queries.groups()
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes groupOf(final String tenantId, final String groupName) {
+ return $queries.groupOf(GroupId.from(TenantId.from(tenantId), groupName))
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ @Override
+ public Resource> routes() {
+ return resource("GroupResource",
+ io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/groups")
+ .body(GroupData.class)
+ .handle(this::provisionGroup),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/groups/{groupName}/description")
+ .param(String.class)
+ .param(String.class)
+ .body(GroupData.class)
+ .handle(this::changeDescription),
+ io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/groups/{groupName}/groups/{innerGroupName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .body(GroupData.class)
+ .handle(this::assignGroup),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/groups/{groupName}/groups/{innerGroupName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .body(GroupData.class)
+ .handle(this::unassignGroup),
+ io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/groups/{groupName}/users/{username}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .body(GroupData.class)
+ .handle(this::assignUser),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/groups/{groupName}/users/{username}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .body(GroupData.class)
+ .handle(this::unassignUser),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/groups")
+ .handle(this::groups),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/groups/{groupName}")
+ .param(String.class)
+ .param(String.class)
+ .handle(this::groupOf)
+ );
+ }
+
+ @Override
+ protected ContentType contentType() {
+ return ContentType.of("application/json", "charset=UTF-8");
+ }
+
+ private String location(final GroupId groupId) {
+ return String.format("/tenants/%s/groups/%s", groupId.tenantId.id, groupId.groupName);
+ }
+
+ private Completes resolve(final String tenantId, final String groupName) {
+ final GroupId groupId = GroupId.from(TenantId.from(tenantId), groupName);
+ final Address address = new GroupAddress(groupId);
+ return grid.actorOf(Group.class, address, Definition.has(GroupEntity.class, Definition.parameters(groupId)));
+ }
+
+ private Group create(final String tenantId, final String groupName) {
+ final GroupId groupId = GroupId.from(TenantId.from(tenantId), groupName);
+ final Address address = new GroupAddress(groupId);
+ return grid.actorFor(Group.class, Definition.has(GroupEntity.class, Definition.parameters(groupId)), address);
+ }
+
+ private class GroupAddress extends ComplexAddress {
+ public GroupAddress(final GroupId groupId) {
+ super(groupId, (id) -> id.idString());
+ }
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResource.java
new file mode 100644
index 00000000..00ebf741
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/PermissionResource.java
@@ -0,0 +1,157 @@
+package io.vlingo.xoom.auth.infrastructure.resource;
+
+import io.vlingo.xoom.actors.Address;
+import io.vlingo.xoom.actors.Definition;
+import io.vlingo.xoom.auth.infrastructure.ConstraintData;
+import io.vlingo.xoom.auth.infrastructure.PermissionData;
+import io.vlingo.xoom.auth.infrastructure.persistence.PermissionQueries;
+import io.vlingo.xoom.auth.infrastructure.persistence.PermissionView;
+import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider;
+import io.vlingo.xoom.auth.model.permission.Permission;
+import io.vlingo.xoom.auth.model.permission.PermissionEntity;
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.http.ContentType;
+import io.vlingo.xoom.http.Response;
+import io.vlingo.xoom.http.ResponseHeader;
+import io.vlingo.xoom.http.resource.DynamicResourceHandler;
+import io.vlingo.xoom.http.resource.Resource;
+import io.vlingo.xoom.lattice.grid.Grid;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized;
+import static io.vlingo.xoom.http.Response.Status.*;
+import static io.vlingo.xoom.http.ResponseHeader.Location;
+import static io.vlingo.xoom.http.resource.ResourceBuilder.resource;
+
+/**
+ * See @ResourceHandlers
+ */
+public class PermissionResource extends DynamicResourceHandler {
+ private final Grid grid;
+ private final PermissionQueries $queries;
+
+ public PermissionResource(final Grid grid) {
+ super(grid.world().stage());
+ this.grid = grid;
+ this.$queries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).permissionQueries;
+ }
+
+ public Completes provisionPermission(final PermissionData data) {
+ return create(data.tenantId, data.name)
+ .provisionPermission(data.name, data.description)
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.id))), serialized(PermissionView.from(state))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())));
+ }
+
+ public Completes enforce(final String tenantId, final String permissionName, final ConstraintData data) {
+ return resolve(tenantId, permissionName)
+ .andThenTo(permission -> permission.enforce(data.toConstraint()))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes enforceReplacement(final String tenantId, final String permissionName, final String constraintName, final ConstraintData data) {
+ return resolve(tenantId, permissionName)
+ .andThenTo(permission -> permission.enforceReplacement(constraintName, data.toConstraint()))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes forget(final String tenantId, final String permissionName, final String constraintName) {
+ return resolve(tenantId, permissionName)
+ .andThenTo(permission -> permission.forget(constraintName))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes changeDescription(final String tenantId, final String permissionName, final PermissionData data) {
+ return resolve(tenantId, permissionName)
+ .andThenTo(permission -> permission.changeDescription(data.description))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(PermissionView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes permissions() {
+ return $queries.permissions()
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes permissionOf(final String tenantId, final String permissionName) {
+ return $queries.permissionOf(PermissionId.from(TenantId.from(tenantId), permissionName))
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ @Override
+ public Resource> routes() {
+ return resource("PermissionResource",
+ io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/permissions")
+ .body(PermissionData.class)
+ .handle(this::provisionPermission),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/permissions/{permissionName}/constraints")
+ .param(String.class)
+ .param(String.class)
+ .body(ConstraintData.class)
+ .handle(this::enforce),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/permissions/{permissionName}/constraints/{constraintName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .body(ConstraintData.class)
+ .handle(this::enforceReplacement),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/permissions/{permissionName}/constraints/{constraintName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::forget),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/permissions/{permissionName}/description")
+ .param(String.class)
+ .param(String.class)
+ .body(PermissionData.class)
+ .handle(this::changeDescription),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/permissions")
+ .handle(this::permissions),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/permissions/{permissionName}")
+ .param(String.class)
+ .param(String.class)
+ .handle(this::permissionOf)
+ );
+ }
+
+ @Override
+ protected ContentType contentType() {
+ return ContentType.of("application/json", "charset=UTF-8");
+ }
+
+ private String location(final PermissionId permissionId) {
+ return String.format("/tenants/%s/permissions/%s", permissionId.tenantId.id, permissionId.permissionName);
+ }
+
+ private Completes resolve(final String tenantId, final String permissionName) {
+ final PermissionId permissionId = PermissionId.from(TenantId.from(tenantId), permissionName);
+ final Address address = new PermissionAddress(permissionId);
+ return grid.actorOf(Permission.class, address, Definition.has(PermissionEntity.class, Definition.parameters(permissionId)));
+ }
+
+ private Permission create(final String tenantId, final String permissionName) {
+ final PermissionId permissionId = PermissionId.from(TenantId.from(tenantId), permissionName);
+ final Address address = new PermissionAddress(permissionId);
+ return grid.actorFor(Permission.class, Definition.has(PermissionEntity.class, Definition.parameters(permissionId)), address);
+ }
+
+ private class PermissionAddress extends ComplexAddress {
+ public PermissionAddress(final PermissionId permissionId) {
+ super(permissionId, (id) -> id.idString());
+ }
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResource.java
new file mode 100644
index 00000000..d707a2df
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/RoleResource.java
@@ -0,0 +1,262 @@
+package io.vlingo.xoom.auth.infrastructure.resource;
+
+import io.vlingo.xoom.actors.Address;
+import io.vlingo.xoom.actors.Definition;
+import io.vlingo.xoom.auth.infrastructure.*;
+import io.vlingo.xoom.auth.infrastructure.persistence.*;
+import io.vlingo.xoom.auth.model.group.GroupId;
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.role.Role;
+import io.vlingo.xoom.auth.model.role.RoleEntity;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.http.ContentType;
+import io.vlingo.xoom.http.Response;
+import io.vlingo.xoom.http.ResponseHeader;
+import io.vlingo.xoom.http.resource.DynamicResourceHandler;
+import io.vlingo.xoom.http.resource.Resource;
+import io.vlingo.xoom.lattice.grid.Grid;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized;
+import static io.vlingo.xoom.http.Response.Status.*;
+import static io.vlingo.xoom.http.ResponseHeader.Location;
+import static io.vlingo.xoom.http.resource.ResourceBuilder.resource;
+
+/**
+ * See @ResourceHandlers
+ */
+public class RoleResource extends DynamicResourceHandler {
+ private final Grid grid;
+ private final RoleQueries $roleQueries;
+ private final PermissionQueries $permissionQueries;
+ private final GroupQueries $groupQueries;
+ private final UserQueries $userQueries;
+
+ public RoleResource(final Grid grid) {
+ super(grid.world().stage());
+ this.grid = grid;
+ this.$roleQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).roleQueries;
+ this.$permissionQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).permissionQueries;
+ this.$groupQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).groupQueries;
+ this.$userQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).userQueries;
+ }
+
+ public Completes provisionRole(final RoleData data) {
+ return create(data.tenantId, data.name)
+ .provisionRole(data.name, data.description)
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.roleId))), serialized(RoleView.from(state))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())));
+ }
+
+ public Completes changeDescription(final String tenantId, final String roleName, final RoleData data) {
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.changeDescription(data.description))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes assignGroup(final String tenantId, final String roleName, final GroupData groupData) {
+ final GroupId groupId = GroupId.from(TenantId.from(tenantId), groupData.name);
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.assignGroup(groupId))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, roleGroupLocation(state.roleId, groupId))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes unassignGroup(final String tenantId, final String roleName, final String groupName) {
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.unassignGroup(GroupId.from(TenantId.from(tenantId), groupName)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes assignUser(final String tenantId, final String roleName, final UserRegistrationData userData) {
+ final UserId userId = UserId.from(TenantId.from(tenantId), userData.username);
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.assignUser(userId))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, roleUserLocation(state.roleId, userId))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes unassignUser(final String tenantId, final String roleName, final String username) {
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.unassignUser(UserId.from(TenantId.from(tenantId), username)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes attach(final String tenantId, final String roleName, final PermissionData permissionData) {
+ final PermissionId permissionId = PermissionId.from(TenantId.from(tenantId), permissionData.name);
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.attach(permissionId))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, rolePermissionLocation(state.roleId, permissionId))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes detach(final String tenantId, final String roleName, final String permissionName) {
+ return resolve(tenantId, roleName)
+ .andThenTo(role -> role.detach(PermissionId.from(TenantId.from(tenantId), permissionName)))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(RoleView.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes roles() {
+ return $roleQueries.roles()
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes roleOf(final String tenantId, final String roleName) {
+ return $roleQueries.roleOf(RoleId.from(TenantId.from(tenantId), roleName))
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes permissionOf(final String tenantIdString, final String roleName, final String permissionName) {
+ final TenantId tenantId = TenantId.from(tenantIdString);
+ final RoleId roleId = RoleId.from(tenantId, roleName);
+ final PermissionId permissionId = PermissionId.from(tenantId, permissionName);
+ return $permissionQueries.permissionOf(permissionId)
+ .andThen(permissionView -> permissionView.isAttachedTo(roleId) ? permissionView : null)
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes groupOf(final String tenantId, final String roleName, final String groupName) {
+ final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName);
+ final GroupId groupId = GroupId.from(roleId.tenantId, groupName);
+ return $groupQueries.groupOf(groupId)
+ .andThen(groupView -> groupView.isInRole(roleId) ? groupView : null)
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes userOf(final String tenantId, final String roleName, final String username) {
+ final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName);
+ final UserId userId = UserId.from(roleId.tenantId, username);
+ return $userQueries.userOf(userId)
+ .andThen(userView -> userView.isInRole(roleId) ? userView : null)
+ .andThenTo(userView -> Completes.withSuccess(entityResponseOf(Ok, serialized(MinimalUserData.from(userView)))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ @Override
+ public Resource> routes() {
+ return resource("RoleResource",
+ io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/roles")
+ .body(RoleData.class)
+ .handle(this::provisionRole),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/roles/{roleName}/description")
+ .param(String.class)
+ .param(String.class)
+ .body(RoleData.class)
+ .handle(this::changeDescription),
+ io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/roles/{roleName}/groups")
+ .param(String.class)
+ .param(String.class)
+ .body(GroupData.class)
+ .handle(this::assignGroup),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/roles/{roleName}/groups/{groupName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::unassignGroup),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}/groups/{groupName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::groupOf),
+ io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/roles/{roleName}/users")
+ .param(String.class)
+ .param(String.class)
+ .body(UserRegistrationData.class)
+ .handle(this::assignUser),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/roles/{roleName}/users/{username}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::unassignUser),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}/users/{username}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::userOf),
+ io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/roles/{roleName}/permissions")
+ .param(String.class)
+ .param(String.class)
+ .body(PermissionData.class)
+ .handle(this::attach),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/roles/{roleName}/permissions/{permissionName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::detach),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}/permissions/{permissionName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::permissionOf),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles")
+ .handle(this::roles),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/roles/{roleName}")
+ .param(String.class)
+ .param(String.class)
+ .handle(this::roleOf)
+ );
+ }
+
+ @Override
+ protected ContentType contentType() {
+ return ContentType.of("application/json", "charset=UTF-8");
+ }
+
+ private String location(final RoleId roleId) {
+ return String.format("/tenants/%s/roles/%s", roleId.tenantId.id, roleId.roleName);
+ }
+
+ private String roleGroupLocation(final RoleId roleId, final GroupId groupId) {
+ return String.format("/tenants/%s/roles/%s/groups/%s", roleId.tenantId.id, roleId.roleName, groupId.groupName);
+ }
+
+ private String roleUserLocation(final RoleId roleId, final UserId userId) {
+ return String.format("/tenants/%s/roles/%s/users/%s", roleId.tenantId.id, roleId.roleName, userId.username);
+ }
+
+ private String rolePermissionLocation(final RoleId roleId, final PermissionId permissionId) {
+ return String.format("/tenants/%s/roles/%s/permissions/%s", roleId.tenantId.id, roleId.roleName, permissionId.permissionName);
+ }
+
+ private Completes resolve(final String tenantId, String roleName) {
+ final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName);
+ final Address address = new RoleAddress(roleId);
+ return grid.actorOf(Role.class, address, Definition.has(RoleEntity.class, Definition.parameters(roleId)));
+ }
+
+ private Role create(String tenantId, String roleName) {
+ final RoleId roleId = RoleId.from(TenantId.from(tenantId), roleName);
+ final Address address = new RoleAddress(roleId);
+ return grid.actorFor(Role.class, Definition.has(RoleEntity.class, Definition.parameters(roleId)), address);
+ }
+
+ private class RoleAddress extends ComplexAddress {
+ public RoleAddress(final RoleId roleId) {
+ super(roleId, (id) -> id.idString());
+ }
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResource.java
new file mode 100644
index 00000000..bb771b3b
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/TenantResource.java
@@ -0,0 +1,140 @@
+package io.vlingo.xoom.auth.infrastructure.resource;
+
+import io.vlingo.xoom.actors.Definition;
+import io.vlingo.xoom.actors.Address;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.http.ContentType;
+import io.vlingo.xoom.http.Response;
+import io.vlingo.xoom.http.ResponseHeader;
+import io.vlingo.xoom.http.resource.Resource;
+import io.vlingo.xoom.http.resource.DynamicResourceHandler;
+import io.vlingo.xoom.lattice.grid.Grid;
+import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider;
+import io.vlingo.xoom.auth.infrastructure.*;
+import io.vlingo.xoom.auth.model.tenant.TenantEntity;
+import io.vlingo.xoom.auth.infrastructure.persistence.TenantQueries;
+import io.vlingo.xoom.auth.model.tenant.Tenant;
+
+import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized;
+import static io.vlingo.xoom.http.Response.Status.*;
+import static io.vlingo.xoom.http.ResponseHeader.Location;
+import static io.vlingo.xoom.http.resource.ResourceBuilder.resource;
+
+/**
+ * See @ResourceHandlers
+ */
+public class TenantResource extends DynamicResourceHandler {
+ private final Grid grid;
+ private final TenantQueries $queries;
+
+ public TenantResource(final Grid grid) {
+ super(grid.world().stage());
+ this.grid = grid;
+ this.$queries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).tenantQueries;
+ }
+
+ public Completes subscribeFor(final TenantData data) {
+ return create()
+ .subscribeFor(data.name, data.description, data.active)
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.tenantId))), serialized(TenantData.from(state))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())));
+ }
+
+ public Completes activate(final String id) {
+ return resolve(id)
+ .andThenTo(tenant -> tenant.activate())
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes deactivate(final String id) {
+ return resolve(id)
+ .andThenTo(tenant -> tenant.deactivate())
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes changeDescription(final String id, final TenantData data) {
+ return resolve(id)
+ .andThenTo(tenant -> tenant.changeDescription(data.description))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes changeName(final String id, final TenantData data) {
+ return resolve(id)
+ .andThenTo(tenant -> tenant.changeName(data.name))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(TenantData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes tenants() {
+ return $queries.tenants()
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes tenantOf(final String id) {
+ return $queries.tenantOf(TenantId.from(id))
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ @Override
+ public Resource> routes() {
+ return resource("TenantResource",
+ io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants")
+ .body(TenantData.class)
+ .handle(this::subscribeFor),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/activate")
+ .param(String.class)
+ .handle(this::activate),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/deactivate")
+ .param(String.class)
+ .handle(this::deactivate),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/description")
+ .param(String.class)
+ .body(TenantData.class)
+ .handle(this::changeDescription),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{id}/name")
+ .param(String.class)
+ .body(TenantData.class)
+ .handle(this::changeName),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants")
+ .handle(this::tenants),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{id}")
+ .param(String.class)
+ .handle(this::tenantOf)
+ );
+ }
+
+ @Override
+ protected ContentType contentType() {
+ return ContentType.of("application/json", "charset=UTF-8");
+ }
+
+ private String location(final TenantId tenantId) {
+ return String.format("/tenants/%s", tenantId.id);
+ }
+
+ private Completes resolve(final String id) {
+ final TenantId tenantId = TenantId.from(id);
+ final Address address = grid.addressFactory().from(tenantId.idString());
+ return grid.actorOf(Tenant.class, address, Definition.has(TenantEntity.class, Definition.parameters(tenantId)));
+ }
+
+ private Tenant create() {
+ final Address address = grid.addressFactory().uniquePrefixedWith("g-");
+ final TenantId tenantId = TenantId.from(address.idString());
+ return grid.actorFor(Tenant.class, Definition.has(TenantEntity.class, Definition.parameters(tenantId)), address);
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/UserResource.java b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/UserResource.java
new file mode 100644
index 00000000..ec178ca0
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/infrastructure/resource/UserResource.java
@@ -0,0 +1,230 @@
+package io.vlingo.xoom.auth.infrastructure.resource;
+
+import io.vlingo.xoom.actors.Address;
+import io.vlingo.xoom.actors.Definition;
+import io.vlingo.xoom.auth.infrastructure.CredentialData;
+import io.vlingo.xoom.auth.infrastructure.ProfileData;
+import io.vlingo.xoom.auth.infrastructure.UserRegistrationData;
+import io.vlingo.xoom.auth.infrastructure.persistence.PermissionQueries;
+import io.vlingo.xoom.auth.infrastructure.persistence.QueryModelStateStoreProvider;
+import io.vlingo.xoom.auth.infrastructure.persistence.RoleQueries;
+import io.vlingo.xoom.auth.infrastructure.persistence.UserQueries;
+import io.vlingo.xoom.auth.model.permission.PermissionId;
+import io.vlingo.xoom.auth.model.role.RoleId;
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import io.vlingo.xoom.auth.model.user.User;
+import io.vlingo.xoom.auth.model.user.UserEntity;
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.auth.model.value.Credential;
+import io.vlingo.xoom.auth.model.value.PersonName;
+import io.vlingo.xoom.auth.model.value.Profile;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.http.ContentType;
+import io.vlingo.xoom.http.Response;
+import io.vlingo.xoom.http.ResponseHeader;
+import io.vlingo.xoom.http.resource.DynamicResourceHandler;
+import io.vlingo.xoom.http.resource.Resource;
+import io.vlingo.xoom.lattice.grid.Grid;
+import io.vlingo.xoom.turbo.ComponentRegistry;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static io.vlingo.xoom.common.serialization.JsonSerialization.serialized;
+import static io.vlingo.xoom.http.Response.Status.*;
+import static io.vlingo.xoom.http.ResponseHeader.Location;
+import static io.vlingo.xoom.http.resource.ResourceBuilder.resource;
+
+/**
+ * See @ResourceHandlers
+ */
+public class UserResource extends DynamicResourceHandler {
+ private final Grid grid;
+ private final UserQueries $userQueries;
+ private final RoleQueries $roleQueries;
+ private final PermissionQueries $permissionQueries;
+
+ public UserResource(final Grid grid) {
+ super(grid.world().stage());
+ this.grid = grid;
+ this.$userQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).userQueries;
+ this.$roleQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).roleQueries;
+ this.$permissionQueries = ComponentRegistry.withType(QueryModelStateStoreProvider.class).permissionQueries;
+ }
+
+ public Completes registerUser(final UserRegistrationData data) {
+ final PersonName name = PersonName.from(data.profile.name.given, data.profile.name.family, data.profile.name.second);
+ final Profile profile = Profile.from(data.profile.emailAddress, name, data.profile.phone);
+ final Set credentials = data.credentials.stream().map(CredentialData::toCredential).collect(Collectors.toSet());
+ return create(data.tenantId, data.username)
+ .registerUser(data.username, profile, credentials, data.active)
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Created, ResponseHeader.headers(ResponseHeader.of(Location, location(state.userId))), serialized(UserRegistrationData.from(state))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage())));
+ }
+
+ public Completes activate(final String tenantId, final String username, final UserRegistrationData data) {
+ return resolve(tenantId, username)
+ .andThenTo(user -> user.activate())
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes deactivate(final String tenantId, final String username, final UserRegistrationData data) {
+ return resolve(tenantId, username)
+ .andThenTo(user -> user.deactivate())
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes addCredential(final String tenantId, final String username, final CredentialData data) {
+ return resolve(tenantId, username)
+ .andThenTo(user -> user.addCredential(data.toCredential()))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes removeCredential(final String tenantId, final String username, final String authority) {
+ return resolve(tenantId, username)
+ .andThenTo(user -> user.removeCredential(authority))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes replaceCredential(final String tenantId, final String username, final String authority, final CredentialData data) {
+ return resolve(tenantId, username)
+ .andThenTo(user -> user.replaceCredential(authority, data.toCredential()))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes replaceProfile(final String tenantId, final String username, final ProfileData data) {
+ return resolve(tenantId, username)
+ .andThenTo(user -> user.replaceProfile(data.toProfile()))
+ .andThenTo(state -> Completes.withSuccess(entityResponseOf(Ok, serialized(UserRegistrationData.from(state)))))
+ .otherwise(noGreeting -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes users() {
+ return $userQueries.users()
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes userOf(final String tenantId, final String username) {
+ return $userQueries.userOf(UserId.from(TenantId.from(tenantId), username))
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes roleOf(final String tenantId, final String username, final String roleName) {
+ final UserId userId = UserId.from(TenantId.from(tenantId), username);
+ final RoleId roleId = RoleId.from(userId.tenantId, roleName);
+ return $roleQueries.roleOf(roleId)
+ .andThen(roleView -> roleView.isInRole(userId) ? roleView : null)
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ public Completes permissionOf(final String tenantId, final String username, final String permissionName) {
+ final UserId userId = UserId.from(TenantId.from(tenantId), username);
+ final PermissionId permissionId = PermissionId.from(userId.tenantId, permissionName);
+ return $permissionQueries.permissionOf(permissionId)
+ .andThen(permissionView -> permissionView.isAttachedTo(userId) ? permissionView : null)
+ .andThenTo(data -> Completes.withSuccess(entityResponseOf(Ok, serialized(data))))
+ .otherwise(arg -> Response.of(NotFound))
+ .recoverFrom(e -> Response.of(InternalServerError, e.getMessage()));
+ }
+
+ @Override
+ public Resource> routes() {
+ return resource("UserResource",
+ io.vlingo.xoom.http.resource.ResourceBuilder.post("/tenants/{tenantId}/users")
+ .body(UserRegistrationData.class)
+ .handle(this::registerUser),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/activate")
+ .param(String.class)
+ .param(String.class)
+ .body(UserRegistrationData.class)
+ .handle(this::activate),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/deactivate")
+ .param(String.class)
+ .param(String.class)
+ .body(UserRegistrationData.class)
+ .handle(this::deactivate),
+ io.vlingo.xoom.http.resource.ResourceBuilder.put("/tenants/{tenantId}/users/{username}/credentials")
+ .param(String.class)
+ .param(String.class)
+ .body(CredentialData.class)
+ .handle(this::addCredential),
+ io.vlingo.xoom.http.resource.ResourceBuilder.delete("/tenants/{tenantId}/users/{username}/credentials/{authority}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::removeCredential),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/credentials/{authority}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .body(CredentialData.class)
+ .handle(this::replaceCredential),
+ io.vlingo.xoom.http.resource.ResourceBuilder.patch("/tenants/{tenantId}/users/{username}/profile")
+ .param(String.class)
+ .param(String.class)
+ .body(ProfileData.class)
+ .handle(this::replaceProfile),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users/{username}/roles/{roleName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::roleOf),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users/{username}/permissions/{permissionName}")
+ .param(String.class)
+ .param(String.class)
+ .param(String.class)
+ .handle(this::permissionOf),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users")
+ .handle(this::users),
+ io.vlingo.xoom.http.resource.ResourceBuilder.get("/tenants/{tenantId}/users/{username}")
+ .param(String.class)
+ .param(String.class)
+ .handle(this::userOf)
+ );
+ }
+
+ @Override
+ protected ContentType contentType() {
+ return ContentType.of("application/json", "charset=UTF-8");
+ }
+
+ private String location(final UserId userId) {
+ return String.format("/tenants/%s/users/%s", userId.tenantId.id, userId.username);
+ }
+
+ private Completes resolve(final String tenantId, final String username) {
+ final UserId userId = UserId.from(TenantId.from(tenantId), username);
+ final Address address = new UserAddress(userId);
+ return grid.actorOf(User.class, address, Definition.has(UserEntity.class, Definition.parameters(userId)));
+ }
+
+ private User create(final String tenantId, final String username) {
+ final UserId userId = UserId.from(TenantId.from(tenantId), username);
+ final Address address = new UserAddress(userId);
+ return grid.actorFor(User.class, Definition.has(UserEntity.class, Definition.parameters(userId)), address);
+ }
+
+ private class UserAddress extends ComplexAddress {
+ public UserAddress(final UserId userId) {
+ super(userId, (id) -> id.idString());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/Group.java b/src/main/java/io/vlingo/xoom/auth/model/group/Group.java
new file mode 100644
index 00000000..08d5711b
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/Group.java
@@ -0,0 +1,19 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.Completes;
+
+public interface Group {
+
+ Completes provisionGroup(final String name, final String description);
+
+ Completes changeDescription(final String description);
+
+ Completes assignGroup(final GroupId groupId);
+
+ Completes unassignGroup(final GroupId groupId);
+
+ Completes assignUser(final UserId userId);
+
+ Completes unassignUser(final UserId userId);
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupAssignedToGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupAssignedToGroup.java
new file mode 100644
index 00000000..7e10231e
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupAssignedToGroup.java
@@ -0,0 +1,28 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class GroupAssignedToGroup extends IdentifiedDomainEvent {
+
+ public final GroupId groupId;
+ public final GroupId innerGroupId;
+
+ public GroupAssignedToGroup(final GroupId groupId, final GroupId innerGroupId) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.groupId = groupId;
+ this.innerGroupId = innerGroupId;
+ }
+
+ @Override
+ public String identity() {
+ return groupId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupDescriptionChanged.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupDescriptionChanged.java
new file mode 100644
index 00000000..3765fcc1
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupDescriptionChanged.java
@@ -0,0 +1,28 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class GroupDescriptionChanged extends IdentifiedDomainEvent {
+
+ public final GroupId groupId;
+ public final String description;
+
+ public GroupDescriptionChanged(final GroupId groupId, final String description) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.groupId = groupId;
+ this.description = description;
+ }
+
+ @Override
+ public String identity() {
+ return groupId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupEntity.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupEntity.java
new file mode 100644
index 00000000..43e2844d
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupEntity.java
@@ -0,0 +1,107 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.model.sourcing.EventSourced;
+
+/**
+ * See EventSourced
+ */
+public final class GroupEntity extends EventSourced implements Group {
+ private GroupState state;
+
+ public GroupEntity(final GroupId groupId) {
+ super(groupId.idString());
+ this.state = GroupState.identifiedBy(groupId);
+ }
+
+ static {
+ EventSourced.registerConsumer(GroupEntity.class, GroupProvisioned.class, GroupEntity::applyGroupProvisioned);
+ EventSourced.registerConsumer(GroupEntity.class, GroupDescriptionChanged.class, GroupEntity::applyGroupDescriptionChanged);
+ EventSourced.registerConsumer(GroupEntity.class, GroupAssignedToGroup.class, GroupEntity::applyGroupAssignedToGroup);
+ EventSourced.registerConsumer(GroupEntity.class, GroupUnassignedFromGroup.class, GroupEntity::applyGroupUnassignedFromGroup);
+ EventSourced.registerConsumer(GroupEntity.class, UserAssignedToGroup.class, GroupEntity::applyUserAssignedToGroup);
+ EventSourced.registerConsumer(GroupEntity.class, UserUnassignedFromGroup.class, GroupEntity::applyUserUnassignedFromGroup);
+ }
+
+ @Override
+ public Completes provisionGroup(final String name, final String description) {
+ return apply(new GroupProvisioned(state.id, name, description), () -> state);
+ }
+
+ @Override
+ public Completes changeDescription(final String description) {
+ return apply(new GroupDescriptionChanged(state.id, description), () -> state);
+ }
+
+ @Override
+ public Completes assignGroup(GroupId groupId) {
+ return apply(new GroupAssignedToGroup(state.id, groupId), () -> state);
+ }
+
+ @Override
+ public Completes unassignGroup(final GroupId groupId) {
+ return apply(new GroupUnassignedFromGroup(state.id, groupId), () -> state);
+ }
+
+ @Override
+ public Completes assignUser(final UserId userId) {
+ return apply(new UserAssignedToGroup(state.id, userId), () -> state);
+ }
+
+ @Override
+ public Completes unassignUser(final UserId userId) {
+ return apply(new UserUnassignedFromGroup(state.id, userId), () -> state);
+ }
+
+ private void applyGroupProvisioned(final GroupProvisioned event) {
+ state = state.provisionGroup(event.name, event.description);
+ }
+
+ private void applyGroupDescriptionChanged(final GroupDescriptionChanged event) {
+ state = state.changeDescription(event.description);
+ }
+
+ private void applyGroupAssignedToGroup(final GroupAssignedToGroup event) {
+ state = state.assignGroup(event.innerGroupId);
+ }
+
+ private void applyGroupUnassignedFromGroup(final GroupUnassignedFromGroup event) {
+ state = state.unassignGroup(event.innerGroupId);
+ }
+
+ private void applyUserAssignedToGroup(final UserAssignedToGroup event) {
+ state = state.assignUser(event.userId);
+ }
+
+ private void applyUserUnassignedFromGroup(final UserUnassignedFromGroup event) {
+ state = state.unassignUser(event.userId);
+ }
+
+ /*
+ * Restores my initial state by means of {@code state}.
+ *
+ * @param snapshot the {@code GroupState} holding my state
+ * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed
+ */
+ @Override
+ @SuppressWarnings("hiding")
+ protected void restoreSnapshot(final GroupState snapshot, final int currentVersion) {
+ // OVERRIDE FOR SNAPSHOT SUPPORT
+ // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced
+ }
+
+ /*
+ * Answer the valid {@code GroupState} instance if a snapshot should
+ * be taken and persisted along with applied {@code Source} instance(s).
+ *
+ * @return GroupState
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ protected GroupState snapshot() {
+ // OVERRIDE FOR SNAPSHOT SUPPORT
+ // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced
+ return null;
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupId.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupId.java
new file mode 100644
index 00000000..efa8f3a4
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupId.java
@@ -0,0 +1,55 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Objects;
+
+public class GroupId {
+ public final TenantId tenantId;
+ public final String groupName;
+
+ public static GroupId from(final TenantId tenantId, final String groupName) {
+ return new GroupId(tenantId, groupName);
+ }
+
+ public static GroupId from(String id) {
+ String[] pair = id.split(":", 2);
+ return from(TenantId.from(pair[0]), pair[1]);
+ }
+
+ private GroupId(final TenantId tenantId, final String groupName) {
+ this.tenantId = tenantId;
+ this.groupName = groupName;
+ }
+
+ public String idString() {
+ return tenantId.id != "" || groupName != "" ? String.format("%s:%s", tenantId.id, groupName) : "";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof GroupId)) {
+ return false;
+ }
+ GroupId groupId = (GroupId) other;
+ return tenantId.equals(groupId.tenantId) && groupName.equals(groupId.groupName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tenantId, groupName);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("tenantId", tenantId.idString())
+ .append("groupName", groupName)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupProvisioned.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupProvisioned.java
new file mode 100644
index 00000000..84bd708f
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupProvisioned.java
@@ -0,0 +1,30 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class GroupProvisioned extends IdentifiedDomainEvent {
+
+ public final GroupId groupId;
+ public final String name;
+ public final String description;
+
+ public GroupProvisioned(final GroupId groupId, final String name, final String description) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.groupId = groupId;
+ this.name = name;
+ this.description = description;
+ }
+
+ @Override
+ public String identity() {
+ return groupId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupState.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupState.java
new file mode 100644
index 00000000..ffefc88d
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupState.java
@@ -0,0 +1,69 @@
+package io.vlingo.xoom.auth.model.group;
+
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.auth.model.value.EncodedMember;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public final class GroupState {
+
+ public final GroupId id;
+ public final String name;
+ public final String description;
+ public final Set members;
+
+ public static GroupState identifiedBy(final GroupId groupId) {
+ return new GroupState(groupId, null, null, Collections.emptySet());
+ }
+
+ public GroupState(final GroupId groupId, final String name, final String description) {
+ this(groupId, name, description, Collections.emptySet());
+ }
+
+ private GroupState(final GroupId groupId, final String name, final String description, final Set members) {
+ this.id = groupId;
+ this.name = name;
+ this.description = description;
+ this.members = Collections.unmodifiableSet(members);
+ }
+
+ public GroupState provisionGroup(final String name, final String description) {
+ return new GroupState(this.id, name, description, Collections.emptySet());
+ }
+
+ public GroupState changeDescription(final String description) {
+ return new GroupState(this.id, this.name, description, members);
+ }
+
+ public GroupState assignGroup(final GroupId innerGroupId) {
+ final Set updatedMembers = includeMember(EncodedMember.group(innerGroupId));
+ return new GroupState(this.id, this.name, this.description, updatedMembers);
+ }
+
+ public GroupState unassignGroup(final GroupId innerGroupId) {
+ final Set updatedMembers = removeMember(EncodedMember.group(innerGroupId));
+ return new GroupState(this.id, this.name, this.description, updatedMembers);
+ }
+
+ public GroupState assignUser(final UserId userId) {
+ final Set updatedMembers = includeMember(EncodedMember.user(userId));
+ return new GroupState(this.id, this.name, this.description, updatedMembers);
+ }
+
+ public GroupState unassignUser(final UserId userId) {
+ final Set updatedMembers = removeMember(EncodedMember.user(userId));
+ return new GroupState(this.id, this.name, this.description, updatedMembers);
+ }
+
+ private Set includeMember(final EncodedMember member) {
+ return Stream.concat(members.stream(), Stream.of(member)).collect(Collectors.toSet());
+ }
+
+ private Set removeMember(final EncodedMember member) {
+ return members.stream().filter(m -> !m.equals(member)).collect(Collectors.toSet());
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/GroupUnassignedFromGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/GroupUnassignedFromGroup.java
new file mode 100644
index 00000000..6c264229
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/GroupUnassignedFromGroup.java
@@ -0,0 +1,28 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class GroupUnassignedFromGroup extends IdentifiedDomainEvent {
+
+ public final GroupId groupId;
+ public final GroupId innerGroupId;
+
+ public GroupUnassignedFromGroup(final GroupId groupId, final GroupId innerGroupId) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.groupId = groupId;
+ this.innerGroupId = innerGroupId;
+ }
+
+ @Override
+ public String identity() {
+ return groupId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/UserAssignedToGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/UserAssignedToGroup.java
new file mode 100644
index 00000000..081b50be
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/UserAssignedToGroup.java
@@ -0,0 +1,29 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class UserAssignedToGroup extends IdentifiedDomainEvent {
+
+ public final GroupId groupId;
+ public final UserId userId;
+
+ public UserAssignedToGroup(final GroupId groupId, final UserId userId) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.groupId = groupId;
+ this.userId = userId;
+ }
+
+ @Override
+ public String identity() {
+ return groupId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/group/UserUnassignedFromGroup.java b/src/main/java/io/vlingo/xoom/auth/model/group/UserUnassignedFromGroup.java
new file mode 100644
index 00000000..08337fa5
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/group/UserUnassignedFromGroup.java
@@ -0,0 +1,29 @@
+package io.vlingo.xoom.auth.model.group;
+
+import io.vlingo.xoom.auth.model.user.UserId;
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class UserUnassignedFromGroup extends IdentifiedDomainEvent {
+
+ public final GroupId groupId;
+ public final UserId userId;
+
+ public UserUnassignedFromGroup(final GroupId groupId, final UserId userId) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.groupId = groupId;
+ this.userId = userId;
+ }
+
+ @Override
+ public String identity() {
+ return groupId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/Permission.java b/src/main/java/io/vlingo/xoom/auth/model/permission/Permission.java
new file mode 100644
index 00000000..59163bd3
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/Permission.java
@@ -0,0 +1,18 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.auth.model.value.Constraint;
+import io.vlingo.xoom.common.Completes;
+
+public interface Permission {
+
+ Completes provisionPermission(final String name, final String description);
+
+ Completes enforce(final Constraint constraint);
+
+ Completes enforceReplacement(String constraintName, final Constraint constraint);
+
+ Completes forget(final String constraintName);
+
+ Completes changeDescription(final String description);
+
+}
\ No newline at end of file
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintEnforced.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintEnforced.java
new file mode 100644
index 00000000..b1b0a3c7
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintEnforced.java
@@ -0,0 +1,29 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+import io.vlingo.xoom.auth.model.value.*;
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class PermissionConstraintEnforced extends IdentifiedDomainEvent {
+
+ public final PermissionId permissionId;
+ public final Constraint constraint;
+
+ public PermissionConstraintEnforced(final PermissionId permissionId, final Constraint constraint) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.permissionId = permissionId;
+ this.constraint = constraint;
+ }
+
+ @Override
+ public String identity() {
+ return permissionId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintForgotten.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintForgotten.java
new file mode 100644
index 00000000..a108ad80
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintForgotten.java
@@ -0,0 +1,27 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class PermissionConstraintForgotten extends IdentifiedDomainEvent {
+
+ public final PermissionId permissionId;
+ public final String constraintName;
+
+ public PermissionConstraintForgotten(final PermissionId permissionId, final String constraintName) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.permissionId = permissionId;
+ this.constraintName = constraintName;
+ }
+
+ @Override
+ public String identity() {
+ return permissionId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintReplacementEnforced.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintReplacementEnforced.java
new file mode 100644
index 00000000..d1719a07
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionConstraintReplacementEnforced.java
@@ -0,0 +1,31 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+import io.vlingo.xoom.auth.model.value.*;
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class PermissionConstraintReplacementEnforced extends IdentifiedDomainEvent {
+
+ public final PermissionId permissionId;
+ public final String constraintName;
+ public final Constraint constraint;
+
+ public PermissionConstraintReplacementEnforced(final PermissionId permissionId, final String constraintName, final Constraint constraint) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.permissionId = permissionId;
+ this.constraintName = constraintName;
+ this.constraint = constraint;
+ }
+
+ @Override
+ public String identity() {
+ return permissionId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionDescriptionChanged.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionDescriptionChanged.java
new file mode 100644
index 00000000..8d19c4b9
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionDescriptionChanged.java
@@ -0,0 +1,28 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+
+/**
+ * See
+ *
+ * Commands, Domain Events, and Identified Domain Events
+ *
+ */
+public final class PermissionDescriptionChanged extends IdentifiedDomainEvent {
+
+ public final PermissionId permissionId;
+ public final String description;
+
+ public PermissionDescriptionChanged(final PermissionId permissionId, final String description) {
+ super(SemanticVersion.from("1.0.0").toValue());
+ this.permissionId = permissionId;
+ this.description = description;
+ }
+
+ @Override
+ public String identity() {
+ return permissionId.idString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionEntity.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionEntity.java
new file mode 100644
index 00000000..939bfc86
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionEntity.java
@@ -0,0 +1,97 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.auth.model.value.Constraint;
+import io.vlingo.xoom.common.Completes;
+import io.vlingo.xoom.lattice.model.sourcing.EventSourced;
+
+/**
+ * See EventSourced
+ */
+public final class PermissionEntity extends EventSourced implements Permission {
+ private PermissionState state;
+
+ public PermissionEntity(final PermissionId permissionId) {
+ super(permissionId.idString());
+ this.state = PermissionState.identifiedBy(permissionId);
+ }
+
+ static {
+ EventSourced.registerConsumer(PermissionEntity.class, PermissionProvisioned.class, PermissionEntity::applyPermissionProvisioned);
+ EventSourced.registerConsumer(PermissionEntity.class, PermissionConstraintEnforced.class, PermissionEntity::applyPermissionConstraintEnforced);
+ EventSourced.registerConsumer(PermissionEntity.class, PermissionConstraintReplacementEnforced.class, PermissionEntity::applyPermissionConstraintReplacementEnforced);
+ EventSourced.registerConsumer(PermissionEntity.class, PermissionConstraintForgotten.class, PermissionEntity::applyPermissionConstraintForgotten);
+ EventSourced.registerConsumer(PermissionEntity.class, PermissionDescriptionChanged.class, PermissionEntity::applyPermissionDescriptionChanged);
+ }
+
+ @Override
+ public Completes provisionPermission(final String name, final String description) {
+ return apply(new PermissionProvisioned(state.id, name, description), () -> state);
+ }
+
+ @Override
+ public Completes enforce(final Constraint constraint) {
+ return apply(new PermissionConstraintEnforced(state.id, constraint), () -> state);
+ }
+
+ @Override
+ public Completes enforceReplacement(final String constraintName, final Constraint constraint) {
+ return apply(new PermissionConstraintReplacementEnforced(state.id, constraintName, constraint), () -> state);
+ }
+
+ @Override
+ public Completes forget(final String constraintName) {
+ return apply(new PermissionConstraintForgotten(state.id, constraintName), () -> state);
+ }
+
+ @Override
+ public Completes changeDescription(final String description) {
+ return apply(new PermissionDescriptionChanged(state.id, description), () -> state);
+ }
+
+ private void applyPermissionProvisioned(final PermissionProvisioned event) {
+ state = state.provisionPermission(event.name, event.description);
+ }
+
+ private void applyPermissionConstraintEnforced(final PermissionConstraintEnforced event) {
+ state = state.enforce(event.constraint);
+ }
+
+ private void applyPermissionConstraintReplacementEnforced(final PermissionConstraintReplacementEnforced event) {
+ state = state.enforceReplacement(event.constraintName, event.constraint);
+ }
+
+ private void applyPermissionConstraintForgotten(final PermissionConstraintForgotten event) {
+ state = state.forget(event.constraintName);
+ }
+
+ private void applyPermissionDescriptionChanged(final PermissionDescriptionChanged event) {
+ state = state.changeDescription(event.description);
+ }
+
+ /*
+ * Restores my initial state by means of {@code state}.
+ *
+ * @param snapshot the {@code PermissionState} holding my state
+ * @param currentVersion the int value of my current version; may be helpful in determining if snapshot is needed
+ */
+ @Override
+ @SuppressWarnings("hiding")
+ protected void restoreSnapshot(final PermissionState snapshot, final int currentVersion) {
+ // OVERRIDE FOR SNAPSHOT SUPPORT
+ // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced
+ }
+
+ /*
+ * Answer the valid {@code PermissionState} instance if a snapshot should
+ * be taken and persisted along with applied {@code Source} instance(s).
+ *
+ * @return PermissionState
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ protected PermissionState snapshot() {
+ // OVERRIDE FOR SNAPSHOT SUPPORT
+ // See: https://docs.vlingo.io/xoom-lattice/entity-cqrs#eventsourced
+ return null;
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionId.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionId.java
new file mode 100644
index 00000000..12329e08
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionId.java
@@ -0,0 +1,50 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.auth.model.tenant.TenantId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.util.Objects;
+
+public class PermissionId {
+ public final TenantId tenantId;
+ public final String permissionName;
+
+ public static PermissionId from(final TenantId tenantId, final String permissionName) {
+ return new PermissionId(tenantId, permissionName);
+ }
+
+ private PermissionId(final TenantId tenantId, final String permissionName) {
+ this.tenantId = tenantId;
+ this.permissionName = permissionName;
+ }
+
+ public String idString() {
+ return tenantId.id != "" || permissionName != "" ? String.format("%s:%s", tenantId.idString(), permissionName) : "";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof PermissionId)) {
+ return false;
+ }
+ PermissionId permissionName = (PermissionId) other;
+ return tenantId.equals(permissionName.tenantId) && this.permissionName.equals(permissionName.permissionName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tenantId, permissionName);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE)
+ .append("tenantId", tenantId)
+ .append("permissionName", permissionName)
+ .toString();
+ }
+}
diff --git a/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionProvisioned.java b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionProvisioned.java
new file mode 100644
index 00000000..879a7ade
--- /dev/null
+++ b/src/main/java/io/vlingo/xoom/auth/model/permission/PermissionProvisioned.java
@@ -0,0 +1,29 @@
+package io.vlingo.xoom.auth.model.permission;
+
+import io.vlingo.xoom.common.version.SemanticVersion;
+import io.vlingo.xoom.lattice.model.IdentifiedDomainEvent;
+
+/**
+ * See
+ *