package co.codewizards.cloudstore.local.transport;
import static co.codewizards.cloudstore.core.io.StreamUtil.*;
import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*;
import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
import static co.codewizards.cloudstore.core.util.AssertUtil.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import javax.jdo.FetchPlan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.config.Config;
import co.codewizards.cloudstore.core.dto.ChangeSetDto;
import co.codewizards.cloudstore.core.dto.ConfigPropSetDto;
import co.codewizards.cloudstore.core.dto.CopyModificationDto;
import co.codewizards.cloudstore.core.dto.DeleteModificationDto;
import co.codewizards.cloudstore.core.dto.ModificationDto;
import co.codewizards.cloudstore.core.dto.RepoFileDto;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.repo.local.LocalRepoManager;
import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction;
import co.codewizards.cloudstore.core.repo.transport.RepoTransport;
import co.codewizards.cloudstore.core.util.AssertUtil;
import co.codewizards.cloudstore.local.LocalRepoTransactionImpl;
import co.codewizards.cloudstore.local.dto.DeleteModificationDtoConverter;
import co.codewizards.cloudstore.local.dto.RepoFileDtoConverter;
import co.codewizards.cloudstore.local.dto.RepositoryDtoConverter;
import co.codewizards.cloudstore.local.persistence.CopyModification;
import co.codewizards.cloudstore.local.persistence.DeleteModification;
import co.codewizards.cloudstore.local.persistence.DeleteModificationDao;
import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepo;
import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepoDao;
import co.codewizards.cloudstore.local.persistence.LocalRepository;
import co.codewizards.cloudstore.local.persistence.LocalRepositoryDao;
import co.codewizards.cloudstore.local.persistence.Modification;
import co.codewizards.cloudstore.local.persistence.ModificationDao;
import co.codewizards.cloudstore.local.persistence.NormalFile;
import co.codewizards.cloudstore.local.persistence.RemoteRepository;
import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDao;
import co.codewizards.cloudstore.local.persistence.RepoFile;
import co.codewizards.cloudstore.local.persistence.RepoFileDao;
public class ChangeSetDtoBuilder {
private static final Logger logger = LoggerFactory.getLogger(ChangeSetDtoBuilder.class);
private final LocalRepoTransaction transaction;
private final RepoTransport repoTransport;
private final UUID clientRepositoryId;
/**
* The path-prefix of the opposite side.
* <p>
* For example, when we are building the {@code ChangeSetDto} on the server-side, then this is
* the prefix used by the client. Thus, let's assume that the client has checked-out the
* sub-directory "/documents", then this is the sub-directory on the server-side inside the server's
* root-directory.
* <p>
* If, in this same scenario, the {@code ChangeSetDto} is built on the client-side, then this
* is an empty string.
*/
private final String pathPrefix;
private LocalRepository localRepository;
private RemoteRepository remoteRepository;
private LastSyncToRemoteRepo lastSyncToRemoteRepo;
private Collection<Modification> modifications;
protected ChangeSetDtoBuilder(final LocalRepoTransaction transaction, final RepoTransport repoTransport) {
this.transaction = assertNotNull(transaction, "transaction");
this.repoTransport = assertNotNull(repoTransport, "repoTransport");
this.clientRepositoryId = assertNotNull(repoTransport.getClientRepositoryId(), "clientRepositoryId");
this.pathPrefix = assertNotNull(repoTransport.getPathPrefix(), "pathPrefix");
}
public static ChangeSetDtoBuilder create(final LocalRepoTransaction transaction, final RepoTransport repoTransport) {
return createObject(ChangeSetDtoBuilder.class, transaction, repoTransport);
}
public ChangeSetDto buildChangeSetDto() {
logger.trace(">>> buildChangeSetDto >>>");
localRepository = null; remoteRepository = null;
lastSyncToRemoteRepo = null; modifications = null;
final ChangeSetDto changeSetDto = createObject(ChangeSetDto.class);
final LocalRepositoryDao localRepositoryDao = transaction.getDao(LocalRepositoryDao.class);
final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class);
final ModificationDao modificationDao = transaction.getDao(ModificationDao.class);
final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class);
localRepository = localRepositoryDao.getLocalRepositoryOrFail();
remoteRepository = remoteRepositoryDao.getRemoteRepositoryOrFail(clientRepositoryId);
logger.trace("localRepositoryId: {}", localRepository.getRepositoryId());
logger.trace("remoteRepositoryId: {}", remoteRepository.getRepositoryId());
// logger.trace("remoteRepository.localPathPrefix: {}", remoteRepository.getLocalPathPrefix()); // same as pathPrefix
logger.trace("pathPrefix: {}", pathPrefix);
changeSetDto.setRepositoryDto(RepositoryDtoConverter.create().toRepositoryDto(localRepository));
prepareLastSyncToRemoteRepo();
logger.info("buildChangeSetDto: localRepositoryId={} remoteRepositoryId={} localRepositoryRevisionSynced={} localRepositoryRevisionInProgress={}",
localRepository.getRepositoryId(), remoteRepository.getRepositoryId(),
lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(),
lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress());
((LocalRepoTransactionImpl)transaction).getPersistenceManager().getFetchPlan().setGroup(FetchPlan.ALL);
modifications = modificationDao.getModificationsAfter(remoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced());
changeSetDto.setModificationDtos(toModificationDtos(modifications));
if (!pathPrefix.isEmpty()) {
final Collection<DeleteModification> deleteModifications = transaction.getDao(DeleteModificationDao.class).getDeleteModificationsForPathOrParentOfPathAfter(
pathPrefix, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), remoteRepository);
if (!deleteModifications.isEmpty()) { // our virtual root was deleted => create synthetic DeleteModificationDto for virtual root
final DeleteModificationDto deleteModificationDto = new DeleteModificationDto();
deleteModificationDto.setId(0);
deleteModificationDto.setLocalRevision(localRepository.getRevision());
deleteModificationDto.setPath("");
changeSetDto.getModificationDtos().add(deleteModificationDto);
}
}
final Collection<RepoFile> repoFiles = repoFileDao.getRepoFilesChangedAfterExclLastSyncFromRepositoryId(
lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), clientRepositoryId);
RepoFile pathPrefixRepoFile = null; // the virtual root for the client
if (!pathPrefix.isEmpty()) {
pathPrefixRepoFile = repoFileDao.getRepoFile(getLocalRepoManager().getLocalRoot(), getPathPrefixFile());
}
final Map<Long, RepoFileDto> id2RepoFileDto = getId2RepoFileDtoWithParents(pathPrefixRepoFile, repoFiles, transaction);
changeSetDto.setRepoFileDtos(new ArrayList<RepoFileDto>(id2RepoFileDto.values()));
changeSetDto.setParentConfigPropSetDto(buildParentConfigPropSetDto());
logger.trace("<<< buildChangeSetDto <<<");
return changeSetDto;
}
protected void prepareLastSyncToRemoteRepo() {
final LastSyncToRemoteRepoDao lastSyncToRemoteRepoDao = transaction.getDao(LastSyncToRemoteRepoDao.class);
lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.getLastSyncToRemoteRepo(remoteRepository);
if (lastSyncToRemoteRepo == null) {
lastSyncToRemoteRepo = new LastSyncToRemoteRepo();
lastSyncToRemoteRepo.setRemoteRepository(remoteRepository);
lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(-1);
}
lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(localRepository.getRevision());
lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.makePersistent(lastSyncToRemoteRepo);
}
/**
* @return the {@code ConfigPropSetDto} for the parent configs or <code>null</code>, if no sync needed.
*/
protected ConfigPropSetDto buildParentConfigPropSetDto() {
logger.trace(">>> buildConfigPropSetDto >>>");
if (pathPrefix.isEmpty()) {
logger.debug("buildConfigPropSetDto: pathPrefix is empty => returning null.");
logger.trace("<<< buildConfigPropSetDto <<< null");
return null;
}
final List<File> configFiles = getExistingConfigFilesAbovePathPrefix();
if (! isFileModifiedAfterLastSync(configFiles) && ! isConfigFileDeletedAfterLastSync()) {
logger.trace("<<< buildConfigPropSetDto <<< null");
return null;
}
final Properties properties = new Properties();
for (final File configFile : configFiles) {
try {
try (InputStream in = castStream(configFile.createInputStream())) {
properties.load(in); // overwrites entries with same key
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
final ConfigPropSetDto result = new ConfigPropSetDto(properties);
logger.trace("<<< buildConfigPropSetDto <<< {}", result);
return result;
}
private boolean isConfigFileDeletedAfterLastSync() {
final String searchSuffix = "/" + Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY;
for (final Modification modification : assertNotNull(modifications, "modifications")) {
if (modification instanceof DeleteModification) {
final DeleteModification deleteModification = (DeleteModification) modification;
if (deleteModification.getPath().endsWith(searchSuffix)) {
logger.trace("isConfigFileDeletedAfterLastSync: returning true, because of deletion: {}", deleteModification.getPath());
return true;
}
}
}
logger.trace("isConfigFileDeletedAfterLastSync: returning false");
return false;
}
protected List<File> getExistingConfigFilesAbovePathPrefix() {
final ArrayList<File> result = new ArrayList<>();
final File localRoot = transaction.getLocalRepoManager().getLocalRoot();
File dir = getPathPrefixFile();
while (! localRoot.equals(dir)) {
dir = assertNotNull(dir.getParentFile(), "dir.parentFile [dir=" + dir + "]");
File configFile = dir.createFile(Config.PROPERTIES_FILE_NAME_FOR_DIRECTORY);
if (configFile.isFile()) {
result.add(configFile);
logger.trace("getExistingConfigFilesAbovePathPrefix: enlisted configFile: {}", configFile);
}
else
logger.trace("getExistingConfigFilesAbovePathPrefix: skipped non-existing configFile: {}", configFile);
}
// Highly unlikely, but maybe another client is connected to an already path-prefixed repository
// in a cascaded setup.
final File metaDir = localRoot.createFile(LocalRepoManager.META_DIR_NAME);
final File parentConfigFile = metaDir.createFile(Config.PROPERTIES_FILE_NAME_PARENT);
if (parentConfigFile.isFile()) {
result.add(parentConfigFile);
logger.trace("getExistingConfigFilesAbovePathPrefix: enlisted configFile: {}", parentConfigFile);
}
else
logger.trace("getExistingConfigFilesAbovePathPrefix: skipped non-existing configFile: {}", parentConfigFile);
Collections.reverse(result); // must be sorted according to inheritance hierarchy with following file overriding previous file
return result;
}
protected boolean isFileModifiedAfterLastSync(final Collection<File> files) {
assertNotNull(files, "files");
assertNotNull(lastSyncToRemoteRepo, "lastSyncToRemoteRepo");
final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class);
final File localRoot = transaction.getLocalRepoManager().getLocalRoot();
for (final File file : files) {
RepoFile repoFile = repoFileDao.getRepoFile(localRoot, file);
if (repoFile == null) {
logger.warn("isFileModifiedAfterLastSync: RepoFile not found for (assuming it is new): {}", file);
return true;
}
if (repoFile.getLocalRevision() > lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()) {
logger.trace("isFileModifiedAfterLastSync: file modified: {}", file);
return true;
}
}
logger.trace("isFileModifiedAfterLastSync: returning false");
return false;
}
protected File getPathPrefixFile() {
if (pathPrefix.isEmpty())
return getLocalRepoManager().getLocalRoot();
else
return createFile(getLocalRepoManager().getLocalRoot(), pathPrefix);
}
protected LocalRepoManager getLocalRepoManager() {
return transaction.getLocalRepoManager();
}
private List<ModificationDto> toModificationDtos(final Collection<Modification> modifications) {
final long startTimestamp = System.currentTimeMillis();
final List<ModificationDto> result = new ArrayList<ModificationDto>(AssertUtil.assertNotNull(modifications, "modifications").size());
for (final Modification modification : modifications) {
final ModificationDto modificationDto = toModificationDto(modification);
if (modificationDto != null)
result.add(modificationDto);
}
logger.debug("toModificationDtos: Creating {} ModificationDtos took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp);
return result;
}
private ModificationDto toModificationDto(final Modification modification) {
ModificationDto modificationDto;
if (modification instanceof CopyModification) {
final CopyModification copyModification = (CopyModification) modification;
String fromPath = copyModification.getFromPath();
String toPath = copyModification.getToPath();
if (!isPathUnderPathPrefix(fromPath) || !isPathUnderPathPrefix(toPath))
return null;
fromPath = repoTransport.unprefixPath(fromPath);
toPath = repoTransport.unprefixPath(toPath);
final CopyModificationDto copyModificationDto = new CopyModificationDto();
modificationDto = copyModificationDto;
copyModificationDto.setFromPath(fromPath);
copyModificationDto.setToPath(toPath);
}
else if (modification instanceof DeleteModification) {
final DeleteModification deleteModification = (DeleteModification) modification;
String path = deleteModification.getPath();
if (!isPathUnderPathPrefix(path))
return null;
path = repoTransport.unprefixPath(path);
modificationDto = DeleteModificationDtoConverter.create().toDeleteModificationDto(deleteModification);
((DeleteModificationDto) modificationDto).setPath(path);
}
else
throw new IllegalArgumentException("Unknown modification type: " + modification);
modificationDto.setId(modification.getId());
modificationDto.setLocalRevision(modification.getLocalRevision());
return modificationDto;
}
private Map<Long, RepoFileDto> getId2RepoFileDtoWithParents(final RepoFile pathPrefixRepoFile, final Collection<RepoFile> repoFiles, final LocalRepoTransaction transaction) {
AssertUtil.assertNotNull(transaction, "transaction");
AssertUtil.assertNotNull(repoFiles, "repoFiles");
RepoFileDtoConverter repoFileDtoConverter = null;
final Map<Long, RepoFileDto> entityID2RepoFileDto = new HashMap<Long, RepoFileDto>();
for (final RepoFile repoFile : repoFiles) {
RepoFile rf = repoFile;
if (rf instanceof NormalFile) {
final NormalFile nf = (NormalFile) rf;
if (nf.isInProgress()) {
continue;
}
}
if (pathPrefixRepoFile != null && !isDirectOrIndirectParent(pathPrefixRepoFile, rf))
continue;
while (rf != null) {
RepoFileDto repoFileDto = entityID2RepoFileDto.get(rf.getId());
if (repoFileDto == null) {
if (repoFileDtoConverter == null)
repoFileDtoConverter = RepoFileDtoConverter.create(transaction);
repoFileDto = repoFileDtoConverter.toRepoFileDto(rf, 0);
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).
if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) {
repoFileDto.setParentId(null); // virtual root has no parent!
repoFileDto.setName(""); // virtual root has no name!
}
entityID2RepoFileDto.put(rf.getId(), repoFileDto);
}
if (repoFile == rf)
repoFileDto.setNeededAsParent(false);
if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf))
break;
rf = rf.getParent();
}
}
return entityID2RepoFileDto;
}
private boolean isDirectOrIndirectParent(final RepoFile parentRepoFile, final RepoFile repoFile) {
AssertUtil.assertNotNull(parentRepoFile, "parentRepoFile");
AssertUtil.assertNotNull(repoFile, "repoFile");
RepoFile rf = repoFile;
while (rf != null) {
if (parentRepoFile.equals(rf))
return true;
rf = rf.getParent();
}
return false;
}
protected boolean isPathUnderPathPrefix(final String path) {
assertNotNull(path, "path");
if (pathPrefix.isEmpty())
return true;
return path.startsWith(pathPrefix);
}
}