diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index dabd6e7dc9..3fade34540 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -217,6 +217,38 @@ + + + + + ${system.bootstrap.config_check.strict} + + + false + + + ${dir.root} + + + ${index.recovery.mode} + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/extension/custom-connection-pool-context.xml.sample b/config/alfresco/extension/custom-connection-pool-context.xml.sample index 546c1d3ac1..cdbef1c0ca 100644 --- a/config/alfresco/extension/custom-connection-pool-context.xml.sample +++ b/config/alfresco/extension/custom-connection-pool-context.xml.sample @@ -9,7 +9,8 @@ --> - + + @@ -28,30 +29,39 @@ false - + ${db.pool.initial} ${db.pool.max} - - 300000 - - - -1 - - - false - - 50000 - - - true + 10000 select 1 + + 300000 + + + 60000 + + + false + + + false + + + true + + + true + + + 30 + diff --git a/config/alfresco/messages/system-messages.properties b/config/alfresco/messages/system-messages.properties index 99ce516daf..f1a1b6e4d9 100644 --- a/config/alfresco/messages/system-messages.properties +++ b/config/alfresco/messages/system-messages.properties @@ -1,4 +1,15 @@ # System-related messages system.err.property_not_set=Property ''{0}'' has not been set: {1} -system.err.duplicate_name=Duplicate child name not allowed: {0} \ No newline at end of file +system.err.duplicate_name=Duplicate child name not allowed: {0} + +# Bootstrap configuration check messages + +system.config_check.warn.dir_root=The Alfresco ''dir.root'' property is set to a relative path ''{0}''. ''dir.root'' should be overridden to point to a specific folder. +system.config_check.msg.dir_root=The Alfresco root data directory (''dir.root'') is: {0} +system.config_check.err.indexes.duplicate_root_node=The store ''{0}'' has a duplicate root node entry. +system.config_check.err.missing_index=CONTENT INTEGRITY ERROR: Indexes not found for {0} stores. +system.config_check.err.missing_content=CONTENT INTEGRITY ERROR: Content not found for {0} stores. +system.config_check.err.fix_dir_root=Ensure that the ''dir.root'' property is pointing to the correct data location. +system.config_check.msg.howto_index_recover=You may set 'index.recovery.mode=FULL' if you need to rebuild the indexes. +system.config_check.warn.starting_with_errors=Alfresco is starting with errors. \ No newline at end of file diff --git a/config/alfresco/model/forumModel.xml b/config/alfresco/model/forumModel.xml index 900605d32d..0b85714e32 100644 --- a/config/alfresco/model/forumModel.xml +++ b/config/alfresco/model/forumModel.xml @@ -46,7 +46,7 @@ fm:forum - true + true false false diff --git a/config/alfresco/model/jcrModel.xml b/config/alfresco/model/jcrModel.xml index b8075aad9c..599428b582 100644 --- a/config/alfresco/model/jcrModel.xml +++ b/config/alfresco/model/jcrModel.xml @@ -24,7 +24,7 @@ d:qname true - true + true d:qname @@ -56,7 +56,7 @@ nt:base - true + true false jcr:content diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index f9e9e31869..6842a43698 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -16,6 +16,9 @@ dir.indexes.lock=${dir.indexes}/locks # The index recovery mode (NONE, VALIDATE, AUTO, FULL) index.recovery.mode=VALIDATE +# Change the failure behaviour of the configuration checker +system.bootstrap.config_check.strict=true + # #################### # # Lucene configuration # # #################### # @@ -64,7 +67,6 @@ lucene.commit.lock.timeout=100000 lucene.lock.poll.interval=100 # Database configuration - db.schema.update=true db.driver=org.gjt.mm.mysql.Driver db.name=alfresco @@ -75,7 +77,6 @@ db.pool.initial=10 db.pool.max=20 # Email configuration - mail.host= mail.port=25 mail.username=anonymous @@ -86,13 +87,11 @@ mail.encoding=UTF-8 mail.header= # System Configuration - system.store=system://system system.descriptor.childname=sys:descriptor system.descriptor.current.childname=sys:descriptor-current # User config - alfresco_user_store.store=user://alfrescoUserStore alfresco_user_store.system_container.childname=sys:system alfresco_user_store.user_container.childname=sys:people @@ -102,7 +101,6 @@ alfresco_user_store.authorities_container.childname=sys:authorities spaces.archive.store=archive://SpacesStore # Spaces Configuration - spaces.store=workspace://SpacesStore spaces.company_home.childname=app:company_home spaces.guest_home.childname=app:guest_home @@ -117,12 +115,10 @@ spaces.wcm.childname=app:wcm spaces.content_forms.childname=app:wcm_forms # Folders for storing people - system.system_container.childname=sys:system system.people_container.childname=sys:people # Folders for storing workflow related info - system.workflow_container.childname=sys:workflow # Are user names case sensitive? @@ -136,7 +132,6 @@ system.workflow_container.childname=sys:workflow # # Must other databases are case sensitive by default. # - user.name.caseSensitive=false # AVM Specific properties. diff --git a/source/java/org/alfresco/repo/action/actionModel.xml b/source/java/org/alfresco/repo/action/actionModel.xml index 9cbdae34fb..158762c431 100644 --- a/source/java/org/alfresco/repo/action/actionModel.xml +++ b/source/java/org/alfresco/repo/action/actionModel.xml @@ -116,7 +116,7 @@ act:action - true + true true @@ -130,7 +130,7 @@ act:action - true + true true diff --git a/source/java/org/alfresco/repo/admin/ConfigurationChecker.java b/source/java/org/alfresco/repo/admin/ConfigurationChecker.java new file mode 100644 index 0000000000..3e3a3c77fd --- /dev/null +++ b/source/java/org/alfresco/repo/admin/ConfigurationChecker.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2006 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.admin; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.i18n.I18NUtil; +import org.alfresco.repo.node.index.FullIndexRecoveryComponent.RecoveryMode; +import org.alfresco.repo.search.impl.lucene.LuceneQueryParser; +import org.alfresco.repo.security.authentication.AuthenticationComponent; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +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.search.LimitBy; +import org.alfresco.service.cmr.search.ResultSet; +import org.alfresco.service.cmr.search.ResultSetRow; +import org.alfresco.service.cmr.search.SearchParameters; +import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.AbstractLifecycleBean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +/** + * Component to perform a bootstrap check of the alignment of the + * database, Lucene indexes and content store. + *

+ * The algorithm is: + *

+ * If any of the steps fail then the bootstrap bean will fail, except if + * the indexes are marked for full recovery. In this case, the Lucene + * checks are not required as the indexes will be due for a rebuild. + * + * @author Derek Hulley + */ +public class ConfigurationChecker extends AbstractLifecycleBean +{ + private static Log logger = LogFactory.getLog(ConfigurationChecker.class); + + private static final String WARN_RELATIVE_DIR_ROOT = "system.config_check.warn.dir_root"; + private static final String MSG_DIR_ROOT = "system.config_check.msg.dir_root"; + private static final String ERR_DUPLICATE_ROOT_NODE = "system.config_check.err.indexes.duplicate_root_node"; + private static final String ERR_MISSING_INDEXES = "system.config_check.err.missing_index"; + private static final String ERR_MISSING_CONTENT = "system.config_check.err.missing_content"; + private static final String ERR_FIX_DIR_ROOT = "system.config_check.err.fix_dir_root"; + private static final String MSG_HOWTO_INDEX_RECOVER = "system.config_check.msg.howto_index_recover"; + private static final String WARN_STARTING_WITH_ERRORS = "system.config_check.warn.starting_with_errors"; + + private boolean strict; + private RecoveryMode indexRecoveryMode; + private String dirRoot; + private boolean checkAllContent; + + private AuthenticationComponent authenticationComponent; + private DictionaryService dictionaryService; + private NodeService nodeService; + private SearchService searchService; + private ContentService contentService; + + public ConfigurationChecker() + { + this.checkAllContent = false; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(50); + sb.append("ConfigurationChecker") + .append("[indexRecoveryMode=").append(indexRecoveryMode) + .append(", checkAllContent=").append(checkAllContent) + .append("]"); + return sb.toString(); + } + + /** + * This flag controls the behaviour of the component in the event of problems being found. + * Generally, the system should be strict, but this can be changed if indexes are + * going to be recovered, or if missing content is acceptable. + * + * @param strict true to prevent system startup if problems are found, otherwise + * false to allow the system to startup regardless. + */ + public void setStrict(boolean strict) + { + this.strict = strict; + } + + /** + * @param checkAllContent true to get all content URLs when checking for + * missing content, or false to just do a quick sanity check against + * the content store. + */ + public void setCheckAllContent(boolean checkAllContent) + { + this.checkAllContent = checkAllContent; + } + + /** + * Set the index recovery mode. If this is + * {@link org.alfresco.repo.node.index.FullIndexRecoveryComponent.RecoveryMode#VALIDATE FULL} + * then the index checks are ignored as the indexes will be scheduled for a rebuild + * anyway. + * + * @see org.alfresco.repo.node.index.FullIndexRecoveryComponent.RecoveryMode + */ + public void setIndexRecoveryMode(String indexRecoveryMode) + { + this.indexRecoveryMode = RecoveryMode.valueOf(indexRecoveryMode); + } + + public void setDirRoot(String dirRoot) + { + this.dirRoot = dirRoot; + } + + public void setAuthenticationComponent(AuthenticationComponent authenticationComponent) + { + this.authenticationComponent = authenticationComponent; + } + + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // authenticate + try + { + authenticationComponent.setSystemUserAsCurrentUser(); + check(); + } + finally + { + authenticationComponent.clearCurrentSecurityContext(); + } + } + + /** + * Performs the check work. + */ + private void check() + { + if (logger.isDebugEnabled()) + { + logger.debug("Starting bootstrap configuration check: " + this); + } + + // check the dir.root + boolean isRelativeRoot = dirRoot.startsWith("."); + if (isRelativeRoot) + { + String msg = I18NUtil.getMessage(WARN_RELATIVE_DIR_ROOT, dirRoot); + logger.warn(msg); + } + File dirRootFile = new File(dirRoot); + String msgDirRoot = I18NUtil.getMessage(MSG_DIR_ROOT, dirRootFile); + logger.info(msgDirRoot); + + // get all root nodes from the NodeService, i.e. database + List storeRefs = nodeService.getStores(); + List missingIndexStoreRefs = new ArrayList(0); + List missingContentStoreRefs = new ArrayList(0); + for (StoreRef storeRef : storeRefs) + { + NodeRef rootNodeRef = nodeService.getRootNode(storeRef); + if (indexRecoveryMode != RecoveryMode.FULL) + { + if (logger.isDebugEnabled()) + { + logger.debug("Checking index for store: " + storeRef); + } + + // perform a Lucene query for the root node + SearchParameters sp = new SearchParameters(); + sp.addStore(storeRef); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery("ID:" + LuceneQueryParser.escape(rootNodeRef.toString())); + + ResultSet results = null; + int size = 0; + try + { + results = searchService.query(sp); + size = results.length(); + } + finally + { + try { results.close(); } catch (Throwable e) {} + } + + if (size == 0) + { + // indexes missing for root node + missingIndexStoreRefs.add(storeRef); + // debug + if (logger.isDebugEnabled()) + { + logger.debug("Index missing for store: \n" + + " store: " + storeRef); + } + } + else if (size > 1) + { + // there are duplicates + String msg = I18NUtil.getMessage(ERR_DUPLICATE_ROOT_NODE, storeRef); + throw new AlfrescoRuntimeException(msg); + } + } + // select a content property + QName contentPropertyQName = null; + Collection typeQNames = dictionaryService.getAllTypes(); + /* BREAK POINT */ contentPropertyFound: + for (QName typeQName : typeQNames) + { + TypeDefinition classDef = dictionaryService.getType(typeQName); + Map propertyDefs = classDef.getProperties(); + for (PropertyDefinition propertyDef : propertyDefs.values()) + { + if (!propertyDef.getDataType().getName().equals(DataTypeDefinition.CONTENT)) + { + continue; + } + contentPropertyQName = propertyDef.getName(); + break contentPropertyFound; + } + } + // do a search for nodes with content + if (contentPropertyQName != null) + { + String attributeName = "\\@" + LuceneQueryParser.escape(contentPropertyQName.toString()); + + SearchParameters sp = new SearchParameters(); + sp.addStore(storeRef); + sp.setLanguage(SearchService.LANGUAGE_LUCENE); + sp.setQuery(attributeName + ":*"); + if (!checkAllContent) + { + sp.setLimit(1); + sp.setLimitBy(LimitBy.FINAL_SIZE); + } + ResultSet results = null; + try + { + results = searchService.query(sp); + // iterate and attempt to get the content + for (ResultSetRow row : results) + { + NodeRef nodeRef = row.getNodeRef(); + ContentReader reader = contentService.getReader(nodeRef, contentPropertyQName); + if (reader == null) + { + // content not written + continue; + } + else if (reader.exists()) + { + // the data is present in the content store + } + else + { + // URL is missing + missingContentStoreRefs.add(storeRef); + // debug + if (logger.isDebugEnabled()) + { + logger.debug("Content missing from store: \n" + + " store: " + storeRef + "\n" + + " content: " + reader); + } + } + // break out if necessary + if (!checkAllContent) + { + break; + } + } + } + finally + { + try { results.close(); } catch (Throwable e) {} + } + } + } + // check for missing indexes + int missingStoreIndexes = missingIndexStoreRefs.size(); + if (missingStoreIndexes > 0) + { + String msg = I18NUtil.getMessage(ERR_MISSING_INDEXES, missingStoreIndexes); + logger.error(msg); + String msgRecover = I18NUtil.getMessage(MSG_HOWTO_INDEX_RECOVER); + logger.info(msgRecover); + } + // check for missing content + int missingStoreContent = missingContentStoreRefs.size(); + if (missingStoreContent > 0) + { + String msg = I18NUtil.getMessage(ERR_MISSING_CONTENT, missingStoreContent); + logger.error(msg); + } + // handle either content or indexes missing + if (missingStoreIndexes > 0 || missingStoreContent > 0) + { + String msg = I18NUtil.getMessage(ERR_FIX_DIR_ROOT, dirRootFile); + logger.error(msg); + + // Now determine the failure behaviour + if (strict) + { + throw new AlfrescoRuntimeException(msg); + } + else + { + String warn = I18NUtil.getMessage(WARN_STARTING_WITH_ERRORS); + logger.warn(warn); + } + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // nothing here + } +} diff --git a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java index 99cc4e9b8c..20c285d9cd 100644 --- a/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java +++ b/source/java/org/alfresco/repo/content/RoutingContentServiceTest.java @@ -259,6 +259,13 @@ public class RoutingContentServiceTest extends TestCase // check the indexing doesn't spank everthing txn.commit(); txn = null; + + // cleanup + txn = getUserTransaction(); + txn.begin(); + nodeService.deleteNode(contentNodeRef); + txn.commit(); + txn = null; } /** diff --git a/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java b/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java index d3296920de..c043b6b58b 100644 --- a/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java +++ b/source/java/org/alfresco/repo/content/metadata/PdfBoxMetadataExtracter.java @@ -49,16 +49,19 @@ public class PdfBoxMetadataExtracter extends AbstractMetadataExtracter is = reader.getContentInputStream(); // stream the document in pdf = PDDocument.load(is); - // Scoop out the metadata - PDDocumentInformation docInfo = pdf.getDocumentInformation(); - - trimPut(ContentModel.PROP_AUTHOR, docInfo.getAuthor(), destination); - trimPut(ContentModel.PROP_TITLE, docInfo.getTitle(), destination); - trimPut(ContentModel.PROP_DESCRIPTION, docInfo.getSubject(), destination); - - Calendar created = docInfo.getCreationDate(); - if (created != null) - destination.put(ContentModel.PROP_CREATED, created.getTime()); + if (!pdf.isEncrypted()) + { + // Scoop out the metadata + PDDocumentInformation docInfo = pdf.getDocumentInformation(); + + trimPut(ContentModel.PROP_AUTHOR, docInfo.getAuthor(), destination); + trimPut(ContentModel.PROP_TITLE, docInfo.getTitle(), destination); + trimPut(ContentModel.PROP_DESCRIPTION, docInfo.getSubject(), destination); + + Calendar created = docInfo.getCreationDate(); + if (created != null) + destination.put(ContentModel.PROP_CREATED, created.getTime()); + } } finally { diff --git a/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java b/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java index 35c234ef1e..b64aa9f1c0 100644 --- a/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java +++ b/source/java/org/alfresco/repo/dictionary/M2AssociationDefinition.java @@ -221,6 +221,15 @@ import org.alfresco.service.namespace.QName; } + /* (non-Javadoc) + * @see org.alfresco.service.cmr.dictionary.AssociationDefinition#isTargetMandatoryEnforced() + */ + public boolean isTargetMandatoryEnforced() + { + return assoc.isTargetMandatoryEnforced(); + } + + /* (non-Javadoc) * @see org.alfresco.repo.dictionary.AssociationDefinition#isTargetMany() */ diff --git a/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java b/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java index 1d35b8f52a..24dc82b58d 100644 --- a/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java +++ b/source/java/org/alfresco/repo/dictionary/M2ClassAssociation.java @@ -35,6 +35,7 @@ public abstract class M2ClassAssociation private String targetClassName = null; private String targetRoleName = null; private Boolean isTargetMandatory = null; + private Boolean isTargetMandatoryEnforced = null; private Boolean isTargetMany = null; @@ -173,6 +174,18 @@ public abstract class M2ClassAssociation { this.isTargetMandatory = isTargetMandatory; } + + + public boolean isTargetMandatoryEnforced() + { + return isTargetMandatoryEnforced == null ? false : isTargetMandatoryEnforced; + } + + + public void setTargetMandatoryEnforced(boolean isTargetMandatoryEnforced) + { + this.isTargetMandatoryEnforced = isTargetMandatoryEnforced; + } public boolean isTargetMany() diff --git a/source/java/org/alfresco/repo/dictionary/m2binding.xml b/source/java/org/alfresco/repo/dictionary/m2binding.xml index 0c9669abf8..b24e31c3ce 100644 --- a/source/java/org/alfresco/repo/dictionary/m2binding.xml +++ b/source/java/org/alfresco/repo/dictionary/m2binding.xml @@ -157,7 +157,10 @@ - + + + + diff --git a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java index 88ea3cf2ad..865a2950df 100644 --- a/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java +++ b/source/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java @@ -557,7 +557,7 @@ public class SchemaBootstrap extends AbstractLifecycleBean { // it was marked as optional, so we just ignore it String msg = I18NUtil.getMessage(MSG_OPTIONAL_STATEMENT_FAILED, sql, e.getMessage(), file.getAbsolutePath(), line); - logger.warn(msg); + logger.debug(msg); } else { diff --git a/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTagger.java b/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTagger.java index db38bd3db6..ba14d73191 100644 --- a/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTagger.java +++ b/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTagger.java @@ -18,7 +18,9 @@ package org.alfresco.repo.node.integrity; import java.io.Serializable; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -30,14 +32,17 @@ import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.TransactionListenerAdapter; import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -52,12 +57,16 @@ public class IncompleteNodeTagger implements NodeServicePolicies.OnCreateNodePolicy, NodeServicePolicies.OnUpdatePropertiesPolicy, NodeServicePolicies.OnAddAspectPolicy, - NodeServicePolicies.OnRemoveAspectPolicy + NodeServicePolicies.OnRemoveAspectPolicy, + NodeServicePolicies.OnCreateChildAssociationPolicy, + NodeServicePolicies.OnDeleteChildAssociationPolicy, + NodeServicePolicies.OnCreateAssociationPolicy, + NodeServicePolicies.OnDeleteAssociationPolicy { private static Log logger = LogFactory.getLog(IncompleteNodeTagger.class); /** key against which the set of nodes to check is stored in the current transaction */ - private static final String KEY_NODE_SET = "IncompleteNodeTagger.NodeSet"; + private static final String KEY_NODES = "IncompleteNodeTagger.Nodes"; private PolicyComponent policyComponent; private DictionaryService dictionaryService; @@ -118,48 +127,107 @@ public class IncompleteNodeTagger QName.createQName(NamespaceService.ALFRESCO_URI, "onRemoveAspect"), this, new JavaBehaviour(this, "onRemoveAspect")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateChildAssociation"), + this, + new JavaBehaviour(this, "onCreateChildAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteChildAssociation"), + this, + new JavaBehaviour(this, "onDeleteChildAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateAssociation"), + this, + new JavaBehaviour(this, "onCreateAssociation")); + policyComponent.bindAssociationBehaviour( + QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteAssociation"), + this, + new JavaBehaviour(this, "onDeleteAssociation")); } /** - * @return Returns the set of nodes to check, or null if none were registered + * @return Returns the set of nodes to check properties, or null if none were registered */ @SuppressWarnings("unchecked") - private Set getNodeSet() + private Map> getNodes() { - return (Set) AlfrescoTransactionSupport.getResource(KEY_NODE_SET); + return (Map>) AlfrescoTransactionSupport.getResource(KEY_NODES); } - + /** * Ensures that this service is registered with the transaction and saves the node - * reference for use later. + * reference for use (property check) later. * * @param nodeRef */ - private void save(NodeRef nodeRef) + private Set save(NodeRef nodeRef) { + Set assocs = null; + // register this service AlfrescoTransactionSupport.bindListener(this); // get the event list - Set nodeRefs = getNodeSet(); - if (nodeRefs == null) + Map> nodes = getNodes(); + if (nodes == null) { - nodeRefs = new HashSet(31, 0.75F); - AlfrescoTransactionSupport.bindResource(KEY_NODE_SET, nodeRefs); + nodes = new HashMap>(31, 0.75F); + AlfrescoTransactionSupport.bindResource(KEY_NODES, nodes); } // add node to the set - nodeRefs.add(nodeRef); + if (nodes.containsKey(nodeRef)) + { + assocs = nodes.get(nodeRef); + } + else + { + nodes.put(nodeRef, null); + } + // done if (logger.isDebugEnabled()) { - logger.debug("Added node reference to set: " + nodeRef); + logger.debug("Added node reference to property set: " + nodeRef); } + + return assocs; } + /** + * Ensures that this service is registered with the transaction and saves the node + * reference for use (association check) later. + * + * @param nodeRef + * @param assocType + */ + private void saveAssoc(NodeRef nodeRef, QName assocType) + { + // register this service + AlfrescoTransactionSupport.bindListener(this); + + Set assocs = save(nodeRef); + if (assocs == null) + { + assocs = new HashSet(7, 0.75f); + Map> nodes = getNodes(); + nodes.put(nodeRef, assocs); + } + if (assocType != null) + { + assocs.add(assocType); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Added association to node: " + nodeRef + ", " + assocType); + } + } + public void onCreateNode(ChildAssociationRef childAssocRef) { NodeRef nodeRef = childAssocRef.getChildRef(); save(nodeRef); + saveAssoc(nodeRef, null); } public void onUpdateProperties( @@ -202,28 +270,69 @@ public class IncompleteNodeTagger save(nodeRef); } + /** + * @see AssocSourceTypeIntegrityEvent + * @see AssocTargetTypeIntegrityEvent + * @see AssocSourceMultiplicityIntegrityEvent + * @see AssocTargetMultiplicityIntegrityEvent + * @see AssocTargetRoleIntegrityEvent + */ + public void onCreateChildAssociation(ChildAssociationRef childAssocRef) + { + saveAssoc(childAssocRef.getParentRef(), childAssocRef.getTypeQName()); + } + + /** + * @see AssocSourceMultiplicityIntegrityEvent + * @see AssocTargetMultiplicityIntegrityEvent + */ + public void onDeleteChildAssociation(ChildAssociationRef childAssocRef) + { + saveAssoc(childAssocRef.getParentRef(), childAssocRef.getTypeQName()); + } + + /** + * @see AssocSourceTypeIntegrityEvent + * @see AssocTargetTypeIntegrityEvent + * @see AssocSourceMultiplicityIntegrityEvent + * @see AssocTargetMultiplicityIntegrityEvent + */ + public void onCreateAssociation(AssociationRef nodeAssocRef) + { + saveAssoc(nodeAssocRef.getSourceRef(), nodeAssocRef.getTypeQName()); + } + + /** + * @see AssocSourceMultiplicityIntegrityEvent + * @see AssocTargetMultiplicityIntegrityEvent + */ + public void onDeleteAssociation(AssociationRef nodeAssocRef) + { + saveAssoc(nodeAssocRef.getSourceRef(), nodeAssocRef.getTypeQName()); + } + /** * Process all the nodes that require checking within the transaction. */ @Override public void beforeCommit(boolean readOnly) { - Set nodeRefs = getNodeSet(); + Map> nodes = getNodes(); // clear the set out of the transaction // there may be processes that react to the addition/removal of the aspect, // and these will, in turn, lead to further events - AlfrescoTransactionSupport.unbindResource(KEY_NODE_SET); + AlfrescoTransactionSupport.unbindResource(KEY_NODES); // process each node - for (NodeRef nodeRef : nodeRefs) + for (Map.Entry> entry : nodes.entrySet()) { - if (nodeService.exists(nodeRef)) + if (nodeService.exists(entry.getKey())) { - processNode(nodeRef); + processNode(entry.getKey(), entry.getValue()); } } } - private void processNode(NodeRef nodeRef) + private void processNode(NodeRef nodeRef, Set assocTypes) { // ignore the node if the marker aspect is already present boolean isTagged = nodeService.hasAspect(nodeRef, ContentModel.ASPECT_INCOMPLETE); @@ -267,7 +376,46 @@ public class IncompleteNodeTagger return; } } - // all properties passed (both class- and aspect-defined) - remove aspect + + // test associations + if (assocTypes != null) + { + Map assocDefs = typeDef.getAssociations(); + if (assocTypes.size() > 0) + { + // check only those associations that have changed + for (QName assocType : assocTypes) + { + AssociationDefinition assocDef = assocDefs.get(assocType); + if (assocDef != null) + { + if (!checkAssociation(nodeRef, assocDef)) + { + addOrRemoveTag(nodeRef, true, isTagged); + return; + } + } + } + } + else + { + // check all associations (typically for new objects) + for (QName assocType : assocDefs.keySet()) + { + AssociationDefinition assocDef = assocDefs.get(assocType); + if (assocDef != null) + { + if (!checkAssociation(nodeRef, assocDef)) + { + addOrRemoveTag(nodeRef, true, isTagged); + return; + } + } + } + } + } + + // all properties and associations passed (both class- and aspect-defined) - remove aspect addOrRemoveTag(nodeRef, false, isTagged); } @@ -304,6 +452,41 @@ public class IncompleteNodeTagger // all properties were present return true; } + + /** + * @param nodeRef + * @param assocDef + * @return + */ + private boolean checkAssociation(NodeRef nodeRef, AssociationDefinition assocDef) + { + boolean complete = true; + + if (assocDef.isTargetMandatory() && !assocDef.isTargetMandatoryEnforced()) + { + int actualSize = 0; + if (assocDef.isChild()) + { + // check the child assocs present + List childAssocRefs = nodeService.getChildAssocs( + nodeRef, + assocDef.getName(), + RegexQNamePattern.MATCH_ALL); + actualSize = childAssocRefs.size(); + } + else + { + // check the target assocs present + List targetAssocRefs = nodeService.getTargetAssocs(nodeRef, assocDef.getName()); + actualSize = targetAssocRefs.size(); + } + if (assocDef.isTargetMandatory() && actualSize == 0) + { + complete = false; + } + } + return complete; + } /** * Adds or removes the {@link ContentModel#ASPECT_INCOMPLETE incomplete} marker aspect. diff --git a/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTaggerTest.java b/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTaggerTest.java index 40eb92edbf..efca907de0 100644 --- a/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTaggerTest.java +++ b/source/java/org/alfresco/repo/node/integrity/IncompleteNodeTaggerTest.java @@ -118,7 +118,7 @@ public class IncompleteNodeTaggerTest extends TestCase tagger.beforeCommit(false); assertEquals(nodeService.hasAspect(nodeRef, ContentModel.ASPECT_INCOMPLETE), mustBeTagged); } - + public void testCreateWithoutProperties() throws Exception { NodeRef nodeRef = createNode("abc", IntegrityTest.TEST_TYPE_WITH_PROPERTIES, null); @@ -130,4 +130,22 @@ public class IncompleteNodeTaggerTest extends TestCase NodeRef nodeRef = createNode("abc", IntegrityTest.TEST_TYPE_WITH_PROPERTIES, properties); checkTagging(nodeRef, false); } + + public void testCreateWithoutAssoc() throws Exception + { + NodeRef nodeRef = createNode("abc", IntegrityTest.TEST_TYPE_WITH_NON_ENFORCED_CHILD_ASSOCS, properties); + checkTagging(nodeRef, true); + } + + public void testCreateWithAssoc() throws Exception + { + NodeRef nodeRef = createNode("abc", IntegrityTest.TEST_TYPE_WITH_NON_ENFORCED_CHILD_ASSOCS, properties); + nodeService.createNode(nodeRef, + IntegrityTest.TEST_ASSOC_CHILD_NON_ENFORCED, + QName.createQName(IntegrityTest.NAMESPACE, "easyas"), + IntegrityTest.TEST_TYPE_WITHOUT_ANYTHING, + null + ); + checkTagging(nodeRef, false); + } } diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java b/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java index 724393d11d..60e2459326 100644 --- a/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityTest.java @@ -62,12 +62,14 @@ public class IntegrityTest extends TestCase public static final QName TEST_TYPE_WITH_PROPERTIES = QName.createQName(NAMESPACE, "typeWithProperties"); public static final QName TEST_TYPE_WITH_ASSOCS = QName.createQName(NAMESPACE, "typeWithAssocs"); public static final QName TEST_TYPE_WITH_CHILD_ASSOCS = QName.createQName(NAMESPACE, "typeWithChildAssocs"); + public static final QName TEST_TYPE_WITH_NON_ENFORCED_CHILD_ASSOCS = QName.createQName(NAMESPACE, "typeWithNonEnforcedChildAssocs"); public static final QName TEST_ASSOC_NODE_ZEROMANY_ZEROMANY = QName.createQName(NAMESPACE, "assoc-0to* - 0to*"); public static final QName TEST_ASSOC_CHILD_ZEROMANY_ZEROMANY = QName.createQName(NAMESPACE, "child-0to* - 0to*"); public static final QName TEST_ASSOC_NODE_ONE_ONE = QName.createQName(NAMESPACE, "assoc-1to1 - 1to1"); public static final QName TEST_ASSOC_CHILD_ONE_ONE = QName.createQName(NAMESPACE, "child-1to1 - 1to1"); public static final QName TEST_ASSOC_ASPECT_ONE_ONE = QName.createQName(NAMESPACE, "aspect-assoc-1to1 - 1to1"); + public static final QName TEST_ASSOC_CHILD_NON_ENFORCED = QName.createQName(NAMESPACE, "child-non-enforced"); public static final QName TEST_ASPECT_WITH_PROPERTIES = QName.createQName(NAMESPACE, "aspectWithProperties"); public static final QName TEST_ASPECT_WITH_ASSOC = QName.createQName(NAMESPACE, "aspectWithAssoc"); diff --git a/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml b/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml index 87e8630782..c64d330f64 100644 --- a/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml +++ b/source/java/org/alfresco/repo/node/integrity/IntegrityTest_model.xml @@ -106,6 +106,25 @@ + + + Type With Child Assocs + sys:base + + + + false + true + + + test:typeWithoutAnything + true + true + + false + + + diff --git a/source/java/org/alfresco/repo/rule/ruleModel.xml b/source/java/org/alfresco/repo/rule/ruleModel.xml index d41bb8b0d6..cf2eca1f61 100644 --- a/source/java/org/alfresco/repo/rule/ruleModel.xml +++ b/source/java/org/alfresco/repo/rule/ruleModel.xml @@ -45,7 +45,7 @@ act:action - true + true false diff --git a/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java b/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java index e6178bdc83..7804a8b566 100644 --- a/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java +++ b/source/java/org/alfresco/service/cmr/dictionary/AssociationDefinition.java @@ -102,6 +102,13 @@ public interface AssociationDefinition * @return true => cardinality > 0 */ public boolean isTargetMandatory(); + + /** + * Is the target class is mandatory, it is enforced? + * + * @return true => enforced + */ + public boolean isTargetMandatoryEnforced(); /** * Can there be many target class instances in this association?