• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

23.53
today-context/src/main/java/infra/scripting/support/StandardScriptFactory.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.scripting.support;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.io.IOException;
23
import java.lang.reflect.InaccessibleObjectException;
24
import java.lang.reflect.InvocationTargetException;
25

26
import javax.script.Invocable;
27
import javax.script.ScriptEngine;
28
import javax.script.ScriptEngineManager;
29

30
import infra.beans.factory.BeanClassLoaderAware;
31
import infra.core.ConstructorNotFoundException;
32
import infra.lang.Assert;
33
import infra.scripting.ScriptCompilationException;
34
import infra.scripting.ScriptFactory;
35
import infra.scripting.ScriptSource;
36
import infra.util.ClassUtils;
37
import infra.util.ObjectUtils;
38
import infra.util.ReflectionUtils;
39
import infra.util.StringUtils;
40

41
/**
42
 * {@link ScriptFactory} implementation based
43
 * on the JSR-223 script engine abstraction (as included in Java 6+).
44
 * Supports JavaScript, Groovy, JRuby, and other JSR-223 compliant engines.
45
 *
46
 * <p>Typically used in combination with a
47
 * {@link ScriptFactoryPostProcessor};
48
 * see the latter's javadoc for a configuration example.
49
 *
50
 * @author Juergen Hoeller
51
 * @author <a href="https://github.com/TAKETODAY">海子 Yang</a>
52
 * @see ScriptFactoryPostProcessor
53
 * @since 4.0
54
 */
55
public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAware {
56

57
  @Nullable
58
  private final String scriptEngineName;
59

60
  private final String scriptSourceLocator;
61

62
  private final Class<?> @Nullable [] scriptInterfaces;
63

64
  @Nullable
1✔
65
  private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
2✔
66

67
  @Nullable
68
  private volatile ScriptEngine scriptEngine;
69

70
  /**
71
   * Create a new StandardScriptFactory for the given script source.
72
   *
73
   * @param scriptSourceLocator a locator that points to the source of the script.
74
   * Interpreted by the post-processor that actually creates the script.
75
   */
76
  public StandardScriptFactory(String scriptSourceLocator) {
77
    this(null, scriptSourceLocator, (Class<?>[]) null);
6✔
78
  }
1✔
79

80
  /**
81
   * Create a new StandardScriptFactory for the given script source.
82
   *
83
   * @param scriptSourceLocator a locator that points to the source of the script.
84
   * Interpreted by the post-processor that actually creates the script.
85
   * @param scriptInterfaces the Java interfaces that the scripted object
86
   * is supposed to implement
87
   */
88
  public StandardScriptFactory(String scriptSourceLocator, Class<?>... scriptInterfaces) {
89
    this(null, scriptSourceLocator, scriptInterfaces);
5✔
90
  }
1✔
91

92
  /**
93
   * Create a new StandardScriptFactory for the given script source.
94
   *
95
   * @param scriptEngineName the name of the JSR-223 ScriptEngine to use
96
   * (explicitly given instead of inferred from the script source)
97
   * @param scriptSourceLocator a locator that points to the source of the script.
98
   * Interpreted by the post-processor that actually creates the script.
99
   */
100
  public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator) {
101
    this(scriptEngineName, scriptSourceLocator, (Class<?>[]) null);
6✔
102
  }
1✔
103

104
  /**
105
   * Create a new StandardScriptFactory for the given script source.
106
   *
107
   * @param scriptEngineName the name of the JSR-223 ScriptEngine to use
108
   * (explicitly given instead of inferred from the script source)
109
   * @param scriptSourceLocator a locator that points to the source of the script.
110
   * Interpreted by the post-processor that actually creates the script.
111
   * @param scriptInterfaces the Java interfaces that the scripted object
112
   * is supposed to implement
113
   */
114
  public StandardScriptFactory(@Nullable String scriptEngineName, String scriptSourceLocator, Class<?> @Nullable ... scriptInterfaces) {
2✔
115
    Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
3✔
116
    this.scriptEngineName = scriptEngineName;
3✔
117
    this.scriptSourceLocator = scriptSourceLocator;
3✔
118
    this.scriptInterfaces = scriptInterfaces;
3✔
119
  }
1✔
120

121
  @Override
122
  public void setBeanClassLoader(ClassLoader classLoader) {
123
    this.beanClassLoader = classLoader;
×
124
  }
×
125

126
  @Override
127
  public String getScriptSourceLocator() {
128
    return this.scriptSourceLocator;
3✔
129
  }
130

131
  @Override
132
  public Class<?> @Nullable [] getScriptInterfaces() {
133
    return this.scriptInterfaces;
3✔
134
  }
135

136
  @Override
137
  public boolean requiresConfigInterface() {
138
    return false;
2✔
139
  }
140

141
  /**
142
   * Load and parse the script via JSR-223's ScriptEngine.
143
   */
144
  @Override
145
  @Nullable
146
  public Object getScriptedObject(ScriptSource scriptSource, Class<?> @Nullable ... actualInterfaces)
147
          throws IOException, ScriptCompilationException {
148

149
    Object script = evaluateScript(scriptSource);
×
150

151
    if (ObjectUtils.isNotEmpty(actualInterfaces)) {
×
152
      boolean adaptationRequired = false;
×
153
      for (Class<?> requestedIfc : actualInterfaces) {
×
154
        if (script instanceof Class ? !requestedIfc.isAssignableFrom((Class<?>) script) :
×
155
                !requestedIfc.isInstance(script)) {
×
156
          adaptationRequired = true;
×
157
          break;
×
158
        }
159
      }
160
      if (adaptationRequired) {
×
161
        script = adaptToInterfaces(script, scriptSource, actualInterfaces);
×
162
      }
163
    }
164

165
    if (script instanceof Class<?> scriptClass) {
×
166
      try {
167
        return ReflectionUtils.accessibleConstructor(scriptClass).newInstance();
×
168
      }
169
      catch (ConstructorNotFoundException ex) {
×
170
        throw new ScriptCompilationException(
×
171
                "No default constructor on script class: " + scriptClass.getName(), ex);
×
172
      }
173
      catch (InstantiationException ex) {
×
174
        throw new ScriptCompilationException(
×
175
                scriptSource, "Unable to instantiate script class: " + scriptClass.getName(), ex);
×
176
      }
177
      catch (IllegalAccessException | InaccessibleObjectException ex) {
×
178
        throw new ScriptCompilationException(
×
179
                scriptSource, "Could not access script constructor: " + scriptClass.getName(), ex);
×
180
      }
181
      catch (InvocationTargetException ex) {
×
182
        throw new ScriptCompilationException(
×
183
                "Failed to invoke script constructor: " + scriptClass.getName(), ex.getTargetException());
×
184
      }
185
    }
186

187
    return script;
×
188
  }
189

190
  protected Object evaluateScript(ScriptSource scriptSource) {
191
    try {
192
      ScriptEngine scriptEngine = this.scriptEngine;
3✔
193
      if (scriptEngine == null) {
2!
194
        scriptEngine = retrieveScriptEngine(scriptSource);
×
195
        if (scriptEngine == null) {
×
196
          throw new IllegalStateException("Could not determine script engine for " + scriptSource);
×
197
        }
198
        this.scriptEngine = scriptEngine;
×
199
      }
200
      return scriptEngine.eval(scriptSource.getScriptAsString());
×
201
    }
202
    catch (Exception ex) {
1✔
203
      throw new ScriptCompilationException(scriptSource, ex);
6✔
204
    }
205
  }
206

207
  @Nullable
208
  protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) {
209
    ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader);
6✔
210

211
    if (this.scriptEngineName != null) {
3!
212
      return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.scriptEngineName);
×
213
    }
214

215
    if (scriptSource instanceof ResourceScriptSource) {
×
216
      String filename = ((ResourceScriptSource) scriptSource).getResource().getName();
×
217
      if (filename != null) {
×
218
        String extension = StringUtils.getFilenameExtension(filename);
×
219
        if (extension != null) {
×
220
          return scriptEngineManager.getEngineByExtension(extension);
×
221
        }
222
      }
223
    }
224

225
    return null;
×
226
  }
227

228
  @Nullable
229
  protected Object adaptToInterfaces(@Nullable Object script, ScriptSource scriptSource, Class<?>... actualInterfaces) {
230
    Class<?> adaptedIfc;
231
    if (actualInterfaces.length == 1) {
×
232
      adaptedIfc = actualInterfaces[0];
×
233
    }
234
    else {
235
      adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader);
×
236
    }
237

238
    if (adaptedIfc != null) {
×
239
      ScriptEngine scriptEngine = this.scriptEngine;
×
240
      if (!(scriptEngine instanceof Invocable invocable)) {
×
241
        throw new ScriptCompilationException(scriptSource,
×
242
                "ScriptEngine must implement Invocable in order to adapt it to an interface: " + scriptEngine);
243
      }
244
      if (script != null) {
×
245
        script = invocable.getInterface(script, adaptedIfc);
×
246
      }
247
      if (script == null) {
×
248
        script = invocable.getInterface(adaptedIfc);
×
249
        if (script == null) {
×
250
          throw new ScriptCompilationException(scriptSource,
×
251
                  "Could not adapt script to interface [%s]".formatted(adaptedIfc.getName()));
×
252
        }
253
      }
254
    }
255

256
    return script;
×
257
  }
258

259
  @Override
260
  @Nullable
261
  public Class<?> getScriptedObjectType(ScriptSource scriptSource)
262
          throws IOException, ScriptCompilationException {
263

264
    return null;
2✔
265
  }
266

267
  @Override
268
  public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
269
    return scriptSource.isModified();
3✔
270
  }
271

272
  @Override
273
  public String toString() {
274
    return "StandardScriptFactory: script source locator [%s]".formatted(this.scriptSourceLocator);
10✔
275
  }
276

277
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc