package co.codewizards.cloudstore.local.persistence; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.Util.*; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.UUID; import javax.jdo.JDOHelper; import javax.jdo.annotations.Discriminator; import javax.jdo.annotations.DiscriminatorStrategy; import javax.jdo.annotations.Index; import javax.jdo.annotations.Indices; import javax.jdo.annotations.NullValue; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.Queries; import javax.jdo.annotations.Query; import javax.jdo.annotations.Unique; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.util.AssertUtil; @PersistenceCapable @Discriminator(strategy=DiscriminatorStrategy.VALUE_MAP) @Unique(name="RepoFile_parent_name", members={"parent", "name"}) @Indices({ @Index(name="RepoFile_parent", members={"parent"}), @Index(name="RepoFile_localRevision", members={"localRevision"}) }) @Queries({ @Query(name="getChildRepoFile_parent_name", value="SELECT UNIQUE WHERE this.parent == :parent && this.name == :name"), @Query(name="getChildRepoFiles_parent", value="SELECT WHERE this.parent == :parent"), @Query( name="getRepoFilesChangedAfter_localRevision_exclLastSyncFromRepositoryId", value="SELECT WHERE this.localRevision > :localRevision && (this.lastSyncFromRepositoryId == null || this.lastSyncFromRepositoryId != :lastSyncFromRepositoryId)") // TODO this necessary == null is IMHO a DN bug! }) public abstract class RepoFile extends Entity implements AutoTrackLocalRevision { private static final Logger logger = LoggerFactory.getLogger(RepoFile.class); private RepoFile parent; @Persistent(nullValue=NullValue.EXCEPTION) private String name; private long localRevision; @Persistent(nullValue = NullValue.EXCEPTION) private Date lastModified; // TODO 1: The direct partner-repository from which this was synced, should be a real relation to the RemoteRepository, // because this is more efficient (not a String, but a long id). // TODO 2: We should additionally store (and forward) the origin repositoryId (UUID/String) to use this feature during // circular syncs over multiple repos - e.g. repoA ---> repoB ---> repoC ---> repoA (again) - this circle would currently // cause https://github.com/cloudstore/cloudstore/issues/25 again (because issue 25 is only solved for direct partners - not indirect). // TODO 3: We should switch from UUID to Uid everywhere (most importantly the repositoryId). // Careful, though: Uid's String-representation is case-sensitive! Due to Windows, it must thus not be used for file names! private String lastSyncFromRepositoryId; public RepoFile getParent() { return parent; } public void setParent(final RepoFile parent) { if (! equal(this.parent, parent)) this.parent = parent; } public String getName() { return name; } public void setName(final String name) { if (! equal(this.name, name)) this.name = name; } /** * {@inheritDoc} * <p> * Note that this does not include modifications of children (in case this is a directory). * If a child is modified, solely this child's localRevision is updated. */ @Override public long getLocalRevision() { return localRevision; } @Override public void setLocalRevision(final long localRevision) { if (! equal(this.localRevision, localRevision)) { if (logger.isDebugEnabled()) { final LocalRepository localRepository = new LocalRepositoryDao().persistenceManager(JDOHelper.getPersistenceManager(this)).getLocalRepositoryOrFail(); logger.debug("setLocalRevision: localRepositoryId={} path='{}' old={} new={}", localRepository.getRepositoryId(), getPath(), this.localRevision, localRevision); } this.localRevision = localRevision; } } /** * Gets the path within the repository from the {@link LocalRepository#getRoot() root} (including) to <code>this</code> (including). * <p> * The first element in the list is the {@code root}. The last element is <code>this</code>. * <p> * If this method is called on the {@code root} itself, the result will be a list with one single element (the root itself). * @return the path within the repository from the {@link LocalRepository#getRoot() root} (including) to <code>this</code> (including). Never <code>null</code>. */ public List<RepoFile> getPathList() { final LinkedList<RepoFile> path = new LinkedList<RepoFile>(); RepoFile rf = this; while (rf != null) { path.addFirst(rf); rf = rf.getParent(); } return Collections.unmodifiableList(path); } /** * Gets the path from the root to <code>this</code>. * <p> * The path's elements are separated by a slash ("/"). The path starts with a slash (like an absolute path), but * is relative to the repository's local root. * @return the path from the root to <code>this</code>. Never <code>null</code>. The repository's root itself has the path "/". */ public String getPath() { final StringBuilder sb = new StringBuilder(); for (final RepoFile repoFile : getPathList()) { if (sb.length() == 0 || sb.charAt(sb.length() - 1) != '/') sb.append('/'); sb.append(repoFile.getName()); } return sb.toString(); } /** * Gets the {@link File} represented by this {@link RepoFile} inside the given repository's {@code localRoot} directory. * @param localRoot the repository's root directory. * @return the {@link File} represented by this {@link RepoFile} inside the given repository's {@code localRoot} directory. */ public File getFile(final File localRoot) { AssertUtil.assertNotNull(localRoot, "localRoot"); File result = localRoot; for (final RepoFile repoFile : getPathList()) { if (repoFile.getParent() == null) // skip the root continue; result = createFile(result, repoFile.getName()); } return result; } /** * Gets the timestamp of the file's last modification. * <p> * It reflects the {@link File#lastModified() File.lastModified} property. * @return the timestamp of the file's last modification. */ public Date getLastModified() { return lastModified; } public void setLastModified(final Date lastModified) { if (! equal(this.lastModified, lastModified)) this.lastModified = lastModified; } public UUID getLastSyncFromRepositoryId() { return lastSyncFromRepositoryId == null ? null : UUID.fromString(lastSyncFromRepositoryId); } public void setLastSyncFromRepositoryId(final UUID repositoryId) { if (! equal(this.getLastSyncFromRepositoryId(), repositoryId)) this.lastSyncFromRepositoryId = repositoryId == null ? null : repositoryId.toString(); } }