1 /* 2 * Copyright (c) 2015, 2017, 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 package jdk.internal.editor.external; 27 28 import java.io.IOException; 29 import java.nio.charset.Charset; 30 import java.nio.file.ClosedWatchServiceException; 31 import java.nio.file.FileSystems; 32 import java.nio.file.FileVisitResult; 33 import java.nio.file.Files; 34 import java.nio.file.Path; 35 import java.nio.file.SimpleFileVisitor; 36 import java.nio.file.WatchKey; 37 import java.nio.file.WatchService; 38 import java.nio.file.attribute.BasicFileAttributes; 39 import java.util.Arrays; 40 import java.util.Scanner; 41 import java.util.function.Consumer; 42 import java.util.stream.Collectors; 43 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; 44 import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; 45 import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; 46 47 /** 48 * Wrapper for controlling an external editor. 49 */ 50 public class ExternalEditor { 51 private final Consumer<String> errorHandler; 52 private final Consumer<String> saveHandler; 53 private final boolean wait; 54 55 private final Runnable suspendInteractiveInput; 56 private final Runnable resumeInteractiveInput; 57 private final Runnable promptForNewLineToEndWait; 58 59 private WatchService watcher; 60 private Thread watchedThread; 61 private Path dir; 62 private Path tmpfile; 63 64 /** 65 * Launch an external editor. 66 * 67 * @param cmd the command to launch (with parameters) 68 * @param initialText initial text in the editor buffer 69 * @param errorHandler handler for error messages 70 * @param saveHandler handler sent the buffer contents on save 71 * @param suspendInteractiveInput a callback to suspend caller (shell) input 72 * @param resumeInteractiveInput a callback to resume caller input 73 * @param wait true, if editor process termination cannot be used to 74 * determine when done 75 * @param promptForNewLineToEndWait a callback to prompt for newline if 76 * wait==true 77 */ edit(String[] cmd, String initialText, Consumer<String> errorHandler, Consumer<String> saveHandler, Runnable suspendInteractiveInput, Runnable resumeInteractiveInput, boolean wait, Runnable promptForNewLineToEndWait)78 public static void edit(String[] cmd, String initialText, 79 Consumer<String> errorHandler, 80 Consumer<String> saveHandler, 81 Runnable suspendInteractiveInput, 82 Runnable resumeInteractiveInput, 83 boolean wait, 84 Runnable promptForNewLineToEndWait) { 85 ExternalEditor ed = new ExternalEditor(errorHandler, saveHandler, suspendInteractiveInput, 86 resumeInteractiveInput, wait, promptForNewLineToEndWait); 87 ed.edit(cmd, initialText); 88 } 89 ExternalEditor(Consumer<String> errorHandler, Consumer<String> saveHandler, Runnable suspendInteractiveInput, Runnable resumeInteractiveInput, boolean wait, Runnable promptForNewLineToEndWait)90 ExternalEditor(Consumer<String> errorHandler, 91 Consumer<String> saveHandler, 92 Runnable suspendInteractiveInput, 93 Runnable resumeInteractiveInput, 94 boolean wait, 95 Runnable promptForNewLineToEndWait) { 96 this.errorHandler = errorHandler; 97 this.saveHandler = saveHandler; 98 this.wait = wait; 99 this.suspendInteractiveInput = suspendInteractiveInput; 100 this.resumeInteractiveInput = resumeInteractiveInput; 101 this.promptForNewLineToEndWait = promptForNewLineToEndWait; 102 } 103 edit(String[] cmd, String initialText)104 private void edit(String[] cmd, String initialText) { 105 try { 106 setupWatch(initialText); 107 launch(cmd); 108 } catch (IOException ex) { 109 errorHandler.accept(ex.getMessage()); 110 } finally { 111 deleteDirectory(); 112 } 113 } 114 115 /** 116 * Creates a WatchService and registers the given directory 117 */ setupWatch(String initialText)118 private void setupWatch(String initialText) throws IOException { 119 this.watcher = FileSystems.getDefault().newWatchService(); 120 this.dir = Files.createTempDirectory("extedit"); 121 this.tmpfile = Files.createTempFile(dir, null, ".java"); 122 Files.write(tmpfile, initialText.getBytes(Charset.forName("UTF-8"))); 123 dir.register(watcher, 124 ENTRY_CREATE, 125 ENTRY_DELETE, 126 ENTRY_MODIFY); 127 watchedThread = new Thread(() -> { 128 for (;;) { 129 WatchKey key; 130 try { 131 key = watcher.take(); 132 } catch (ClosedWatchServiceException ex) { 133 // The watch service has been closed, we are done 134 break; 135 } catch (InterruptedException ex) { 136 // tolerate an interrupt 137 continue; 138 } 139 140 if (!key.pollEvents().isEmpty()) { 141 saveFile(); 142 } 143 144 boolean valid = key.reset(); 145 if (!valid) { 146 // The watch service has been closed, we are done 147 break; 148 } 149 } 150 }); 151 watchedThread.start(); 152 } 153 launch(String[] cmd)154 private void launch(String[] cmd) throws IOException { 155 String[] params = Arrays.copyOf(cmd, cmd.length + 1); 156 params[cmd.length] = tmpfile.toString(); 157 ProcessBuilder pb = new ProcessBuilder(params); 158 pb = pb.inheritIO(); 159 160 try { 161 suspendInteractiveInput.run(); 162 Process process = pb.start(); 163 // wait to exit edit mode in one of these ways... 164 if (wait) { 165 // -wait option -- ignore process exit, wait for carriage-return 166 Scanner scanner = new Scanner(System.in); 167 promptForNewLineToEndWait.run(); 168 scanner.nextLine(); 169 } else { 170 // wait for process to exit 171 process.waitFor(); 172 } 173 } catch (IOException ex) { 174 errorHandler.accept("process IO failure: " + ex.getMessage()); 175 } catch (InterruptedException ex) { 176 errorHandler.accept("process interrupt: " + ex.getMessage()); 177 } finally { 178 try { 179 watcher.close(); 180 watchedThread.join(); //so that saveFile() is finished. 181 saveFile(); 182 } catch (InterruptedException ex) { 183 errorHandler.accept("process interrupt: " + ex.getMessage()); 184 } finally { 185 resumeInteractiveInput.run(); 186 } 187 } 188 } 189 saveFile()190 private void saveFile() { 191 try { 192 saveHandler.accept(Files.lines(tmpfile).collect(Collectors.joining("\n", "", "\n"))); 193 } catch (IOException ex) { 194 errorHandler.accept("Failure in read edit file: " + ex.getMessage()); 195 } 196 } 197 deleteDirectory()198 private void deleteDirectory() { 199 try { 200 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { 201 @Override 202 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 203 throws IOException { 204 Files.delete(file); 205 return FileVisitResult.CONTINUE; 206 } 207 208 @Override 209 public FileVisitResult postVisitDirectory(Path directory, IOException fail) 210 throws IOException { 211 if (fail == null) { 212 Files.delete(directory); 213 return FileVisitResult.CONTINUE; 214 } 215 throw fail; 216 } 217 }); 218 } catch (IOException exc) { 219 // ignore: The end-user will not want to see this, it is in a temp 220 // directory so it will go away eventually, and tests verify that 221 // the deletion is occurring. 222 } 223 } 224 } 225