From 6d127dcec621da76e817db7809ba6edbf074b486 Mon Sep 17 00:00:00 2001 From: Sylvain Jermini Date: Wed, 27 Dec 2017 19:20:49 +0100 Subject: [PATCH 01/31] #349 stubs/initial work --- .../alfio/config/DataSourceConfiguration.java | 2 +- .../api/admin/ScriptController.java | 69 ++++++++++++ src/main/java/alfio/model/ScriptSupport.java | 47 ++++++++ .../alfio/repository/ScriptRepository.java | 61 +++++++++++ src/main/java/alfio/scripting/Script.java | 34 ++++++ .../java/alfio/scripting/ScriptMetadata.java | 25 +++++ .../scripting/ScriptingExecutionService.java | 93 ++++++++++++++++ .../alfio/scripting/ScriptingService.java | 100 ++++++++++++++++++ .../db/HSQLDB/V22_1.14.0__ADD_SCRIPTING.sql | 31 ++++++ .../webapp/WEB-INF/templates/admin/index.ms | 4 + .../add-update/scripting-add-update.html | 1 + .../add-update/scripting-add-update.js | 12 +++ .../js/admin/feature/scripting/scripting.html | 29 +++++ .../js/admin/feature/scripting/scripting.js | 32 ++++++ .../js/admin/ng-app/admin-application.js | 3 + 15 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 src/main/java/alfio/controller/api/admin/ScriptController.java create mode 100644 src/main/java/alfio/model/ScriptSupport.java create mode 100644 src/main/java/alfio/repository/ScriptRepository.java create mode 100644 src/main/java/alfio/scripting/Script.java create mode 100644 src/main/java/alfio/scripting/ScriptMetadata.java create mode 100644 src/main/java/alfio/scripting/ScriptingExecutionService.java create mode 100644 src/main/java/alfio/scripting/ScriptingService.java create mode 100644 src/main/resources/alfio/db/HSQLDB/V22_1.14.0__ADD_SCRIPTING.sql create mode 100644 src/main/webapp/resources/js/admin/feature/scripting/add-update/scripting-add-update.html create mode 100644 src/main/webapp/resources/js/admin/feature/scripting/add-update/scripting-add-update.js create mode 100644 src/main/webapp/resources/js/admin/feature/scripting/scripting.html create mode 100644 src/main/webapp/resources/js/admin/feature/scripting/scripting.js diff --git a/src/main/java/alfio/config/DataSourceConfiguration.java b/src/main/java/alfio/config/DataSourceConfiguration.java index d4af6175ef..c2184d4328 100644 --- a/src/main/java/alfio/config/DataSourceConfiguration.java +++ b/src/main/java/alfio/config/DataSourceConfiguration.java @@ -70,7 +70,7 @@ @EnableTransactionManagement @EnableScheduling @EnableAsync -@ComponentScan(basePackages = {"alfio.manager"}) +@ComponentScan(basePackages = {"alfio.manager", "alfio.scripting"}) @Log4j2 public class DataSourceConfiguration implements ResourceLoaderAware { diff --git a/src/main/java/alfio/controller/api/admin/ScriptController.java b/src/main/java/alfio/controller/api/admin/ScriptController.java new file mode 100644 index 0000000000..60f110c925 --- /dev/null +++ b/src/main/java/alfio/controller/api/admin/ScriptController.java @@ -0,0 +1,69 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ + +package alfio.controller.api.admin; + +import alfio.manager.user.UserManager; +import alfio.model.ScriptSupport; +import alfio.model.user.Role; +import alfio.scripting.Script; +import alfio.scripting.ScriptingService; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.Validate; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.util.List; + +@RestController +@AllArgsConstructor +@RequestMapping("/admin/api") +public class ScriptController { + + private final ScriptingService scriptingService; + private final UserManager userManager; + + + @RequestMapping(value = "/scripting", method = RequestMethod.GET) + public List listAll(Principal principal) { + ensureAdmin(principal); + return scriptingService.listAll(); + } + + @RequestMapping(value = "/scripting", method = RequestMethod.POST) + public void createOrUpdate(@RequestBody Script script, Principal principal) { + ensureAdmin(principal); + scriptingService.createOrUpdate(script); + } + + + @RequestMapping(value = "/scripting/{path}", method = RequestMethod.DELETE) + public void delete(@PathVariable("path") String path, Principal principal) { + ensureAdmin(principal); + scriptingService.delete(path); + } + + @RequestMapping(value = "/scripting/{path}/toggle/{enable}", method = RequestMethod.POST) + public void toggle(@PathVariable("path") String path, @PathVariable("enable") boolean enable, Principal principal) { + ensureAdmin(principal); + scriptingService.toggle(path, enable); + } + + private void ensureAdmin(Principal principal) { + Validate.isTrue(userManager.isAdmin(userManager.findUserByUsername(principal.getName()))); + } +} diff --git a/src/main/java/alfio/model/ScriptSupport.java b/src/main/java/alfio/model/ScriptSupport.java new file mode 100644 index 0000000000..16ac699461 --- /dev/null +++ b/src/main/java/alfio/model/ScriptSupport.java @@ -0,0 +1,47 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.model; + +import ch.digitalfondue.npjt.ConstructorAnnotationRowMapper.Column; + +public class ScriptSupport { + + private final String path; + private final String name; + private final String hash; + private final boolean enabled; + private final boolean async; + private final String script; + private final String configuration; + + + public ScriptSupport(@Column("path") String path, + @Column("name") String name, + @Column("hash") String hash, + @Column("enabled") boolean enabled, + @Column("async") boolean async, + @Column("script") String script, + @Column("configuration") String configuration) { + this.path = path; + this.name = name; + this.hash = hash; + this.enabled = enabled; + this.async = async; + this.script = script; + this.configuration = configuration; + } +} diff --git a/src/main/java/alfio/repository/ScriptRepository.java b/src/main/java/alfio/repository/ScriptRepository.java new file mode 100644 index 0000000000..dbc0c43126 --- /dev/null +++ b/src/main/java/alfio/repository/ScriptRepository.java @@ -0,0 +1,61 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ + +package alfio.repository; + +import alfio.model.ScriptSupport; +import ch.digitalfondue.npjt.Bind; +import ch.digitalfondue.npjt.Query; +import ch.digitalfondue.npjt.QueryRepository; + +import java.util.List; + +@QueryRepository +public interface ScriptRepository { + + @Query("insert into script_support(path, name, hash, enabled, async, script, configuration) values " + + " (:path, :name, :hash, :enabled, :async, :script, :configuration)") + int insert(@Bind("path") String path, + @Bind("name") String name, + @Bind("hash") String hash, + @Bind("enabled") boolean enabled, + @Bind("async") boolean async, + @Bind("script") String script, + @Bind("configuration") String configuration); + + @Query("update script_support set enabled = :enabled where path = :path") + int toggle(@Bind("path") String path, @Bind("enabled") boolean enabled); + + @Query("insert into script_event(path, event) values " + + " (:path, :event)") + int insert(@Bind("path") String path, @Bind("event ") String event); + + @Query("select count(*) from script_support where path = :path") + int hasPath(@Bind("path") String path); + + @Query("select script from script_support where path = :path") + String getScript(@Bind("path") String path); + + @Query("delete from script_event where path = :path") + int deleteEventsForPath(@Bind("path") String path); + + @Query("delete from script_support where path = :path") + int deleteScriptForPath(@Bind("path") String path); + + @Query("select * from script_support order by path") + List listAll(); +} diff --git a/src/main/java/alfio/scripting/Script.java b/src/main/java/alfio/scripting/Script.java new file mode 100644 index 0000000000..905cd74736 --- /dev/null +++ b/src/main/java/alfio/scripting/Script.java @@ -0,0 +1,34 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ + +package alfio.scripting; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Map; + +@Getter +@AllArgsConstructor +public class Script { + + private final String path; + private final String name; + private final String script; + private final Map configuration; + private final boolean enabled; +} diff --git a/src/main/java/alfio/scripting/ScriptMetadata.java b/src/main/java/alfio/scripting/ScriptMetadata.java new file mode 100644 index 0000000000..b35b2420da --- /dev/null +++ b/src/main/java/alfio/scripting/ScriptMetadata.java @@ -0,0 +1,25 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ + +package alfio.scripting; + +import java.util.List; + +public class ScriptMetadata { + boolean async; + List events; +} diff --git a/src/main/java/alfio/scripting/ScriptingExecutionService.java b/src/main/java/alfio/scripting/ScriptingExecutionService.java new file mode 100644 index 0000000000..4df1f55d4b --- /dev/null +++ b/src/main/java/alfio/scripting/ScriptingExecutionService.java @@ -0,0 +1,93 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ + +package alfio.scripting; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +import javax.script.*; +import java.util.Map; +import java.util.concurrent.*; +import java.util.function.Supplier; + + +// +// table {path, name, hash, script content, params} +// where path is unique, in our case can be: +// +// /name +// /organization/name +// /organization/event/name +// /organization/event/ticket_category/name +// +// for async execution -> for each path make a queue + +@Service +@Log4j2 +public class ScriptingExecutionService { + + private final static Compilable engine = (Compilable) new ScriptEngineManager().getEngineByName("javascript"); + private final Cache compiledScriptCache = Caffeine.newBuilder() + .expireAfterAccess(12, TimeUnit.HOURS) + .build(); + private final Cache asyncExecutors = Caffeine.newBuilder() + .expireAfterAccess(12, TimeUnit.HOURS) + .build(); + + public T executeScript(String path, String name, String hash, Supplier scriptFetcher, Map params) { + CompiledScript compiledScript = compiledScriptCache.get(hash, (key) -> { + try { + return engine.compile(scriptFetcher.get()); + } catch (ScriptException se) { + log.warn("Was not able to compile script " + name, se); + throw new IllegalStateException(se); + } + }); + return (T) executeScript(name, compiledScript, params); + } + + public void executeScriptAsync(String path, String name, String hash, Supplier scriptFetcher, Map params) { + asyncExecutors.get(path, (key) -> Executors.newSingleThreadExecutor()).submit(() -> { + executeScript(path, name, hash, scriptFetcher, params); + }); + } + + public static T executeScript(String name, String script, Map params) { + try { + CompiledScript compiledScript = engine.compile(script); + return (T) executeScript(name, compiledScript, params); + } catch (ScriptException se) { + log.warn("Was not able to compile script", se); + throw new IllegalStateException(se); + } + } + + private static Object executeScript(String name, CompiledScript script, Map params) { + try { + ScriptContext newContext = new SimpleScriptContext(); + Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE); + engineScope.putAll(params); + return script.eval(newContext); + } catch (ScriptException ex) { + log.warn("Error while executing script " + name, ex); + throw new IllegalStateException(ex); + } + } +} diff --git a/src/main/java/alfio/scripting/ScriptingService.java b/src/main/java/alfio/scripting/ScriptingService.java new file mode 100644 index 0000000000..5db412ee80 --- /dev/null +++ b/src/main/java/alfio/scripting/ScriptingService.java @@ -0,0 +1,100 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ + +package alfio.scripting; + +import alfio.model.ScriptSupport; +import alfio.repository.ScriptRepository; +import alfio.util.Json; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.tuple.Triple; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@Log4j2 +@AllArgsConstructor +public class ScriptingService { + + private final ScriptingExecutionService scriptingExecutionService; + + private final ScriptRepository scriptRepository; + + @Transactional + public void createOrUpdate(Script script) { + String hash = DigestUtils.sha256Hex(script.getScript()); + ScriptMetadata scriptMetadata = ScriptingExecutionService.executeScript( + script.getName(), + script.getScript() + "\n;return getScriptMetadata();", + script.getConfiguration()); + + + if(scriptRepository.hasPath(script.getPath()) > 0) { + scriptRepository.deleteEventsForPath(script.getPath()); + scriptRepository.deleteScriptForPath(script.getPath()); + } + + scriptRepository.insert(script.getPath(), script.getName(), hash, script.isEnabled(), scriptMetadata.async, script.getScript(), Json.toJson(script.getConfiguration())); + for(String event : scriptMetadata.events) { + scriptRepository.insert(script.getPath(), event); + } + } + + @Transactional + public void toggle(String path, boolean status) { + scriptRepository.toggle(path, status); + } + + @Transactional + public void delete(String path) { + scriptRepository.deleteEventsForPath(path); + scriptRepository.deleteScriptForPath(path); + } + + public String getScript(String path) { + return scriptRepository.getScript(path); + } + + public T executeScriptsForEvent(String event, String basePath, Map payload) { + List> activePaths = getActiveScriptsForEvent(event, basePath); + T res = null; + Map input = new HashMap<>(payload); + input.put("event", event); + for(Triple activePath : activePaths) { + String path = activePath.getLeft(); + res = scriptingExecutionService.executeScript(activePath.getLeft(), activePath.getMiddle(), activePath.getRight(), + () -> getScript(path)+"\n;return executeScript(event);", input); + input.put("output", res); + } + return res; + } + + private List> getActiveScriptsForEvent(String event, String basePath) { + return Collections.emptyList(); + } + + public List listAll() { + return scriptRepository.listAll(); + } +} diff --git a/src/main/resources/alfio/db/HSQLDB/V22_1.14.0__ADD_SCRIPTING.sql b/src/main/resources/alfio/db/HSQLDB/V22_1.14.0__ADD_SCRIPTING.sql new file mode 100644 index 0000000000..63e6a4bd7e --- /dev/null +++ b/src/main/resources/alfio/db/HSQLDB/V22_1.14.0__ADD_SCRIPTING.sql @@ -0,0 +1,31 @@ +-- +-- This file is part of alf.io. +-- +-- alf.io is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- alf.io is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with alf.io. If not, see . +-- + +create table script_support ( + path varchar(256) not null, + name varchar(256) not null, + hash varchar(256) not null, + enabled boolean not null, + async boolean not null, + script clob not null, + configuration clob +); + +create table script_event ( + path_fk varchar(256) not null, + event varchar(256) not null +); \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/templates/admin/index.ms b/src/main/webapp/WEB-INF/templates/admin/index.ms index acb4862687..be7d3b4ace 100644 --- a/src/main/webapp/WEB-INF/templates/admin/index.ms +++ b/src/main/webapp/WEB-INF/templates/admin/index.ms @@ -83,6 +83,9 @@ + + + @@ -114,6 +117,7 @@
  • Organizations
  • Users
  • Configuration
  • +
  • Scripting
  • {{/isOwner}}