Commit b37e4523 authored by Matija Obreza's avatar Matija Obreza

Merge branch '23-repository-paths' into 'master'

Resolve "Repository Paths"

Closes #23

See merge request !22
parents 7deb9686 7a2277de
Pipeline #6547 passed with stage
in 2 minutes and 49 seconds
......@@ -172,12 +172,6 @@
<artifactId>application-blocks-security</artifactId>
<version>1.5-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
......
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository;
/**
* The InvalidRepositoryPathException is thrown when Repository is not happy
* with your selected path.
*/
public class FolderNotEmptyException extends FileRepositoryException {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = 1L;
/**
* Instantiates a new invalid repository path exception.
*
* @param message the message
*/
public FolderNotEmptyException(final String message) {
super(message);
}
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.migration;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.genesys.filerepository.model.QRepositoryFile;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.persistence.RepositoryFilePersistence;
import org.genesys.filerepository.service.RepositoryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import com.querydsl.jpa.impl.JPAQueryFactory;
/**
* Creates {@link RepositoryFolder} entries and their parents - Links all
* {@link RepositoryFile} to their parent folders
*
* @author Matija Obreza
*/
@Component
public class RepositoryUpgrade20180920 {
private static final Logger LOG = LoggerFactory.getLogger(RepositoryUpgrade20180920.class);
/** The tx manager. */
@Autowired
@Qualifier("transactionManager")
protected PlatformTransactionManager txManager;
@Autowired
private RepositoryFilePersistence fileRepository;
@Autowired
private RepositoryService repositoryService;
@Autowired
private JPAQueryFactory jpaQueryFactory;
public void doUpgrade() throws Exception {
TransactionTemplate tmpl = new TransactionTemplate(txManager);
tmpl.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
makeFolders();
} catch (Throwable e) {
LOG.error("Could not migrate 1.0 to 1.1 model: {}", e.getMessage(), e);
}
}
});
tmpl.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
moveFilesToFolders();
} catch (Throwable e) {
LOG.error("Could not migrate 1.0 to 1.1 model: {}", e.getMessage(), e);
}
}
});
}
/**
* Do the migration.
*
* @throws Exception the exception
*/
void makeFolders() throws Exception {
long count = 0;
// get distinct paths
List<String> paths = jpaQueryFactory.selectFrom(QRepositoryFile.repositoryFile).select(QRepositoryFile.repositoryFile.path).distinct().orderBy(
QRepositoryFile.repositoryFile.path.asc()).fetch();
for (String folderName : paths) {
final Path folderPath = Paths.get(folderName);
RepositoryFolder folder = repositoryService.getFolder(folderPath);
if (folder == null) {
LOG.warn("Creating repository path={}", folderPath);
folder = repositoryService.ensureFolder(folderPath);
LOG.warn("Created folder={}", folder.getFolderPath());
count++;
}
}
if (count == 0) {
LOG.warn("\n\n\t** All repository folders have been created **\n\t You can remove the {} bean.\n\n", this.getClass().getName());
}
}
/**
* Do the migration.
*
* @throws Exception the exception
*/
void moveFilesToFolders() throws Exception {
long count = 0;
for (RepositoryFile repoFile : fileRepository.findAll()) {
if (repoFile.getFolder() == null) {
RepositoryFolder repoFolder = repositoryService.getFolder(Paths.get(repoFile.getPath()));
if (repoFile.getFolder() == null || repoFile.getFolder().getId() != repoFolder.getId()) {
LOG.warn("Upgrading {} to folder={}", repoFile.getPath(), repoFolder.getPath());
repoFile.setFolder(repoFolder);
fileRepository.save(repoFile);
count++;
}
}
}
if (count == 0) {
LOG.warn("\n\n\t** All repository files have been moved to proper folders **\n\t You can remove the {} bean.\n\n", this.getClass().getName());
}
}
}
......@@ -16,6 +16,8 @@
package org.genesys.filerepository.model;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
......@@ -28,6 +30,7 @@ import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.OrderColumn;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.genesys.blocks.model.AuditedVersionedModel;
import org.genesys.blocks.model.Copyable;
......@@ -78,6 +81,9 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel
@OrderColumn(name = "position")
private List<RepositoryImage> images;
@Transient
private Path folderPath;
/**
* Gets the path.
*
......@@ -175,4 +181,18 @@ public class ImageGallery extends AuditedVersionedModel implements AclAwareModel
copy.images = new ArrayList<>(this.images);
return copy;
}
/**
* Gets the folder path.
*
* @return the folder path
*/
public Path getFolderPath() {
synchronized (this) {
if (folderPath == null) {
this.folderPath = Paths.get(this.path);
}
}
return this.folderPath;
}
}
......@@ -20,6 +20,7 @@ import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import org.genesys.filerepository.metadata.DocumentMetadata;
......@@ -53,6 +54,7 @@ public class RepositoryDocument extends RepositoryFile implements DocumentMetada
*/
@Override
@PrePersist
@PreUpdate
protected void prePersist() {
// Don't forget the superclass!
super.prePersist();
......
......@@ -16,11 +16,14 @@
package org.genesys.filerepository.model;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Date;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
......@@ -28,33 +31,41 @@ import javax.persistence.Index;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.AuditedVersionedModelWithoutId;
import org.genesys.blocks.model.Copyable;
import org.genesys.blocks.model.InMemoryIdGenerator;
import org.genesys.blocks.model.SelfCleaning;
import org.genesys.blocks.security.model.AclAwareModel;
import org.genesys.filerepository.metadata.BaseMetadata;
import org.genesys.filerepository.service.BytesStorageService;
import org.hibernate.annotations.Type;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
/**
* The RepositoryFile.
*/
@Entity
@Table(name = "repository_file",
// indexes
indexes = { @Index(unique = false, columnList = "path", name = "IX_repoFile_path") }
indexes = { @Index(unique = false, columnList = "folder_id", name = "IX_repoFile_path") }
// unique
, uniqueConstraints = { @UniqueConstraint(columnNames = { "path", "originalFilename" }) })
, uniqueConstraints = { @UniqueConstraint(columnNames = { "folder_id", "originalFilename" }) })
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class RepositoryFile extends AuditedVersionedModelWithoutId implements AclAwareModel, BaseMetadata, Copyable<RepositoryFile> {
public class RepositoryFile extends AuditedVersionedModelWithoutId implements AclAwareModel, BaseMetadata, Copyable<RepositoryFile>, SelfCleaning {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -4816923593950502695L;
......@@ -70,11 +81,21 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
@Type(type = "uuid-binary")
private UUID uuid;
/** The path. */
// Path in the repository
@Column(nullable = false, length = 250)
/** Path in the repository. */
@Column(length = 250)
private String path;
/**
* The repository folder.
*
* @since 1.1
*/
@ManyToOne(cascade = {}, fetch = FetchType.LAZY, optional = false)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "path")
@JsonIdentityReference(alwaysAsId = true)
@JsonProperty(access = Access.READ_ONLY)
private RepositoryFolder folder;
/** The original filename as provided by the end user. */
@Column(nullable = false, length = 250)
private String originalFilename;
......@@ -90,7 +111,7 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
/** The subject. */
@Column
@Lob
@Type(type = "org.hibernate.type.TextType")
@Type(type = "org.hibernate.type.TextType")
private String subject;
/** The description. */
......@@ -158,13 +179,25 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
private int size;
/**
* Pre persist.
* Before persist and any update
*/
@PrePersist
@PreUpdate
protected void prePersist() {
if (uuid == null) {
uuid = UUID.randomUUID();
}
trimStringsToNull();
}
/**
* For repository files, the parent object is always a {@link RepositoryFolder}
* (can't be null).
*/
@Override
public AclAwareModel aclParentObject() {
return this.folder;
}
/*
......@@ -184,7 +217,7 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
* @return relative URL to the file resource
*/
public String getUrl() {
return getStorageFullPath();
return getStorageFullPath().toString();
}
/**
......@@ -225,11 +258,11 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
*
* @return the storage path
*/
public String getStoragePath() {
public Path getStoragePath() {
if (uuid == null) {
return null;
}
return "/" + uuid.toString().substring(0, 3);
return Paths.get("/", uuid.toString().substring(0, 3));
}
/**
......@@ -238,8 +271,8 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
*
* @return the storage full path
*/
public String getStorageFullPath() {
return getStoragePath() + "/" + getFilename();
public Path getStorageFullPath() {
return getStoragePath().resolve(getFilename());
}
/*
......@@ -544,24 +577,6 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
this.extension = extension;
}
/**
* Gets the path.
*
* @return the path
*/
public String getPath() {
return path;
}
/**
* Sets the path.
*
* @param path the new path
*/
public void setPath(final String path) {
this.path = path;
}
/**
* Gets the original url.
*
......@@ -652,19 +667,55 @@ public class RepositoryFile extends AuditedVersionedModelWithoutId implements Ac
this.size = size;
}
/**
* Gets the folder.
*
* @return the folder
*/
public RepositoryFolder getFolder() {
return folder;
}
/**
* Sets the folder.
*
* @param folder the new folder
*/
public void setFolder(RepositoryFolder folder) {
this.folder = folder;
}
/**
* Gets the path.
*
* @return the path
*/
public String getPath() {
return path;
}
/**
* Sets the path.
*
* @param path the new path
*/
public void setPath(String path) {
this.path = path;
}
/*
* (non-Javadoc)
* @see org.genesys.blocks.model.Copyable#apply(java.lang.Object)
*/
@Override
public RepositoryFile apply(final RepositoryFile source) {
this.path = StringUtils.defaultIfBlank(source.path, this.path);
if (StringUtils.isNotBlank(source.originalFilename)) {
// also updates extension
setOriginalFilename(source.originalFilename);
}
this.active = source.active;
this.folder = source.folder;
this.accessRights = source.accessRights;
this.bibliographicCitation = source.bibliographicCitation;
this.contentType = source.contentType;
......
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.filerepository.model;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.genesys.blocks.model.UuidModel;
import org.genesys.blocks.security.model.AclAwareModel;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.service.impl.PathValidator;
import org.hibernate.annotations.Type;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
/**
* <b>Repository Folder</b> represents a location (path) of files in the
* repository. It is immutable and it's <code>path</code> cannot be modified.
*
* @since 1.1-SNAPSHOT
*/
@Entity
@Table(name = "repositoryfolder")
public class RepositoryFolder extends UuidModel implements AclAwareModel {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = -7947000802758739238L;
/**
* Reference to parent Folder. Root folders have this set to null. This
* establishes hierarchical structure of folders.
*
* Because paths are immutable, this cannot be updated.
*/
@ManyToOne(cascade = {}, optional = true, fetch = FetchType.LAZY)
@JoinColumn(updatable = false)
@JsonIgnore
private RepositoryFolder parent;
/**
* List of sub-folders.
*/
@OneToMany(cascade = {}, fetch = FetchType.LAZY, mappedBy = "parent")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "path")
@JsonIdentityReference(alwaysAsId = true)
private List<RepositoryFolder> children;
/** List of files in this folder. */
@OneToMany(cascade = {}, fetch = FetchType.LAZY, mappedBy = "folder")
// @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
// property = "originalFilename")
// @JsonIdentityReference(alwaysAsId = true)
@JsonIgnore
private List<RepositoryFile> files;
/**
* The name of the folder within parent Folder. It is immutable and must not be
* updated.
*/
@Column(name = "name", nullable = false, updatable = false)
private String name;
/**
* Unique path in the repository. It is immutable and must not be updated. It is
* based on the parent Folder path + name.
*/
@Column(name = "path", nullable = false, unique = true, updatable = false)
private String path;
/**
* A human-friendly folder title, not to be confused with {@link #name} above.
*/
@Column
private String title;
/** Folder description. */
@Lob
@Type(type = "org.hibernate.type.TextType")
private String description;
/** The folder path. */
@Transient
private Path folderPath;
/**
* Pre persist.
*/
@PrePersist
protected void prePersist() {
if (uuid == null) {
uuid = UUID.randomUUID();
}
this.path = this.parent == null ? "/" + this.name : this.parent.getFolderPath().resolve(this.name).normalize().toAbsolutePath().toString();
}
/**
* The parent folder is generally the parent object for any folder.
*
* @return the ACL parent object
*/
@Override
public AclAwareModel aclParentObject() {
return this.parent;
}
/**
* Gets the parent.
*
* @return the parent
*/
public RepositoryFolder getParent() {
return parent;
}
/**
* Sets the parent.
*
* @param parent the new parent
*/
public void setParent(RepositoryFolder parent) {
this.parent = parent;
}
/**
* Gets the children.
*
* @return the children
*/
public List<RepositoryFolder> getChildren() {
return children;
}
/**
* Sets the children.
*
* @param children the new children
*/
public void setChildren(List<RepositoryFolder> children) {
this.children = children;
}
/**
* Gets the path.
*
* @return the path
*/
public String getPath() {
return path;
}