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