Java tutorial
/* * ============================================================= * Copyright (C) 2007-2011 Edgenius (http://www.edgenius.com) * ============================================================= * License Information: http://www.edgenius.com/licensing/edgenius/2.0/ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2.0 * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * http://www.gnu.org/licenses/gpl.txt * * **************************************************************** */ package com.edgenius.core.repository; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import javax.activation.MimetypesFileTypeMap; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.transaction.annotation.Transactional; import com.edgenius.core.Global; import com.edgenius.core.dao.CrFileNodeDAO; import com.edgenius.core.dao.CrWorkspaceDAO; import com.edgenius.core.model.CrFileNode; import com.edgenius.core.model.CrWorkspace; import com.edgenius.core.model.User; import com.edgenius.core.util.AuditLogger; import com.edgenius.core.util.FileUtil; import com.edgenius.core.util.FileUtilException; import com.edgenius.core.util.ThreadInterruptManager; import com.edgenius.core.util.ZipFileUtil; import com.edgenius.core.util.ZipFileUtilException; /** * * Repository Root -> Space -> Node Type(Page, Space or User) -> IdentifierUUID (PageUuid or Username) -> NodeUUID -> * ->1 (version number) -> physical file * 2 (version number) -> physical file * <br> * The Node Type, please refer to RepositoryService.TYPE_* static variables. * @author dapeng * */ @Transactional public class SimpleRepositoryServiceImpl implements RepositoryService, InitializingBean { private static final Logger log = LoggerFactory.getLogger(SimpleRepositoryServiceImpl.class); private static final String QUOTA_FILE_NAME = "quota"; private static final String TMP_BULK_CHECKIN = "bulkcheckin"; public static final String LOCK_DIR = "_lock_"; //any file will rename to this name, so that system can be handle any special character in physical file name. public static final String DEFAULT_FILE_NAME = "0"; //120 seconds private static final int TIMEOUT = 120; private MessageDigest md5Digest = null; private ReentrantLock writeLock = new ReentrantLock(); private HashMap<String, ConditionGroup> spaceLockMap = new HashMap<String, ConditionGroup>(); private Resource homeDirResource; private String homeDir; private CrFileNodeDAO crFileNodeDAO; private CrWorkspaceDAO crWorkspaceDAO; public void createWorkspace(String spacename, String username, String password) throws RepositoryException, RepositoryTiemoutExcetpion { //TODO: need verify permission? try { acquireLock(spacename, null, null); String uuid = UUID.nameUUIDFromBytes(spacename.getBytes()).toString(); CrWorkspace crW = new CrWorkspace(); //leave user/pass empty crW.setName(spacename); crW.setSpaceUuid(uuid); crW.setQuota(Global.SpaceQuota); String spaceDir = FileUtil.getFullPath(homeDir, uuid); File space = new File(spaceDir); if (space.exists()) { throw new RepositoryException("Space already exist in repository " + spacename); } if (!space.mkdir()) { log.error("Space " + spacename + " directory can not created successfully"); throw new RepositoryException("Space " + spacename + " directory can not created successfully"); } else { crWorkspaceDAO.saveOrUpdate(crW); // does not user Quota file 16/11/2007 // createQuotaFile(spacename); log.info("Space " + spacename + " is created"); } } finally { releaseLock(spacename, null, null); } } public String createIdentifier(ITicket ticket, String type, String identifierUuid) throws RepositoryException, RepositoryTiemoutExcetpion { // so far use identifierUuid as directory name, the weakness is, identifierUuid must be REAL unique. // maybe I need create another unique folder and mapping this folder to this identifierUuid. if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for identifierUuid " + identifierUuid; log.warn(error); throw new RepositoryException("Permission denied: " + error); } try { acquireLock(ticket.getSpacename(), identifierUuid, null); log.info("New identifier will be generated for identifierUuid:" + identifierUuid); CrWorkspace crW = getCrWorkspace(ticket); String typeNodeDir = FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), type); File typeNode = new File(typeNodeDir); if (!typeNode.exists()) { if (!typeNode.mkdirs()) { throw new RepositoryException( "create type " + type + " in space " + ticket.getSpacename() + " failed"); } } //pageuuid or username File identifier = new File(FileUtil.getFullPath(typeNodeDir, identifierUuid)); if (identifier.exists()) { log.warn("Identifier directory already exsit in creating request " + identifierUuid); } else { if (!identifier.mkdir()) throw new RepositoryException("New identifier directory created failed " + identifierUuid); } log.info("Repository create a new type " + type + " in space " + ticket.getSpacename() + " UUID is" + identifierUuid); } finally { releaseLock(ticket.getSpacename(), identifierUuid, null); } //so far return same value with input return identifierUuid; } public void removeWorkspace(ITicket ticket, String spacename) throws RepositoryException, RepositoryTiemoutExcetpion { if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for space " + spacename; log.warn(error); throw new RepositoryException("Permission denied: " + error); } try { acquireLock(spacename, null, null); CrWorkspace crW = getCrWorkspace(ticket); String spaceDir = FileUtil.getFullPath(homeDir, crW.getSpaceUuid()); File space = new File(spaceDir); FileUtils.deleteDirectory(space); crWorkspaceDAO.removeObject(crW); log.info("Repository space " + spacename + " is removed"); } catch (IOException e) { throw new RepositoryException("Remove space " + spacename + " failed from repository"); } finally { releaseLock(spacename, null, null); } } public FileNode downloadFile(ITicket ticket, String nodeUuid, String version, User downloader) throws RepositoryException { if (!ticket.isAllowRead()) { String error = "Workspace has not read permission " + ticket.getSpacename(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } try { log.info("Download file from " + ticket.getSpacename() + " by nodeUuid:" + nodeUuid + ". version:" + version); CrFileNode crFilenode; if (NumberUtils.toInt(version) == 0) { //download current version crFilenode = crFileNodeDAO.getBaseByNodeUuid(nodeUuid); } else { //chose special version for download crFilenode = crFileNodeDAO.getVersionNode(nodeUuid, NumberUtils.toInt(version)); } if (crFilenode == null) { log.warn("Could get file from reposiotry by uuid " + nodeUuid); return null; } if (crFilenode.getStatus() != 0 && downloader != null) { //if file is draft - see Draft.MANUAL_DRAFT Draft.AUTO_DRAFT - then need check if download user has permission if (!downloader.getUsername().equals(crFilenode.getCreator())) { log.warn("User " + downloader + " has no permission to read file node:" + nodeUuid); return null; } } return getFileNode(ticket, crFilenode, true); } catch (Exception e) { log.error("Failed on download", e); return null; } } public void copy(ITicket fromTicket, String fromType, String fromIdentifierUuid, ITicket toTicket, String toType, String toIdentifierUuid) throws RepositoryException, RepositoryTiemoutExcetpion { if (!fromTicket.isAllowRead()) { String error = "Workspace has not read permission " + fromTicket.getSpacename(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } if (!toTicket.isAllowRead()) { String error = "Workspace has not write permission " + toTicket.getSpacename(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } String msg = "Copy Identifier " + fromIdentifierUuid + " from space " + fromTicket.getSpacename() + " to " + toTicket.getSpacename() + " to Identifier " + toIdentifierUuid; try { acquireLock(toTicket.getSpacename(), toIdentifierUuid, null); CrWorkspace crWFrom = getCrWorkspace(fromTicket); CrWorkspace crWTo = getCrWorkspace(toTicket); log.info(msg); String srcDir = FileUtil.getFullPath(homeDir, crWFrom.getSpaceUuid(), fromType, fromIdentifierUuid); String toDir = FileUtil.getFullPath(homeDir, crWTo.getSpaceUuid(), toType, toIdentifierUuid); //copy physical files FileUtils.copyDirectory(new File(srcDir), new File(toDir)); //copy records in DB List<CrFileNode> fromList = crFileNodeDAO.getIdentifierNodes(fromType, fromIdentifierUuid); for (CrFileNode crFileNode : fromList) { CrFileNode node = (CrFileNode) crFileNode.clone(); //treat this as TO rather than PO node.setUid(null); node.setIdentifierUuid(toIdentifierUuid); node.setNodeType(toType); node.setSpaceUname(toTicket.getSpacename()); //need update the target directory to new nodeUUID, which is unique for a file node (but could have version). String oldNodeUuid = node.getNodeUuid(); String newNodeUuid = UUID.randomUUID().toString(); String oldFileNodeDir = FileUtil.getFullPath(toDir, oldNodeUuid); String newFileNodeDir = FileUtil.getFullPath(toDir, newNodeUuid); if (FileUtil.rename(oldFileNodeDir, newFileNodeDir)) { node.setNodeUuid(newNodeUuid); crFileNodeDAO.saveOrUpdate(node); } else { log.error("Some file node can not be rename to new. Lost data.old NodeUuid" + oldNodeUuid + " new NodeUuid" + newNodeUuid); } } } catch (IOException e) { log.error("Copy node failed:" + msg, e); throw new RepositoryException(msg); } finally { releaseLock(toTicket.getSpacename(), toIdentifierUuid, null); } } public CrFileNode getLatestCRFileNode(ITicket ticket, String type, String identifierUuid, String fileName) { List<CrFileNode> nodes = crFileNodeDAO.getIdentifierNodes(type, identifierUuid, fileName); if (nodes == null || nodes.size() == 0) { return null; } //get latest version - we assume filename is unique under identifierUuid, duplicated filename will be versionable. return nodes.get(0); } public List<FileNode> getAllIdentifierNodes(ITicket ticket, String type, String identifierUuid, boolean withResource) throws RepositoryException { if (!ticket.isAllowRead()) { String error = "Workspace has not read permission " + ticket.getSpacename(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } CrWorkspace crW = getCrWorkspace(ticket); List<CrFileNode> nodes = crFileNodeDAO.getIdentifierNodes(type, identifierUuid); List<FileNode> list = new ArrayList<FileNode>(); //retrieve all geniuswiki:file nodes under this identifier for (Iterator<CrFileNode> iter = nodes.iterator(); iter.hasNext();) { CrFileNode fileNode = iter.next(); //copy geniuswiki:file history as well FileNode my = FileNode.copyPersistToNode(fileNode); //copy geniuswiki:filenode->nt:resource node as well if (withResource) { File file = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), fileNode.getNodeType(), fileNode.getIdentifierUuid(), fileNode.getNodeUuid(), Integer.valueOf(fileNode.getVersion()).toString(), SimpleRepositoryServiceImpl.DEFAULT_FILE_NAME)); try { my.setFile(FileUtils.openInputStream(file)); } catch (IOException e) { log.warn("Failed get node " + file.getAbsolutePath() + " with error " + e); } } list.add(my); } return list; } public List<FileNode> getAllSpaceNodes(ITicket ticket, String type, String spaceUname, boolean withResource) throws RepositoryException { if (!ticket.isAllowRead()) { String error = "Workspace has not read permission " + ticket.getSpacename(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } CrWorkspace crW = getCrWorkspace(ticket); List<CrFileNode> nodes = crFileNodeDAO.getSpaceNodes(type, spaceUname); List<FileNode> list = new ArrayList<FileNode>(); //retrieve all geniuswiki:file nodes under this identifier for (Iterator<CrFileNode> iter = nodes.iterator(); iter.hasNext();) { CrFileNode fileNode = iter.next(); //copy geniuswiki:file history as well FileNode my = FileNode.copyPersistToNode(fileNode); //copy geniuswiki:filenode->nt:resource node as well if (withResource) { File file = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), fileNode.getNodeType(), fileNode.getIdentifierUuid(), fileNode.getNodeUuid(), Integer.valueOf(fileNode.getVersion()).toString(), SimpleRepositoryServiceImpl.DEFAULT_FILE_NAME)); try { my.setFile(FileUtils.openInputStream(file)); } catch (IOException e) { log.warn("Failed get node " + file.getAbsolutePath() + " with error " + e); } } list.add(my); } return list; } public boolean hasIdentifierNode(ITicket ticket, String type, String identifierUuid) throws RepositoryException { if (!ticket.isAllowRead()) { String error = "Workspace has not read permission " + ticket.getSpacename(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } CrWorkspace crW = getCrWorkspace(ticket); File id = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), type, identifierUuid)); return id.exists(); } public ITicket login(final String spacename, final String username, final String password) throws RepositoryException { return new ITicket() { public String getPassword() { return password; } public String getSpacename() { if (StringUtils.isBlank(spacename)) return DEFAULT_SPACE_NAME; return spacename; } public String getUsername() { return username; } public boolean isAllowRead() { return true; } public boolean isAllowWrite() { return true; } }; } public FileNode removeFile(ITicket ticket, String nodeUuid, String version) throws RepositoryException { if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for node " + nodeUuid; log.warn(error); throw new RepositoryException("Permission denied: " + error); } log.info("File will removed from " + ticket.getSpacename() + ". NodeUUID: " + nodeUuid + ". Version: " + version); //get this node base, so that we can know NodeType and IdentifierUuid CrFileNode filenode = crFileNodeDAO.getBaseByNodeUuid(nodeUuid); try { acquireLock(ticket.getSpacename(), filenode.getIdentifierUuid(), nodeUuid); CrWorkspace crW = getCrWorkspace(ticket); if (NumberUtils.toInt(version) == 0) { //remove history together crFileNodeDAO.removeByNodeUuid(nodeUuid); File fileDir = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), filenode.getNodeType(), filenode.getIdentifierUuid(), filenode.getNodeUuid())); FileUtils.cleanDirectory(fileDir); if (fileDir.delete()) log.info("All history " + filenode.getNodeUuid() + " is removed"); else log.info("Node " + filenode.getNodeUuid() + "physcial file can not be removed"); } else { //only remove special version if (!crFileNodeDAO.removeVersion(nodeUuid, NumberUtils.toInt(version))) { log.warn("No version " + version + " exist in Database"); } String verDir = FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), filenode.getNodeType(), filenode.getIdentifierUuid(), filenode.getNodeUuid(), Integer.valueOf(filenode.getVersion()).toString()); File ver = new File(verDir); FileUtils.cleanDirectory(ver); if (ver.delete()) log.info("Version " + version + " is removed"); else log.warn("Version " + version + " physcial file can not be removed"); } log.info("Remove is done"); return FileNode.copyPersistToNode(filenode); } catch (Exception e) { log.error("Remove file node failed ", e); throw new RepositoryException("Remove file node failed " + e); } finally { releaseLock(ticket.getSpacename(), filenode.getIdentifierUuid(), nodeUuid); } } public void removeIdentifier(ITicket ticket, String type, String identifierUuid) throws RepositoryException, RepositoryTiemoutExcetpion { if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for identifierUuid " + identifierUuid; log.warn(error); throw new RepositoryException("Permission denied: " + error); } String msg = "Type " + type + " in space " + ticket.getSpacename() + " identifier:" + identifierUuid + " is going to remove"; try { acquireLock(ticket.getSpacename(), identifierUuid, null); log.info(msg); CrWorkspace crW = getCrWorkspace(ticket); File id = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), type, identifierUuid)); FileUtils.deleteDirectory(id); crFileNodeDAO.removeByIdentifier(identifierUuid); } catch (IOException e) { log.error("Remove identifier failed :" + msg, e); throw new RepositoryException(msg); } finally { releaseLock(ticket.getSpacename(), identifierUuid, null); } log.info("Type " + type + " in space " + ticket.getSpacename() + " identifier:" + identifierUuid + " is removed"); } @SuppressWarnings("unchecked") public List<FileNode> saveFile(ITicket ticket, FileNode attachment, boolean md5DigestRequired, boolean discardSaveDiffMd5) throws RepositoryException, RepositoryTiemoutExcetpion, RepositoryQuotaException { List<FileNode> checkedIn = new ArrayList<FileNode>(); if (!attachment.isBulkZip()) { //TODO: does it need return only check-in successfully? checkedIn.add(attachment); } else { //process bulk upload String dir = null; try { dir = FileUtil.createTempDirectory(TMP_BULK_CHECKIN); ZipFileUtil.expandZipToFolder(attachment.getFile(), dir); //retrieve all files and check-in Collection<File> files = FileUtils.listFiles(new File(dir), null, true); if (files != null) { MimetypesFileTypeMap mineMap = new MimetypesFileTypeMap(); for (File file : files) { try { FileNode node = new FileNode(); //use same comment for all upload node.setComment(attachment.getComment()); node.setShared(attachment.isShared()); node.setFile(new FileInputStream(file)); node.setFilename(FileUtil.getFileName(file.getName())); node.setContentType(mineMap.getContentType(file)); node.setType(RepositoryService.TYPE_ATTACHMENT); node.setIdentifier(attachment.getIdentifier()); node.setCreateor(attachment.getCreateor()); node.setStatus(attachment.getStatus()); node.setSize(file.length()); node.setBulkZip(false); checkedIn.addAll(saveFile(ticket, node, md5DigestRequired, discardSaveDiffMd5)); } catch (Exception e) { log.error("Unable process some files in bulk zip", e); } } } } catch (FileUtilException e) { throw new RepositoryException("Unable create temp dir for bulk upload", e); } catch (ZipFileUtilException e) { throw new RepositoryException("Unable unzip bulk uploaded file", e); } finally { if (dir != null) { try { FileUtil.deleteDir(dir); } catch (IOException e) { log.error("Unable to delete directory " + dir); } } } return checkedIn; } //TODO: consider thread-safe if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for identifierUuid " + attachment.getIdentifier(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } checkSpaceQuota(ticket, attachment.getSize()); try { //lock at identifier level so that multiple users upload will still keep version works. acquireLock(ticket.getSpacename(), attachment.getIdentifier(), null); CrWorkspace crW = getCrWorkspace(ticket); List<CrFileNode> nodes = getBaseNodes(attachment.getType(), attachment.getIdentifier()); CrFileNode existFile = null, filenode = new CrFileNode(); log.info("File is going to save to " + ticket.getSpacename() + ""); //page->attachment->file->resource for (Iterator<CrFileNode> iter = nodes.iterator(); iter.hasNext();) { CrFileNode node = iter.next(); //if file is under same Identifier(page), and file name is same, then version the item. if (StringUtils.equalsIgnoreCase(node.getFilename(), attachment.getFilename())) { existFile = node; break; } } if (existFile != null) { //increase version filenode.setVersion(existFile.getVersion() + 1); filenode.setNodeUuid(existFile.getNodeUuid()); log.info("FileNode is appending version to a existed node :" + filenode.getNodeUuid() + " with new version " + filenode.getVersion()); } else { //this node name is useless now, so just create a random unique one filenode.setVersion(1); filenode.setNodeUuid(UUID.randomUUID().toString()); //TODO: encoding is important for lucene index building and search, here just set empty. filenode.setEncoding(""); File id = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), attachment.getType(), attachment.getIdentifier(), filenode.getNodeUuid())); if (id.exists()) { throw new RepositoryException("Node uuid directory already exist"); } if (!id.mkdirs()) { throw new RepositoryException( "Node uuid directory create failed. Full path is " + id.getAbsolutePath()); } log.info("FileNode is creating a new node :" + filenode.getNodeUuid()); } filenode.setSpaceUname(ticket.getSpacename()); resetMetaData(attachment, filenode); String verRootDir = FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), filenode.getNodeType(), filenode.getIdentifierUuid(), filenode.getNodeUuid(), new Integer(filenode.getVersion()).toString()); File verFile = new File(verRootDir); if (!verFile.mkdirs()) { //this is just ensure the case if MD5 is duplicated, system try to delete that version directory but failed... //at that case, only empty directory left there. if (verFile.exists() && verFile.list().length > 0) { throw new RepositoryException( "Node uuid " + filenode.getNodeUuid() + " can not create version directory " + Integer.valueOf(filenode.getVersion()).toString()); } } OutputStream file = null; File ofile = new File(verRootDir, DEFAULT_FILE_NAME); try { file = new FileOutputStream(ofile); //save physical file byte[] content = new byte[1024 * 1024]; int len; md5DigestRequired = md5DigestRequired && (md5Digest != null); while ((len = attachment.getFile().read(content)) != -1) { if (md5DigestRequired) { md5Digest.update(content, 0, len); } file.write(content, 0, len); } file.flush(); if (md5DigestRequired) { filenode.setMd5Digest(new String(Hex.encodeHex(md5Digest.digest()))); } if (discardSaveDiffMd5 && filenode.getVersion() > 1) { //compare if (filenode.getMd5Digest().equals(existFile.getMd5Digest())) { //tell to delete version directory as well in finally{}! checkedIn = null; log.info("MD5 is same and ignore checked in"); return null; } } //create new record in DB crFileNodeDAO.saveOrUpdate(filenode); //set back NodeUuid and Version to attachment attachment.setNodeUuid(filenode.getNodeUuid()); attachment.setVersion(Integer.valueOf(filenode.getVersion()).toString()); attachment.setDate(filenode.getModifiedDate().getTime()); log.debug("File node create on " + filenode.getModifiedDate() + " by version " + attachment.getVersion()); } catch (Exception e) { throw new RepositoryException("Failed save node " + e); } finally { if (file != null) { try { file.close(); } catch (Exception e) { log.error("Unable to close uploaded file"); } if (checkedIn == null) { if (!ofile.delete()) { log.error("Version file {} deleted failed when MD5 duplicated case", ofile.getAbsolutePath()); ofile.deleteOnExit(); } } } if (checkedIn == null) { //ignored check-in if (!verFile.delete()) { log.error("Version directory {} deleted failed when MD5 duplicated case", verRootDir); } } } } finally { releaseLock(ticket.getSpacename(), attachment.getIdentifier(), null); if (attachment.getFile() != null) { try { attachment.getFile().close(); } catch (Exception e) { } } } return checkedIn; } public void updateMetaData(ITicket ticket, FileNode attachment) throws RepositoryException { if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for nodeUUID " + attachment.getNodeUuid() + " version " + attachment.getVersion(); log.warn(error); throw new RepositoryException("Permission denied: " + error); } CrFileNode filenode; int ver = NumberUtils.toInt(attachment.getVersion(), -1); if (ver == -1) filenode = crFileNodeDAO.getBaseByNodeUuid(attachment.getNodeUuid()); else filenode = crFileNodeDAO.getVersionNode(attachment.getNodeUuid(), ver); //update all info except nodeUUID and version FileNode.copyNodeToPersist(attachment, filenode); crFileNodeDAO.saveOrUpdate(filenode); } public FileNode updateMetaData(ITicket ticket, String nodeUuid, String name, String desc) throws RepositoryException { if (!ticket.isAllowWrite()) { String error = "Workspace has not write permission " + ticket.getSpacename() + " for nodeUUID " + nodeUuid; log.warn(error); throw new RepositoryException("Permission denied: " + error); } CrFileNode filenode = crFileNodeDAO.getBaseByNodeUuid(nodeUuid); if (filenode == null) { log.error("unable to find file node by nodeUUID:" + nodeUuid); return null; } filenode.setComment(desc); filenode.setFilename(name); filenode.setModifiedDate(new Date()); crFileNodeDAO.saveOrUpdate(filenode); //update - currently, this useful for index service -which need get back createDate() return FileNode.copyPersistToNode(filenode); } //JDK1.6 @Override public FileNode getMetaDate(ITicket ticket, String nodeUuid, Integer version) throws RepositoryException { if (!ticket.isAllowRead()) { String error = "Workspace has not reading permission " + ticket.getSpacename() + " for nodeUUID " + nodeUuid; log.warn(error); throw new RepositoryException("Permission denied: " + error); } CrFileNode crFilenode = crFileNodeDAO.getVersionNode(nodeUuid, version); try { return getFileNode(ticket, crFilenode, false); } catch (FileNotFoundException e) { //it won't happen } return null; } public int updateExistWorkspacesQuota(long size) { return crWorkspaceDAO.updateExistWorkspacesQuota(size); } public void updateWorkspaceQuota(String spacename, long size) { crWorkspaceDAO.updateWorkspacesQuota(spacename, size); } public long[] getSpaceQuoteUsage(ITicket ticket, String spacename) { long[] ret = new long[2]; CrWorkspace crW = getCrWorkspace(ticket); if (crW == null) { //maybe $SYSTEM$ log.warn("Unable to get CR Workspace: " + spacename); return new long[] { 0, 0 }; } long quota = crW.getQuota(); long used = 0; File spaceDir = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid())); //if dir not exist, then just treat it as 0 size and valid if (spaceDir.exists()) { //append file size used = FileUtils.sizeOfDirectory(spaceDir); } ret[0] = used; ret[1] = quota; return ret; } //******************************************************************** // Private methods //******************************************************************** /** * @param ticket * @param filenode * @return * @throws FileNotFoundException */ private FileNode getFileNode(ITicket ticket, CrFileNode filenode, boolean withDownload) throws FileNotFoundException { FileNode attachment = new FileNode(); attachment.setFilename(filenode.getFilename()); if (withDownload) { CrWorkspace crW = getCrWorkspace(ticket); String baseFilename = FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), filenode.getNodeType(), filenode.getIdentifierUuid(), filenode.getNodeUuid(), Integer.valueOf(filenode.getVersion()).toString(), DEFAULT_FILE_NAME); InputStream baseFile = new FileInputStream(baseFilename); attachment.setFile(baseFile); } attachment.setType(filenode.getNodeType()); attachment.setContentType(filenode.getContentType()); attachment.setIdentifier(filenode.getIdentifierUuid()); return attachment; } /** * get CrWorkspace from database. Also need handle default workspace. * @param ticket * @return */ private CrWorkspace getCrWorkspace(ITicket ticket) { CrWorkspace crW; if (ticket.getSpacename().equals(DEFAULT_SPACE_NAME)) { crW = new CrWorkspace(); crW.setName(DEFAULT_SPACE_NAME); crW.setQuota(0); crW.setSpaceUuid(DEFAULT_SPACE_NAME); } else { crW = crWorkspaceDAO.getBySpaceName(ticket.getSpacename()); } return crW; } /** * @param ticket * @param size * @return * @throws RepositoryQuotaException */ private void checkSpaceQuota(ITicket ticket, long size) throws RepositoryQuotaException { long[] quota = getSpaceQuoteUsage(ticket, ticket.getSpacename()); //space not exist or unlimited if (quota[0] <= 0 || quota[1] <= 0) return; long spaceSize = quota[0] + size; if (spaceSize > quota[1]) { String error = "Space " + ticket.getSpacename() + " quota is " + Arrays.toString(quota) + " and current size is " + spaceSize; log.info(error); throw new RepositoryQuotaException(error); } } /** * @obsolete,use CrWorksSpace table replace Quota file (I thought, every space should has a file which contains Quota info) * @throws RepositoryQuotaException */ private void checkSpaceQuotaFile(ITicket ticket, long size) throws RepositoryQuotaException { CrWorkspace crW = getCrWorkspace(ticket); String spaceDir = FileUtil.getFullPath(homeDir, crW.getSpaceUuid()); File quotaFile = new File(FileUtil.getFullPath(spaceDir, QUOTA_FILE_NAME)); long quota = Global.SpaceQuota; if (!quotaFile.exists()) { //it is already be default quota size createQuotaFile(ticket); } else { //read quota from file InputStream is = null; try { is = new FileInputStream(quotaFile); byte[] out = new byte[1024]; int len = -1; StringBuffer sb = new StringBuffer(); while ((len = is.read(out)) != -1) { sb.append(new String(out, 0, len)); } quota = NumberUtils.toLong(sb.toString(), Global.SpaceQuota); } catch (Exception e) { log.error("Failed get quota size for space " + ticket.getSpacename(), e); //nothing } finally { IOUtils.closeQuietly(is); } } //append file size long spaceSize = FileUtils.sizeOfDirectory(new File(spaceDir)) + size; if (spaceSize > quota) { String error = "Space " + ticket.getSpacename() + " quota is " + quota + " and current size is " + spaceSize; log.info(error); throw new RepositoryQuotaException(error); } } /** * @obsolete,use CrWorksSpace table replace Quota file * @param ticket * @param quota */ private void createQuotaFile(ITicket ticket) { CrWorkspace crW = getCrWorkspace(ticket); File quota = new File(FileUtil.getFullPath(homeDir, crW.getSpaceUuid(), QUOTA_FILE_NAME)); //failure tolerance: try to create new default one OutputStream os = null; try { os = new FileOutputStream(quota); os.write(Long.valueOf(Global.SpaceQuota).toString().getBytes()); } catch (IOException e) { log.error("Quota file for space " + ticket.getSpacename() + " created failed", e); } finally { try { if (os != null) os.close(); } catch (IOException e) { //nothing } } } private void acquireLock(String spacename, String identifier, String nodeUuid) throws RepositoryTiemoutExcetpion { log.debug("Request - Repository space {} identifier {} node {} acquire write lock in thread {}. ", new String[] { spacename, identifier, nodeUuid, Thread.currentThread().getName() }); try { writeLock.lock(); //this aquireLock may recover from another lock, which will terminate in TIMEOUT, now cancel it, so that //once lock acquired correct, the thread can go through without any timeout limit(for big file upload, it is necessary). ThreadInterruptManager.removeThread(Thread.currentThread()); ConditionGroup cond = spaceLockMap.get(spacename); //maybe it is performance bottleneck if (identifier == null && nodeUuid == null) { //space level lock if (cond == null) { cond = new ConditionGroup(); spaceLockMap.put(spacename, cond); //this space never has a lock whatever any level, success acquired lock cond.spaceCondition = writeLock.newCondition(); } else { //need check itself and all sub-level node lock, if found any, then choose first one to lock thread Condition relCond = null; if (cond.spaceCondition != null) { relCond = cond.spaceCondition; } if (cond.identifierConditionMap.size() > 0) { relCond = cond.identifierConditionMap.values().iterator().next(); } else if (cond.nodeConditionMap.size() > 0) { Map<String, Condition> nodeMap = cond.nodeConditionMap.values().iterator().next(); if (nodeMap.size() > 0) { relCond = nodeMap.values().iterator().next(); } } if (relCond == null) { //not relative lock, success acquired lock , success acquired lock cond.spaceCondition = writeLock.newCondition(); } else { //2 minutes wait other relative lock release ThreadInterruptManager.addThread(Thread.currentThread(), TIMEOUT); relCond.await(); //do next cycle compete to acquire lock. acquireLock(spacename, identifier, nodeUuid); } } } else if (nodeUuid == null) { //identifier level lock if (cond == null) { cond = new ConditionGroup(); spaceLockMap.put(spacename, cond); //this space does not has a lock whatever any level yet, success acquired lock Condition idCond = writeLock.newCondition(); cond.identifierConditionMap.put(identifier, idCond); } else { //need check space level, and sub-level Condition relCond = null; if (cond.spaceCondition != null) { relCond = cond.spaceCondition; } else if (cond.identifierConditionMap.containsKey(identifier)) { relCond = cond.identifierConditionMap.get(identifier); } else if (cond.nodeConditionMap.containsKey(identifier)) { Map<String, Condition> nodeMap = cond.nodeConditionMap.get(identifier); if (nodeMap.size() > 0) { relCond = nodeMap.values().iterator().next(); } } if (relCond == null) { //no lock in this level, success acquired lock Condition idCond = writeLock.newCondition(); cond.identifierConditionMap.put(identifier, idCond); } else { //2 minutes wait, if ThreadInterruptManager.addThread(Thread.currentThread(), TIMEOUT); relCond.await(); //do next cycle compete to acquire lock. acquireLock(spacename, identifier, nodeUuid); } } } else { //node level lock if (cond == null) { cond = new ConditionGroup(); spaceLockMap.put(spacename, cond); //this space never has a lock whatever any level, success acquired lock Condition nodeCond = writeLock.newCondition(); Map<String, Condition> nodeMap = new HashMap<String, Condition>(); nodeMap.put(nodeUuid, nodeCond); cond.nodeConditionMap.put(identifier, nodeMap); } else { //need check space level, and its direct identifier level Condition relCond = null; if (cond.spaceCondition != null) { relCond = cond.spaceCondition; } else if (cond.identifierConditionMap.containsKey(identifier)) { relCond = cond.identifierConditionMap.get(identifier); } else if (cond.nodeConditionMap.containsKey(identifier)) { Map<String, Condition> nodeMap = cond.nodeConditionMap.get(identifier); relCond = nodeMap.get(nodeUuid); } if (relCond == null) { //not relative lock, success acquired lock , success acquired lock Condition nodeCond = writeLock.newCondition(); Map<String, Condition> nodeMap = cond.nodeConditionMap.get(identifier); if (nodeMap == null) { nodeMap = new HashMap<String, Condition>(); } nodeMap.put(nodeUuid, nodeCond); cond.nodeConditionMap.put(identifier, nodeMap); } else { //2 minutes wait, if ThreadInterruptManager.addThread(Thread.currentThread(), TIMEOUT); relCond.await(); //do next cycle compete to acquire lock. acquireLock(spacename, identifier, nodeUuid); } } } log.debug("Repository space {} identifier {} node {} acquire write lock successfully in thread {}. ", new String[] { spacename, identifier, nodeUuid, Thread.currentThread().getName() }); } catch (InterruptedException e) { log.info("Acquire space lock concurrent interrupted"); throw new RepositoryTiemoutExcetpion( "Repository cannot acquired write permssion in time period " + TIMEOUT + ". Timeout exception"); } finally { writeLock.unlock(); } } /** * release space level lock. */ private void releaseLock(String spacename, String identifier, String nodeUuid) { log.debug("Request - Release lock on space {} identifier {} node {} in {}", new String[] { spacename, identifier, nodeUuid, Thread.currentThread().getName() }); try { writeLock.lock(); ConditionGroup cond = spaceLockMap.get(spacename); if (cond == null) { AuditLogger.error("Unexpected case. No lock to relase for space " + spacename); } //maybe it is performance bottleneck if (identifier == null && nodeUuid == null) { //space level lock if (cond.spaceCondition != null) { //could have multiple threads waiting for this signal... cond.spaceCondition.signalAll(); cond.spaceCondition = null; } } else if (nodeUuid == null) { //identifier level lock Condition idCond = cond.identifierConditionMap.get(identifier); if (idCond != null) { idCond.signalAll(); cond.identifierConditionMap.remove(identifier); } } else { //node level lock Map<String, Condition> nodeMap = cond.nodeConditionMap.get(identifier); if (nodeMap != null) { Condition nodeCond = nodeMap.get(nodeUuid); if (nodeCond != null) { nodeCond.signalAll(); nodeMap.remove(nodeUuid); //no more node level lock, then clear this node identifier map. if (nodeMap.size() == 0) { cond.nodeConditionMap.remove(identifier); } } } } log.debug("Repository space {} identifier {} node {} release write lock successfully in {}", new String[] { spacename, identifier, nodeUuid, Thread.currentThread().getName() }); } finally { writeLock.unlock(); } } private void resetMetaData(FileNode attachment, CrFileNode filenode) { filenode.setIdentifierUuid(attachment.getIdentifier()); filenode.setNodeType(attachment.getType()); filenode.setShared(attachment.isShared()); filenode.setStatus(attachment.getStatus()); filenode.setFilename(attachment.getFilename() == null ? "" : attachment.getFilename()); filenode.setComment(attachment.getComment() == null ? "" : attachment.getComment()); filenode.setEncoding(attachment.getEncoding() == null ? "" : attachment.getEncoding()); //some properties saved in fileNode: filenode.setCreator(attachment.getCreateor() == null ? "" : attachment.getCreateor()); filenode.setContentType(attachment.getContentType() == null ? "" : attachment.getContentType()); filenode.setSize(attachment.getSize()); filenode.setModifiedDate(Calendar.getInstance().getTime()); } /** * Get base node of all nodes under identifier. * @param identifier * @return */ private List<CrFileNode> getBaseNodes(String nodeType, String identifier) { //first, get all nodes with version history: this result sorted by nodeUuid and version, so that, can easily find out base version List<CrFileNode> all = crFileNodeDAO.getIdentifierNodes(nodeType, identifier); //filter out base version node List<CrFileNode> baseNodes = new ArrayList<CrFileNode>(); String uuid = null; for (CrFileNode crFileNode : all) { if (!crFileNode.getNodeUuid().equals(uuid)) { //first one must be base version baseNodes.add(crFileNode); uuid = crFileNode.getNodeUuid(); } } return baseNodes; } //******************************************************************** // InitializingBean method //******************************************************************** public void afterPropertiesSet() throws Exception { if (homeDirResource == null) { throw new BeanInitializationException("Must specify a repository homeDirResource property"); } try { md5Digest = MessageDigest.getInstance("MD5"); } catch (Exception e) { log.error("Unable to initialize MD digest API. Digest function is disabled in Repository method", e); } //check if Repository root exists, if no, create one File locationDir = homeDirResource.getFile(); if (!locationDir.exists()) { if (!locationDir.mkdirs()) { throw new BeanInitializationException("Repository home dir can not created " + homeDirResource); } } this.homeDir = locationDir.getAbsolutePath(); //clean all lock files when system start. File lockDir = new File(FileUtil.getFullPath(homeDir, LOCK_DIR)); if (!lockDir.exists()) { lockDir.mkdir(); } else { FileUtils.cleanDirectory(lockDir); } } //******************************************************************** // private class //******************************************************************** private static class ConditionGroup { //3 level condition // static final int LEVEL_NODE = 0; // static final int LEVEL_IDENTIFIER = 1; // static final int LEVEL_SPACE = 2; public Condition spaceCondition; //so far don't use ConcurrentMap since these fields will be operated under synchorized Lock. public Map<String, Condition> identifierConditionMap = new HashMap<String, Condition>(); //2 String keys: identifierUuid, nodeUuid public Map<String, Map<String, Condition>> nodeConditionMap = new HashMap<String, Map<String, Condition>>(); } //******************************************************************** // set / get method //******************************************************************** public void setHomeDirResource(Resource homeDirResource) { this.homeDirResource = homeDirResource; } public void setCrFileNodeDAO(CrFileNodeDAO crFileNodeDAO) { this.crFileNodeDAO = crFileNodeDAO; } public void setCrWorkspaceDAO(CrWorkspaceDAO crWorkspaceDAO) { this.crWorkspaceDAO = crWorkspaceDAO; } }