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