package com.eas.client; import com.eas.client.cache.PlatypusIndexer; import com.eas.client.changes.Change; import com.eas.client.scripts.ScriptedResource; import com.eas.script.Scripts; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import jdk.nashorn.api.scripting.AbstractJSObject; import jdk.nashorn.api.scripting.JSObject; import jdk.nashorn.internal.runtime.ECMAException; /** * Multi datasource client. It allows to use js modules as datasources, * validators and appliers. * * @author mg */ public class ScriptedDatabasesClient extends DatabasesClient { protected PlatypusIndexer indexer; // key - validator name, value datasources list protected Map<String, Collection<String>> validators = new HashMap<>(); /** * * @param aDefaultDatasourceName * @param aIndexer * @param aAutoFillMetadata * @param aValidators * @param aMaxJdbcThreads * @throws Exception */ public ScriptedDatabasesClient(String aDefaultDatasourceName, PlatypusIndexer aIndexer, boolean aAutoFillMetadata, Map<String, Collection<String>> aValidators, int aMaxJdbcThreads) throws Exception { this(aDefaultDatasourceName, aIndexer, aAutoFillMetadata, aMaxJdbcThreads); validators.putAll(aValidators); } /** * * @param aDefaultDatasourceName * @param aIndexer * @param aAutoFillMetadata * @param aMaxJdbcThreads * @throws Exception */ public ScriptedDatabasesClient(String aDefaultDatasourceName, PlatypusIndexer aIndexer, boolean aAutoFillMetadata, int aMaxJdbcThreads) throws Exception { super(aDefaultDatasourceName, aAutoFillMetadata, aMaxJdbcThreads); indexer = aIndexer; } /** * Adds transaction validator module. Validator modules are used in commit * to verify transaction changes log. They mey consume particuled changes * and optionally send them to custom data store or a service. If validator * module detects an errorneous data changes, than it should thor ab * exception. * * @param aModuleName * @param aDatasources */ public void addValidator(String aModuleName, Collection<String> aDatasources) { validators.put(aModuleName, aDatasources); } private static class CallPoint { private final JSObject module; private final JSObject function; public CallPoint(JSObject aModule, JSObject aFunction) { super(); module = aModule; function = aFunction; } } private static class CollectionAsyncProcess<T> { private final Collection<T> processed; private final Scripts.Space space; private final Consumer<Void> onSuccess; private final Consumer<Exception> onFailure; private int completed; private final Set<Exception> exceptions = new HashSet<>(); public CollectionAsyncProcess(Collection<T> aProcessed, Scripts.Space aSpace, Consumer<Void> aOnSuccess, Consumer<Exception> aOnFailure) { processed = aProcessed; space = aSpace; onSuccess = aOnSuccess; onFailure = aOnFailure; } public void complete() { complete(null); } public void complete(Exception aFailureCause) { if (aFailureCause != null) { exceptions.add(aFailureCause); } if (++completed == processed.size()) { if (exceptions.isEmpty()) { if (onSuccess != null) { space.process(() -> { onSuccess.accept(null); }); } } else if (onFailure != null) { StringBuilder eMessagesSum = new StringBuilder(); exceptions.stream().forEach((ex) -> { if (eMessagesSum.length() > 0) { eMessagesSum.append("\n"); } eMessagesSum.append(ex.getMessage() != null && !ex.getMessage().isEmpty() ? ex.getMessage() : ex.toString()); }); space.process(() -> { onFailure.accept(new IllegalStateException(eMessagesSum.toString())); }); } } } public void perform(Consumer<T> action) { if (processed.isEmpty()) { space.process(() -> { onSuccess.accept(null); }); } else { processed.stream().forEach(action); } } } @Override public int commit(Map<String, List<Change>> aChangeLogs, Consumer<Integer> onSuccess, Consumer<Exception> onFailure) throws Exception { Scripts.Space space = Scripts.getSpace(); if (onSuccess != null) { CollectionAsyncProcess<Map.Entry<String, List<Change>>> logsProcess = new CollectionAsyncProcess<>(aChangeLogs.entrySet(), space, v -> { try { super.commit(aChangeLogs, onSuccess, onFailure); } catch (Exception ex) { Logger.getLogger(ScriptedDatabasesClient.class.getName()).log(Level.SEVERE, null, ex); } }, onFailure); logsProcess.perform(dsEntry -> { String dsName = dsEntry.getKey(); List<Change> changeLog = dsEntry.getValue(); validate(dsName, changeLog, v -> { logsProcess.complete(null); }, ex -> { logsProcess.complete(ex); }, space); }); return 0; } else { aChangeLogs.entrySet().stream().forEach(dsEntry -> { String dataSourceName = dsEntry.getKey(); List<Change> log = dsEntry.getValue(); validate(dataSourceName, log, null, null, space); }); return super.commit(aChangeLogs, null, null); } } protected JSObject createModule(String aModuleName) { return Scripts.getSpace().createModule(aModuleName); } private void validate(final String aDatasourceName, final List<Change> aLog, Consumer<Void> onSuccess, Consumer<Exception> onFailure, Scripts.Space aSpace) { Collection<String> requiredModules = validators.entrySet().stream() .filter(vEntry -> { Collection<String> datasourcesUnderControl = vEntry.getValue(); return (((datasourcesUnderControl == null || datasourcesUnderControl.isEmpty()) && (aDatasourceName == null || Objects.equals(aDatasourceName, defaultDatasourceName))) || (datasourcesUnderControl != null && datasourcesUnderControl.contains(aDatasourceName))); }) .map(vEntry -> vEntry.getKey()) .collect(Collectors.toSet()); if (onSuccess != null) { try { ScriptedResource._require(requiredModules.stream().toArray(size -> new String[size]), null, Scripts.getSpace(), new HashSet<>(), v -> { Collection<CallPoint> validatorsPoints = toCallPoints(requiredModules); JSObject jsLog = validatorsPoints.isEmpty() ? aSpace.makeArray() : aSpace.toJsArray(aLog); CollectionAsyncProcess<CallPoint> logProcess = new CollectionAsyncProcess<>(validatorsPoints, aSpace, onSuccess, onFailure); logProcess.perform(validatorPoint -> { try { validatorPoint.function.call(validatorPoint.module, new Object[]{jsLog, aDatasourceName, new AbstractJSObject() { @Override public Object call(final Object thiz, final Object... args) { logProcess.complete(null); return null; } }, new AbstractJSObject() { @Override public Object call(final Object thiz, final Object... args) { if (args.length > 0) { if (args[0] instanceof Exception) { logProcess.complete((Exception) args[0]); } else { logProcess.complete(new Exception(String.valueOf(aSpace.toJava(args[0])))); } } else { logProcess.complete(new Exception("No error information from validate method")); } return null; } } }); } catch (ECMAException ex) { logProcess.complete(ex); } }); }, onFailure); } catch (Exception ex) { Logger.getLogger(ScriptedDatabasesClient.class.getName()).log(Level.SEVERE, null, ex); } } else { try { ScriptedResource._require(requiredModules.stream().toArray(size -> new String[size]), null, Scripts.getSpace(), new HashSet<>()); } catch (Exception ex) { throw new IllegalStateException(ex); } Collection<CallPoint> validatorsPoints = toCallPoints(requiredModules); JSObject jsLog = validatorsPoints.isEmpty() ? aSpace.makeArray() : aSpace.toJsArray(aLog); validatorsPoints.stream().forEach((v) -> { v.function.call(v.module, new Object[]{jsLog, aDatasourceName}); }); } } private Collection<CallPoint> toCallPoints(Collection<String> requiredModules) { return requiredModules.stream() .map(validatorName -> createModule(validatorName)) .filter(module -> module != null) .filter(module -> module.getMember("validate") instanceof JSObject) .map(module -> new CallPoint(module, (JSObject) module.getMember("validate"))) .collect(Collectors.toList()); } }