contrib/jwrapper: skip wrapping Java classes with invalid names
[nit.git] / contrib / jwrapper / src / code_generator.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Copyright 2014 Frédéric Vachon <fredvac@gmail.com>
4 # Copyright 2015 Alexis Laferrière <alexis.laf@xymus.net>
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17
18 # Services to generate extern class `in "Java"`
19 module code_generator
20
21 intrude import model
22
23 class CodeGenerator
24
25 # Path to the output file
26 var file_name: String
27
28 # Model of Java class being wrapped
29 var model: JavaModel
30
31 # Comment out methods with unknown (unwrapped) types
32 var comment_unknown_types: Bool
33
34 # Generate stub classes for unknown types used in the generated module
35 var stub_for_unknown_types: Bool
36
37 # Output file
38 var file_out: Writer = new FileWriter.open(file_name) is lazy, writable
39
40 # Name of the Nit module to generate
41 var module_name: nullable String is lazy do
42 if file_name.file_extension == "nit" then
43 # Output file ends with .nit, we expect it to be a valid name
44 return file_name.basename(".nit")
45 else return null
46 end
47
48 # Generate the Nit module into `file_out`
49 fun generate
50 do
51 # License
52 file_out.write license
53
54 # Module declaration
55 var module_name = module_name
56 if module_name != null then file_out.write "module {module_name}\n"
57 file_out.write "\n"
58
59 # All importations
60 var imports = new HashSet[String]
61 imports.add "import java\n"
62 for key, jclass in model.classes do
63 for import_ in jclass.imports do imports.add "import android::{import_}\n"
64 end
65 file_out.write imports.join("\n")
66 file_out.write "\n"
67
68 for key, jclass in model.classes do
69 # Skip anonymous classes
70 if jclass.class_type.is_anonymous then continue
71
72 # Skip classes with an invalid name at the Java language level
73 if jclass.class_type.extern_equivalent.has("-") then continue
74
75 generate_class_header(jclass.class_type)
76
77 for id, signatures in jclass.methods do
78 for signature in signatures do if not signature.is_static then
79 generate_method(jclass, id, id, signature.return_type, signature.params)
80 file_out.write "\n"
81 end
82 end
83
84 # Constructors
85 for constructor in jclass.constructors do
86 var complex = jclass.constructors.length != 1 and constructor.params.not_empty
87 var base_name = if complex then "from" else ""
88 var name = jclass.nit_name_for(base_name, constructor.params, complex, false)
89
90 generate_constructor(jclass, constructor, name)
91 end
92
93 # Attributes
94 for id, attribute in jclass.attributes do if not attribute.is_static then
95 generate_getter_setter(jclass, id, attribute)
96 end
97
98 # JNI services
99 generate_jni_services jclass.class_type
100
101 # Close the class
102 file_out.write "end\n\n"
103
104 # Static functions as top-level methods
105 var static_functions_prefix = jclass.class_type.extern_name.to_snake_case
106 for id, signatures in jclass.methods do
107 for signature in signatures do if signature.is_static then
108 var nit_id = static_functions_prefix + "_" + id
109 generate_method(jclass, id, nit_id, signature.return_type, signature.params, is_static=true)
110 file_out.write "\n"
111 end
112 end
113
114 # Static attributes as top-level getters and setters
115 for id, attribute in jclass.attributes do if attribute.is_static then
116 generate_getter_setter(jclass, id, attribute)
117 end
118
119 # Primitive arrays
120 for d in [1..opt_arrays.value] do
121 generate_primitive_array(jclass, d)
122 end
123 end
124
125 if stub_for_unknown_types then
126 for jtype, nit_type in model.unknown_types do
127 generate_unknown_class_header(jtype)
128 file_out.write "\n"
129 end
130 end
131 end
132
133 # License for the header of the generated Nit module
134 var license = """
135 # This file is part of NIT (http://www.nitlanguage.org).
136 #
137 # Licensed under the Apache License, Version 2.0 (the "License");
138 # you may not use this file except in compliance with the License.
139 # You may obtain a copy of the License at
140 #
141 # http://www.apache.org/licenses/LICENSE-2.0
142 #
143 # Unless required by applicable law or agreed to in writing, software
144 # distributed under the License is distributed on an "AS IS" BASIS,
145 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
146 # See the License for the specific language governing permissions and
147 # limitations under the License.
148
149 # This code has been generated using `jwrapper`
150 """ is writable
151
152 private fun generate_class_header(jtype: JavaType)
153 do
154 var nit_type = model.java_to_nit_type(jtype)
155 file_out.write "# Java class: {jtype}\n"
156 file_out.write "extern class {nit_type} in \"Java\" `\{ {jtype.extern_equivalent} `\}\n"
157 file_out.write "\tsuper JavaObject\n\n"
158 end
159
160 private fun generate_unknown_class_header(jtype: JavaType)
161 do
162 var nit_type = jtype.extern_name
163
164 file_out.write "extern class {nit_type} in \"Java\" `\{ {jtype.extern_equivalent} `\}\n"
165 file_out.write "\tsuper JavaObject\n\nend\n"
166 end
167
168 private fun generate_method(java_class: JavaClass, java_method_id, method_id: String,
169 java_return_type: JavaType, java_params: Array[JavaType], is_static: nullable Bool)
170 do
171 var java_args = new Array[String]
172 var nit_params = new Array[String]
173 var nit_id = "arg"
174 var nit_id_no = 0
175 var c = ""
176
177 # Parameters
178 for jparam in java_params do
179 var nit_type = model.java_to_nit_type(jparam)
180
181 if not nit_type.is_known and comment_unknown_types then c = "#"
182 if jparam.is_vararg then c = "#"
183
184 java_args.add "{jparam.param_cast}{nit_id}{nit_id_no}"
185 nit_params.add "{nit_id}{nit_id_no}: {nit_type}"
186
187 nit_id_no += 1
188 end
189
190 # Method identifier
191 method_id = method_id.to_nit_method_name
192 method_id = java_class.nit_name_for(method_id, java_params, java_class.methods[java_method_id].length > 1, is_static == true)
193
194 # Build the signature
195 var nit_signature = new Array[String]
196 nit_signature.add "fun {method_id}"
197 if not java_params.is_empty then nit_signature.add "({nit_params.join(", ")})"
198
199 # Return value
200 var return_type = null
201 if not java_return_type.is_void then
202 return_type = model.java_to_nit_type(java_return_type)
203
204 if not return_type.is_known and comment_unknown_types then c = "#"
205 if java_return_type.is_vararg then c = "#"
206
207 nit_signature.add ": " + return_type.to_s
208 end
209
210 # Build the call in Java
211 var java_call
212 if is_static == true then
213 java_call = java_class.class_type.package_name
214 else java_call = "self"
215 java_call += ".{java_method_id}({java_args.join(", ")})"
216
217 if return_type != null then java_call = "return {java_return_type.return_cast}" + java_call
218
219 # Tabulation
220 var t = "\t"
221 if is_static == true then t = ""
222 var ct = c+t
223
224 # Write
225 file_out.write """
226 {{{t}}}# Java implementation: {{{java_return_type}}} {{{java_class}}}.{{{java_method_id}}}({{{java_params.join(", ")}}})
227 {{{ct}}}{{{nit_signature.join}}} in "Java" `{
228 {{{ct}}} {{{java_call}}};
229 {{{ct}}}`}
230 """
231 end
232
233 # Generate getter and setter to access an attribute, of field
234 private fun generate_getter_setter(java_class: JavaClass, java_id: String,
235 attribute: JavaAttribute)
236 do
237 var java_type = attribute.java_type
238 var nit_type = model.java_to_nit_type(java_type)
239
240 var nit_id = java_id
241 if attribute.is_static then nit_id = java_class.class_type.extern_name.to_snake_case + "_" + nit_id
242 nit_id = nit_id.to_nit_method_name
243 nit_id = java_class.nit_name_for(nit_id, [java_type], false, attribute.is_static)
244
245 var c = ""
246 if not nit_type.is_known and comment_unknown_types then c = "#"
247 if java_type.is_vararg then c = "#"
248
249 var recv
250 if attribute.is_static then
251 recv = java_class.class_type.package_name
252 else recv = "self"
253
254 # Tabulation
255 var t = "\t"
256 if attribute.is_static then t = ""
257 var ct = c+t
258
259 file_out.write """
260 {{{t}}}# Java getter: {{{java_class}}}.{{{java_id}}}
261 {{{ct}}}fun {{{nit_id}}}: {{{nit_type}}} in "Java" `{
262 {{{ct}}} return {{{recv}}}.{{{java_id}}};
263 {{{ct}}}`}
264
265 {{{t}}}# Java setter: {{{java_class}}}.{{{java_id}}}
266 {{{ct}}}fun {{{nit_id}}}=(value: {{{nit_type}}}) in "Java" `{
267 {{{ct}}} {{{recv}}}.{{{java_id}}} = value;
268 {{{ct}}}`}
269
270 """
271 end
272
273 # Generate getter and setter to access an attribute, of field
274 private fun generate_constructor(java_class: JavaClass, constructor: JavaConstructor, name: String)
275 do
276 var c = ""
277 var nit_params_s = ""
278 var java_params_s = ""
279
280 if constructor.params.not_empty then
281 var nit_params = new Array[String]
282 var java_params = new Array[String]
283 var param_id = 'a'
284 for java_type in constructor.params do
285
286 java_params.add "{java_type.param_cast}{param_id}"
287
288 var nit_type = model.java_to_nit_type(java_type)
289 nit_params.add "{param_id}: {nit_type}"
290 param_id = param_id.successor(1)
291
292 if not nit_type.is_known and comment_unknown_types then c = "#"
293 if java_type.is_vararg then c = "#"
294 end
295
296 nit_params_s = "(" + nit_params.join(", ") + ")"
297 java_params_s = java_params.join(", ")
298 end
299
300 file_out.write """
301 # Java constructor: {{{java_class}}}
302 {{{c}}} new {{{name}}}{{{nit_params_s}}} in "Java" `{
303 {{{c}}} return new {{{java_class.class_type.package_name}}}({{{java_params_s}}});
304 {{{c}}} `}
305
306 """
307 end
308
309 private fun generate_primitive_array(java_class: JavaClass, dimensions: Int)
310 do
311 var base_java_type = java_class.class_type
312 var java_type = base_java_type.clone
313 java_type.array_dimension = dimensions
314
315 var base_nit_type = model.java_to_nit_type(base_java_type)
316 var nit_type = model.java_to_nit_type(java_type)
317
318 file_out.write """
319 # Java primitive array: {{{java_type}}}
320 extern class {{{nit_type}}} in "Java" `{ {{{java_type.extern_equivalent}}} `}
321 super AbstractJavaArray[{{{base_nit_type}}}]
322
323 # Get a new array of the given `size`
324 new(size: Int) in "Java" `{ return new {{{base_java_type}}}[(int)size]; `}
325
326 redef fun [](i) in "Java" `{ return self[(int)i]; `}
327
328 redef fun []=(i, e) in "Java" `{ self[(int)i] = e; `}
329
330 redef fun length in "Java" `{ return self.length; `}
331
332 """
333 generate_jni_services(java_type)
334 file_out.write """
335 end
336
337 """
338 end
339
340 # Generate JNI related services
341 #
342 # For now, mostly avoid issue #845, but more services could be generated as needed.
343 private fun generate_jni_services(java_type: JavaType)
344 do
345 var nit_type = model.java_to_nit_type(java_type)
346
347 file_out.write """
348 redef fun new_global_ref import sys, Sys.jni_env `{
349 Sys sys = {{{nit_type}}}_sys(self);
350 JNIEnv *env = Sys_jni_env(sys);
351 return (*env)->NewGlobalRef(env, self);
352 `}
353
354 redef fun pop_from_local_frame_with_env(jni_env) `{
355 return (*jni_env)->PopLocalFrame(jni_env, self);
356 `}
357 """
358 end
359 end
360
361 redef class Sys
362 # List of Nit keywords
363 #
364 # These may also be keywords in Java, but there they would be used capitalized.
365 private var nit_keywords = new HashSet[String].from(["abort", "abstract", "and", "assert",
366 "break", "class", "continue", "do", "else", "end", "enum", "extern", "false", "implies",
367 "import", "init", "interface", "intrude", "if", "in", "is", "isa", "isset", "for", "label",
368 "loop", "module", "new", "not", "null", "nullable", "or", "package", "private",
369 "protected", "public", "return", "self", "super", "then", "true", "type", "var", "while",
370
371 # Top-level methods
372 "class_name", "get_time", "hash", "inspect", "inspect_head", "is_same_type",
373 "is_same_instance", "object_id", "output", "output_class_name", "sys", "to_s",
374
375 # Pointer or JavaObject methods
376 "free"])
377
378 # Name of methods used at the top-level
379 #
380 # Used by `JavaClass::nit_name_for` with static properties.
381 private var top_level_used_names = new HashSet[String]
382 end
383
384 redef class String
385
386 # Convert the Java method name `self` to the Nit style
387 #
388 # * Converts to snake case
389 # * Strips `Get` and `Set`
390 # * Add suffix `=` to setters
391 fun to_nit_method_name: String
392 do
393 var name = self.to_snake_case
394
395 # Strip the '_' prefix
396 while name.has_prefix("_") do name = name.substring(1, name.length-1)
397
398 # Escape Nit keywords
399 if nit_keywords.has(name) then name += "_"
400
401 # If the name starts by something other than a letter, prefix with `java_`
402 if not name.chars.first.is_letter then name = "java_" + name
403
404 name = name.replace("$", "_")
405
406 return name
407 end
408 end
409
410 redef class JavaClass
411 # Property names used in this class
412 private var used_names = new HashSet[String]
413
414 # Get an available property name for the Java property with `name` and parameters
415 #
416 # If `use_parameters_name` then expect that there will be conflicts,
417 # so use the types of `parameters` to build the name.
418 private fun nit_name_for(name: String, parameters: Array[JavaType], use_parameters_name: Bool, is_static: Bool): String
419 do
420 # Append the name of each parameter
421 if use_parameters_name then
422 for param in parameters do
423 name += "_" + param.id
424 end
425 end
426
427 # Set of property names, local or top-level
428 var used_names
429 if is_static then
430 used_names = sys.top_level_used_names
431 else used_names = self.used_names
432
433 # As a last resort, append numbers to the name
434 var base_name = name
435 var count = 1
436 while used_names.has(name) do
437 name = base_name + count.to_s
438 count += 1
439 end
440
441 used_names.add name
442 return name
443 end
444 end