001package co.codewizards.cloudstore.local.persistence;
002
003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
004import static co.codewizards.cloudstore.core.util.Util.*;
005import static java.util.Objects.*;
006
007import java.util.Collections;
008import java.util.Date;
009import java.util.LinkedList;
010import java.util.List;
011import java.util.UUID;
012
013import javax.jdo.JDOHelper;
014import javax.jdo.annotations.Discriminator;
015import javax.jdo.annotations.DiscriminatorStrategy;
016import javax.jdo.annotations.FetchGroup;
017import javax.jdo.annotations.FetchGroups;
018import javax.jdo.annotations.Index;
019import javax.jdo.annotations.Indices;
020import javax.jdo.annotations.NullValue;
021import javax.jdo.annotations.PersistenceCapable;
022import javax.jdo.annotations.Persistent;
023import javax.jdo.annotations.Queries;
024import javax.jdo.annotations.Query;
025import javax.jdo.annotations.Unique;
026
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import co.codewizards.cloudstore.core.oio.File;
031
032@PersistenceCapable
033@Discriminator(strategy = DiscriminatorStrategy.VALUE_MAP)
034@Unique(name = "RepoFile_parent_name", members = {"parent", "name"})
035@Indices({
036        @Index(name = "RepoFile_parent", members = {"parent"}),
037        @Index(name = "RepoFile_localRevision", members = {"localRevision"})
038})
039@Queries({
040        @Query(name = "getChildRepoFile_parent_name", value = "SELECT UNIQUE WHERE this.parent == :parent && this.name == :name"),
041        @Query(name = "getChildRepoFiles_parent", value = "SELECT WHERE this.parent == :parent"),
042        @Query(
043                        name = "getRepoFilesChangedAfter_localRevision_exclLastSyncFromRepositoryId",
044                        value = "SELECT WHERE this.localRevision > :localRevision && (this.lastSyncFromRepositoryId == null || this.lastSyncFromRepositoryId != :lastSyncFromRepositoryId)") // TODO this necessary == null is IMHO a DN bug!
045})
046@FetchGroups({
047        @FetchGroup(name = FetchGroupConst.CHANGE_SET_DTO, members = {@Persistent(name = "parent")})
048})
049public abstract class RepoFile extends Entity implements AutoTrackLocalRevision {
050        private static final Logger logger = LoggerFactory.getLogger(RepoFile.class);
051
052        private RepoFile parent;
053
054        @Persistent(nullValue=NullValue.EXCEPTION)
055        private String name;
056
057        private long localRevision;
058
059        @Persistent(nullValue = NullValue.EXCEPTION)
060        private Date lastModified;
061
062        // TODO 1: The direct partner-repository from which this was synced, should be a real relation to the RemoteRepository,
063        // because this is more efficient (not a String, but a long id).
064        // TODO 2: We should additionally store (and forward) the origin repositoryId (UUID/String) to use this feature during
065        // circular syncs over multiple repos - e.g. repoA ---> repoB ---> repoC ---> repoA (again) - this circle would currently
066        // cause https://github.com/cloudstore/cloudstore/issues/25 again (because issue 25 is only solved for direct partners - not indirect).
067        // TODO 3: We should switch from UUID to Uid everywhere (most importantly the repositoryId).
068        // Careful, though: Uid's String-representation is case-sensitive! Due to Windows, it must thus not be used for file names!
069        private String lastSyncFromRepositoryId;
070
071        public RepoFile getParent() {
072                return parent;
073        }
074        public void setParent(final RepoFile parent) {
075                if (! equal(this.parent, parent))
076                        this.parent = parent;
077        }
078
079        public String getName() {
080                return name;
081        }
082        public void setName(final String name) {
083                if (! equal(this.name, name))
084                        this.name = name;
085        }
086
087        /**
088         * {@inheritDoc}
089         * <p>
090         * Note that this does not include modifications of children (in case this is a directory).
091         * If a child is modified, solely this child's localRevision is updated.
092         */
093        @Override
094        public long getLocalRevision() {
095                return localRevision;
096        }
097        @Override
098        public void setLocalRevision(final long localRevision) {
099                if (! equal(this.localRevision, localRevision)) {
100                        if (logger.isDebugEnabled()) {
101                                final LocalRepository localRepository = new LocalRepositoryDao().persistenceManager(JDOHelper.getPersistenceManager(this)).getLocalRepositoryOrFail();
102                                logger.debug("setLocalRevision: localRepositoryId={} path='{}' old={} new={}", localRepository.getRepositoryId(), getPath(), this.localRevision, localRevision);
103                        }
104                        this.localRevision = localRevision;
105                }
106        }
107
108        /**
109         * Gets the path within the repository from the {@link LocalRepository#getRoot() root} (including) to <code>this</code> (including).
110         * <p>
111         * The first element in the list is the {@code root}. The last element is <code>this</code>.
112         * <p>
113         * If this method is called on the {@code root} itself, the result will be a list with one single element (the root itself).
114         * @return the path within the repository from the {@link LocalRepository#getRoot() root} (including) to <code>this</code> (including). Never <code>null</code>.
115         */
116        public List<RepoFile> getPathList() {
117                final LinkedList<RepoFile> path = new LinkedList<RepoFile>();
118                RepoFile rf = this;
119                while (rf != null) {
120                        path.addFirst(rf);
121                        rf = rf.getParent();
122                }
123                return Collections.unmodifiableList(path);
124        }
125
126        /**
127         * Gets the path from the root to <code>this</code>.
128         * <p>
129         * The path's elements are separated by a slash ("/"). The path starts with a slash (like an absolute path), but
130         * is relative to the repository's local root.
131         * @return the path from the root to <code>this</code>. Never <code>null</code>. The repository's root itself has the path "/".
132         */
133        public String getPath() {
134                final StringBuilder sb = new StringBuilder();
135                for (final RepoFile repoFile : getPathList()) {
136                        if (sb.length() == 0 || sb.charAt(sb.length() - 1) != '/')
137                                sb.append('/');
138
139                        sb.append(repoFile.getName());
140                }
141                return sb.toString();
142        }
143
144        /**
145         * Gets the {@link File} represented by this {@link RepoFile} inside the given repository's {@code localRoot} directory.
146         * @param localRoot the repository's root directory.
147         * @return the {@link File} represented by this {@link RepoFile} inside the given repository's {@code localRoot} directory.
148         */
149        public File getFile(final File localRoot) {
150                requireNonNull(localRoot, "localRoot");
151                File result = localRoot;
152                for (final RepoFile repoFile : getPathList()) {
153                        if (repoFile.getParent() == null) // skip the root
154                                continue;
155
156                        result = createFile(result, repoFile.getName());
157                }
158                return result;
159        }
160
161        /**
162         * Gets the timestamp of the file's last modification.
163         * <p>
164         * It reflects the {@link File#lastModified() File.lastModified} property.
165         * @return the timestamp of the file's last modification.
166         */
167        public Date getLastModified() {
168                return lastModified;
169        }
170        public void setLastModified(final Date lastModified) {
171                if (! equal(this.lastModified, lastModified))
172                        this.lastModified = lastModified;
173        }
174
175        public UUID getLastSyncFromRepositoryId() {
176                return lastSyncFromRepositoryId == null ? null : UUID.fromString(lastSyncFromRepositoryId);
177        }
178        public void setLastSyncFromRepositoryId(final UUID repositoryId) {
179                if (! equal(this.getLastSyncFromRepositoryId(), repositoryId))
180                        this.lastSyncFromRepositoryId = repositoryId == null ? null : repositoryId.toString();
181        }
182}