001package co.codewizards.cloudstore.local.transport; 002 003import static co.codewizards.cloudstore.core.io.StreamUtil.*; 004import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*; 005import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 006import static co.codewizards.cloudstore.core.util.AssertUtil.*; 007import static java.util.Objects.*; 008 009import java.io.IOException; 010import java.io.InputStream; 011import java.util.ArrayList; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017import java.util.Properties; 018import java.util.UUID; 019 020import javax.jdo.FetchPlan; 021 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import co.codewizards.cloudstore.core.config.Config; 026import co.codewizards.cloudstore.core.dto.ChangeSetDto; 027import co.codewizards.cloudstore.core.dto.ConfigPropSetDto; 028import co.codewizards.cloudstore.core.dto.CopyModificationDto; 029import co.codewizards.cloudstore.core.dto.DeleteModificationDto; 030import co.codewizards.cloudstore.core.dto.ModificationDto; 031import co.codewizards.cloudstore.core.dto.RepoFileDto; 032import co.codewizards.cloudstore.core.dto.RepositoryDto; 033import co.codewizards.cloudstore.core.oio.File; 034import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; 035import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction; 036import co.codewizards.cloudstore.core.repo.transport.RepoTransport; 037import co.codewizards.cloudstore.core.util.AssertUtil; 038import co.codewizards.cloudstore.local.ContextWithPersistenceManager; 039import co.codewizards.cloudstore.local.dto.DeleteModificationDtoConverter; 040import co.codewizards.cloudstore.local.dto.RepoFileDtoConverter; 041import co.codewizards.cloudstore.local.dto.RepositoryDtoConverter; 042import co.codewizards.cloudstore.local.persistence.CopyModification; 043import co.codewizards.cloudstore.local.persistence.DeleteModification; 044import co.codewizards.cloudstore.local.persistence.DeleteModificationDao; 045import co.codewizards.cloudstore.local.persistence.FetchGroupConst; 046import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepo; 047import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepoDao; 048import co.codewizards.cloudstore.local.persistence.LocalRepository; 049import co.codewizards.cloudstore.local.persistence.LocalRepositoryDao; 050import co.codewizards.cloudstore.local.persistence.Modification; 051import co.codewizards.cloudstore.local.persistence.ModificationDao; 052import co.codewizards.cloudstore.local.persistence.NormalFile; 053import co.codewizards.cloudstore.local.persistence.RemoteRepository; 054import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDao; 055import co.codewizards.cloudstore.local.persistence.RepoFile; 056import co.codewizards.cloudstore.local.persistence.RepoFileDao; 057 058public class ChangeSetDtoBuilder { 059 060 private static final Logger logger = LoggerFactory.getLogger(ChangeSetDtoBuilder.class); 061 062 private final LocalRepoTransaction transaction; 063 private final RepoTransport repoTransport; 064 private final UUID clientRepositoryId; 065 066 private static final UUID NULL_UUID = new UUID(0, 0); 067 068 /** 069 * The path-prefix of the opposite side. 070 * <p> 071 * For example, when we are building the {@code ChangeSetDto} on the server-side, then this is 072 * the prefix used by the client. Thus, let's assume that the client has checked-out the 073 * sub-directory "/documents", then this is the sub-directory on the server-side inside the server's 074 * root-directory. 075 * <p> 076 * If, in this same scenario, the {@code ChangeSetDto} is built on the client-side, then this 077 * is an empty string. 078 */ 079 private final String pathPrefix; 080 081 private LocalRepository localRepository; 082 private RemoteRepository remoteRepository; 083 private LastSyncToRemoteRepo lastSyncToRemoteRepo; 084 private Collection<Modification> modifications; 085 086 protected ChangeSetDtoBuilder(final LocalRepoTransaction transaction, final RepoTransport repoTransport) { 087 this.transaction = requireNonNull(transaction, "transaction"); 088 this.repoTransport = requireNonNull(repoTransport, "repoTransport"); 089 this.clientRepositoryId = requireNonNull(repoTransport.getClientRepositoryId(), "clientRepositoryId"); 090 this.pathPrefix = requireNonNull(repoTransport.getPathPrefix(), "pathPrefix"); 091 } 092 093 public static ChangeSetDtoBuilder create(final LocalRepoTransaction transaction, final RepoTransport repoTransport) { 094 return createObject(ChangeSetDtoBuilder.class, transaction, repoTransport); 095 } 096 097 public RepositoryDto prepareBuildChangeSetDto(Long lastSyncToRemoteRepoLocalRepositoryRevisionSynced) { 098 localRepository = null; remoteRepository = null; 099 lastSyncToRemoteRepo = null; modifications = null; 100 101 final LocalRepositoryDao localRepositoryDao = transaction.getDao(LocalRepositoryDao.class); 102 final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); 103 104 localRepository = localRepositoryDao.getLocalRepositoryOrFail(); 105 remoteRepository = remoteRepositoryDao.getRemoteRepositoryOrFail(clientRepositoryId); 106 107 // We must already read + return the RepositoryDto in this method, because the revision will already 108 // be incremented when buildChangeSetDto(...) is called! 109 // And we must do this, *BEFORE* changing anything, i.e. before prepareLastSyncToRemoteRepo(...)! 110 RepositoryDto repositoryDto = RepositoryDtoConverter.create().toRepositoryDto(localRepository); 111 112 prepareLastSyncToRemoteRepo(lastSyncToRemoteRepoLocalRepositoryRevisionSynced); 113 114 return repositoryDto; 115 } 116 117 public ChangeSetDto buildChangeSetDto(RepositoryDto repositoryDto) { 118 requireNonNull(repositoryDto, "repositoryDto"); 119 logger.trace(">>> buildChangeSetDto >>>"); 120 121 localRepository = null; remoteRepository = null; 122 lastSyncToRemoteRepo = null; modifications = null; 123 124 final ChangeSetDto changeSetDto = createObject(ChangeSetDto.class); 125 126 final LocalRepositoryDao localRepositoryDao = transaction.getDao(LocalRepositoryDao.class); 127 final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); 128 final ModificationDao modificationDao = transaction.getDao(ModificationDao.class); 129 final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class); 130 final LastSyncToRemoteRepoDao lastSyncToRemoteRepoDao = transaction.getDao(LastSyncToRemoteRepoDao.class); 131 132 localRepository = localRepositoryDao.getLocalRepositoryOrFail(); 133 remoteRepository = remoteRepositoryDao.getRemoteRepositoryOrFail(clientRepositoryId); 134 lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.getLastSyncToRemoteRepoOrFail(remoteRepository); 135 136 logger.trace("localRepositoryId: {}", localRepository.getRepositoryId()); 137 logger.trace("remoteRepositoryId: {}", remoteRepository.getRepositoryId()); 138// logger.trace("remoteRepository.localPathPrefix: {}", remoteRepository.getLocalPathPrefix()); // same as pathPrefix 139 logger.trace("pathPrefix: {}", pathPrefix); 140 141 changeSetDto.setRepositoryDto(repositoryDto); 142 143// prepareLastSyncToRemoteRepo(lastSyncToRemoteRepoLocalRepositoryRevisionSynced); 144 logger.info("buildChangeSetDto: localRepositoryId={} remoteRepositoryId={} localRepositoryRevisionSynced={} localRepositoryRevisionInProgress={}", 145 localRepository.getRepositoryId(), remoteRepository.getRepositoryId(), 146 lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), 147 lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress()); 148 149 ((ContextWithPersistenceManager)transaction).getPersistenceManager().getFetchPlan() 150 .setGroups(FetchPlan.DEFAULT, FetchGroupConst.CHANGE_SET_DTO); 151 152 modifications = modificationDao.getModificationsAfter(remoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 153 changeSetDto.setModificationDtos(toModificationDtos(modifications)); 154 155 if (!pathPrefix.isEmpty()) { 156 final Collection<DeleteModification> deleteModifications = transaction.getDao(DeleteModificationDao.class).getDeleteModificationsForPathOrParentOfPathAfter( 157 pathPrefix, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), remoteRepository); 158 if (!deleteModifications.isEmpty()) { // our virtual root was deleted => create synthetic DeleteModificationDto for virtual root 159 final DeleteModificationDto deleteModificationDto = new DeleteModificationDto(); 160 deleteModificationDto.setId(0); 161 deleteModificationDto.setLocalRevision(localRepository.getRevision()); 162 deleteModificationDto.setPath(""); 163 changeSetDto.getModificationDtos().add(deleteModificationDto); 164 } 165 } 166 167 final Collection<RepoFile> repoFiles = repoFileDao.getRepoFilesChangedAfterExclLastSyncFromRepositoryId( 168 lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), 169 lastSyncToRemoteRepo.isResyncMode() ? NULL_UUID : clientRepositoryId); 170 171 RepoFile pathPrefixRepoFile = null; // the virtual root for the client 172 if (!pathPrefix.isEmpty()) { 173 pathPrefixRepoFile = repoFileDao.getRepoFile(getLocalRepoManager().getLocalRoot(), getPathPrefixFile()); 174 } 175 final Map<Long, RepoFileDto> id2RepoFileDto = getId2RepoFileDtoWithParents(pathPrefixRepoFile, repoFiles, transaction); 176 changeSetDto.setRepoFileDtos(new ArrayList<RepoFileDto>(id2RepoFileDto.values())); 177 178 changeSetDto.setParentConfigPropSetDto(buildParentConfigPropSetDto()); 179 logger.trace("<<< buildChangeSetDto <<<"); 180 return changeSetDto; 181 } 182 183 protected void prepareLastSyncToRemoteRepo(Long lastSyncToRemoteRepoLocalRepositoryRevisionSynced) { 184 final LastSyncToRemoteRepoDao lastSyncToRemoteRepoDao = transaction.getDao(LastSyncToRemoteRepoDao.class); 185 lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.getLastSyncToRemoteRepo(remoteRepository); 186 if (lastSyncToRemoteRepo == null) { 187 lastSyncToRemoteRepo = new LastSyncToRemoteRepo(); 188 lastSyncToRemoteRepo.setRemoteRepository(remoteRepository); 189 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(-1); 190 } 191 if (lastSyncToRemoteRepoLocalRepositoryRevisionSynced != null) { 192 boolean resyncMode = lastSyncToRemoteRepoLocalRepositoryRevisionSynced.longValue() != lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(); 193 if (resyncMode) { 194 lastSyncToRemoteRepo.setResyncMode(true); 195 logger.warn("prepareLastSyncToRemoteRepo: Enabling resyncMode! lastSyncToRemoteRepoLocalRepositoryRevisionSynced={} overwrites lastSyncToRemoteRepo.localRepositoryRevisionSynced={}", 196 lastSyncToRemoteRepoLocalRepositoryRevisionSynced, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 197 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(lastSyncToRemoteRepoLocalRepositoryRevisionSynced); 198 } else { 199 if (lastSyncToRemoteRepo.isResyncMode()) { 200 logger.warn("prepareLastSyncToRemoteRepo: resyncMode still active! lastSyncToRemoteRepoLocalRepositoryRevisionSynced={}", 201 lastSyncToRemoteRepoLocalRepositoryRevisionSynced); 202 } 203 } 204 } 205 lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(localRepository.getRevision()); 206 lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.makePersistent(lastSyncToRemoteRepo); 207 } 208 209 /** 210 * @return the {@code ConfigPropSetDto} for the parent configs or <code>null</code>, if no sync needed. 211 */ 212 protected ConfigPropSetDto buildParentConfigPropSetDto() { 213 logger.trace(">>> buildConfigPropSetDto >>>"); 214 if (pathPrefix.isEmpty()) { 215 logger.debug("buildConfigPropSetDto: pathPrefix is empty => returning null."); 216 logger.trace("<<< buildConfigPropSetDto <<< null"); 217 return null; 218 } 219 220 final List<File> configFiles = getExistingConfigFilesAbovePathPrefix(); 221 if (! isFileModifiedAfterLastSync(configFiles) && ! isConfigFileDeletedAfterLastSync()) { 222 logger.trace("<<< buildConfigPropSetDto <<< null"); 223 return null; 224 } 225 226 final Properties properties = new Properties(); 227 for (final File configFile : configFiles) { 228 try { 229 try (InputStream in = castStream(configFile.createInputStream())) { 230 properties.load(in); // overwrites entries with same key 231 } 232 } catch (IOException e) { 233 throw new RuntimeException(e); 234 } 235 } 236 237 final ConfigPropSetDto result = new ConfigPropSetDto(properties); 238 239 logger.trace("<<< buildConfigPropSetDto <<< {}", result); 240 return result; 241 } 242 243 private boolean isConfigFileDeletedAfterLastSync() { 244 final String searchSuffix = "/" + Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY; 245 for (final Modification modification : requireNonNull(modifications, "modifications")) { 246 if (modification instanceof DeleteModification) { 247 final DeleteModification deleteModification = (DeleteModification) modification; 248 if (deleteModification.getPath().endsWith(searchSuffix)) { 249 logger.trace("isConfigFileDeletedAfterLastSync: returning true, because of deletion: {}", deleteModification.getPath()); 250 return true; 251 } 252 } 253 } 254 logger.trace("isConfigFileDeletedAfterLastSync: returning false"); 255 return false; 256 } 257 258 protected List<File> getExistingConfigFilesAbovePathPrefix() { 259 final ArrayList<File> result = new ArrayList<>(); 260 final File localRoot = transaction.getLocalRepoManager().getLocalRoot(); 261 262 File dir = getPathPrefixFile(); 263 while (! localRoot.equals(dir)) { 264 dir = requireNonNull(dir.getParentFile(), "dir.parentFile [dir=" + dir + "]"); 265 File configFile = dir.createFile(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY); 266 if (configFile.isFile()) { 267 result.add(configFile); 268 logger.trace("getExistingConfigFilesAbovePathPrefix: enlisted configFile: {}", configFile); 269 } 270 else 271 logger.trace("getExistingConfigFilesAbovePathPrefix: skipped non-existing configFile: {}", configFile); 272 } 273 274 // Highly unlikely, but maybe another client is connected to an already path-prefixed repository 275 // in a cascaded setup. 276 final File metaDir = localRoot.createFile(LocalRepoManager.META_DIR_NAME); 277 final File parentConfigFile = metaDir.createFile(Config.PROPERTIES_FILE_NAME_PARENT); 278 if (parentConfigFile.isFile()) { 279 result.add(parentConfigFile); 280 logger.trace("getExistingConfigFilesAbovePathPrefix: enlisted configFile: {}", parentConfigFile); 281 } 282 else 283 logger.trace("getExistingConfigFilesAbovePathPrefix: skipped non-existing configFile: {}", parentConfigFile); 284 285 Collections.reverse(result); // must be sorted according to inheritance hierarchy with following file overriding previous file 286 return result; 287 } 288 289 protected boolean isFileModifiedAfterLastSync(final Collection<File> files) { 290 requireNonNull(files, "files"); 291 requireNonNull(lastSyncToRemoteRepo, "lastSyncToRemoteRepo"); 292 293 final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class); 294 final File localRoot = transaction.getLocalRepoManager().getLocalRoot(); 295 for (final File file : files) { 296 RepoFile repoFile = repoFileDao.getRepoFile(localRoot, file); 297 if (repoFile == null) { 298 logger.warn("isFileModifiedAfterLastSync: RepoFile not found for (assuming it is new): {}", file); 299 return true; 300 } 301 if (repoFile.getLocalRevision() > lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()) { 302 logger.trace("isFileModifiedAfterLastSync: file modified: {}", file); 303 return true; 304 } 305 } 306 logger.trace("isFileModifiedAfterLastSync: returning false"); 307 return false; 308 } 309 310 protected File getPathPrefixFile() { 311 if (pathPrefix.isEmpty()) 312 return getLocalRepoManager().getLocalRoot(); 313 else 314 return createFile(getLocalRepoManager().getLocalRoot(), pathPrefix); 315 } 316 317 protected LocalRepoManager getLocalRepoManager() { 318 return transaction.getLocalRepoManager(); 319 } 320 321 private List<ModificationDto> toModificationDtos(final Collection<Modification> modifications) { 322 final long startTimestamp = System.currentTimeMillis(); 323 final List<ModificationDto> result = new ArrayList<ModificationDto>(requireNonNull(modifications, "modifications").size()); 324 for (final Modification modification : modifications) { 325 final ModificationDto modificationDto = toModificationDto(modification); 326 if (modificationDto != null) 327 result.add(modificationDto); 328 } 329 logger.debug("toModificationDtos: Creating {} ModificationDtos took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp); 330 return result; 331 } 332 333 private ModificationDto toModificationDto(final Modification modification) { 334 ModificationDto modificationDto; 335 if (modification instanceof CopyModification) { 336 final CopyModification copyModification = (CopyModification) modification; 337 338 String fromPath = copyModification.getFromPath(); 339 String toPath = copyModification.getToPath(); 340 if (!isPathUnderPathPrefix(fromPath) || !isPathUnderPathPrefix(toPath)) 341 return null; 342 343 fromPath = repoTransport.unprefixPath(fromPath); 344 toPath = repoTransport.unprefixPath(toPath); 345 346 final CopyModificationDto copyModificationDto = new CopyModificationDto(); 347 modificationDto = copyModificationDto; 348 copyModificationDto.setFromPath(fromPath); 349 copyModificationDto.setToPath(toPath); 350 } 351 else if (modification instanceof DeleteModification) { 352 final DeleteModification deleteModification = (DeleteModification) modification; 353 354 String path = deleteModification.getPath(); 355 if (!isPathUnderPathPrefix(path)) 356 return null; 357 358 path = repoTransport.unprefixPath(path); 359 360 modificationDto = DeleteModificationDtoConverter.create().toDeleteModificationDto(deleteModification); 361 ((DeleteModificationDto) modificationDto).setPath(path); 362 } 363 else 364 throw new IllegalArgumentException("Unknown modification type: " + modification); 365 366 modificationDto.setId(modification.getId()); 367 modificationDto.setLocalRevision(modification.getLocalRevision()); 368 369 return modificationDto; 370 } 371 372 private Map<Long, RepoFileDto> getId2RepoFileDtoWithParents(final RepoFile pathPrefixRepoFile, final Collection<RepoFile> repoFiles, final LocalRepoTransaction transaction) { 373 requireNonNull(transaction, "transaction"); 374 requireNonNull(repoFiles, "repoFiles"); 375 RepoFileDtoConverter repoFileDtoConverter = null; 376 final Map<Long, RepoFileDto> entityID2RepoFileDto = new HashMap<Long, RepoFileDto>(); 377 for (final RepoFile repoFile : repoFiles) { 378 RepoFile rf = repoFile; 379 if (rf instanceof NormalFile) { 380 final NormalFile nf = (NormalFile) rf; 381 if (nf.isInProgress()) { 382 continue; 383 } 384 } 385 386 if (pathPrefixRepoFile != null && !isDirectOrIndirectParent(pathPrefixRepoFile, rf)) 387 continue; 388 389 while (rf != null) { 390 RepoFileDto repoFileDto = entityID2RepoFileDto.get(rf.getId()); 391 if (repoFileDto == null) { 392 if (repoFileDtoConverter == null) 393 repoFileDtoConverter = RepoFileDtoConverter.create(transaction); 394 395 repoFileDto = repoFileDtoConverter.toRepoFileDto(rf, 0); 396 repoFileDto.setNeededAsParent(true); // initially true, but not default-value in DTO so that it is omitted in the XML, if it is false (the majority are false). 397 if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) { 398 repoFileDto.setParentId(null); // virtual root has no parent! 399 repoFileDto.setName(""); // virtual root has no name! 400 } 401 402 entityID2RepoFileDto.put(rf.getId(), repoFileDto); 403 } 404 405 if (repoFile == rf) 406 repoFileDto.setNeededAsParent(false); 407 408 if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) 409 break; 410 411 rf = rf.getParent(); 412 } 413 } 414 return entityID2RepoFileDto; 415 } 416 417 private boolean isDirectOrIndirectParent(final RepoFile parentRepoFile, final RepoFile repoFile) { 418 requireNonNull(parentRepoFile, "parentRepoFile"); 419 requireNonNull(repoFile, "repoFile"); 420 RepoFile rf = repoFile; 421 while (rf != null) { 422 if (parentRepoFile.equals(rf)) 423 return true; 424 425 rf = rf.getParent(); 426 } 427 return false; 428 } 429 430 protected boolean isPathUnderPathPrefix(final String path) { 431 requireNonNull(path, "path"); 432 if (pathPrefix.isEmpty()) 433 return true; 434 435 return path.startsWith(pathPrefix); 436 } 437}