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 boolean isResyncMode() { 177 return resyncMode; 178 } 179 180 protected void prepareLastSyncToRemoteRepo(Long lastSyncToRemoteRepoLocalRepositoryRevisionSynced) { 181 final LastSyncToRemoteRepoDao lastSyncToRemoteRepoDao = transaction.getDao(LastSyncToRemoteRepoDao.class); 182 lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.getLastSyncToRemoteRepo(remoteRepository); 183 if (lastSyncToRemoteRepo == null) { 184 lastSyncToRemoteRepo = new LastSyncToRemoteRepo(); 185 lastSyncToRemoteRepo.setRemoteRepository(remoteRepository); 186 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(-1); 187 } 188 if (lastSyncToRemoteRepoLocalRepositoryRevisionSynced != null) { 189 resyncMode = lastSyncToRemoteRepoLocalRepositoryRevisionSynced.longValue() != lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(); 190 if (resyncMode) { 191 logger.warn("prepareLastSyncToRemoteRepo: Enabling resyncMode! lastSyncToRemoteRepoLocalRepositoryRevisionSynced={} overwrites lastSyncToRemoteRepo.localRepositoryRevisionSynced={}", 192 lastSyncToRemoteRepoLocalRepositoryRevisionSynced, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 193 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(lastSyncToRemoteRepoLocalRepositoryRevisionSynced); 194 } 195 } 196 lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(localRepository.getRevision()); 197 lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.makePersistent(lastSyncToRemoteRepo); 198 } 199 200 /** 201 * @return the {@code ConfigPropSetDto} for the parent configs or <code>null</code>, if no sync needed. 202 */ 203 protected ConfigPropSetDto buildParentConfigPropSetDto() { 204 logger.trace(">>> buildConfigPropSetDto >>>"); 205 if (pathPrefix.isEmpty()) { 206 logger.debug("buildConfigPropSetDto: pathPrefix is empty => returning null."); 207 logger.trace("<<< buildConfigPropSetDto <<< null"); 208 return null; 209 } 210 211 final List<File> configFiles = getExistingConfigFilesAbovePathPrefix(); 212 if (! isFileModifiedAfterLastSync(configFiles) && ! isConfigFileDeletedAfterLastSync()) { 213 logger.trace("<<< buildConfigPropSetDto <<< null"); 214 return null; 215 } 216 217 final Properties properties = new Properties(); 218 for (final File configFile : configFiles) { 219 try { 220 try (InputStream in = castStream(configFile.createInputStream())) { 221 properties.load(in); // overwrites entries with same key 222 } 223 } catch (IOException e) { 224 throw new RuntimeException(e); 225 } 226 } 227 228 final ConfigPropSetDto result = new ConfigPropSetDto(properties); 229 230 logger.trace("<<< buildConfigPropSetDto <<< {}", result); 231 return result; 232 } 233 234 private boolean isConfigFileDeletedAfterLastSync() { 235 final String searchSuffix = "/" + Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY; 236 for (final Modification modification : assertNotNull(modifications, "modifications")) { 237 if (modification instanceof DeleteModification) { 238 final DeleteModification deleteModification = (DeleteModification) modification; 239 if (deleteModification.getPath().endsWith(searchSuffix)) { 240 logger.trace("isConfigFileDeletedAfterLastSync: returning true, because of deletion: {}", deleteModification.getPath()); 241 return true; 242 } 243 } 244 } 245 logger.trace("isConfigFileDeletedAfterLastSync: returning false"); 246 return false; 247 } 248 249 protected List<File> getExistingConfigFilesAbovePathPrefix() { 250 final ArrayList<File> result = new ArrayList<>(); 251 final File localRoot = transaction.getLocalRepoManager().getLocalRoot(); 252 253 File dir = getPathPrefixFile(); 254 while (! localRoot.equals(dir)) { 255 dir = assertNotNull(dir.getParentFile(), "dir.parentFile [dir=" + dir + "]"); 256 File configFile = dir.createFile(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY); 257 if (configFile.isFile()) { 258 result.add(configFile); 259 logger.trace("getExistingConfigFilesAbovePathPrefix: enlisted configFile: {}", configFile); 260 } 261 else 262 logger.trace("getExistingConfigFilesAbovePathPrefix: skipped non-existing configFile: {}", configFile); 263 } 264 265 // Highly unlikely, but maybe another client is connected to an already path-prefixed repository 266 // in a cascaded setup. 267 final File metaDir = localRoot.createFile(LocalRepoManager.META_DIR_NAME); 268 final File parentConfigFile = metaDir.createFile(Config.PROPERTIES_FILE_NAME_PARENT); 269 if (parentConfigFile.isFile()) { 270 result.add(parentConfigFile); 271 logger.trace("getExistingConfigFilesAbovePathPrefix: enlisted configFile: {}", parentConfigFile); 272 } 273 else 274 logger.trace("getExistingConfigFilesAbovePathPrefix: skipped non-existing configFile: {}", parentConfigFile); 275 276 Collections.reverse(result); // must be sorted according to inheritance hierarchy with following file overriding previous file 277 return result; 278 } 279 280 protected boolean isFileModifiedAfterLastSync(final Collection<File> files) { 281 assertNotNull(files, "files"); 282 assertNotNull(lastSyncToRemoteRepo, "lastSyncToRemoteRepo"); 283 284 final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class); 285 final File localRoot = transaction.getLocalRepoManager().getLocalRoot(); 286 for (final File file : files) { 287 RepoFile repoFile = repoFileDao.getRepoFile(localRoot, file); 288 if (repoFile == null) { 289 logger.warn("isFileModifiedAfterLastSync: RepoFile not found for (assuming it is new): {}", file); 290 return true; 291 } 292 if (repoFile.getLocalRevision() > lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()) { 293 logger.trace("isFileModifiedAfterLastSync: file modified: {}", file); 294 return true; 295 } 296 } 297 logger.trace("isFileModifiedAfterLastSync: returning false"); 298 return false; 299 } 300 301 protected File getPathPrefixFile() { 302 if (pathPrefix.isEmpty()) 303 return getLocalRepoManager().getLocalRoot(); 304 else 305 return createFile(getLocalRepoManager().getLocalRoot(), pathPrefix); 306 } 307 308 protected LocalRepoManager getLocalRepoManager() { 309 return transaction.getLocalRepoManager(); 310 } 311 312 private List<ModificationDto> toModificationDtos(final Collection<Modification> modifications) { 313 final long startTimestamp = System.currentTimeMillis(); 314 final List<ModificationDto> result = new ArrayList<ModificationDto>(AssertUtil.assertNotNull(modifications, "modifications").size()); 315 for (final Modification modification : modifications) { 316 final ModificationDto modificationDto = toModificationDto(modification); 317 if (modificationDto != null) 318 result.add(modificationDto); 319 } 320 logger.debug("toModificationDtos: Creating {} ModificationDtos took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp); 321 return result; 322 } 323 324 private ModificationDto toModificationDto(final Modification modification) { 325 ModificationDto modificationDto; 326 if (modification instanceof CopyModification) { 327 final CopyModification copyModification = (CopyModification) modification; 328 329 String fromPath = copyModification.getFromPath(); 330 String toPath = copyModification.getToPath(); 331 if (!isPathUnderPathPrefix(fromPath) || !isPathUnderPathPrefix(toPath)) 332 return null; 333 334 fromPath = repoTransport.unprefixPath(fromPath); 335 toPath = repoTransport.unprefixPath(toPath); 336 337 final CopyModificationDto copyModificationDto = new CopyModificationDto(); 338 modificationDto = copyModificationDto; 339 copyModificationDto.setFromPath(fromPath); 340 copyModificationDto.setToPath(toPath); 341 } 342 else if (modification instanceof DeleteModification) { 343 final DeleteModification deleteModification = (DeleteModification) modification; 344 345 String path = deleteModification.getPath(); 346 if (!isPathUnderPathPrefix(path)) 347 return null; 348 349 path = repoTransport.unprefixPath(path); 350 351 modificationDto = DeleteModificationDtoConverter.create().toDeleteModificationDto(deleteModification); 352 ((DeleteModificationDto) modificationDto).setPath(path); 353 } 354 else 355 throw new IllegalArgumentException("Unknown modification type: " + modification); 356 357 modificationDto.setId(modification.getId()); 358 modificationDto.setLocalRevision(modification.getLocalRevision()); 359 360 return modificationDto; 361 } 362 363 private Map<Long, RepoFileDto> getId2RepoFileDtoWithParents(final RepoFile pathPrefixRepoFile, final Collection<RepoFile> repoFiles, final LocalRepoTransaction transaction) { 364 AssertUtil.assertNotNull(transaction, "transaction"); 365 AssertUtil.assertNotNull(repoFiles, "repoFiles"); 366 RepoFileDtoConverter repoFileDtoConverter = null; 367 final Map<Long, RepoFileDto> entityID2RepoFileDto = new HashMap<Long, RepoFileDto>(); 368 for (final RepoFile repoFile : repoFiles) { 369 RepoFile rf = repoFile; 370 if (rf instanceof NormalFile) { 371 final NormalFile nf = (NormalFile) rf; 372 if (nf.isInProgress()) { 373 continue; 374 } 375 } 376 377 if (pathPrefixRepoFile != null && !isDirectOrIndirectParent(pathPrefixRepoFile, rf)) 378 continue; 379 380 while (rf != null) { 381 RepoFileDto repoFileDto = entityID2RepoFileDto.get(rf.getId()); 382 if (repoFileDto == null) { 383 if (repoFileDtoConverter == null) 384 repoFileDtoConverter = RepoFileDtoConverter.create(transaction); 385 386 repoFileDto = repoFileDtoConverter.toRepoFileDto(rf, 0); 387 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). 388 if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) { 389 repoFileDto.setParentId(null); // virtual root has no parent! 390 repoFileDto.setName(""); // virtual root has no name! 391 } 392 393 entityID2RepoFileDto.put(rf.getId(), repoFileDto); 394 } 395 396 if (repoFile == rf) 397 repoFileDto.setNeededAsParent(false); 398 399 if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) 400 break; 401 402 rf = rf.getParent(); 403 } 404 } 405 return entityID2RepoFileDto; 406 } 407 408 private boolean isDirectOrIndirectParent(final RepoFile parentRepoFile, final RepoFile repoFile) { 409 AssertUtil.assertNotNull(parentRepoFile, "parentRepoFile"); 410 AssertUtil.assertNotNull(repoFile, "repoFile"); 411 RepoFile rf = repoFile; 412 while (rf != null) { 413 if (parentRepoFile.equals(rf)) 414 return true; 415 416 rf = rf.getParent(); 417 } 418 return false; 419 } 420 421 protected boolean isPathUnderPathPrefix(final String path) { 422 assertNotNull(path, "path"); 423 if (pathPrefix.isEmpty()) 424 return true; 425 426 return path.startsWith(pathPrefix); 427 } 428}