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}