Skip to content

Commit 5ce9f65

Browse files
authored
feat: Add JRuby controller skeleton generation (gluonhq#596)
1 parent 449d8e3 commit 5ce9f65

File tree

11 files changed

+526
-7
lines changed

11 files changed

+526
-7
lines changed

kit/src/main/java/com/oracle/javafx/scenebuilder/kit/skeleton/AbstractSkeletonCreator.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, Gluon and/or its affiliates.
2+
* Copyright (c) 2021, 2023, Gluon and/or its affiliates.
33
* All rights reserved. Use is subject to license terms.
44
*
55
* This file is available and licensed under the following license:
@@ -38,13 +38,13 @@
3838
import java.util.Map;
3939
import java.util.ResourceBundle;
4040

41-
abstract class AbstractSkeletonCreator {
41+
abstract class AbstractSkeletonCreator implements SkeletonConverter {
4242

4343
static final String NL = System.lineSeparator();
4444
static final String INDENT = " "; //NOI18N
4545
static final String FXML_ANNOTATION = "@FXML";
4646

47-
String createFrom(SkeletonContext context) {
47+
public String createFrom(SkeletonContext context) {
4848
final StringBuilder sb = new StringBuilder();
4949

5050
appendHeaderComment(context, sb);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2023, Gluon and/or its affiliates.
3+
* All rights reserved. Use is subject to license terms.
4+
*
5+
* This file is available and licensed under the following license:
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions
9+
* are met:
10+
*
11+
* - Redistributions of source code must retain the above copyright
12+
* notice, this list of conditions and the following disclaimer.
13+
* - Redistributions in binary form must reproduce the above copyright
14+
* notice, this list of conditions and the following disclaimer in
15+
* the documentation and/or other materials provided with the distribution.
16+
* - Neither the name of Oracle Corporation nor the names of its
17+
* contributors may be used to endorse or promote products derived
18+
* from this software without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package com.oracle.javafx.scenebuilder.kit.skeleton;
33+
34+
@FunctionalInterface
35+
public interface SkeletonConverter {
36+
String createFrom(SkeletonContext context);
37+
}

kit/src/main/java/com/oracle/javafx/scenebuilder/kit/skeleton/SkeletonCreator.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, Gluon and/or its affiliates.
2+
* Copyright (c) 2021, 2023, Gluon and/or its affiliates.
33
* All rights reserved. Use is subject to license terms.
44
*
55
* This file is available and licensed under the following license:
@@ -35,6 +35,7 @@ class SkeletonCreator {
3535

3636
private final SkeletonCreatorJava skeletonCreatorJava = new SkeletonCreatorJava();
3737
private final SkeletonCreatorKotlin skeletonCreatorKotlin = new SkeletonCreatorKotlin();
38+
private final SkeletonCreatorJRuby skeletonCreatorJRuby = new SkeletonCreatorJRuby();
3839

3940
/**
4041
* @return a code skeleton for the given context
@@ -45,6 +46,8 @@ String createFrom(SkeletonContext context) {
4546
return skeletonCreatorJava.createFrom(context);
4647
case KOTLIN:
4748
return skeletonCreatorKotlin.createFrom(context);
49+
case JRUBY:
50+
return skeletonCreatorJRuby.createFrom(context);
4851
default:
4952
throw new IllegalArgumentException("Language not supported: " + context.getSettings().getLanguage());
5053
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* Copyright (c) 2023, Gluon and/or its affiliates.
3+
* All rights reserved. Use is subject to license terms.
4+
*
5+
* This file is available and licensed under the following license:
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions
9+
* are met:
10+
*
11+
* - Redistributions of source code must retain the above copyright
12+
* notice, this list of conditions and the following disclaimer.
13+
* - Redistributions in binary form must reproduce the above copyright
14+
* notice, this list of conditions and the following disclaimer in
15+
* the documentation and/or other materials provided with the distribution.
16+
* - Neither the name of Oracle Corporation nor the names of its
17+
* contributors may be used to endorse or promote products derived
18+
* from this software without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
package com.oracle.javafx.scenebuilder.kit.skeleton;
33+
34+
import com.oracle.javafx.scenebuilder.kit.i18n.I18N;
35+
36+
import java.lang.reflect.TypeVariable;
37+
import java.net.URL;
38+
import java.util.Map;
39+
import java.util.ResourceBundle;
40+
import java.util.regex.Matcher;
41+
import java.util.regex.Pattern;
42+
43+
public class SkeletonCreatorJRuby implements SkeletonConverter {
44+
45+
static final String NL = System.lineSeparator();
46+
static final String INDENT = " "; //NOI18N
47+
48+
public String createFrom(SkeletonContext context) {
49+
50+
final StringBuilder sb = new StringBuilder();
51+
appendHeaderComment(context, sb);
52+
// Ruby supports packages... but we ignore it here because Java package != Ruby Module and
53+
// equating the two can cause confusion. Let the user fix it up themselves
54+
appendClass(context, sb);
55+
56+
return sb.toString();
57+
}
58+
59+
public String createApplicationFrom(SkeletonContext context) {
60+
61+
return "require 'jrubyfx'\n" +
62+
"\n" +
63+
createFrom(context) +
64+
"\n" +
65+
"fxml_root File.dirname(__FILE__) # or wherever you save the fxml file to\n" +
66+
"\n" +
67+
"class " + makeClassName(context) + "Application < JRubyFX::Application\n" +
68+
" def start(stage)\n" +
69+
" " + makeClassName(context) + ".load_into(stage)\n" +
70+
" #stage.title = \"" + makeClassName(context) + "\"\n" +
71+
" stage.show\n" +
72+
" end\n" +
73+
" launch\n" +
74+
"end\n";
75+
}
76+
77+
static Pattern importExtractor = Pattern.compile("import (([^.]+)\\..*)");
78+
79+
void appendImports(SkeletonContext context, StringBuilder sb) {
80+
boolean output = false;
81+
// Optional, really, as JRubyFX imports them by default
82+
// Only "import" non-javafx ones in a comment
83+
for (String importStatement : context.getImports()) {
84+
Matcher matcher = importExtractor.matcher(importStatement);
85+
matcher.matches();
86+
String rootName = matcher.group(2);
87+
if (rootName.equals("javafx"))
88+
continue; // JRubyFX already imports these
89+
sb.append(INDENT).append("# java_import '").append(matcher.group(1)).append("'").append(NL);
90+
output = true;
91+
}
92+
if (output)
93+
sb.append(NL);
94+
}
95+
96+
97+
void appendHeaderComment(SkeletonContext context, StringBuilder sb) {
98+
if (!context.getSettings().isWithComments()) {
99+
return;
100+
}
101+
102+
final String title = I18N.getString("skeleton.window.title", context.getDocumentName());
103+
sb.append("# ").append(title).append(NL); //NOI18N
104+
}
105+
106+
107+
void appendClass(SkeletonContext context, StringBuilder sb) {
108+
109+
String controllerClassName = makeClassName(context);
110+
111+
sb.append("class ").append(controllerClassName); //NOI18N
112+
113+
sb.append(NL);
114+
sb.append(INDENT).append("include JRubyFX::Controller").append(NL).append(NL); //NOI18N
115+
116+
appendImports(context, sb);
117+
118+
if (context.getSettings().isWithComments()) {
119+
sb.append(INDENT).append("# Marks this class as being a controller for the given fxml document").append(NL); //NOI18N
120+
sb.append(INDENT).append("# This creates @instance_variables for all fx:id").append(NL); //NOI18N
121+
}
122+
String documentName = context.getDocumentName();
123+
if (!documentName.contains(".fxml")) {
124+
documentName += ".fxml";
125+
}
126+
sb.append(INDENT).append("fxml '").append(documentName).append("'").append(NL).append(NL); //NOI18N
127+
128+
129+
if (context.getSettings().isWithComments()) {
130+
sb.append(INDENT).append("# These @instance_variables will be injected by FXMLLoader & JRubyFX").append(NL); //NOI18N
131+
}
132+
133+
appendFieldsWithFxId(context, sb);
134+
135+
appendFieldsResourcesAndLocation(context, sb);
136+
137+
appendInitialize(context, sb);
138+
139+
appendEventHandlers(context, sb);
140+
141+
sb.append("end").append(NL); //NOI18N
142+
}
143+
144+
private String makeClassName(SkeletonContext context) {
145+
String controllerClassName = "PleaseProvideControllerClassName";
146+
147+
if (hasController(context)) {
148+
controllerClassName = getControllerClassName(context);
149+
}
150+
return controllerClassName;
151+
}
152+
153+
154+
private boolean hasController(SkeletonContext context) {
155+
return context.getFxController() != null && !context.getFxController().isEmpty();
156+
}
157+
158+
private String getControllerClassName(SkeletonContext context) {
159+
String simpleName = context.getFxController().replace("$", "."); //NOI18N
160+
int dot = simpleName.lastIndexOf('.');
161+
if (dot > -1) {
162+
simpleName = simpleName.substring(dot + 1);
163+
}
164+
return simpleName;
165+
}
166+
167+
void appendFieldParameters(StringBuilder sb, Class<?> fieldClazz) {
168+
final TypeVariable<? extends Class<?>>[] parameters = fieldClazz.getTypeParameters();
169+
if (parameters.length > 0) {
170+
sb.append("<"); //NOI18N
171+
String sep = ""; //NOI18N
172+
for (TypeVariable<?> ignored : parameters) {
173+
sb.append(sep);
174+
sb.append("?"); //NOI18N
175+
sep = ", "; //NOI18N
176+
}
177+
sb.append(">"); //NOI18N
178+
}
179+
}
180+
181+
182+
void appendFieldsResourcesAndLocation(SkeletonContext context, StringBuilder sb) {
183+
if (!context.getSettings().isFull()) {
184+
return;
185+
}
186+
187+
// these aren't built into JRubyFX's fxml_helper.rb, so just manually add the fields for reification
188+
if (context.getSettings().isWithComments()) {
189+
sb.append(INDENT).append("# ResourceBundle that was given to the FXMLLoader. Access as self.resources, or @resources if instance_variable is true").append(NL); //NOI18N
190+
}
191+
sb.append(INDENT);
192+
sb.append("java_field '@javafx.fxml.FXML java.util.ResourceBundle resources', instance_variable: true");
193+
194+
if (context.getSettings().isWithComments()) {
195+
sb.append(NL).append(NL).append(INDENT).append("# URL location of the FXML file that was given to the FXMLLoader. Access as self.location, or @location if instance_variable is true"); //NOI18N
196+
}
197+
sb.append(NL).append(INDENT);
198+
sb.append("java_field '@javafx.fxml.FXML java.net.URL location', instance_variable: true");
199+
sb.append(NL).append(NL);
200+
}
201+
202+
203+
void appendFieldsWithFxId(SkeletonContext context, StringBuilder sb) {
204+
for (Map.Entry<String, Class<?>> variable : context.getVariables().entrySet()) {
205+
sb.append(INDENT).append("# @").append(variable.getKey()).append(": \t").append(variable.getValue().getSimpleName()); //NOI18N
206+
appendFieldParameters(sb, variable.getValue()); // just for reference
207+
sb.append(NL);
208+
}
209+
sb.append(NL);
210+
}
211+
212+
void appendInitialize(SkeletonContext context, StringBuilder sb) {
213+
if (!context.getSettings().isFull()) {
214+
return;
215+
}
216+
if (context.getSettings().isWithComments()) {
217+
sb.append(INDENT).append("# Called by JRubyFX after FXML loading is complete. Different from Java, same as normal Ruby"); //NOI18N
218+
sb.append(NL);
219+
}
220+
221+
sb.append(INDENT);
222+
sb.append("def initialize()"); //NOI18N
223+
sb.append(NL);
224+
appendAssertions(context, sb);
225+
sb.append(NL);
226+
sb.append(INDENT);
227+
sb.append("end").append(NL).append(NL); //NOI18N
228+
}
229+
230+
void appendEventHandlers(SkeletonContext context, StringBuilder sb) {
231+
for (Map.Entry<String, String> entry : context.getEventHandlers().entrySet()) {
232+
String methodName = entry.getKey();
233+
String eventClassName = entry.getValue();
234+
235+
final String methodNamePured = methodName.replace("#", ""); //NOI18N
236+
237+
sb.append(INDENT);
238+
appendEventHandler(methodNamePured, eventClassName, sb);
239+
sb.append(NL).append(NL);
240+
}
241+
}
242+
243+
void appendEventHandler(String methodName, String eventClassName, StringBuilder sb) {
244+
sb.append("def "); //NOI18N
245+
sb.append(methodName);
246+
sb.append("(").append("event) # event: ").append(eventClassName).append(NL).append(NL); //NOI18N
247+
sb.append(INDENT).append("end"); //NOI18N
248+
}
249+
250+
void appendAssertions(SkeletonContext context, StringBuilder sb) {
251+
for (String assertion : context.getAssertions()) {
252+
sb.append(INDENT).append(INDENT)
253+
.append("raise 'fx:id=\"").append(assertion).append("\" was not injected: check your FXML file ") //NOI18N
254+
.append("\"").append(context.getDocumentName()).append("\".' if ") //NOI18N
255+
.append("@").append(assertion).append(".nil?").append(NL); //NOI18N
256+
}
257+
}
258+
259+
}

kit/src/main/java/com/oracle/javafx/scenebuilder/kit/skeleton/SkeletonFileNameProposal.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, Gluon and/or its affiliates.
2+
* Copyright (c) 2021, 2023, Gluon and/or its affiliates.
33
* All rights reserved. Use is subject to license terms.
44
*
55
* This file is available and licensed under the following license:
@@ -120,6 +120,9 @@ private File adjustToSrcMainDirWhenPossible(File controllerAtFxmlLocation) {
120120
case KOTLIN:
121121
location = location.replace(resources, kotlin);
122122
break;
123+
case JRUBY:
124+
// use default location in "resources"
125+
break;
123126
}
124127
}
125128
}

kit/src/main/java/com/oracle/javafx/scenebuilder/kit/skeleton/SkeletonSettings.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, Gluon and/or its affiliates.
2+
* Copyright (c) 2021, 2023, Gluon and/or its affiliates.
33
* All rights reserved. Use is subject to license terms.
44
*
55
* This file is available and licensed under the following license:
@@ -38,7 +38,7 @@ class SkeletonSettings {
3838
private FORMAT_TYPE textFormat = FORMAT_TYPE.COMPACT;
3939

4040
enum LANGUAGE {
41-
JAVA("Java", ".java"), KOTLIN("Kotlin", ".kt");
41+
JAVA("Java", ".java"), KOTLIN("Kotlin", ".kt"), JRUBY("JRuby", ".rb");
4242

4343
private final String name;
4444
private final String ext;

0 commit comments

Comments
 (0)