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}