001package co.codewizards.cloudstore.core.repo.local;
002
003import static co.codewizards.cloudstore.core.io.StreamUtil.*;
004import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
005import static co.codewizards.cloudstore.core.util.Util.*;
006import static java.util.Objects.*;
007
008import java.beans.PropertyChangeListener;
009import java.beans.PropertyChangeSupport;
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.OutputStream;
013import java.net.MalformedURLException;
014import java.net.URL;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.Collections;
018import java.util.Date;
019import java.util.LinkedHashSet;
020import java.util.List;
021import java.util.Map.Entry;
022import java.util.Properties;
023import java.util.Set;
024import java.util.UUID;
025
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import co.codewizards.cloudstore.core.collection.LazyUnmodifiableList;
030import co.codewizards.cloudstore.core.config.ConfigDir;
031import co.codewizards.cloudstore.core.config.ConfigImpl;
032import co.codewizards.cloudstore.core.dto.DateTime;
033import co.codewizards.cloudstore.core.io.LockFile;
034import co.codewizards.cloudstore.core.io.LockFileFactory;
035import co.codewizards.cloudstore.core.oio.File;
036import co.codewizards.cloudstore.core.util.PropertiesUtil;
037
038public class LocalRepoRegistryImpl implements LocalRepoRegistry
039{
040        private static final Logger logger = LoggerFactory.getLogger(LocalRepoRegistry.class);
041
042        private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
043
044        private static final String PROP_KEY_PREFIX_REPOSITORY_ID = "repositoryId:";
045        private static final String PROP_KEY_PREFIX_REPOSITORY_ALIAS = "repositoryAlias:";
046        private static final String PROP_EVICT_DEAD_ENTRIES_LAST_TIMESTAMP = "evictDeadEntriesLastTimestamp";
047        /**
048         * @deprecated Replaced by {@link #CONFIG_KEY_EVICT_DEAD_ENTRIES_PERIOD}.
049         */
050        @Deprecated
051        private static final String PROP_EVICT_DEAD_ENTRIES_PERIOD = "evictDeadEntriesPeriod";
052        private static final long LOCK_TIMEOUT_MS = 10000L; // 10 s
053
054        private File registryFile;
055        private long repoRegistryFileLastModified;
056        private Properties repoRegistryProperties;
057        private boolean repoRegistryPropertiesDirty;
058
059        private static class LocalRepoRegistryHolder {
060                public static final LocalRepoRegistry INSTANCE = new LocalRepoRegistryImpl();
061        }
062
063        public static LocalRepoRegistry getInstance() {
064                return LocalRepoRegistryHolder.INSTANCE;
065        }
066
067        private LocalRepoRegistryImpl() { }
068
069        private File getRegistryFile() {
070                if (registryFile == null) {
071                        final File old = createFile(ConfigDir.getInstance().getFile(), "repositoryList.properties"); // old name until 0.9.0
072                        registryFile = createFile(ConfigDir.getInstance().getFile(), LOCAL_REPO_REGISTRY_FILE);
073                        if (old.exists() && !registryFile.exists())
074                                old.renameTo(registryFile);
075                }
076                return registryFile;
077        }
078
079        @Override
080        public synchronized Collection<UUID> getRepositoryIds() {
081                loadRepoRegistryIfNeeded();
082                final List<UUID> result = new ArrayList<UUID>();
083                for (final Entry<Object, Object> me : repoRegistryProperties.entrySet()) {
084                        final String key = String.valueOf(me.getKey());
085                        if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ID)) {
086                                final UUID repositoryId = UUID.fromString(key.substring(PROP_KEY_PREFIX_REPOSITORY_ID.length()));
087                                result.add(repositoryId);
088                        }
089                }
090                Collections.sort(result); // guarantee a stable order to prevent Heisenbugs
091                return Collections.unmodifiableList(result);
092        }
093
094        @Override
095        public synchronized UUID getRepositoryId(final String repositoryName) {
096                requireNonNull(repositoryName, "repositoryName");
097                loadRepoRegistryIfNeeded();
098                final String repositoryIdString = repoRegistryProperties.getProperty(getPropertyKeyForAlias(repositoryName));
099                if (repositoryIdString != null) {
100                        final UUID repositoryId = UUID.fromString(repositoryIdString);
101                        return repositoryId;
102                }
103
104                UUID repositoryId;
105                try {
106                        repositoryId = UUID.fromString(repositoryName);
107                } catch (final IllegalArgumentException x) {
108                        return null;
109                }
110
111                final String localRootString = repoRegistryProperties.getProperty(getPropertyKeyForID(repositoryId));
112                if (localRootString == null)
113                        return null;
114
115                return repositoryId;
116        }
117
118        @Override
119        public UUID getRepositoryIdOrFail(final String repositoryName) {
120                final UUID repositoryId = getRepositoryId(repositoryName);
121                if (repositoryId == null)
122                        throw new IllegalArgumentException("Unknown repositoryName (neither a known ID nor a known alias): " + repositoryName);
123
124                return repositoryId;
125        }
126
127        @Override
128        public URL getLocalRootURLForRepositoryNameOrFail(final String repositoryName) {
129                try {
130                        return getLocalRootForRepositoryNameOrFail(repositoryName).toURI().toURL();
131                } catch (final MalformedURLException e) {
132                        throw new RuntimeException(e);
133                }
134        }
135
136        @Override
137        public synchronized URL getLocalRootURLForRepositoryName(final String repositoryName) {
138                final File localRoot = getLocalRootForRepositoryName(repositoryName);
139                if (localRoot == null)
140                        return null;
141
142                try {
143                        return localRoot.toURI().toURL();
144                } catch (final MalformedURLException e) {
145                        throw new RuntimeException(e);
146                }
147        }
148
149        @Override
150        public File getLocalRootForRepositoryNameOrFail(final String repositoryName) {
151                final File localRoot = getLocalRootForRepositoryName(repositoryName);
152                if (localRoot == null)
153                        throw new IllegalArgumentException("Unknown repositoryName (neither a known repositoryAlias, nor a known repositoryId): " + repositoryName);
154
155                return localRoot;
156        }
157
158        @Override
159        public synchronized File getLocalRootForRepositoryName(final String repositoryName) {
160                requireNonNull(repositoryName, "repositoryName");
161
162                // If the repositoryName is an alias, this should find the corresponding repositoryId.
163                final UUID repositoryId = getRepositoryId(repositoryName);
164                if (repositoryId == null)
165                        return null;
166
167                return getLocalRoot(repositoryId);
168        }
169
170        @Override
171        public synchronized File getLocalRoot(final UUID repositoryId) {
172                requireNonNull(repositoryId, "repositoryId");
173                loadRepoRegistryIfNeeded();
174                final String localRootString = repoRegistryProperties.getProperty(getPropertyKeyForID(repositoryId));
175                if (localRootString == null)
176                        return null;
177
178                final File localRoot = createFile(localRootString);
179                return localRoot;
180        }
181
182        @Override
183        public File getLocalRootOrFail(final UUID repositoryId) {
184                final File localRoot = getLocalRoot(repositoryId);
185                if (localRoot == null)
186                        throw new IllegalArgumentException("Unknown repositoryId: " + repositoryId);
187
188                return localRoot;
189        }
190
191        @Override
192        public synchronized void putRepositoryAlias(final String repositoryAlias, final UUID repositoryId) {
193                requireNonNull(repositoryAlias, "repositoryAlias");
194                requireNonNull(repositoryId, "repositoryId");
195
196                if (repositoryAlias.isEmpty())
197                        throw new IllegalArgumentException("repositoryAlias must not be empty!");
198
199                if ("ALL".equals(repositoryAlias))
200                        throw new IllegalArgumentException("repositoryAlias cannot be named 'ALL'! This is a reserved key word.");
201
202                if (repositoryAlias.startsWith("_"))
203                        throw new IllegalArgumentException("repositoryAlias must not start with '_': " + repositoryAlias);
204
205                if (repositoryAlias.indexOf('/') >= 0)
206                        throw new IllegalArgumentException("repositoryAlias must not contain a '/': " + repositoryAlias);
207
208                boolean modified = false;
209                try ( final LockFile lockFile = acquireLockFile(); ) {
210                        loadRepoRegistryIfNeeded();
211                        getLocalRootOrFail(repositoryId); // make sure, this is a known repositoryId!
212                        final String propertyKey = getPropertyKeyForAlias(repositoryAlias);
213                        final String oldRepositoryIdString = repoRegistryProperties.getProperty(propertyKey);
214                        final String repositoryIdString = repositoryId.toString();
215                        if (!repositoryIdString.equals(oldRepositoryIdString)) {
216                                modified = true;
217                                setProperty(propertyKey, repositoryIdString);
218                        }
219                        storeRepoRegistryIfDirty();
220                }
221                if (modified)
222                        fireRepositoryAliasesChanged();
223        }
224
225        @Override
226        public synchronized Collection<String> getRepositoryAliases() {
227                final Set<String> result= new LinkedHashSet<>();
228                final Collection<UUID> repositoryIds = getRepositoryIds();
229                for (final UUID repositoryId : repositoryIds) {
230                        final Collection<String> repositoryAliases = getRepositoryAliasesOrFail(repositoryId.toString());
231                        result.addAll(repositoryAliases);
232                }
233                return result;
234        }
235
236        @Override
237        public synchronized void removeRepositoryAlias(final String repositoryAlias) {
238                requireNonNull(repositoryAlias, "repositoryAlias");
239                boolean modified = false;
240                try ( LockFile lockFile = acquireLockFile(); ) {
241                        loadRepoRegistryIfNeeded();
242                        final String propertyKey = getPropertyKeyForAlias(repositoryAlias);
243                        final String repositoryIdString = repoRegistryProperties.getProperty(propertyKey);
244                        if (repositoryIdString != null) {
245                                modified = true;
246                                removeProperty(propertyKey);
247                        }
248                        storeRepoRegistryIfDirty();
249                }
250                if (modified)
251                        fireRepositoryAliasesChanged();
252        }
253
254        @Override
255        public synchronized void putRepository(final UUID repositoryId, final File localRoot) {
256                requireNonNull(repositoryId, "repositoryId");
257                requireNonNull(localRoot, "localRoot");
258
259                if (!localRoot.isAbsolute())
260                        throw new IllegalArgumentException("localRoot is not absolute.");
261
262                boolean modified = false;
263                try ( final LockFile lockFile = acquireLockFile(); ) {
264                        loadRepoRegistryIfNeeded();
265                        final String propertyKey = getPropertyKeyForID(repositoryId);
266                        final String oldLocalRootPath = repoRegistryProperties.getProperty(propertyKey);
267                        final String localRootPath = localRoot.getPath();
268                        if (!localRootPath.equals(oldLocalRootPath)) {
269                                modified = true;
270                                setProperty(propertyKey, localRootPath);
271                        }
272                        storeRepoRegistryIfDirty();
273                }
274                if (modified)
275                        fireRepositoryIdsChanged();
276        }
277
278        protected Date getPropertyAsDate(final String key) {
279                final String value = getProperty(key);
280                if (value == null || value.trim().isEmpty())
281                        return null;
282
283                return new DateTime(value).toDate();
284        }
285
286        private void setProperty(final String key, final Date value) {
287                setProperty(key, new DateTime(requireNonNull(value, "value")).toString());
288        }
289
290        private String getProperty(final String key) {
291                return repoRegistryProperties.getProperty(requireNonNull(key, "key"));
292        }
293
294        private void setProperty(final String key, final String value) {
295                final Object oldValue = repoRegistryProperties.setProperty(requireNonNull(key, "key"), requireNonNull(value, "value"));
296                if (!equal(oldValue, (Object) value))
297                        repoRegistryPropertiesDirty = true;
298        }
299
300        private void removeProperty(final String key) {
301                if (repoRegistryProperties.remove(requireNonNull(key, "key")) != null)
302                        repoRegistryPropertiesDirty = true;
303        }
304
305        @Override
306        public synchronized Collection<String> getRepositoryAliasesOrFail(final String repositoryName) throws IllegalArgumentException {
307                return getRepositoryAliases(repositoryName, true);
308        }
309
310        @Override
311        public synchronized Collection<String> getRepositoryAliases(final String repositoryName) {
312                return getRepositoryAliases(repositoryName, false);
313        }
314
315        private Collection<String> getRepositoryAliases(final String repositoryName, final boolean fail) throws IllegalArgumentException {
316                try ( final LockFile lockFile = acquireLockFile(); ) {
317                        final UUID repositoryId = fail ? getRepositoryIdOrFail(repositoryName) : getRepositoryId(repositoryName);
318                        if (repositoryId == null)
319                                return null;
320
321                        final List<String> result = new ArrayList<String>();
322                        for (final Entry<Object, Object> me : repoRegistryProperties.entrySet()) {
323                                final String key = String.valueOf(me.getKey());
324                                if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ALIAS)) {
325                                        final String value = String.valueOf(me.getValue());
326                                        final UUID mappedRepositoryId = UUID.fromString(value);
327                                        if (mappedRepositoryId.equals(repositoryId))
328                                                result.add(key.substring(PROP_KEY_PREFIX_REPOSITORY_ALIAS.length()));
329                                }
330                        }
331                        Collections.sort(result);
332                        return Collections.unmodifiableList(result);
333                }
334        }
335
336        private String getPropertyKeyForAlias(final String repositoryAlias) {
337                return PROP_KEY_PREFIX_REPOSITORY_ALIAS + requireNonNull(repositoryAlias, "repositoryAlias");
338        }
339
340        private String getPropertyKeyForID(final UUID repositoryId) {
341                return PROP_KEY_PREFIX_REPOSITORY_ID + requireNonNull(repositoryId, "repositoryId").toString();
342        }
343
344        private void loadRepoRegistryIfNeeded() {
345                boolean modified = false;
346                try ( final LockFile lockFile = acquireLockFile(); ) {
347                        if (repoRegistryProperties == null || repoRegistryFileLastModified != getRegistryFile().lastModified()) {
348                                loadRepoRegistry();
349                                modified = true;
350                        }
351
352                        if (evictDeadEntriesPeriodically())
353                                modified = true;
354                }
355
356                if (modified) {
357                        // We don't know what exactly changed => fire all events ;-)
358                        fireRepositoryIdsChanged();
359                        fireRepositoryAliasesChanged();
360                }
361        }
362
363        private void fireRepositoryIdsChanged() {
364                firePropertyChange(PropertyEnum.repositoryIds, null, new LazyUnmodifiableList<UUID>() {
365                        @Override
366                        protected Collection<UUID> loadElements() {
367                                return getRepositoryIds();
368                        }
369                });
370        }
371
372        private void fireRepositoryAliasesChanged() {
373                firePropertyChange(PropertyEnum.repositoryAliases, null, new LazyUnmodifiableList<String>() {
374                        @Override
375                        protected java.util.Collection<String> loadElements() {
376                                return getRepositoryAliases();
377                        }
378                });
379        }
380
381        private LockFile acquireLockFile() {
382                return LockFileFactory.getInstance().acquire(getRegistryFile(), LOCK_TIMEOUT_MS);
383        }
384
385        private void loadRepoRegistry() {
386                try {
387                        final File registryFile = getRegistryFile();
388                        if (registryFile.exists() && registryFile.length() > 0) {
389                                final Properties properties = new Properties();
390                                try ( final LockFile lockFile = acquireLockFile(); ) {
391                                        try (final InputStream in = castStream(lockFile.createInputStream())) {
392                                                properties.load(in);
393                                        }
394                                }
395                                repoRegistryProperties = properties;
396                        }
397                        else
398                                repoRegistryProperties = new Properties();
399
400                        repoRegistryFileLastModified = registryFile.lastModified();
401                        repoRegistryPropertiesDirty = false;
402                } catch (final IOException e) {
403                        throw new IllegalStateException(e);
404                }
405        }
406
407        private void storeRepoRegistryIfDirty() {
408                if (repoRegistryPropertiesDirty) {
409                        storeRepoRegistry();
410                        repoRegistryPropertiesDirty = false;
411                }
412        }
413
414        private void storeRepoRegistry() {
415                if (repoRegistryProperties == null)
416                        throw new IllegalStateException("repoRegistryProperties not loaded, yet!");
417
418                try {
419                        final File registryFile = getRegistryFile();
420                        try ( final LockFile lockFile = acquireLockFile(); ) {
421                                try (final OutputStream out = castStream(lockFile.createOutputStream())) {
422                                        repoRegistryProperties.store(out, null);
423                                }
424                        }
425                        repoRegistryFileLastModified = registryFile.lastModified();
426                } catch (final IOException e) {
427                        throw new IllegalStateException(e);
428                }
429        }
430
431        /**
432         * Checks, which entries point to non-existing directories or directories which are not (anymore) repositories
433         * and removes them.
434         */
435        private boolean evictDeadEntriesPeriodically() {
436                final Long period = ConfigImpl.getInstance().getPropertyAsLong(CONFIG_KEY_EVICT_DEAD_ENTRIES_PERIOD, DEFAULT_EVICT_DEAD_ENTRIES_PERIOD);
437                removeProperty(PROP_EVICT_DEAD_ENTRIES_PERIOD);
438                final Date last = getPropertyAsDate(PROP_EVICT_DEAD_ENTRIES_LAST_TIMESTAMP);
439                if (last != null) {
440                        final long millisAfterLast = System.currentTimeMillis() - last.getTime();
441                        if (millisAfterLast >= 0 && millisAfterLast <= period) // < 0 : travelled back in time
442                                return false;
443                }
444                final boolean modified = evictDeadEntries();
445                setProperty(PROP_EVICT_DEAD_ENTRIES_LAST_TIMESTAMP, new Date());
446                return modified;
447        }
448
449
450        private boolean evictDeadEntries() {
451                boolean modified = false;
452                for (final Entry<Object, Object> me : new ArrayList<Entry<Object, Object>>(repoRegistryProperties.entrySet())) {
453                        final String key = String.valueOf(me.getKey());
454                        final String value = String.valueOf(me.getValue());
455                        UUID repositoryIdFromRegistry;
456                        if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ALIAS)) {
457                                repositoryIdFromRegistry = UUID.fromString(value);
458                        } else if (key.startsWith(PROP_KEY_PREFIX_REPOSITORY_ID)) {
459                                repositoryIdFromRegistry = UUID.fromString(key.substring(PROP_KEY_PREFIX_REPOSITORY_ID.length()));
460                        } else
461                                continue;
462
463                        final String localRootString = repoRegistryProperties.getProperty(getPropertyKeyForID(repositoryIdFromRegistry));
464                        if (localRootString == null) {
465                                modified = true;
466                                evictDeadEntry(key);
467                                continue;
468                        }
469
470                        final File localRoot = createFile(localRootString);
471                        if (!localRoot.isDirectory()) {
472                                modified = true;
473                                evictDeadEntry(key);
474                                continue;
475                        }
476
477                        final File repoMetaDir = createFile(localRoot, LocalRepoManager.META_DIR_NAME);
478                        if (!repoMetaDir.isDirectory()) {
479                                modified = true;
480                                evictDeadEntry(key);
481                                continue;
482                        }
483
484                        final File repositoryPropertiesFile = createFile(repoMetaDir, LocalRepoManager.REPOSITORY_PROPERTIES_FILE_NAME);
485                        if (!repositoryPropertiesFile.exists()) {
486                                logger.warn("evictDeadEntries: File does not exist (repo corrupt?!): {}", repositoryPropertiesFile);
487                                continue;
488                        }
489
490                        Properties repositoryProperties;
491                        try {
492                                repositoryProperties = PropertiesUtil.load(repositoryPropertiesFile);
493                        } catch (final IOException e) {
494                                logger.warn("evictDeadEntries: Could not read file (repo corrupt?!): {}", repositoryPropertiesFile);
495                                logger.warn("evictDeadEntries: " + e, e);
496                                continue;
497                        }
498
499                        final String repositoryIdFromRepo = repositoryProperties.getProperty(LocalRepoManager.PROP_REPOSITORY_ID);
500                        if (repositoryIdFromRepo == null) {
501                                logger.warn("evictDeadEntries: repositoryProperties '{}' do not contain key='{}'!", repositoryPropertiesFile, LocalRepoManager.PROP_REPOSITORY_ID);
502                                // Old repos don't have the repo-id in the properties, yet.
503                                // This is automatically added, when the LocalRepoManager is started up for this repo, the next time.
504                                // For now, we ignore it.
505                                continue;
506                        }
507
508                        if (!repositoryIdFromRegistry.toString().equals(repositoryIdFromRepo)) { // new repo was created at the same location
509                                modified = true;
510                                evictDeadEntry(key);
511                                continue;
512                        }
513                }
514                return modified;
515        }
516
517        private void evictDeadEntry(final String key) {
518                repoRegistryPropertiesDirty = true;
519                final Object value = repoRegistryProperties.remove(key);
520                logger.info("evictDeadEntry: key='{}' value='{}'", key, value);
521        }
522
523
524        @Override
525        public void addPropertyChangeListener(PropertyChangeListener listener) {
526                propertyChangeSupport.addPropertyChangeListener(listener);
527        }
528
529        @Override
530        public void addPropertyChangeListener(Property property, PropertyChangeListener listener) {
531                propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
532        }
533
534        @Override
535        public void removePropertyChangeListener(PropertyChangeListener listener) {
536                propertyChangeSupport.removePropertyChangeListener(listener);
537        }
538
539        @Override
540        public void removePropertyChangeListener(Property property, PropertyChangeListener listener) {
541                propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
542        }
543
544        protected void firePropertyChange(Property property, Object oldValue, Object newValue) {
545                propertyChangeSupport.firePropertyChange(property.name(), oldValue, newValue);
546        }
547}