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