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