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