package org.simantics.document.ui.graphfile; import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; import static java.nio.file.StandardWatchEventKinds.OVERFLOW; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.ui.PartInitException; import org.simantics.Simantics; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.common.request.UniqueRead; import org.simantics.db.exception.DatabaseException; import org.simantics.document.DocumentResource; import org.simantics.document.ui.Activator; import org.simantics.editors.Editors; import org.simantics.graphfile.util.GraphFileUtil; import org.simantics.layer0.Layer0; import org.simantics.ui.workbench.editor.AbstractResourceEditorAdapter; import org.simantics.ui.workbench.editor.EditorAdapter; import org.simantics.utils.ui.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ExternalEditorAdapter extends AbstractResourceEditorAdapter implements EditorAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(ExternalEditorAdapter.class); private static final Map tempFiles = new ConcurrentHashMap<>(); private static ExternalFileWatcher fileWatcher; public ExternalEditorAdapter() { super(Messages.ExternalEditorAdapter_ExternalEditor, Activator.imageDescriptorFromPlugin("com.famfamfam.silk", "icons/page.png")); //$NON-NLS-2$ //$NON-NLS-3$ } @Override public boolean canHandle(ReadGraph g, Resource r) throws DatabaseException { DocumentResource doc = DocumentResource.getInstance(g); if (!g.isInstanceOf(r, doc.FileDocument)) return false; return true; } @Override protected void openEditor(final Resource input) throws Exception { // ensure watching watch(); Path path = null; for (Entry entry : tempFiles.entrySet()) { Resource value = entry.getValue(); if (input.equals(value)) { path = entry.getKey(); } } if (path == null) { path = Simantics.getSession().syncRequest(new UniqueRead() { @Override public Path perform(ReadGraph graph) throws DatabaseException { StringBuilder sb = new StringBuilder(); String uri = graph.getPossibleURI(input); if (uri != null) { sb.append(generateSHA1(uri)).append("_"); //$NON-NLS-1$ } sb.append(graph.getRelatedValue(input, Layer0.getInstance(graph).HasName).toString()); Path filePath = Activator.getInstanceLocation().resolve(sb.toString()); tempFiles.computeIfAbsent(filePath, t -> { try { GraphFileUtil.writeDataToFile(graph, input, filePath.toFile()); } catch (IOException | DatabaseException e) { LOGGER.error("Could not write data to file ", e); //$NON-NLS-1$ } return input; }); LOGGER.info("Adding tempFile {} with resource {}", filePath, input); //$NON-NLS-1$ return filePath; } }); } try { Editors.openExternalEditor(path.toFile()); } catch (PartInitException e) { ExceptionUtils.logAndShowError(e); } } public static String generateSHA1(String message) { return hashString(message, "SHA-1"); //$NON-NLS-1$ } private static String hashString(String message, String algorithm) { try { MessageDigest digest = MessageDigest.getInstance(algorithm); byte[] hashedBytes = digest.digest(message.getBytes("UTF-8")); //$NON-NLS-1$ return convertByteArrayToHexString(hashedBytes); } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) { // Should not happen LOGGER.error("Could not generate hash", ex); //$NON-NLS-1$ } return ""; //$NON-NLS-1$ } private static String convertByteArrayToHexString(byte[] arrayBytes) { StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < arrayBytes.length; i++) { stringBuffer.append(Integer.toString((arrayBytes[i] & 0xff) + 0x100, 16) .substring(1)); } return stringBuffer.toString(); } private static void watch() throws IOException { if (fileWatcher == null) { synchronized (ExternalEditorAdapter.class) { if (fileWatcher == null) { fileWatcher = new ExternalFileWatcher(); } } } } static class ExternalFileWatcher{ private ExecutorService service = Executors.newFixedThreadPool(1, r -> { Thread t = new Thread(r); t.setDaemon(true); return t; }); private final WatchService ws; private final AtomicBoolean stopped = new AtomicBoolean(true); private ConcurrentHashMap keys = new ConcurrentHashMap<>(); public ExternalFileWatcher() throws IOException { ws = FileSystems.getDefault().newWatchService(); register(Activator.getInstanceLocation()); service.submit(() -> { stopped.set(false); while (!stopped.get()) { try { WatchKey key = ws.take(); for (WatchEvent watchEvent : key.pollEvents()) { if (OVERFLOW == watchEvent.kind()) continue; // loop @SuppressWarnings("unchecked") WatchEvent pathEvent = (WatchEvent) watchEvent; Kind kind = pathEvent.kind(); Path parent = keys.get(key); Path newPath = parent.resolve(pathEvent.context()); if (ENTRY_MODIFY == kind) { LOGGER.info("New path modified: " + newPath); //$NON-NLS-1$ Resource resource = tempFiles.get(newPath); if (resource != null) { GraphFileUtil.writeDataToGraph(newPath.toFile(), resource); } else { LOGGER.warn("No resource found for {}", newPath.toAbsolutePath()); //$NON-NLS-1$ } } else if (ENTRY_DELETE == kind) { System.out.println("New path deleted: " + newPath); //$NON-NLS-1$ } } if (!key.reset()) { keys.remove(key); // break; // loop } } catch (InterruptedException e) { if (!stopped.get()) LOGGER.error("Could not stop", e); //$NON-NLS-1$ } catch (Throwable t) { LOGGER.error("An error occured", t); //$NON-NLS-1$ } } }); } public void stop() { stopped.set(true); } private void register(Path path) throws IOException { if (Files.isDirectory(path)) { LOGGER.info("Registering path {}", path); //$NON-NLS-1$ WatchKey key = path.toAbsolutePath().register(ws, ENTRY_DELETE, ENTRY_MODIFY); keys.put(key, path); } } } public static void stopFileWatcher() { tempFiles.clear(); if (fileWatcher != null) { fileWatcher.stop(); fileWatcher = null; } } }