diff --git a/config/alfresco/hibernate-context.xml b/config/alfresco/hibernate-context.xml index 8b36c30b19..920ec7815d 100644 --- a/config/alfresco/hibernate-context.xml +++ b/config/alfresco/hibernate-context.xml @@ -46,7 +46,8 @@ org/alfresco/repo/domain/hibernate/AppliedPatch.hbm.xml org/alfresco/repo/domain/hibernate/Permission.hbm.xml org/alfresco/repo/avm/hibernate/AVM.hbm.xml - org/alfresco/repo/attributes/hibernate/Attributes.hbm.xml + org/alfresco/repo/attributes/hibernate/Attributes.hbm.xml + org/alfresco/repo/domain/hibernate/UsageDelta.hbm.xml @@ -228,7 +229,13 @@ - + + + + + + + @@ -238,6 +245,9 @@ + + + diff --git a/config/alfresco/model/contentModel.xml b/config/alfresco/model/contentModel.xml index b394441108..c322d5e38e 100644 --- a/config/alfresco/model/contentModel.xml +++ b/config/alfresco/model/contentModel.xml @@ -184,7 +184,15 @@ d:text - + + + + d:long + + + d:long + + diff --git a/config/alfresco/node-services-context.xml b/config/alfresco/node-services-context.xml index a67086c5a8..f1bec03ebf 100644 --- a/config/alfresco/node-services-context.xml +++ b/config/alfresco/node-services-context.xml @@ -196,5 +196,27 @@ 5000 - + + + + + + + + + + + + + + + + + + + + ${system.usages.enabled} + + + diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml index 0ca29ab718..629d9afc17 100644 --- a/config/alfresco/public-services-context.xml +++ b/config/alfresco/public-services-context.xml @@ -563,6 +563,36 @@ ${server.transaction.mode.default} + + + + + + + org.alfresco.service.cmr.usage.ContentUsageService + + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index dba1cbbc19..9739681b41 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -804,6 +804,13 @@ - + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index a29a913819..8fdebc7459 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -172,3 +172,6 @@ user.name.caseSensitive=false # AVM Specific properties. avm.remote.idlestream.timeout=30000 + +# ECM content usages/quotas +system.usages.enabled=true diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml index 494fd9f17b..17e6d5bf5b 100644 --- a/config/alfresco/scheduled-jobs-context.xml +++ b/config/alfresco/scheduled-jobs-context.xml @@ -231,4 +231,123 @@ 0 30 3 * * ? + + + + + + + + + + + + + + + + + + + + + + + ${system.usages.enabled} + + + + + + + + + + + + + + + + + + + + + + + + ${system.usages.enabled} + + + true + + + + + + + + + org.alfresco.repo.usage.UserUsageCollapseJob + + + + + + + + + + + + + + + + + + + + 5 + + + 5 + + + + + + + + + + org.alfresco.repo.usage.UserUsageBootstrapJob + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + + \ No newline at end of file diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties index 61c9dc2dc7..0b9abb63c9 100644 --- a/config/alfresco/version.properties +++ b/config/alfresco/version.properties @@ -19,4 +19,4 @@ version.build=@build-number@ # Schema number -version.schema=113 +version.schema=114 diff --git a/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java b/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java index a8668a640a..83ab646f82 100644 --- a/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java +++ b/source/java/org/alfresco/filesys/smb/server/NTProtocolHandler.java @@ -1482,8 +1482,15 @@ public class NTProtocolHandler extends CoreProtocolHandler m_sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos); return; } + catch (DiskFullException ex) + { + m_sess.sendErrorResponseSMB(SMBStatus.NTDiskFull, SMBStatus.HRDWriteFault, SMBStatus.ErrHrd); + return; + } catch (java.io.IOException ex) { + if (logger.isDebugEnabled() && m_sess.hasDebug(SMBSrvSession.DBG_FILE)) + logger.debug("IOException ignore: "+ex); } // Remove the file from the connections list of open files diff --git a/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java b/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java index 3bdf0778fc..3b9f883cd1 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/ContentNetworkFile.java @@ -35,6 +35,7 @@ import java.nio.charset.Charset; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.filesys.server.filesys.AccessDeniedException; +import org.alfresco.filesys.server.filesys.DiskFullException; import org.alfresco.filesys.server.filesys.FileAttribute; import org.alfresco.filesys.server.filesys.FileInfo; import org.alfresco.filesys.server.filesys.FileOpenParams; @@ -52,6 +53,7 @@ import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.usage.ContentQuotaException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -382,7 +384,14 @@ public class ContentNetworkFile extends NodeRefNetworkFile // Update node properties ContentData contentData = content.getContentData(); - nodeService.setProperty( getNodeRef(), ContentModel.PROP_CONTENT, contentData); + try + { + nodeService.setProperty( getNodeRef(), ContentModel.PROP_CONTENT, contentData); + } + catch (ContentQuotaException qe) + { + throw new DiskFullException(qe.getMessage()); + } } else { diff --git a/source/java/org/alfresco/model/ContentModel.java b/source/java/org/alfresco/model/ContentModel.java index 30a6b82d0c..ad88e13643 100644 --- a/source/java/org/alfresco/model/ContentModel.java +++ b/source/java/org/alfresco/model/ContentModel.java @@ -168,6 +168,9 @@ public interface ContentModel static final QName PROP_PRESENCEPROVIDER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "presenceProvider"); static final QName PROP_PRESENCEUSERNAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "presenceUsername"); + static final QName PROP_SIZE_CURRENT = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "sizeCurrent"); + static final QName PROP_SIZE_QUOTA = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "sizeQuota"); + // Ownable aspect static final QName ASPECT_OWNABLE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "ownable"); static final QName PROP_OWNER = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "owner"); diff --git a/source/java/org/alfresco/repo/content/RoutingContentService.java b/source/java/org/alfresco/repo/content/RoutingContentService.java index e6ee3e0c27..a57cd08d2e 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentService.java +++ b/source/java/org/alfresco/repo/content/RoutingContentService.java @@ -59,6 +59,7 @@ import org.alfresco.service.cmr.repository.NoTransformerException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.usage.ContentQuotaException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; @@ -567,6 +568,10 @@ public class RoutingContentService implements ContentService " value: " + contentData); } } + catch (ContentQuotaException qe) + { + throw qe; + } catch (Throwable e) { throw new ContentIOException("Failed to set content property on stream closure: \n" + diff --git a/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml index c707a0987a..a67c6e0cb5 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml @@ -500,6 +500,19 @@ where node.store.key.protocol = :protocol and node.store.key.identifier = :identifier - + + + + select + node + from + org.alfresco.repo.domain.hibernate.NodeImpl as node + join node.properties prop + where + index(prop) = :propQName and + prop.stringValue = :propStringValue and + node.store.key.protocol = :protocol and + node.store.key.identifier = :identifier + diff --git a/source/java/org/alfresco/repo/domain/hibernate/UsageDelta.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/UsageDelta.hbm.xml new file mode 100644 index 0000000000..29247d9e71 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/hibernate/UsageDelta.hbm.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + sum(deltaSize) + from + org.alfresco.repo.usage.hibernate.UsageDeltaImpl as usage_delta + where + usage_delta.node = :node + + + + + + select + distinct usage_delta.node + from + org.alfresco.repo.usage.hibernate.UsageDeltaImpl as usage_delta + + + + + select + usage_delta + from + org.alfresco.repo.usage.hibernate.UsageDeltaImpl as usage_delta + where + usage_delta.node = :node + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/db/NodeDaoService.java b/source/java/org/alfresco/repo/node/db/NodeDaoService.java index fa076e896c..ad15290081 100644 --- a/source/java/org/alfresco/repo/node/db/NodeDaoService.java +++ b/source/java/org/alfresco/repo/node/db/NodeDaoService.java @@ -293,6 +293,8 @@ public interface NodeDaoService */ public int getNodeCount(final StoreRef storeRef); + public Collection getNodesWithPropertyStringValueForStore(final StoreRef storeRef, final QName propQName, final String propStringValue); + public Transaction getTxnById(long txnId); /** * Get all transactions in a given time range. Since time-based retrieval doesn't guarantee uniqueness diff --git a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java index 55a5d97c04..a675442cd9 100644 --- a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java @@ -60,6 +60,7 @@ import org.alfresco.repo.tenant.TenantService; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.TransactionAwareSingleton; import org.alfresco.repo.transaction.TransactionalDao; +import org.alfresco.repo.usage.UsageDeltaDAO; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.InvalidTypeException; import org.alfresco.service.cmr.repository.AssociationExistsException; @@ -113,6 +114,8 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements private static final String QUERY_GET_CHILD_ASSOCS_FOR_STORE = "node.GetChildAssocsForStore"; private static final String QUERY_GET_NODES_EXCEPT_ROOT_FOR_STORE = "node.GetNodesExceptRootForStore"; + private static final String QUERY_NODES_WITH_PROPERTY_STRING_VALUE_FOR_STORE = "node.GetNodesWithPropertyStringValueForStore"; + private static Log logger = LogFactory.getLog(HibernateNodeDaoServiceImpl.class); /** Log to trace parent association caching: classname + .ParentAssocsCache */ private static Log loggerParentAssocsCache = LogFactory.getLog(HibernateNodeDaoServiceImpl.class.getName() + ".ParentAssocsCache"); @@ -131,7 +134,14 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements /** used for debugging */ private Set changeTxnIdSet; - TenantService tenantService; + private UsageDeltaDAO usageDeltaDao; + private TenantService tenantService; + + + public void setUsageDeltaDao(UsageDeltaDAO usageDeltaDao) + { + this.usageDeltaDao = usageDeltaDao; + } public void setTenantService(TenantService tenantService) { @@ -616,6 +626,13 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements { getHibernateTemplate().delete(assoc); } + + if (isDebugEnabled) + { + logger.debug("Deleting usage deltas of node (if any)" + node.getId()); + } + usageDeltaDao.deleteDeltas(node); + // update the node status NodeRef nodeRef = node.getNodeRef(); NodeStatus nodeStatus = getNodeStatus(nodeRef, true); @@ -1427,6 +1444,29 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements // done return count.intValue(); } + + + @SuppressWarnings("unchecked") + public Collection getNodesWithPropertyStringValueForStore(final StoreRef storeRef, final QName propQName, final String propStringValue) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(QUERY_NODES_WITH_PROPERTY_STRING_VALUE_FOR_STORE); + query.setString("protocol", storeRef.getProtocol()) + .setString("identifier", storeRef.getIdentifier()) + .setParameter("propQName", propQName) + .setString("propStringValue", propStringValue) + .setReadOnly(true); + return query.list(); + } + }; + + List queryResults = (List) getHibernateTemplate().execute(callback); + return queryResults; + } + /* * Queries for transactions diff --git a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java index 0064f46e16..352ac35b45 100644 --- a/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java +++ b/source/java/org/alfresco/repo/security/person/PersonServiceImpl.java @@ -607,6 +607,10 @@ public class PersonServiceImpl implements PersonService, properties.put(ContentModel.PROP_EMAIL, ""); properties.put(ContentModel.PROP_ORGID, ""); properties.put(ContentModel.PROP_HOME_FOLDER_PROVIDER, defaultHomeFolderProvider); + + properties.put(ContentModel.PROP_SIZE_CURRENT, 0L); + properties.put(ContentModel.PROP_SIZE_QUOTA, -1L); // no quota + return properties; } @@ -615,6 +619,9 @@ public class PersonServiceImpl implements PersonService, String userName = DefaultTypeConverter.INSTANCE.convert(String.class, properties .get(ContentModel.PROP_USERNAME)); properties.put(ContentModel.PROP_USERNAME, userName); + + properties.put(ContentModel.PROP_SIZE_CURRENT, 0L); + return nodeService.createNode(getPeopleContainer(), ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_PERSON, ContentModel.TYPE_PERSON, properties).getChildRef(); } diff --git a/source/java/org/alfresco/repo/usage/ContentUsageImpl.java b/source/java/org/alfresco/repo/usage/ContentUsageImpl.java new file mode 100644 index 0000000000..4f42251314 --- /dev/null +++ b/source/java/org/alfresco/repo/usage/ContentUsageImpl.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.node.NodeServicePolicies; +import org.alfresco.repo.policy.JavaBehaviour; +import org.alfresco.repo.policy.PolicyComponent; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.usage.ContentUsageService; +import org.alfresco.service.cmr.usage.UsageService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implements Content Usage service and policies/behaviour. + * + */ +public class ContentUsageImpl implements ContentUsageService, + NodeServicePolicies.OnCreateNodePolicy, + NodeServicePolicies.OnUpdatePropertiesPolicy, + NodeServicePolicies.BeforeDeleteNodePolicy +{ + // Logger + private static Log logger = LogFactory.getLog(ContentUsageImpl.class); + + private NodeService nodeService; + private PersonService personService; + private PolicyComponent policyComponent; + private UsageService usageService; + + private boolean enabled = true; + + public static final StoreRef SPACES_STOREREF = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore"); + + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setUsageService(UsageService usageService) + { + this.usageService = usageService; + } + + public void setPolicyComponent(PolicyComponent policyComponent) + { + this.policyComponent = policyComponent; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + + /** + * The initialise method + */ + public void init() + { + if (enabled) + { + // Register interest in the onCreateNode policy + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateNode"), + ContentModel.TYPE_CONTENT, + new JavaBehaviour(this, "onCreateNode")); + + // Register interest in the onUpdateProperties policy + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"), + ContentModel.TYPE_CONTENT, + new JavaBehaviour(this, "onUpdateProperties")); + + // Register interest in the beforeDeleteNode policy + policyComponent.bindClassBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"), + ContentModel.TYPE_CONTENT, + new JavaBehaviour(this, "beforeDeleteNode")); + } + } + + + /** + * Called when a new node has been created. + * + * @param childAssocRef the created child association reference + */ + public void onCreateNode(ChildAssociationRef childAssocRef) + { + NodeRef nodeRef = childAssocRef.getChildRef(); + if (nodeRef.getStoreRef().equals(SPACES_STOREREF)) + { + // Get content size + + // TODO use data dictionary to get content property + ContentData contentData = (ContentData)nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); + Long contentSize = (contentData == null ? null : contentData.getSize()); + + // Get owner/creator + String owner = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_OWNER); + if (owner == null) + { + owner = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_CREATOR); + } + + if (contentSize != null && contentSize != 0 && owner != null) + { + // new node with non-empty content size + if (logger.isDebugEnabled()) logger.debug("onCreateNode: contentSize="+contentSize+", nodeRef="+nodeRef+", ownerAfter="+owner); + incrementUserUsage(owner, contentSize, nodeRef); + } + } + } + + /** + * Called after a node's properties have been changed. + * + * @param nodeRef reference to the updated node + * @param before the node's properties before the change + * @param after the node's properties after the change + */ + public void onUpdateProperties( + NodeRef nodeRef, + Map before, + Map after) + { + if (nodeRef.getStoreRef().equals(SPACES_STOREREF)) + { + // Check for change in content size + + // TODO use data dictionary to get content property + ContentData contentDataBefore = (ContentData)before.get(ContentModel.PROP_CONTENT); + Long contentSizeBefore = (contentDataBefore == null ? null : contentDataBefore.getSize()); + ContentData contentDataAfter = (ContentData)after.get(ContentModel.PROP_CONTENT); + Long contentSizeAfter = (contentDataAfter == null ? null : contentDataAfter.getSize()); + + // Check for change in owner/creator + String ownerBefore = (String)before.get(ContentModel.PROP_OWNER); + if (ownerBefore == null) + { + ownerBefore = (String)before.get(ContentModel.PROP_CREATOR); + } + String ownerAfter = (String)after.get(ContentModel.PROP_OWNER); + if (ownerAfter == null) + { + ownerAfter = (String)after.get(ContentModel.PROP_CREATOR); + } + + // check change in size (and possibly owner) + if (contentSizeBefore == null && contentSizeAfter != null && contentSizeAfter != 0 && ownerAfter != null) + { + // new size has been added - note: ownerBefore does not matter since the contentSizeBefore is null + if (logger.isDebugEnabled()) logger.debug("onUpdateProperties: updateSize (null -> "+contentSizeAfter+"): nodeRef="+nodeRef+", ownerAfter="+ownerAfter); + incrementUserUsage(ownerAfter, contentSizeAfter, nodeRef); + + } + else if (contentSizeAfter == null && contentSizeBefore != null && contentSizeBefore != 0 && ownerBefore != null) + { + // old size has been removed - note: ownerAfter does not matter since contentSizeAfter is null + if (logger.isDebugEnabled()) logger.debug("onUpdateProperties: updateSize ("+contentSizeBefore+" -> null): nodeRef="+nodeRef+", ownerBefore="+ownerBefore); + decrementUserUsage(ownerBefore, contentSizeBefore, nodeRef); + } + else if (contentSizeBefore != null && contentSizeAfter != null) + { + if (contentSizeBefore.equals(contentSizeAfter) == false) + { + // size has changed (and possibly owner) + if (logger.isDebugEnabled()) logger.debug("onUpdateProperties: updateSize ("+contentSizeBefore+" -> "+contentSizeAfter+"): nodeRef="+nodeRef+", ownerBefore="+ownerBefore+", ownerAfter="+ownerAfter); + + if (contentSizeBefore != 0 && ownerBefore != null) + { + decrementUserUsage(ownerBefore, contentSizeBefore, nodeRef); + } + if (contentSizeAfter != 0 && ownerAfter != null) + { + incrementUserUsage(ownerAfter, contentSizeAfter, nodeRef); + } + } + else + { + // same size - check change in owner only + if (ownerBefore == null && ownerAfter != null && contentSizeAfter != 0 && ownerAfter != null) + { + // new owner has been added + if (logger.isDebugEnabled()) logger.debug("onUpdateProperties: updateOwner (null -> "+ownerAfter+"): nodeRef="+nodeRef+", contentSize="+contentSizeAfter); + incrementUserUsage(ownerAfter, contentSizeAfter, nodeRef); + } + else if (ownerAfter == null && ownerBefore != null && contentSizeBefore != 0) + { + // old owner has been removed + if (logger.isDebugEnabled()) logger.debug("onUpdateProperties: updateOwner ("+ownerBefore+" -> null): nodeRef="+nodeRef+", contentSize="+contentSizeBefore); + decrementUserUsage(ownerBefore, contentSizeBefore, nodeRef); + } + else if (ownerBefore != null && ownerAfter != null && ownerBefore.equals(ownerAfter) == false) + { + // owner has changed (size has not) + if (logger.isDebugEnabled()) logger.debug("onUpdateProperties: updateOwner ("+ownerBefore+" -> "+ownerAfter+"): nodeRef="+nodeRef+", contentSize="+contentSizeBefore); + + if (contentSizeBefore != 0) + { + decrementUserUsage(ownerBefore, contentSizeBefore, nodeRef); + } + if (contentSizeAfter != 0) + { + incrementUserUsage(ownerAfter, contentSizeAfter, nodeRef); + } + } + } + } + } + } + + /** + * Called before a node is deleted. + * + * @param nodeRef the node reference + */ + public void beforeDeleteNode(NodeRef nodeRef) + { + if (nodeRef.getStoreRef().equals(SPACES_STOREREF)) + { + // TODO use data dictionary to get content property + ContentData contentData = (ContentData)nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT); + + if (contentData != null) + { + long contentSize = contentData.getSize(); + String owner = (String)nodeService.getProperty(nodeRef, ContentModel.PROP_OWNER); + + if (contentSize != 0 && owner != null) + { + // decrement usage if node is being deleted + if (logger.isDebugEnabled()) logger.debug("beforeDeleteNode: nodeRef="+nodeRef+", owner="+owner+", contentSize="+contentSize); + decrementUserUsage(owner, contentSize, nodeRef); + } + } + } + } + + private void incrementUserUsage(String userName, long contentSize, NodeRef contentNodeRef) + { + // increment usage - add positive delta + if (logger.isDebugEnabled()) logger.debug("incrementUserUsage: username="+userName+", contentSize="+contentSize+", contentNodeRef="+contentNodeRef); + + long currentSize = getUserUsage(userName); + long quotaSize = getUserQuota(userName); + + long newSize = currentSize + contentSize; + + // check whether user's quota exceeded + if ((quotaSize != -1) && (newSize > quotaSize)) + { + if (logger.isWarnEnabled()) + { + logger.warn("User (" + userName + ") quota exceeded: content=" + contentSize + + ", usage=" + currentSize + + ", quota=" + quotaSize); + } + } + + NodeRef personNodeRef = personService.getPerson(userName); + usageService.insertDelta(personNodeRef, contentSize); + } + + private void decrementUserUsage(String userName, long contentSize, NodeRef contentNodeRef) + { + // decrement usage - add negative delta + if (logger.isDebugEnabled()) logger.debug("decrementUserUsage: username="+userName+", contentSize="+contentSize+", contentNodeRef="+contentNodeRef); + + long currentSize = getUserUsage(userName); + + long newSize = currentSize + contentSize; + + if (newSize < 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("User (" + userName + ") has negative usage (" + newSize + ") - reset to 0"); + } + } + + NodeRef personNodeRef = personService.getPerson(userName); + usageService.insertDelta(personNodeRef, (-contentSize)); + } + + /** + * Set user's usage. Should only be called by background (collapse) job ! + * + * @param userName + * @param currentUsage + */ + public void setUserStoredUsage(NodeRef personNodeRef, long currentUsage) + { + if (personNodeRef != null) + { + nodeService.setProperty(personNodeRef, ContentModel.PROP_SIZE_CURRENT, new Long(currentUsage)); + } + } + + public long getUserStoredUsage(NodeRef personNodeRef) + { + Long currentUsage = null; + if (personNodeRef != null) + { + currentUsage = (Long)nodeService.getProperty(personNodeRef, ContentModel.PROP_SIZE_CURRENT); + } + + return (currentUsage == null ? -1 : currentUsage); + } + + public long getUserUsage(String userName) + { + long currentUsage = -1; + + NodeRef personNodeRef = personService.getPerson(userName); + if (personNodeRef != null) + { + currentUsage = getUserStoredUsage(personNodeRef); + } + + if (currentUsage != -1) + { + // add any deltas + currentUsage = currentUsage + usageService.getTotalDeltaSize(personNodeRef); + + if (currentUsage < 0) + { + if (logger.isWarnEnabled()) + { + logger.warn("User usage ("+ userName+") is negative ("+currentUsage+") overriding to 0"); + } + currentUsage = 0; + } + } + + return currentUsage; + } + + /** + * Set user's current quota. + * Usually called by Web Client (Admin Console) if admin is changing/setting a user's quota. + * + * @param userName + * @param currentQuota + */ + public void setUserQuota(String userName, long currentQuota) + { + NodeRef personNodeRef = personService.getPerson(userName); + if (personNodeRef != null) + { + nodeService.setProperty(personNodeRef, ContentModel.PROP_SIZE_QUOTA, new Long(currentQuota)); + } + } + + public long getUserQuota(String userName) + { + Long currentQuota = null; + + NodeRef personNodeRef = personService.getPerson(userName); + if (personNodeRef != null) + { + currentQuota = (Long)nodeService.getProperty(personNodeRef, ContentModel.PROP_SIZE_QUOTA); + } + + return (currentQuota == null ? -1 : currentQuota); + } + + public boolean getEnabled() + { + return enabled; + } +} diff --git a/source/java/org/alfresco/repo/usage/UsageDelta.java b/source/java/org/alfresco/repo/usage/UsageDelta.java new file mode 100644 index 0000000000..4f70145266 --- /dev/null +++ b/source/java/org/alfresco/repo/usage/UsageDelta.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import org.alfresco.repo.domain.Node; + +/** + * Interface for persistent usage delta objects. + * + */ +public interface UsageDelta +{ + public Node getNode(); + + public void setNode(Node node); + + public long getDeltaSize(); + + public void setDeltaSize(long usageDeltaSize); +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/usage/UsageDeltaDAO.java b/source/java/org/alfresco/repo/usage/UsageDeltaDAO.java new file mode 100644 index 0000000000..8dd4e73857 --- /dev/null +++ b/source/java/org/alfresco/repo/usage/UsageDeltaDAO.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import java.util.Set; + +import org.alfresco.repo.domain.Node; + +/** + * The interface to persist usage delta information. + * + */ +public interface UsageDeltaDAO +{ + /** + * Create a usage delta entry. + * + * @param deltaInfo + */ + public void insertDelta(UsageDelta deltaInfo); + + /** + * Get the total delta size for a node. + * + * @param node + * @return sum of delta sizes (in bytes) - can be +ve or -ve + */ + public long getTotalDeltaSize(Node node); + + + public Set getUsageDeltaNodes(); + + public int deleteDeltas(Node node); +} diff --git a/source/java/org/alfresco/repo/usage/UsageServiceImpl.java b/source/java/org/alfresco/repo/usage/UsageServiceImpl.java new file mode 100644 index 0000000000..9eee352bee --- /dev/null +++ b/source/java/org/alfresco/repo/usage/UsageServiceImpl.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.usage.hibernate.UsageDeltaImpl; +import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.usage.UsageService; +import org.alfresco.util.ParameterCheck; + +/** + * The implementation of the UsageService for tracking usages. + * + */ +public class UsageServiceImpl implements UsageService +{ + private UsageDeltaDAO usageDeltaDao; + private NodeDaoService nodeDaoService; + private TenantService tenantService; + + //private static Log logger = LogFactory.getLog(UsageServiceImpl.class); + + + public void setUsageDeltaDao(UsageDeltaDAO usageDeltaDao) + { + this.usageDeltaDao = usageDeltaDao; + } + + public void setNodeDaoService(NodeDaoService nodeDaoService) + { + this.nodeDaoService = nodeDaoService; + } + + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + + + public void insertDelta(NodeRef usageNodeRef, long deltaSize) + { + UsageDelta delta = new UsageDeltaImpl(); + + // delta properties + delta.setNode(getNodeNotNull(usageNodeRef)); + delta.setDeltaSize(deltaSize); + + usageDeltaDao.insertDelta(delta); + } + + public long getTotalDeltaSize(NodeRef usageNodeRef) + { + return usageDeltaDao.getTotalDeltaSize(getNodeNotNull(usageNodeRef)); + } + + public Set getUsageDeltaNodes() + { + Set nodes = usageDeltaDao.getUsageDeltaNodes(); + + // convert nodes to nodeRefs + Set results = new HashSet(nodes.size()); + for (Node node : nodes) + { + results.add(tenantService.getBaseName(node.getNodeRef())); + } + return results; + } + + public int deleteDeltas(NodeRef usageNodeRef) + { + return usageDeltaDao.deleteDeltas(getNodeNotNull(usageNodeRef)); + } + + private Node getNodeNotNull(NodeRef nodeRef) throws InvalidNodeRefException + { + ParameterCheck.mandatory("nodeRef", nodeRef); + + Node unchecked = nodeDaoService.getNode(tenantService.getName(nodeRef)); + if (unchecked == null) + { + throw new InvalidNodeRefException("Node does not exist: " + nodeRef, nodeRef); + } + return unchecked; + } +} diff --git a/source/java/org/alfresco/repo/usage/UserUsageBootstrapJob.java b/source/java/org/alfresco/repo/usage/UserUsageBootstrapJob.java new file mode 100644 index 0000000000..f5488b6dd5 --- /dev/null +++ b/source/java/org/alfresco/repo/usage/UserUsageBootstrapJob.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Bootstraps user's content usage. This job is performed once at startup. + * + * If usages are enabled (as specified by 'system.usages.enabled=true' repository property) then will calculate + * usages for all users that have no current usage. + * + * If usages are disabled (as specified by 'system.usages.enabled=false' repository property) then will clear + * current usages for all users. + */ +public class UserUsageBootstrapJob implements Job +{ + private static final String KEY_COMPONENT = "userUsageBootstrapComponent"; + + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap jobData = context.getJobDetail().getJobDataMap(); + UserUsageTrackingComponent usageComponent = (UserUsageTrackingComponent) jobData.get(KEY_COMPONENT); + if (usageComponent == null) + { + throw new JobExecutionException("Missing job data: " + KEY_COMPONENT); + } + // perform the content usage calculations + usageComponent.execute(); + } +} diff --git a/source/java/org/alfresco/repo/usage/UserUsageCollapseJob.java b/source/java/org/alfresco/repo/usage/UserUsageCollapseJob.java new file mode 100644 index 0000000000..7ad8edb4c5 --- /dev/null +++ b/source/java/org/alfresco/repo/usage/UserUsageCollapseJob.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Collapses user's content usage delta. This is performed as a regular background job. + */ +public class UserUsageCollapseJob implements Job +{ + private static final String KEY_COMPONENT = "userUsageCollapseComponent"; + + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap jobData = context.getJobDetail().getJobDataMap(); + UserUsageTrackingComponent usageComponent = (UserUsageTrackingComponent) jobData.get(KEY_COMPONENT); + if (usageComponent == null) + { + throw new JobExecutionException("Missing job data: " + KEY_COMPONENT); + } + // perform the content usage calculations + usageComponent.execute(); + } +} diff --git a/source/java/org/alfresco/repo/usage/UserUsageTrackingComponent.java b/source/java/org/alfresco/repo/usage/UserUsageTrackingComponent.java new file mode 100644 index 0000000000..08a266b86e --- /dev/null +++ b/source/java/org/alfresco/repo/usage/UserUsageTrackingComponent.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.TransactionServiceImpl; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.repository.ContentData; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.usage.UsageService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * User Usage Tracking Component - to allow user usages to be collapsed or re-calculated + * + * - used by UserUsageCollapseJob to collapse usage deltas. + * - used by UserUsageBootstrapJob to either clear all usages or (re-)calculate all missing usages. + */ +public class UserUsageTrackingComponent +{ + private static Log logger = LogFactory.getLog(UserUsageTrackingComponent.class); + + private static boolean busy = false; + + private boolean bootstrap = false; + + private NodeDaoService nodeDaoService; + private TransactionServiceImpl transactionService; + private ContentUsageImpl contentUsageImpl; + + private PersonService personService; + private NodeService nodeService; + private UsageService usageService; + + private boolean enabled = true; + + + public void setNodeDaoService(NodeDaoService nodeDaoService) + { + this.nodeDaoService = nodeDaoService; + } + + public void setTransactionService(TransactionServiceImpl transactionService) + { + this.transactionService = transactionService; + } + + public void setContentUsageImpl(ContentUsageImpl contentUsageImpl) + { + this.contentUsageImpl = contentUsageImpl; + } + + public void setBootstrap(boolean bootstrap) + { + this.bootstrap = bootstrap; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setUsageService(UsageService usageService) + { + this.usageService = usageService; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + + public void execute() + { + try + { + if (! busy && ! enabled) + { + busy = true; + + // disabled - remove all usages + if (bootstrap == true) + { + if (logger.isDebugEnabled()) + { + logger.debug("Disabled - clear usages for all users ..."); + } + + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + + // wrap to make the request in a transaction + RetryingTransactionCallback clearAllUsages = new RetryingTransactionCallback() + { + public Integer execute() throws Throwable + { + Set allPeople = personService.getAllPeople(); + + for (NodeRef personNodeRef : allPeople) + { + nodeService.setProperty(personNodeRef, ContentModel.PROP_SIZE_CURRENT, null); + usageService.deleteDeltas(personNodeRef); + } + return allPeople.size(); + } + }; + // execute in txn + int count = txnHelper.doInTransaction(clearAllUsages, false); + + if (logger.isDebugEnabled()) + { + logger.debug("... cleared usage for " + count + " users"); + } + } + } + else if (! busy && enabled) + { + busy = true; + + if (bootstrap == true) + { + if (logger.isDebugEnabled()) + { + logger.debug("Enabled - calculate usages for all users (without usage) ..."); + } + + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + + // wrap to make the request in a transaction + RetryingTransactionCallback> getAllPeople = new RetryingTransactionCallback>() + { + public Set execute() throws Throwable + { + Set allPeople = personService.getAllPeople(); + Set userNames = new HashSet(); + + for (NodeRef personNodeRef : allPeople) + { + Long currentUsage = (Long)nodeService.getProperty(personNodeRef, ContentModel.PROP_SIZE_CURRENT); + if (currentUsage == null) + { + String userName = (String)nodeService.getProperty(personNodeRef, ContentModel.PROP_USERNAME); + userNames.add(userName); + } + } + return userNames; + } + }; + // execute in READ-ONLY txn + final Set userNames = txnHelper.doInTransaction(getAllPeople, true); + + for (String userName : userNames) + { + recalculateUsage(userName); + } + + if (logger.isDebugEnabled()) + { + logger.debug("... calculated usage for " + userNames.size() + " users"); + } + } + else + { + // Collapse usage deltas (if a person has initial usage set) + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + + // wrap to make the request in a transaction + RetryingTransactionCallback collapseUsages = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + // Get distinct candidates + Set usageNodeRefs = usageService.getUsageDeltaNodes(); + + for(NodeRef usageNodeRef : usageNodeRefs) + { + QName nodeType = nodeService.getType(usageNodeRef); + + if (nodeType.equals(ContentModel.TYPE_PERSON)) + { + NodeRef personNodeRef = usageNodeRef; + String userName = (String)nodeService.getProperty(personNodeRef, ContentModel.PROP_USERNAME); + + long currentUsage = contentUsageImpl.getUserStoredUsage(personNodeRef); + if (currentUsage != -1) + { + // collapse the usage deltas + currentUsage = contentUsageImpl.getUserUsage(userName); + usageService.deleteDeltas(personNodeRef); + contentUsageImpl.setUserStoredUsage(personNodeRef, currentUsage); + + if (logger.isDebugEnabled()) + { + logger.debug("Collapsed usage: username=" + userName + ", usage=" + currentUsage); + } + } + else + { + if (logger.isWarnEnabled()) + { + logger.warn("Initial usage for user has not yet been calculated: " + userName); + } + } + } + } + return null; + } + }; + + txnHelper.doInTransaction(collapseUsages, false); + } + } + } + finally + { + busy = false; + } + } + + /** + * Recalculate content usage for given user. Required if upgrading an existing Alfresco, for users that + * have not had their initial usage calculated. In a future release, could also be called explicitly by + * a SysAdmin, eg. via a JMX operation. + * + * @param userName + */ + public void recalculateUsage(final String userName) + { + final StoreRef storeRef = ContentUsageImpl.SPACES_STOREREF; + + RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper(); + + // wrap to make the request in a transaction + RetryingTransactionCallback calculatePersonCurrentUsage = new RetryingTransactionCallback() + { + public Long execute() throws Throwable + { + // get nodes for which user is owner + Collection ownerNodes = nodeDaoService.getNodesWithPropertyStringValueForStore(storeRef, ContentModel.PROP_OWNER, userName); + + long totalUsage = 0; + for (Node ownerNode : ownerNodes) + { + if (ownerNode.getTypeQName().equals(ContentModel.TYPE_CONTENT)) + { + ContentData contentData = ContentData.createContentProperty(ownerNode.getProperties().get(ContentModel.PROP_CONTENT).getStringValue()); + totalUsage = totalUsage + contentData.getSize(); + } + } + + // get nodes for which user is creator, and then filter out those that have an owner + Collection creatorNodes = nodeDaoService.getNodesWithPropertyStringValueForStore(storeRef, ContentModel.PROP_CREATOR, userName); + + for (Node creatorNode : creatorNodes) + { + if (creatorNode.getTypeQName().equals(ContentModel.TYPE_CONTENT) && + creatorNode.getProperties().get(ContentModel.PROP_OWNER) == null) + { + ContentData contentData = ContentData.createContentProperty(creatorNode.getProperties().get(ContentModel.PROP_CONTENT).getStringValue()); + totalUsage = totalUsage + contentData.getSize(); + } + } + + if (logger.isDebugEnabled()) + { + long quotaSize = contentUsageImpl.getUserQuota(userName); + logger.debug("Recalc usage ("+ userName+") totalUsage="+totalUsage+", quota="+quotaSize); + } + + return totalUsage; + } + }; + // execute in READ-ONLY txn + final Long currentUsage = txnHelper.doInTransaction(calculatePersonCurrentUsage, true); + + // wrap to make the request in a transaction + RetryingTransactionCallback setUserCurrentUsage = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + NodeRef personNodeRef = personService.getPerson(userName); + contentUsageImpl.setUserStoredUsage(personNodeRef, currentUsage); + usageService.deleteDeltas(personNodeRef); + return null; + } + }; + txnHelper.doInTransaction(setUserCurrentUsage, false); + } +} diff --git a/source/java/org/alfresco/repo/usage/hibernate/HibernateUsageDeltaDAO.java b/source/java/org/alfresco/repo/usage/hibernate/HibernateUsageDeltaDAO.java new file mode 100644 index 0000000000..fe7aa9f25f --- /dev/null +++ b/source/java/org/alfresco/repo/usage/hibernate/HibernateUsageDeltaDAO.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage.hibernate; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.transaction.TransactionalDao; +import org.alfresco.repo.usage.UsageDelta; +import org.alfresco.repo.usage.UsageDeltaDAO; +import org.alfresco.util.GUID; +import org.hibernate.Query; +import org.hibernate.Session; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * Hibernate-specific implementation of the persistence-independent Usage Delta DAO interface + * + */ +public class HibernateUsageDeltaDAO extends HibernateDaoSupport implements UsageDeltaDAO, TransactionalDao +{ + private static final String QUERY_GET_DELTAS = "usage.GetDeltas"; + private static final String QUERY_GET_TOTAL_DELTA_SIZE = "usage.GetTotalDeltaSize"; + private static final String QUERY_GET_USAGE_DELTA_NODES = "usage.GetUsageDeltaNodes"; + + /** a uuid identifying this unique instance */ + private final String uuid; + + + /** + * + */ + public HibernateUsageDeltaDAO() + { + this.uuid = GUID.generate(); + } + + /** + * Checks equality by type and uuid + */ + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + else if (!(obj instanceof HibernateUsageDeltaDAO)) + { + return false; + } + HibernateUsageDeltaDAO that = (HibernateUsageDeltaDAO) obj; + return this.uuid.equals(that.uuid); + } + + /** + * @see #uuid + */ + public int hashCode() + { + return uuid.hashCode(); + } + + /** + * NO-OP + */ + public void beforeCommit() + { + } + + /** + * Does this Session contain any changes which must be + * synchronized with the store? + * + * @return true => changes are pending + */ + public boolean isDirty() + { + // create a callback for the task + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + return session.isDirty(); + } + }; + // execute the callback + return ((Boolean)getHibernateTemplate().execute(callback)).booleanValue(); + } + + /** + * Just flushes the session + */ + public void flush() + { + getSession().flush(); + } + + @SuppressWarnings("unchecked") + public int deleteDeltas(final Node node) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(QUERY_GET_DELTAS); + query.setParameter("node", node); + return query.list(); + } + }; + + // execute + List queryResults = (List)getHibernateTemplate().execute(callback); + + for (UsageDelta usageDelta : queryResults) + { + getHibernateTemplate().delete(usageDelta); + } + + return queryResults.size(); + } + + @SuppressWarnings("unchecked") + public long getTotalDeltaSize(final Node node) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(QUERY_GET_TOTAL_DELTA_SIZE); + query.setParameter("node", node); + query.setReadOnly(true); + return query.uniqueResult(); + } + }; + // execute read-only tx + Long queryResult = (Long)getHibernateTemplate().execute(callback); + + return (queryResult == null ? 0 : queryResult); + } + + public void insertDelta(UsageDelta deltaInfo) + { + // Save + getSession().save(deltaInfo); + } + + @SuppressWarnings("unchecked") + public Set getUsageDeltaNodes() + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session.getNamedQuery(QUERY_GET_USAGE_DELTA_NODES); + query.setReadOnly(true); + return query.list(); + } + }; + // execute read-only tx + List queryResults = (List)getHibernateTemplate().execute(callback); + Set results = new HashSet(queryResults.size()); + for (Node node : queryResults) + { + results.add(node); + } + return results; + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/usage/hibernate/UsageDeltaImpl.java b/source/java/org/alfresco/repo/usage/hibernate/UsageDeltaImpl.java new file mode 100644 index 0000000000..f8fb325070 --- /dev/null +++ b/source/java/org/alfresco/repo/usage/hibernate/UsageDeltaImpl.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.usage.hibernate; + +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.usage.UsageDelta; + +/** + * Usage Delta Implementation + * + */ +public class UsageDeltaImpl implements UsageDelta +{ + private Long id; + private Long version; + + private Node node; + private long deltaSize; // +ve or -ve or 0 (in bytes) + + + public Long getId() + { + return id; + } + + /** + * For Hibernate use + */ + @SuppressWarnings("unused") + private void setId(Long id) + { + this.id = id; + } + + public Long getVersion() + { + return version; + } + + /** + * For Hibernate use + */ + @SuppressWarnings("unused") + private void setVersion(Long version) + { + this.version = version; + } + + + public Node getNode() + { + return node; + } + + public void setNode(Node node) + { + this.node = node; + } + + public long getDeltaSize() + { + return deltaSize; + } + + public void setDeltaSize(long deltaSize) + { + this.deltaSize = deltaSize; + } +} diff --git a/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java b/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java index c51abcdbb5..b1866f6a1f 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java @@ -116,7 +116,7 @@ public class WorkflowInterpreter extends BaseInterpreter if (!transactionService.isReadOnly()) { - interpretCommand("var bpm:package package 1"); + //interpretCommand("var bpm:package package 1"); // only used for testing workflows, causes increment usage/delta interpretCommand("var bpm:assignee person admin"); } diff --git a/source/java/org/alfresco/service/cmr/usage/ContentQuotaException.java b/source/java/org/alfresco/service/cmr/usage/ContentQuotaException.java new file mode 100644 index 0000000000..05e02e1fce --- /dev/null +++ b/source/java/org/alfresco/service/cmr/usage/ContentQuotaException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.service.cmr.usage; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * + */ +public class ContentQuotaException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 1346806021547860709L; + + public ContentQuotaException(String msg) + { + super(msg); + } + + public ContentQuotaException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java b/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java new file mode 100644 index 0000000000..5475c1e42a --- /dev/null +++ b/source/java/org/alfresco/service/cmr/usage/ContentUsageService.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.service.cmr.usage; + +import org.alfresco.service.Auditable; +import org.alfresco.service.PublicService; + +@PublicService +public interface ContentUsageService +{ + /** + * Gets user usage + * + * @return Return user's current calculated usage (in bytes) + */ + @Auditable + public long getUserUsage(String userName); + + /** + * Gets user quota + * + * Note: -1 means no quota limit set + * + * @return Return user's quota (in bytes). + */ + @Auditable + public long getUserQuota(String userName); + + /** + * Set user quota. + * + * Note: It is possible to set a quota that is below the current usage. At this point + * the user will be over quota until their usage is decreased. + * + * Note: -1 means no quota limit set + * + * @param User's new quota (in bytes) + */ + @Auditable + public void setUserQuota(String userName, long newQuota); + + /** + * Are ContentUsages enabled (refer to 'system.usages.enabled' repository property) ? + * + * @return true if ContentUsages are enabled, otherwise false + */ + @Auditable + public boolean getEnabled(); +} diff --git a/source/java/org/alfresco/service/cmr/usage/UsageService.java b/source/java/org/alfresco/service/cmr/usage/UsageService.java new file mode 100644 index 0000000000..6c7c7c66c5 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/usage/UsageService.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005-2007 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * 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. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.service.cmr.usage; + +import java.util.Set; + +import org.alfresco.service.NotAuditable; +import org.alfresco.service.PublicService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * The public API by which applications can create usage delta entries. + * + */ +@PublicService +public interface UsageService +{ + /** + * Add a usage delta entry. + */ + @NotAuditable + public void insertDelta(NodeRef usageNodeRef, long deltaSize); + + /** + * Get sum of usage delta sizes. + */ + @NotAuditable + public long getTotalDeltaSize(NodeRef usageNodeRef); + + /** + * Get distinct set of usage delta nodes + */ + @NotAuditable + public Set getUsageDeltaNodes(); + + /** + * Delete the usage delta nodes + */ + @NotAuditable + public int deleteDeltas(NodeRef usageNodeRef); +}