diff --git a/src/main/java/sirius/biz/scripting/EntityScriptableEvent.java b/src/main/java/sirius/biz/scripting/EntityScriptableEvent.java new file mode 100644 index 000000000..caeaa3f96 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/EntityScriptableEvent.java @@ -0,0 +1,40 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +import sirius.db.mixing.Entity; + +/** + * Provides a base class for all custom events which are associated with an entity. + * + * @param the generic type of the entity + */ +public abstract class EntityScriptableEvent extends TypedScriptableEvent { + + private final E entity; + + protected EntityScriptableEvent(E entity) { + this.entity = entity; + } + + /** + * Returns the entity associated with this event. + * + * @return the entity associated with this event + */ + public E getEntity() { + return entity; + } + + @SuppressWarnings("unchecked") + @Override + public Class getType() { + return (Class) entity.getClass(); + } +} diff --git a/src/main/java/sirius/biz/scripting/ScriptableEvent.java b/src/main/java/sirius/biz/scripting/ScriptableEvent.java new file mode 100644 index 000000000..94d74f53d --- /dev/null +++ b/src/main/java/sirius/biz/scripting/ScriptableEvent.java @@ -0,0 +1,63 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +import sirius.kernel.health.HandledException; + +import java.util.Optional; + +/** + * Provides a base class for all custom events handled by a {@link ScriptableEventDispatcher}. + */ +public abstract class ScriptableEvent { + + /** + * Stores if all event handlers completed successfully. + */ + protected boolean success; + + /** + * Stores if an error / exception occurred while invoking an event handler. + */ + protected boolean failed; + + /** + * Stores the exception which occurred while invoking an event handler. + */ + protected HandledException error; + + /** + * Determines if the event was successful. + * + * @return true if the event was successful, false otherwise + */ + public boolean isSuccess() { + return success; + } + + /** + * Determines if the event failed (an exception occurred within an event handler). + *

+ * Note, that the exception itself can be obtained via {@link #getError()}. + * + * @return true if the event failed, false otherwise + */ + public boolean isFailed() { + return failed; + } + + /** + * Provides the error which occurred when handling the event. + * + * @return the error that occurred wrapped as optional or an empty optional if the event was successful + */ + public Optional getError() { + return Optional.ofNullable(error); + } +} diff --git a/src/main/java/sirius/biz/scripting/ScriptableEventDispatcher.java b/src/main/java/sirius/biz/scripting/ScriptableEventDispatcher.java new file mode 100644 index 000000000..77ac29ca4 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/ScriptableEventDispatcher.java @@ -0,0 +1,33 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +/** + * Describes a dispatcher which can handle custom events. + *

+ * This is usually fetched via {@link ScriptableEvents} and will handle all events for a given tenant, + * based on a given script. If no custom handling script is present, a NOOP dispatcher is used + * which will be marked as {@link #isActive() inactive} (so that some events might get optimized away). + */ +public interface ScriptableEventDispatcher { + + /** + * Determines if this dispatcher is active and will actually handle events. + * + * @return true if a real dispatcher is present, false if a NOOP dispatcher is used + */ + boolean isActive(); + + /** + * Handles the given event. + * + * @param event the event to handle + */ + void handleEvent(ScriptableEvent event); +} diff --git a/src/main/java/sirius/biz/scripting/ScriptableEventDispatcherRepository.java b/src/main/java/sirius/biz/scripting/ScriptableEventDispatcherRepository.java new file mode 100644 index 000000000..351a4d904 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/ScriptableEventDispatcherRepository.java @@ -0,0 +1,44 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +import sirius.kernel.di.std.AutoRegister; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; + +/** + * Provides a repository which stores and manages {@link ScriptableEventDispatcher custom event dispatchers}. + *

+ * Note that the repository isn't usually accessed directly. Instead, the {@link ScriptableEvents} class should be used. + */ +@AutoRegister +public interface ScriptableEventDispatcherRepository { + + /** + * Fetches all available dispatchers for the given tenant. + * + * @param tenantId the tenant for which to fetch the dispatchers + * @return a list of all available dispatchers for the given tenant + */ + List fetchAvailableDispatchers(@Nonnull String tenantId); + + /** + * Fetches the dispatcher with the given name for the given tenant. + * + * @param tenantId the tenant for which to fetch the dispatcher + * @param name the name of the dispatcher to fetch + * @return the dispatcher with the given name for the given tenant wrapped as optional or an empty optional if + * no such dispatcher exists. NOTE: if an empty name is given, the first dispatcher for the given tenant + * is used. This helps to simplify the usage of custom events. + */ + Optional fetchDispatcher(@Nonnull String tenantId, @Nullable String name); +} diff --git a/src/main/java/sirius/biz/scripting/ScriptableEventRegistry.java b/src/main/java/sirius/biz/scripting/ScriptableEventRegistry.java new file mode 100644 index 000000000..4df73b9b3 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/ScriptableEventRegistry.java @@ -0,0 +1,42 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +import sirius.kernel.commons.Callback; + +/** + * Provides the interface as seen by the scripting engine to register custom event handlers. + *

+ * The tenant specific script is provided with an instance of this registry and can then add handlers + * as needed. + */ +public interface ScriptableEventRegistry { + + /** + * Adds a handler for the given event type. + * + * @param eventType the type of events to handle + * @param handler the handler to handle the event + * @param the generic type of the event + */ + void registerHandler(Class eventType, Callback handler); + + /** + * Adds a typed handler for the given event type and inner type + * + * @param eventType the type of events to handle + * @param type the inner type within the event to process + * @param handler the handler to handle the event + * @param the generic inner type of the event + * @param the generic type of the event + */ + > void registerTypedHandler(Class eventType, + Class type, + Callback handler); +} diff --git a/src/main/java/sirius/biz/scripting/ScriptableEvents.java b/src/main/java/sirius/biz/scripting/ScriptableEvents.java new file mode 100644 index 000000000..ec274152b --- /dev/null +++ b/src/main/java/sirius/biz/scripting/ScriptableEvents.java @@ -0,0 +1,93 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +import sirius.kernel.di.std.Part; +import sirius.kernel.di.std.Register; +import sirius.web.security.ScopeInfo; +import sirius.web.security.UserContext; +import sirius.web.security.UserInfo; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; + +/** + * Provides access to tenant specific custom event dispatchers. + *

+ * Event dispatchers are stored and managed by a {@link ScriptableEventDispatcherRepository}. Commonly these are + * defined via scripts which modify a {@link ScriptableEventRegistry} and are then transformed into a + * {@link ScriptableEventDispatcher} with the help of a {@link SimpleScriptableEventDispatcher}. + */ +@Register(classes = ScriptableEvents.class) +public class ScriptableEvents { + + private static final ScriptableEventDispatcher NOOP_DISPATCHER = new ScriptableEventDispatcher() { + + @Override + public boolean isActive() { + return false; + } + + @Override + public void handleEvent(ScriptableEvent event) { + // do nothing + } + }; + + @Part + @Nullable + private ScriptableEventDispatcherRepository dispatcherRepository; + + /** + * Fetches the dispatcher for the current tenant. + * + * @param name the name of the dispatcher to fetch + * @return the dispatcher for the current tenant with the given name or a NOOP dispatcher if no such dispatcher + * exists. Note, if an empty name is given, the first available dispatcher for the current tenant is used. + * This way, if exactly one dispatcher is present, it will be used in all import processes etc. + */ + public ScriptableEventDispatcher fetchDispatcherForCurrentTenant(@Nullable String name) { + if (dispatcherRepository == null) { + return NOOP_DISPATCHER; + } + + if (!ScopeInfo.DEFAULT_SCOPE.getScopeType().equals(UserContext.getCurrentScope().getScopeType())) { + return NOOP_DISPATCHER; + } + + UserInfo currentUser = UserContext.getCurrentUser(); + if (!currentUser.isLoggedIn()) { + return NOOP_DISPATCHER; + } + + return dispatcherRepository.fetchDispatcher(currentUser.getTenantId(), name).orElse(NOOP_DISPATCHER); + } + + /** + * Fetches all available dispatchers for the current tenant. + * + * @return a list of all available dispatchers for the current tenant + */ + public List fetchDispatchersForCurrentTenant() { + if (dispatcherRepository == null) { + return Collections.emptyList(); + } + if (!ScopeInfo.DEFAULT_SCOPE.getScopeType().equals(UserContext.getCurrentScope().getScopeType())) { + return Collections.emptyList(); + } + + UserInfo currentUser = UserContext.getCurrentUser(); + if (!currentUser.isLoggedIn()) { + return Collections.emptyList(); + } + + return dispatcherRepository.fetchAvailableDispatchers(currentUser.getTenantId()); + } +} diff --git a/src/main/java/sirius/biz/scripting/SimpleScriptableEventDispatcher.java b/src/main/java/sirius/biz/scripting/SimpleScriptableEventDispatcher.java new file mode 100644 index 000000000..83d980af4 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/SimpleScriptableEventDispatcher.java @@ -0,0 +1,111 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +import sirius.biz.process.Processes; +import sirius.kernel.async.TaskContext; +import sirius.kernel.commons.Callback; +import sirius.kernel.commons.Watch; +import sirius.kernel.di.std.Part; +import sirius.kernel.health.Exceptions; +import sirius.kernel.health.HandledException; +import sirius.kernel.nls.NLS; + +import java.util.HashMap; +import java.util.Map; + +/** + * Provides a {@link ScriptableEventDispatcher} which also implements {@link ScriptableEventRegistry}. + *

+ * An instance of this class can be first supplied to a custom script in order to pick up handlers and will then + * be used to dispatch upcoming events to these handlers. + */ +public class SimpleScriptableEventDispatcher implements ScriptableEventDispatcher, ScriptableEventRegistry { + + @Part + private static Processes processes; + + private volatile boolean active = false; + + private final Map> handlers = new HashMap<>(); + + @Override + public void registerHandler(Class eventType, Callback handler) { + handlers.put(eventType.getName(), handler); + active = true; + } + + @Override + public > void registerTypedHandler(Class eventType, + Class type, + Callback handler) { + handlers.put(buildTypedEventHandlerName(eventType, type), handler); + active = true; + } + + private String buildTypedEventHandlerName(Class eventType, Class type) { + return eventType.getName() + "::" + type.getName(); + } + + @Override + public boolean isActive() { + return active; + } + + @SuppressWarnings("unchecked") + @Override + public void handleEvent(ScriptableEvent event) { + Callback handler = (Callback) handlers.get(determineEventKey(event)); + if (handler == null) { + return; + } + + Watch watch = Watch.start(); + try { + handler.invoke(event); + event.success = true; + } catch (HandledException handledException) { + handleEventHandlerException(handledException); + event.failed = true; + event.error = handledException; + } catch (Exception e) { + HandledException handledException = Exceptions.handle(Scripting.LOG, e); + handleEventHandlerException(handledException); + event.failed = true; + event.error = handledException; + } + TaskContext.get().addTiming(NLS.get("CustomEventHandler.customEvents"), watch.elapsedMillis()); + } + + private String determineEventKey(ScriptableEvent event) { + if (event instanceof TypedScriptableEvent typedEvent) { + return buildTypedEventHandlerName(typedEvent.getClass(), typedEvent.getType()); + } else { + return event.getClass().getName(); + } + } + + private void handleEventHandlerException(HandledException handledException) { + if (processes.fetchCurrentProcess().isPresent()) { + handleExceptionInProcess(handledException); + } else { + processes.executeInStandbyProcessForCurrentTenant("custom-event-handler", + () -> NLS.get("CustomEventHandler.customEvents"), + ignored -> handleExceptionInProcess(handledException)); + } + } + + private void handleExceptionInProcess(HandledException handledException) { + TaskContext.get() + .log(NLS.fmtr("CustomEventHandler.message") + .set("system", TaskContext.get().getSystemString()) + .set("message", handledException.getMessage()) + .format()); + } +} diff --git a/src/main/java/sirius/biz/scripting/TypedScriptableEvent.java b/src/main/java/sirius/biz/scripting/TypedScriptableEvent.java new file mode 100644 index 000000000..021e94061 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/TypedScriptableEvent.java @@ -0,0 +1,26 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting; + +/** + * Provides a base class for all custom events which can be internally typed to a specified type. + *

+ * This might be used to events which are the same for many classed (e.g. update events for entities). + * + * @param the type of object for which this event occurred. + */ +public abstract class TypedScriptableEvent extends ScriptableEvent { + + /** + * The of objects for which this event occurred. + * + * @return the type of object for which this event occurred + */ + public abstract Class getType(); +} diff --git a/src/main/java/sirius/biz/scripting/mongo/MongoCustomEventDispatcherRepository.java b/src/main/java/sirius/biz/scripting/mongo/MongoCustomEventDispatcherRepository.java new file mode 100644 index 000000000..a27e702a3 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/mongo/MongoCustomEventDispatcherRepository.java @@ -0,0 +1,114 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting.mongo; + +import sirius.biz.scripting.ScriptableEventDispatcher; +import sirius.biz.scripting.ScriptableEventDispatcherRepository; +import sirius.biz.scripting.ScriptableEventRegistry; +import sirius.biz.scripting.SimpleScriptableEventDispatcher; +import sirius.db.mongo.Mango; +import sirius.kernel.async.TaskContext; +import sirius.kernel.commons.Strings; +import sirius.kernel.di.std.Part; +import sirius.kernel.di.std.Register; +import sirius.kernel.health.HandledException; +import sirius.kernel.tokenizer.Position; +import sirius.pasta.noodle.Callable; +import sirius.pasta.noodle.ScriptingException; +import sirius.pasta.noodle.SimpleEnvironment; +import sirius.pasta.noodle.compiler.CompilationContext; +import sirius.pasta.noodle.compiler.NoodleCompiler; +import sirius.pasta.noodle.compiler.SourceCodeInfo; +import sirius.pasta.noodle.sandbox.SandboxMode; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; + +/** + * Stores and manages {@link ScriptableEventDispatcher custom event dispatchers} in a MongoDB. + */ +@Register(framework = MongoCustomEventDispatcherRepository.FRAMEWORK_SCRIPTING_MONGO) +public class MongoCustomEventDispatcherRepository implements ScriptableEventDispatcherRepository { + + /** + * Defines the framework which uses MongoDB to store and provide script based event dispatchers. + */ + public static final String FRAMEWORK_SCRIPTING_MONGO = "biz.scripting-mongo"; + + /** + * Contains the name of the variable which holds the {@link ScriptableEventRegistry} in a script. + */ + public static final String SCRIPT_PARAMETER_REGISTRY = "registry"; + + @Part + private Mango mango; + + @Override + public List fetchAvailableDispatchers(@Nonnull String tenantId) { + return mango.select(MongoCustomScript.class) + .eq(MongoCustomScript.TENANT, tenantId) + .orderAsc(MongoCustomScript.CODE) + .queryList() + .stream() + .map(MongoCustomScript::getCode) + .toList(); + } + + @Override + public Optional fetchDispatcher(@Nonnull String tenantId, @Nullable String name) { + if (Strings.isEmpty(name)) { + List mongoCustomScripts = + mango.select(MongoCustomScript.class).eq(MongoCustomScript.TENANT, tenantId).limit(2).queryList(); + if (mongoCustomScripts.size() == 1) { + return compileAndLoad(mongoCustomScripts.getFirst()); + } else { + return Optional.empty(); + } + } else { + return mango.select(MongoCustomScript.class) + .eq(MongoCustomScript.TENANT, tenantId) + .eq(MongoCustomScript.CODE, name) + .first() + .flatMap(this::compileAndLoad); + } + } + + private Optional compileAndLoad(MongoCustomScript script) { + try { + if (Strings.isEmpty(script.getScript())) { + return Optional.empty(); + } + + CompilationContext compilationContext = + new CompilationContext(SourceCodeInfo.forInlineCode(script.getScript(), SandboxMode.WARN_ONLY)); + + compilationContext.getVariableScoper() + .defineVariable(Position.UNKNOWN, + SCRIPT_PARAMETER_REGISTRY, + ScriptableEventRegistry.class); + NoodleCompiler compiler = new NoodleCompiler(compilationContext); + Callable compiledScript = compiler.compileScript(); + + SimpleScriptableEventDispatcher dispatcher = new SimpleScriptableEventDispatcher(); + SimpleEnvironment environment = new SimpleEnvironment(); + environment.writeVariable(0, dispatcher); + compiledScript.call(environment); + + return Optional.of(dispatcher); + } catch (ScriptingException | HandledException exception) { + TaskContext.get() + .log("Failed compiling custom event dispatcher '%s': %s", + script.getCode(), + exception.getMessage()); + return Optional.empty(); + } + } +} diff --git a/src/main/java/sirius/biz/scripting/mongo/MongoCustomScript.java b/src/main/java/sirius/biz/scripting/mongo/MongoCustomScript.java new file mode 100644 index 000000000..ee3d2c257 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/mongo/MongoCustomScript.java @@ -0,0 +1,72 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting.mongo; + +import sirius.biz.importer.AutoImport; +import sirius.biz.mongo.PrefixSearchContent; +import sirius.biz.tenants.mongo.MongoTenantAware; +import sirius.biz.web.Autoloaded; +import sirius.db.mixing.Mapping; +import sirius.db.mixing.annotations.Index; +import sirius.db.mixing.annotations.NullAllowed; +import sirius.db.mixing.annotations.Unique; +import sirius.db.mongo.Mango; +import sirius.kernel.commons.Strings; +import sirius.kernel.nls.NLS; + +/** + * Stores a custom scripting profile for a tenant. + */ +@Index(name = "lookup", columns = {"tenant", "code"}, columnSettings = {Mango.INDEX_ASCENDING, Mango.INDEX_ASCENDING}) +public class MongoCustomScript extends MongoTenantAware { + + /** + * Contains the code or name of the script. + */ + public static final Mapping CODE = Mapping.named("code"); + @Unique(within = "tenant") + @PrefixSearchContent + @Autoloaded + @AutoImport + private String code; + + /** + * Contains the actual scripting code. + */ + public static final Mapping SCRIPT = Mapping.named("script"); + @NullAllowed + @Autoloaded + @AutoImport + private String script; + + @Override + public String toString() { + if (Strings.isFilled(code)) { + return code; + } else { + return NLS.get("MongoCustomScript.unnamedScript"); + } + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getScript() { + return script; + } + + public void setScript(String script) { + this.script = script; + } +} diff --git a/src/main/java/sirius/biz/scripting/mongo/MongoCustomScriptController.java b/src/main/java/sirius/biz/scripting/mongo/MongoCustomScriptController.java new file mode 100644 index 000000000..8e97b7ed5 --- /dev/null +++ b/src/main/java/sirius/biz/scripting/mongo/MongoCustomScriptController.java @@ -0,0 +1,110 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.scripting.mongo; + +import sirius.biz.scripting.ScriptableEventRegistry; +import sirius.biz.scripting.ScriptingController; +import sirius.biz.web.BizController; +import sirius.biz.web.MongoPageHelper; +import sirius.db.mixing.query.QueryField; +import sirius.db.mongo.Mango; +import sirius.kernel.di.std.Part; +import sirius.kernel.di.std.Register; +import sirius.kernel.tokenizer.Position; +import sirius.pasta.noodle.compiler.CompilationContext; +import sirius.pasta.noodle.compiler.NoodleCompiler; +import sirius.pasta.noodle.compiler.SourceCodeInfo; +import sirius.pasta.noodle.sandbox.SandboxMode; +import sirius.pasta.tagliatelle.compiler.TemplateCompiler; +import sirius.web.controller.Routed; +import sirius.web.http.WebContext; +import sirius.web.security.Permission; +import sirius.web.services.InternalService; +import sirius.web.services.JSONStructuredOutput; + +/** + * Provides the management UI for {@link MongoCustomScript custom scripts}. + */ +@Register(framework = MongoCustomEventDispatcherRepository.FRAMEWORK_SCRIPTING_MONGO) +public class MongoCustomScriptController extends BizController { + + private static final String PARAM_SCRIPT = "script"; + + @Part + private Mango mango; + + /** + * Lists all scripts available for the current tenant. + * + * @param webContext the request to handle + */ + @Routed("/scripting/scripts") + @Permission(ScriptingController.PERMISSION_SCRIPTING) + public void listScripts(WebContext webContext) { + MongoPageHelper pageHelper = + MongoPageHelper.withQuery(tenants.forCurrentTenant(mango.select(MongoCustomScript.class) + .orderAsc(MongoCustomScript.CODE))) + .withContext(webContext); + + pageHelper.withSearchFields(QueryField.startsWith(MongoCustomScript.SEARCH_PREFIXES)); + webContext.respondWith().template("/templates/biz/scripting/mongo-scripts.html.pasta", pageHelper.asPage()); + } + + /** + * Modifies / manages the given script. + * + * @param webContext the request to handle + */ + @Routed("/scripting/scripts/:1") + @Permission(ScriptingController.PERMISSION_SCRIPTING) + public void editScript(WebContext webContext, String id) { + MongoCustomScript script = findForTenant(MongoCustomScript.class, id); + boolean requestHandled = prepareSave(webContext).withAfterSaveURI("/scripting/scripts").saveEntity(script); + if (!requestHandled) { + webContext.respondWith().template("/templates/biz/scripting/mongo-script.html.pasta", script); + } + } + + /** + * Deletes the given script. + * + * @param webContext the request to handle + */ + @Routed("/scripting/scripts/:1/delete") + @Permission(ScriptingController.PERMISSION_SCRIPTING) + public void deleteScript(WebContext webContext, String id) { + deleteEntity(webContext, tryFindForTenant(MongoCustomScript.class, id)); + webContext.respondWith().redirectToGet("/scripting/scripts"); + } + + /** + * Runs the compiler on a given script and reports all errors or warnings. + * + * @param webContext the request to handle + * @param output the output to write to + */ + @Routed("/scripting/api/compile") + @InternalService + @Permission(ScriptingController.PERMISSION_SCRIPTING) + public void compile(WebContext webContext, JSONStructuredOutput output) { + if (webContext.isSafePOST()) { + String script = webContext.get(PARAM_SCRIPT).asString(); + CompilationContext compilationContext = + new CompilationContext(SourceCodeInfo.forInlineCode(script, SandboxMode.WARN_ONLY)); + compilationContext.getVariableScoper() + .defineVariable(Position.UNKNOWN, + MongoCustomEventDispatcherRepository.SCRIPT_PARAMETER_REGISTRY, + ScriptableEventRegistry.class); + + NoodleCompiler compiler = new NoodleCompiler(compilationContext); + compiler.compileScript(); + TemplateCompiler.reportAsJson(compilationContext.getErrors(), output); + } + } +} diff --git a/src/main/java/sirius/biz/tenants/mongo/MongoTenantMetricComputer.java b/src/main/java/sirius/biz/tenants/mongo/MongoTenantMetricComputer.java index 46c1a0918..8a0fbd5a9 100644 --- a/src/main/java/sirius/biz/tenants/mongo/MongoTenantMetricComputer.java +++ b/src/main/java/sirius/biz/tenants/mongo/MongoTenantMetricComputer.java @@ -9,11 +9,15 @@ package sirius.biz.tenants.mongo; import sirius.biz.analytics.flags.PerformanceFlag; +import sirius.biz.analytics.metrics.MetricComputerContext; import sirius.biz.model.LoginData; +import sirius.biz.scripting.mongo.MongoCustomEventDispatcherRepository; +import sirius.biz.scripting.mongo.MongoCustomScript; import sirius.biz.tenants.UserAccount; import sirius.biz.tenants.UserAccountData; import sirius.biz.tenants.metrics.computers.TenantMetricComputer; import sirius.db.mongo.Mango; +import sirius.kernel.Sirius; import sirius.kernel.di.std.Part; import sirius.kernel.di.std.Register; @@ -32,14 +36,33 @@ public class MongoTenantMetricComputer extends TenantMetricComputer PerformanceFlag.register(MongoTenant.class, "active-users", 0).makeVisible().markAsFilter(); /** - * Marks tenants which hase users that use the video academy. + * Marks tenants which have users that use the video academy. */ public static final PerformanceFlag ACADEMY_USERS = PerformanceFlag.register(MongoTenant.class, "academy-users", 1).makeVisible().markAsFilter(); + /** + * Marks tenants which have at least one {@link MongoCustomScript custom script}. + */ + public static final PerformanceFlag CUSTOM_SCRIPTS = + PerformanceFlag.register(MongoTenant.class, "custom-scripts", 2).makeVisible().markAsFilter(); + @Part private Mango mango; + @Override + public void compute(MetricComputerContext context, MongoTenant tenant) throws Exception { + super.compute(context, tenant); + + if (Sirius.isFrameworkEnabled(MongoCustomEventDispatcherRepository.FRAMEWORK_SCRIPTING_MONGO)) { + tenant.getPerformanceData() + .modify() + .set(CUSTOM_SCRIPTS, + mango.select(MongoCustomScript.class).eq(MongoCustomScript.TENANT, tenant.getId()).exists()) + .commit(); + } + } + @Override protected PerformanceFlag getAcademyUsersFlag() { return ACADEMY_USERS; diff --git a/src/main/resources/biz_de.properties b/src/main/resources/biz_de.properties index 46cb916f3..fe5cd348d 100644 --- a/src/main/resources/biz_de.properties +++ b/src/main/resources/biz_de.properties @@ -196,6 +196,8 @@ Country.us = USA Country.vn = Vietnam Country.xk = Kosovo Country.za = Südafrika +CustomEventHandler.customEvents = Kunden-Scripting +CustomEventHandler.message = Fehler in Kunden-Script - System: ${system}: ${message} DashboardBrowserDistributionChart.description = Zeigt die Anzahl der Aufrufe der Backend-Startseite je Browser an. DashboardBrowserDistributionChart.label = Browser-Verteilung im Backend DashboardController.help = Hilfe & Support @@ -666,6 +668,10 @@ MongoCodeListExportJobFactory.description = Exportiert eine Codeliste als CSV od MongoCodeListExportJobFactory.label = Codeliste exportieren MongoCodeListImportJobFactory.description = Importiert eine Codeliste aus einer CSV oder Excel Datei. MongoCodeListImportJobFactory.label = Codeliste importieren +MongoCustomScript.code = Code +MongoCustomScript.plural = Kunden-Scripte +MongoCustomScript.script = Quelltext +MongoCustomScript.unnamedScript = Unbenanntes Script MongoTenantExportJobFactory.description = Exportiert Mandanten in eine CSV oder Excel Datei. MongoTenantExportJobFactory.label = Mandanten exportieren MongoUserAccountExportJobFactory.description = Exportiert Anwender in eine CSV oder Excel Datei. @@ -745,6 +751,7 @@ Parameter.required = Der Parameter ${name} muss gefüllt sein. PerformanceData.flags = Flags PerformanceFlag.academy-user = Nutzt Video-Academy PerformanceFlag.academy-users = Nutzt Video-Academy +PerformanceFlag.custom-scripts = Nutzt Kunden-Scripting PerformanceFlag.active-user = Aktiv PerformanceFlag.active-users = Hat aktive Nutzer PerformanceFlag.frequent-user = Regelmäßig Aktiv diff --git a/src/main/resources/component-070-biz.conf b/src/main/resources/component-070-biz.conf index e8f05a557..a7a17a402 100644 --- a/src/main/resources/component-070-biz.conf +++ b/src/main/resources/component-070-biz.conf @@ -113,6 +113,10 @@ sirius.frameworks { # Provides a storage option for in a MongoDB. biz.analytics-metrics-mongo = false + # Uses MongoDB to store and manage tenant specific scripts to handle incoming + # script event (e.g. in import processes). + biz.scripting-mongo = false + # Provides an uplink to talk to one or more Jupiter instances. jupiter = false diff --git a/src/main/resources/default/extensions/biz-menu/menu-custom-scripts.html.pasta b/src/main/resources/default/extensions/biz-menu/menu-custom-scripts.html.pasta new file mode 100644 index 000000000..c306fb58f --- /dev/null +++ b/src/main/resources/default/extensions/biz-menu/menu-custom-scripts.html.pasta @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/main/resources/default/extensions/biz-menu/menu-tenants.html.pasta b/src/main/resources/default/extensions/biz-menu/menu-tenants.html.pasta index f14792a3b..2cad90ae3 100644 --- a/src/main/resources/default/extensions/biz-menu/menu-tenants.html.pasta +++ b/src/main/resources/default/extensions/biz-menu/menu-tenants.html.pasta @@ -15,7 +15,6 @@ - @@ -33,6 +32,5 @@ url="/academy" /> - diff --git a/src/main/resources/default/extensions/biz-tycho-menu/menu-custom-scripts.html.pasta b/src/main/resources/default/extensions/biz-tycho-menu/menu-custom-scripts.html.pasta new file mode 100644 index 000000000..3707ed8b4 --- /dev/null +++ b/src/main/resources/default/extensions/biz-tycho-menu/menu-custom-scripts.html.pasta @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/main/resources/default/extensions/biz-tycho-menu/menu-tenants.html.pasta b/src/main/resources/default/extensions/biz-tycho-menu/menu-tenants.html.pasta index 8bd2601ca..30b30642f 100644 --- a/src/main/resources/default/extensions/biz-tycho-menu/menu-tenants.html.pasta +++ b/src/main/resources/default/extensions/biz-tycho-menu/menu-tenants.html.pasta @@ -8,14 +8,13 @@ + permission="permission-manage-system-users"/> + permission="permission-manage-user-accounts"/> - @@ -25,12 +24,12 @@ + permission="permission-select-user-account"/> + url="/academy"/> diff --git a/src/main/resources/default/templates/biz/scripting/mongo-script.html.pasta b/src/main/resources/default/templates/biz/scripting/mongo-script.html.pasta new file mode 100644 index 000000000..dc1dc29eb --- /dev/null +++ b/src/main/resources/default/templates/biz/scripting/mongo-script.html.pasta @@ -0,0 +1,74 @@ + + + + + + + + +

  • + @i18n("MongoCustomScript.plural") +
  • +
  • + @script +
  • + + + + + + + + + + + + +
    + @script.getScript() +
    + + +
    + + + diff --git a/src/main/resources/default/templates/biz/scripting/mongo-scripts.html.pasta b/src/main/resources/default/templates/biz/scripting/mongo-scripts.html.pasta new file mode 100644 index 000000000..da8d1ec1b --- /dev/null +++ b/src/main/resources/default/templates/biz/scripting/mongo-scripts.html.pasta @@ -0,0 +1,46 @@ + + + + +
  • + @i18n("MongoCustomScript.plural") +
  • +
    + + + + + + + + +
    +
    + + + + + + + + + + + + + + + +
    @i18n("MongoCustomScript.code") +
    + @script.getCode() + + +
    + +
    +
    +
    +