cancel
Showing results for 
Search instead for 
Did you mean: 

Bi-Directional Associations

stevewickii
Champ in-the-making
Champ in-the-making
I am using Alfresco 2.9.0B build 683.
I need to create a bidirectional association between a cmSmiley Tongueroduct and a cmSmiley Tongueest.  All my attempts have failed.  Please help.

I followed these directions: http://wiki.alfresco.com/wiki/Data_Dictionary_Guide#Non-Child_Associations

customModel.xml
<?xml version="1.0" encoding="UTF-8"?>

<!– Custom Model –>

<!– Note: This model is pre-configured to load at startup of the Repository.  So, all custom –>
<!–       types and aspects added here will automatically be registered –>

<model name="custom:customModel" xmlns="http://www.alfresco.org/model/dictionary/1.0">

   <!– Optional meta-data about the model –>  
   <description>Custom Model</description>
   <author>Stephen M. Wick</author>
   <version>1.0</version>

   <imports>
      <!– Import Alfresco Dictionary Definitions –>
      <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
      <!– Import Alfresco Content Domain Model Definitions –>
      <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
   </imports>

   <!– Introduction of new namespaces defined by this model –>
   <!– NOTE: The following namespace custom.model should be changed to reflect your own namespace –>
   <namespaces>
      <namespace uri="custom.model" prefix="custom"/>
   </namespaces>

   <types>
      <type name="cm:product">
         <title>Product</title>
         <parent>cm:content</parent>
         <associations>
            <association name="cm:crops">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:crop</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
            <association name="cm:pests">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:pest</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
         </associations>
      </type>
      <type name="cm:crop">
         <title>Crop</title>
         <parent>cm:content</parent>
         <associations>
            <association name="cm:products">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:product</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
            <association name="cm:pests">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:pest</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
         </associations>
      </type>
      <type name="cm:pest">
         <title>Pest</title>
         <parent>cm:content</parent>
         <associations>
            <association name="cm:products">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:product</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
            <association name="cm:crops">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:crop</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
         </associations>
      </type>
   </types>
</model>

I get this error: org.alfresco.service.cmr.dictionary.DictionaryException: Found duplicate association definition cmSmiley Tongueests within class cm:crop and class cmSmiley Tongueroduct

This uni-directional model works great…  I just can't see which cmSmiley Tongueroduct(s) are associated with my cm:crop form the Details screen for the cm:crop.

unidirectional-customModel.xml
<?xml version="1.0" encoding="UTF-8"?>

<!– Custom Model –>

<!– Note: This model is pre-configured to load at startup of the Repository.  So, all custom –>
<!–       types and aspects added here will automatically be registered –>

<model name="custom:customModel" xmlns="http://www.alfresco.org/model/dictionary/1.0">

   <!– Optional meta-data about the model –>  
   <description>Custom Model</description>
   <author>Stephen M. Wick</author>
   <version>1.0</version>

   <imports>
      <!– Import Alfresco Dictionary Definitions –>
      <import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
      <!– Import Alfresco Content Domain Model Definitions –>
      <import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
   </imports>

   <!– Introduction of new namespaces defined by this model –>
   <!– NOTE: The following namespace custom.model should be changed to reflect your own namespace –>
   <namespaces>
      <namespace uri="custom.model" prefix="custom"/>
   </namespaces>

   <types>
      <type name="cm:product">
         <title>Product</title>
         <parent>cm:content</parent>
         <associations>
            <association name="cm:crops">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:crop</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
            <association name="cm:pests">
               <source>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </source>
               <target>
                  <class>cm:pest</class>
                  <mandatory>false</mandatory>
                  <many>true</many>
               </target>
            </association>
         </associations>
      </type>
      <type name="cm:crop">
         <title>Crop</title>
         <parent>cm:content</parent>
      </type>
      <type name="cm:pest">
         <title>Pest</title>
         <parent>cm:content</parent>
      </type>
   </types>
</model>

Keywords: bidirectional two-way two way association relationship node
12 REPLIES 12

stevewickii
Champ in-the-making
Champ in-the-making
Well, I have tried a bunch of stuff without success.

I tried adding <role> tags for the <source> and <target>.  Still the fsSmiley Tongueest and fs:crop nodes do not have references to the fsSmiley Tongueroduct.

I tried defining a common base content-type for fsSmiley Tongueroduct, fs:crop and fsSmiley Tongueest named fs:related, which defines the association fs:crops, fsSmiley Tongueests and fsSmiley Tongueroducts.  This got around the duplicate name violation, but still didn't solve my problem.  Adding an fs:crop to an fsSmiley Tongueroduct's fs:crops property does not add the fsSmiley Tongueroduct to the fs:crop's fsSmiley Tongueroducts property.

alr
Champ in-the-making
Champ in-the-making
Hi Steve

I have stumbled across the same problem and could not get it to work either. Interestingly, when taking a quick look at the org.alfresco.repo.domain.NodeAssoc interface, you can read in the first comment:


* Represents a generic association between two nodes.  The association is named
* and bidirectional by default.

But thats all about explanations - also, trying to show the association in the web-client-config-custom.xml file for the target only results in an error in the alfresco log (the below written association is only defined in cns:text)


   <config evaluator="node-type" condition="cns:text">
      <property-sheet>
         <show-association name="cns:textTemplateAssoc" />
      </property-sheet>
   </config>
   
   <config evaluator="node-type" condition="cns:textTemplate">
      <property-sheet>
         <show-association name="cns:textTemplateAssoc" />
      </property-sheet>
   </config>


09:33:13,450 User:admin WARN  [component.property.UIAssociation] Failed to find association definition for association 'cns:textTemplateAssoc'

Doesn't really help, but shows my tries up until now. Good luck for more work!

stevewickii
Champ in-the-making
Champ in-the-making
Thanks alr.

I reviewed about 60 Alfresco JIRA tickets, and found the following, which seem to be related to this problem.

https://issues.alfresco.com/jira/browse/ALFCOM-588
https://issues.alfresco.com/jira/browse/ALFCOM-1336
https://issues.alfresco.com/jira/browse/ENH-28

I voted on all of these tickets and added them to my watch list.  Please do the same if you are interested in getting this issue fixed.

Ticket ALFCOM-588 makes it sound like it is already possible to use the NodeService class to get the Source node from the Target node given an association ref.  A quick look at NodeServiceImpl.java (Alfresco 2.9.0B build 683) reveals that this functionality is not available.

NodeServiceImpl.java
    /**
     * @throws UnsupportedOperationException always
     */
    public List<AssociationRef> getSourceAssocs(NodeRef sourceRef, QNamePattern qnamePattern)
    {
        // This operation is not supported for a version store
        throw new UnsupportedOperationException(MSG_UNSUPPORTED);
    }

It looks like peer-associations are unidirectional as of Alfresco 2.9.0B build 683.

stevewickii
Champ in-the-making
Champ in-the-making
Here's one option to work around the problem.  This is by no means a fix.

When creating Presentation Templates or Scripts/Web Scripts, use the companyhome.childrenByLuceneSearch[] feature or search.luceneSearch() respectively, to search for the Source node(s) by content type and association name.

Examples

Given that content types fsSmiley Tongueroduct and fs:crop extend cm:content, and fsSmiley Tongueroduct defines an association to fs:crop named "fs:crops"…

Here is a FreeMarker Presentation Template for fs:crop nodes (Target node) named crop_info.ftl, which displays the name of the crop and lists out a link to each associated fsSmiley Tongueroduct (Source node).

crop_info.ftl
<#– Shows some Crop info about the current document –>
<#if document?exists>
   <h4>Crop Info:</h4>
   <table>
   <tr><td><b>Name:</b></td><td>${document.name}</td></tr>
   <tr><td><b>Products:</b></td><td>
   <table>
      <#list companyhome.childrenByLuceneSearch["+TYPE:\"fs:product\""] as product>
         <#list product.assocs["fs:crops"] as crop>
         <#if crop.name == document.name>
         <tr><td><a href="/alfresco/n/showDocDetails/workspace/SpacesStore/${product.id}">${product.name}</a></td></tr>
         </#if>
      </#list>
      </#list>
   </table>
   </td></tr>
    </table>
<#else>
   No document found!
</#if>

stevewickii
Champ in-the-making
Champ in-the-making
I checked out the Alfresco project from subversion, and verified that NodeServiceImpl.getSourceAssocs() is not implemented to return source associations (it is implemented to throw an UnsupportedOperationException).  That means that as of Alfresco Labs 3b, bidirectional associations are still unsupported.

alr
Champ in-the-making
Champ in-the-making
Hey Steve,

your fix still implies, that one adds the bi-directional relationship manually. This is not really what one would want. The ideal case would be some sort of hook, which triggers, when an object from my domainModel gets edited and automatically the second association is created. I've peered a little bit around in the source today, but haven't found anything what would do somethink like this - but perhaps I am just blind.

Help is always highly appreciated. Smiley Happy


Regards,
  Alexander

alr
Champ in-the-making
Champ in-the-making
Hey,

actually the solution is pretty simple.


public class RuleTriggerBidirectionalAssoc extends RuleTriggerAbstractBase
   implements NodeServicePolicies.OnDeleteAssociationPolicy, NodeServicePolicies.OnCreateAssociationPolicy {

   public void onDeleteAssociation(AssociationRef nodeAssocRef) {
         nodeService.removeAssociation(nodeAssocRef.getTargetRef(), nodeAssocRef.getSourceRef(),
               getReverseQnameAssociationName(nodeAssocRef.getTypeQName()));
   }

   public void onCreateAssociation(AssociationRef nodeAssocRef) {
         nodeService.createAssociation(nodeAssocRef.getTargetRef(), nodeAssocRef.getSourceRef(),
               getReverseQnameAssociationName(nodeAssocRef.getTypeQName()));
   }

   public void registerRuleTrigger() {
      this.policyComponent.bindAssociationBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onCreateAssociation"), this,
            new JavaBehaviour(this, "onCreateAssociation"));
      
      this.policyComponent.bindAssociationBehaviour(QName.createQName(NamespaceService.ALFRESCO_URI, "onDeleteAssociation"), this,
            new JavaBehaviour(this, "onDeleteAssociation"));
      
   }

}

The logic for the getReverseQnameAssociationName() method needs to pick out the right reverse association. All you need afterwards is a little spring configuration addition in some bidrectional-assoc-context.xml file, which is also put in the jar.
For performance issues you should check, that you only do update associations which are covered in your own model for example - but those checks can be added quite quickly.


<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
   <bean id="on-association-change-trigger" class="org.test.alfresco.repo.rule.trigger.RuleTriggerBidirectionalAssoc"
      parent="rule-trigger-base" />
</beans>

And that's it basically. I only stumbled upon triggers by incident, I have to admit. 🙂

stevewickii
Champ in-the-making
Champ in-the-making
Great info Alexander.  I can't try it right now, because I'm working on another issue, but I'll let you know how it works on my system as soon as I am able.

Thanks!

adstrim
Champ in-the-making
Champ in-the-making
I applied your code and it worked fine. Thanks.
To make it generic and bidirectional I use QName like this sample:A2B and sample:B2A and I implement getReverseQnameAssociationName


    public QName getReverseQnameAssociationName(QName qName) {
        String reverseLocalName = new StringBuffer(qName.getLocalName()).reverse().toString();
        QName reverseQName = QName.createQName(qName.getNamespaceURI(), reverseLocalName);
        return reverseQName;
    }

However there is nothing to impose the one to one constraint. Meaning another instance of A can associate itself to b and now there is no reverse association for A2B. (Two A are associated to B and B points only to one of them) unless you make the other relationship readonly

        <property-sheet>
            <show-association name="sample:s2c" read-only="true" />
        </property-sheet>
Also this constraint imposes to have a reverse association for all associations defined in Alfresco. To remove such a restriction we can catch and ignore the corresponding exception

    public void onDeleteAssociation(AssociationRef nodeAssocRef) {
        try {
            nodeService.removeAssociation(nodeAssocRef.getTargetRef(), nodeAssocRef.getSourceRef(), getReverseQnameAssociationName(nodeAssocRef.getTypeQName()));
        } catch (IllegalArgumentException exception) {
            if (exception.getMessage().indexOf(NOT_DEFINED_EXCEPTION_MESSAGE) == -1)
                throw exception;
            // otherwise ignore it
        }
    }

    public void onCreateAssociation(AssociationRef nodeAssocRef) {
        try {
            nodeService.createAssociation(nodeAssocRef.getTargetRef(), nodeAssocRef.getSourceRef(), getReverseQnameAssociationName(nodeAssocRef.getTypeQName()));
        } catch (IllegalArgumentException exception) {
            if (exception.getMessage().indexOf(NOT_DEFINED_EXCEPTION_MESSAGE) == -1)
                throw exception;
            // otherwise ignore it
        }
    }