package co.codewizards.cloudstore.core.repo.local; import static co.codewizards.cloudstore.core.io.StreamUtil.*; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.Util.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.collection.LazyUnmodifiableList; import co.codewizards.cloudstore.core.config.ConfigDir; import co.codewizards.cloudstore.core.config.ConfigImpl; import co.codewizards.cloudstore.core.dto.DateTime; import co.codewizards.cloudstore.core.io.LockFile; import co.codewizards.cloudstore.core.io.LockFileFactory; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.util.PropertiesUtil; public class LocalRepoRegistryImpl implements LocalRepoRegistry { private static final Logger logger = LoggerFactory.getLogger(LocalRepoRegistry.class); private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); private static final String PROP_KEY_PREFIX_REPOSITORY_ID = "repositoryId:"; private static final String PROP_KEY_PREFIX_REPOSITORY_ALIAS = "repositoryAlias:"; private static final String PROP_EVICT_DEAD_ENTRIES_LAST_TIMESTAMP = "evictDeadEntriesLastTimestamp"; /** * @deprecated Replaced by {@link #CONFIG_KEY_EVICT_DEAD_ENTRIES_PERIOD}. */ @Deprecated private static final String PROP_EVICT_DEAD_ENTRIES_PERIOD = "evictDeadEntriesPeriod"; private static final long LOCK_TIMEOUT_MS = 10000L; // 10 s private File registryFile; private long repoRegistryFileLastModified; private Properties repoRegistryProperties; private boolean repoRegistryPropertiesDirty; private static class LocalRepoRegistryHolder { public static final LocalRepoRegistry INSTANCE = new LocalRepoRegistryImpl(); } public static LocalRepoRegistry getInstance() { return LocalRepoRegistryHolder.INSTANCE; } private LocalRepoRegistryImpl() { } private File getRegistryFile() { if (registryFile == null) { final File old = createFile(ConfigDir.getInstance().getFile(), "repositoryList.properties"); // old name until 0.9.0 registryFile = createFile(ConfigDir.getInstance().getFile(), LOCAL_REPO_REGISTRY_FILE); if (old.exists() && !registryFile.exists()) old.renameTo(registryFile); } return registryFile; } @Override public synchronized Collection<UUID> getRepositoryIds() { loadRepoRegistryIfNeeded(); final List<UUID> result = new ArrayList<UUID>(); for (final Entry<Object, Object> me : repoRegistryProperties.entrySet()) { final String key = String.valueOf(me.getKey()); if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ID)) { final UUID repositoryId = UUID.fromString(key.substring(PROP_KEY_PREFIX_REPOSITORY_ID.length())); result.add(repositoryId); } } Collections.sort(result); // guarantee a stable order to prevent Heisenbugs return Collections.unmodifiableList(result); } @Override public synchronized UUID getRepositoryId(final String repositoryName) { assertNotNull(repositoryName, "repositoryName"); loadRepoRegistryIfNeeded(); final String repositoryIdString = repoRegistryProperties.getProperty(getPropertyKeyForAlias(repositoryName)); if (repositoryIdString != null) { final UUID repositoryId = UUID.fromString(repositoryIdString); return repositoryId; } UUID repositoryId; try { repositoryId = UUID.fromString(repositoryName); } catch (final IllegalArgumentException x) { return null; } final String localRootString = repoRegistryProperties.getProperty(getPropertyKeyForID(repositoryId)); if (localRootString == null) return null; return repositoryId; } @Override public UUID getRepositoryIdOrFail(final String repositoryName) { final UUID repositoryId = getRepositoryId(repositoryName); if (repositoryId == null) throw new IllegalArgumentException("Unknown repositoryName (neither a known ID nor a known alias): " + repositoryName); return repositoryId; } @Override public URL getLocalRootURLForRepositoryNameOrFail(final String repositoryName) { try { return getLocalRootForRepositoryNameOrFail(repositoryName).toURI().toURL(); } catch (final MalformedURLException e) { throw new RuntimeException(e); } } @Override public synchronized URL getLocalRootURLForRepositoryName(final String repositoryName) { final File localRoot = getLocalRootForRepositoryName(repositoryName); if (localRoot == null) return null; try { return localRoot.toURI().toURL(); } catch (final MalformedURLException e) { throw new RuntimeException(e); } } @Override public File getLocalRootForRepositoryNameOrFail(final String repositoryName) { final File localRoot = getLocalRootForRepositoryName(repositoryName); if (localRoot == null) throw new IllegalArgumentException("Unknown repositoryName (neither a known repositoryAlias, nor a known repositoryId): " + repositoryName); return localRoot; } @Override public synchronized File getLocalRootForRepositoryName(final String repositoryName) { assertNotNull(repositoryName, "repositoryName"); // If the repositoryName is an alias, this should find the corresponding repositoryId. final UUID repositoryId = getRepositoryId(repositoryName); if (repositoryId == null) return null; return getLocalRoot(repositoryId); } @Override public synchronized File getLocalRoot(final UUID repositoryId) { assertNotNull(repositoryId, "repositoryId"); loadRepoRegistryIfNeeded(); final String localRootString = repoRegistryProperties.getProperty(getPropertyKeyForID(repositoryId)); if (localRootString == null) return null; final File localRoot = createFile(localRootString); return localRoot; } @Override public File getLocalRootOrFail(final UUID repositoryId) { final File localRoot = getLocalRoot(repositoryId); if (localRoot == null) throw new IllegalArgumentException("Unknown repositoryId: " + repositoryId); return localRoot; } @Override public synchronized void putRepositoryAlias(final String repositoryAlias, final UUID repositoryId) { assertNotNull(repositoryAlias, "repositoryAlias"); assertNotNull(repositoryId, "repositoryId"); if (repositoryAlias.isEmpty()) throw new IllegalArgumentException("repositoryAlias must not be empty!"); if ("ALL".equals(repositoryAlias)) throw new IllegalArgumentException("repositoryAlias cannot be named 'ALL'! This is a reserved key word."); if (repositoryAlias.startsWith("_")) throw new IllegalArgumentException("repositoryAlias must not start with '_': " + repositoryAlias); if (repositoryAlias.indexOf('/') >= 0) throw new IllegalArgumentException("repositoryAlias must not contain a '/': " + repositoryAlias); boolean modified = false; try ( final LockFile lockFile = acquireLockFile(); ) { loadRepoRegistryIfNeeded(); getLocalRootOrFail(repositoryId); // make sure, this is a known repositoryId! final String propertyKey = getPropertyKeyForAlias(repositoryAlias); final String oldRepositoryIdString = repoRegistryProperties.getProperty(propertyKey); final String repositoryIdString = repositoryId.toString(); if (!repositoryIdString.equals(oldRepositoryIdString)) { modified = true; setProperty(propertyKey, repositoryIdString); } storeRepoRegistryIfDirty(); } if (modified) fireRepositoryAliasesChanged(); } @Override public synchronized Collection<String> getRepositoryAliases() { final Set<String> result= new LinkedHashSet<>(); final Collection<UUID> repositoryIds = getRepositoryIds(); for (final UUID repositoryId : repositoryIds) { final Collection<String> repositoryAliases = getRepositoryAliasesOrFail(repositoryId.toString()); result.addAll(repositoryAliases); } return result; } @Override public synchronized void removeRepositoryAlias(final String repositoryAlias) { assertNotNull(repositoryAlias, "repositoryAlias"); boolean modified = false; try ( LockFile lockFile = acquireLockFile(); ) { loadRepoRegistryIfNeeded(); final String propertyKey = getPropertyKeyForAlias(repositoryAlias); final String repositoryIdString = repoRegistryProperties.getProperty(propertyKey); if (repositoryIdString != null) { modified = true; removeProperty(propertyKey); } storeRepoRegistryIfDirty(); } if (modified) fireRepositoryAliasesChanged(); } @Override public synchronized void putRepository(final UUID repositoryId, final File localRoot) { assertNotNull(repositoryId, "repositoryId"); assertNotNull(localRoot, "localRoot"); if (!localRoot.isAbsolute()) throw new IllegalArgumentException("localRoot is not absolute."); boolean modified = false; try ( final LockFile lockFile = acquireLockFile(); ) { loadRepoRegistryIfNeeded(); final String propertyKey = getPropertyKeyForID(repositoryId); final String oldLocalRootPath = repoRegistryProperties.getProperty(propertyKey); final String localRootPath = localRoot.getPath(); if (!localRootPath.equals(oldLocalRootPath)) { modified = true; setProperty(propertyKey, localRootPath); } storeRepoRegistryIfDirty(); } if (modified) fireRepositoryIdsChanged(); } protected Date getPropertyAsDate(final String key) { final String value = getProperty(key); if (value == null || value.trim().isEmpty()) return null; return new DateTime(value).toDate(); } private void setProperty(final String key, final Date value) { setProperty(key, new DateTime(assertNotNull(value, "value")).toString()); } private String getProperty(final String key) { return repoRegistryProperties.getProperty(assertNotNull(key, "key")); } private void setProperty(final String key, final String value) { final Object oldValue = repoRegistryProperties.setProperty(assertNotNull(key, "key"), assertNotNull(value, "value")); if (!equal(oldValue, (Object) value)) repoRegistryPropertiesDirty = true; } private void removeProperty(final String key) { if (repoRegistryProperties.remove(assertNotNull(key, "key")) != null) repoRegistryPropertiesDirty = true; } @Override public synchronized Collection<String> getRepositoryAliasesOrFail(final String repositoryName) throws IllegalArgumentException { return getRepositoryAliases(repositoryName, true); } @Override public synchronized Collection<String> getRepositoryAliases(final String repositoryName) { return getRepositoryAliases(repositoryName, false); } private Collection<String> getRepositoryAliases(final String repositoryName, final boolean fail) throws IllegalArgumentException { try ( final LockFile lockFile = acquireLockFile(); ) { final UUID repositoryId = fail ? getRepositoryIdOrFail(repositoryName) : getRepositoryId(repositoryName); if (repositoryId == null) return null; final List<String> result = new ArrayList<String>(); for (final Entry<Object, Object> me : repoRegistryProperties.entrySet()) { final String key = String.valueOf(me.getKey()); if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ALIAS)) { final String value = String.valueOf(me.getValue()); final UUID mappedRepositoryId = UUID.fromString(value); if (mappedRepositoryId.equals(repositoryId)) result.add(key.substring(PROP_KEY_PREFIX_REPOSITORY_ALIAS.length())); } } Collections.sort(result); return Collections.unmodifiableList(result); } } private String getPropertyKeyForAlias(final String repositoryAlias) { return PROP_KEY_PREFIX_REPOSITORY_ALIAS + assertNotNull(repositoryAlias, "repositoryAlias"); } private String getPropertyKeyForID(final UUID repositoryId) { return PROP_KEY_PREFIX_REPOSITORY_ID + assertNotNull(repositoryId, "repositoryId").toString(); } private void loadRepoRegistryIfNeeded() { boolean modified = false; try ( final LockFile lockFile = acquireLockFile(); ) { if (repoRegistryProperties == null || repoRegistryFileLastModified != getRegistryFile().lastModified()) { loadRepoRegistry(); modified = true; } if (evictDeadEntriesPeriodically()) modified = true; } if (modified) { // We don't know what exactly changed => fire all events ;-) fireRepositoryIdsChanged(); fireRepositoryAliasesChanged(); } } private void fireRepositoryIdsChanged() { firePropertyChange(PropertyEnum.repositoryIds, null, new LazyUnmodifiableList<UUID>() { @Override protected Collection<UUID> loadElements() { return getRepositoryIds(); } }); } private void fireRepositoryAliasesChanged() { firePropertyChange(PropertyEnum.repositoryAliases, null, new LazyUnmodifiableList<String>() { @Override protected java.util.Collection<String> loadElements() { return getRepositoryAliases(); } }); } private LockFile acquireLockFile() { return LockFileFactory.getInstance().acquire(getRegistryFile(), LOCK_TIMEOUT_MS); } private void loadRepoRegistry() { try { final File registryFile = getRegistryFile(); if (registryFile.exists() && registryFile.length() > 0) { final Properties properties = new Properties(); try ( final LockFile lockFile = acquireLockFile(); ) { try (final InputStream in = castStream(lockFile.createInputStream())) { properties.load(in); } } repoRegistryProperties = properties; } else repoRegistryProperties = new Properties(); repoRegistryFileLastModified = registryFile.lastModified(); repoRegistryPropertiesDirty = false; } catch (final IOException e) { throw new IllegalStateException(e); } } private void storeRepoRegistryIfDirty() { if (repoRegistryPropertiesDirty) { storeRepoRegistry(); repoRegistryPropertiesDirty = false; } } private void storeRepoRegistry() { if (repoRegistryProperties == null) throw new IllegalStateException("repoRegistryProperties not loaded, yet!"); try { final File registryFile = getRegistryFile(); try ( final LockFile lockFile = acquireLockFile(); ) { try (final OutputStream out = castStream(lockFile.createOutputStream())) { repoRegistryProperties.store(out, null); } } repoRegistryFileLastModified = registryFile.lastModified(); } catch (final IOException e) { throw new IllegalStateException(e); } } /** * Checks, which entries point to non-existing directories or directories which are not (anymore) repositories * and removes them. */ private boolean evictDeadEntriesPeriodically() { final Long period = ConfigImpl.getInstance().getPropertyAsLong(CONFIG_KEY_EVICT_DEAD_ENTRIES_PERIOD, DEFAULT_EVICT_DEAD_ENTRIES_PERIOD); removeProperty(PROP_EVICT_DEAD_ENTRIES_PERIOD); final Date last = getPropertyAsDate(PROP_EVICT_DEAD_ENTRIES_LAST_TIMESTAMP); if (last != null) { final long millisAfterLast = System.currentTimeMillis() - last.getTime(); if (millisAfterLast >= 0 && millisAfterLast <= period) // < 0 : travelled back in time return false; } final boolean modified = evictDeadEntries(); setProperty(PROP_EVICT_DEAD_ENTRIES_LAST_TIMESTAMP, new Date()); return modified; } private boolean evictDeadEntries() { boolean modified = false; for (final Entry<Object, Object> me : new ArrayList<Entry<Object, Object>>(repoRegistryProperties.entrySet())) { final String key = String.valueOf(me.getKey()); final String value = String.valueOf(me.getValue()); UUID repositoryIdFromRegistry; if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ALIAS)) { repositoryIdFromRegistry = UUID.fromString(value); } else if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ID)) { repositoryIdFromRegistry = UUID.fromString(key.substring(PROP_KEY_PREFIX_REPOSITORY_ID.length())); } else continue; final String localRootString = repoRegistryProperties.getProperty(getPropertyKeyForID(repositoryIdFromRegistry)); if (localRootString == null) { modified = true; evictDeadEntry(key); continue; } final File localRoot = createFile(localRootString); if (!localRoot.isDirectory()) { modified = true; evictDeadEntry(key); continue; } final File repoMetaDir = createFile(localRoot, LocalRepoManager.META_DIR_NAME); if (!repoMetaDir.isDirectory()) { modified = true; evictDeadEntry(key); continue; } final File repositoryPropertiesFile = createFile(repoMetaDir, LocalRepoManager.REPOSITORY_PROPERTIES_FILE_NAME); if (!repositoryPropertiesFile.exists()) { logger.warn("evictDeadEntries: File does not exist (repo corrupt?!): {}", repositoryPropertiesFile); continue; } Properties repositoryProperties; try { repositoryProperties = PropertiesUtil.load(repositoryPropertiesFile); } catch (final IOException e) { logger.warn("evictDeadEntries: Could not read file (repo corrupt?!): {}", repositoryPropertiesFile); logger.warn("evictDeadEntries: " + e, e); continue; } final String repositoryIdFromRepo = repositoryProperties.getProperty(LocalRepoManager.PROP_REPOSITORY_ID); if (repositoryIdFromRepo == null) { logger.warn("evictDeadEntries: repositoryProperties '{}' do not contain key='{}'!", repositoryPropertiesFile, LocalRepoManager.PROP_REPOSITORY_ID); // Old repos don't have the repo-id in the properties, yet. // This is automatically added, when the LocalRepoManager is started up for this repo, the next time. // For now, we ignore it. continue; } if (!repositoryIdFromRegistry.toString().equals(repositoryIdFromRepo)) { // new repo was created at the same location modified = true; evictDeadEntry(key); continue; } } return modified; } private void evictDeadEntry(final String key) { repoRegistryPropertiesDirty = true; final Object value = repoRegistryProperties.remove(key); logger.info("evictDeadEntry: key='{}' value='{}'", key, value); } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } @Override public void addPropertyChangeListener(Property property, PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(property.name(), listener); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); } @Override public void removePropertyChangeListener(Property property, PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(property.name(), listener); } protected void firePropertyChange(Property property, Object oldValue, Object newValue) { propertyChangeSupport.firePropertyChange(property.name(), oldValue, newValue); } }