diff --git a/cms-core/pom.xml b/cms-core/pom.xml index c12c589e9e..112a8ad3be 100644 --- a/cms-core/pom.xml +++ b/cms-core/pom.xml @@ -436,6 +436,16 @@ handlebars + + + org.apache.groovy + groovy + + + org.apache.groovy + groovy-json + + org.glassfish.jersey.containers diff --git a/cms-core/src/main/java/com/gentics/api/ChannelScope.java b/cms-core/src/main/java/com/gentics/api/ChannelScope.java new file mode 100644 index 0000000000..8b7c65a9a9 --- /dev/null +++ b/cms-core/src/main/java/com/gentics/api/ChannelScope.java @@ -0,0 +1,40 @@ +package com.gentics.api; + +import com.gentics.api.lib.exception.NodeException; +import com.gentics.contentnode.factory.ChannelTrx; +import com.gentics.contentnode.factory.NoMcTrx; +import com.gentics.contentnode.object.Node; +import com.gentics.contentnode.object.parttype.NodePartType; +import com.gentics.contentnode.render.RenderUtils; + +import groovy.lang.Closure; + +/** + * Channel Scope helper which should be used in Groovy Scripts to load objects in the scope of channels + */ +public class ChannelScope { + /** + * Private constructor + */ + private ChannelScope() { + } + + /** + * Run the closure body in a {@link ChannelTrx} + * @param type of the return value + * @param scope scope (channel) + * @param body closure body + * @return return value + * @throws NodeException + */ + public static T withChannel(Object scope, Closure body) throws NodeException { + Node node = null; + try (final NoMcTrx nMcTrx = new NoMcTrx()) { + node = RenderUtils.getObject(scope, Node.class, NodePartType.class, NodePartType::getNode); + } + + try (ChannelTrx cTrx = new ChannelTrx(node)) { + return body.call(cTrx); + } + } +} diff --git a/cms-core/src/main/java/com/gentics/api/Loader.java b/cms-core/src/main/java/com/gentics/api/Loader.java new file mode 100644 index 0000000000..44529d12a9 --- /dev/null +++ b/cms-core/src/main/java/com/gentics/api/Loader.java @@ -0,0 +1,220 @@ +package com.gentics.api; + +import static com.gentics.contentnode.render.RenderUtils.wrap; + +import java.util.Map; + +import com.gentics.api.lib.exception.NodeException; +import com.gentics.api.lib.resolving.Resolvable; +import com.gentics.contentnode.factory.Transaction; +import com.gentics.contentnode.factory.TransactionManager; +import com.gentics.contentnode.object.File; +import com.gentics.contentnode.object.Folder; +import com.gentics.contentnode.object.ImageFile; +import com.gentics.contentnode.object.Node; +import com.gentics.contentnode.object.NodeObject; +import com.gentics.contentnode.object.Page; +import com.gentics.lib.log.NodeLogger; + +/** + * Loader helper, which should be used in Groovy Scripts to load objects + */ +public class Loader { + private static NodeLogger logger = NodeLogger.getNodeLogger(Loader.class); + + /** + * Map of types to type ids + */ + private final static Map TYPES = Map.of( + "page", Page.TYPE_PAGE, + "folder", Folder.TYPE_FOLDER, + "file", File.TYPE_FILE, + "image", ImageFile.TYPE_IMAGE, + "node", Node.TYPE_NODE); + + /** + * Private constructor + */ + private Loader() { + } + + /** + * Load object with given type and id + * @param type object type + * @param id object id (as integer) + * @return loaded object or null, if not found + * @throws NodeException + */ + public static Resolvable object(int type, int id) throws NodeException { + Transaction t = TransactionManager.getCurrentTransaction(); + + Class clazz = t.getClass(type); + if (clazz == null) { + logger.warn("Unable to load object of unknown type ID %d".formatted(type)); + return null; + } + + NodeObject object = t.getObject(clazz, id); + if (object == null && type == File.TYPE_FILE) { + return object(ImageFile.TYPE_IMAGE, id); + } + + return wrap(object); + } + + /** + * Load object with given type and id + * @param type object type + * @param id object id (as string) + * @return loaded object or null, if not found + * @throws NodeException + */ + public static Resolvable object(int type, String id) throws NodeException { + Transaction t = TransactionManager.getCurrentTransaction(); + + Class clazz = t.getClass(type); + if (clazz == null) { + logger.warn("Unable to load object of unknown type ID %d".formatted(type)); + return null; + } + + NodeObject object = t.getObject(clazz, id); + if (object == null && type == File.TYPE_FILE) { + return object(ImageFile.TYPE_IMAGE, id); + } + + return wrap(object); + } + + /** + * Load object with given type and id + * @param type type + * @param id id (as integer) + * @return loaded object or null, if not found + * @throws NodeException + */ + public static Resolvable object(String type, int id) throws NodeException { + if (TYPES.containsKey(type)) { + return object(TYPES.get(type), id); + } else { + logger.warn("Unable to load object of unknown type %s".formatted(type)); + return null; + } + } + + /** + * Load object with given type and id + * @param type type + * @param id id (as string) + * @return loaded object or null, if not found + * @throws NodeException + */ + public static Resolvable object(String type, String id) throws NodeException { + if (TYPES.containsKey(type)) { + return object(TYPES.get(type), id); + } else { + logger.warn("Unable to load object of unknown type %s".formatted(type)); + return null; + } + } + + /** + * Load page + * @param id page id + * @return wrapped page + * @throws NodeException + */ + public static Resolvable page(int id) throws NodeException { + return object(Page.TYPE_PAGE, id); + } + + /** + * Load page + * @param id page id + * @return wrapped page + * @throws NodeException + */ + public static Resolvable page(String id) throws NodeException { + return object(Page.TYPE_PAGE, id); + } + + /** + * Load folder + * @param id folder id + * @return wrapped folder + * @throws NodeException + */ + public static Resolvable folder(int id) throws NodeException { + return object(Folder.TYPE_FOLDER, id); + } + + /** + * Load folder + * @param id folder id + * @return wrapped folder + * @throws NodeException + */ + public static Resolvable folder(String id) throws NodeException { + return object(Folder.TYPE_FOLDER, id); + } + + /** + * Load file + * @param id file id + * @return wrapped file + * @throws NodeException + */ + public static Resolvable file(int id) throws NodeException { + return object(File.TYPE_FILE, id); + } + + /** + * Load file + * @param id file id + * @return wrapped file + * @throws NodeException + */ + public static Resolvable file(String id) throws NodeException { + return object(File.TYPE_FILE, id); + } + + /** + * Load image + * @param id image id + * @return wrapped image + * @throws NodeException + */ + public static Resolvable image(int id) throws NodeException { + return object(ImageFile.TYPE_IMAGE, id); + } + + /** + * Load image + * @param id image id + * @return wrapped image + * @throws NodeException + */ + public static Resolvable image(String id) throws NodeException { + return object(ImageFile.TYPE_IMAGE, id); + } + + /** + * Load node + * @param id node id + * @return wrapped node + * @throws NodeException + */ + public static Resolvable node(int id) throws NodeException { + return object(Node.TYPE_NODE, id); + } + + /** + * Load node + * @param id node id + * @return wrapped node + * @throws NodeException + */ + public static Resolvable node(String id) throws NodeException { + return object(Node.TYPE_NODE, id); + } +} diff --git a/cms-core/src/main/java/com/gentics/contentnode/devtools/AbstractSynchronizer.java b/cms-core/src/main/java/com/gentics/contentnode/devtools/AbstractSynchronizer.java index bfde4df53f..8eeac9f107 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/devtools/AbstractSynchronizer.java +++ b/cms-core/src/main/java/com/gentics/contentnode/devtools/AbstractSynchronizer.java @@ -28,6 +28,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import com.gentics.api.lib.etc.ObjectTransformer; import com.gentics.api.lib.exception.NodeException; @@ -559,8 +560,10 @@ protected void jsonToFile(Object object, File file) throws NodeException { */ protected String getProposedFilename(Part part) { // special case for HandlebarsPartType - if (part.getPartTypeId() == 43) { + if (part.getPartTypeId() == Part.HANDLEBARS) { return "part." + part.getKeyname() + ".hbs"; + } else if (part.getPartTypeId() == Part.GROOVY) { + return "part." + part.getKeyname() + ".groovy"; } switch (Property.Type.get(part.getPartTypeId())) { case STRING: @@ -593,7 +596,7 @@ protected boolean isJsonFile(Part part) { * @return true iff the filename belongs to a part value */ protected boolean isPartFilename(String filename) { - return filename.startsWith("part.") && (filename.endsWith(".txt") || filename.endsWith(".html") || filename.endsWith(".json") || filename.endsWith(".hbs")); + return Strings.CI.startsWith(filename, "part.") && Strings.CI.endsWithAny(filename, ".txt", ".html", ".json", ".hbs", ".groovy"); } /** diff --git a/cms-core/src/main/java/com/gentics/contentnode/devtools/MainPackageSynchronizer.java b/cms-core/src/main/java/com/gentics/contentnode/devtools/MainPackageSynchronizer.java index da16c49a5d..d1b6c3cbc6 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/devtools/MainPackageSynchronizer.java +++ b/cms-core/src/main/java/com/gentics/contentnode/devtools/MainPackageSynchronizer.java @@ -38,7 +38,7 @@ public class MainPackageSynchronizer extends PackageSynchronizer { * List containing invalid subpackage directory names */ public final static List INVALID_SUBPACKAGE_NAMES = Arrays.asList(CONSTRUCTS_DIR, DATASOURCES_DIR, OBJECTPROPERTIES_DIR, TEMPLATES_DIR, FILES_DIR, - CR_FRAGMENTS_DIR, CONTENTREPOSITORIES_DIR, HANDLEBARS_DIR); + CR_FRAGMENTS_DIR, CONTENTREPOSITORIES_DIR, HANDLEBARS_DIR, SCRIPTS_DIR); /** * Lambda that generates the rest model for a package diff --git a/cms-core/src/main/java/com/gentics/contentnode/devtools/PackageSynchronizer.java b/cms-core/src/main/java/com/gentics/contentnode/devtools/PackageSynchronizer.java index 2b459f92af..f19f5ee15f 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/devtools/PackageSynchronizer.java +++ b/cms-core/src/main/java/com/gentics/contentnode/devtools/PackageSynchronizer.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.io.Reader; import java.io.StringWriter; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -27,7 +26,7 @@ import java.util.function.Consumer; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.tautua.markdownpapers.Markdown; import com.gentics.api.lib.exception.NodeException; @@ -97,6 +96,11 @@ public abstract class PackageSynchronizer { */ public final static String HANDLEBARS_DIR = "handlebars"; + /** + * Name of the subdirectory containing (groovy) scripts + */ + public final static String SCRIPTS_DIR = "scripts"; + /** * Directory map for object class */ @@ -186,6 +190,16 @@ public PackageSynchronizer(Path packagePath, boolean registerWatchers) throws No // clean the cached handlebars helpers handlebarsHelpers = null; }); + // add a handler for changes in the scripts directory + pathHandlers.put(new File(packagePath.toFile(), SCRIPTS_DIR).toPath(), changedPath -> { + try { + for (Node node : getNodes()) { + Synchronizer.invalidateGroovyClassLoader(node); + } + } catch (NodeException e) { + Synchronizer.invalidateGroovyClassLoader(); + } + }); if (registerWatchers) { try { Synchronizer.registerAll(packagePath); @@ -516,11 +530,11 @@ public String getHandlebarsHelpers() throws IOException { if (helpersDirectory.isDirectory()) { StringBuilder registerHelpers = new StringBuilder(); - File[] files = helpersDirectory.listFiles((dir, name) -> StringUtils.endsWith(name, ".js")); + File[] files = helpersDirectory.listFiles((dir, name) -> Strings.CI.endsWith(name, ".js")); if (files != null) { for (File helperFile : files) { - String helperNameShort = StringUtils.removeEnd(helperFile.getName(), ".js"); + String helperNameShort = Strings.CI.removeEnd(helperFile.getName(), ".js"); String helperName = String.format("%s.%s", packageName, helperNameShort); String helperFileContents = FileUtils.readFileToString(helperFile, StandardCharsets.UTF_8); String register = String.format("Handlebars.registerHelper('%s', %s)", helperName, helperFileContents); @@ -547,6 +561,20 @@ public File getHandlebarsPartialsDirectory() { return new File(handlebarsDirectory, "partials"); } + /** + * Get the script files of the package + * @return script files + */ + public File[] getScriptFiles() { + File scriptsDirectory = new File(this.packagePath.toFile(), SCRIPTS_DIR); + + if (scriptsDirectory.isDirectory()) { + return scriptsDirectory.listFiles((dir, name) -> Strings.CI.endsWith(name, ".groovy")); + } + + return new File[0]; + } + /** * Add a synchronizer implementation * @param path base path diff --git a/cms-core/src/main/java/com/gentics/contentnode/devtools/Synchronizer.java b/cms-core/src/main/java/com/gentics/contentnode/devtools/Synchronizer.java index d400a59dd9..bafd545639 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/devtools/Synchronizer.java +++ b/cms-core/src/main/java/com/gentics/contentnode/devtools/Synchronizer.java @@ -20,7 +20,12 @@ import java.util.function.Function; import java.util.stream.Collectors; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.io.FileUtils; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.Phases; +import org.codehaus.groovy.tools.GroovyClass; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; @@ -36,7 +41,9 @@ import com.gentics.contentnode.etc.NodePreferences; import com.gentics.contentnode.etc.PrefixedThreadFactory; import com.gentics.contentnode.etc.QueueWithDelay; +import com.gentics.contentnode.etc.Timing; import com.gentics.contentnode.factory.Transaction; +import com.gentics.contentnode.i18n.I18NHelper; import com.gentics.contentnode.object.Construct; import com.gentics.contentnode.object.ContentRepository; import com.gentics.contentnode.object.Datasource; @@ -52,6 +59,8 @@ import com.gentics.lib.etc.StringUtils; import com.gentics.lib.log.NodeLogger; +import groovy.lang.GroovyClassLoader; + /** * Synchronization implementation for the {@link Feature#DEVTOOLS} */ @@ -142,6 +151,11 @@ public class Synchronizer { */ private static ObjectMapper alternateMapper; + /** + * Map of {@link GroovyClassLoader} instances per Node + */ + private static Map groovyClassLoaderPerNode = MapUtils.synchronizedMap(new HashMap<>()); + static { AtomicInteger priority = new AtomicInteger(); for (Class clazz : CLASSES) { @@ -470,6 +484,9 @@ public static void removePackage(Node node, String packageName) throws NodeExcep st.setInt(1, node.getId()); st.setString(2, packageName); }); + + // invalidate the groovy classloader for the node + invalidateGroovyClassLoader(node); } /** @@ -494,9 +511,62 @@ public static void addPackage(Node node, String packageName) throws NodeExceptio AbstractSynchronizer synchronizer = packageSynchronizer.synchronizersPerClass .get(clazz); synchronizer.assignAll(node); - } } + // invalidate the groovy classloader for the node + invalidateGroovyClassLoader(node); + } + + /** + * Get the {@link GroovyClassLoader} instance for the given Node. The class loader will contain all + * classes compiled from scripts found in packages, which are assigned to the node + * @param node + * @return + * @throws NodeException + */ + public static GroovyClassLoader getGroovyClassLoader(Node node) throws NodeException { + Set nodePackages = Synchronizer.getPackages(node); + + return groovyClassLoaderPerNode.computeIfAbsent(node, n -> { + CompilerConfiguration config = new CompilerConfiguration(); + + GroovyClassLoader gcl = new GroovyClassLoader(Synchronizer.class.getClassLoader(), config); + + try (Timing timing = Timing.get(-1, duration -> { + logger.info("Compiled scripts for node %s in %d ms".formatted(I18NHelper.getName(node), duration)); + })) { + CompilationUnit unit = new CompilationUnit(config, null, gcl); + for (String packageName : nodePackages) { + MainPackageSynchronizer mainPack = Synchronizer.getPackage(packageName); + unit.addSources(mainPack.getScriptFiles()); + } + unit.compile(Phases.CLASS_GENERATION); + + for (GroovyClass groovyClass : unit.getClasses()) { + gcl.defineClass(groovyClass.getName(), groovyClass.getBytes()); + } + } catch (NodeException ignored) { + } + + return gcl; + }); + } + + /** + * Invalidate the {@link GroovyClassLoader} for the give node + * @param node node + */ + public static void invalidateGroovyClassLoader(Node node) { + groovyClassLoaderPerNode.remove(node); + } + + /** + * Invalidate the {@link GroovyClassLoader} for all nodes + */ + public static void invalidateGroovyClassLoader() { + groovyClassLoaderPerNode.clear(); + } + /** * Get the package synchronizer for the given path or null * @param path path @@ -530,6 +600,7 @@ public static void removePackageSynchronizer(PackageContainer container, Path */ public static void clearCache() { container.clearCache(); + invalidateGroovyClassLoader(); } /** diff --git a/cms-core/src/main/java/com/gentics/contentnode/object/Part.java b/cms-core/src/main/java/com/gentics/contentnode/object/Part.java index ef03139901..a1d8f38a58 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/object/Part.java +++ b/cms-core/src/main/java/com/gentics/contentnode/object/Part.java @@ -70,7 +70,9 @@ public abstract class Part extends AbstractContentObject implements I18nNamedNod public static final int FILEUPLOAD = 38; public static final int FOLDERUPLOAD = 39; public static final int NODE = 40; - + public static final int HANDLEBARS = 43; + public static final int GROOVY = 45; + /** * The ttype of the part object. */ diff --git a/cms-core/src/main/java/com/gentics/contentnode/object/parttype/groovy/GroovyPartType.java b/cms-core/src/main/java/com/gentics/contentnode/object/parttype/groovy/GroovyPartType.java new file mode 100644 index 0000000000..e60b0bcc6e --- /dev/null +++ b/cms-core/src/main/java/com/gentics/contentnode/object/parttype/groovy/GroovyPartType.java @@ -0,0 +1,120 @@ +package com.gentics.contentnode.object.parttype.groovy; + +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.Phases; +import org.codehaus.groovy.tools.GroovyClass; + +import com.gentics.api.lib.exception.NodeException; +import com.gentics.contentnode.factory.Transaction; +import com.gentics.contentnode.factory.TransactionManager; +import com.gentics.contentnode.object.Construct; +import com.gentics.contentnode.object.Part; +import com.gentics.contentnode.object.Tag; +import com.gentics.contentnode.object.Value; +import com.gentics.contentnode.object.ValueContainer; +import com.gentics.contentnode.object.parttype.TextPartType; +import com.gentics.contentnode.render.RenderResult; +import com.gentics.contentnode.render.RenderType; +import com.gentics.contentnode.resolving.NodeObjectResolverContext; +import com.gentics.contentnode.resolving.ResolvableGetter; +import com.gentics.contentnode.rest.model.Property; +import com.gentics.contentnode.rest.model.Property.Type; +import com.gentics.contentnode.rest.util.MiscUtils; +import com.gentics.contentnode.utils.GroovyUtils; + +/** + * Implementation of the Groovy PartType + */ +public class GroovyPartType extends TextPartType { + + private static final long serialVersionUID = 416330914123548280L; + + /** + * Create an instance + * @param value value + * @throws NodeException + */ + public GroovyPartType(Value value) throws NodeException { + super(value); + } + + /** + * Execute the groovy script + * @return + * @throws NodeException + */ + @ResolvableGetter + public Object getExecute() throws NodeException { + String code = getText(); + + if (StringUtils.isBlank(code)) { + return null; + } + + Transaction t = TransactionManager.getCurrentTransaction(); + RenderType renderType = t.getRenderType(); + + Tag tag = NodeObjectResolverContext.getNodeObject(Tag.class); + + if (null != tag) { + renderType.push(tag); + } + try { + renderType.createCMSResolver(); + try { + CompilationUnit unit = GroovyUtils.getCurrentCompilationUnit(); + + Value value = getValueObject(); + String constructKeyword = Optional.ofNullable(value).map(v -> MiscUtils.execOrNull(Value::getContainer, v)) + .map(cont -> MiscUtils.execOrNull(ValueContainer::getConstruct, cont)).map(Construct::getKeyword) + .orElse(""); + String partKeyword = Optional.ofNullable(value).map(v -> MiscUtils.execOrNull(Value::getPart, v)) + .map(Part::getKeyname).orElse(""); + int valueId = Optional.ofNullable(value).map(Value::getId).orElse(0); + String scriptClassName = "%s_%s_%d".formatted(constructKeyword, partKeyword, valueId); + String scriptName = "%s.groovy".formatted(scriptClassName); + + // check whether the unit already contains the class + GroovyClass groovyClass = GroovyUtils.findGroovyClass(unit, scriptClassName); + + // class does not exist, so add it to the compilation unit and compile + if (groovyClass == null) { + unit.addSource(scriptName, code); + unit.compile(Phases.CLASS_GENERATION); + + // when compiled, add it to the class path + groovyClass = GroovyUtils.findGroovyClass(unit, scriptClassName); + if (groovyClass != null) { + unit.getClassLoader().defineClass(groovyClass.getName(), groovyClass.getBytes()); + } + } + + return GroovyUtils.call(unit.getClassLoader(), scriptClassName, script -> { + GroovyUtils.injectCmsResolver(script); + }); + } catch (CompilationFailedException e) { + throw new NodeException(e); + } finally { + renderType.popCMSResolver(); + } + } finally { + if (tag != null) { + renderType.pop(tag); + } + } + } + + @Override + public Type getPropertyType() { + return Property.Type.RICHTEXT; + } + + @Override + public String render(RenderResult result, String template) throws NodeException { + return ""; + } +} diff --git a/cms-core/src/main/java/com/gentics/contentnode/object/parttype/handlebars/HelperSource.java b/cms-core/src/main/java/com/gentics/contentnode/object/parttype/handlebars/HelperSource.java index 665bf029eb..9fb7a8c016 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/object/parttype/handlebars/HelperSource.java +++ b/cms-core/src/main/java/com/gentics/contentnode/object/parttype/handlebars/HelperSource.java @@ -12,6 +12,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.codehaus.groovy.control.CompilationUnit; import com.gentics.api.lib.datasource.Datasource; import com.gentics.api.lib.etc.ObjectTransformer; @@ -40,8 +42,10 @@ import com.gentics.contentnode.resolving.ResolvableMapWrappable; import com.gentics.contentnode.resolving.ResolvableMapWrapper; import com.gentics.contentnode.resolving.ResolvableMapWrapper.RenderContext; +import com.gentics.contentnode.utils.GroovyUtils; import com.gentics.lib.render.Renderable; import com.github.jknack.handlebars.Options; +import com.github.jknack.handlebars.helper.HelperFunction; /** * Source for helpers used when rendering a {@link HandlebarsPartType} @@ -153,7 +157,7 @@ public static Collection gtx_sort(Object objects, String sortBy, String } int iSortOrder = Datasource.SORTORDER_ASC; - if (StringUtils.equalsIgnoreCase(sortOrder, "desc")) { + if (Strings.CI.equals(sortOrder, "desc")) { iSortOrder = Datasource.SORTORDER_DESC; } @@ -228,4 +232,24 @@ public static CharSequence gtx_i18n(Object object, Options options) throws NodeE return null; } + + /** + * Script helper for calling groovy scripts (from devtool packages) + * @param name script name + * @param options additional options + * @return return value of the script + * @throws NodeException + */ + @HelperFunction("gtx_script") + public static Object callScript(String name, Options options) throws NodeException { + CompilationUnit unit = GroovyUtils.getCurrentCompilationUnit(); + + return GroovyUtils.call(unit.getClassLoader(), name, script -> { + GroovyUtils.injectCmsResolver(script); + + for (String key : options.hash.keySet()) { + script.setProperty(key, options.hash.get(key)); + } + }); + } } diff --git a/cms-core/src/main/java/com/gentics/contentnode/publish/Publisher.java b/cms-core/src/main/java/com/gentics/contentnode/publish/Publisher.java index a97fc764ad..4aee7b7f02 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/publish/Publisher.java +++ b/cms-core/src/main/java/com/gentics/contentnode/publish/Publisher.java @@ -51,6 +51,7 @@ import com.gentics.contentnode.factory.object.FileOnlineStatus; import com.gentics.contentnode.factory.object.FormFactory; import com.gentics.contentnode.factory.url.StaticUrlFactory; +import com.gentics.contentnode.i18n.I18NHelper; import com.gentics.contentnode.image.CNGenticsImageStore; import com.gentics.contentnode.image.CNGenticsImageStore.ImageInformation; import com.gentics.contentnode.jmx.MBeanRegistry; @@ -72,6 +73,7 @@ import com.gentics.contentnode.render.RenderType; import com.gentics.contentnode.runtime.ConfigurationValue; import com.gentics.contentnode.runtime.NodeConfigRuntimeConfiguration; +import com.gentics.contentnode.utils.GroovyUtils; import com.gentics.lib.content.FilesystemAttributeValue; import com.gentics.lib.datasource.mccr.MCCRHelper; import com.gentics.lib.db.SQLExecutor; @@ -84,6 +86,8 @@ import com.gentics.lib.log.profilerconstants.JavaParserConstants; import com.gentics.lib.render.exception.PublishException; +import groovy.lang.GroovyRuntimeException; + /** * Main publish process class. This implements and controls the publish process. * @@ -1029,6 +1033,20 @@ private Map initializeWorkPhases(List publishedNodes, Me if (renderResult != null) { renderResult.info(Publisher.class, "Marking changes about to be published in this run"); } + + // get the Scripts CompilationUnits for all published nodes. This will probably compile the scripts used for the nodes + // and will fail if not all scripts can be compiled + RenderType renderType = TransactionManager.getCurrentTransaction().getRenderType(); + for (Node node : publishedNodes) { + if (!node.isChannel()) { + try { + renderType.getCompilationUnit(node); + } catch (GroovyRuntimeException e) { + throw new NodeException("Error while compiling scripts for %s".formatted(I18NHelper.getName(node)), e); + } + } + } + objectsToPublishCount = PublishQueue.startPublishProcess(publishedNodes); // set the number of objects to publish into the JMX bean diff --git a/cms-core/src/main/java/com/gentics/contentnode/render/RenderType.java b/cms-core/src/main/java/com/gentics/contentnode/render/RenderType.java index f9cf9913dc..1dcab58669 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/render/RenderType.java +++ b/cms-core/src/main/java/com/gentics/contentnode/render/RenderType.java @@ -2,7 +2,6 @@ import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; @@ -15,10 +14,10 @@ import java.util.Stack; import java.util.Vector; -import com.gentics.contentnode.utils.ResourcePath; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.control.CompilerConfiguration; import com.gentics.api.lib.etc.ObjectTransformer; import com.gentics.api.lib.exception.NodeException; @@ -51,6 +50,7 @@ import com.gentics.contentnode.object.parttype.handlebars.HelperSource; import com.gentics.contentnode.resolving.StackResolvable; import com.gentics.contentnode.resolving.StackResolver; +import com.gentics.contentnode.utils.ResourcePath; import com.gentics.lib.genericexceptions.NotYetImplementedException; import com.gentics.lib.log.NodeLogger; import com.gentics.lib.log.RuntimeProfiler; @@ -61,6 +61,8 @@ import com.github.jknack.handlebars.helper.StringHelpers; import com.github.jknack.handlebars.io.TemplateLoader; +import groovy.lang.GroovyClassLoader; + /** * RenderType provides informations and settings about how code should be rendered. * The information is stored in a stack, which can be used to quickly change the @@ -179,6 +181,11 @@ public class RenderType implements RenderInfo { */ private Map handlebarsPerNode = new HashMap<>(); + /** + * Groovy Compilation Units per Node + */ + private Map compilationUnitsPerNode = new HashMap<>(); + /** * A public constructor, which provides the renderType with all required informations. * @param editMode the editmode to set to the root level. @@ -1507,6 +1514,34 @@ public Handlebars getHandlebars(Node node) throws NodeException, IOException { return handlebarsPerNode.get(node); } + /** + * Get the {@link CompilationUnit} instance for the given node. The unit will + * already have the scripts contained in all devtool packages, which are + * assigned to the node compiled and defined in its classloader + * + * @param node node + * @return unit + * @throws NodeException + */ + public CompilationUnit getCompilationUnit(Node node) throws NodeException { + if (!compilationUnitsPerNode.containsKey(node)) { + CompilerConfiguration config = new CompilerConfiguration(); + + ClassLoader baseClassLoader; + if (Synchronizer.getStatus() == Status.UP) { + baseClassLoader = Synchronizer.getGroovyClassLoader(node); + } else { + baseClassLoader = RenderType.class.getClassLoader(); + } + + GroovyClassLoader gcl = new GroovyClassLoader(baseClassLoader, config); + CompilationUnit unit = new CompilationUnit(config, null, gcl); + compilationUnitsPerNode.put(node, unit); + } + + return compilationUnitsPerNode.get(node); + } + /** * AutoCloseable instance that will set the parameter back to the original value when closed */ diff --git a/cms-core/src/main/java/com/gentics/contentnode/render/RenderUtils.java b/cms-core/src/main/java/com/gentics/contentnode/render/RenderUtils.java index b436e4206b..32bc7ddd8e 100644 --- a/cms-core/src/main/java/com/gentics/contentnode/render/RenderUtils.java +++ b/cms-core/src/main/java/com/gentics/contentnode/render/RenderUtils.java @@ -55,6 +55,7 @@ import com.gentics.contentnode.parser.tag.struct.ParseStructRenderer; import com.gentics.contentnode.publish.FilePublisher; import com.gentics.contentnode.publish.mesh.MeshPublisher; +import com.gentics.contentnode.resolving.ResolvableMapWrappable; import com.gentics.contentnode.resolving.ResolvableMapWrapper; import com.gentics.contentnode.rest.model.ContentRepositoryModel.Type; import com.gentics.contentnode.runtime.NodeConfigRuntimeConfiguration; @@ -366,6 +367,21 @@ public static T getObject(Object obje return null; } + /** + * Wrap the object for returning + * @param object object to wrap + * @return wrapped object + */ + public static Resolvable wrap(NodeObject object) { + if (object instanceof ResolvableMapWrappable wrappable) { + return new ResolvableMapWrapper(wrappable); + } else if (object != null) { + return new RenderableResolvable(object); + } else { + return null; + } + } + /** * Unwrap instances of {@link ResolvableMapWrapper} (recursively) * @param object object to unwrap diff --git a/cms-core/src/main/java/com/gentics/contentnode/utils/GroovyUtils.java b/cms-core/src/main/java/com/gentics/contentnode/utils/GroovyUtils.java new file mode 100644 index 0000000000..cf573b52d5 --- /dev/null +++ b/cms-core/src/main/java/com/gentics/contentnode/utils/GroovyUtils.java @@ -0,0 +1,98 @@ +package com.gentics.contentnode.utils; + +import org.apache.commons.lang3.Strings; +import org.codehaus.groovy.control.CompilationUnit; +import org.codehaus.groovy.tools.GroovyClass; + +import com.gentics.api.lib.etc.ObjectTransformer; +import com.gentics.api.lib.exception.NodeException; +import com.gentics.contentnode.etc.Consumer; +import com.gentics.contentnode.factory.TransactionManager; +import com.gentics.contentnode.object.Node; +import com.gentics.contentnode.object.parttype.CMSResolver; +import com.gentics.contentnode.render.RenderType; +import com.gentics.contentnode.resolving.ResolvableMapWrapper; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.Script; + +/** + * Utilities for calling groovy scripts + */ +public final class GroovyUtils { + /** + * Private constructor + */ + private GroovyUtils() { + } + + /** + * Load the class from the class loader and if it is a {@link Script}, create an instance, prepare it with the given handler and run it + * @param gcl groovy class loader + * @param className class name + * @param prepareScript consumer to prepare the script (add properties) + * @return script return value + * @throws NodeException + */ + public static Object call(GroovyClassLoader gcl, String className, Consumer