From 678c140a94cecef913626bdbcb3ea0d129f3c6ba Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Mon, 2 Feb 2015 22:18:38 +0100 Subject: [PATCH] =?UTF-8?q?T=C5=99=C3=ADdy=20pro=20pr=C3=A1ci=20se=20soubo?= =?UTF-8?q?ry=20a=20p=C5=99=C3=ADlohami=20z=C3=A1znam=C5=AF.=20refs=20#131?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../info/bukova/isspst/data/FileMetainfo.java | 71 ++++- .../bukova/isspst/data/TripRequirement.java | 39 +-- .../TripRequirementServiceImpl.java | 20 +- .../isspst/storage/AbstractFileStorage.java | 103 +++++++ .../isspst/storage/DocumentFileStorage.java | 66 ++++ .../storage/DocumentFileStorageImpl.java | 222 ++++++++++++++ .../isspst/storage/EntityWithAttachment.java | 18 ++ .../bukova/isspst/storage/FileStorage.java | 19 +- .../isspst/storage/LocalFileStorage.java | 102 +------ .../info/bukova/isspst/storage/MimeTypes.java | 284 ++++++++++++++++++ .../isspst/storage/StorageController.java | 66 ++++ .../ui/requirement/TripRequirementForm.java | 60 +++- .../ui/requirement/TripRequirementList.java | 12 +- .../webapp/WEB-INF/spring/root-context.xml | 9 +- src/main/webapp/WEB-INF/storage.properties | 2 + src/main/webapp/app/search/searchForm.zul | 109 +++---- .../trips/requirements/requirementsForm.zul | 257 +++++++++------- 17 files changed, 1144 insertions(+), 315 deletions(-) create mode 100644 src/main/java/info/bukova/isspst/storage/AbstractFileStorage.java create mode 100644 src/main/java/info/bukova/isspst/storage/DocumentFileStorage.java create mode 100644 src/main/java/info/bukova/isspst/storage/DocumentFileStorageImpl.java create mode 100644 src/main/java/info/bukova/isspst/storage/EntityWithAttachment.java create mode 100644 src/main/java/info/bukova/isspst/storage/MimeTypes.java create mode 100644 src/main/java/info/bukova/isspst/storage/StorageController.java create mode 100644 src/main/webapp/WEB-INF/storage.properties diff --git a/src/main/java/info/bukova/isspst/data/FileMetainfo.java b/src/main/java/info/bukova/isspst/data/FileMetainfo.java index 9a6ba906..f04484ff 100644 --- a/src/main/java/info/bukova/isspst/data/FileMetainfo.java +++ b/src/main/java/info/bukova/isspst/data/FileMetainfo.java @@ -1,15 +1,15 @@ package info.bukova.isspst.data; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Table; - import org.hibernate.annotations.Type; import org.hibernate.search.annotations.Analyze; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Index; import org.hibernate.search.annotations.Indexed; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + @Entity @Table(name = "FILE_METAINFO") @Indexed @@ -27,6 +27,12 @@ public class FileMetainfo extends BaseData { @Type(type = "text") @Field(index = Index.YES, analyze = Analyze.YES) private String content; + @Column(name = "MD5") + private String md5; + @Column(name = "DESCRIPTION") + private String description; + @Column(name = "CONTENT_TYPE") + private String contentType; public String getFileName() { return fileName; @@ -68,4 +74,61 @@ public class FileMetainfo extends BaseData { this.content = content; } + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FileMetainfo)) return false; + + FileMetainfo that = (FileMetainfo) o; + + if (getId() != 0 && getId() != that.getId()) return false; + if (recordId != that.recordId) return false; + if (content != null ? !content.equals(that.content) : that.content != null) return false; + if (contentType != null ? !contentType.equals(that.contentType) : that.contentType != null) return false; + if (description != null ? !description.equals(that.description) : that.description != null) return false; + if (fileName != null ? !fileName.equals(that.fileName) : that.fileName != null) return false; + if (md5 != null ? !md5.equals(that.md5) : that.md5 != null) return false; + if (moduleId != null ? !moduleId.equals(that.moduleId) : that.moduleId != null) return false; + if (pathInFilesystem != null ? !pathInFilesystem.equals(that.pathInFilesystem) : that.pathInFilesystem != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = fileName != null ? fileName.hashCode() : 0; + result = 31 * result + (pathInFilesystem != null ? pathInFilesystem.hashCode() : 0); + result = 31 * result + (moduleId != null ? moduleId.hashCode() : 0); + result = 31 * result + recordId; + result = 31 * result + (content != null ? content.hashCode() : 0); + result = 31 * result + (md5 != null ? md5.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (contentType != null ? contentType.hashCode() : 0); + return result; + } } diff --git a/src/main/java/info/bukova/isspst/data/TripRequirement.java b/src/main/java/info/bukova/isspst/data/TripRequirement.java index 7048fe6b..6339cbac 100644 --- a/src/main/java/info/bukova/isspst/data/TripRequirement.java +++ b/src/main/java/info/bukova/isspst/data/TripRequirement.java @@ -1,31 +1,20 @@ package info.bukova.isspst.data; +import info.bukova.isspst.storage.EntityWithAttachment; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.hibernate.search.annotations.*; + +import javax.persistence.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; -import javax.persistence.Column; -import javax.persistence.Embedded; -import javax.persistence.Entity; -import javax.persistence.JoinColumn; -import javax.persistence.JoinTable; -import javax.persistence.ManyToMany; -import javax.persistence.OneToMany; -import javax.persistence.Table; - -import org.hibernate.annotations.LazyCollection; -import org.hibernate.annotations.LazyCollectionOption; -import org.hibernate.search.annotations.Analyze; -import org.hibernate.search.annotations.Field; -import org.hibernate.search.annotations.Index; -import org.hibernate.search.annotations.Indexed; -import org.hibernate.search.annotations.IndexedEmbedded; - @Entity @Table(name = "TRIPREQUIREMENT") @Indexed -public class TripRequirement extends RequirementBase { +public class TripRequirement extends RequirementBase implements EntityWithAttachment { @Column(name = "TRIP_FROM") @Field(index = Index.YES, analyze = Analyze.YES) @@ -49,7 +38,7 @@ public class TripRequirement extends RequirementBase { private Boolean requireDownPayment; @Column(name = "DOWN_PAYMENT", precision = 15, scale = 4) private BigDecimal downPayment; - @OneToMany + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @LazyCollection(LazyCollectionOption.TRUE) @IndexedEmbedded private List attachedFiles; @@ -57,6 +46,7 @@ public class TripRequirement extends RequirementBase { public TripRequirement() { this.setOwnedBy(new User()); passengers = new ArrayList(); + attachedFiles = new ArrayList(); } public String getFrom() { @@ -131,6 +121,7 @@ public class TripRequirement extends RequirementBase { this.downPayment = downPayment; } + @Override public List getAttachedFiles() { return attachedFiles; } @@ -139,4 +130,14 @@ public class TripRequirement extends RequirementBase { this.attachedFiles = attachedFiles; } + @Override + public void addAttachment(FileMetainfo metaInfo) { + attachedFiles.add(metaInfo); + } + + @Override + public void removeAttachment(FileMetainfo metainfo) { + attachedFiles.remove(metainfo); + } + } diff --git a/src/main/java/info/bukova/isspst/services/requirement/TripRequirementServiceImpl.java b/src/main/java/info/bukova/isspst/services/requirement/TripRequirementServiceImpl.java index 64522ce9..53411223 100644 --- a/src/main/java/info/bukova/isspst/services/requirement/TripRequirementServiceImpl.java +++ b/src/main/java/info/bukova/isspst/services/requirement/TripRequirementServiceImpl.java @@ -1,21 +1,17 @@ package info.bukova.isspst.services.requirement; import info.bukova.isspst.Constants; -import info.bukova.isspst.data.NumberSeries; -import info.bukova.isspst.data.RequirementState; -import info.bukova.isspst.data.TripBill; -import info.bukova.isspst.data.TripRequirement; -import info.bukova.isspst.data.User; +import info.bukova.isspst.data.*; import info.bukova.isspst.services.LazyLoader; import info.bukova.isspst.services.tripbill.TripBillService; import info.bukova.isspst.services.workgroups.WorkgroupService; - -import java.util.Date; - +import org.hibernate.Hibernate; import org.hibernate.LazyInitializationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; +import java.util.Date; + public class TripRequirementServiceImpl extends RequirementBaseServiceImpl implements TripRequirementService, RequirementBaseService { @@ -53,6 +49,14 @@ public class TripRequirementServiceImpl extends RequirementBaseServiceImpl implements FileStorage { + + protected void saveFileDataToPath(byte[] data, String path) { + + File file = new File(path); + FileOutputStream os = null; + try { + os = new FileOutputStream(file); + os.write(data); + os.flush(); + os.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (os != null) { + os.flush(); + os.close(); + } + } catch (IOException e) { + throw new StorageException("Cannot close stream", e.getCause()); + } + } + } + + protected void saveFileToPtah(File file, String path) { + File dest = new File(path + File.pathSeparator + file.getName()); + FileOutputStream fos = null; + + try { + fos = new FileOutputStream(dest); + fos.write(readFile(file)); + fos.flush(); + fos.close(); + } catch (FileNotFoundException e) { + throw new StorageException("Cannot move file: " + file.getName(), e.getCause()); + } catch (IOException e) { + throw new StorageException("Cannot move file: " + file.getName(), e.getCause()); + } finally { + if (fos != null) { + try { + fos.flush(); + fos.close(); + } catch (IOException e) { + throw new StorageException("Cannot close stream", e.getCause()); + } + } + } + } + + protected void removeFileByPath(String path) { + File f = new File(path); + if (!f.delete()) { + throw new StorageException("Cannot delete file: " + path); + } + } + + protected byte[] fileDataFromPath(String path) { + File f = new File(path); + return readFile(f); + } + + protected byte[] readFile(File file) { + byte[] out = new byte[(int) file.length()]; + + FileInputStream fis = null; + try { + fis = new FileInputStream(file); + fis.read(out); + fis.close(); + } catch (FileNotFoundException e) { + throw new StorageException("File cannot be found: " + file.getName(), e.getCause()); + } catch (IOException e) { + throw new StorageException("Cannot read file: " + file.getName(), e.getCause()); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + throw new StorageException("Cannot close stream", e.getCause()); + } + } + } + + return out; + } + + protected File fileFromPath(String path) { + return new File(path); + } + +} diff --git a/src/main/java/info/bukova/isspst/storage/DocumentFileStorage.java b/src/main/java/info/bukova/isspst/storage/DocumentFileStorage.java new file mode 100644 index 00000000..6dcaf0e4 --- /dev/null +++ b/src/main/java/info/bukova/isspst/storage/DocumentFileStorage.java @@ -0,0 +1,66 @@ +package info.bukova.isspst.storage; + +import info.bukova.isspst.data.FileMetainfo; + +import java.io.File; + +/** + * Rozhraní servisního objektu pro práci s přílohami u záznamů. Přílohy jsou reprezentovány objektem FileMetainfo, + * kde je zobrazované jméno souboru, vygenerovaný GUID, pod kterým je soubor fyzicky uložen a MD5 součet používaný + * pro deduplikaci. + * + * @see info.bukova.isspst.data.FileMetainfo + * + * @author Pepa Rokos + */ +public interface DocumentFileStorage extends FileStorage { + + /** + * Uloží data předaná jako byte[] do uložiště příloh a vrátí metainformace. + * Metoda řeší deduplikaci souborů, takže v uložišti je fyzicky vždy jen jedna kopie. + * + * @see info.bukova.isspst.data.FileMetainfo + * + * @param data Data souboru + * @param name Zobrazovaný název souboru + * @return Metainformace o souboru + */ + public FileMetainfo saveAndCreateInfo(byte[] data, String name); + + /** + * Uloží data předaná jako File do uložiště příloh a vrátí metainformace. + * Metoda řeší deduplikaci souborů, takže v uložišti je fyzicky vždy jen jedna kopie. + * + * @see info.bukova.isspst.data.FileMetainfo + * + * @param file Objekt reprezentující soubor + * @param name Zobrazovaný název souboru + * @return Metainformace o souboru + */ + public FileMetainfo saveAndCreateInfo(File file, String name); + + /** + * Vrátí metainformace podle fyzické cesty v uložišti. + * + * @param path Cesta k souboru v uložišti + * @return Metainformace o souboru + */ + public FileMetainfo getMetainfoForPath(String path); + + /** + * Bezpečně odstraní přílohu ze záznamu. Záznam o příloze odstraní z kolekce příloh a následně zavolá metodu + * removeFile(FileMetainfo info), která soubor fyzicky smaže, pokud už není nikam linkovaný. + * + * @param entity Entita s přílohami + * @param metaInfo Příloha k odstranění + */ + public void removeAttachment(EntityWithAttachment entity, FileMetainfo metaInfo); + + /** + * Bezpečně odstraní všechny přílohy ze záznamu. + * + * @param entity Entita s přílohami + */ + public void removaAllAttachments(EntityWithAttachment entity); + +} diff --git a/src/main/java/info/bukova/isspst/storage/DocumentFileStorageImpl.java b/src/main/java/info/bukova/isspst/storage/DocumentFileStorageImpl.java new file mode 100644 index 00000000..7a6a9387 --- /dev/null +++ b/src/main/java/info/bukova/isspst/storage/DocumentFileStorageImpl.java @@ -0,0 +1,222 @@ +package info.bukova.isspst.storage; + +import info.bukova.isspst.dao.QueryDao; +import info.bukova.isspst.data.FileMetainfo; +import org.apache.commons.codec.binary.Hex; +import org.hibernate.Query; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.io.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.UUID; + +/** + * @author Pepa Rokos + * + * Třída pro práci s uložištěm příloh u záznamů. Na soubory příloh se odkazuje pomocí objektů FileMetainfo. + * + * @see info.bukova.isspst.data.FileMetainfo + */ +public class DocumentFileStorageImpl extends AbstractFileStorage implements DocumentFileStorage { + + private String rootPath; + @Autowired + private QueryDao queryDao; + + /** + * Nastavuje kořenová adresář pro ukládání příloh. Nastavuje se absolutní cesta na filesystému serveru. + * + * @param rootPath kořenový adresář pro přílohy + */ + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } + + @Override + public void saveFile(byte[] data, FileMetainfo fileID) { + String fileName = generateFileName(fileID.getFileName()); + + saveFileDataToPath(data, rootPath + File.separator + fileName); + fileID.setPathInFilesystem(fileName); + fileID.setContentType(MimeTypes.getContentType(data, fileName)); + } + + @Override + public void saveFile(File file, FileMetainfo fileID) { + String fileName = generateFileName(fileID.getFileName()); + + saveFileToPtah(file, rootPath + File.separator + fileName); + fileID.setPathInFilesystem(fileName); + fileID.setContentType(MimeTypes.getContentType(readFile(file), fileName)); + } + + private String generateFileName(String originalName) { + String extension = null; + String fileName = UUID.randomUUID().toString(); + + if (originalName != null && !originalName.isEmpty()) { + extension = MimeTypes.fileExtension(originalName); + } + + if (extension != null) { + fileName = fileName + "." + extension; + } + + return fileName; + } + + @Override + @Transactional + public void removeFile(FileMetainfo fileID) { + if (fileID.getPathInFilesystem() == null || fileID.getPathInFilesystem().isEmpty()) { + return; + } + + if (infosForPath(fileID.getPathInFilesystem()).size() <= 1) { + removeFileByPath(rootPath + File.separator + fileID.getPathInFilesystem()); + } + } + + @Override + public void moveFile(String source, String destination) { + throw new UnsupportedOperationException(); + } + + @Override + public void createDirectory(String dir) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] fileData(FileMetainfo fileID) { + if (fileID.getPathInFilesystem() != null && !fileID.getPathInFilesystem().isEmpty()) { + return fileDataFromPath(rootPath + File.separator + fileID.getPathInFilesystem()); + } + + return null; + } + + @Override + public File file(FileMetainfo fileID) { + if (fileID.getPathInFilesystem() != null && !fileID.getPathInFilesystem().isEmpty()) { + return fileFromPath(rootPath + File.separator + fileID.getPathInFilesystem()); + } + + return null; + } + + @Override + public boolean dirExists(String path) { + throw new UnsupportedOperationException(); + } + + @Override + public String serverPath(FileMetainfo fileID) { + return "/api/dl/" + fileID.getPathInFilesystem() + "/" + fileID.getFileName(); + } + + @Override + @Transactional + public FileMetainfo saveAndCreateInfo(byte[] data, String name) { + FileMetainfo metaInfo = new FileMetainfo(); + metaInfo.setFileName(name); + + if (!checkForDuplicate(new ByteArrayInputStream(data), metaInfo)) { + saveFile(data, metaInfo); + } + + return metaInfo; + } + + @Override + @Transactional + public FileMetainfo saveAndCreateInfo(File file, String name) { + FileMetainfo metaInfo = new FileMetainfo(); + metaInfo.setFileName(name); + + try { + if (!checkForDuplicate(new FileInputStream(file), metaInfo)) { + saveFile(file, metaInfo); + } + } catch (FileNotFoundException e) { + //TODO: ošetřit + e.printStackTrace(); + } + + return metaInfo; + } + + @Override + @Transactional + public FileMetainfo getMetainfoForPath(String path) { + if (infosForPath(path).isEmpty()) { + return null; + } + + return infosForPath(path).get(0); + } + + private List infosForPath(String path) { + Query q = queryDao.getQuery("from FileMetainfo info where info.pathInFilesystem = :path"); + q.setString("path", path); + + return q.list(); + } + + private boolean checkForDuplicate(InputStream is, FileMetainfo info) { + String md5 = null; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] dataBytes = new byte[1024]; + int nread = 0; + + while ((nread = is.read(dataBytes)) != -1) { + md.update(dataBytes, 0, nread); + } + + md5 = new String(Hex.encodeHex(md.digest())); + } catch (NoSuchAlgorithmException e) { + //TODO: ošetřit + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + } + + Query q = queryDao.getQuery("select info from FileMetainfo info where info.md5 = :md5"); + q.setString("md5", md5); + List found = (List) q.list(); + + if (!found.isEmpty()) { + FileMetainfo foundInfo = found.get(0); + info.setPathInFilesystem(foundInfo.getPathInFilesystem()); + info.setMd5(foundInfo.getMd5()); + info.setContentType(foundInfo.getContentType()); + + return true; + } else { + info.setMd5(md5); + return false; + } + } + + @Override + @Transactional + public void removeAttachment(EntityWithAttachment entity, FileMetainfo metaInfo) { + entity.removeAttachment(metaInfo); + removeFile(metaInfo); + } + + @Override + @Transactional + public void removaAllAttachments(EntityWithAttachment entity) { + for (FileMetainfo metaInfo : entity.getAttachedFiles()) { + removeFile(metaInfo); + } + + entity.getAttachedFiles().clear(); + } +} diff --git a/src/main/java/info/bukova/isspst/storage/EntityWithAttachment.java b/src/main/java/info/bukova/isspst/storage/EntityWithAttachment.java new file mode 100644 index 00000000..fdefbd1e --- /dev/null +++ b/src/main/java/info/bukova/isspst/storage/EntityWithAttachment.java @@ -0,0 +1,18 @@ +package info.bukova.isspst.storage; + +import info.bukova.isspst.data.FileMetainfo; + +import java.util.List; + +/** + * Rozhraní datových entit s přílohami. + * + * @author Pepa Rokos + */ +public interface EntityWithAttachment { + + public List getAttachedFiles(); + public void addAttachment(FileMetainfo metaInfo); + public void removeAttachment(FileMetainfo metainfo); + +} diff --git a/src/main/java/info/bukova/isspst/storage/FileStorage.java b/src/main/java/info/bukova/isspst/storage/FileStorage.java index f631ac2b..90a0eea2 100644 --- a/src/main/java/info/bukova/isspst/storage/FileStorage.java +++ b/src/main/java/info/bukova/isspst/storage/FileStorage.java @@ -2,17 +2,22 @@ package info.bukova.isspst.storage; import java.io.File; -public interface FileStorage { +/** + * Rozhraní pro uložiště souborů + * + * @param typ objektu, kterým se odkazuje na soubory + */ +public interface FileStorage { // public String getRootPath(); - public void saveFile(byte[] data, String fileName); - public void saveFile(File file, String path); - public void removeFile(String fileName); + public void saveFile(byte[] data, T fileID); + public void saveFile(File file, T fileId); + public void removeFile(T fileID); public void moveFile(String source, String destination); public void createDirectory(String dir); - public byte[] fileData(String fileName); - public File file(String fileName); + public byte[] fileData(T fileID); + public File file(T fileID); public boolean dirExists(String path); - public String serverPath(String fileName); + public String serverPath(T fileID); } diff --git a/src/main/java/info/bukova/isspst/storage/LocalFileStorage.java b/src/main/java/info/bukova/isspst/storage/LocalFileStorage.java index dc5d3150..00d86515 100644 --- a/src/main/java/info/bukova/isspst/storage/LocalFileStorage.java +++ b/src/main/java/info/bukova/isspst/storage/LocalFileStorage.java @@ -1,14 +1,9 @@ package info.bukova.isspst.storage; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; - import javax.servlet.ServletContext; +import java.io.*; -public class LocalFileStorage implements FileStorage { +public class LocalFileStorage extends AbstractFileStorage { private String rootPath; private ServletContext context; @@ -26,96 +21,27 @@ public class LocalFileStorage implements FileStorage { } @Override - public void saveFile(byte[] data, String fileName) { - - File file = new File(getFullPath() + fileName); - FileOutputStream os = null; - try { - os = new FileOutputStream(file); - os.write(data); - os.flush(); - os.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (os != null) { - os.flush(); - os.close(); - } - } catch (IOException e) { - throw new StorageException("Cannot close stream", e.getCause()); - } - } + public void saveFile(byte[] data, String fileID) { + saveFileDataToPath(data, getFullPath() + fileID); } - @Override - public void saveFile(File file, String path) { - File dest = new File(getFullPath() + path + File.pathSeparator + file.getName()); - FileOutputStream fos = null; - - try { - fos = new FileOutputStream(dest); - fos.write(fileData(file.getName())); - fos.flush(); - fos.close(); - } catch (FileNotFoundException e) { - throw new StorageException("Cannot move file: " + file.getName(), e.getCause()); - } catch (IOException e) { - throw new StorageException("Cannot move file: " + file.getName(), e.getCause()); - } finally { - if (fos != null) { - try { - fos.flush(); - fos.close(); - } catch (IOException e) { - throw new StorageException("Cannot close stream", e.getCause()); - } - } - } + public void saveFile(File file, String fileID) { + saveFileToPtah(file, getFullPath() + fileID); } @Override - public void removeFile(String fileName) { - File f = new File(getFullPath() + fileName); - if (!f.delete()) { - throw new StorageException("Cannot delete file: " + getFullPath() - + fileName); - } + public void removeFile(String fileID) { + removeFileByPath(getFullPath() + fileID); } @Override - public byte[] fileData(String fileName) { - File f = new File(getFullPath() + fileName); - byte[] out = new byte[(int) f.length()]; - - FileInputStream fis = null; - try { - fis = new FileInputStream(f); - fis.read(out); - fis.close(); - } catch (FileNotFoundException e) { - throw new StorageException("File cannot be found: " + fileName, e.getCause()); - } catch (IOException e) { - throw new StorageException("Cannot read file: " + fileName, e.getCause()); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - throw new StorageException("Cannot close stream", e.getCause()); - } - } - } - - return out; + public byte[] fileData(String fileID) { + return fileDataFromPath(getFullPath() + fileID); } @Override - public File file(String fileName) { - return new File(getFullPath() + fileName); + public File file(String fileID) { + return fileFromPath(getFullPath() + fileID); } @Override @@ -139,8 +65,8 @@ public class LocalFileStorage implements FileStorage { } @Override - public String serverPath(String fileName) { - return context.getRealPath(rootPath + File.separator + fileName); + public String serverPath(String fileID) { + return context.getRealPath(rootPath + File.separator + fileID); } } diff --git a/src/main/java/info/bukova/isspst/storage/MimeTypes.java b/src/main/java/info/bukova/isspst/storage/MimeTypes.java new file mode 100644 index 00000000..74fb2059 --- /dev/null +++ b/src/main/java/info/bukova/isspst/storage/MimeTypes.java @@ -0,0 +1,284 @@ +package info.bukova.isspst.storage; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; + +/** + * Created by pepa on 30.1.15. + */ +public class MimeTypes { + + public static String getContentType(byte[] data) + { + return getContentType(data, null); + } + + public static String getContentType(byte[] data, String name) + { + if (data == null) + { + return null; + } + byte[] header = new byte[11]; + System.arraycopy(data, 0, header, 0, Math.min(data.length, header.length)); + int c1 = header[0] & 0xff; + int c2 = header[1] & 0xff; + int c3 = header[2] & 0xff; + int c4 = header[3] & 0xff; + int c5 = header[4] & 0xff; + int c6 = header[5] & 0xff; + int c7 = header[6] & 0xff; + int c8 = header[7] & 0xff; + int c9 = header[8] & 0xff; + int c10 = header[9] & 0xff; + int c11 = header[10] & 0xff; + + if (c1 == 0xCA && c2 == 0xFE && c3 == 0xBA && c4 == 0xBE) + { + return "application/java-vm"; + } + + if (c1 == 0xD0 && c2 == 0xCF && c3 == 0x11 && c4 == 0xE0 && c5 == 0xA1 && c6 == 0xB1 && c7 == 0x1A && c8 == 0xE1) + { + // if the name is set then check if it can be validated by name, because it could be a xls or powerpoint + String contentType = guessContentTypeFromName(name); + if (contentType != null) + { + return contentType; + } + return "application/msword"; + } + if (c1 == 0x25 && c2 == 0x50 && c3 == 0x44 && c4 == 0x46 && c5 == 0x2d && c6 == 0x31 && c7 == 0x2e) + { + return "application/pdf"; + } + + if (c1 == 0x38 && c2 == 0x42 && c3 == 0x50 && c4 == 0x53 && c5 == 0x00 && c6 == 0x01) + { + return "image/photoshop"; + } + + if (c1 == 0x25 && c2 == 0x21 && c3 == 0x50 && c4 == 0x53) + { + return "application/postscript"; + } + + if (c1 == 0xff && c2 == 0xfb && c3 == 0x30) + { + return "audio/mp3"; + } + + if (c1 == 0x49 && c2 == 0x44 && c3 == 0x33) + { + return "audio/mp3"; + } + + if (c1 == 0xAC && c2 == 0xED) + { + // next two bytes are version number, currently 0x00 0x05 + return "application/x-java-serialized-object"; + } + + if (c1 == '<') + { + if (c2 == '!' || + ((c2 == 'h' && (c3 == 't' && c4 == 'm' && c5 == 'l' || c3 == 'e' && c4 == 'a' && c5 == 'd') || (c2 == 'b' && c3 == 'o' && c4 == 'd' && c5 == 'y'))) || + ((c2 == 'H' && (c3 == 'T' && c4 == 'M' && c5 == 'L' || c3 == 'E' && c4 == 'A' && c5 == 'D') || (c2 == 'B' && c3 == 'O' && c4 == 'D' && c5 == 'Y')))) + { + return "text/html"; + } + + if (c2 == '?' && c3 == 'x' && c4 == 'm' && c5 == 'l' && c6 == ' ') + { + return "application/xml"; + } + } + + // big and little endian UTF-16 encodings, with byte order mark + if (c1 == 0xfe && c2 == 0xff) + { + if (c3 == 0 && c4 == '<' && c5 == 0 && c6 == '?' && c7 == 0 && c8 == 'x') + { + return "application/xml"; + } + } + + if (c1 == 0xff && c2 == 0xfe) + { + if (c3 == '<' && c4 == 0 && c5 == '?' && c6 == 0 && c7 == 'x' && c8 == 0) + { + return "application/xml"; + } + } + + if (c1 == 'B' && c2 == 'M') + { + return "image/bmp"; + } + + if (c1 == 0x49 && c2 == 0x49 && c3 == 0x2a && c4 == 0x00) + { + return "image/tiff"; + } + + if (c1 == 0x4D && c2 == 0x4D && c3 == 0x00 && c4 == 0x2a) + { + return "image/tiff"; + } + + if (c1 == 'G' && c2 == 'I' && c3 == 'F' && c4 == '8') + { + return "image/gif"; + } + + if (c1 == '#' && c2 == 'd' && c3 == 'e' && c4 == 'f') + { + return "image/x-bitmap"; + } + + if (c1 == '!' && c2 == ' ' && c3 == 'X' && c4 == 'P' && c5 == 'M' && c6 == '2') + { + return "image/x-pixmap"; + } + + if (c1 == 137 && c2 == 80 && c3 == 78 && c4 == 71 && c5 == 13 && c6 == 10 && c7 == 26 && c8 == 10) + { + return "image/png"; + } + + if (c1 == 0xFF && c2 == 0xD8 && c3 == 0xFF) + { + if (c4 == 0xE0) + { + return "image/jpeg"; + } + + /** + * File format used by digital cameras to store images. Exif Format can be read by any application supporting JPEG. Exif Spec can be found at: + * http://www.pima.net/standards/it10/PIMA15740/Exif_2-1.PDF + */ + if ((c4 == 0xE1) && (c7 == 'E' && c8 == 'x' && c9 == 'i' && c10 == 'f' && c11 == 0)) + { + return "image/jpeg"; + } + + if (c4 == 0xEE) + { + return "image/jpg"; + } + } + + /** + * According to http://www.opendesign.com/files/guestdownloads/OpenDesign_Specification_for_.dwg_files.pdf + * first 6 bytes are of type "AC1018" (for example) and the next 5 bytes are 0x00. + */ + if ((c1 == 0x41 && c2 == 0x43) && (c7 == 0x00 && c8 == 0x00 && c9 == 0x00 && c10 == 0x00 && c11 == 0x00)) + { + return "application/acad"; + } + + if (c1 == 0x2E && c2 == 0x73 && c3 == 0x6E && c4 == 0x64) + { + return "audio/basic"; // .au + // format, + // big + // endian + } + + if (c1 == 0x64 && c2 == 0x6E && c3 == 0x73 && c4 == 0x2E) + { + return "audio/basic"; // .au + // format, + // little + // endian + } + + if (c1 == 'R' && c2 == 'I' && c3 == 'F' && c4 == 'F') + { + /* + * I don't know if this is official but evidence suggests that .wav files start with "RIFF" - brown + */ + return "audio/x-wav"; + } + + if (c1 == 'P' && c2 == 'K') + { + // its application/zip but this could be a open office thing if name is given + String contentType = guessContentTypeFromName(name); + if (contentType != null) + { + return contentType; + } + return "application/zip"; + } + return guessContentTypeFromName(name); + } + + private static final Map mimeTypes = new HashMap(); + + public static String guessContentTypeFromName(String name) + { + if (name == null) return null; + + int lastIndex = name.lastIndexOf('.'); + if (lastIndex != -1) + { + String extention = name.substring(lastIndex + 1).toLowerCase(); + if (mimeTypes.size() == 0) + { + HashMap tempMap = new HashMap(); + InputStream is = MimeTypes.class.getResourceAsStream("mime.types.properties"); + try + { + Properties properties = new Properties(); + properties.load(is); + for (Object key : properties.keySet()) + { + String property = properties.getProperty((String)key); + StringTokenizer st = new StringTokenizer(property, " "); + while (st.hasMoreTokens()) + { + tempMap.put(st.nextToken(), (String)key); + } + } + } + catch (IOException e) + { + //Debug.error(e); + } + finally + { + try + { + is.close(); + } + catch (IOException e) + { + //Debug.error(e); + } + } + synchronized (mimeTypes) + { + mimeTypes.putAll(tempMap); + } + } + return mimeTypes.get(extention); + } + return null; + } + + public static String fileExtension(String fileName) { + int index = fileName.lastIndexOf("."); + + if (index > -1) { + return fileName.substring(index + 1); + } + + return ""; + } + +} diff --git a/src/main/java/info/bukova/isspst/storage/StorageController.java b/src/main/java/info/bukova/isspst/storage/StorageController.java new file mode 100644 index 00000000..3ffc4df8 --- /dev/null +++ b/src/main/java/info/bukova/isspst/storage/StorageController.java @@ -0,0 +1,66 @@ +package info.bukova.isspst.storage; + +import info.bukova.isspst.data.FileMetainfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Created by pepa on 2.2.15. + */ + +@Controller +public class StorageController { + + @Autowired + private DocumentFileStorage documentStorage; + private static final Logger logger = LoggerFactory.getLogger(StorageController.class); + + @RequestMapping(value = "dl/{path}/{name}") + public void viewFile(HttpServletResponse response, @PathVariable("path") String path, + @PathVariable("name") String name) { + ServletOutputStream os = null; + + try { + os = response.getOutputStream(); + FileMetainfo metainfo = new FileMetainfo(); + metainfo.setPathInFilesystem(path); + byte[] data = null; + + try { + data = documentStorage.fileData(metainfo); + metainfo.setContentType(MimeTypes.getContentType(data, path)); + } catch (StorageException e) { + response.sendError(404); + } + + if (data != null) { + response.setContentType(metainfo.getContentType()); + response.setContentLength(data.length); + + os.write(data, 0, data.length); + os.flush(); + os.close(); + } + } catch (IOException e) { + logger.error("I/O error: " + e.getMessage()); + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + } + +} diff --git a/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementForm.java b/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementForm.java index a39ec9fe..f3206d4c 100644 --- a/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementForm.java +++ b/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementForm.java @@ -1,24 +1,21 @@ package info.bukova.isspst.ui.requirement; -import java.util.List; - -import org.zkoss.bind.annotation.BindingParam; -import org.zkoss.bind.annotation.Command; -import org.zkoss.bind.annotation.Init; -import org.zkoss.bind.annotation.NotifyChange; -import org.zkoss.zk.ui.select.annotation.WireVariable; - -import info.bukova.isspst.data.SettingsData; -import info.bukova.isspst.data.TripRequirement; -import info.bukova.isspst.data.User; -import info.bukova.isspst.data.Workgroup; +import info.bukova.isspst.data.*; import info.bukova.isspst.services.requirement.RequirementTypeService; import info.bukova.isspst.services.requirement.TripRequirementService; import info.bukova.isspst.services.settings.GlobalSettingsService; import info.bukova.isspst.services.users.UserService; import info.bukova.isspst.services.workgroups.WorkgroupService; +import info.bukova.isspst.storage.DocumentFileStorage; import info.bukova.isspst.ui.FormViewModel; import info.bukova.isspst.validators.TripRequirementFormValidator; +import org.zkoss.bind.annotation.*; +import org.zkoss.zk.ui.event.UploadEvent; +import org.zkoss.zk.ui.select.annotation.WireVariable; +import org.zkoss.zul.Filedownload; + +import java.util.ArrayList; +import java.util.List; public class TripRequirementForm extends FormViewModel { @@ -32,9 +29,13 @@ public class TripRequirementForm extends FormViewModel { private RequirementTypeService reqTypeService; @WireVariable private TripRequirementService tripRequirementService; + @WireVariable + private DocumentFileStorage documentStorage; private List centres; private List users; private List passengers; + private List attachments; + private List forDelete; private User selUser; private TripRequirementFormValidator validator; @@ -44,6 +45,8 @@ public class TripRequirementForm extends FormViewModel { centres = reqTypeService.filterCentres(getDataBean().getType(), workgroupService.getUserCentres(userService.getCurrent())); users = userService.getUsersForCombo(); passengers = getDataBean().getPassengers(); + attachments = getDataBean().getAttachedFiles(); + forDelete = new ArrayList(); // kolekce příloh na smazání v případě uložení záznamu validator = new TripRequirementFormValidator(); } @@ -87,4 +90,37 @@ public class TripRequirementForm extends FormViewModel { return passengers; } + @Command + @NotifyChange("attachments") + public void uploadAttachment(@ContextParam(ContextType.TRIGGER_EVENT) UploadEvent upEvent) { + FileMetainfo metaInfo = documentStorage.saveAndCreateInfo(upEvent.getMedia().getByteData(), + upEvent.getMedia().getName()); + + getDataBean().addAttachment(metaInfo); + } + + @Command + @NotifyChange("attachments") + public void deleteAttachment(@BindingParam("attachment") FileMetainfo metaInfo) { + getDataBean().removeAttachment(metaInfo); + forDelete.add(metaInfo); // smazat až v případě uložení záznamu + } + + @Command + public void downloadAttachment(@BindingParam("attachment") FileMetainfo metaInfo) { + Filedownload.save(documentStorage.fileData(metaInfo), metaInfo.getContentType(), metaInfo.getFileName()); + } + + public List getAttachments() { + return attachments; + } + + @Override + protected void doSave() { + for (FileMetainfo info : forDelete) { + documentStorage.removeFile(info); + } + + super.doSave(); + } } diff --git a/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementList.java b/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementList.java index 3175c074..98eb7aa0 100644 --- a/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementList.java +++ b/src/main/java/info/bukova/isspst/ui/requirement/TripRequirementList.java @@ -7,10 +7,6 @@ import info.bukova.isspst.services.requirement.TripRequirementService; import info.bukova.isspst.services.users.UserService; import info.bukova.isspst.services.workgroups.WorkgroupService; import info.bukova.isspst.ui.ListViewModel; - -import java.util.ArrayList; -import java.util.List; - import org.springframework.security.access.AccessDeniedException; import org.zkoss.bind.BindUtils; import org.zkoss.bind.annotation.GlobalCommand; @@ -18,6 +14,9 @@ import org.zkoss.bind.annotation.Init; import org.zkoss.bind.annotation.NotifyChange; import org.zkoss.zk.ui.select.annotation.WireVariable; +import java.util.ArrayList; +import java.util.List; + public class TripRequirementList extends ListViewModel { @WireVariable @@ -79,11 +78,6 @@ public class TripRequirementList extends ListViewModel { return myCentres; } - @Override - protected void loadLazyDataForEdit(TripRequirement data) { - tripRequirementService.loadPassangers(data); - } - @Override protected List getListFromService() { diff --git a/src/main/webapp/WEB-INF/spring/root-context.xml b/src/main/webapp/WEB-INF/spring/root-context.xml index 5d8e4363..fcd38e91 100644 --- a/src/main/webapp/WEB-INF/spring/root-context.xml +++ b/src/main/webapp/WEB-INF/spring/root-context.xml @@ -29,6 +29,7 @@ /WEB-INF/mail.properties /WEB-INF/gmail.properties /WEB-INF/ad.properties + /WEB-INF/storage.properties @@ -52,10 +53,10 @@ ${jdbc.dialect} - true + false update filesystem - ./ + ${storage.fulltextIndex} org.apache.lucene.analysis.cz.CzechAnalyzer @@ -176,6 +177,10 @@ + + + + diff --git a/src/main/webapp/WEB-INF/storage.properties b/src/main/webapp/WEB-INF/storage.properties new file mode 100644 index 00000000..3b71ef87 --- /dev/null +++ b/src/main/webapp/WEB-INF/storage.properties @@ -0,0 +1,2 @@ +storage.root=/home/pepa/tmp/isspst +storage.fulltextIndex=/home/pepa/tmp/isspst \ No newline at end of file diff --git a/src/main/webapp/app/search/searchForm.zul b/src/main/webapp/app/search/searchForm.zul index fcfc66db..7afb6c31 100644 --- a/src/main/webapp/app/search/searchForm.zul +++ b/src/main/webapp/app/search/searchForm.zul @@ -1,57 +1,62 @@ - - + - - + - - -
- - - - -
-
- - - - - - - -
+ + - + + +
+ + + + +
+
+ + + + + + + +
+ +
\ No newline at end of file diff --git a/src/main/webapp/main/trips/requirements/requirementsForm.zul b/src/main/webapp/main/trips/requirements/requirementsForm.zul index 9b496973..7c141cc3 100644 --- a/src/main/webapp/main/trips/requirements/requirementsForm.zul +++ b/src/main/webapp/main/trips/requirements/requirementsForm.zul @@ -1,5 +1,8 @@ - + - - - - - - - - ${labels.RequirementsFormNumberSerie} : - - - - - - ${labels.RequirementsFormReqDate} : - - - - - - ${labels.RequirementsFormCenter} : - - - - - - - - ${labels.RequirementsFormFrom} : - - - - - - ${labels.RequirementsFormStartDateTime} : - - - - - - - - - - ${labels.RequirementsFormTo} : - - - - - - ${labels.RequirementsFormPurpose} : - - - - - - ${labels.RequirementsFormEndTravel} : - - - - - - ${labels.RequirementsFormEndDate} : - - - - - - + + + + + + + + + ${labels.RequirementsFormNumberSerie} : + + + + + + ${labels.RequirementsFormReqDate} : + + + + + + ${labels.RequirementsFormCenter} : + + + + + + + + ${labels.RequirementsFormFrom} : + + + + + + ${labels.RequirementsFormStartDateTime} : + + + + + + + + + + ${labels.RequirementsFormTo} : + + + + + + ${labels.RequirementsFormPurpose} : + + + + + + ${labels.RequirementsFormEndTravel} : + + + + + + ${labels.RequirementsFormEndDate} : + + + + + + + +