Skip to content

Commit 45c9854

Browse files
ronyflakevinrushforth
authored andcommitted
8238080: FXMLLoader: if script engines implement javax.script.Compilable compile scripts
Reviewed-by: kcr, aghaisas
1 parent 15f97ed commit 45c9854

29 files changed

+2551
-30
lines changed

build.gradle

+4-2
Original file line numberDiff line numberDiff line change
@@ -3578,6 +3578,7 @@ project(":systemTests") {
35783578
testapp5
35793579
testapp6
35803580
testscriptapp1
3581+
testscriptapp2
35813582
}
35823583

35833584
def nonModSrcSets = [
@@ -3591,7 +3592,8 @@ project(":systemTests") {
35913592
sourceSets.testapp4,
35923593
sourceSets.testapp5,
35933594
sourceSets.testapp6,
3594-
sourceSets.testscriptapp1
3595+
sourceSets.testscriptapp1,
3596+
sourceSets.testscriptapp2
35953597
]
35963598

35973599
project.ext.buildModule = false
@@ -3691,7 +3693,7 @@ project(":systemTests") {
36913693
}
36923694
test.dependsOn(createTestApps);
36933695

3694-
def modtestapps = [ "testapp2", "testapp3", "testapp4", "testapp5", "testapp6", "testscriptapp1" ]
3696+
def modtestapps = [ "testapp2", "testapp3", "testapp4", "testapp5", "testapp6", "testscriptapp1", "testscriptapp2" ]
36953697
modtestapps.each { testapp ->
36963698
def testappCapital = testapp.capitalize()
36973699
def copyTestAppTask = task("copy${testappCapital}", type: Copy) {

modules/javafx.fxml/src/main/docs/javafx/fxml/doc-files/introduction_to_fxml.html

+21-14
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
<html lang="en">
3131
<head>
32-
<link href="fxml.css" type="text/css" rel="stylesheet"/>
32+
<link href="fxml.css" type="text/css" rel="stylesheet"/>
3333
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
3434
<title>Introduction to FXML | JavaFX @FXVERSION@</title>
3535
<meta name="description" content="The document introduces FXML, an XML-based declarative markup language for defining user interfaces in JavaFX @FXVERSION@ applications."/>
@@ -597,13 +597,13 @@ <h4><a id="expression_binding">Expression Binding</a></h4>
597597
<tr><th scope="row">- <br/>(unary operator)</th><td>Unary minus operator, applied on a number</td>
598598
<tr><th scope="row">! <br/>(unary operator)</th><td>Unary negation of a boolean</td></tr>
599599
<tr><th scope="row">+ - <br />
600-
* /
601-
%</th> <td>Numerical binary operators</td></tr>
600+
* /
601+
%</th> <td>Numerical binary operators</td></tr>
602602
<tr><th scope="row">&amp;&amp; ||</th><td>Boolean binary operators</td></tr>
603603
<tr><th scope="row">&gt; &gt;= <br />
604-
&lt; &lt;= <br />
605-
== !=</th>
606-
<td>Binary operators of comparison.<br/> Both arguments must be of type Comparable</td></tr>
604+
&lt; &lt;= <br />
605+
== !=</th>
606+
<td>Binary operators of comparison.<br/> Both arguments must be of type Comparable</td></tr>
607607
</table>
608608

609609
<h3><a id="static_property_attributes">Static Properties</a></h3>
@@ -641,6 +641,8 @@ <h4><a id="script_event_handlers">Script Event Handlers</a></h4>
641641

642642
<p>Note the use of the language processing instruction at the beginning of the code snippet. This PI tells the FXML loader which scripting language should be used to execute the event handler. A page language must be specified whenever inline script is used in an FXML document, and can only be specified once per document. However, this does not apply to external scripts, which may be implemented using any number of supported scripting languages. Scripting is discussed in more detail in the next section.</p>
643643

644+
<p>Note: to turn off automatic compilation of script code place the processing instruction <span class="code">&lt;?compile false?&gt;</span> before the element that contains the script. To turn on compilation of script code again use the processing instruction <span class="code">&lt;?compile true?&gt;</span> (or short: <span class="code">&lt;?compile?&gt;</span>). The compile processing instruction can be used repeatedly to turn compilation of script code off and on.</p>
645+
644646
<h4><a id="controller_method_event_handlers">Controller Method Event Handlers</a></h4>
645647
<p>A controller method event handler is a method defined by a document's "controller". A controller is an object that is associated with the deserialized contents of an FXML document and is responsible for coordinating the behaviors of the objects (often user interface elements) defined by the document.</p>
646648

@@ -700,15 +702,15 @@ <h4><a id="expression_handlers">Event handlers from expressions</a></h4>
700702
</pre>
701703

702704
<p> With the controller that contains a field like this </p>
703-
705+
704706
<pre class="code">
705707
public class MyController {
706-
708+
707709
&#64;FXML
708710
public EventHandler&lt;ActionEvent&gt; onActionHandler = new EventHandler&lt;&gt;() { ... }
709711

710712
...
711-
}
713+
}
712714
</pre>
713715

714716
<p> Note that other kinds of expressions, like <a href="#expression_binding">binding expressions</a>
@@ -763,14 +765,18 @@ <h4><a id="collections_and_property_handlers">Special handlers for collections a
763765
&lt;VBox fx:controller="com.foo.MyController"
764766
xmlns:fx="http://javafx.com/fxml" onParentChange="#handleParentChange"/&gt;
765767
</pre>
766-
768+
767769
<p>Note that collections and properties do not currently support scripting handlers.</p>
768770

769771
<h2><a id="scripting">Scripting</a></h2>
770772
<p>
771773
The <span class="code">&lt;fx:script&gt;</span> tag allows a caller to import scripting code into or embed script within a FXML file. Any JVM scripting language can be used, including JavaScript, Groovy, and Clojure, among others. Script code is often used to define event handlers directly in markup or in an associated source file, since event handlers can often be written more concisely in more loosely-typed scripting languages than they can in a statically-typed language such as Java.</p>
772774

773-
<p>For example, the following markup defines a function called <span class="code">handleButtonAction()</span> that is called by the action handler attached to the <span class="code">Button</span> element:</p>
775+
<p>Scripts are compiled by default, when they are first loaded, if the <span class="code">ScriptEngine</span> implements the <span class="code">javax.script.Compilable</span> interface. If compilation fails, the <span class="code">FXMLLoader</span> will fall back to interpreted mode.</p>
776+
777+
<p>Note: to turn off automatic compilation of script code place the processing instruction <span class="code">&lt;?compile false?&gt;</span> before the script element. To turn on compilation of script code again use the processing instruction <span class="code">&lt;?compile true?&gt;</span> (or short: <span class="code">&lt;?compile?&gt;</span>). The compile processing instruction can be used repeatedly to turn compilation of script code off and on.</p>
778+
779+
<p>The following example markup defines a function called <span class="code">handleButtonAction()</span> that is called by the action handler attached to the <span class="code">Button</span> element:</p>
774780

775781
<pre class="code">
776782
&lt;?language javascript?&gt;
@@ -798,6 +804,8 @@ <h2><a id="scripting">Scripting</a></h2>
798804

799805
<div class="caption">example.fxml</div>
800806
<pre class="code">
807+
&lt;?language javascript?&gt;
808+
801809
&lt;?import javafx.scene.control.*?&gt;
802810
&lt;?import javafx.scene.layout.*?&gt;
803811

@@ -833,8 +841,7 @@ <h2><a id="scripting">Scripting</a></h2>
833841
&lt;Label text="$myText"/&gt;
834842
</pre>
835843

836-
837-
<p><strong>Warning:</strong>As of JavaFX 8, <span class="code">importClass()</span> javascript function is no longer supported. You have to use fully qualified names as in the example above or load a nashorn compatibility script.</p>
844+
<p><strong>Warning:</strong> As of JavaFX 8, <span class="code">importClass()</span> javascript function is no longer supported. You have to use fully qualified names as in the example above or load a nashorn compatibility script.</p>
838845

839846
<pre class="code">
840847
load("nashorn:mozilla_compat.js");
@@ -843,7 +850,7 @@ <h2><a id="scripting">Scripting</a></h2>
843850
function handleButtonAction(event) {
844851
System.out.println('You clicked me!');
845852
}
846-
</pre>
853+
</pre>
847854

848855
<h2><a id="controllers">Controllers</a></h2>
849856
<p>While it can be convenient to write simple event handlers in script, either inline or defined in external files, it is often preferable to define more complex application logic in a compiled, strongly-typed language such as Java. As discussed earlier, the <span class="code">fx:controller</span> attribute allows a caller to associate a "controller" class with an FXML document. A controller is a compiled class that implements the "code behind" the object hierarchy defined by the document.</p>

modules/javafx.fxml/src/main/java/javafx/fxml/FXMLLoader.java

+95-13
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
import javafx.util.Callback;
6464

6565
import javax.script.Bindings;
66+
import javax.script.Compilable;
67+
import javax.script.CompiledScript;
6668
import javax.script.ScriptContext;
6769
import javax.script.ScriptEngine;
6870
import javax.script.ScriptEngineManager;
@@ -1558,21 +1560,55 @@ public void processStartElement() throws IOException {
15581560
location = new URL(FXMLLoader.this.location, source);
15591561
}
15601562
Bindings engineBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
1561-
engineBindings.put(engine.FILENAME, location.getPath());
1563+
String filename = location.getPath();
1564+
engineBindings.put(engine.FILENAME, filename);
15621565

15631566
InputStreamReader scriptReader = null;
1567+
String script = null;
15641568
try {
15651569
scriptReader = new InputStreamReader(location.openStream(), charset);
1566-
engine.eval(scriptReader);
1567-
} catch(ScriptException exception) {
1568-
exception.printStackTrace();
1570+
StringBuilder sb = new StringBuilder();
1571+
final int bufSize = 4096;
1572+
char[] charBuffer = new char[bufSize];
1573+
int n;
1574+
do {
1575+
n = scriptReader.read(charBuffer,0,bufSize);
1576+
if (n > 0) {
1577+
sb.append(new String(charBuffer,0,n));
1578+
}
1579+
} while (n > -1);
1580+
script = sb.toString();
1581+
} catch (IOException exception) {
1582+
throw constructLoadException(exception);
15691583
} finally {
15701584
if (scriptReader != null) {
15711585
scriptReader.close();
15721586
}
15731587
}
1574-
} catch (IOException exception) {
1575-
throw constructLoadException(exception);
1588+
try {
1589+
if (engine instanceof Compilable && compileScript) {
1590+
CompiledScript compiledScript = null;
1591+
try {
1592+
compiledScript = ((Compilable) engine).compile(script);
1593+
} catch (ScriptException compileExc) {
1594+
Logging.getJavaFXLogger().warning(filename + ": compiling caused \"" + compileExc +
1595+
"\", falling back to evaluating script in uncompiled mode");
1596+
}
1597+
if (compiledScript != null) {
1598+
compiledScript.eval();
1599+
} else { // fallback to uncompiled mode
1600+
engine.eval(script);
1601+
}
1602+
} else {
1603+
engine.eval(script);
1604+
}
1605+
} catch (ScriptException exception) {
1606+
System.err.println(filename + ": caused ScriptException");
1607+
exception.printStackTrace();
1608+
}
1609+
}
1610+
catch (IOException exception) {
1611+
throw constructLoadException(exception);
15761612
}
15771613
}
15781614
}
@@ -1583,13 +1619,31 @@ public void processEndElement() throws IOException {
15831619

15841620
if (value != null && !staticLoad) {
15851621
// Evaluate the script
1622+
String filename = null;
15861623
try {
15871624
Bindings engineBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
1588-
engineBindings.put(scriptEngine.FILENAME, location.getPath() + "-script_starting_at_line_"
1589-
+ (getLineNumber() - (int) ((String) value).codePoints().filter(c -> c == '\n').count()));
1590-
scriptEngine.eval((String)value);
1625+
String script = (String) value;
1626+
filename = location.getPath() + "-script_starting_at_line_"
1627+
+ (getLineNumber() - (int) script.codePoints().filter(c -> c == '\n').count());
1628+
engineBindings.put(scriptEngine.FILENAME, filename);
1629+
if (scriptEngine instanceof Compilable && compileScript) {
1630+
CompiledScript compiledScript = null;
1631+
try {
1632+
compiledScript = ((Compilable) scriptEngine).compile(script);
1633+
} catch (ScriptException compileExc) {
1634+
Logging.getJavaFXLogger().warning(filename + ": compiling caused \"" + compileExc +
1635+
"\", falling back to evaluating script in uncompiled mode");
1636+
}
1637+
if (compiledScript != null) {
1638+
compiledScript.eval();
1639+
} else { // fallback to uncompiled mode
1640+
scriptEngine.eval(script);
1641+
}
1642+
} else {
1643+
scriptEngine.eval(script);
1644+
}
15911645
} catch (ScriptException exception) {
1592-
System.err.println(exception.getMessage());
1646+
System.err.println(filename + ": caused ScriptException\n" + exception.getMessage());
15931647
}
15941648
}
15951649
}
@@ -1681,11 +1735,24 @@ private static class ScriptEventHandler implements EventHandler<Event> {
16811735
public final String script;
16821736
public final ScriptEngine scriptEngine;
16831737
public final String filename;
1738+
public CompiledScript compiledScript;
1739+
public boolean isCompiled = false;
16841740

16851741
public ScriptEventHandler(String script, ScriptEngine scriptEngine, String filename) {
16861742
this.script = script;
16871743
this.scriptEngine = scriptEngine;
16881744
this.filename = filename;
1745+
if (scriptEngine instanceof Compilable && compileScript) {
1746+
try {
1747+
// supply the filename to the scriptEngine engine scope Bindings in case it is needed for compilation
1748+
scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE).put(scriptEngine.FILENAME, filename);
1749+
this.compiledScript = ((Compilable) scriptEngine).compile(script);
1750+
this.isCompiled = true;
1751+
} catch (ScriptException compileExc) {
1752+
Logging.getJavaFXLogger().warning(filename + ": compiling caused \"" + compileExc +
1753+
"\", falling back to evaluating script in uncompiled mode");
1754+
}
1755+
}
16891756
}
16901757

16911758
@Override
@@ -1699,9 +1766,13 @@ public void handle(Event event) {
16991766
localBindings.put(scriptEngine.FILENAME, filename);
17001767
// Execute the script
17011768
try {
1702-
scriptEngine.eval(script, localBindings);
1703-
} catch (ScriptException exception){
1704-
throw new RuntimeException(exception);
1769+
if (isCompiled) {
1770+
compiledScript.eval(localBindings);
1771+
} else {
1772+
scriptEngine.eval(script, localBindings);
1773+
}
1774+
} catch (ScriptException exception) {
1775+
throw new RuntimeException(filename + ": caused ScriptException", exception);
17051776
}
17061777
}
17071778
}
@@ -1819,6 +1890,7 @@ public void invoke(Object... params) {
18191890
private Element current = null;
18201891

18211892
private ScriptEngine scriptEngine = null;
1893+
private static boolean compileScript = true;
18221894

18231895
private List<String> packages = new LinkedList<String>();
18241896
private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
@@ -1845,6 +1917,12 @@ public void invoke(Object... params) {
18451917
*/
18461918
public static final String IMPORT_PROCESSING_INSTRUCTION = "import";
18471919

1920+
/**
1921+
* The tag name of the compile processing instruction.
1922+
* @since 15
1923+
*/
1924+
public static final String COMPILE_PROCESSING_INSTRUCTION = "compile";
1925+
18481926
/**
18491927
* Prefix of 'fx' namespace.
18501928
*/
@@ -2678,6 +2756,10 @@ private void processProcessingInstruction() throws LoadException {
26782756
processLanguage();
26792757
} else if (piTarget.equals(IMPORT_PROCESSING_INSTRUCTION)) {
26802758
processImport();
2759+
} else if (piTarget.equals(COMPILE_PROCESSING_INSTRUCTION)) {
2760+
String strCompile = xmlStreamReader.getPIData().trim();
2761+
// if PIData() is empty string then default to true, otherwise use Boolean.parseBoolean(string) to determine the boolean value
2762+
compileScript = (strCompile.length() == 0 ? true : Boolean.parseBoolean(strCompile));
26812763
}
26822764
}
26832765

tests/system/src/test/java/test/launchertest/ModuleLauncherTest.java

+24
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class ModuleLauncherTest {
4444
private static final String modulePath5 = System.getProperty("launchertest.testapp5.module.path");
4545
private static final String modulePath6 = System.getProperty("launchertest.testapp6.module.path");
4646
private static final String modulePathScript1 = System.getProperty("launchertest.testscriptapp1.module.path");
47+
private static final String modulePathScript2 = System.getProperty("launchertest.testscriptapp2.module.path");
4748

4849
private static final String moduleName = "mymod";
4950

@@ -281,4 +282,27 @@ public void testFXMLScriptDeployment() throws Exception {
281282
doTestLaunchModule(modulePathScript1, "myapp1.FXMLScriptDeployment");
282283
}
283284

285+
@Test (timeout = 15000)
286+
public void testFXMLScriptDeployment2Compile_On() throws Exception {
287+
doTestLaunchModule(modulePathScript2, "myapp2.FXMLScriptDeployment2Compile_On");
288+
}
289+
290+
@Test (timeout = 15000)
291+
public void testFXMLScriptDeployment2Compile_Off() throws Exception {
292+
doTestLaunchModule(modulePathScript2, "myapp2.FXMLScriptDeployment2Compile_Off");
293+
}
294+
295+
@Test (timeout = 15000)
296+
public void testFXMLScriptDeployment2Compile_On_Off() throws Exception {
297+
doTestLaunchModule(modulePathScript2, "myapp2.FXMLScriptDeployment2Compile_On_Off");
298+
}
299+
300+
@Test (timeout = 15000)
301+
public void testFXMLScriptDeployment2Compile_Off_On() throws Exception {
302+
doTestLaunchModule(modulePathScript2, "myapp2.FXMLScriptDeployment2Compile_Off_On");
303+
}
304+
@Test (timeout = 15000)
305+
public void testFXMLScriptDeployment2Compile_Fail_Compilation() throws Exception {
306+
doTestLaunchModule(modulePathScript2, "myapp2.FXMLScriptDeployment2Compile_Fail_Compilation");
307+
}
284308
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
pseudoScriptEngine.RgfPseudoScriptEngineFactory
2-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
26+
module mymod {
27+
requires javafx.controls;
28+
requires javafx.fxml;
29+
30+
requires java.scripting;
31+
provides javax.script.ScriptEngineFactory with pseudoScriptEngineCompilable.RgfPseudoScriptEngineCompilableFactory;
32+
exports pseudoScriptEngineCompilable;
33+
exports myapp2;
34+
}

0 commit comments

Comments
 (0)