Introduction
When upgrading a customer from Alfresco enterprise 5.0.2 to 5.2.5, we noticed that when an exception is thrown by a behavior, it is not always properly bubbled to the CMIS Client. Instead, the client might receive a more cryptic error that doesn’t list the actual cause of the issue.
In this blog post, we will explore why this happens and how to get around it to ensure CMIS clients get fully informed of why their action failed.
Basic use case
We will work on a fictional use case where we would want to prevent users from adding phone numbers in the description field of documents. To achieve this, we create a simple behavior that will, every time a document is updated, throw an error when a phone number is detected:
public class PhoneNumberFilter { private PolicyComponent eventManager; public void setPolicyComponent(PolicyComponent policyComponent) { this.eventManager = policyComponent; } public void registerEventHandlers() { eventManager.bindClassBehaviour(NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onUpdateDocumentProperties", Behaviour.NotificationFrequency.TRANSACTION_COMMIT)); } public void onUpdateDocumentProperties(final NodeRef nodeRef, final Map<QName, Serializable> before, final Map<QName, Serializable> after) { if (null != after.get(ContentModel.PROP_DESCRIPTION) && !after.get(ContentModel.PROP_DESCRIPTION).equals(before.get(ContentModel.PROP_DESCRIPTION))) { this.checkPhoneNumberRestriction((MLText) after.get(ContentModel.PROP_DESCRIPTION)); } } private void checkPhoneNumberRestriction(MLText afterDescription) { // Note that the regex is overly simplistic for the needs of this blog post String patternString = "\\(\\d{3}\\)\\d{3}-\\d{4}"; Pattern pattern = Pattern.compile(patternString); Matcher matcher = pattern.matcher(afterDescription.getDefaultValue()); if (matcher.matches()) { throw new AlfrescoRuntimeException("The description contains a phone number which is not allowed"); } } }
Once our Alfresco server is started with our custom behaviour, we can access it with a CMIS client. We will use CMIS Workbench for the purpose of the post.
We will create a document:
Attempt to modify the description with a phone number:
And be denied with the following exception which does not display the message we threw:
This is inconvenient because the user will not understand why his metadata update was rejected and it can lead to serious confusion. Especially if, as is the case in the above code, we do not explicitly log the error ourselves. Because Alfresco doesn’t either and there won’t be any trace of the Exception having occurred anything in Alfresco’s log.
Simple workaround
A simple workaround would be to ensure that our behavior runs at the event level rather than on transaction commit. It turns out that exceptions thrown by behaviors running on Behaviour.NotificationFrequency.FIRST_EVENT or Behaviour.NotificationFrequency.EVERY_EVENT do propagate the error properly all the way to the user. As such, if we replace the registerEventHandlers function with the following:
public void registerEventHandlers() { eventManager.bindClassBehaviour(NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onUpdateDocumentProperties", Behaviour.NotificationFrequency.EVERY_EVENT)); }
The error message will be friendlier and more descriptive:
However, this approach has its limitations too. You will notice that we used EVERY_EVENT as a single update will call this method once for every changed property, hence FIRST_EVENT might not be the one triggered for the change in the description but the change in another property.
And due to that exact restriction, this approach will not work if the restriction we want to implement is based on several properties.
Advanced use case
Let’s say that our use case has evolved and we allow people to fill in phone numbers in the descriptions of documents that were authored by ttutone, while still forbidding it for other documents.
The simple workaround above would not work anymore as, when triggering on EVERY_EVENT, we will never have a call with both the latest author and and the new description value.
Advanced workaround
It turns out that there is a specific set of exceptions that Alfresco will dig out from the causes stack and return instead of its wrapping exception. The logic for it and the list of those exceptions is in AlfrescoCmisExceptionInterceptor.java
The exception from that list that fits our needs is the IntegrityException.
Please note that this approach is not an official documented approach by Alfresco and that this list or even the core logic of this exception wrapping is subject to changes by Alfresco without notice with any version upgrade. As such, while we consider it a clean way to work around the issue we’re facing, it is something you will want to re-test with each upgrade.
But, as mentioned in the introduction, the need for this workaround already stems from the fact that Alfresco changed the way they bubbled CMIS exceptions; so this workaround makes you neither more nor less shielded from changes.
The new code that leverages this approach to fulfill our new use case is the following:
public class PhoneNumberFilter { private PolicyComponent eventManager; private final static List<String> ALLOWED_PHONE_AUTHORS = Arrays.asList("ttutone"); public void setPolicyComponent(PolicyComponent policyComponent) { this.eventManager = policyComponent; } public void registerEventHandlers() { eventManager.bindClassBehaviour(NodeServicePolicies.OnUpdatePropertiesPolicy.QNAME, ContentModel.TYPE_CONTENT, new JavaBehaviour(this, "onUpdateDocumentProperties", Behaviour.NotificationFrequency.TRANSACTION_COMMIT)); } public void onUpdateDocumentProperties(final NodeRef nodeRef, final Map<QName, Serializable> before, final Map<QName, Serializable> after) { if (null != after.get(ContentModel.PROP_DESCRIPTION) && !after.get(ContentModel.PROP_DESCRIPTION).equals(before.get(ContentModel.PROP_DESCRIPTION))) { this.checkPhoneNumberRestriction((MLText) after.get(ContentModel.PROP_DESCRIPTION), (String) after.get(ContentModel.PROP_AUTHOR)); } } private void checkPhoneNumberRestriction(MLText afterDescription, String author) { // Note that the regex is overly simplistic for the needs of this blog post String patternString = "\\(\\d{3}\\)\\d{3}-\\d{4}"; Pattern pattern = Pattern.compile(patternString); Matcher matcher = pattern.matcher(afterDescription.getDefaultValue()); if (matcher.matches()) { // We should base the restriction on a group and not a hardcoded list of usernames but we will keep it // simple for this post's purposes if(!ALLOWED_PHONE_AUTHORS.contains(author)) { throw new IntegrityException("The description contains a phone number which is not allowed", null); } } } }
As you can see, we are back to running the behavior on TRANSACTION_COMMIT, but are now throwing an IntegrityException (if you use this approach, we would recommend creating your own subclass and add a decent amount of Javadoc as to why this specific exception is needed). With this new code, documents authored by ttutone can have a phone number added to their description, but documents authored by anyone else will receive our error message with a short text prepended by Alfresco’s logic:
Conclusion
Detailed error messages go a long way to provide clarity for end users. They can figure out what went wrong and whether or not it was intentional. As such, if your behavior exceptions can potentially end up being thrown as a result of a CMIS call, you will probably want to look into the above approaches.