package org.simantics.acorn.internal; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileLock; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import java.util.Properties; import java.util.stream.Stream; import org.simantics.acorn.GraphClientImpl2; import org.simantics.db.Database; import org.simantics.db.DatabaseUserAgent; import org.simantics.db.ServiceLocator; import org.simantics.db.server.DatabaseStartException; import org.simantics.db.server.ProCoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import fi.vtt.simantics.procore.internal.StaticSessionProperties; /** * @author Tuukka Lehtonen */ public class AcornDatabase implements Database { private static final Logger LOGGER = LoggerFactory.getLogger(AcornDatabase.class); private static final String LOCK_FILE_NAME = "lock"; private final Path folder; private final Path lockFile; private GraphClientImpl2 currentClient; private DatabaseUserAgent userAgent; private RandomAccessFile raLockFile; private FileLock lock; private boolean isRunning; public AcornDatabase(Path folder) { this.folder = folder; this.lockFile = folder.resolve(LOCK_FILE_NAME); } @Override public DatabaseUserAgent getUserAgent() { return userAgent; } @Override public void setUserAgent(DatabaseUserAgent dbUserAgent) { userAgent = dbUserAgent; } @Override public Status getStatus() { return Status.Local; } @Override public File getFolder() { return folder.toFile(); } @Override public boolean isFolderOk() { return isFolderOk(folder.toFile()); } @Override public boolean isFolderOk(File aFolder) { if (!aFolder.isDirectory()) return false; return true; } @Override public boolean isFolderEmpty() { return isFolderEmpty(folder.toFile()); } @Override public boolean isFolderEmpty(File aFolder) { Path path = aFolder.toPath(); if (!Files.isDirectory(path)) return false; try (DirectoryStream folderStream = Files.newDirectoryStream(path)) { return !folderStream.iterator().hasNext(); } catch (IOException e) { LOGGER.error("Failed to open folder stream. folder=" + path, e); return false; } } @Override public void initFolder(Properties properties) throws ProCoreException { try { Files.createDirectories(folder); } catch (IOException e) { throw new ProCoreException(e); } } @Override public void deleteFiles() throws ProCoreException { deleteTree(folder); File vgPath = StaticSessionProperties.virtualGraphStoragePath; if (vgPath != null) { try (Stream vgs = Files.list(vgPath.toPath())) { for (Path p : vgs.toArray(Path[]::new)) deleteTree(p); } catch (IOException e) { throw new ProCoreException(e); } } } @Override public synchronized void start() throws ProCoreException { try { raLockFile = new RandomAccessFile(lockFile.toFile(), "rw"); lock = raLockFile.getChannel().tryLock(); if (lock == null) { safeLoggingClose(raLockFile, lockFile); throw new ProCoreException("The database in folder " + folder.toAbsolutePath() + " is already in use!"); } isRunning = true; } catch (IOException e) { LOGGER.error("Failed to start database at " + folder.toAbsolutePath(), e); safeLoggingClose(raLockFile, lockFile); throw new ProCoreException("Failed to start database at " + folder.toAbsolutePath(), e); } } @Override public boolean isRunning() throws ProCoreException { return isRunning; } @Override public synchronized boolean tryToStop() throws ProCoreException { if (!isRunning) return false; try { safeLoggingClose(lock, lockFile); lock = null; safeLoggingClose(raLockFile, lockFile); raLockFile = null; Files.deleteIfExists(lockFile); isRunning = false; safeLoggingClose(currentClient, currentClient.getDbFolder()); currentClient = null; } catch (IOException e) { LOGGER.error("Failed to start database at " + folder.toAbsolutePath(), e); } return true; } @Override public void connect() throws ProCoreException { } @Override public boolean isConnected() throws ProCoreException { return isRunning; } @Override public String execute(String command) throws ProCoreException { throw new UnsupportedOperationException("execute(" + command + ")"); } @Override public void disconnect() throws ProCoreException { } @Override public void clone(File to, int revision, boolean saveHistory) throws ProCoreException { // TODO: implement throw new UnsupportedOperationException(); } @Override public Path createFromChangeSets(int revision) throws ProCoreException { // TODO: implement throw new UnsupportedOperationException(); } @Override public void deleteGuard() throws ProCoreException { // TODO: implement throw new UnsupportedOperationException(); } @Override public Path dumpChangeSets() throws ProCoreException { // TODO: implement throw new UnsupportedOperationException(); } @Override public void purgeDatabase() throws ProCoreException { if(currentClient == null) throw new IllegalStateException("No current session."); currentClient.purgeDatabase(); } @Override public long serverGetTailChangeSetId() throws ProCoreException { if(currentClient == null) throw new IllegalStateException("No current session."); return currentClient.getTailChangeSetId(); } @Override public Session newSession(ServiceLocator locator) throws ProCoreException { try { if(currentClient != null) throw new DatabaseStartException(folder.toFile(), "A session is already running. Only one session is supported."); currentClient = new GraphClientImpl2(this, folder, locator); return currentClient; } catch (IOException e) { throw new ProCoreException(e); } } @Override public Journal getJournal() throws ProCoreException { // TODO: implement throw new UnsupportedOperationException(); } static class Visitor extends SimpleFileVisitor { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { try { Files.delete(file); } catch (IOException ioe) { LOGGER.error("Failed to delete file {}", file, ioe); throw ioe; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { if (e == null) { try { Files.delete(dir); } catch (IOException ioe) { LOGGER.error("Failed to delete directory {}", dir, ioe); throw ioe; } return FileVisitResult.CONTINUE; } throw e; } } private static void deleteTree(Path path) throws ProCoreException { if (!Files.exists(path)) return; try { Files.walkFileTree(path, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new Visitor()); } catch (IOException e) { throw new ProCoreException("Could not delete " + path, e); } } @Override public String getCompression() { return "LZ4"; } private static void safeLoggingClose(AutoCloseable closeable, Path file) { if (closeable == null) return; try (AutoCloseable c = closeable) { } catch (Exception e) { LOGGER.error("Failed to close " + closeable.getClass() + " of " + file.toAbsolutePath(), e); } } private static void safeLoggingClose(Database.Session session, Path file) { if (session == null) return; try { session.close(); } catch (Exception e) { LOGGER.error("Failed to close " + session.getClass() + " of " + file.toAbsolutePath(), e); } } }