Skip to content

Commit

Permalink
Merge pull request #22 from swisscom/bugfix/21-javascript-scripting
Browse files Browse the repository at this point in the history
Bugfix/21 javascript scripting
  • Loading branch information
sabberworm authored Oct 31, 2024
2 parents 0469a87 + 7c42719 commit 1fb2a85
Show file tree
Hide file tree
Showing 17 changed files with 119 additions and 29 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The builder offers various methods to configure the runner:
- `#registerFile(String, Function<String, File>)`, `#registerFiles(Map<String, Function<String, File>>)`: Register file creators that allow scripts to aggregate information into output files.
- `#addDefaultUtils(boolean)`: if set to `true`, The script will know about the `arrays`, `stream`, `class`, and `collections` utility namespaces.
- `#runHandler(com.swisscom.aem.tools.jcrhopper.config.RunHandler)`: Set a listener that is informed about script output and events. The default run handler does nothing (but log messages are logged using Slf4j in any case).
- `#scriptEngineManager(javax.script.ScriptEngineManager)`: Set the script engine manager to use when searching for non-JEXL scripting engines.

Once the builder is configured, it can be turned into a runner with either `#build(Script)` or `#build(String)` (for JSON scripts).

Expand Down
1 change: 1 addition & 0 deletions mock/script-builder.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<div
class="jcr-hopper-builder"
data-run-endpoint="/mock/mock-response.jsonl"
data-valid-scripting-languages='{"js": "JavaScript", "jexl": "JEXL", "groovy": "Groovy"}'
></div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
<div data-sly-use.info="com.swisscom.aem.tools.jcrhopper.impl.HopRunnerInfo" class="jcr-hopper-builder" data-run-endpoint="${info.endpoint}"></div>
<div
data-sly-use.info="com.swisscom.aem.tools.jcrhopper.impl.HopRunnerInfo"
class="jcr-hopper-builder"
data-run-endpoint="${info.endpoint}"
data-valid-scripting-languages="${info.validScriptingLanguages}"
></div>
11 changes: 7 additions & 4 deletions src/main/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { HistoryUpdater, useHistoryImmutable } from './hooks/useHistoryImmutable
import { useOnce } from './hooks/useOnce';
import { ScriptEditor } from './sections/ScriptEditor';

export const RunEndpointContext = createContext('');
export const EnvironmentContext = createContext({
runEndpoint: '/',
validScriptingLanguages: {} as Record<string, string>,
});
export const ScriptContext = createContext<HistoryUpdater<Script>>(null!);

const RootElement = styled('div')`
Expand Down Expand Up @@ -78,22 +81,22 @@ const RootElement = styled('div')`
}
`;

export const App: FC<{ runEndpoint: string }> = props => {
export const App: FC<{ runEndpoint: string; validScriptingLanguages: Record<string, string> }> = props => {
const initialScript = useOnce(getInitialScript);

const scriptContext = useHistoryImmutable(initialScript, current => {
window.sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(current));
});

return (
<RunEndpointContext.Provider value={props.runEndpoint}>
<EnvironmentContext.Provider value={{ ...props }}>
<ScriptContext.Provider value={scriptContext}>
<RootElement>
<Toolbar />
<ScriptEditor />
<Runner />
</RootElement>
</ScriptContext.Provider>
</RunEndpointContext.Provider>
</EnvironmentContext.Provider>
);
};
4 changes: 2 additions & 2 deletions src/main/frontend/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ patchCoralUiCreateElement();

function init() {
const target = document.querySelector<HTMLElement>('.jcr-hopper-builder')!;
const { runEndpoint } = target.dataset;
const { runEndpoint, validScriptingLanguages } = target.dataset;
const root = createRoot(target.parentElement!.parentElement!);
root.render(
<React.StrictMode>
<App runEndpoint={runEndpoint!}></App>
<App runEndpoint={runEndpoint!} validScriptingLanguages={JSON.parse(validScriptingLanguages || '{}')}></App>
</React.StrictMode>,
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/frontend/model/hops/runScript.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { AnyHop } from '.';

export const SCRIPT_LANGUAGES = {
export const SCRIPT_LANGUAGES: Record<string, string> = {
jexl: 'JEXL',
js: 'JavaScript',
};

export interface Type extends AnyHop {
type: 'runScript';
code: string;
extension: keyof typeof SCRIPT_LANGUAGES;
extension: string;
putLocalsBackIntoScope?: boolean;
}

Expand All @@ -22,7 +22,7 @@ export const title = 'Run a Script';

export function shortDescription(config: Type) {
const lang = config.extension ?? 'js';
const name = SCRIPT_LANGUAGES[lang] ?? lang.toUpperCase();
const name = lang in SCRIPT_LANGUAGES ? SCRIPT_LANGUAGES[lang] : lang.toUpperCase();

const lines = config.code?.split(/\n/).filter(Boolean).length;
if (!lines) {
Expand Down
13 changes: 10 additions & 3 deletions src/main/frontend/sections/RunControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { FC, FormEvent, useContext, useRef } from 'react';

import { styled } from 'goober';
import { RunEndpointContext, ScriptContext } from '../App';
import { EnvironmentContext, ScriptContext } from '../App';

const Elm = styled('form', React.forwardRef)`
grid-auto-flow: row;
Expand All @@ -13,7 +13,7 @@ const Elm = styled('form', React.forwardRef)`

export const RunControls: FC<{ runWith: (data: FormData) => Promise<void> }> = ({ runWith }) => {
const { current: script } = useContext(ScriptContext);
const endpoint = useContext(RunEndpointContext);
const environmentContext = useContext(EnvironmentContext);

const formRef = useRef<HTMLFormElement>(null);

Expand All @@ -31,7 +31,14 @@ export const RunControls: FC<{ runWith: (data: FormData) => Promise<void> }> = (
}

return (
<Elm className="run-controls" ref={formRef} method="POST" action={endpoint} encType="multipart/form-data" onSubmit={run}>
<Elm
className="run-controls"
ref={formRef}
method="POST"
action={environmentContext.runEndpoint}
encType="multipart/form-data"
onSubmit={run}
>
{script.parameters.length ? (
<fieldset>
<legend>Arguments</legend>
Expand Down
6 changes: 3 additions & 3 deletions src/main/frontend/sections/Runner.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React, { FC, useContext, useState } from 'react';

import { RunEndpointContext } from '../App';
import { EnvironmentContext } from '../App';
import { Output } from './Output';
import { Run } from '../model/Run';
import { RunControls } from './RunControls';

export const Runner: FC = () => {
const endpoint = useContext(RunEndpointContext);
const environmentContext = useContext(EnvironmentContext);

const [runs, setRuns] = useState<Run[]>([]);

async function runWith(data: FormData) {
const response = fetch(endpoint, {
const response = fetch(environmentContext.runEndpoint, {
method: 'POST',
body: data,
});
Expand Down
25 changes: 14 additions & 11 deletions src/main/frontend/sections/editor/types/RunScriptStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,28 @@ import React, { forwardRef, useContext } from 'react';
import { Hop } from '../../../model/hops';
import { StepEditor } from '../../../widgets/StepEditor';

import { SCRIPT_LANGUAGES, shortDescription, title, Type, iconFor } from '../../../model/hops/runScript';
import { shortDescription, title, Type, iconFor } from '../../../model/hops/runScript';
import { Help } from '../../../widgets/Help';
import { Select } from '../../../widgets/Select';
import { CodeEditor } from '../../../widgets/CodeEditor';
import { ScriptContext } from '../../../App';
import { CodeEditor, EditorLanguage } from '../../../widgets/CodeEditor';
import { EnvironmentContext, ScriptContext } from '../../../App';
import { Switch } from '../../../widgets/Switch';

export const RunScriptStep = forwardRef<HTMLDivElement, { parentHops: Hop[]; hop: Type }>(function RunScriptStep({ parentHops, hop }, ref) {
const scriptContext = useContext(ScriptContext);
const { validScriptingLanguages } = useContext(EnvironmentContext);
const languageName = validScriptingLanguages[hop.extension];

return (
<StepEditor icon={iconFor(hop)} parentHops={parentHops} hop={hop} title={shortDescription(hop)} ref={ref}>
<Select
label="Language"
list={Object.entries(SCRIPT_LANGUAGES) as [keyof typeof SCRIPT_LANGUAGES, string][]}
list={Object.entries(validScriptingLanguages).map(([extension, name]) => [extension, `${name} (${extension})`])}
value={hop.extension}
onChange={extension => (hop.extension = extension)}
/>
<CodeEditor
language={hop.extension}
language={hop.extension as EditorLanguage}
lines={10}
value={hop.code}
onChange={code => {
Expand All @@ -36,9 +38,9 @@ export const RunScriptStep = forwardRef<HTMLDivElement, { parentHops: Hop[]; hop
onChange={putLocalsBackIntoScope => (hop.putLocalsBackIntoScope = putLocalsBackIntoScope)}
/>
<Help title={title}>
<h5>{SCRIPT_LANGUAGES[hop.extension]} Script Code</h5>
<h5>{languageName} Script Code</h5>
<p>
The {SCRIPT_LANGUAGES[hop.extension]} script code to run.
The {languageName} script code to run.
<br />
{hop.extension === 'jexl' ? (
<small>
Expand All @@ -47,14 +49,14 @@ export const RunScriptStep = forwardRef<HTMLDivElement, { parentHops: Hop[]; hop
Syntax Reference.
</a>
</small>
) : (
) : ['js', 'ecma'].includes(hop.extension) ? (
<small>
See the{' '}
<a href="https://www.oracle.com/technical-resources/articles/java/jf14-nashorn.html">
Nashorn Guide.
</a>
</small>
)}
) : undefined}
</p>
<p>The standard variables for expressions are available:</p>
<ul>
Expand Down Expand Up @@ -96,8 +98,9 @@ export const RunScriptStep = forwardRef<HTMLDivElement, { parentHops: Hop[]; hop
If this is set, all local variables your script creates will be available in subsequent hops.
<br />
<small>
Note: For JEXL scripts, that means variables not declared with `var` or `let` but implicitly created
by assignment without a previous declaration.
Note: For JEXL scripts, that means variables not declared with <code>var</code> or <code>let</code>{' '}
but implicitly created by assignment without a previous declaration. In JavaScript, <code>var</code>{' '}
creates such a variable while <code>const</code> and <code>let</code> do not.
</small>
</p>
</Help>
Expand Down
4 changes: 3 additions & 1 deletion src/main/frontend/widgets/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { useDebounce } from '@uidotdev/usehooks';
import { Editor } from '@monaco-editor/react';
import type * as monaco from 'monaco-editor';

export type EditorLanguage = 'json' | 'groovy' | 'jexl' | 'sql' | 'js';

export const CodeEditor: React.FC<{
value: string;
onChange(val: string, hasErrors: boolean): void;
language: 'json' | 'groovy' | 'jexl' | 'sql' | 'js';
language: EditorLanguage;
lines?: number;
}> = ({ value: outsideValue, onChange, language, lines = 13 }) => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/swisscom/aem/tools/impl/HopContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.script.ScriptEngineManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -123,6 +124,11 @@ public Map<String, Object> getVariables() {
return Collections.unmodifiableMap(variables);
}

@Override
public ScriptEngineManager getScriptEngineManager() {
return runner.getScriptEngineManager();
}

// region Logging functions
@Override
public String getName() {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/swisscom/aem/tools/impl/RunnerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.script.ScriptEngineManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.jexl3.JexlBuilder;
Expand All @@ -43,6 +44,9 @@ public class RunnerImpl implements Runner {
private final Map<String, Object> variables;
private final Map<String, Function<String, File>> fileTypeSuppliers;

@Getter
private final ScriptEngineManager scriptEngineManager;

@Getter
private final RunHandler runHandler;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.swisscom.aem.tools.impl.extension;

import com.swisscom.aem.tools.jcrhopper.RunnerBuilder;
import com.swisscom.aem.tools.jcrhopper.osgi.RunnerBuilderExtension;
import javax.script.ScriptEngineManager;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;

@Component(service = RunnerBuilderExtension.class)
public class ScriptEngineProviderExtension implements RunnerBuilderExtension {

@Reference(cardinality = ReferenceCardinality.MANDATORY)
private ScriptEngineManager scriptEngineManager;

@Override
public void configure(RunnerBuilder builder) {
builder.scriptEngineManager(scriptEngineManager);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private static void runJexl(Config config, HopContext context, Map<String, Objec

private static void runGenericScript(Config config, HopContext context, Map<String, Object> params, String extension, Writer writer)
throws HopperException {
final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
final ScriptEngineManager scriptEngineManager = context.getScriptEngineManager();
final ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension);
if (engine == null) {
throw new IllegalArgumentException("Script type " + extension + " not valid");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.script.ScriptEngineManager;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
Expand All @@ -43,6 +44,13 @@ public class RunnerBuilder {
private final Map<String, Object> variables = new HashMap<>();
private final Map<String, Function<String, File>> fileTypeSuppliers = new HashMap<>();

/**
* The script engine manager for running script types other than JEXL.
*/
@Setter
@Getter
private ScriptEngineManager scriptEngineManager = new ScriptEngineManager();

private final Gson gson;

/**
Expand Down Expand Up @@ -237,6 +245,7 @@ public Runner build(Script script) {
utils,
new HashMap<>(variables),
new HashMap<>(fileTypeSuppliers),
scriptEngineManager,
runHandler,
script
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.script.ScriptEngineManager;
import org.slf4j.Logger;

public interface HopContext extends Logger {
Expand Down Expand Up @@ -102,4 +103,9 @@ public interface HopContext extends Logger {
* @return a helper for JCR property manipulation
*/
JcrFunctions getJcrFunctions();

/**
* @return the script engine manager for running various script types
*/
ScriptEngineManager getScriptEngineManager();
}
Loading

0 comments on commit 1fb2a85

Please sign in to comment.