diff --git a/config/alfresco/ehcache-default.xml b/config/alfresco/ehcache-default.xml index 0ed693e163..d041028986 100644 --- a/config/alfresco/ehcache-default.xml +++ b/config/alfresco/ehcache-default.xml @@ -67,15 +67,6 @@ timeToLiveSeconds="0" overflowToDisk="false" /> - - - - - - ${cache.strategy} ${cache.strategy} ${cache.strategy} - ${cache.strategy} - ${cache.strategy} - ${cache.strategy} ${cache.strategy} ${cache.strategy} @@ -209,7 +206,7 @@ - + diff --git a/config/alfresco/messages/patch-service.properties b/config/alfresco/messages/patch-service.properties index 98957f5d22..d98071a699 100644 --- a/config/alfresco/messages/patch-service.properties +++ b/config/alfresco/messages/patch-service.properties @@ -88,3 +88,11 @@ patch.uifacetsAspectRemovalPatch.updated=Successfully removed the uifacets aspec patch.guestPersonPermission2.description=Change Guest Person permission to visible by all users as 'Consumer'. patch.guestPersonPermission2.result=Updated Guest Person permission to visible by all users as 'Consumer'. + +patch.schemaUpgradeScript.description=Ensures that the database upgrade script has been run. +patch.schemaUpgradeScript.err.not_executed=The schema upgrade script, ''{0}'', has not been run against this database. + +patch.uniqueChildName.description=Checks and renames duplicate children. +patch.uniqueChildName.copyOf=({0}) +patch.uniqueChildName.result=Checked {0} associations and fixed {1} duplicates. See file {2} for details. + diff --git a/config/alfresco/messages/system-messages.properties b/config/alfresco/messages/system-messages.properties index 6dc3a63764..99ce516daf 100644 --- a/config/alfresco/messages/system-messages.properties +++ b/config/alfresco/messages/system-messages.properties @@ -1,3 +1,4 @@ # 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 diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index bb8bb5a94c..0241ae4919 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -343,7 +343,6 @@ - patch.scriptsFolder patch.scriptsFolder.description @@ -362,7 +361,7 @@ - + patch.topLevelGroupParentChildAssociationTypePatch patch.topLevelGroupParentChildAssociationTypePatch.description @@ -440,4 +439,38 @@ + + patch.schemaUpdateScript-V1.4-1 + patch.patch.schemaUpgradeScriptPatch.description + 0 + 19 + 20 + + AlfrescoSchemaUpdate-1.4-1-xxx.sql + + + + patch.uniqueChildName + patch.uniqueChildName.description + 0 + 19 + 20 + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index 19037c2f82..bbe50d57eb 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -373,6 +373,7 @@ org.alfresco.service.cmr.model.FileFolderService.listFiles=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read org.alfresco.service.cmr.model.FileFolderService.listFolders=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read org.alfresco.service.cmr.model.FileFolderService.search=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read + org.alfresco.service.cmr.model.FileFolderService.searchSimple=ACL_NODE.0.sys:base.ReadChildren,AFTER_ACL_NODE.sys:base.Read org.alfresco.service.cmr.model.FileFolderService.rename=ACL_PARENT.0.sys:base.CreateChildren,AFTER_ACL_NODE.sys:base.WriteProperties org.alfresco.service.cmr.model.FileFolderService.move=ACL_NODE.0.sys:base.DeleteNode,ACL_NODE.1.sys:base.CreateChildren org.alfresco.service.cmr.model.FileFolderService.copy=ACL_NODE.0.sys:base.Read,ACL_NODE.1.sys:base.CreateChildren diff --git a/config/alfresco/version.properties b/config/alfresco/version.properties index bc9019c376..4daccb2687 100644 --- a/config/alfresco/version.properties +++ b/config/alfresco/version.properties @@ -19,4 +19,4 @@ version.build=@build-number@ # Schema number -version.schema=19 +version.schema=20 diff --git a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java index 3461fb5cfc..9b59e7db2d 100644 --- a/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java +++ b/source/java/org/alfresco/filesys/ftp/FTPSrvSession.java @@ -137,7 +137,7 @@ public class FTPSrvSession extends SrvSession implements Runnable // Flag to control whether data transfers use a seperate thread - private static boolean UseThreadedDataTransfer = true; + private static boolean UseThreadedDataTransfer = false; // Session socket diff --git a/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java b/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java index 84e0d080db..aa5a889f2d 100644 --- a/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java +++ b/source/java/org/alfresco/filesys/smb/server/repo/CifsHelper.java @@ -31,6 +31,7 @@ import org.alfresco.filesys.server.filesys.FileAttribute; import org.alfresco.filesys.server.filesys.FileExistsException; import org.alfresco.filesys.server.filesys.FileInfo; import org.alfresco.filesys.server.filesys.FileName; +import org.alfresco.filesys.util.WildCard; import org.alfresco.model.ContentModel; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.model.FileFolderService; @@ -402,11 +403,7 @@ public class CifsHelper } /** - * Performs an XPath query to get the first-level descendents matching the given path - * - * @param pathRootNodeRef - * @param pathElement - * @return + * Searches for the node or nodes that match the path element for the given parent node */ private List getDirectDescendents(NodeRef pathRootNodeRef, String pathElement) { @@ -416,18 +413,36 @@ public class CifsHelper " Path Root: " + pathRootNodeRef + "\n" + " Path Element: " + pathElement); } - // escape for the Lucene syntax search - String escapedPathElement = SearchLanguageConversion.convertCifsToLucene(pathElement); - // do the lookup - List childInfos = fileFolderService.search( - pathRootNodeRef, - escapedPathElement, - false); - // convert to noderefs - List results = new ArrayList(childInfos.size()); - for (org.alfresco.service.cmr.model.FileInfo info : childInfos) + List results = null; + // if this contains no wildcards, then we can fasttrack it + if (!WildCard.containsWildcards(pathElement)) { - results.add(info.getNodeRef()); + // a specific name is required + NodeRef foundNodeRef = fileFolderService.searchSimple(pathRootNodeRef, pathElement); + if (foundNodeRef == null) + { + results = Collections.emptyList(); + } + else + { + results = Collections.singletonList(foundNodeRef); + } + } + else + { + // escape for the Lucene syntax search + String escapedPathElement = SearchLanguageConversion.convertCifsToLucene(pathElement); + // do the lookup + List childInfos = fileFolderService.search( + pathRootNodeRef, + escapedPathElement, + false); + // convert to noderefs + results = new ArrayList(childInfos.size()); + for (org.alfresco.service.cmr.model.FileInfo info : childInfos) + { + results.add(info.getNodeRef()); + } } // done return results; diff --git a/source/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java b/source/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java new file mode 100644 index 0000000000..69dfd1e092 --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2005 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.patch.impl; + +import org.alfresco.repo.admin.patch.AbstractPatch; +import org.alfresco.service.cmr.admin.PatchException; + +/** + * This patch ensures that an upgrade script has been executed. Upgrade scripts + * should create an entry for the patch with the required ID and execution status + * so that the code in this class is never called. If called, an exception message + * is always generated. + * + * @author Derek Hulley + */ +public class SchemaUpgradeScriptPatch extends AbstractPatch +{ + private static final String MSG_NOT_EXECUTED = "patch.schemaUpgradeScript.err.not_executed"; + + private String scriptName; + + public SchemaUpgradeScriptPatch() + { + } + + /** + * Set the name of the upgrade script to execute. + * + * @param scriptName the script filename + */ + public void setScriptName(String scriptName) + { + this.scriptName = scriptName; + } + + protected void checkProperties() + { + super.checkProperties(); + checkPropertyNotNull(scriptName, "scriptName"); + } + + /** + * @see #MSG_NOT_EXECUTED + */ + @Override + protected String applyInternal() throws Exception + { + throw new PatchException(MSG_NOT_EXECUTED, scriptName); + } +} diff --git a/source/java/org/alfresco/repo/admin/patch/impl/UniqueChildNamePatch.java b/source/java/org/alfresco/repo/admin/patch/impl/UniqueChildNamePatch.java new file mode 100644 index 0000000000..d2525361cd --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/impl/UniqueChildNamePatch.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2005 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.patch.impl; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Date; +import java.util.List; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.admin.patch.AbstractPatch; +import org.alfresco.repo.domain.ChildAssoc; +import org.alfresco.repo.domain.Node; +import org.alfresco.repo.node.db.NodeDaoService; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.Path; +import org.alfresco.service.namespace.QName; +import org.hibernate.Query; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; + +/** + * Checks that all child node names are unique for the associations that require it. + * + * @author Derek Hulley + */ +public class UniqueChildNamePatch extends AbstractPatch +{ + private static final String MSG_SUCCESS = "patch.uniqueChildName.result"; + private static final String MSG_COPY_OF = "patch.uniqueChildName.copyOf"; + /** the number of associations to process at a time */ + private static final int MAX_RESULTS = 1000; + + private SessionFactory sessionFactory; + private DictionaryService dictionaryService; + private NodeDaoService nodeDaoService; + + public UniqueChildNamePatch() + { + } + + public void setSessionFactory(SessionFactory sessionFactory) + { + this.sessionFactory = sessionFactory; + } + + /** + * @param dictionaryService The service used to sort out the associations + * that require duplicate checks + */ + public void setDictionaryService(DictionaryService dictionaryService) + { + this.dictionaryService = dictionaryService; + } + + /** + * @param nodeDaoService The service that generates the CRC values + */ + public void setNodeDaoService(NodeDaoService nodeDaoService) + { + this.nodeDaoService = nodeDaoService; + } + + @Override + protected void checkProperties() + { + super.checkProperties(); + checkPropertyNotNull(sessionFactory, "sessionFactory"); + checkPropertyNotNull(dictionaryService, "dictionaryService"); + checkPropertyNotNull(nodeDaoService, "nodeDaoService"); + } + + @Override + protected String applyInternal() throws Exception + { + // initialise the helper + HibernateHelper helper = new HibernateHelper(); + helper.setSessionFactory(sessionFactory); + + String msg = helper.assignCrc(); + + // done + return msg; + } + + private class HibernateHelper extends HibernateDaoSupport + { + private File logFile; + private FileChannel channel; + + private HibernateHelper() throws IOException + { + logFile = new File("./UniqueChildNamePatch.log"); + // open the file for appending + RandomAccessFile outputFile = new RandomAccessFile(logFile, "rw"); + channel = outputFile.getChannel(); + // move to the end of the file + channel.position(channel.size()); + // add a newline and it's ready + writeLine("").writeLine(""); + writeLine("UniqueChildNamePatch executing on " + new Date()); + } + + private HibernateHelper write(Object obj) throws IOException + { + channel.write(ByteBuffer.wrap(obj.toString().getBytes())); + return this; + } + private HibernateHelper writeLine(Object obj) throws IOException + { + write(obj); + write("\n"); + return this; + } + + public String assignCrc() throws Exception + { + // get the association types to check + @SuppressWarnings("unused") + List assocTypeQNames = getUsedAssocQNames(); + + int fixed = 0; + int processed = 0; + // check loop through all associations, looking for duplicates + for (QName assocTypeQName : assocTypeQNames) + { + AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName); + if (!(assocDef instanceof ChildAssociationDefinition)) + { + String msg = "WARNING: Non-child association used to link a child node: " + assocTypeQName; + writeLine(msg); + logger.warn(msg); + continue; + } + ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef; + if (childAssocDef.getDuplicateChildNamesAllowed()) + { + continue; + } + write("Checking for name duplicates on association type ").writeLine(assocTypeQName); + + // get all child associations until there are no more results + long lastAssocId = Long.MIN_VALUE; + int lastResultCount = 1; + while(lastResultCount > 0) + { + writeLine(String.format("...Processed %7d associations with %3d duplicates found...", processed, fixed)); + + List results = getAssociations(assocTypeQName, lastAssocId) ; + lastResultCount = results.size(); + for (Object[] objects : results) + { + ChildAssoc childAssoc = (ChildAssoc) objects[0]; + Node childNode = (Node) objects[1]; + NodeRef childNodeRef = childNode.getNodeRef(); + + // get the current name + String childName = (String) nodeService.getProperty(childNodeRef, ContentModel.PROP_NAME); + + lastAssocId = childAssoc.getId(); + String usedChildName = childName; + processed++; + boolean duplicate = false; + while(true) + { + try + { + // push the name back to the node + nodeService.setProperty(childNodeRef, ContentModel.PROP_NAME, usedChildName); + break; // no issues - no duplicate + } + catch (DuplicateChildNodeNameException e) + { + // there was a duplicate, so adjust the name and change the node property + duplicate = true; + // assign a new name + usedChildName = childName + I18NUtil.getMessage(MSG_COPY_OF, processed); + // try again + } + } + // if duplicated, report it + if (duplicate) + { + fixed++; + // get the node path + NodeRef parentNodeRef = childAssoc.getParent().getNodeRef(); + Path path = nodeService.getPath(parentNodeRef); + writeLine(" Changed duplicated child name:"); + writeLine(" Parent: " + parentNodeRef); + writeLine(" Parent path: " + path); + writeLine(" Duplicate name: " + childName); + writeLine(" Replaced with: " + usedChildName); + } + } + } + } + + + // build the result message + String msg = I18NUtil.getMessage(MSG_SUCCESS, processed, fixed, logFile); + return msg; + } + + @SuppressWarnings("unchecked") + private List getUsedAssocQNames() + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session + .createQuery( + "select distinct assoc.typeQName " + + "from org.alfresco.repo.domain.hibernate.ChildAssocImpl as assoc"); + return query.list(); + } + }; + List results = (List) getHibernateTemplate().execute(callback); + return results; + } + + /** + * @return Returns a list of ChildAssoc and String instances + */ + @SuppressWarnings("unchecked") + private List getAssociations(final QName assocTypeQName, final long lastAssocId) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session + .getNamedQuery("node.patch.GetAssocsAndChildNames") + .setLong("lastAssocId", lastAssocId) + .setParameter("assocTypeQName", assocTypeQName) + .setMaxResults(MAX_RESULTS); + return query.list(); + } + }; + List results = (List) getHibernateTemplate().execute(callback); + return results; + } + } +} diff --git a/source/java/org/alfresco/repo/content/filestore/FileIOTest.java b/source/java/org/alfresco/repo/content/filestore/FileIOTest.java index 0d0a4dd6d9..a47a1542f6 100644 --- a/source/java/org/alfresco/repo/content/filestore/FileIOTest.java +++ b/source/java/org/alfresco/repo/content/filestore/FileIOTest.java @@ -22,6 +22,9 @@ import java.io.FileOutputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.CRC32; import junit.framework.TestCase; @@ -111,4 +114,30 @@ public class FileIOTest extends TestCase countRead = channelRead.read(bufferRead); assertEquals("Expected full read", 26, countRead); } + + public void testCrcPerformance() throws Exception + { + long before = System.nanoTime(); + int count = 1000000; + Set results = new HashSet(count); + boolean negatives = false; + for (int i = 0; i < count; i++) + { + CRC32 crc = new CRC32(); + crc.update(Integer.toString(i).getBytes()); + long value = crc.getValue(); + if (value < 0) + { + negatives = true; + } + if (!results.add(value)) + { + System.out.println("Duplicate on " + i); + } + } + long after = System.nanoTime(); + long delta = after - before; + double aveNs = (double)delta / (double)count; + System.out.println(String.format("CRC32: %10.2f ns per item. Negatives=" + negatives, aveNs)); + } } diff --git a/source/java/org/alfresco/repo/domain/ChildAssoc.java b/source/java/org/alfresco/repo/domain/ChildAssoc.java index 72978ec4ea..6a3035670c 100644 --- a/source/java/org/alfresco/repo/domain/ChildAssoc.java +++ b/source/java/org/alfresco/repo/domain/ChildAssoc.java @@ -66,6 +66,28 @@ public interface ChildAssoc extends Comparable */ public void setTypeQName(QName assocTypeQName); + /** + * @return Returns the child node name. This may be truncated, in which case it + * will end with ... + */ + public String getChildNodeName(); + + /** + * @param childNodeName the name of the child node, which may be truncated and + * terminated with ... in order to not exceed 50 characters. + */ + public void setChildNodeName(String childNodeName); + + /** + * @return Returns the crc value for the child node name. + */ + public long getChildNodeNameCrc(); + + /** + * @param crc the crc value + */ + public void setChildNodeNameCrc(long crc); + /** * @return Returns the qualified name of this association */ diff --git a/source/java/org/alfresco/repo/domain/Node.java b/source/java/org/alfresco/repo/domain/Node.java index f988b5554d..da46574599 100644 --- a/source/java/org/alfresco/repo/domain/Node.java +++ b/source/java/org/alfresco/repo/domain/Node.java @@ -56,24 +56,8 @@ public interface Node public void setTypeQName(QName typeQName); -// public NodeStatus getStatus(); -// -// public void setStatus(NodeStatus status); -// public Set getAspects(); - /** - * @return Returns all the regular associations for which this node is a target - */ - public Collection getSourceNodeAssocs(); - - /** - * @return Returns all the regular associations for which this node is a source - */ - public Collection getTargetNodeAssocs(); - - public Collection getChildAssocs(); - public Collection getParentAssocs(); public Map getProperties(); diff --git a/source/java/org/alfresco/repo/domain/NodeAssoc.java b/source/java/org/alfresco/repo/domain/NodeAssoc.java index d4ccaca1f5..d2c93757b3 100644 --- a/source/java/org/alfresco/repo/domain/NodeAssoc.java +++ b/source/java/org/alfresco/repo/domain/NodeAssoc.java @@ -42,12 +42,6 @@ public interface NodeAssoc */ public void buildAssociation(Node sourceNode, Node targetNode); - /** - * Performs the necessary work on the {@link #getSource()() source} and - * {@link #getTarget()() target} nodes to maintain the inverse association sets - */ - public void removeAssociation(); - public AssociationRef getNodeAssocRef(); public Node getSource(); diff --git a/source/java/org/alfresco/repo/domain/hibernate/AppliedPatch.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/AppliedPatch.hbm.xml index 052088eb21..427b826b3c 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/AppliedPatch.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/AppliedPatch.hbm.xml @@ -9,7 +9,7 @@ parentAssocs = contentNode.getParentAssocs(); @@ -362,7 +301,6 @@ public class HibernateNodeTest extends BaseSpringTest Map checkProperties = checkNode.getProperties(); assertTrue("Propery map retrieved was not the same instance", checkProperties == properties); assertTrue("Property not found", checkProperties.containsKey(ContentModel.PROP_NAME)); -// assertTrue("Property value instance retrieved not the same", checkProperties) flushAndClear(); // commit the transaction @@ -380,8 +318,6 @@ public class HibernateNodeTest extends BaseSpringTest assertNotNull(checkNode); checkAspects = checkNode.getAspects(); -// assertTrue("Node retrieved was not same instance", checkNode == node); - txn.commit(); } catch (Throwable e) @@ -465,37 +401,10 @@ public class HibernateNodeTest extends BaseSpringTest // now read the structure back in from the container down containerNodeStatus = (NodeStatus) getSession().get(NodeStatusImpl.class, containerNodeKey); containerNode = containerNodeStatus.getNode(); - Collection assocs = containerNode.getChildAssocs(); - for (ChildAssoc assoc : assocs) - { - Node childNode = assoc.getChild(); - Store store = childNode.getStore(); - childNode.getAspects().size(); - childNode.getProperties().size(); - childNode.getParentAssocs().size(); - childNode.getChildAssocs().size(); - childNode.getSourceNodeAssocs().size(); - childNode.getTargetNodeAssocs().size(); - DbAccessControlList acl = childNode.getAccessControlList(); - if (acl != null) - { - acl.getEntries().size(); - } - } // clear out again getSession().clear(); - // now remove a property from each child - containerNodeStatus = (NodeStatus) getSession().get(NodeStatusImpl.class, containerNodeKey); - containerNode = containerNodeStatus.getNode(); - assocs = containerNode.getChildAssocs(); - for (ChildAssoc assoc : assocs) - { - Node childNode = assoc.getChild(); - PropertyValue removed = childNode.getProperties().remove(ContentModel.PROP_ARCHIVED_BY); - assertNotNull("Property was not present", removed); - } // expect that just the specific property gets removed in the delete statement getSession().flush(); getSession().clear(); 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 72fe0ce91d..79485306d3 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/Node.hbm.xml @@ -11,7 +11,7 @@ - - - - - - - - - - - - - - - - + + table="alf_child_assoc" > - - - - - - - - - + + + + + + + + + + + + + @@ -199,30 +171,32 @@ + table="alf_node_assoc" > - - - - - - - - - + + + + + + + + + + + @@ -232,39 +206,107 @@ org.alfresco.repo.domain.hibernate.StoreImpl as store + + update + org.alfresco.repo.domain.hibernate.ChildAssocImpl assoc + set + assoc.childNodeName = :newName, + assoc.childNodeNameCrc = :newNameCrc + where + assoc.id = :childAssocId + + + + select + assoc + from + org.alfresco.repo.domain.hibernate.ChildAssocImpl as assoc + where + assoc.parent.id = :parentId + order by + assoc.index, + assoc.id + + + + select + assoc + from + org.alfresco.repo.domain.hibernate.ChildAssocImpl as assoc + where + assoc.parent.id = :parentId and + assoc.typeQName = :typeQName and + assoc.childNodeName = :childNodeName and + assoc.childNodeNameCrc = :childNodeNameCrc + order by + assoc.index, + assoc.id + + + + select + assoc.typeQName, + assoc.qname, + assoc.isPrimary, + assoc.index, + child.id, + child.store.key.protocol, + child.store.key.identifier, + child.uuid as parentUuid + from + org.alfresco.repo.domain.hibernate.ChildAssocImpl as assoc + join assoc.parent as parent + join assoc.child as child + where + assoc.parent.id = :parentId + order by + assoc.index, + assoc.id + + select assoc from org.alfresco.repo.domain.hibernate.NodeAssocImpl as assoc where - assoc.source = :sourceNode and - assoc.target = :targetNode and + assoc.source.id = :sourceId and + assoc.target.id = :targetId and assoc.typeQName = :assocTypeQName - + select - target + assoc from org.alfresco.repo.domain.hibernate.NodeAssocImpl as assoc - join assoc.target as target where - assoc.source = :sourceNode and - assoc.typeQName = :assocTypeQName + assoc.source.id = :nodeId or + assoc.target.id = :nodeId - + select - source + assoc from org.alfresco.repo.domain.hibernate.NodeAssocImpl as assoc join assoc.source as source + join assoc.target as target where - assoc.target = :targetNode and - assoc.typeQName = :assocTypeQName + assoc.source.id = :sourceId - + + + select + assoc + from + org.alfresco.repo.domain.hibernate.NodeAssocImpl as assoc + join assoc.source as source + join assoc.target as target + where + assoc.target.id = :targetId + + select distinct status.changeTxnId @@ -333,4 +375,20 @@ node.properties.multiValued = false + + :lastAssocId and + assoc.typeQName = :assocTypeQName + order by + assoc.id + ]]> + + diff --git a/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java b/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java index 0d9bf82d9c..10aab749ee 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/NodeAssocImpl.java @@ -57,17 +57,6 @@ public class NodeAssocImpl implements NodeAssoc, Serializable // add the forward associations this.setTarget(targetNode); this.setSource(sourceNode); - // add the inverse associations - sourceNode.getTargetNodeAssocs().add(this); - targetNode.getSourceNodeAssocs().add(this); - } - - public void removeAssociation() - { - // maintain inverse assoc from source node to this instance - this.getSource().getTargetNodeAssocs().remove(this); - // maintain inverse assoc from target node to this instance - this.getTarget().getSourceNodeAssocs().remove(this); } public AssociationRef getNodeAssocRef() diff --git a/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java b/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java index 1e505c347d..2336570da7 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java +++ b/source/java/org/alfresco/repo/domain/hibernate/NodeImpl.java @@ -29,7 +29,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import org.alfresco.repo.domain.ChildAssoc; import org.alfresco.repo.domain.DbAccessControlList; import org.alfresco.repo.domain.Node; -import org.alfresco.repo.domain.NodeAssoc; import org.alfresco.repo.domain.PropertyValue; import org.alfresco.repo.domain.Store; import org.alfresco.service.cmr.repository.NodeRef; @@ -52,12 +51,8 @@ public class NodeImpl extends LifecycleAdapter implements Node, Serializable private Store store; private String uuid; private QName typeQName; -// private NodeStatus status; private Set aspects; - private Collection sourceNodeAssocs; - private Collection targetNodeAssocs; private Collection parentAssocs; - private Collection childAssocs; private Map properties; private DbAccessControlList accessControlList; @@ -68,10 +63,7 @@ public class NodeImpl extends LifecycleAdapter implements Node, Serializable public NodeImpl() { aspects = new HashSet(5); - sourceNodeAssocs = new HashSet(5); - targetNodeAssocs = new HashSet(5); parentAssocs = new HashSet(5); - childAssocs = new HashSet(11); properties = new HashMap(5); ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); @@ -227,16 +219,6 @@ public class NodeImpl extends LifecycleAdapter implements Node, Serializable this.typeQName = typeQName; } -// public NodeStatus getStatus() -// { -// return status; -// } -// -// public void setStatus(NodeStatus status) -// { -// this.status = status; -// } -// public Set getAspects() { return aspects; @@ -251,34 +233,6 @@ public class NodeImpl extends LifecycleAdapter implements Node, Serializable this.aspects = aspects; } - public Collection getSourceNodeAssocs() - { - return sourceNodeAssocs; - } - - /** - * For Hibernate use - */ - @SuppressWarnings("unused") - private void setSourceNodeAssocs(Collection sourceNodeAssocs) - { - this.sourceNodeAssocs = sourceNodeAssocs; - } - - public Collection getTargetNodeAssocs() - { - return targetNodeAssocs; - } - - /** - * For Hibernate use - */ - @SuppressWarnings("unused") - private void setTargetNodeAssocs(Collection targetNodeAssocs) - { - this.targetNodeAssocs = targetNodeAssocs; - } - public Collection getParentAssocs() { return parentAssocs; @@ -293,20 +247,6 @@ public class NodeImpl extends LifecycleAdapter implements Node, Serializable this.parentAssocs = parentAssocs; } - public Collection getChildAssocs() - { - return childAssocs; - } - - /** - * For Hibernate use - */ - @SuppressWarnings("unused") - private void setChildAssocs(Collection childAssocs) - { - this.childAssocs = childAssocs; - } - public Map getProperties() { return properties; diff --git a/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml b/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml index 8c0490ff4a..c79ac4586d 100644 --- a/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml +++ b/source/java/org/alfresco/repo/domain/hibernate/Permission.hbm.xml @@ -8,7 +8,7 @@ nodeRefs = searchInternal(parentFolderRef, name, true, true, false); - if (nodeRefs.size() == 0) - { - // no match - return; - } - // we found a match, so raise the exception - NodeRef duplicateNodeRef = nodeRefs.get(0); - FileInfo duplicateFileInfo = toFileInfo(duplicateNodeRef); - throw new FileExistsException(duplicateFileInfo); - } - /** * Exception when the type is not a valid File or Folder type * @@ -321,6 +301,20 @@ public class FileFolderServiceImpl implements FileFolderService } return results; } + + public NodeRef searchSimple(NodeRef contextNodeRef, String name) + { + NodeRef childNodeRef = nodeService.getChildByName(contextNodeRef, ContentModel.ASSOC_CONTAINS, name); + if (logger.isDebugEnabled()) + { + logger.debug( + "Simple name search results: \n" + + " parent: " + contextNodeRef + "\n" + + " name: " + name + "\n" + + " result: " + childNodeRef); + } + return childNodeRef; + } /** * @see #search(NodeRef, String, boolean, boolean, boolean) @@ -378,7 +372,7 @@ public class FileFolderServiceImpl implements FileFolderService * file info objects. This allows {@link #checkExists(NodeRef, String)} to * bypass the retrieval of node properties. */ - public List searchInternal( + private List searchInternal( NodeRef contextNodeRef, String namePattern, boolean fileSearch, @@ -547,7 +541,6 @@ public class FileFolderServiceImpl implements FileFolderService } // there is nothing to do if both the name and parent folder haven't changed - boolean checkExists = true; if (targetParentRef.equals(assocRef.getParentRef())) { if (newName.equals(beforeFileInfo.getName())) @@ -563,16 +556,9 @@ public class FileFolderServiceImpl implements FileFolderService } else if (newName.equalsIgnoreCase(beforeFileInfo.getName())) { - checkExists = false; } } - // check for existing file or folder - if (checkExists) - { - checkExists(targetParentRef, newName); - } - QName qname = QName.createQName( NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(newName)); @@ -613,8 +599,15 @@ public class FileFolderServiceImpl implements FileFolderService String currentName = (String)nodeService.getProperty(targetNodeRef, ContentModel.PROP_NAME); if (currentName.equals(newName) == false) { - // changed the name property - nodeService.setProperty(targetNodeRef, ContentModel.PROP_NAME, newName); + try + { + // changed the name property + nodeService.setProperty(targetNodeRef, ContentModel.PROP_NAME, newName); + } + catch (DuplicateChildNodeNameException e) + { + throw new FileExistsException(targetParentRef, newName); + } } // get the details after the operation @@ -658,9 +651,6 @@ public class FileFolderServiceImpl implements FileFolderService throw new AlfrescoRuntimeException("The type is not supported by this service: " + typeQName); } - // check for existing file or folder - checkExists(parentNodeRef, name); - // set up initial properties Map properties = new HashMap(11); properties.put(ContentModel.PROP_NAME, (Serializable) name); @@ -676,12 +666,21 @@ public class FileFolderServiceImpl implements FileFolderService QName qname = QName.createQName( NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(name)); - ChildAssociationRef assocRef = nodeService.createNode( - parentNodeRef, - ContentModel.ASSOC_CONTAINS, - qname, - typeQName, - properties); + ChildAssociationRef assocRef = null; + try + { + assocRef = nodeService.createNode( + parentNodeRef, + ContentModel.ASSOC_CONTAINS, + qname, + typeQName, + properties); + } + catch (DuplicateChildNodeNameException e) + { + throw new FileExistsException(parentNodeRef, name); + } + NodeRef nodeRef = assocRef.getChildRef(); FileInfo fileInfo = toFileInfo(nodeRef); // done @@ -716,31 +715,25 @@ public class FileFolderServiceImpl implements FileFolderService NodeRef currentParentRef = parentNodeRef; // just loop and create if necessary - FileInfo lastFileInfo = null; for (String pathElement : pathElements) { - try + // does it exist? + NodeRef nodeRef = searchSimple(currentParentRef, pathElement); + if (nodeRef == null) { // not present - make it FileInfo createdFileInfo = create(currentParentRef, pathElement, folderTypeQName); currentParentRef = createdFileInfo.getNodeRef(); - lastFileInfo = createdFileInfo; } - catch (FileExistsException e) + else { - // it exists - just get it - List fileInfos = search(currentParentRef, pathElement, false, true, false); - if (fileInfos.size() == 0) - { - // ? It must have been removed - throw new AlfrescoRuntimeException("Path element has just been removed: " + pathElement); - } - currentParentRef = fileInfos.get(0).getNodeRef(); - lastFileInfo = fileInfos.get(0); + // it exists + currentParentRef = nodeRef; } } // done - return lastFileInfo; + FileInfo fileInfo = toFileInfo(currentParentRef); + return fileInfo; } public List getNamePath(NodeRef rootNodeRef, NodeRef nodeRef) throws FileNotFoundException diff --git a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java index 65853b26bb..d42eccd354 100644 --- a/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java +++ b/source/java/org/alfresco/repo/model/filefolder/FileFolderServiceImplTest.java @@ -66,7 +66,8 @@ public class FileFolderServiceImplTest extends TestCase private static final String NAME_L1_FILE_A = "L1- File A"; private static final String NAME_L1_FILE_B = "L1- File B"; private static final String NAME_L1_FILE_C = "L1- File C (%_)"; - private static final String NAME_DUPLICATE = "DUPLICATE"; + private static final String NAME_CHECK_FILE = "CHECK_FILE"; + private static final String NAME_CHECK_FOLDER = "CHECK_FOLDER"; private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); @@ -258,11 +259,11 @@ public class FileFolderServiceImplTest extends TestCase */ public void testGetByName() throws Exception { - FileInfo fileInfo = getByName(NAME_DUPLICATE, true); + FileInfo fileInfo = getByName(NAME_CHECK_FOLDER, true); assertNotNull(fileInfo); assertTrue(fileInfo.isFolder()); - fileInfo = getByName(NAME_DUPLICATE, false); + fileInfo = getByName(NAME_CHECK_FILE, false); assertNotNull(fileInfo); assertFalse(fileInfo.isFolder()); } @@ -308,6 +309,10 @@ public class FileFolderServiceImplTest extends TestCase // make sure that it is an immediate child of the root List checkFileInfos = fileFolderService.search(workingRootNodeRef, NAME_L1_FOLDER_A, false); assertEquals("Folder not moved to root", 1, checkFileInfos.size()); + // rename properly + FileInfo checkFileInfo = fileFolderService.move(folderToMoveRef, null, "new name"); + checkFileInfos = fileFolderService.search(workingRootNodeRef, checkFileInfo.getName(), false); + assertEquals("Folder not renamed in root", 1, checkFileInfos.size()); // attempt illegal rename (existing) try { @@ -318,10 +323,6 @@ public class FileFolderServiceImplTest extends TestCase { // expected } - // rename properly - FileInfo checkFileInfo = fileFolderService.move(folderToMoveRef, null, "new name"); - checkFileInfos = fileFolderService.search(workingRootNodeRef, checkFileInfo.getName(), false); - assertEquals("Folder not renamed in root", 1, checkFileInfos.size()); } public void testCopy() throws Exception @@ -495,6 +496,22 @@ public class FileFolderServiceImplTest extends TestCase } } + public void testSearchSimple() throws Exception + { + FileInfo folderInfo = getByName(NAME_L0_FOLDER_A, true); + assertNotNull(folderInfo); + NodeRef folderNodeRef = folderInfo.getNodeRef(); + // search for a file that is not there + NodeRef phantomNodeRef = fileFolderService.searchSimple(folderNodeRef, "aaaaaaa"); + assertNull("Found non-existent node by name", phantomNodeRef); + // search for a file that is there + NodeRef fileNodeRef = fileFolderService.searchSimple(folderNodeRef, NAME_L1_FILE_A); + assertNotNull("Didn't find file", fileNodeRef); + // double check + FileInfo checkInfo = getByName(NAME_L1_FILE_A, false); + assertEquals("Incorrect node found", checkInfo.getNodeRef(), fileNodeRef); + } + public void testResolveNamePath() throws Exception { FileInfo fileInfo = getByName(NAME_L1_FILE_A, false); @@ -546,4 +563,20 @@ public class FileFolderServiceImplTest extends TestCase String checkContent = reader.getContentString(); assertEquals("Content mismatch", content, checkContent); } + + public void testLongFileNames() throws Exception + { + String fileName = + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890"; + FileInfo fileInfo = fileFolderService.create(workingRootNodeRef, fileName, ContentModel.TYPE_CONTENT); + // see if we can get it again + NodeRef fileNodeRef = fileFolderService.searchSimple(workingRootNodeRef, fileName); + assertNotNull("Long filename not found", fileNodeRef); + assertEquals(fileInfo.getNodeRef(), fileNodeRef); + } } diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java index 4f92dbb370..0a5709f410 100644 --- a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java @@ -32,6 +32,7 @@ import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.dictionary.DictionaryComponent; import org.alfresco.repo.dictionary.DictionaryDAO; import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.repo.domain.hibernate.ChildAssocImpl; import org.alfresco.repo.domain.hibernate.NodeImpl; import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.policy.JavaBehaviour; @@ -47,6 +48,7 @@ import org.alfresco.service.cmr.repository.AssociationRef; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.CyclicChildRelationshipException; +import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -107,6 +109,7 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest public static final QName PROP_QNAME_PROP1 = QName.createQName(NAMESPACE, "prop1"); public static final QName PROP_QNAME_PROP2 = QName.createQName(NAMESPACE, "prop2"); public static final QName ASSOC_TYPE_QNAME_TEST_CHILDREN = ContentModel.ASSOC_CHILDREN; + public static final QName ASSOC_TYPE_QNAME_TEST_CONTAINS = ContentModel.ASSOC_CONTAINS; public static final QName ASSOC_TYPE_QNAME_TEST_NEXT = QName.createQName(NAMESPACE, "next"); public static final QName TYPE_QNAME_TEST_MULTIPLE_TESTER = QName.createQName(NAMESPACE, "multiple-tester"); @@ -730,9 +733,10 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest private int countChildrenOfNode(NodeRef nodeRef) { String query = - "select node.childAssocs" + + "select childAssoc" + " from " + - NodeImpl.class.getName() + " node" + + ChildAssocImpl.class.getName() + " childAssoc" + + " join childAssoc.parent node" + " where node.uuid = ? and node.store.key.protocol = ? and node.store.key.identifier = ?"; Session session = getSession(); List results = session.createQuery(query) @@ -761,17 +765,24 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest public void testAddChild() throws Exception { - NodeRef childNodeRef = nodeService.createNode( + NodeRef childNodeRef1 = nodeService.createNode( rootNodeRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, - QName.createQName("pathA"), + QName.createQName("C1"), ContentModel.TYPE_CONTAINER).getChildRef(); - int countBefore = countChildrenOfNode(rootNodeRef); - assertEquals("Root children count incorrect", 1, countBefore); + int count = countChildrenOfNode(rootNodeRef); + assertEquals("Root children count incorrect", 1, count); + NodeRef childNodeRef2 = nodeService.createNode( + childNodeRef1, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("C2"), + ContentModel.TYPE_CONTAINER).getChildRef(); + count = countChildrenOfNode(rootNodeRef); + assertEquals("Root children count incorrect", 1, count); // associate the two nodes nodeService.addChild( rootNodeRef, - childNodeRef, + childNodeRef2, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("pathB")); // there should now be 2 child assocs on the root @@ -782,7 +793,7 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest try { nodeService.addChild( - childNodeRef, + childNodeRef1, rootNodeRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("backToRoot")); @@ -807,10 +818,6 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest QName.createQName("pathA"), ContentModel.TYPE_CONTAINER); NodeRef childARef = pathARef.getChildRef(); - ChildAssociationRef pathBRef = nodeService.addChild( - parentRef, childARef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("pathB")); - ChildAssociationRef pathCRef = nodeService.addChild( - parentRef, childARef, ASSOC_TYPE_QNAME_TEST_CHILDREN, QName.createQName("pathC")); AssociationRef pathDRef = nodeService.createAssociation( parentRef, childARef, ASSOC_TYPE_QNAME_TEST_NEXT); // remove the child - this must cascade @@ -826,24 +833,6 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest 0, nodeService.getTargetAssocs(parentRef, RegexQNamePattern.MATCH_ALL).size()); } - public void testAddAndRemoveChild() throws Exception - { - ChildAssociationRef pathARef = nodeService.createNode( - rootNodeRef, - ASSOC_TYPE_QNAME_TEST_CHILDREN, - QName.createQName("pathA"), - ContentModel.TYPE_CONTAINER); - NodeRef childRef = pathARef.getChildRef(); - // make a duplication, but non-primary, child associaton - nodeService.addChild( - rootNodeRef, - pathARef.getChildRef(), - pathARef.getTypeQName(), - pathARef.getQName()); - // now remove the association - it will cascade to the child - nodeService.removeChild(rootNodeRef, childRef); - } - public enum TestEnum { TEST_ONE, @@ -1521,4 +1510,137 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest " total nodes: " + totalNodes + "\n" + " total assocs: " + totalAssocs); } + + /** + * Check that the duplicate child name is detected and thrown correctly + */ + public void testDuplicateCatch() throws Exception + { + NodeRef parentRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("parent_child"), + ContentModel.TYPE_CONTAINER).getChildRef(); + ChildAssociationRef pathARef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("pathA"), + ContentModel.TYPE_CONTENT); + // no issue with this + ChildAssociationRef pathBRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("pathB"), + ContentModel.TYPE_CONTENT); + AlfrescoTransactionSupport.flush(); + // there should be no issue with a duplicate association names any more + ChildAssociationRef pathBDuplicateRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("pathB"), + ContentModel.TYPE_CONTENT); + AlfrescoTransactionSupport.flush(); + // Now create nodes with duplicate cm:name properties + Map props = new HashMap(5); + props.put(ContentModel.PROP_NAME, "ABC"); + ChildAssociationRef pathAbcRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("ABC"), + ContentModel.TYPE_CONTENT, + props); + AlfrescoTransactionSupport.flush(); + try + { + // now check that the duplicate is detected with attention to being case-insensitive + props.put(ContentModel.PROP_NAME, "abc"); + ChildAssociationRef pathAbcDuplicateRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("ABC-duplicate"), + ContentModel.TYPE_CONTENT, + props); + fail("Failed to throw duplicate child name exception"); + } + catch (DuplicateChildNodeNameException e) + { + // expected + } + } + + /** + * Checks that the unique constraint doesn't break delete and create within the same + * transaction. + */ + public void testDeleteAndAddSameName() throws Exception + { + NodeRef parentRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("parent_child"), + ContentModel.TYPE_CONTAINER).getChildRef(); + // create node ABC + Map props = new HashMap(5); + props.put(ContentModel.PROP_NAME, "ABC"); + ChildAssociationRef pathAbcRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("ABC"), + ContentModel.TYPE_CONTENT, + props); + NodeRef abcRef = pathAbcRef.getChildRef(); + AlfrescoTransactionSupport.flush(); + // delete ABC + nodeService.deleteNode(abcRef); + // create it again + pathAbcRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("ABC"), + ContentModel.TYPE_CONTENT, + props); + // there should not be any failure when doing this in the same transaction + } + + public void testGetByName() throws Exception + { + NodeRef parentRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName("parent_child"), + ContentModel.TYPE_CONTAINER).getChildRef(); + // create node ABC + Map props = new HashMap(5); + props.put(ContentModel.PROP_NAME, "ABC"); + ChildAssociationRef pathAbcRef = nodeService.createNode( + parentRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("ABC"), + ContentModel.TYPE_CONTENT, + props); + NodeRef abcRef = pathAbcRef.getChildRef(); + // create node DEF + props.put(ContentModel.PROP_NAME, "DEF"); + ChildAssociationRef pathDefRef = nodeService.createNode( + abcRef, + ASSOC_TYPE_QNAME_TEST_CONTAINS, + QName.createQName("DEF"), + ContentModel.TYPE_CONTENT, + props); + NodeRef defRef = pathDefRef.getChildRef(); + + // now browse down using the node service + NodeRef checkParentRef = nodeService.getChildByName(rootNodeRef, ASSOC_TYPE_QNAME_TEST_CHILDREN, parentRef.getId()); + assertNotNull("First level, non-named node not found", checkParentRef); + assertEquals(parentRef, checkParentRef); + NodeRef checkAbcRef = nodeService.getChildByName(checkParentRef, ASSOC_TYPE_QNAME_TEST_CONTAINS, "abc"); + assertNotNull("Second level, named node 'ABC' not found", checkAbcRef); + assertEquals(abcRef, checkAbcRef); + NodeRef checkDefRef = nodeService.getChildByName(checkAbcRef, ASSOC_TYPE_QNAME_TEST_CONTAINS, "def"); + assertNotNull("Third level, named node 'DEF' not found", checkDefRef); + assertEquals(defRef, checkDefRef); + // check that we get null where not present + NodeRef checkHijRef = nodeService.getChildByName(checkAbcRef, ASSOC_TYPE_QNAME_TEST_CONTAINS, "hij"); + assertNull("Third level, named node 'HIJ' should not have been found", checkHijRef); + } } diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java index e2fba6daa8..04cdfacfb8 100644 --- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -41,6 +42,8 @@ import org.alfresco.repo.node.StoreArchiveMap; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.service.cmr.dictionary.AspectDefinition; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; import org.alfresco.service.cmr.dictionary.ClassDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; @@ -63,8 +66,10 @@ import org.alfresco.service.cmr.repository.NodeRef.Status; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QNamePattern; +import org.alfresco.util.ParameterCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.util.Assert; /** @@ -278,29 +283,35 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl String newId = generateGuid(properties); // create the node instance - Node node = nodeDaoService.newNode(store, newId, nodeTypeQName); + Node childNode = nodeDaoService.newNode(store, newId, nodeTypeQName); // get the parent node Node parentNode = getNodeNotNull(parentRef); - // create the association - invoke policy behaviour - ChildAssoc childAssoc = nodeDaoService.newChildAssoc(parentNode, node, true, assocTypeQName, assocQName); - ChildAssociationRef childAssocRef = childAssoc.getChildAssocRef(); - // Set the default property values addDefaultPropertyValues(nodeTypeDef, properties); // Add the default aspects to the node - addDefaultAspects(nodeTypeDef, node, childAssocRef.getChildRef(), properties); + addDefaultAspects(nodeTypeDef, childNode, properties); // set the properties - it is a new node so only set properties if there are any - Map propertiesBefore = getProperties(childAssocRef.getChildRef()); + Map propertiesBefore = getPropertiesImpl(childNode); Map propertiesAfter = null; if (properties.size() > 0) { - propertiesAfter = setPropertiesImpl(childAssocRef.getChildRef(), properties); + propertiesAfter = setPropertiesImpl(childNode, properties); } + // create the association + ChildAssoc childAssoc = nodeDaoService.newChildAssoc( + parentNode, + childNode, + true, + assocTypeQName, + assocQName); + setChildUniqueName(childNode); // ensure uniqueness + ChildAssociationRef childAssocRef = childAssoc.getChildAssocRef(); + // Invoke policy behaviour invokeOnCreateNode(childAssocRef); invokeOnUpdateNode(parentRef); @@ -318,8 +329,10 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl * * @param nodeTypeDef */ - private void addDefaultAspects(ClassDefinition classDefinition, Node node, NodeRef nodeRef, Map properties) + private void addDefaultAspects(ClassDefinition classDefinition, Node node, Map properties) { + NodeRef nodeRef = node.getNodeRef(); + // get the mandatory aspects for the node type List defaultAspectDefs = classDefinition.getDefaultAspects(); @@ -333,7 +346,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl invokeOnAddAspect(nodeRef, defaultAspectDef.getName()); // Now add any default aspects for this aspect - addDefaultAspects(defaultAspectDef, node, nodeRef, properties); + addDefaultAspects(defaultAspectDef, node, properties); } } @@ -432,8 +445,15 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // remove the child assoc from the old parent // don't cascade as we will still need the node afterwards nodeDaoService.deleteChildAssoc(oldAssoc, false); + // create a new assoc - ChildAssoc newAssoc = nodeDaoService.newChildAssoc(newParentNode, nodeToMove, true, assocTypeQName, assocQName); + ChildAssoc newAssoc = nodeDaoService.newChildAssoc( + newParentNode, + nodeToMove, + true, + assocTypeQName, + assocQName); + setChildUniqueName(nodeToMove); // ensure uniqueness ChildAssociationRef newAssocRef = newAssoc.getChildAssocRef(); // If the node is moving stores, then drag the node hierarchy with it @@ -521,8 +541,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl node.setTypeQName(typeQName); // Add the default aspects to the node (update the properties with any new default values) - Map properties = this.getProperties(nodeRef); - addDefaultAspects(nodeTypeDef, node, nodeRef, properties); + Map properties = this.getPropertiesImpl(node); + addDefaultAspects(nodeTypeDef, node, properties); this.setProperties(nodeRef, properties); // Invoke policies @@ -552,7 +572,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl Node node = getNodeNotNull(nodeRef); // attach the properties to the current node properties - Map nodeProperties = getProperties(nodeRef); + Map nodeProperties = getPropertiesImpl(node); if (aspectProperties != null) { @@ -563,7 +583,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl addDefaultPropertyValues(aspectDef, nodeProperties); // Add any dependant aspect - addDefaultAspects(aspectDef, node, nodeRef, nodeProperties); + addDefaultAspects(aspectDef, node, nodeProperties); // Set the property values back on the node setProperties(nodeRef, nodeProperties); @@ -705,21 +725,19 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl invokeBeforeUpdateNode(parentRef); invokeBeforeCreateChildAssociation(parentRef, childRef, assocTypeQName, assocQName); - // check that both nodes belong to the same store - if (!parentRef.getStoreRef().equals(childRef.getStoreRef())) - { - throw new InvalidNodeRefException("Parent and child nodes must belong to the same store: \n" + - " parent: " + parentRef + "\n" + - " child: " + childRef, - childRef); - } - // get the parent node and ensure that it is a container node Node parentNode = getNodeNotNull(parentRef); // get the child node Node childNode = getNodeNotNull(childRef); // make the association - ChildAssoc assoc = nodeDaoService.newChildAssoc(parentNode, childNode, false, assocTypeQName, assocQName); + ChildAssoc assoc = nodeDaoService.newChildAssoc( + parentNode, + childNode, + false, + assocTypeQName, + assocQName); + // ensure name uniqueness + setChildUniqueName(childNode); ChildAssociationRef assocRef = assoc.getChildAssocRef(); NodeRef childNodeRef = assocRef.getChildRef(); @@ -742,7 +760,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // get all the child assocs ChildAssociationRef primaryAssocRef = null; - Collection assocs = parentNode.getChildAssocs(); + Collection assocs = nodeDaoService.getChildAssocs(parentNode); assocs = new HashSet(assocs); // copy set as we will be modifying it for (ChildAssoc assoc : assocs) { @@ -780,6 +798,13 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl public Map getProperties(NodeRef nodeRef) throws InvalidNodeRefException { Node node = getNodeNotNull(nodeRef); + return getPropertiesImpl(node); + } + + private Map getPropertiesImpl(Node node) throws InvalidNodeRefException + { + NodeRef nodeRef = node.getNodeRef(); + Map nodeProperties = node.getProperties(); Map ret = new HashMap(nodeProperties.size()); // copy values @@ -849,12 +874,16 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl */ public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException { - // Invoke policy behaviours + Node node = getNodeNotNull(nodeRef); + + // Invoke policy behaviours invokeBeforeUpdateNode(nodeRef); // Do the set properties - Map propertiesBefore = getProperties(nodeRef); - Map propertiesAfter = setPropertiesImpl(nodeRef, properties); + Map propertiesBefore = getPropertiesImpl(node); + Map propertiesAfter = setPropertiesImpl(node, properties); + + setChildUniqueName(node); // ensure uniqueness // Invoke policy behaviours invokeOnUpdateNode(nodeRef); @@ -865,24 +894,18 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl * Does the work of setting the property values. Returns a map containing the state of the properties after the set * operation is complete. * - * @param nodeRef the node reference + * @param node the node * @param properties the map of property values * @return the map of property values after the set operation is complete * @throws InvalidNodeRefException */ - private Map setPropertiesImpl(NodeRef nodeRef, Map properties) throws InvalidNodeRefException + private Map setPropertiesImpl(Node node, Map properties) throws InvalidNodeRefException { - if (properties == null) - { - throw new IllegalArgumentException("Properties may not be null"); - } + ParameterCheck.mandatory("properties", properties); // remove referencable properties removeReferencableProperties(properties); - // find the node - Node node = getNodeNotNull(nodeRef); - // copy properties onto node Map nodeProperties = node.getProperties(); nodeProperties.clear(); @@ -898,6 +921,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl } // update the node status + NodeRef nodeRef = node.getNodeRef(); nodeDaoService.recordChangeId(nodeRef); // Return the properties after @@ -917,10 +941,18 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // Invoke policy behaviours invokeBeforeUpdateNode(nodeRef); - // Do the set operation - Map propertiesBefore = getProperties(nodeRef); - Map propertiesAfter = setPropertyImpl(nodeRef, qname, value); + // get the node + Node node = getNodeNotNull(nodeRef); + // Do the set operation + Map propertiesBefore = getPropertiesImpl(node); + Map propertiesAfter = setPropertyImpl(node, qname, value); + + if (qname.equals(ContentModel.PROP_NAME)) + { + setChildUniqueName(node); // ensure uniqueness + } + // Invoke policy behaviours invokeOnUpdateNode(nodeRef); invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter); @@ -930,16 +962,15 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl * Does the work of setting a property value. Returns the values of the properties after the set operation is * complete. * - * @param nodeRef the node reference + * @param node the node * @param qname the qname of the property * @param value the value of the property * @return the values of the properties after the set operation is complete * @throws InvalidNodeRefException */ - public Map setPropertyImpl(NodeRef nodeRef, QName qname, Serializable value) throws InvalidNodeRefException + public Map setPropertyImpl(Node node, QName qname, Serializable value) throws InvalidNodeRefException { - // get the node - Node node = getNodeNotNull(nodeRef); + NodeRef nodeRef = node.getNodeRef(); Map properties = node.getProperties(); PropertyDefinition propertyDef = dictionaryService.getProperty(qname); @@ -950,7 +981,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // update the node status nodeDaoService.recordChangeId(nodeRef); - return getProperties(nodeRef); + return getPropertiesImpl(node); } /** @@ -1009,36 +1040,50 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl { Node node = getNodeNotNull(nodeRef); // get the assocs pointing from it - Collection childAssocs = node.getChildAssocs(); + Collection childAssocRefs = nodeDaoService.getChildAssocRefs(node); // shortcut if there are no assocs - if (childAssocs.size() == 0) + if (childAssocRefs.size() == 0) { return Collections.emptyList(); } // sort results - ArrayList orderedList = new ArrayList(childAssocs); + ArrayList orderedList = new ArrayList(childAssocRefs); Collections.sort(orderedList); // list of results - List results = new ArrayList(childAssocs.size()); int nthSibling = 0; - for (ChildAssoc assoc : orderedList) + Iterator iterator = orderedList.iterator(); + while(iterator.hasNext()) { + ChildAssociationRef childAssocRef = iterator.next(); // does the qname match the pattern? - if (!qnamePattern.isMatch(assoc.getQname()) || !typeQNamePattern.isMatch(assoc.getTypeQName())) + if (!qnamePattern.isMatch(childAssocRef.getQName()) || !typeQNamePattern.isMatch(childAssocRef.getTypeQName())) { - // no match - ignore - continue; + // no match - remove + iterator.remove(); + } + else + { + childAssocRef.setNthSibling(nthSibling); + nthSibling++; } - ChildAssociationRef assocRef = assoc.getChildAssocRef(); - // slot the value in the right spot - assocRef.setNthSibling(nthSibling); - nthSibling++; - // get the child - results.add(assoc.getChildAssocRef()); } // done - return results; + return orderedList; + } + + public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName) + { + Node node = getNodeNotNull(nodeRef); + ChildAssoc childAssoc = nodeDaoService.getChildAssoc(node, assocTypeQName, childName); + if (childAssoc != null) + { + return childAssoc.getChild().getNodeRef(); + } + else + { + return null; + } } public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException @@ -1111,11 +1156,10 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl } public List getTargetAssocs(NodeRef sourceRef, QNamePattern qnamePattern) - throws InvalidNodeRefException { Node sourceNode = getNodeNotNull(sourceRef); // get all assocs to target - Collection assocs = sourceNode.getTargetNodeAssocs(); + Collection assocs = nodeDaoService.getTargetNodeAssocs(sourceNode); List nodeAssocRefs = new ArrayList(assocs.size()); for (NodeAssoc assoc : assocs) { @@ -1131,11 +1175,10 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl } public List getSourceAssocs(NodeRef targetRef, QNamePattern qnamePattern) - throws InvalidNodeRefException { - Node sourceNode = getNodeNotNull(targetRef); + Node targetNode = getNodeNotNull(targetRef); // get all assocs to source - Collection assocs = sourceNode.getSourceNodeAssocs(); + Collection assocs = nodeDaoService.getSourceNodeAssocs(targetNode); List nodeAssocRefs = new ArrayList(assocs.size()); for (NodeAssoc assoc : assocs) { @@ -1437,7 +1480,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // add the node to the map nodesById.put(id, node); // recurse into the primary children - Collection childAssocs = node.getChildAssocs(); + Collection childAssocs = nodeDaoService.getChildAssocs(node); for (ChildAssoc childAssoc : childAssocs) { // cascade into primary associations @@ -1464,7 +1507,8 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl List childAssocsToDelete = new ArrayList(5); // child associations ArrayList archivedChildAssocRefs = new ArrayList(5); - for (ChildAssoc assoc : node.getChildAssocs()) + Collection childAssocs = nodeDaoService.getChildAssocs(node); + for (ChildAssoc assoc : childAssocs) { Long relatedNodeId = assoc.getChild().getId(); if (nodesById.containsKey(relatedNodeId)) @@ -1497,7 +1541,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl List nodeAssocsToDelete = new ArrayList(5); // source associations ArrayList archivedSourceAssocRefs = new ArrayList(5); - for (NodeAssoc assoc : node.getSourceNodeAssocs()) + for (NodeAssoc assoc : nodeDaoService.getSourceNodeAssocs(node)) { Long relatedNodeId = assoc.getSource().getId(); if (nodesById.containsKey(relatedNodeId)) @@ -1510,7 +1554,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl } // target associations ArrayList archivedTargetAssocRefs = new ArrayList(5); - for (NodeAssoc assoc : node.getTargetNodeAssocs()) + for (NodeAssoc assoc : nodeDaoService.getTargetNodeAssocs(node)) { Long relatedNodeId = assoc.getTarget().getId(); if (nodesById.containsKey(relatedNodeId)) @@ -1669,10 +1713,21 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl continue; } Node parentNode = getNodeNotNull(parentNodeRef); - nodeDaoService.newChildAssoc(parentNode, node, assocRef.isPrimary(), assocRef.getTypeQName(), assocRef.getQName()); + // get the name to use for the unique child check + QName assocTypeQName = assocRef.getTypeQName(); + nodeDaoService.newChildAssoc( + parentNode, + node, + assocRef.isPrimary(), + assocTypeQName, + assocRef.getQName()); } properties.remove(ContentModel.PROP_ARCHIVED_PARENT_ASSOCS); } + + // make sure that the node name uniqueness is enforced + setChildUniqueName(node); + // restore child associations Collection childAssocRefs = (Collection) getProperty( nodeRef, @@ -1687,7 +1742,16 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl continue; } Node childNode = getNodeNotNull(childNodeRef); - nodeDaoService.newChildAssoc(node, childNode, assocRef.isPrimary(), assocRef.getTypeQName(), assocRef.getQName()); + QName assocTypeQName = assocRef.getTypeQName(); + // get the name to use for the unique child check + nodeDaoService.newChildAssoc( + node, + childNode, + assocRef.isPrimary(), + assocTypeQName, + assocRef.getQName()); + // ensure that the name uniqueness is enforced for the child node + setChildUniqueName(childNode); } properties.remove(ContentModel.PROP_ARCHIVED_CHILD_ASSOCS); } @@ -1730,4 +1794,56 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // remove the aspect node.getAspects().remove(ContentModel.ASPECT_ARCHIVED_ASSOCS); } + + /** + * Checks the dictionary's definition of the association to assign a unique name to the child node. + * + * @param assocTypeQName the type of the child association + * @param childNode the child node being added. The name will be extracted from it, if necessary. + * @return Returns the value to be put on the child association for uniqueness, or null if + */ + private void setChildUniqueName(Node childNode) + { + // get the name property + Map properties = childNode.getProperties(); + PropertyValue nameValue = properties.get(ContentModel.PROP_NAME); + String useName = null; + if (nameValue == null) + { + // no name has been assigned, so assign the ID of the child node + useName = childNode.getUuid(); + } + else + { + useName = (String) nameValue.getValue(DataTypeDefinition.TEXT); + } + // get all the parent assocs + Collection parentAssocs = childNode.getParentAssocs(); + for (ChildAssoc assoc : parentAssocs) + { + QName assocTypeQName = assoc.getTypeQName(); + AssociationDefinition assocDef = dictionaryService.getAssociation(assocTypeQName); + if (!assocDef.isChild()) + { + throw new DataIntegrityViolationException("Child association has non-child type: " + assoc.getId()); + } + ChildAssociationDefinition childAssocDef = (ChildAssociationDefinition) assocDef; + if (childAssocDef.getDuplicateChildNamesAllowed()) + { + // the name is irrelevant, so it doesn't need to be put into the unique key + nodeDaoService.setChildNameUnique(assoc, null); + } + else + { + nodeDaoService.setChildNameUnique(assoc, useName); + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug( + "Unique name set for all " + parentAssocs.size() + " parent associations: \n" + + " name: " + useName); + } + } } diff --git a/source/java/org/alfresco/repo/node/db/NodeDaoService.java b/source/java/org/alfresco/repo/node/db/NodeDaoService.java index 474ba27a81..3fba9af4f9 100644 --- a/source/java/org/alfresco/repo/node/db/NodeDaoService.java +++ b/source/java/org/alfresco/repo/node/db/NodeDaoService.java @@ -16,6 +16,7 @@ */ package org.alfresco.repo.node.db; +import java.util.Collection; import java.util.List; import org.alfresco.repo.domain.ChildAssoc; @@ -24,6 +25,7 @@ import org.alfresco.repo.domain.NodeAssoc; import org.alfresco.repo.domain.NodeStatus; import org.alfresco.repo.domain.Store; import org.alfresco.service.cmr.dictionary.InvalidTypeException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -128,6 +130,30 @@ public interface NodeDaoService QName assocTypeQName, QName qname); + /** + * Change the name of the child node. + * + * @param childAssoc the child association to change + * @param childName the name to put on the association + */ + public void setChildNameUnique(ChildAssoc childAssoc, String childName); + + /** + * Get all child associations for a given node + * + * @param parentNode the parent of the child associations + * @return Returns all child associations for the given node + */ + public Collection getChildAssocs(final Node parentNode); + + /** + * Get a collection of all child association references for a given parent node. + * + * @param parentNode the parent node + * @return Returns a collection of association references + */ + public Collection getChildAssocRefs(Node parentNode); + /** * @return Returns a matching association or null if one was not found * @@ -138,7 +164,12 @@ public interface NodeDaoService Node childNode, QName assocTypeQName, QName qname); - + + /** + * @return Returns an association matching the given parent, type and child name - or null + * if not found + */ + public ChildAssoc getChildAssoc(Node parentNode, QName assocTypeQName, String childName); /** * @param assoc the child association to remove @@ -164,6 +195,11 @@ public interface NodeDaoService Node targetNode, QName assocTypeQName); + /** + * @return Returns a list of all node associations associated with the given node + */ + public List getNodeAssocsToAndFrom(final Node node); + /** * @return Returns the node association or null if not found */ @@ -172,6 +208,16 @@ public interface NodeDaoService Node targetNode, QName assocTypeQName); + /** + * @return Returns all the node associations where the node is the source + */ + public List getTargetNodeAssocs(Node sourceNode); + + /** + * @return Returns all the node associations where the node is the target + */ + public List getSourceNodeAssocs(Node targetNode); + /** * @param assoc the node association to remove */ 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 3c582044ac..6ecccd65d1 100644 --- a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java @@ -19,7 +19,9 @@ package org.alfresco.repo.node.db.hibernate; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.zip.CRC32; +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.domain.ChildAssoc; import org.alfresco.repo.domain.Node; @@ -37,9 +39,11 @@ import org.alfresco.repo.node.db.NodeDaoService; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.repo.transaction.TransactionalDao; import org.alfresco.service.cmr.dictionary.InvalidTypeException; -import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.AssociationExistsException; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.DuplicateChildNodeNameException; import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; import org.hibernate.ObjectDeletedException; @@ -58,6 +62,14 @@ import org.springframework.orm.hibernate3.support.HibernateDaoSupport; public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements NodeDaoService, TransactionalDao { private static final String QUERY_GET_ALL_STORES = "store.GetAllStores"; + private static final String UPDATE_SET_CHILD_ASSOC_NAME = "node.updateChildAssocName"; + private static final String QUERY_GET_CHILD_ASSOCS = "node.GetChildAssocs"; + private static final String QUERY_GET_CHILD_ASSOC_BY_TYPE_AND_NAME = "node.GetChildAssocByTypeAndName"; + private static final String QUERY_GET_CHILD_ASSOC_REFS = "node.GetChildAssocRefs"; + private static final String QUERY_GET_NODE_ASSOC = "node.GetNodeAssoc"; + private static final String QUERY_GET_NODE_ASSOCS_TO_AND_FROM = "node.GetNodeAssocsToAndFrom"; + private static final String QUERY_GET_TARGET_ASSOCS = "node.GetTargetAssocs"; + private static final String QUERY_GET_SOURCE_ASSOCS = "node.GetSourceAssocs"; private static final String QUERY_GET_CONTENT_DATA_STRINGS = "node.GetContentDataStrings"; /** a uuid identifying this unique instance */ @@ -303,25 +315,17 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements deleteChildAssoc(assoc, false); // we don't cascade upwards } // delete all child assocs - Collection childAssocs = node.getChildAssocs(); + Collection childAssocs = getChildAssocs(node); childAssocs = new ArrayList(childAssocs); for (ChildAssoc assoc : childAssocs) { deleteChildAssoc(assoc, cascade); // potentially cascade downwards } - // delete all target assocs - Collection targetAssocs = node.getTargetNodeAssocs(); - targetAssocs = new ArrayList(targetAssocs); - for (NodeAssoc assoc : targetAssocs) + // delete all node associations to and from + List nodeAssocs = getNodeAssocsToAndFrom(node); + for (NodeAssoc assoc : nodeAssocs) { - deleteNodeAssoc(assoc); - } - // delete all source assocs - Collection sourceAssocs = node.getSourceNodeAssocs(); - sourceAssocs = new ArrayList(sourceAssocs); - for (NodeAssoc assoc : sourceAssocs) - { - deleteNodeAssoc(assoc); + getHibernateTemplate().delete(assoc); } // update the node status NodeRef nodeRef = node.getNodeRef(); @@ -330,9 +334,34 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements nodeStatus.setChangeTxnId(AlfrescoTransactionSupport.getTransactionId()); // finally delete the node getHibernateTemplate().delete(node); + // flush to ensure constraints can't be violated + getSession().flush(); // done } - + + private long getCrc(String str) + { + CRC32 crc = new CRC32(); + crc.update(str.getBytes()); + return crc.getValue(); + } + + private static final String TRUNCATED_NAME_INDICATOR = "~~~"; + private String getShortName(String str) + { + int length = str.length(); + if (length <= 50) + { + return str; + } + else + { + StringBuilder ret = new StringBuilder(50); + ret.append(str.substring(0, 47)).append(TRUNCATED_NAME_INDICATOR); + return ret.toString(); + } + } + public ChildAssoc newChildAssoc( Node parentNode, Node childNode, @@ -340,17 +369,170 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements QName assocTypeQName, QName qname) { + /* + * This initial child association creation will fail IFF there is already + * an association of the given type between the two nodes. For new association + * creation, this can only occur if two transactions attempt to create a secondary + * child association between the same two nodes. As this is unlikely, it is + * appropriate to just throw a runtime exception and let the second transaction + * fail. + * + * We don't need to flush the session beforehand as there won't be any deletions + * of the assocation pending. The association deletes, on the other hand, have + * to flush early in order to ensure that the database unique index isn't violated + * if the association is recreated subsequently. + */ + + String uuid = childNode.getUuid(); + ChildAssoc assoc = new ChildAssocImpl(); assoc.setTypeQName(assocTypeQName); - assoc.setIsPrimary(isPrimary); + assoc.setChildNodeName(getShortName(uuid)); + assoc.setChildNodeNameCrc(getCrc(uuid)); assoc.setQname(qname); + assoc.setIsPrimary(isPrimary); assoc.buildAssociation(parentNode, childNode); - // persist - getHibernateTemplate().save(assoc); + // persist it, catching the duplicate child name + try + { + getHibernateTemplate().save(assoc); + } + catch (DataIntegrityViolationException e) + { + throw new AlfrescoRuntimeException("An association already exists between the two nodes: \n" + + " parent: " + parentNode.getId() + "\n" + + " child: " + childNode.getId() + "\n" + + " assoc: " + assocTypeQName, + e); + } // done return assoc; } + public void setChildNameUnique(final ChildAssoc childAssoc, String childName) + { + /* + * As the Hibernate session is rendered useless when an exception is + * bubbled up, we go direct to the database to update the child association. + * This preserves the session and client code can catch the resulting + * exception and react to it whilst in the same transaction. + * + * We ensure that case-insensitivity is maintained by persisting + * the lowercase version of the child node name. + */ + + String childNameNew = null; + if (childName == null) + { + childNameNew = childAssoc.getChild().getUuid(); + } + else + { + childNameNew = childName.toLowerCase(); + } + + final String childNameNewShort = getShortName(childNameNew); + final long childNameNewCrc = getCrc(childNameNew); + + // check if the name has changed + if (childAssoc.getChildNodeNameCrc() == childNameNewCrc) + { + if (childAssoc.getChildNodeName().equals(childNameNewShort)) + { + // nothing changed + return; + } + } + + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + session.flush(); + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.UPDATE_SET_CHILD_ASSOC_NAME) + .setString("newName", childNameNewShort) + .setLong("newNameCrc", childNameNewCrc) + .setLong("childAssocId", childAssoc.getId()); + return (Integer) query.executeUpdate(); + } + }; + try + { + Integer count = (Integer) getHibernateTemplate().execute(callback); + // refresh the entity directly + getHibernateTemplate().refresh(childAssoc); + if (count.intValue() == 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("ChildAssoc not updated: " + childAssoc.getId()); + } + } + } + catch (DataIntegrityViolationException e) + { + NodeRef parentNodeRef = childAssoc.getParent().getNodeRef(); + QName assocTypeQName = childAssoc.getTypeQName(); + throw new DuplicateChildNodeNameException(parentNodeRef, assocTypeQName, childName); + } + } + + @SuppressWarnings("unchecked") + public Collection getChildAssocs(final Node parentNode) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_CHILD_ASSOCS) + .setLong("parentId", parentNode.getId()); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + return queryResults; + } + + @SuppressWarnings("unchecked") + public Collection getChildAssocRefs(final Node parentNode) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_CHILD_ASSOC_REFS) + .setLong("parentId", parentNode.getId()); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + Collection refs = new ArrayList(queryResults.size()); + NodeRef parentNodeRef = parentNode.getNodeRef(); + for (Object[] row : queryResults) + { + String childProtocol = (String) row[5]; + String childIdentifier = (String) row[6]; + String childUuid = (String) row[7]; + NodeRef childNodeRef = new NodeRef(new StoreRef(childProtocol, childIdentifier), childUuid); + QName assocTypeQName = (QName) row[0]; + QName assocQName = (QName) row[1]; + Boolean assocIsPrimary = (Boolean) row[2]; + Integer assocIndex = (Integer) row[3]; + ChildAssociationRef assocRef = new ChildAssociationRef( + assocTypeQName, + parentNodeRef, + assocQName, + childNodeRef, + assocIsPrimary.booleanValue(), + assocIndex.intValue()); + refs.add(assocRef); + } + return refs; + } + public ChildAssoc getChildAssoc( Node parentNode, Node childNode, @@ -363,7 +545,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements qname, childNode.getNodeRef()); // get all the parent's child associations - Collection assocs = parentNode.getChildAssocs(); + Collection assocs = getChildAssocs(parentNode); // hunt down the desired assoc for (ChildAssoc assoc : assocs) { @@ -381,6 +563,28 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements return null; } + public ChildAssoc getChildAssoc(final Node parentNode, final QName assocTypeQName, final String childName) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + String childNameLower = childName.toLowerCase(); + String childNameShort = getShortName(childNameLower); + long childNameLowerCrc = getCrc(childNameLower); + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_CHILD_ASSOC_BY_TYPE_AND_NAME) + .setLong("parentId", parentNode.getId()) + .setParameter("typeQName", assocTypeQName) + .setParameter("childNodeName", childNameShort) + .setLong("childNodeNameCrc", childNameLowerCrc); + return query.uniqueResult(); + } + }; + ChildAssoc childAssoc = (ChildAssoc) getHibernateTemplate().execute(callback); + return childAssoc; + } + /** * Manually enforces cascade deletions down primary associations */ @@ -403,6 +607,10 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements * duplicate call will be received to do this */ } + + // To ensure the validity of the constraint enforcement by the database, + // we have to flush here + getSession().flush(); } public ChildAssoc getPrimaryParentAssoc(Node node) @@ -458,45 +666,101 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements assoc.setTypeQName(assocTypeQName); assoc.buildAssociation(sourceNode, targetNode); // persist - getHibernateTemplate().save(assoc); + try + { + getHibernateTemplate().save(assoc); + } + catch (DataIntegrityViolationException e) + { + throw new AssociationExistsException( + sourceNode.getNodeRef(), + targetNode.getNodeRef(), + assocTypeQName, + e); + } // done return assoc; } + @SuppressWarnings("unchecked") + public List getNodeAssocsToAndFrom(final Node node) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_NODE_ASSOCS_TO_AND_FROM) + .setLong("nodeId", node.getId()); + return query.list(); + } + }; + List results = (List) getHibernateTemplate().execute(callback); + return results; + } + public NodeAssoc getNodeAssoc( final Node sourceNode, final Node targetNode, final QName assocTypeQName) { - AssociationRef nodeAssocRef = new AssociationRef( - sourceNode.getNodeRef(), - assocTypeQName, - targetNode.getNodeRef()); - // get all the source's target associations - Collection assocs = sourceNode.getTargetNodeAssocs(); - // hunt down the desired assoc - for (NodeAssoc assoc : assocs) + HibernateCallback callback = new HibernateCallback() { - // is it a match? - if (!assoc.getNodeAssocRef().equals(nodeAssocRef)) // not a match + public Object doInHibernate(Session session) { - continue; + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_NODE_ASSOC) + .setLong("sourceId", sourceNode.getId()) + .setLong("targetId", targetNode.getId()) + .setParameter("assocTypeQName", assocTypeQName); + return query.uniqueResult(); } - else + }; + NodeAssoc result = (NodeAssoc) getHibernateTemplate().execute(callback); + return result; + } + + @SuppressWarnings("unchecked") + public List getTargetNodeAssocs(final Node sourceNode) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) { - return assoc; + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_TARGET_ASSOCS) + .setLong("sourceId", sourceNode.getId()); + return query.list(); } - } - // not found - return null; + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + return queryResults; + } + + @SuppressWarnings("unchecked") + public List getSourceNodeAssocs(final Node targetNode) + { + HibernateCallback callback = new HibernateCallback() + { + public Object doInHibernate(Session session) + { + Query query = session + .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_SOURCE_ASSOCS) + .setLong("targetId", targetNode.getId()); + return query.list(); + } + }; + List queryResults = (List) getHibernateTemplate().execute(callback); + return queryResults; } public void deleteNodeAssoc(NodeAssoc assoc) { - // maintain inverse association sets - assoc.removeAssociation(); - // remove instance + // Remove instance getHibernateTemplate().delete(assoc); + // Flush to ensure that the database constraints aren't violated if the assoc + // is recreated in the transaction + getSession().flush(); } @SuppressWarnings("unchecked") @@ -513,24 +777,4 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements List queryResults = (List) getHibernateTemplate().execute(callback); return queryResults; } -} - - - - - - - - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java b/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java index 8661e22e38..a5b66172d5 100644 --- a/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java +++ b/source/java/org/alfresco/repo/node/integrity/AssocTargetRoleIntegrityEvent.java @@ -21,14 +21,10 @@ import java.util.List; import org.alfresco.service.cmr.dictionary.AssociationDefinition; import org.alfresco.service.cmr.dictionary.ChildAssociationDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.InvalidNodeRefException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** * Event to check the association target role name @@ -37,8 +33,6 @@ import org.apache.commons.logging.LogFactory; */ public class AssocTargetRoleIntegrityEvent extends AbstractIntegrityEvent { - private static Log logger = LogFactory.getLog(AssocTargetRoleIntegrityEvent.class); - public AssocTargetRoleIntegrityEvent( NodeService nodeService, DictionaryService dictionaryService, @@ -51,7 +45,6 @@ public class AssocTargetRoleIntegrityEvent extends AbstractIntegrityEvent public void checkIntegrity(List eventResults) { - NodeRef sourceNodeRef = getNodeRef(); QName assocTypeQName = getTypeQName(); QName assocQName = getQName(); @@ -80,7 +73,6 @@ public class AssocTargetRoleIntegrityEvent extends AbstractIntegrityEvent // perform required checks checkAssocQNameRegex(eventResults, childAssocDef, assocQName); - checkAssocQNameDuplicate(eventResults, childAssocDef, sourceNodeRef, assocQName); } /** @@ -108,39 +100,4 @@ public class AssocTargetRoleIntegrityEvent extends AbstractIntegrityEvent } } } - - /** - * Checks that the association name matches the constraints imposed by the model. - */ - protected void checkAssocQNameDuplicate( - List eventResults, - ChildAssociationDefinition assocDef, - NodeRef sourceNodeRef, - QName assocQName) - { - if (assocDef.getDuplicateChildNamesAllowed()) - { - // nothing to do - return; - } - QName assocTypeQName = assocDef.getName(); - // see if there is another association with the same name - try - { - List childAssocs = nodeService.getChildAssocs(sourceNodeRef, assocTypeQName, assocQName); - // duplicates not allowed - if (childAssocs.size() > 1) - { - IntegrityRecord result = new IntegrityRecord( - "Duplicate child associations are not allowed: \n" + - " Association: " + assocDef + "\n" + - " Name: " + assocQName); - eventResults.add(result); - } - } - catch (InvalidNodeRefException e) - { - // node has gone - } - } } diff --git a/source/java/org/alfresco/repo/version/NodeServiceImpl.java b/source/java/org/alfresco/repo/version/NodeServiceImpl.java index 62b50a957a..0de161ca00 100644 --- a/source/java/org/alfresco/repo/version/NodeServiceImpl.java +++ b/source/java/org/alfresco/repo/version/NodeServiceImpl.java @@ -442,6 +442,15 @@ public class NodeServiceImpl implements NodeService, VersionModel return result; } + /** + * @throws UnsupportedOperationException always + */ + public NodeRef getChildByName(NodeRef nodeRef, QName assocTypeQName, String childName) + { + // This operation is not supported for a verion store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + /** * Simulates the node begin attached ot the root node of the version store. */ diff --git a/source/java/org/alfresco/service/cmr/model/FileExistsException.java b/source/java/org/alfresco/service/cmr/model/FileExistsException.java index 23310a9f8f..1dfa85fab4 100644 --- a/source/java/org/alfresco/service/cmr/model/FileExistsException.java +++ b/source/java/org/alfresco/service/cmr/model/FileExistsException.java @@ -16,29 +16,37 @@ */ package org.alfresco.service.cmr.model; +import org.alfresco.service.cmr.repository.NodeRef; + /** * Common, checked exception thrown when an operation fails because * of a name clash. * * @author Derek Hulley */ -public class FileExistsException extends Exception +public class FileExistsException extends RuntimeException { private static final long serialVersionUID = -4133713912784624118L; - private FileInfo existing; + private NodeRef parentNodeRef; + private String name; - public FileExistsException(FileInfo existing) + public FileExistsException(NodeRef parentNodeRef, String name) { - super("" + - (existing.isFolder() ? "Folder " : "File ") + - existing.getName() + + super("Existing file or folder " + + name + " already exists"); - this.existing = existing; + this.parentNodeRef = parentNodeRef; + this.name = name; } - public FileInfo getExisting() + public NodeRef getParentNodeRef() { - return existing; + return parentNodeRef; + } + + public String getName() + { + return name; } } diff --git a/source/java/org/alfresco/service/cmr/model/FileFolderService.java b/source/java/org/alfresco/service/cmr/model/FileFolderService.java index 259e756622..2b939f4fe2 100644 --- a/source/java/org/alfresco/service/cmr/model/FileFolderService.java +++ b/source/java/org/alfresco/service/cmr/model/FileFolderService.java @@ -61,6 +61,16 @@ public interface FileFolderService @Auditable(key = Auditable.Key.ARG_0, parameters = {"contextNodeRef"}) public List listFolders(NodeRef contextNodeRef); + /** + * Get a simple list of nodes that have the given name within the parent node + * + * @param contextNodeRef the parent node + * @param name the name of the node to search for + * @return Returns the node that has the given name - or null if not found + */ + @Auditable(key = Auditable.Key.ARG_0, parameters = {"contextNodeRef", "name"}) + public NodeRef searchSimple(NodeRef contextNodeRef, String name); + /** * Searches for all files and folders with the matching name pattern, * using wildcard characters * and ?. diff --git a/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java b/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java index 9ccd21d69f..003e253a29 100644 --- a/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java +++ b/source/java/org/alfresco/service/cmr/repository/AssociationExistsException.java @@ -33,9 +33,7 @@ public class AssociationExistsException extends RuntimeException private QName qname; /** - * @param sourceRef the source of the association - * @param targetRef the target of the association - * @param qname the qualified name of the association + * @see #AssociationExistsException(NodeRef, NodeRef, QName, Throwable) */ public AssociationExistsException(NodeRef sourceRef, NodeRef targetRef, QName qname) { @@ -45,6 +43,20 @@ public class AssociationExistsException extends RuntimeException this.qname = qname; } + /** + * @param sourceRef the source of the association + * @param targetRef the target of the association + * @param qname the qualified name of the association + * @param cause a causal exception + */ + public AssociationExistsException(NodeRef sourceRef, NodeRef targetRef, QName qname, Throwable cause) + { + super(cause); + this.sourceRef = sourceRef; + this.targetRef = targetRef; + this.qname = qname; + } + public NodeRef getSourceRef() { return sourceRef; diff --git a/source/java/org/alfresco/service/cmr/repository/DuplicateChildNodeNameException.java b/source/java/org/alfresco/service/cmr/repository/DuplicateChildNodeNameException.java new file mode 100644 index 0000000000..405c42a434 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/repository/DuplicateChildNodeNameException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2005 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.service.cmr.repository; + +import org.alfresco.i18n.I18NUtil; +import org.alfresco.service.namespace.QName; + + +/** + * Thrown when a child node cm:name property violates the data dictionary + * duplicate child association constraint. + * + * @author Derek Hulley + */ +public class DuplicateChildNodeNameException extends RuntimeException +{ + private static final long serialVersionUID = 5143099335847200453L; + + private static final String ERR_DUPLICATE_NAME = "system.err.duplicate_name"; + + private NodeRef parentNodeRef; + private QName assocTypeQName; + private String name; + + public DuplicateChildNodeNameException(NodeRef parentNodeRef, QName assocTypeQName, String name) + { + super(I18NUtil.getMessage(ERR_DUPLICATE_NAME, name)); + this.parentNodeRef = parentNodeRef; + this.assocTypeQName = assocTypeQName; + this.name = name; + } + + public NodeRef getParentNodeRef() + { + return parentNodeRef; + } + + public QName getAssocTypeQName() + { + return assocTypeQName; + } + + public String getName() + { + return name; + } +} diff --git a/source/java/org/alfresco/service/cmr/repository/NodeService.java b/source/java/org/alfresco/service/cmr/repository/NodeService.java index 88e178ed64..96b192cf52 100644 --- a/source/java/org/alfresco/service/cmr/repository/NodeService.java +++ b/source/java/org/alfresco/service/cmr/repository/NodeService.java @@ -417,6 +417,21 @@ public interface NodeService QNamePattern qnamePattern) throws InvalidNodeRefException; + /** + * Get the node with the given name within the context of the parent node. The name + * is case-insensitive as Alfresco has to support case-insensitive clients as standard. + * + * @param nodeRef the parent node - usuall a container + * @param assocTypeQName the type of the association + * @param childName the name of the node as per the property cm:name + * @return Returns the child node or null if not found + */ + @Auditable(key = Auditable.Key.ARG_0 ,parameters = {"nodeRef", "assocTypeQName", "childName"}) + public NodeRef getChildByName( + NodeRef nodeRef, + QName assocTypeQName, + String childName); + /** * Fetches the primary parent-child relationship. *

diff --git a/source/test-resources/filefolder/filefolder-test-import.xml b/source/test-resources/filefolder/filefolder-test-import.xml index 9d7609596f..90e2430380 100644 --- a/source/test-resources/filefolder/filefolder-test-import.xml +++ b/source/test-resources/filefolder/filefolder-test-import.xml @@ -43,15 +43,15 @@ L0- Folder C - + - DUPLICATE + CHECK_FILE contentUrl=classpath:quick/quick.txt|mimetype=text/plain|size=|encoding= - - - DUPLICATE - + + + CHECK_FOLDER +