package co.codewizards.cloudstore.local; import static co.codewizards.cloudstore.core.io.StreamUtil.*; import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.DerbyUtil.*; import static co.codewizards.cloudstore.core.util.StringUtil.*; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; import java.util.SortedSet; import java.util.Timer; import java.util.TimerTask; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.jdo.JDOHelper; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.io.LockFile; import co.codewizards.cloudstore.core.io.LockFileFactory; import co.codewizards.cloudstore.core.io.TimeoutException; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.progress.ProgressMonitor; import co.codewizards.cloudstore.core.progress.SubProgressMonitor; import co.codewizards.cloudstore.core.repo.local.CreateRepositoryContext; import co.codewizards.cloudstore.core.repo.local.FileAlreadyRepositoryException; import co.codewizards.cloudstore.core.repo.local.FileNoDirectoryException; import co.codewizards.cloudstore.core.repo.local.FileNoRepositoryException; import co.codewizards.cloudstore.core.repo.local.FileNotFoundException; import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper; import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerCloseEvent; import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerCloseListener; import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerException; import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; import co.codewizards.cloudstore.core.repo.local.LocalRepoMetaData; import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistry; import co.codewizards.cloudstore.core.repo.local.LocalRepoRegistryImpl; import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction; import co.codewizards.cloudstore.core.repo.local.RepositoryCorruptException; import co.codewizards.cloudstore.core.util.AssertUtil; import co.codewizards.cloudstore.core.util.IOUtil; import co.codewizards.cloudstore.core.util.PropertiesUtil; import co.codewizards.cloudstore.local.db.DatabaseAdapter; import co.codewizards.cloudstore.local.db.DatabaseAdapterFactoryRegistry; import co.codewizards.cloudstore.local.persistence.CloudStorePersistenceCapableClassesProvider; import co.codewizards.cloudstore.local.persistence.Directory; import co.codewizards.cloudstore.local.persistence.LocalRepository; import co.codewizards.cloudstore.local.persistence.LocalRepositoryDao; import co.codewizards.cloudstore.local.persistence.RemoteRepository; import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDao; import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequest; import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequestDao; /** * Manager of a repository. * <p> * All operations on a repository are performed via this manager (or an object associated with it). * <p> * For every repository (identified by its root directory) there is one single instance. Use the * {@link LocalRepoManagerFactory} to obtain a {@code LocalRepoManager}. * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co */ class LocalRepoManagerImpl implements LocalRepoManager { private static final Logger logger = LoggerFactory.getLogger(LocalRepoManagerImpl.class); protected final String id = Integer.toHexString(System.identityHashCode(this)); private long closeDeferredMillis = Long.MIN_VALUE; private static final long lockTimeoutMillis = 30000; // TODO make configurable! private static final long remoteRepositoryRequestExpiryAge = 24 * 60 * 60 * 1000L; private final File localRoot; private LockFile lockFile; private UUID repositoryId; /** * The properties read from {@link LocalRepoManager#REPOSITORY_PROPERTIES_FILE_NAME}. * <p> * <b>Important:</b> This is only assigned from {@link #createRepositoryPropertiesFile()} / {@link #checkRepositoryPropertiesFile()} * until {@link #updateRepositoryPropertiesFile()}. Afterwards, it is <code>null</code> again! */ private Properties repositoryProperties; private PersistenceManagerFactory persistenceManagerFactory; private final AtomicInteger openReferenceCounter = new AtomicInteger(); private final List<LocalRepoManagerCloseListener> localRepoManagerCloseListeners = new CopyOnWriteArrayList<LocalRepoManagerCloseListener>(); private String connectionURL; private boolean deleteMetaDir; private int closeDeferredTimerSerial; private Timer closeDeferredTimer; private TimerTask closeDeferredTimerTask; private final Lock lock = new ReentrantLock(); private LocalRepoMetaDataImpl localRepoMetaDataImpl; private final Timer deleteExpiredRemoteRepositoryRequestsTimer = new Timer("deleteExpiredRemoteRepositoryRequestsTimer-" + id, true); private final TimerTask deleteExpiredRemoteRepositoryRequestsTimeTask = new TimerTask() { @Override public void run() { if (isOpen()) deleteExpiredRemoteRepositoryRequests(); } }; { deleteExpiredRemoteRepositoryRequestsTimer.schedule( deleteExpiredRemoteRepositoryRequestsTimeTask, 60 * 60 * 1000L, 60 * 60 * 1000L); // TODO make times configurable } private byte[] privateKey; private byte[] publicKey; private boolean closing; private boolean closeAbortable = true; protected LocalRepoManagerImpl(final File localRoot, final boolean createRepository) throws LocalRepoManagerException { logger.info("[{}]<init>: localRoot='{}'", id, localRoot); this.localRoot = assertValidLocalRoot(localRoot); boolean releaseLockFile = true; deleteMetaDir = false; // only delete, if it is created in initMetaDirectory(...) try { // TODO Make this more robust: If we have a power-outage between directory creation and the finally block, // we end in an inconsistent state. We can avoid this by tracking the creation process in a properties // file later (somehow making this really transactional). if (createRepository) { repositoryId = CreateRepositoryContext.repositoryIdThreadLocal.get(); if (repositoryId == null) repositoryId = UUID.randomUUID(); } initMetaDir(createRepository); if (repositoryId == null) repositoryId = readRepositoryIdFromRepositoryPropertiesFile(); initPersistenceManagerFactory(createRepository); deleteExpiredRemoteRepositoryRequests(); syncWithLocalRepoRegistry(); updateRepositoryPropertiesFile(); releaseLockFile = false; deleteMetaDir = false; // if we come here, creation is successful => NO deletion } finally { if (releaseLockFile && lockFile != null) lockFile.release(); if (deleteMetaDir) { IOUtil.deleteDirectoryRecursively(getMetaDir()); // if (repositoryId != null) // TODO should be removed - will be evicted after some time, anyway, but // LocalRepoRegistry.getInstance().removeRepository(repositoryId); } } } private UUID readRepositoryIdFromRepositoryPropertiesFile() { final File repositoryPropertiesFile = createFile(getMetaDir(), REPOSITORY_PROPERTIES_FILE_NAME); try { final Properties repositoryProperties = new Properties(); try (InputStream in = castStream(repositoryPropertiesFile.createInputStream())) { repositoryProperties.load(in); } final String repositoryIdStr = repositoryProperties.getProperty(PROP_REPOSITORY_ID); if (isEmpty(repositoryIdStr)) throw new IllegalStateException("repositoryProperties.getProperty(PROP_REPOSITORY_ID) is empty!"); final UUID repositoryId = UUID.fromString(repositoryIdStr); return repositoryId; } catch (Exception x) { throw new RuntimeException("Reading readRepositoryId from '" + repositoryPropertiesFile.getAbsolutePath() + "' failed: " + x, x); } } @Override public void putRepositoryAlias(final String repositoryAlias) { AssertUtil.assertNotNull(repositoryAlias, "repositoryAlias"); final LocalRepoTransactionImpl transaction = beginWriteTransaction(); try { final LocalRepository localRepository = transaction.getDao(LocalRepositoryDao.class).getLocalRepositoryOrFail(); if (!localRepository.getAliases().contains(repositoryAlias)) localRepository.getAliases().add(repositoryAlias); LocalRepoRegistryImpl.getInstance().putRepositoryAlias(repositoryAlias, repositoryId); transaction.commit(); } finally { transaction.rollbackIfActive(); } } @Override public void removeRepositoryAlias(final String repositoryAlias) { AssertUtil.assertNotNull(repositoryAlias, "repositoryAlias"); final LocalRepoTransactionImpl transaction = beginWriteTransaction(); try { final LocalRepository localRepository = transaction.getDao(LocalRepositoryDao.class).getLocalRepositoryOrFail(); localRepository.getAliases().remove(repositoryAlias); LocalRepoRegistryImpl.getInstance().removeRepositoryAlias(repositoryAlias); transaction.commit(); } finally { transaction.rollbackIfActive(); } } private File assertValidLocalRoot(final File localRoot) { AssertUtil.assertNotNull(localRoot, "localRoot"); if (!localRoot.isAbsolute()) throw new IllegalArgumentException("localRoot is not absolute."); if (!localRoot.exists()) throw new FileNotFoundException(localRoot); if (!localRoot.isDirectory()) throw new FileNoDirectoryException(localRoot); assertNotInsideOtherRepository(localRoot); assertNotContainingOtherRepository(localRoot); return localRoot; } private void assertNotInsideOtherRepository(final File localRoot) { final File localRootFound = LocalRepoHelper.getLocalRootContainingFile(localRoot); if (localRootFound != null && !localRootFound.equals(localRoot)) throw new FileAlreadyRepositoryException(localRoot); } private void assertNotContainingOtherRepository(final File localRoot) { final File localRootFound = LocalRepoHelper.getLocalRootContainingFile(localRoot); if (localRootFound != null && localRootFound.equals(localRoot)) return; final Collection<File> localRootsFound = LocalRepoHelper.getLocalRootsContainedInDirectory(localRoot); if (! localRootsFound.isEmpty()) throw new FileAlreadyRepositoryException(localRoot); } private void initMetaDir(final boolean createRepository) throws LocalRepoManagerException { final File metaDir = getMetaDir(); if (createRepository) { if (metaDir.exists()) { throw new FileAlreadyRepositoryException(localRoot); } deleteMetaDir = true; metaDir.mkdir(); initLockFile(); createRepositoryPropertiesFile(); try { try (DatabaseAdapter databaseAdapter = DatabaseAdapterFactoryRegistry.getInstance().createDatabaseAdapter();) { databaseAdapter.setRepositoryId(assertNotNull(repositoryId, "repositoryId")); databaseAdapter.setLocalRoot(assertNotNull(localRoot, "localRoot")); databaseAdapter.createPersistencePropertiesFileAndDatabase(); } } catch (Exception x) { throw new LocalRepoManagerException(x); } } else { if (!metaDir.exists()) throw new FileNoRepositoryException(localRoot); initLockFile(); checkRepositoryPropertiesFile(); } } private void initLockFile() { final File lock = createFile(getMetaDir(), REPOSITORY_LOCK_FILE_NAME); try { lockFile = LockFileFactory.getInstance().acquire(lock, 100); } catch (final TimeoutException x) { logger.warn("[{}]initLockFile: Repository '{}' is currently locked by another process. Will wait {} ms for it...", id, localRoot, lockTimeoutMillis); } if (lockFile == null) { lockFile = LockFileFactory.getInstance().acquire(lock, lockTimeoutMillis); } logger.info("[{}]initLockFile: Repository '{}' locked successfully.", id, localRoot); } private void createRepositoryPropertiesFile() { final int version = 2; final File repositoryPropertiesFile = createFile(getMetaDir(), REPOSITORY_PROPERTIES_FILE_NAME); try { repositoryProperties = new Properties(); repositoryProperties.setProperty(PROP_REPOSITORY_ID, assertNotNull(repositoryId, "repositoryId").toString()); repositoryProperties.setProperty(PROP_VERSION, Integer.valueOf(version).toString()); PropertiesUtil.store(repositoryPropertiesFile, repositoryProperties, null); } catch (final IOException e) { throw new RuntimeException(e); } } private void checkRepositoryPropertiesFile() throws LocalRepoManagerException { final File repositoryPropertiesFile = createFile(getMetaDir(), REPOSITORY_PROPERTIES_FILE_NAME); if (!repositoryPropertiesFile.exists()) throw new RepositoryCorruptException(localRoot, String.format("Meta-directory does not contain '%s'!", REPOSITORY_PROPERTIES_FILE_NAME)); try { repositoryProperties = PropertiesUtil.load(repositoryPropertiesFile); } catch (final IOException e) { throw new RuntimeException(e); } String version = repositoryProperties.getProperty(PROP_VERSION); if (version == null || version.isEmpty()) throw new RepositoryCorruptException(localRoot, String.format("Meta-file '%s' does not contain property '%s'!", REPOSITORY_PROPERTIES_FILE_NAME, PROP_VERSION)); version = version.trim(); int ver; try { ver = Integer.parseInt(version); } catch (final NumberFormatException x) { throw new RepositoryCorruptException(localRoot, String.format("Meta-file '%s' contains an illegal value (not a number) for property '%s'!", REPOSITORY_PROPERTIES_FILE_NAME, PROP_VERSION)); } // Because version 1 was used by 0.9.0, we do not provide compatibility, yet. Maybe we add compatibility // code converting version 1 into 2, later. // Further, this check prevents old versions to work with a newer repo (and possibly corrupt it). if (ver != 2) throw new RepositoryCorruptException(localRoot, "Repository is not version 2!"); } private void syncWithLocalRepoRegistry() { AssertUtil.assertNotNull(repositoryId, "repositoryId"); final LocalRepoRegistry localRepoRegistry = LocalRepoRegistryImpl.getInstance(); localRepoRegistry.putRepository(repositoryId, localRoot); final LocalRepoTransactionImpl transaction = beginWriteTransaction(); try { final LocalRepository localRepository = transaction.getDao(LocalRepositoryDao.class).getLocalRepositoryOrFail(); for (final String repositoryAlias : new ArrayList<>(localRepository.getAliases())) { final UUID repositoryIdInRegistry = localRepoRegistry.getRepositoryId(repositoryAlias); if (repositoryIdInRegistry == null) { localRepoRegistry.putRepositoryAlias(repositoryAlias, repositoryId); logger.info("syncWithLocalRepoRegistry: Alias '{}' of repository '{}' copied from repo to repoRegistry.", repositoryAlias, repositoryId); } else if (repositoryId.equals(repositoryIdInRegistry)) { logger.debug("syncWithLocalRepoRegistry: Alias '{}' of repository '{}' already in-sync.", repositoryAlias, repositoryId); } else { localRepository.getAliases().remove(repositoryAlias); logger.warn("syncWithLocalRepoRegistry: Alias '{}' removed from repository '{}', because of conflicting entry (repository '{}') in localRepoRegistry.", repositoryAlias, repositoryId, repositoryIdInRegistry); } } final Collection<String> repositoryAliases = localRepoRegistry.getRepositoryAliases(repositoryId.toString()); if (repositoryAliases != null) { for (final String repositoryAlias : repositoryAliases) { if (!localRepository.getAliases().contains(repositoryAlias)) { localRepository.getAliases().add(repositoryAlias); logger.info("syncWithLocalRepoRegistry: Alias '{}' of repository '{}' copied from repoRegistry to repo.", repositoryAlias, repositoryId); } } } transaction.commit(); } finally { transaction.rollbackIfActive(); } } private void updateRepositoryPropertiesFile() { AssertUtil.assertNotNull(repositoryProperties, "repositoryProperties"); final File repositoryPropertiesFile = createFile(getMetaDir(), REPOSITORY_PROPERTIES_FILE_NAME); try { boolean store = false; final String repositoryId = AssertUtil.assertNotNull(getRepositoryId(), "repositoryId").toString(); if (!repositoryId.equals(repositoryProperties.getProperty(PROP_REPOSITORY_ID))) { repositoryProperties.setProperty(PROP_REPOSITORY_ID, repositoryId); store = true; } final LocalRepoTransactionImpl transaction = beginReadTransaction(); try { final LocalRepository localRepository = transaction.getDao(LocalRepositoryDao.class).getLocalRepositoryOrFail(); final SortedSet<String> repositoryAliases = new TreeSet<>(localRepository.getAliases()); final String aliasesString = repositoryAliasesToString(repositoryAliases); if (!aliasesString.equals(repositoryProperties.getProperty(PROP_REPOSITORY_ALIASES))) { repositoryProperties.setProperty(PROP_REPOSITORY_ALIASES, aliasesString); store = true; } transaction.commit(); } finally { transaction.rollbackIfActive(); } if (store) PropertiesUtil.store(repositoryPropertiesFile, repositoryProperties, null); repositoryProperties = null; // not needed anymore => gc } catch (final IOException e) { throw new RuntimeException(e); } } private String repositoryAliasesToString(final Set<String> repositoryAliases) { AssertUtil.assertNotNull(repositoryAliases, "repositoryAliases"); final StringBuilder sb = new StringBuilder(); sb.append('/'); for (final String repositoryAlias : repositoryAliases) { sb.append(repositoryAlias); sb.append('/'); } return sb.toString(); } private void initPersistenceManagerFactory(final boolean createRepository) throws LocalRepoManagerException { logger.debug("[{}]initPersistenceManagerFactory: Starting up PersistenceManagerFactory...", id); final long beginTimestamp = System.currentTimeMillis(); // initPersistenceManagerFactoryAndPersistenceCapableClassesWithRetry(createRepository); initPersistenceManagerFactoryAndPersistenceCapableClasses(createRepository); final PersistenceManager pm = persistenceManagerFactory.getPersistenceManager(); try { pm.currentTransaction().begin(); if (createRepository) { createAndPersistLocalRepository(pm); } else { assertSinglePersistentLocalRepository(pm); } logger.info("[{}]initPersistenceManagerFactory: repositoryId={}", id, repositoryId); pm.currentTransaction().commit(); } finally { if (pm.currentTransaction().isActive()) pm.currentTransaction().rollback(); pm.close(); } logger.info("[{}]initPersistenceManagerFactory: Started up PersistenceManagerFactory successfully in {} ms.", id, System.currentTimeMillis() - beginTimestamp); } private void deleteExpiredRemoteRepositoryRequests() { lock.lock(); try { final PersistenceManager pm = persistenceManagerFactory.getPersistenceManager(); try { pm.currentTransaction().begin(); final RemoteRepositoryRequestDao dao = new RemoteRepositoryRequestDao().persistenceManager(pm); final Collection<RemoteRepositoryRequest> expiredRequests = dao.getRemoteRepositoryRequestsChangedBefore(new Date(System.currentTimeMillis() - remoteRepositoryRequestExpiryAge)); pm.deletePersistentAll(expiredRequests); pm.currentTransaction().commit(); } finally { if (pm.currentTransaction().isActive()) pm.currentTransaction().rollback(); pm.close(); } } finally { lock.unlock(); } } private void initPersistenceManagerFactoryAndPersistenceCapableClasses(final boolean createRepository) { final Map<String, String> persistenceProperties = getPersistenceProperties(createRepository); persistenceManagerFactory = JDOHelper.getPersistenceManagerFactory(persistenceProperties); final PersistenceManager pm = persistenceManagerFactory.getPersistenceManager(); try { try { initPersistenceCapableClasses(pm); } catch (final Exception x) { if (x instanceof RuntimeException) throw (RuntimeException)x; else throw new RuntimeException(x); } } finally { if (pm != null) pm.close(); } } private void initPersistenceCapableClasses(final PersistenceManager pm) { final ServiceLoader<CloudStorePersistenceCapableClassesProvider> sl = ServiceLoader.load(CloudStorePersistenceCapableClassesProvider.class); for (final Iterator<CloudStorePersistenceCapableClassesProvider> it = sl.iterator(); it.hasNext(); ) { final CloudStorePersistenceCapableClassesProvider provider = it.next(); final Class<?>[] classes = provider.getPersistenceCapableClasses(); if (classes != null) { for (Class<?> clazz : classes) { final Class<?> c = getExtendingClass(clazz); pm.getExtent(c); } } } } private void assertSinglePersistentLocalRepository(final PersistenceManager pm) { try { final LocalRepository localRepository = new LocalRepositoryDao().persistenceManager(pm).getLocalRepositoryOrFail(); readRepositoryMainProperties(localRepository); } catch (final IllegalStateException x) { throw new RepositoryCorruptException(localRoot, x.getMessage()); } } private void createAndPersistLocalRepository(final PersistenceManager pm) { LocalRepository localRepository = createObject(LocalRepository.class, assertNotNull(repositoryId, "repositoryId")); final Directory root = createObject(Directory.class); root.setName(""); root.setLastModified(new Date(localRoot.lastModified())); localRepository.setRoot(root); generatePublicPrivateKey(localRepository); localRepository = pm.makePersistent(localRepository); readRepositoryMainProperties(localRepository); } private void readRepositoryMainProperties(final LocalRepository localRepository) { assertNotNull(localRepository, "localRepository"); repositoryId = assertNotNull(localRepository.getRepositoryId(), "localRepository.repositoryId"); publicKey = assertNotNull(localRepository.getPublicKey(), "localRepository.publicKey"); privateKey = assertNotNull(localRepository.getPrivateKey(), "localRepository.privateKey"); } private static final String KEY_STORE_PASSWORD_STRING = "CloudStore-key-store"; private static final char[] KEY_STORE_PASSWORD_CHAR_ARRAY = KEY_STORE_PASSWORD_STRING.toCharArray(); private final SecureRandom random = new SecureRandom(); private void generatePublicPrivateKey(final LocalRepository localRepository) { try { final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null, KEY_STORE_PASSWORD_CHAR_ARRAY); final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(getKeySize(), random); final KeyPair pair = keyGen.generateKeyPair(); localRepository.setPrivateKey(pair.getPrivate().getEncoded()); localRepository.setPublicKey(pair.getPublic().getEncoded()); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } } private File getMetaDir() { return createFile(localRoot, META_DIR_NAME); } private Map<String, String> getPersistenceProperties(final boolean createRepository) { final Map<String, String> persistenceProperties = new PersistencePropertiesProvider(repositoryId, localRoot).getPersistenceProperties(); // connectionURL = persistenceProperties.get(PersistencePropertiesEnum.CONNECTION_URL_ORIGINAL.key); connectionURL = persistenceProperties.get(PersistencePropertiesEnum.CONNECTION_URL.key); return persistenceProperties; } @Override public File getLocalRoot() { return localRoot; } protected PersistenceManagerFactory getPersistenceManagerFactory() { return persistenceManagerFactory; } @Override public void addLocalRepoManagerCloseListener(final LocalRepoManagerCloseListener listener) { localRepoManagerCloseListeners.add(listener); } @Override public void removeLocalRepoManagerCloseListener(final LocalRepoManagerCloseListener listener) { localRepoManagerCloseListeners.remove(listener); } private class CloseTimerTask extends TimerTask { private volatile boolean cancelled; @Override public void run() { if (cancelled) return; _close(); } @Override public boolean cancel() { cancelled = true; final boolean result = super.cancel(); return result; } }; protected boolean open() { boolean result; lock.lock(); try { logger.debug("[{}]open: closing={} closeAbortable={}", id, closing, closeAbortable); result = !closing || closeAbortable; if (result) { closing = false; closeAbortable = true; if (closeDeferredTimerTask != null) { closeDeferredTimerTask.cancel(); closeDeferredTimerTask = null; } if (closeDeferredTimer != null) { closeDeferredTimer.cancel(); closeDeferredTimer = null; } } } finally { lock.unlock(); } if (result) openReferenceCounter.incrementAndGet(); return result; } protected long getCloseDeferredMillis() { if (closeDeferredMillis < 0) { long closeDeferredMillis = PropertiesUtil.getSystemPropertyValueAsLong( SYSTEM_PROPERTY_CLOSE_DEFERRED_MILLIS, DEFAULT_CLOSE_DEFERRED_MILLIS); if (closeDeferredMillis < 0) { logger.warn("System property '{}': closeDeferredMillis {} is less than 0! Using default {} instead!", SYSTEM_PROPERTY_CLOSE_DEFERRED_MILLIS, closeDeferredMillis, DEFAULT_CLOSE_DEFERRED_MILLIS); closeDeferredMillis = DEFAULT_CLOSE_DEFERRED_MILLIS; } this.closeDeferredMillis = closeDeferredMillis; } return closeDeferredMillis; } @Override public void close() { lock.lock(); try { closing = true; } finally { lock.unlock(); } final int openReferenceCounterValue = openReferenceCounter.decrementAndGet(); if (openReferenceCounterValue > 0) { logger.debug("[{}]close: leaving with openReferenceCounterValue={}", id, openReferenceCounterValue); return; } if (openReferenceCounterValue < 0) { throw new IllegalStateException("openReferenceCounterValue < 0"); } final long closeDeferredMillis = getCloseDeferredMillis(); if (closeDeferredMillis > 0) { logger.info("[{}]close: Deferring shut down of real LocalRepoManager {} ms.", id, closeDeferredMillis); lock.lock(); try { // Because of this error: // Caused by: java.lang.IllegalStateException: Timer already cancelled. // at java.util.Timer.sched(Timer.java:397) ~[na:1.7.0_45] // at java.util.Timer.schedule(Timer.java:193) ~[na:1.7.0_45] // at co.codewizards.cloudstore.core.repo.local.LocalRepoManagerImpl.close(LocalRepoManagerImpl.java:403) ~[co.codewizards.cloudstore.core-1.0.0-SNAPSHOT.jar:na] // and because even when recreating the timer in a catch clause still did not prevent // sometimes tasks to not be called, anymore, we now create a new timer every time. if (closeDeferredTimer == null) closeDeferredTimer = new Timer("closeDeferredTimer-" + id + '-' + Integer.toString(++closeDeferredTimerSerial, 36), true); if (closeDeferredTimerTask == null) { closeDeferredTimerTask = new CloseTimerTask(); closeDeferredTimer.schedule(closeDeferredTimerTask, closeDeferredMillis); } } finally { lock.unlock(); } } else _close(); } private void _close() { lock.lock(); try { if (!closing) { // closing was aborted logger.info("[{}]_close: Closing was aborted. Returning immediately.", id); return; } closeAbortable = false; if (closeDeferredTimerTask != null) { closeDeferredTimerTask.cancel(); closeDeferredTimerTask = null; } } finally { lock.unlock(); } logger.info("[{}]_close: Shutting down real LocalRepoManager.", id); // TODO defer this (don't immediately close) // TODO the timeout should be configurable final LocalRepoManagerCloseEvent event = new LocalRepoManagerCloseEvent(this, this, true); for (final LocalRepoManagerCloseListener listener : localRepoManagerCloseListeners) { listener.preClose(event); } deleteExpiredRemoteRepositoryRequestsTimer.cancel(); lock.lock(); try { if (persistenceManagerFactory != null) { try { persistenceManagerFactory.close(); } catch (final Exception x) { logger.warn("Closing PersistenceManagerFactory failed: " + x, x); } persistenceManagerFactory = null; try { shutdownDerbyDatabase(connectionURL); } catch (final Exception x) { logger.warn("Shutting down Derby database failed: " + x, x); } } if (lockFile != null) { try { lockFile.release(); } catch (final Exception x) { logger.warn("Releasing LockFile failed: " + x, x); } lockFile = null; } } finally { lock.unlock(); } for (final LocalRepoManagerCloseListener listener : localRepoManagerCloseListeners) { listener.postClose(event); } } @Override public UUID getRepositoryId() { return repositoryId; } @Override public byte[] getPublicKey() { return publicKey; } @Override public byte[] getPrivateKey() { return privateKey; } @Override public byte[] getRemoteRepositoryPublicKeyOrFail(final UUID repositoryId) { final byte[] result; final LocalRepoTransactionImpl transaction = beginReadTransaction(); try { final RemoteRepository remoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(repositoryId); result = remoteRepository.getPublicKey(); transaction.commit(); } finally { transaction.rollbackIfActive(); } return result; } @Override public boolean isOpen() { lock.lock(); try { return persistenceManagerFactory != null; } finally { lock.unlock(); } } protected void assertOpen() { if (!isOpen()) throw new IllegalStateException("This LocalRepoManagerImpl is closed!"); } @Override public LocalRepoTransactionImpl beginReadTransaction() { lock.lock(); try { assertOpen(); return new LocalRepoTransactionImpl(this, false); } finally { lock.unlock(); } } @Override public LocalRepoTransactionImpl beginWriteTransaction() { lock.lock(); try { assertOpen(); return new LocalRepoTransactionImpl(this, true); } finally { lock.unlock(); } } @Override public void localSync(final ProgressMonitor monitor) { monitor.beginTask("Local sync...", 100); try { final LocalRepoTransactionImpl transaction = beginWriteTransaction(); try { monitor.worked(1); LocalRepoSync.create(transaction).ignoreRulesEnabled(true).sync(new SubProgressMonitor(monitor, 98)); transaction.commit(); monitor.worked(1); } finally { transaction.rollbackIfActive(); } } finally { monitor.done(); } } @Override public Map<UUID, URL> getRemoteRepositoryId2RemoteRootMap() { final Map<UUID, URL> result; try ( LocalRepoTransaction transaction = beginReadTransaction(); ) { result = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryId2RemoteRootMap(); transaction.commit(); } return result; } @Override public void putRemoteRepository(final UUID repositoryId, final URL remoteRoot, final byte[] publicKey, final String localPathPrefix) { assertNotNull(repositoryId, "repositoryId"); assertNotNull(publicKey, "publicKey"); final LocalRepoTransactionImpl transaction = beginWriteTransaction(); try { final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); if (remoteRoot != null) { final RemoteRepository otherRepoWithSameRemoteRoot = remoteRepositoryDao.getRemoteRepository(remoteRoot); if (otherRepoWithSameRemoteRoot != null && !repositoryId.equals(otherRepoWithSameRemoteRoot.getRepositoryId())) throw new IllegalStateException(String.format("Duplicate remoteRoot! The RemoteRepository '%s' already has the same remoteRoot '%s'! The remoteRoot must be unique!", otherRepoWithSameRemoteRoot.getRepositoryId(), remoteRoot)); } RemoteRepository remoteRepository = remoteRepositoryDao.getRemoteRepository(repositoryId); if (remoteRepository == null) { remoteRepository = createObject(RemoteRepository.class, repositoryId); remoteRepository.setRevision(-1); } remoteRepository.setRemoteRoot(remoteRoot); remoteRepository.setPublicKey(publicKey); remoteRepository.setLocalPathPrefix(localPathPrefix); remoteRepositoryDao.makePersistent(remoteRepository); // just in case, it is new (otherwise this has no effect, anyway). final RemoteRepositoryRequestDao remoteRepositoryRequestDao = transaction.getDao(RemoteRepositoryRequestDao.class); final RemoteRepositoryRequest remoteRepositoryRequest = remoteRepositoryRequestDao.getRemoteRepositoryRequest(repositoryId); if (remoteRepositoryRequest != null) remoteRepositoryRequestDao.deletePersistent(remoteRepositoryRequest); transaction.commit(); } finally { transaction.rollbackIfActive(); } } @Override public void deleteRemoteRepository(final UUID repositoryId) { AssertUtil.assertNotNull(repositoryId, "entityID"); final LocalRepoTransactionImpl transaction = beginWriteTransaction(); try { final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); final RemoteRepository remoteRepository = remoteRepositoryDao.getRemoteRepository(repositoryId); if (remoteRepository != null) remoteRepositoryDao.deletePersistent(remoteRepository); transaction.commit(); } finally { transaction.rollbackIfActive(); } } protected int getKeySize() { final int keySize = PropertiesUtil.getSystemPropertyValueAsInt(SYSTEM_PROPERTY_KEY_SIZE, DEFAULT_KEY_SIZE); if (keySize < 1024) { logger.warn("System property '{}': keySize {} is out of range! Using default {} instead!", SYSTEM_PROPERTY_KEY_SIZE, keySize, DEFAULT_KEY_SIZE); return DEFAULT_KEY_SIZE; } return keySize; } @Override public Lock getLock() { return lock; } @Override public String getLocalPathPrefixOrFail(final URL remoteRoot) { final String localPathPrefix; final LocalRepoTransaction transaction = beginReadTransaction(); try { final RemoteRepository remoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(remoteRoot); localPathPrefix = remoteRepository.getLocalPathPrefix(); transaction.commit(); } finally { transaction.rollbackIfActive(); } return localPathPrefix; } @Override public String getLocalPathPrefixOrFail(final UUID repositoryId) { final String localPathPrefix; final LocalRepoTransaction transaction = beginReadTransaction(); try { final RemoteRepository clientRemoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(repositoryId); localPathPrefix = clientRemoteRepository.getLocalPathPrefix(); transaction.commit(); } finally { transaction.rollbackIfActive(); } return localPathPrefix; } @Override public UUID getRemoteRepositoryIdOrFail(final URL remoteRoot) { UUID remoteRepositoryId; final LocalRepoTransaction transaction = beginReadTransaction(); try { final RemoteRepository remoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(remoteRoot); remoteRepositoryId = remoteRepository.getRepositoryId(); transaction.commit(); } finally { transaction.rollbackIfActive(); } return remoteRepositoryId; } @Override public void finalize() throws Throwable { super.finalize(); } @Override public LocalRepoMetaData getLocalRepoMetaData() { lock.lock(); try { if (localRepoMetaDataImpl == null) { localRepoMetaDataImpl = createObject(LocalRepoMetaDataImpl.class); localRepoMetaDataImpl.setLocalRepoManager(this); } return localRepoMetaDataImpl; } finally { lock.unlock(); } } }