Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .chachalog/js-server-extension-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
# Allowed version bumps: patch, minor, major
javascript-modules: minor
---

Java modules can now consume JavaScript-declared server extensions through the new `JSServerExtensionInvoker` OSGi service. A module can define its own extension type, let JavaScript modules contribute entries via `server.registry.add`, and invoke their callbacks from Java without depending on GraalVM APIs. This enables, for example, form-field validators written in JavaScript to run during server-side form submission processing.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2002-2023 Jahia Solutions Group SA. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jahia.modules.javascript.modules.engine.sdk;

import java.util.List;
import java.util.Map;

/**
* Public SDK entry point letting <strong>other OSGi bundles</strong> consume JavaScript-declared server
* extensions registered via {@code server.registry.add(type, key, entry)} in JS modules — without any
* dependency on GraalVM/polyglot types or on the engine internals.
*
* <p>This is the supported extension surface for modules (such as Formidable) that define their own
* server-side extension type and need to run the JS callbacks contributed against it. It complements the
* built-in registrars ({@code node-validator}, {@code action}, …), which wire JS entries to Jahia's own
* extension points; here the <em>consumer</em> owns the extension point.
*
* <p>All work happens inside a single pooled GraalVM context for the duration of one {@link #forEach}
* call. GraalVM values (the JS callables stored in entries) are only valid during that call, so callables
* must be invoked through the {@link Invoker} passed to the handler, never captured for later use.
*/
public interface JSServerExtensionInvoker {

/**
* Iterates all registry entries of {@code registryType} (across every deployed JS module) within one
* JS context, invoking {@code handler} for each. Results that are non-null are collected and returned
* in registry order.
*
* <p>The handler receives the entry as a plain {@code Map<String,Object>} (scalar fields such as
* {@code nodeType} are plain Java; function fields are opaque handles to pass to the {@link Invoker}).
* A handler exception propagates to the caller — callers that need fail-closed semantics should catch
* their own errors inside the handler and translate them into a result.
*
* @param registryType the JS registry type to look up (e.g. {@code "formidable-field-validator"})
* @param handler invoked once per matching entry; return {@code null} to skip an entry
* @param <T> the result type accumulated across entries
* @return the non-null handler results, in registry order (never {@code null})
*/
<T> List<T> forEach(String registryType, ExtensionHandler<T> handler);

/** Handles a single registry entry, optionally invoking its JS callables through {@code invoker}. */
@FunctionalInterface
interface ExtensionHandler<T> {
T handle(Map<String, Object> entry, Invoker invoker);
}

/** Invokes a JS callable stored in a registry entry and converts its result to plain Java. */
@FunctionalInterface
interface Invoker {
/**
* Executes {@code callable} (a function field read from an entry) with {@code args} and returns
* the result converted to plain Java: {@code null}, {@link Boolean}, {@link Long}/{@link Double},
* {@link String}, {@link List}, or {@link Map}. Host objects passed as arguments (e.g. a
* {@code JCRNodeWrapper}) are forwarded to JS as-is.
*
* @throws RuntimeException if the callable is not executable or throws
*/
Object call(Object callable, Object... args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2002-2023 Jahia Solutions Group SA. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jahia.modules.javascript.modules.engine.sdk;

import org.graalvm.polyglot.Value;
import org.jahia.modules.javascript.modules.engine.jsengine.GraalVMEngine;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Default {@link JSServerExtensionInvoker}. Runs each {@link #forEach} within one pooled GraalVM context
* (re-resolving the registry inside the context, as GraalVM contexts are recycled on module (un)deploy),
* and converts GraalVM {@link Value} results to plain Java so callers never see polyglot types.
*/
@Component(service = JSServerExtensionInvoker.class, immediate = true)
public class JSServerExtensionInvokerImpl implements JSServerExtensionInvoker {

private GraalVMEngine graalVMEngine;

@Reference(cardinality = ReferenceCardinality.MANDATORY)
public void setGraalVMEngine(GraalVMEngine graalVMEngine) {
this.graalVMEngine = graalVMEngine;
}

@Override
public <T> List<T> forEach(String registryType, ExtensionHandler<T> handler) {
return graalVMEngine.doWithContext(contextProvider -> {
List<T> results = new ArrayList<>();
Invoker invoker = (callable, args) -> convert(Value.asValue(callable).execute(args));
Map<String, Object> filter = new HashMap<>();
filter.put("type", registryType);
for (Map<String, Object> entry : contextProvider.getRegistry().find(filter)) {
T result = handler.handle(entry, invoker);
if (result != null) {
results.add(result);
}
}
return results;
});
}

/** Recursively converts a GraalVM value to plain Java ({@code null}/Boolean/Long/Double/String/List/Map). */
static Object convert(Value value) {
if (value == null || value.isNull()) {
return null;
}
if (value.isBoolean()) {
return value.asBoolean();
}
if (value.isNumber()) {
return value.fitsInLong() ? (Object) value.asLong() : (Object) value.asDouble();
}
if (value.isString()) {
return value.asString();
}
if (value.hasArrayElements()) {
List<Object> list = new ArrayList<>((int) value.getArraySize());
for (long i = 0; i < value.getArraySize(); i++) {
list.add(convert(value.getArrayElement(i)));
}
return list;
}
if (value.hasMembers()) {
Map<String, Object> map = new LinkedHashMap<>();
for (String key : value.getMemberKeys()) {
map.put(key, convert(value.getMember(key)));
}
return map;
}
return value.toString();
}
}
4 changes: 4 additions & 0 deletions javascript-modules-engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
<_dsannotations>*</_dsannotations>
<!-- those OSGI dependencies are not provided by Jahia and should be embedded in the bundle -->
<Embed-Dependency>bndlib,chromeinspector,commons-pool2,pax-swissbox-bnd,graal-sdk,truffle-api,js,icu4j,regex</Embed-Dependency>
<!-- Public SDK surface for other bundles that define their own JS server extension type
(e.g. Formidable's form-field validators). Only this package is exported: it exposes
no GraalVM/polyglot or engine-internal types, so consumers stay decoupled. -->
<Export-Package>org.jahia.modules.javascript.modules.engine.sdk;version="${project.version}"</Export-Package>
</instructions>
<!-- because the Java classes of javascript-modules-engine-java are unpacked into the target/classes folder, -->
<!-- the dependency can and should be excluded to avoid duplicates -->
Expand Down
Loading