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