HookServices.groovy 8.75 KB
import javax.transaction.Synchronization
import javax.transaction.xa.XAResource
import javax.transaction.xa.Xid

import groovy.transform.Field

import org.slf4j.Logger
import org.slf4j.LoggerFactory

import org.moqui.util.RestClient
import org.moqui.util.ObjectUtilities
import org.moqui.context.ExecutionContext
import org.moqui.entity.EntityCondition
import org.moqui.entity.EntityFind
import org.moqui.entity.EntityList
import org.moqui.entity.EntityValue

@Field Logger logger = LoggerFactory.getLogger(getClass().getName())
//'HookServices')

class HookSynchronization implements Synchronization {
    private final ExecutionContext ec;

    private static class EntityToken {
        protected final String entityName
        protected final String keyName
        protected final Object keyValue
        protected final Map<String, Object> extraParameters = new HashMap<String, Object>()

        protected EntityToken(String entityName, String keyName, Object keyValue) {
            this.entityName = entityName
            this.keyName = keyName
            this.keyValue = keyValue
        }

        public boolean equals(Object other) {
            if (!(other instanceof EntityToken)) {
                return false
            }
            EntityToken that = (EntityToken) other
            return entityName.equals(that.entityName) && keyName.equals(that.keyName) && keyValue.equals(that.keyValue)
        }

        public int hashCode() {
            return entityName.hashCode() ^ keyName.hashCode() ^ keyValue.hashCode()
        }
    }

    private Map<EntityToken, Map<String, Object>> updates = new LinkedHashMap<>()

    protected HookSynchronization(ExecutionContext ec) {
        this.ec = ec
    }

    protected void add(String entityName, String keyName, Object keyValue, Map<String, Object> extraParameters) {
        EntityToken entityToken = new EntityToken(entityName, keyName, keyValue)
        // This always move the value to the end of the list
        Map<String, Object> existingExtraParameters = updates.remove(entityToken)
        Map<String, Object> newExtraParameters = [:]
        if (existingExtraParameters) {
            newExtraParameters.putAll(existingExtraParameters)
        }
        if (extraParameters) {
            newExtraParameters.putAll(extraParameters)
        }
        updates.put(entityToken, newExtraParameters)
    }

    @Override
    public void beforeCompletion() {
        List<Map<String, Object>> updates = []
        for (Map.Entry<EntityToken, Map<String, Object>> entry: this.updates.entrySet()) {
            EntityToken token = entry.getKey()
            Map<String, Object> extraParameters = entry.getValue()
            updates.add([
                entityName: token.entityName,
                keyName: token.keyName,
                keyValue: token.keyValue,
                extraParameters: extraParameters,
            ])
        }
        ec.getService().async().name("keycloak.HookServices.process#Updates").parameter('updates', updates).call()
    }

    @Override
    public void afterCompletion(int status) {

    }
}

/*
class HookXAResource implements XAResource {
    private static class EntityPk {
        protected String entityName;
        protected Map<String, Object> pk;

        protected EntityPk(EntityValue entity) {
            this.entityName = entity.getEntityName()
            this.pk = entity.getPrimaryKeys()
        }

        public boolean equals(Object other) {
            if (!(object instanceof EntityPk)) {
                return false
            }
            EntityPk that = (EntityPk) other
            return entityName.equals(that.entityName) && pk.equals(that.pk)
        }

        public int hashCode() {
            return entityName.hashCode ^ pk.hashCode()
        }
    }

    private Map<Xid, Set<EntityPk>> modifiedValues = new HashMap<>()
    private int timeout = 60

    @Override
    public void commit(Xid xid, boolean onePhase) {

    }

    @Override
    public void end(Xid xid, int flags) {
        if (flags & TMFAIL) {

        } else if (flags & TMSUSPEND) {

        } else if (flags & TMSUCCESS) {

        }
    }

    @Override
    public void forget(Xid xid) {
        modifiedValues.remove(xid)
    }

    @Override
    public int getTransactionTimeout() {
        return timeout
    }

    @Override
    public boolean isSameRM(XAResource xar) {
        return getClass().equals(xar.getClass())
    }

    @Override
    public int prepare(Xid xid) {
        if (!keycloak.connected()) {
            throw new XAException(XAException.XAERR_RMFAIL)
        }
    }

    @Override
    public Xid[] recover(int flag) {

    }

    @Override
    public void rollback(Xid xid) {

    }

    @Override
    public boolean setTransactionTimeout(int timeout) {
        this.timeout = timeout
    }

    @Override
    public void start(Xid xid, int flags) {

    }

}
*/

HookSynchronization getHookSync() {
    HookSynchronization hookSync = ec.transaction.getActiveSynchronization(getClass().getName())
    if (hookSync == null) {
        hookSync = new HookSynchronization(ec)
        ec.transaction.putAndEnlistActiveSynchronization(getClass().getName(), hookSync)
    }
    return hookSync
}

Map<String, Object> handleEntityUpdate() {
    logger.info("handleEntityUpdate: ${context.entityName}[${context.value}]")
    logger.info("context: ${context}")
    ExecutionContext ec = context.ec
    Map<String, Object> contextRoot = ec.getContextRoot()
    Map<String, Set<Object>> toRegister = contextRoot['KeycloakEntityRegistrations']
    logger.info("toRegister: ${toRegister}")
    if (toRegister == null) {
        toRegister = contextRoot['KeycloakEntityRegistrations'] = [:]
    }

    HookSynchronization hookSync = getHookSync()

    List<Map<String, Object>> queue = [[entityName: context.entityName, value: context.value]]

    while (!queue.isEmpty()) {
        Map<String, Object> entry = queue.remove(0)
        logger.info("processing entry: ${entry}")
        String entityName = entry.entityName
        Object value = entry.value
        Set<Object> entityRegistrations = toRegister[entityName]
        if (entityRegistrations == null) {
            entityRegistrations = toRegister[entityName] = new HashSet()
            entityRegistrations.add(value)
        } else if (entityRegistrations.contains(value)) {
            continue
        }
        String keyName
        switch (entityName) {
            case 'Party':
                EntityList userAccounts = ec.entity.find('UserAccount').condition('partyId', value).list()
                for (EntityValue userAccount: userAccounts) {
                    queue.add([entityName: 'UserAccount', value: userAccount.userId])
                }
                continue
            case 'ContactMech':
                EntityList partyContactMechs = ec.entity.find('PartyContactMech').condition('contactMechId', value).list()
                for (EntityValue partyContactMech: partyContactMechs) {
                    queue.add([entityName: 'Party', value: partyContactMech.partyId])
                }
                continue
            case 'UserAccount':
                keyName = 'userId'
                break
            case 'UserGroup':
                keyName = 'userGroupId'
                break
            case 'UserGroupMember':
                keyName = 'userGroupId'
                break
            case 'UserPermission':
                keyName = 'userPermissionId'
                break
            case 'PartyClassification':
                keyName = 'partyClassificationId'
                break
            case 'RoleType':
                keyName = 'roleTypeId'
                break
        }
        ec.getLogger().info("Registered synchronization for ${entityName}: [${value}]")
        hookSync.add(entityName, keyName, value, [:])
        //ec.getService().special().name("keycloak.KeycloakServices.send#${entityName}").parameter(keyName, value).registerOnCommit()
    }
}

Map<String, Object> handleUpdatePasswordInternal() {
    String userId = ec.context.userId
    String newPassword = ec.context.newPassword
    Boolean requirePasswordChange = ec.context.requirePasswordChange
    ec.getLogger().info("Registered synchronization for update#PasswordInternal: [${userId}]")
    HookSynchronization hookSync = getHookSync()
    hookSync.add('UserAccount', 'userId', userId, [
        newPassword: newPassword,
        requirePasswordChange: requirePasswordChange,
    ])
}

Map<String, Object> processUpdates() {
    List<Map<String, Object>> updates = context.updates

    for (Map<String, Object> update: updates) {
        Map<String, Object> parameters = [:]
        parameters[update.keyName] = update.keyValue
        parameters.putAll(update.extraParameters)
        ec.getService().sync().name("keycloak.KeycloakServices.send#${update.entityName}").parameters(parameters).call()
    }
    return [:]
}