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