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}