Java tutorial
/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2015 Everlaw. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package netbeanstypescript; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.json.simple.parser.ParseException; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandleFactory; import org.netbeans.modules.csl.api.Severity; import org.netbeans.modules.csl.spi.DefaultError; import org.netbeans.modules.parsing.api.Snapshot; import org.netbeans.modules.parsing.api.Source; import org.netbeans.modules.parsing.spi.indexing.Context; import org.netbeans.modules.parsing.spi.indexing.ErrorsCache; import org.netbeans.modules.parsing.spi.indexing.ErrorsCache.Convertor; import org.netbeans.modules.parsing.spi.indexing.Indexable; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.filesystems.URLMapper; import org.openide.modules.InstalledFileLocator; import org.openide.util.RequestProcessor; /** * * @author jeffrey */ public class TSService { static final Logger log = Logger.getLogger(TSService.class.getName()); static final RequestProcessor RP = new RequestProcessor("TSService", 1, true); private static class ExceptionFromJS extends Exception { ExceptionFromJS(String msg) { super(msg); } } static void stringToJS(StringBuilder sb, CharSequence s) { sb.append('"'); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c < 0x20) { sb.append("\\u"); for (int j = 12; j >= 0; j -= 4) { sb.append("0123456789ABCDEF".charAt((c >> j) & 0x0F)); } } else { if (c == '\\' || c == '"') { sb.append('\\'); } sb.append(c); } } sb.append('"'); } static final String builtinLibPrefix = "(builtin)/"; static final Map<String, FileObject> builtinLibs = new HashMap<>(); static { URL libDirURL = TSService.class.getClassLoader().getResource("netbeanstypescript/lib"); for (FileObject lib : URLMapper.findFileObject(libDirURL).getChildren()) { builtinLibs.put(builtinLibPrefix + lib.getNameExt(), lib); } } // All access to the TSService state below should be done with this lock acquired. This lock // has a fair ordering policy so error checking won't starve other user actions. private static final Lock lock = new ReentrantLock(true); private static NodeJSProcess nodejs = null; private static final Map<URL, ProgramData> programs = new HashMap<>(); private static final Map<String, FileData> allFiles = new HashMap<>(); private static class NodeJSProcess { OutputStream stdin; BufferedReader stdout; String error; Set<Integer> supportedCodeFixes = new HashSet<>(); int nextProgId = 0; NodeJSProcess() throws Exception { log.info("Starting nodejs"); File file = InstalledFileLocator.getDefault().locate("nbts-services.js", "netbeanstypescript", false); // Node installs to /usr/local/bin on OS X, but OS X doesn't put /usr/local/bin in the // PATH of applications started from the GUI for (String command : new String[] { "nodejs", "node", "/usr/local/bin/node" }) { try { Process process = new ProcessBuilder().command(command, "--harmony", file.toString()).start(); stdin = process.getOutputStream(); stdout = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); process.getErrorStream().close(); error = null; break; } catch (Exception e) { error = "Error creating Node.js process. Make sure the \"nodejs\" or \"node\" executable is installed and on your PATH." + "\n\nClose all TypeScript projects and reopen to retry." + "\n\n" + e; } } for (String code : (List<String>) eval("ts.getSupportedCodeFixes()\n")) { supportedCodeFixes.add(Integer.valueOf(code)); } StringBuilder initLibs = new StringBuilder(); for (Map.Entry<String, FileObject> lib : builtinLibs.entrySet()) { initLibs.append("void(builtinLibs["); stringToJS(initLibs, lib.getKey()); initLibs.append("]="); stringToJS(initLibs, Source.create(lib.getValue()).createSnapshot().getText()); initLibs.append(");"); } eval(initLibs.append('\n').toString()); } final Object eval(String code) throws ParseException, ExceptionFromJS { if (error != null) { return null; } log.log(Level.FINER, "OUT[{0}]: {1}", new Object[] { code.length(), code.length() > 120 ? code.substring(0, 120) + "...\n" : code }); long t1 = System.currentTimeMillis(); String s; try { stdin.write(code.getBytes(StandardCharsets.UTF_8)); stdin.flush(); while ((s = stdout.readLine()) != null && s.charAt(0) == 'L') { log.fine((String) JSONValue.parseWithException(s.substring(1))); } } catch (Exception e) { error = "Error communicating with Node.js process." + "\n\nClose all TypeScript projects and reopen to retry." + "\n\n" + e; return null; } log.log(Level.FINER, "IN[{0},{1}]: {2}\n", new Object[] { s.length(), System.currentTimeMillis() - t1, s.length() > 120 ? s.substring(0, 120) + "..." : s }); if (s.charAt(0) == 'X') { throw new ExceptionFromJS((String) JSONValue.parseWithException(s.substring(1))); } else if (s.equals("undefined")) { return null; // JSON parser doesn't like undefined } else { return JSONValue.parseWithException(s); } } void close() throws IOException { if (stdin != null) stdin.close(); if (stdout != null) stdout.close(); } } private static class ProgramData { final FileObject root; final String progVar; final Map<String, FileData> byRelativePath = new HashMap<>(); final List<FileObject> needCompileOnSave = new ArrayList<>(); boolean needErrorsUpdate; Object currentErrorsUpdate; ProgramData(FileObject root) throws Exception { this.root = root; progVar = "p" + nodejs.nextProgId++; StringBuilder newProgram = new StringBuilder(progVar).append("= new Program("); stringToJS(newProgram, root.getPath()); nodejs.eval(newProgram.append(")\n").toString()); } Object call(String method, Object... args) { StringBuilder sb = new StringBuilder(progVar).append('.').append(method).append('('); for (Object arg : args) { if (sb.charAt(sb.length() - 1) != '(') sb.append(','); if (arg instanceof CharSequence) { stringToJS(sb, (CharSequence) arg); } else { sb.append(String.valueOf(arg)); } } sb.append(")\n"); try { return nodejs.eval(sb.toString()); } catch (Exception e) { log.log(Level.INFO, "Exception in nodejs.eval", e); return null; } } final void addFile(FileData fd, Snapshot s, boolean modified) { call("updateFile", fd.path, s.getText(), modified); byRelativePath.put(fd.indexable.getRelativePath(), fd); needErrorsUpdate = true; } String removeFile(Indexable indexable) throws Exception { FileData fd = byRelativePath.remove(indexable.getRelativePath()); if (fd != null) { needErrorsUpdate = true; call("deleteFile", fd.path); return fd.path; } return null; } void dispose() throws Exception { nodejs.eval("delete " + progVar + "\n"); } } private static class FileData { ProgramData program; FileObject fileObject; Indexable indexable; String path; } static void addFile(Snapshot snapshot, Indexable indxbl, Context cntxt) { lock.lock(); try { URL rootURL = cntxt.getRootURI(); ProgramData program = programs.get(rootURL); if (program == null) { if (nodejs == null) { nodejs = new NodeJSProcess(); } program = new ProgramData(cntxt.getRoot()); } programs.put(rootURL, program); FileData fi = new FileData(); fi.program = program; fi.fileObject = snapshot.getSource().getFileObject(); fi.indexable = indxbl; fi.path = fi.fileObject.getPath(); allFiles.put(fi.path, fi); program.addFile(fi, snapshot, cntxt.checkForEditorModifications()); if (!cntxt.isAllFilesIndexing() && !cntxt.checkForEditorModifications()) { program.needCompileOnSave.add(fi.fileObject); } } catch (Exception e) { throw new RuntimeException(e); } finally { lock.unlock(); } } static void removeFile(Indexable indxbl, Context cntxt) { lock.lock(); try { ProgramData program = programs.get(cntxt.getRootURI()); if (program != null) { try { String path = program.removeFile(indxbl); allFiles.remove(path); } catch (Exception e) { throw new RuntimeException(e); } } } finally { lock.unlock(); } } static final Convertor<JSONObject> errorConvertor = new Convertor<JSONObject>() { @Override public ErrorsCache.ErrorKind getKind(JSONObject err) { int category = ((Number) err.get("category")).intValue(); return category == 0 ? ErrorsCache.ErrorKind.WARNING : ErrorsCache.ErrorKind.ERROR; } @Override public int getLineNumber(JSONObject err) { return ((Number) err.get("line")).intValue(); } @Override public String getMessage(JSONObject err) { return (String) err.get("messageText"); } }; static void preIndex(URL rootURI) { lock.lock(); try { ProgramData program = programs.get(rootURI); // Stop errors update task so it doesn't starve indexing. We'll restart it in postIndex. if (program != null) { program.currentErrorsUpdate = null; } } finally { lock.unlock(); } } static void postIndex(final URL rootURI) { final ProgramData program; final Object currentUpdate; final String[] files; final FileObject[] compileOnSave; lock.lock(); try { program = programs.get(rootURI); if (program == null || !program.needErrorsUpdate) { return; } program.needErrorsUpdate = false; program.currentErrorsUpdate = currentUpdate = new Object(); files = program.byRelativePath.keySet().toArray(new String[0]); compileOnSave = program.needCompileOnSave.toArray(new FileObject[0]); program.needCompileOnSave.clear(); } finally { lock.unlock(); } new Runnable() { RequestProcessor.Task task = RP.create(this); ProgressHandle progress = ProgressHandleFactory.createHandle("TypeScript error checking", task); @Override public void run() { TSIndexerFactory.compileIfEnabled(program.root, compileOnSave); progress.start(files.length); try { long t1 = System.currentTimeMillis(); for (int i = 0; i < files.length; i++) { String fileName = files[i]; progress.progress(fileName, i); if (fileName.endsWith(".json")) { continue; } lock.lockInterruptibly(); try { if (program.currentErrorsUpdate != currentUpdate) { return; // this task has been superseded } FileData fi = program.byRelativePath.get(fileName); if (fi == null) { continue; } JSONObject errors = (JSONObject) program.call("getDiagnostics", fi.path); if (errors != null) { ErrorsCache.setErrors(rootURI, fi.indexable, (List<JSONObject>) errors.get("errs"), errorConvertor); } } finally { lock.unlock(); } } log.log(Level.FINE, "updateErrors for {0} completed in {1}ms", new Object[] { rootURI, System.currentTimeMillis() - t1 }); } catch (InterruptedException e) { log.log(Level.INFO, "updateErrors for {0} cancelled by user", rootURI); } finally { progress.finish(); } } }.task.schedule(0); } static void removeProgram(URL rootURL) { lock.lock(); try { ProgramData program = programs.remove(rootURL); if (program == null) { return; } program.currentErrorsUpdate = null; // stop any updateErrors task Iterator<FileData> iter = allFiles.values().iterator(); while (iter.hasNext()) { FileData fd = iter.next(); if (fd.program == program) { iter.remove(); } } try { program.dispose(); } catch (Exception e) { throw new RuntimeException(e); } if (programs.isEmpty()) { log.info("No programs left; shutting down nodejs"); try { nodejs.close(); } catch (IOException e) { } nodejs = null; } } finally { lock.unlock(); } } static void updateFile(Snapshot snapshot) { FileObject fo = snapshot.getSource().getFileObject(); if (fo == null) { return; } lock.lock(); try { FileData fd = allFiles.get(fo.getPath()); if (fd != null) { fd.program.call("updateFile", fd.path, snapshot.getText(), true); } } finally { lock.unlock(); } } static List<DefaultError> getDiagnostics(Snapshot snapshot) { FileObject fo = snapshot.getSource().getFileObject(); if (fo == null) { return Arrays .asList(new DefaultError(null, "FileObject is null", null, fo, 0, 1, true, Severity.ERROR)); } lock.lock(); try { FileData fd = allFiles.get(fo.getPath()); if (fd == null) { return Arrays.asList(new DefaultError(null, "Unknown source root for file " + fo.getPath(), null, fo, 0, 1, true, Severity.ERROR)); } JSONObject diags = (JSONObject) fd.program.call("getDiagnostics", fd.path); if (diags == null) { return Arrays.asList( new DefaultError(null, nodejs.error != null ? nodejs.error : "Error in getDiagnostics", null, fo, 0, 1, true, Severity.ERROR)); } List<DefaultError> errors = new ArrayList<>(); for (String metaError : (List<String>) diags.get("metaErrors")) { errors.add(new DefaultError(null, metaError, null, fo, 0, 1, true, Severity.ERROR)); } for (JSONObject err : (List<JSONObject>) diags.get("errs")) { int start = ((Number) err.get("start")).intValue(); int length = ((Number) err.get("length")).intValue(); String messageText = (String) err.get("messageText"); int category = ((Number) err.get("category")).intValue(); int code = ((Number) err.get("code")).intValue(); boolean fix = nodejs.supportedCodeFixes.contains(code); errors.add(new DefaultError(fix ? Integer.toString(code) : null, messageText, null, fo, start, start + length, false, category == 0 ? Severity.WARNING : Severity.ERROR)); } return errors; } finally { lock.unlock(); } } public static Object call(String method, FileObject fileObj, Object... args) { if (fileObj == null) { return null; } lock.lock(); try { FileData fd = allFiles.get(fileObj.getPath()); if (fd == null) { return null; } Object[] filenameAndArgs = new Object[args.length + 1]; filenameAndArgs[0] = fd.path; System.arraycopy(args, 0, filenameAndArgs, 1, args.length); return fd.program.call(method, filenameAndArgs); } finally { lock.unlock(); } } static FileObject findIndexedFileObject(String path) { lock.lock(); try { FileData fd = allFiles.get(path); return fd != null ? fd.fileObject : null; } finally { lock.unlock(); } } static FileObject findAnyFileObject(String path) { return path.startsWith(builtinLibPrefix) ? builtinLibs.get(path) : FileUtil.toFileObject(new File(path)); } }