001package co.codewizards.cloudstore.local; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.io.File; 006import java.io.FileInputStream; 007import java.io.IOException; 008import java.io.InputStream; 009import java.nio.file.Files; 010import java.nio.file.Path; 011import java.security.MessageDigest; 012import java.security.NoSuchAlgorithmException; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.Date; 017import java.util.HashMap; 018import java.util.HashSet; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022 023import javax.jdo.PersistenceManager; 024 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028import co.codewizards.cloudstore.core.progress.ProgressMonitor; 029import co.codewizards.cloudstore.core.progress.SubProgressMonitor; 030import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction; 031import co.codewizards.cloudstore.core.util.HashUtil; 032import co.codewizards.cloudstore.core.util.IOUtil; 033import co.codewizards.cloudstore.local.persistence.CopyModification; 034import co.codewizards.cloudstore.local.persistence.DeleteModification; 035import co.codewizards.cloudstore.local.persistence.DeleteModificationDAO; 036import co.codewizards.cloudstore.local.persistence.Directory; 037import co.codewizards.cloudstore.local.persistence.FileChunk; 038import co.codewizards.cloudstore.local.persistence.ModificationDAO; 039import co.codewizards.cloudstore.local.persistence.NormalFile; 040import co.codewizards.cloudstore.local.persistence.NormalFileDAO; 041import co.codewizards.cloudstore.local.persistence.RemoteRepository; 042import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDAO; 043import co.codewizards.cloudstore.local.persistence.RepoFile; 044import co.codewizards.cloudstore.local.persistence.RepoFileDAO; 045import co.codewizards.cloudstore.local.persistence.Symlink; 046 047public class LocalRepoSync { 048 049 private static final Logger logger = LoggerFactory.getLogger(LocalRepoSync.class); 050 051 private final LocalRepoTransaction transaction; 052 private final File localRoot; 053 private final RepoFileDAO repoFileDAO; 054 private final NormalFileDAO normalFileDAO; 055 private final RemoteRepositoryDAO remoteRepositoryDAO; 056 private final ModificationDAO modificationDAO; 057 private final DeleteModificationDAO deleteModificationDAO; 058 private Collection<RemoteRepository> remoteRepositories; 059 060 private final Map<String, Set<String>> sha1AndLength2Paths = new HashMap<String, Set<String>>(); 061 062 public LocalRepoSync(LocalRepoTransaction transaction) { 063 this.transaction = assertNotNull("transaction", transaction); 064 localRoot = this.transaction.getLocalRepoManager().getLocalRoot(); 065 repoFileDAO = this.transaction.getDAO(RepoFileDAO.class); 066 normalFileDAO = this.transaction.getDAO(NormalFileDAO.class); 067 remoteRepositoryDAO = this.transaction.getDAO(RemoteRepositoryDAO.class); 068 modificationDAO = this.transaction.getDAO(ModificationDAO.class); 069 deleteModificationDAO = this.transaction.getDAO(DeleteModificationDAO.class); 070 } 071 072 public void sync(ProgressMonitor monitor) { 073 sync(null, localRoot, monitor); 074 } 075 076 public RepoFile sync(File file, ProgressMonitor monitor) { 077 if (!(assertNotNull("file", file).isAbsolute())) 078 throw new IllegalArgumentException("file is not absolute: " + file); 079 080 if (localRoot.equals(file)) { 081 return sync(null, file, monitor); 082 } 083 084 monitor.beginTask("Local sync...", 100); 085 try { 086 File parentFile = file.getParentFile(); 087 RepoFile parentRepoFile = repoFileDAO.getRepoFile(localRoot, parentFile); 088 if (parentRepoFile == null) { 089 // If the file does not exist and its RepoFile neither exists, then 090 // this is in sync already and we can simply leave. This regularly 091 // happens during the deletion of a directory which is the connection-point 092 // of a remote-repository. The following re-up-sync then leads us here. 093 // To speed up things, we simply quit as this is a valid state. 094 if (!Files.isSymbolicLink(file.toPath()) && !file.exists() && repoFileDAO.getRepoFile(localRoot, file) == null) 095 return null; 096 097 // In the unlikely event, that this is not a valid state, we simply sync all 098 // and return. 099 sync(null, localRoot, new SubProgressMonitor(monitor, 99)); 100 final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file); 101 if (repoFile != null) // if it still does not exist, we run into the re-sync below and this might quickly return null, if that is correct or otherwise sync what's needed. 102 return repoFile; 103 104 parentRepoFile = repoFileDAO.getRepoFile(localRoot, parentFile); 105 if (parentRepoFile == null && parentFile.exists()) 106 throw new IllegalStateException("RepoFile not found for existing file/dir: " + parentFile.getAbsolutePath()); 107 } 108 109 monitor.worked(1); 110 111 return sync(parentRepoFile, file, new SubProgressMonitor(monitor, 99)); 112 } finally { 113 monitor.done(); 114 } 115 } 116 117 /** 118 * Sync the single given {@code file}. 119 * <p> 120 * If {@code file} is a directory, it recursively syncs all its children. 121 * @param parentRepoFile the parent. May be <code>null</code>, if the file is the repository's root-directory. 122 * For non-root files, this must not be <code>null</code>! 123 * @param file the file to be synced. Must not be <code>null</code>. 124 * @param monitor the progress-monitor. Must not be <code>null</code>. 125 * @return the {@link RepoFile} corresponding to the given {@code file}. Is <code>null</code>, if the given 126 * {@code file} does not exist; otherwise it is never <code>null</code>. 127 */ 128 private RepoFile sync(final RepoFile parentRepoFile, final File file, final ProgressMonitor monitor) { 129 assertNotNull("file", file); 130 assertNotNull("monitor", monitor); 131 monitor.beginTask("Local sync...", 100); 132 try { 133 RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file); 134 135 // If the type changed - e.g. from normal file to directory - or if the file was deleted 136 // we must delete the old instance. 137 if (repoFile != null && !isRepoFileTypeCorrect(repoFile, file)) { 138 deleteRepoFile(repoFile, false); 139 repoFile = null; 140 } 141 142 final boolean fileIsSymlink = Files.isSymbolicLink(file.toPath()); 143 if (repoFile == null) { 144 if (!fileIsSymlink && !file.exists()) 145 return null; 146 147 repoFile = createRepoFile(parentRepoFile, file, new SubProgressMonitor(monitor, 50)); 148 if (repoFile == null) { // ignoring non-normal files. 149 return null; 150 } 151 } else if (isModified(repoFile, file)) 152 updateRepoFile(repoFile, file, new SubProgressMonitor(monitor, 50)); 153 else 154 monitor.worked(50); 155 156 final Set<String> childNames = new HashSet<String>(); 157 if (!fileIsSymlink) { 158 final SubProgressMonitor childSubProgressMonitor = new SubProgressMonitor(monitor, 50); 159 final File[] children = file.listFiles(new FilenameFilterSkipMetaDir()); 160 if (children != null && children.length > 0) { 161 childSubProgressMonitor.beginTask("Local sync...", children.length); 162 for (final File child : children) { 163 childNames.add(child.getName()); 164 sync(repoFile, child, new SubProgressMonitor(childSubProgressMonitor, 1)); 165 } 166 } 167 childSubProgressMonitor.done(); 168 } 169 170 final Collection<RepoFile> childRepoFiles = repoFileDAO.getChildRepoFiles(repoFile); 171 for (final RepoFile childRepoFile : childRepoFiles) { 172 if (!childNames.contains(childRepoFile.getName())) { 173 deleteRepoFile(childRepoFile); 174 } 175 } 176 177 transaction.flush(); 178 return repoFile; 179 } finally { 180 monitor.done(); 181 } 182 } 183 184 /** 185 * Determines, if the type of the given {@code repoFile} matches the type 186 * of the file in the file system referenced by the given {@code file}. 187 * @param repoFile the {@link RepoFile} currently representing the given {@code file} in the database. 188 * Must not be <code>null</code>. 189 * @param file the file in the file system. Must not be <code>null</code>. 190 * @return <code>true</code>, if both types correspond to each other; <code>false</code> otherwise. If 191 * the file does not exist (anymore) in the file system, <code>false</code> is returned, too. 192 */ 193 private boolean isRepoFileTypeCorrect(final RepoFile repoFile, final File file) { 194 assertNotNull("repoFile", repoFile); 195 assertNotNull("file", file); 196 197 if (Files.isSymbolicLink(file.toPath())) 198 return repoFile instanceof Symlink; 199 200 if (file.isFile()) 201 return repoFile instanceof NormalFile; 202 203 if (file.isDirectory()) 204 return repoFile instanceof Directory; 205 206 return false; 207 } 208 209 public boolean isModified(final RepoFile repoFile, final File file) { 210 final long fileLastModified = IOUtil.getLastModifiedNoFollow(file); 211 if (repoFile.getLastModified().getTime() != fileLastModified) { 212 if (logger.isDebugEnabled()) { 213 logger.debug("isModified: repoFile.lastModified != file.lastModified: repoFile.lastModified={} file.lastModified={} file={}", 214 repoFile.getLastModified(), new Date(fileLastModified), file); 215 } 216 return true; 217 } 218 219 final Path filePath = file.toPath(); 220 if (Files.isSymbolicLink(filePath)) { 221 if (!(repoFile instanceof Symlink)) 222 throw new IllegalArgumentException("repoFile is not an instance of Symlink! file=" + file); 223 224 final Symlink symlink = (Symlink) repoFile; 225 final String fileSymlinkTarget = readSymbolicLink(filePath); 226 return !fileSymlinkTarget.equals(symlink.getTarget()); 227 } 228 else if (file.isFile()) { 229 if (!(repoFile instanceof NormalFile)) 230 throw new IllegalArgumentException("repoFile is not an instance of NormalFile! file=" + file); 231 232 final NormalFile normalFile = (NormalFile) repoFile; 233 if (normalFile.getLength() != file.length()) { 234 if (logger.isDebugEnabled()) { 235 logger.debug("isModified: normalFile.length != file.length: repoFile.length={} file.length={} file={}", 236 normalFile.getLength(), file.length(), file); 237 } 238 return true; 239 } 240 241 if (normalFile.getFileChunks().isEmpty()) // TODO remove this - only needed for downward compatibility! 242 return true; 243 } 244 245 return false; 246 } 247 248 private String readSymbolicLink(Path path) { 249 try { 250 final Path targetPath = Files.readSymbolicLink(path); 251 return IOUtil.toPathString(targetPath); 252 } catch (IOException e) { 253 throw new RuntimeException(e); 254 } 255 } 256 257 private RepoFile createRepoFile(RepoFile parentRepoFile, File file, ProgressMonitor monitor) { 258 if (parentRepoFile == null) 259 throw new IllegalStateException("Creating the root this way is not possible! Why is it not existing, yet?!???"); 260 261 monitor.beginTask("Local sync...", 100); 262 try { 263 RepoFile repoFile; 264 final Path filePath = file.toPath(); 265 if (Files.isSymbolicLink(filePath)) { 266 final Symlink symlink = (Symlink) (repoFile = new Symlink()); 267 symlink.setTarget(readSymbolicLink(filePath)); 268 } else if (file.isDirectory()) { 269 repoFile = new Directory(); 270 } else if (file.isFile()) { 271 final NormalFile normalFile = (NormalFile) (repoFile = new NormalFile()); 272 sha(normalFile, file, new SubProgressMonitor(monitor, 99)); 273 } else { 274 if (file.exists()) 275 logger.warn("createRepoFile: File exists, but is neither a directory nor a normal file! Skipping: {}", file); 276 else 277 logger.warn("createRepoFile: File does not exist! Skipping: {}", file); 278 279 return null; 280 } 281 282 repoFile.setParent(parentRepoFile); 283 repoFile.setName(file.getName()); 284 repoFile.setLastModified(new Date(IOUtil.getLastModifiedNoFollow(file))); 285 286 if (repoFile instanceof NormalFile) 287 createCopyModificationsIfPossible((NormalFile)repoFile); 288 289 return repoFileDAO.makePersistent(repoFile); 290 } finally { 291 monitor.done(); 292 } 293 } 294 295 public void updateRepoFile(final RepoFile repoFile, final File file, final ProgressMonitor monitor) { 296 logger.debug("updateRepoFile: id={} file={}", repoFile.getId(), file); 297 monitor.beginTask("Local sync...", 100); 298 try { 299 final Path filePath = file.toPath(); 300 if (Files.isSymbolicLink(filePath)) { 301 if (!(repoFile instanceof Symlink)) 302 throw new IllegalArgumentException("repoFile is not an instance of Symlink! file=" + file); 303 304 Symlink symlink = (Symlink) repoFile; 305 symlink.setTarget(readSymbolicLink(filePath)); 306 } 307 else if (file.isFile()) { 308 if (!(repoFile instanceof NormalFile)) 309 throw new IllegalArgumentException("repoFile is not an instance of NormalFile!"); 310 311 NormalFile normalFile = (NormalFile) repoFile; 312 sha(normalFile, file, new SubProgressMonitor(monitor, 100)); 313 } 314 repoFile.setLastSyncFromRepositoryId(null); 315 repoFile.setLastModified(new Date(IOUtil.getLastModifiedNoFollow(file))); 316 } finally { 317 monitor.done(); 318 } 319 } 320 321 public void deleteRepoFile(RepoFile repoFile) { 322 deleteRepoFile(repoFile, true); 323 } 324 325 private void deleteRepoFile(RepoFile repoFile, boolean createDeleteModifications) { 326 RepoFile parentRepoFile = assertNotNull("repoFile", repoFile).getParent(); 327 if (parentRepoFile == null) 328 throw new IllegalStateException("Deleting the root is not possible!"); 329 330 PersistenceManager pm = ((co.codewizards.cloudstore.local.LocalRepoTransactionImpl)transaction).getPersistenceManager(); 331 332 // We make sure, nothing interferes with our deletions (see comment below). 333 pm.flush(); 334 335 if (createDeleteModifications) 336 createDeleteModifications(repoFile); 337 338 deleteRepoFileWithAllChildrenRecursively(repoFile); 339 340 // DN batches UPDATE and DELETE statements. This sometimes causes foreign key violations and other errors in 341 // certain situations. Additionally, the deleted objects still linger in the 1st-level-cache and re-using them 342 // causes "javax.jdo.JDOUserException: Cannot read fields from a deleted object". This happens when switching 343 // from a directory to a file (or vice versa). 344 // We therefore must flush to be on the safe side. And to be extra-safe, we flush before and after deletion. 345 pm.flush(); 346 } 347 348 private int getMaxCopyModificationCount(NormalFile newNormalFile) { 349 final long fileLength = newNormalFile.getLength(); 350 if (fileLength < 10 * 1024) // 10 KiB 351 return 0; 352 353 if (fileLength < 100 * 1024) // 100 KiB 354 return 1; 355 356 if (fileLength < 1024 * 1024) // 1 MiB 357 return 2; 358 359 if (fileLength < 10 * 1024 * 1024) // 10 MiB 360 return 3; 361 362 if (fileLength < 100 * 1024 * 1024) // 100 MiB 363 return 5; 364 365 if (fileLength < 1024 * 1024 * 1024) // 1 GiB 366 return 7; 367 368 if (fileLength < 10 * 1024 * 1024 * 1024) // 10 GiB 369 return 9; 370 371 return 11; 372 } 373 374 private void createCopyModificationsIfPossible(NormalFile newNormalFile) { 375 // A CopyModification is not necessary for an empty file. And since this method is called 376 // during RepoTransport.beginPutFile(...), we easily filter out this unwanted case already. 377 // Changed to dynamic limit of CopyModifications depending on file size. 378 // The bigger the file, the more it's worth the overhead. 379 final int maxCopyModificationCount = getMaxCopyModificationCount(newNormalFile); 380 if (maxCopyModificationCount < 1) 381 return; 382 383 Set<String> fromPaths = new HashSet<String>(); 384 385 Set<String> paths = sha1AndLength2Paths.get(getSha1AndLength(newNormalFile.getSha1(), newNormalFile.getLength())); 386 if (paths != null) { 387 List<String> pathList = new ArrayList<>(paths); 388 Collections.shuffle(pathList); 389 390 for (String path : pathList) { 391 createCopyModifications(path, newNormalFile, fromPaths); 392 if (fromPaths.size() >= maxCopyModificationCount) 393 return; 394 } 395 } 396 397 List<NormalFile> normalFiles = new ArrayList<>(normalFileDAO.getNormalFilesForSha1(newNormalFile.getSha1(), newNormalFile.getLength())); 398 Collections.shuffle(normalFiles); 399 for (NormalFile normalFile : normalFiles) { 400// if (normalFile.isInProgress()) // Additional check. Do we really want this? I don't think so! 401// continue; 402 403 if (newNormalFile.equals(normalFile)) // should never happen, because newNormalFile is not yet persisted, but we write robust code that doesn't break easily after refactoring. 404 continue; 405 406 createCopyModifications(normalFile, newNormalFile, fromPaths); 407 if (fromPaths.size() >= maxCopyModificationCount) 408 return; 409 } 410 411 List<DeleteModification> deleteModifications = new ArrayList<>(deleteModificationDAO.getDeleteModificationsForSha1(newNormalFile.getSha1(), newNormalFile.getLength())); 412 Collections.shuffle(deleteModifications); 413 for (DeleteModification deleteModification : deleteModifications) { 414 createCopyModifications(deleteModification, newNormalFile, fromPaths); 415 if (fromPaths.size() >= maxCopyModificationCount) 416 return; 417 } 418 } 419 420 private void createCopyModifications(DeleteModification deleteModification, NormalFile toNormalFile, Set<String> fromPaths) { 421 assertNotNull("deleteModification", deleteModification); 422 assertNotNull("toNormalFile", toNormalFile); 423 assertNotNull("fromPaths", fromPaths); 424 425 if (deleteModification.getLength() != toNormalFile.getLength()) 426 throw new IllegalArgumentException("fromNormalFile.length != toNormalFile.length"); 427 428 if (!deleteModification.getSha1().equals(toNormalFile.getSha1())) 429 throw new IllegalArgumentException("fromNormalFile.sha1 != toNormalFile.sha1"); 430 431 createCopyModifications(deleteModification.getPath(), toNormalFile, fromPaths); 432 } 433 434 private void createCopyModifications(String fromPath, NormalFile toNormalFile, Set<String> fromPaths) { 435 assertNotNull("fromPath", fromPath); 436 assertNotNull("toNormalFile", toNormalFile); 437 assertNotNull("fromPaths", fromPaths); 438 439 if (!fromPaths.add(fromPath)) // already done before => prevent duplicates. 440 return; 441 442 for (RemoteRepository remoteRepository : getRemoteRepositories()) { 443 CopyModification modification = new CopyModification(); 444 modification.setRemoteRepository(remoteRepository); 445 modification.setFromPath(fromPath); 446 modification.setToPath(toNormalFile.getPath()); 447 modification.setLength(toNormalFile.getLength()); 448 modification.setSha1(toNormalFile.getSha1()); 449 modificationDAO.makePersistent(modification); 450 } 451 } 452 453 private void createCopyModifications(NormalFile fromNormalFile, NormalFile toNormalFile, Set<String> fromPaths) { 454 assertNotNull("fromNormalFile", fromNormalFile); 455 assertNotNull("toNormalFile", toNormalFile); 456 assertNotNull("fromPaths", fromPaths); 457 458 if (fromNormalFile.getLength() != toNormalFile.getLength()) 459 throw new IllegalArgumentException("fromNormalFile.length != toNormalFile.length"); 460 461 if (!fromNormalFile.getSha1().equals(toNormalFile.getSha1())) 462 throw new IllegalArgumentException("fromNormalFile.sha1 != toNormalFile.sha1"); 463 464 createCopyModifications(fromNormalFile.getPath(), toNormalFile, fromPaths); 465 } 466 467 private void createDeleteModifications(RepoFile repoFile) { 468 assertNotNull("repoFile", repoFile); 469 NormalFile normalFile = null; 470 if (repoFile instanceof NormalFile) 471 normalFile = (NormalFile) repoFile; 472 473 for (RemoteRepository remoteRepository : getRemoteRepositories()) { 474 DeleteModification modification = new DeleteModification(); 475 modification.setRemoteRepository(remoteRepository); 476 modification.setPath(repoFile.getPath()); 477 modification.setLength(normalFile == null ? -1 : normalFile.getLength()); 478 modification.setSha1(normalFile == null ? null : normalFile.getSha1()); 479 modificationDAO.makePersistent(modification); 480 } 481 } 482 483 private Collection<RemoteRepository> getRemoteRepositories() { 484 if (remoteRepositories == null) 485 remoteRepositories = Collections.unmodifiableCollection(remoteRepositoryDAO.getObjects()); 486 487 return remoteRepositories; 488 } 489 490 private void deleteRepoFileWithAllChildrenRecursively(RepoFile repoFile) { 491 assertNotNull("repoFile", repoFile); 492 for (RepoFile childRepoFile : repoFileDAO.getChildRepoFiles(repoFile)) { 493 deleteRepoFileWithAllChildrenRecursively(childRepoFile); 494 } 495 putIntoSha1AndLength2PathsIfNormalFile(repoFile); 496 repoFileDAO.deletePersistent(repoFile); 497 } 498 499 private void putIntoSha1AndLength2PathsIfNormalFile(RepoFile repoFile) { 500 if (repoFile instanceof NormalFile) { 501 NormalFile normalFile = (NormalFile) repoFile; 502 String sha1AndLength = getSha1AndLength(normalFile.getSha1(), normalFile.getLength()); 503 Set<String> paths = sha1AndLength2Paths.get(sha1AndLength); 504 if (paths == null) { 505 paths = new HashSet<>(1); 506 sha1AndLength2Paths.put(sha1AndLength, paths); 507 } 508 paths.add(normalFile.getPath()); 509 } 510 } 511 512 private String getSha1AndLength(String sha1, long length) { 513 return sha1 + ':' + length; 514 } 515 516 private void sha(NormalFile normalFile, File file, ProgressMonitor monitor) { 517 monitor.beginTask("Local sync...", (int)Math.min(file.length(), Integer.MAX_VALUE)); 518 try { 519 normalFile.getFileChunks().clear(); 520 transaction.flush(); 521 522 MessageDigest mdAll = MessageDigest.getInstance(HashUtil.HASH_ALGORITHM_SHA); 523 MessageDigest mdChunk = MessageDigest.getInstance(HashUtil.HASH_ALGORITHM_SHA); 524 525 final int bufLength = 32 * 1024; 526 final int chunkLength = 32 * bufLength; // 1 MiB chunk size 527 528 long offset = 0; 529 InputStream in = new FileInputStream(file); 530 try { 531 FileChunk fileChunk = null; 532 533 byte[] buf = new byte[bufLength]; 534 while (true) { 535 if (fileChunk == null) { 536 fileChunk = new FileChunk(); 537 fileChunk.setRepoFile(normalFile); 538 fileChunk.setOffset(offset); 539 fileChunk.setLength(0); 540 mdChunk.reset(); 541 } 542 543 int bytesRead = in.read(buf, 0, buf.length); 544 545 if (bytesRead > 0) { 546 mdAll.update(buf, 0, bytesRead); 547 mdChunk.update(buf, 0, bytesRead); 548 offset += bytesRead; 549 fileChunk.setLength(fileChunk.getLength() + bytesRead); 550 } 551 552 if (bytesRead < 0 || fileChunk.getLength() >= chunkLength) { 553 fileChunk.setSha1(HashUtil.encodeHexStr(mdChunk.digest())); 554 fileChunk.makeReadOnly(); 555 normalFile.getFileChunks().add(fileChunk); 556 fileChunk = null; 557 558 if (bytesRead < 0) { 559 break; 560 } 561 } 562 563 if (bytesRead > 0) 564 monitor.worked(bytesRead); 565 } 566 } finally { 567 in.close(); 568 } 569 normalFile.setSha1(HashUtil.encodeHexStr(mdAll.digest())); 570 normalFile.setLength(offset); 571 572 long fileLength = file.length(); // Important to check it now at the end. 573 if (fileLength != offset) { 574 logger.warn("sha: file.length() != bytesReadTotal :: File seems to be written concurrently! file='{}' file.length={} bytesReadTotal={}", 575 file, fileLength, offset); 576 } 577 } catch (NoSuchAlgorithmException e) { 578 throw new RuntimeException(e); 579 } catch (IOException e) { 580 throw new RuntimeException(e); 581 } finally { 582 monitor.done(); 583 } 584 } 585}