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