ce31b52438ae6bebfb502d9359592df33fb63f0d
[jalview.git] / utils / jalviewjs / Java2ScriptCompiler.java
1 package net.sf.j2s.core;
2
3 import java.io.BufferedReader;
4 import java.io.File;
5 import java.io.FileFilter;
6 import java.io.FileInputStream;
7 import java.io.FileNotFoundException;
8 import java.io.FileOutputStream;
9 import java.io.IOException;
10 import java.io.InputStreamReader;
11 import java.nio.file.Files;
12 import java.nio.file.StandardCopyOption;
13 import java.util.ArrayList;
14 import java.util.Date;
15 import java.util.Enumeration;
16 import java.util.HashSet;
17 import java.util.Hashtable;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Properties;
21
22 import org.eclipse.core.resources.IFile;
23 import org.eclipse.core.resources.IProject;
24 import org.eclipse.core.runtime.IProduct;
25 import org.eclipse.jdt.core.IJavaProject;
26 import org.eclipse.jdt.core.JavaCore;
27 import org.eclipse.jdt.core.dom.AST;
28 import org.eclipse.jdt.core.dom.ASTParser;
29 import org.eclipse.jdt.core.dom.CompilationUnit;
30
31 /**
32  * The main (and currently only operational) Java2Script compiler.
33  * 
34  * @author Bob Hanson
35  *
36  */
37 class Java2ScriptCompiler {
38         /**
39          * The name of the J2S options file, aka as the "Dot-j2s" file.
40          */
41         private static final String J2S_OPTIONS_FILE_NAME = ".j2s";
42         
43         // BH: added "true".equals(getProperty(props,
44         // "j2s.compiler.allow.compression")) to ensure compression only occurs when
45         // desired
46         private static final int JSL_LEVEL = AST.JLS8; // deprecation just because Java has moved on
47         private boolean showJ2SSettings = true;
48
49         // We copy all non .java files from any directory from which we loaded a
50         // java file into the site directory
51         private final HashSet<String> copyResources = new HashSet<String>();
52         private Map<String, String> htMethodsCalled;
53         private List<String> lstMethodsDeclared;
54
55         private Properties props;
56         private String htmlTemplate = null;
57
58         private String projectFolder;
59
60         private String j2sPath;
61
62         private String logDeclared;
63
64         private boolean logAllCalls;
65
66         private String logCalled;
67
68         private String excludedPaths;
69
70         private String siteFolder;
71
72         private boolean testing;
73         
74         private List<String> lstExcludedPaths;
75
76         private boolean isCleanBuild;
77
78         boolean isCompilationParticipant;
79
80         private ASTParser astParser;
81
82         private IJavaProject project;
83
84         private boolean isDebugging;
85
86         static boolean isActive(IJavaProject project) {
87                 try {
88                         return new File(project.getProject().getLocation().toOSString(), J2S_OPTIONS_FILE_NAME).exists();
89                 } catch (@SuppressWarnings("unused") Exception e) {
90                         return false;
91                 }
92         }
93
94         Java2ScriptCompiler() {
95                 // initialized only once using CompilationParticipant; every time using
96                 // older Builder idea
97         }
98
99         /**
100          * only for CompilationParticipant
101          * @param isClean
102          */
103         void startBuild(boolean isClean) {
104                 // at the beginning of a clean build, clear data
105                 isCleanBuild = isClean;
106                 htmlTemplate = null;
107                 if (isClean) {
108                         copyResources.clear();
109                         lstMethodsDeclared = null;
110                         htMethodsCalled = null;
111                 }
112
113         }
114
115         /**
116          * from Java2ScriptCompilationParticipant.java
117          * 
118          * get all necessary .j2s params for a build
119          * 
120          * @param project
121          * @param isCompilationParticipant
122          * @return true if this is a j2s project and is enabled
123          * 
124          */
125         boolean initializeProject(IJavaProject project, boolean isCompilationParticipant) {
126                 this.project = project;
127                 this.isCompilationParticipant = isCompilationParticipant;
128                 if (!isActive(project)) {
129                         // the file .j2s does not exist in the project directory -- skip this project
130                         return false;
131                 }
132                 projectFolder = project.getProject().getLocation().toOSString();
133                 props = new Properties();
134                 try {
135                         File j2sFile = new File(projectFolder, J2S_OPTIONS_FILE_NAME);
136                         props.load(new FileInputStream(j2sFile));
137                         String status = getProperty("j2s.compiler.status");
138                         if (!"enable".equals(status) && !"enabled".equals(status)) {
139                                 if (getFileContents(j2sFile).trim().length() == 0) {
140                                   writeToFile(j2sFile, getDefaultJ2SFile());
141                                 } else {
142                                  // not enabled
143                                 return false;
144                                 }
145                         }
146                 } catch (FileNotFoundException e1) {
147                         e1.printStackTrace();
148                 } catch (IOException e1) {
149                         e1.printStackTrace();
150                 }
151
152                 File file;
153                 siteFolder = getProperty("j2s.site.directory");
154                 if (siteFolder == null)
155                         siteFolder = "site";
156                 siteFolder = projectFolder + "/" + siteFolder;
157                 j2sPath = siteFolder + "/swingjs/j2s";
158
159                 if (isDebugging)
160                         System.out.println("Java2ScriptCompiler writing to " + j2sPath);
161                 // method declarations and invocations are only logged
162                 // when the designated files are deleted prior to building
163
164                 logDeclared = (isCompilationParticipant && !isCleanBuild ? null : getProperty("j2s.log.methods.declared"));
165                 if (logDeclared != null) {
166                         if (!(file = new File(projectFolder, logDeclared)).exists()) {
167                                 lstMethodsDeclared = new ArrayList<String>();
168                                 System.err.println("logging methods declared to " + file);
169                         }
170                         logDeclared = projectFolder + "/" + logDeclared;
171                 }
172                 logAllCalls = false;
173
174                 logCalled = (isCompilationParticipant && !isCleanBuild ? null : getProperty("j2s.log.methods.called"));
175                 if (logCalled != null) {
176                         if (!(file = new File(projectFolder, logCalled)).exists()) {
177                                 htMethodsCalled = new Hashtable<String, String>();
178                                 System.err.println("logging methods called to " + file);
179                         }
180                         logCalled = projectFolder + "/" + logCalled;
181                         logAllCalls = "true".equals(getProperty("j2s.log.all.calls"));
182                 }
183
184                 excludedPaths = getProperty("j2s.excluded.paths");
185
186                 lstExcludedPaths = null;
187
188                 if (excludedPaths != null) {
189                         lstExcludedPaths = new ArrayList<String>();
190                         String[] paths = excludedPaths.split(";");
191                         for (int i = 0; i < paths.length; i++)
192                                 if (paths[i].trim().length() > 0)
193                                         lstExcludedPaths.add(paths[i].trim() + "/");
194                         if (lstExcludedPaths.size() == 0)
195                                 lstExcludedPaths = null;
196                 }
197
198                 testing = "true".equals(getProperty("j2s.testing"));
199                 
200                 String prop = getProperty("j2s.compiler.nonqualified.packages");
201                 // older version of the name
202                 String nonqualifiedPackages = getProperty("j2s.compiler.nonqualified.classes");
203                 nonqualifiedPackages = (prop == null ? "" : prop)
204                         + (nonqualifiedPackages == null ? "" : (prop == null ? "" : ";") + nonqualifiedPackages);
205             if (nonqualifiedPackages.length() == 0)
206                 nonqualifiedPackages = null;
207                 // includes @j2sDebug blocks
208                 isDebugging = "debug".equals(getProperty("j2s.compiler.mode"));
209
210                 String classReplacements = getProperty("j2s.class.replacements");
211
212                 String htmlTemplateFile = getProperty("j2s.template.html");
213                 if (htmlTemplateFile == null)
214                         htmlTemplateFile = "template.html";
215
216                 if (htmlTemplate == null) {
217                         file = new File(projectFolder, htmlTemplateFile);
218                         if (!file.exists()) {
219                                 String html = getDefaultHTMLTemplate();
220                                 System.err.println("creating new htmltemplate\n" + html);
221                                 writeToFile(file, html);
222                         }
223                         htmlTemplate = getFileContents(file);
224                         if (showJ2SSettings)
225                                 System.err.println("using HTML template " + file);
226                 }
227
228                 Java2ScriptVisitor.setDebugging(isDebugging);
229                 Java2ScriptVisitor.setLogging(lstMethodsDeclared, htMethodsCalled, logAllCalls);
230
231                 Java2ScriptVisitor.NameMapper.setNonQualifiedNamePackages(nonqualifiedPackages);
232                 Java2ScriptVisitor.NameMapper.setClassReplacements(classReplacements);
233                 
234                 astParser = ASTParser.newParser(JSL_LEVEL);
235         
236                 return true;
237         }
238
239         /**
240          * from Java2ScriptCompilationParticipant.java
241          * 
242          * process the source file into JavaScript using the JDT abstract syntax
243          * tree parser and visitor
244          * 
245          * @param javaSource
246          */
247         boolean compileToJavaScript(IFile javaSource) {
248                 String fileName = new String(javaSource.getFullPath().removeFirstSegments(1).toPortableString());
249                 if (lstExcludedPaths != null) {
250                         for (int i = lstExcludedPaths.size(); --i >= 0;)
251                                 if (fileName.startsWith(lstExcludedPaths.get(i))) {
252                                         return true;
253                                 }
254                 }
255                 org.eclipse.jdt.core.ICompilationUnit createdUnit = JavaCore.createCompilationUnitFrom(javaSource);
256                 astParser.setSource(createdUnit);
257                 // note: next call must come before each createAST call
258                 astParser.setResolveBindings(true); 
259                 CompilationUnit root = (CompilationUnit) astParser.createAST(null);
260                 // If the Java2ScriptVisitor is ever extended, it is important to set the project.
261                 // Java2ScriptVisitor#addClassOrInterface uses getClass().newInstance().setproject(project). 
262                 Java2ScriptVisitor visitor = new Java2ScriptVisitor().setProject(project, testing);
263
264                 try {
265
266                         // transpile the code
267
268                         root.accept(visitor);
269
270                         // generate the .js file(s) in the site directory
271
272                         outputJavaScript(visitor, j2sPath);
273
274                         logMethods(logCalled, logDeclared, logAllCalls);
275
276                         // add the HTML files in the site directory
277
278                         addHTML(visitor.getAppList(true), siteFolder, htmlTemplate, true);
279                         addHTML(visitor.getAppList(false), siteFolder, htmlTemplate, false);
280                 } catch (Throwable e) {
281                         e.printStackTrace();
282                         e.printStackTrace(System.out);
283                         // find the file and delete it.
284                         String filePath = j2sPath;
285                         String rootName = root.getJavaElement().getElementName();
286                         rootName = rootName.substring(0, rootName.lastIndexOf('.'));
287                         String packageName = visitor.getMyPackageName();
288                         if (packageName != null) {
289                                 File folder = new File(filePath, packageName.replace('.', File.separatorChar));
290                                 filePath = folder.getAbsolutePath();
291                                 File jsFile = new File(filePath, rootName + ".js"); //$NON-NLS-1$
292                                 if (jsFile.exists()) {
293                                         System.out.println("Java2ScriptCompiler deleting " + jsFile);
294                                         jsFile.delete();
295                                 }
296                         }
297                         return false;
298                 }
299                 String packageName = visitor.getMyPackageName();
300                 if (packageName != null) {
301                         int pt = packageName.indexOf(".");
302                         if (pt >= 0)
303                                 packageName = packageName.substring(0, pt);
304                         if (!copyResources.contains(packageName)) {
305                                 copyResources.add(packageName);
306                                 File src = new File(projectFolder + "/src", packageName);
307                                 File dest = new File(j2sPath, packageName);
308                                 copySiteResources(src, dest);
309                         }
310                 }
311                 return true;
312         }
313
314         //// private methods ////
315         
316         
317         private void logMethods(String logCalled, String logDeclared, boolean doAppend) {
318                 if (htMethodsCalled != null)
319                         try {
320                                 File file = new File(logCalled);
321                                 file.createNewFile();
322                                 FileOutputStream fos = new FileOutputStream(file, doAppend);
323                                 for (String key : htMethodsCalled.keySet()) {
324                                         String val = htMethodsCalled.get(key);
325                                         fos.write(key.getBytes());
326                                         if (!val.equals("-")) {
327                                                 fos.write(',');
328                                                 fos.write(val.getBytes());
329                                         }
330                                         fos.write('\n');
331                                 }
332                                 fos.close();
333                         } catch (Exception e) {
334                                 System.err.println("Cannot log to " + logCalled + " " + e.getMessage());
335                         }
336                 if (lstMethodsDeclared != null)
337                         try {
338                                 File file = new File(logDeclared);
339                                 file.createNewFile();
340                                 FileOutputStream fos = new FileOutputStream(file, true);
341                                 for (int i = 0, n = lstMethodsDeclared.size(); i < n; i++) {
342                                         fos.write(lstMethodsDeclared.get(i).getBytes());
343                                         fos.write('\n');
344                                 }
345                                 fos.close();
346                         } catch (Exception e) {
347                                 System.err.println("Cannot log to " + logDeclared + " " + e.getMessage());
348                         }
349         }
350
351         private String getProperty(String key) {
352                 String val = props.getProperty(key);
353                 if (showJ2SSettings)
354                         System.err.println(key + " = " + val);
355                 return val;
356         }
357
358         private void outputJavaScript(Java2ScriptVisitor visitor, String j2sPath) {
359
360                 // fragments[0] is package]
361                 List<String> elements = visitor.getElementList();
362
363                 // BH all compression is deprecated --- use Google Closure Compiler
364
365                 String packageName = visitor.getMyPackageName();
366                 for (int i = 0; i < elements.size();) {
367                         String elementName = elements.get(i++);
368                         String element = elements.get(i++);
369                         createJSFile(j2sPath, packageName, elementName, element);
370                 }
371                 showJ2SSettings = false; // just once per compilation run
372         }
373
374         private void createJSFile(String j2sPath, String packageName, String elementName, String js) {
375                 if (packageName != null) {
376                         File folder = new File(j2sPath, packageName.replace('.', File.separatorChar));
377                         j2sPath = folder.getAbsolutePath();
378                         if (!folder.exists() || !folder.isDirectory()) {
379                                 if (!folder.mkdirs()) {
380                                         throw new RuntimeException("Failed to create folder " + j2sPath); //$NON-NLS-1$
381                                 }
382                         }
383                 }
384                 File f = new File(j2sPath, elementName + ".js");
385                 if (isDebugging)
386                         System.out.println("Java2ScriptCompiler creating " + f);
387                 writeToFile(f, js);
388         }
389
390         private String getFileContents(File file) {
391                 try {
392                         StringBuilder sb = new StringBuilder();
393                         FileInputStream is = new FileInputStream(file);
394                         BufferedReader reader = new BufferedReader(new InputStreamReader(is));
395                         String line = null;
396                         while ((line = reader.readLine()) != null) {
397                                 sb.append(line).append("\n");
398                         }
399                         reader.close();
400                         return sb.toString();
401                 } catch (@SuppressWarnings("unused") IOException e) {
402                         //
403                 }
404                 return null;
405         }
406
407         private void writeToFile(File file, String data) {
408                 if (data == null)
409                         return;
410                 try {
411                         FileOutputStream os = new FileOutputStream(file);
412                         os.write(data.getBytes("UTF-8"));
413                         os.close();
414                 } catch (IOException e) {
415                         e.printStackTrace();
416                 }
417     }
418
419         /**
420          * The default .j2s file. Replaces .j2s only if it is found but is empty.
421          * 
422          * OK, I know this should be a resource.
423          * 
424          * 
425          * 
426          */
427         
428         private String getDefaultJ2SFile() {
429
430                 return "#j2s default configuration file created by net.sf.java2script_" 
431         + CorePlugin.VERSION + " " + new Date() + "\n\n" +
432         "#enable the Java2Script transpiler -- comment out to disable\n" + 
433                 "j2s.compiler.status=enable\n" + 
434                 "\n" + 
435                 "\n" + 
436                 "# destination directory for all JavaScript\n" + 
437                 "j2s.site.directory=site\n" + 
438                 "\n" + 
439                 "# uncomment j2s.* lines to process:\n" + 
440                 "\n" + 
441                 "# a semicolon-separated list of package-level file paths to be excluded\n" + 
442                 "#j2s.excluded.paths=test;testng\n" + 
443                 "\n" + 
444                 "# output file name for logging methods declared - delete the file to regenerate a listing \n" + 
445                 "#j2s.log.methods.declared=methodsDeclared.csv\n" + 
446                 "\n" + 
447                 "#output file name for logging methods called - delete the file to regenerate a listing\n" + 
448                 "#j2s.log.methods.called=methodsCalled.csv\n" + 
449                 "\n" + 
450                 "#if set, every instance of methods called will be logged\n" + 
451                 "#otherwise, only the first call to a method will be logged \n" + 
452                 "#output will be comma-separated: called method,caller class \n" + 
453                 "#j2s.log.all.calls=true\n" + 
454                 "\n" + 
455                 "# a semicolon-separated list of packages that contain classes for which the method names\n" + 
456                 "# in their classes should not be \"qualified\" to indicate their parameter types. \n" + 
457                 "# This option is useful if you have an API interface in Java that refers to JavaScript \n" + 
458                 "# methods such as calling window or jQuery functions or the methods in Clazz or J2S. \n" + 
459                 "# The classes must not have any methods that are overloaded - with the\n" + 
460                 "# same name but different paramater type, as JavaScript will only see the last one.\n" + 
461                 "#j2s.compiler.nonqualified.packages=org.jmol.api.js;jspecview.api.js\n" + 
462                 "\n" + 
463                 "# uncomment to add debugging output. Start eclipse with the -consoleLog option to see output.\n" + 
464                 "#j2s.compiler.mode=debug\n" + 
465                 "\n" + 
466                 "# a semicolon-separated list of from->to listings of package (foo.) or class (foo.bar) \n" + 
467                 "# replacements to be made. This option allows for having one class or package used in Java\n" + 
468                 "# and another used in JavaScript. Take care with this. All methods in both packages must\n" + 
469                 "# have exactly the same parameter signature. We use it in Jalview to provide a minimal\n" + 
470                 "# JavaScript implementation of a large third-party library while still using that library's\n" + 
471                 "# jar file in Java.\n" + 
472                 "#j2s.class.replacements=org.apache.log4j.->jalview.javascript.log4j.\n" + 
473                 "\n" + 
474                 "# uncomment and change if you do not want to use the template.html file created for you\n" + 
475                 "# in your project directory. A default template file will be created by the transpiler \n" + 
476                 "# directory if there is none there already.\n" + 
477                 "#j2s.template.html=template.html\n";
478         }
479         /**
480          * The default template file. The user can specify another in the .j2s file
481          * using template.html=.....
482          * 
483          * @return default template with _NAME_, _CODE_, and _MAIN_ to fill in.
484          */
485         private String getDefaultHTMLTemplate() {
486                 return "<!DOCTYPE html>\n" + 
487                                 "<html>\n" + 
488                                 "<head>\n" + 
489                                 "<title>SwingJS test _NAME_</title><meta charset=\"utf-8\" />\n" + 
490                                 "<script src=\"swingjs/swingjs2.js\"></script>\n" + 
491                                 "<script>\n" + 
492                                 "if (!self.SwingJS)alert('swingjs2.js was not found. It needs to be in swingjs folder in the same directory as ' + document.location.href)\n" + 
493                                 "Info = {\n" + 
494                                 "  code: _CODE_,\n" + 
495                                 "  main: _MAIN_,\n" + 
496                                 "  core: \"NONE\",\n" + 
497                                 "       width: 850,\n" + 
498                                 "       height: 550,\n" + 
499                                 "  readyFunction: null,\n" + 
500                                 "       serverURL: 'https://chemapps.stolaf.edu/jmol/jsmol/php/jsmol.php',\n" + 
501                                 "       j2sPath: 'swingjs/j2s',\n" + 
502                                 "       console:'sysoutdiv',\n" + 
503                                 "       allowjavascript: true\n" + 
504                                 "}\n" + 
505                                 "</script>\n" + 
506                                 "</head>\n" + 
507                                 "<body>\n" + 
508                                 "<script>\n" + 
509                                 "SwingJS.getApplet('testApplet', Info)\n" + 
510                                 "getClassList = function(){J2S._saveFile('_j2sclasslist.txt', Clazz.ClassFilesLoaded.sort().join('\\n'))}\n" + 
511                                 "</script>\n" + 
512                                 "<div style=\"position:absolute;left:900px;top:30px;width:600px;height:300px;\">\n" + 
513                                 "<div id=\"sysoutdiv\" contentEditable=\"true\" style=\"border:1px solid green;width:100%;height:95%;overflow:auto\"></div>\n" + 
514                                 "This is System.out. <a href=\"javascript:testApplet._clearConsole()\">clear it</a> <br>Add ?j2snocore to URL to see full class list; ?j2sdebug to use uncompressed j2s/core files <br><a href=\"javascript:getClassList()\">get _j2sClassList.txt</a>\n" + 
515                                 "</div>\n" + 
516                                 "</body>\n" + 
517                                 "</html>\n";
518         }
519
520         /**
521          * Create a test HTML file for the applet or application. It will go into
522          * <project>/site.
523          * 
524          * @param appList
525          * @param siteFolder
526          * @param template
527          * @param isApplet
528          */
529         private void addHTML(ArrayList<String> appList, String siteFolder, String template, boolean isApplet) {
530                 if (appList == null || template == null)
531                         return;
532                 for (int i = appList.size(); --i >= 0;) {
533                         String cl = appList.get(i);
534                         String _NAME_ = cl.substring(cl.lastIndexOf(".") + 1);
535                         String fname = cl.replaceAll("\\.", "_") + (isApplet ? "_applet" : "") + ".html";
536                         cl = "\"" + cl + "\"";
537                         String _MAIN_ = (isApplet ? "null" : cl);
538                         String _CODE_ = (isApplet ? cl : "null");
539                         template = template.replace("_NAME_", _NAME_).replace("_CODE_", _CODE_).replace("_MAIN_", _MAIN_);
540                         System.err.println("Java2Script creating " + siteFolder + "/" + fname);
541                         writeToFile(new File(siteFolder, fname), template);
542                 }
543         }
544
545         private FileFilter filter = new FileFilter() {
546
547                 @Override
548                 public boolean accept(File pathname) {
549                         return pathname.isDirectory() || !pathname.getName().endsWith(".java");
550                 }
551
552         };
553
554         private void copySiteResources(File from, File dest) {
555                 copyNonclassFiles(from, dest);
556         }
557
558         private void copyNonclassFiles(File dir, File target) {
559                 if (dir.equals(target))
560                         return;
561                 File[] files = dir.listFiles(filter);
562                 File f = null;
563                 if (files != null) 
564                         try {
565                                 if (!target.exists())
566                                         Files.createDirectories(target.toPath());
567                                 for (int i = 0; i < files.length; i++) {
568                                         f = files[i];
569                                         if (f == null) {
570                                                 //
571                                         } else if (f.isDirectory()) {
572                                                 copyNonclassFiles(f, new File(target, f.getName()));
573                                         } else {
574                                                 Files.copy(f.toPath(), new File(target, f.getName()).toPath(),
575                                                                 StandardCopyOption.REPLACE_EXISTING);
576                                                 System.err.println("copied to site: " + f.toPath());
577                                         }
578                                 }
579                         } catch (IOException e1) {
580                                 // TODO Auto-generated catch block
581                                 System.err.println("error copying " + f + " to " + target);
582                                 e1.printStackTrace();
583                         }
584         }
585
586 }