### Eclipse Workspace Patch 1.0 #P james-v2.3 Index: src/java/org/apache/mailet/CompositeMatcher.java =================================================================== --- src/java/org/apache/mailet/CompositeMatcher.java (revision 0) +++ src/java/org/apache/mailet/CompositeMatcher.java (revision 0) @@ -0,0 +1,47 @@ +package org.apache.mailet; + +import java.util.Iterator; +import org.apache.mailet.Matcher; +import java.lang.String; + +/** + * A CompositeMatcher contains child matchers that are invoked in turn and their + * recipient results are composed from the composite operation. See And, Or, Xor and Not. + * One or more children may be supplied to a composite via declaration inside a processor + * in the james-config.xml file. When the composite is the outter-most declaration it must be named, as in the example below. + * The composite matcher may be referenced by name and used in a subsequent mailet. Any matcher may be included as a child of + * a composite matcher, including another composite matcher or the Not matcher. + * As a consequence, the class names: And, Or, Not and Xor are permanently reserved. + *
+ *+ * @author Ralph Holland 2010-01-11 + */ +public interface CompositeMatcher extends Matcher +{ + + /** + * @return Iterator if child Matchers + */ + public Iterator iterator(); + + /** + * Add a child matcher to this composite matcher. This is called by SpoolManager.setupMatcher() + * @param matcher Matcher is the child that this composite treats. + */ + public void add(Matcher matcher); + + +} Index: src/test/org/apache/james/transport/matchers/OrTest.java =================================================================== --- src/test/org/apache/james/transport/matchers/OrTest.java (revision 0) +++ src/test/org/apache/james/transport/matchers/OrTest.java (revision 0) @@ -0,0 +1,78 @@ +package org.apache.james.transport.matchers; + +import org.apache.james.test.mock.mailet.MockMail; +import org.apache.james.test.mock.mailet.MockMailContext; +import org.apache.james.test.mock.mailet.MockMatcherConfig; + +import org.apache.mailet.MailAddress; +import org.apache.mailet.Matcher; +import org.apache.mailet.CompositeMatcher; + +import javax.mail.MessagingException; +import javax.mail.internet.ParseException; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +import junit.framework.TestCase; + +public class OrTest extends TestCase { + + private MockMail mockedMail; + + private CompositeMatcher matcher; + + public OrTest(String arg0) throws UnsupportedEncodingException { + super(arg0); + } + + private void setupMockedMail() throws ParseException { + mockedMail = new MockMail(); + mockedMail.setRecipients(Arrays.asList(new MailAddress[] { + new MailAddress("test@james.apache.org"), + new MailAddress("test2@james.apache.org") })); + + } + + /** + * Setup a composite Or matcher and test it + * @throws MessagingException + */ + private void setupMatcher() throws MessagingException { + MockMailContext context = new MockMailContext(); + matcher = new Or(); + MockMatcherConfig mci = new MockMatcherConfig("Or",context); + matcher.init(mci); + Matcher child = null; + MockMatcherConfig sub = null; + child = new RecipientIsRegex(); + sub = new MockMatcherConfig("RecipientIsRegex=test@james.apache.org",context); + child.init(sub); + matcher.add(child); + child = new RecipientIsRegex(); + sub = new MockMatcherConfig("RecipientIsRegex=test2@james.apache.org",context); + child.init(sub); + matcher.add(child); + } + + // test if all recipients was returned + public void testAllRecipientsReturned() throws MessagingException { + setupMockedMail(); + setupMatcher(); + + Collection matchedRecipients = matcher.match(mockedMail); + + assertNotNull(matchedRecipients); + assertEquals(matchedRecipients.size(), mockedMail.getRecipients().size()); + + // Now ensure they match the actual recipients + Iterator iterator = matchedRecipients.iterator(); + MailAddress address = (MailAddress)iterator.next(); + assertEquals(address,"test@james.apache.org"); + address = (MailAddress)iterator.next(); + assertEquals(address,"test2@james.apache.org"); + } + +} Index: src/java/org/apache/james/transport/matchers/And.java =================================================================== --- src/java/org/apache/james/transport/matchers/And.java (revision 0) +++ src/java/org/apache/james/transport/matchers/And.java (revision 0) @@ -0,0 +1,92 @@ +package org.apache.james.transport.matchers; + +import org.apache.mailet.CompositeMatcherBase; +import java.util.Collection; +import java.util.Iterator; +import java.util.ArrayList; +import org.apache.mailet.MailAddress; +import org.apache.mailet.Mail; +import javax.mail.MessagingException; +import org.apache.mailet.Matcher; +/** + * This matcher performs And conjunction between the two recipients + * + */ +public class And extends CompositeMatcherBase +{ + + + /** + * This is the And CompositeMatcher - consider it to be an intersection of the results. + * If any match returns an empty recipient result the matching is short-circuited. + * @return Collection of Recipient from the And composition results of the child Matchers. + */ + public Collection match(Mail mail) throws MessagingException + { + Collection finalResult = null; + Matcher matcher = null; + boolean first = true; + for (Iterator matcherIter = iterator(); matcherIter.hasNext(); ) + { + matcher = (Matcher)(matcherIter.next()); + Collection result = matcher.match(mail); + + if (result==null) + { + // short-circuit + // log("Matching with " + matcher.getMatcherConfig().getMatcherName() + " result.size()=0"); + return new ArrayList(0); + } + if (result.size()==0) + { + return result; + } + + // log("Matching with " + matcher.getMatcherConfig().getMatcherName() + " result.size()="+result.size()); + + if (first) + { + finalResult = result; + first = false; + } + else + { + // Check if we need to And ... + // if the finalResult and the subsequent result are the same collection, then it contains the same recipients + // so we can short-circuit building the AND of the two + if (finalResult!=result) + { + if (result!=null) + { + // the two results are different collections, so we AND them + // Ensure that the finalResult only contains recipients in the result collection + Collection newResult = new ArrayList(); + MailAddress recipient = null; + for (Iterator i = finalResult.iterator(); i.hasNext(); ) + { + recipient = (MailAddress)i.next(); + // log("recipient="+recipient.toString()); + if (result.contains(recipient)) + { + newResult.add(recipient); + } + } + recipient = null; + // basically the finalResult gets replaced with a smaller result + // otherwise finalResult would have been equal to result (in all cases) + finalResult = newResult; + } + else + { + finalResult = result; + } + } + } + result = null; + } + matcher = null; + // log("answer is "+finalResult.toString()); + return finalResult; + } + +} Index: src/test/org/apache/james/transport/matchers/XorTest.java =================================================================== --- src/test/org/apache/james/transport/matchers/XorTest.java (revision 0) +++ src/test/org/apache/james/transport/matchers/XorTest.java (revision 0) @@ -0,0 +1,100 @@ +package org.apache.james.transport.matchers; + +import org.apache.james.test.mock.mailet.MockMail; +import org.apache.james.test.mock.mailet.MockMailContext; +import org.apache.james.test.mock.mailet.MockMatcherConfig; + +import org.apache.mailet.MailAddress; +import org.apache.mailet.Matcher; +import org.apache.mailet.CompositeMatcher; + +import javax.mail.MessagingException; +import javax.mail.internet.ParseException; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +import junit.framework.TestCase; + +public class XorTest extends TestCase { + + private MockMailContext context; + private MockMail mockedMail; + + private CompositeMatcher matcher; + + public XorTest(String arg0) throws UnsupportedEncodingException { + super(arg0); + } + + private void setupMockedMail() throws ParseException { + mockedMail = new MockMail(); + mockedMail.setRecipients(Arrays.asList(new MailAddress[] { + new MailAddress("test@james.apache.org"), + new MailAddress("test2@james.apache.org") })); + + } + + /** + * Setup a composite Or matcher and test it + * @throws MessagingException + */ + private void setupMatcher() throws MessagingException { + context = new MockMailContext(); + matcher = new Xor(); + MockMatcherConfig mci = new MockMatcherConfig("Xor",context); + matcher.init(mci); + } + + private void setupChild(String match) throws MessagingException + { + Matcher child = null; + if (match.equals("All")) + { + child = new All(); + } + else + { + child = new RecipientIsRegex(); + } + MockMatcherConfig sub = new MockMatcherConfig(match,context); + child.init(sub); + matcher.add(child); + + } + + // test if all recipients was returned + public void testIntersectSame() throws MessagingException { + setupMockedMail(); + setupMatcher(); + setupChild("RecipientIsRegex=test@james.apache.org"); + setupChild("RecipientIsRegex=test@james.apache.org"); + + Collection matchedRecipients = matcher.match(mockedMail); + + assertNotNull(matchedRecipients); + assertEquals(0,matchedRecipients.size()); + } + + public void testNoIntersect() throws MessagingException { + setupMockedMail(); + setupMatcher(); + setupChild("RecipientIsRegex=test@james.apache.org"); + setupChild("RecipientIsRegex=test2@james.apache.org"); + + Collection matchedRecipients = matcher.match(mockedMail); + + assertNotNull(matchedRecipients); + assertEquals(2,matchedRecipients.size()); + + Iterator iterator = matchedRecipients.iterator(); + MailAddress address = (MailAddress)iterator.next(); + assertEquals(address,"test@james.apache.org"); + address = (MailAddress)iterator.next(); + assertEquals(address,"test2@james.apache.org"); + } + + +} Index: src/java/org/apache/james/transport/matchers/Not.java =================================================================== --- src/java/org/apache/james/transport/matchers/Not.java (revision 0) +++ src/java/org/apache/james/transport/matchers/Not.java (revision 0) @@ -0,0 +1,43 @@ +package org.apache.james.transport.matchers; + +import org.apache.mailet.CompositeMatcherBase; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import org.apache.mailet.Matcher; +import org.apache.mailet.Mail; +import javax.mail.MessagingException; + + + +public class Not extends CompositeMatcherBase { + + /** + * This is the Not CompositeMatcher - consider what wasn't in the result set of each child matcher. + * Of course it is easier to understand if it only includes one matcher in the composition, + * the normal recommended use. @See CompositeMatcher interface. + * @return Collectiom of Recipient from the Negated composition of the child Matcher(s). + */ + public Collection match(Mail mail) throws MessagingException + { + Collection finalResult = mail.getRecipients(); + Matcher matcher = null; + for (Iterator matcherIter = iterator(); matcherIter.hasNext(); ) + { + matcher = (Matcher)(matcherIter.next()); +// log("Matching with " + matcher.getMatcherConfig().getMatcherName()); + Collection result = matcher.match(mail); + if (result==finalResult) + { + // Not is an empty list + finalResult = null; + } + else if(result!=null) + { + finalResult = new ArrayList(finalResult); + finalResult.removeAll(result); + } + } + return finalResult; + } +} Index: src/java/org/apache/james/transport/matchers/HasHeaderWithRegex.java =================================================================== --- src/java/org/apache/james/transport/matchers/HasHeaderWithRegex.java (revision 0) +++ src/java/org/apache/james/transport/matchers/HasHeaderWithRegex.java (revision 0) @@ -0,0 +1,102 @@ +package org.apache.james.transport.matchers; + +import javax.mail.internet.MimeMessage; +import org.apache.mailet.GenericMatcher; +import org.apache.mailet.Mail; +import org.apache.mailet.MatcherConfig; +import java.util.Collection; +import javax.mail.MessagingException; +import java.io.Serializable; + +import org.apache.oro.text.regex.MalformedPatternException; +import org.apache.oro.text.regex.Pattern; +import org.apache.oro.text.regex.Perl5Compiler; +import org.apache.oro.text.regex.Perl5Matcher; + +import java.util.Collection; + +import javax.mail.MessagingException; + + /** + *+ * + * + *+ * + *+ * + *+ * + * + * + * + * *spam + *
Matches mails that have a specified header that matches a regular expression + */ +public class HasHeaderWithRegex extends GenericMatcher +{ + Pattern pattern = null; + String headerName = null; + String patternStr = null; + + public void init (MatcherConfig conf) throws MessagingException + { + super.init(conf); + String condition = conf.getCondition(); + int idx = condition.indexOf(','); + if (idx != -1) { + headerName = condition.substring(0,idx).trim(); + patternStr = condition.substring (idx+1, condition.length()).trim(); + //log("headerName: "+headerName+" pattern "+patternStr); + try { + Perl5Compiler compiler = new Perl5Compiler(); + pattern = compiler.compile(patternStr); + } + catch(MalformedPatternException mpe) + { + throw new MessagingException("Malformed pattern: " + patternStr, mpe); + } + } else + { + throw new MessagingException ("malformed condition for HasHeaderWithRegex. must be of the form: attr,regex"); + } + } + + public Collection match(Mail mail) { + MimeMessage message = null; + String[] headers = null; + try + { + message = mail.getMessage(); + headers = message.getHeader(headerName); + } + catch(MessagingException me) + { + headers = null; + } + finally + { + message = null; + } + if (headers == null) + { + //log("header " + headerName + " not found."); + return null; + } + Perl5Matcher matcher = new Perl5Matcher(); + String header = null; + for (int i =0; i < headers.length; i++) + { + header = headers[i]; + //log( "matching header " + headerName + ": " + header + ": with " + patternStr + ":" ); + if (matcher.matches(header, pattern)) + { + //log( "TRUE matched header " + headerName + ": " + header + ": with " + patternStr + ":" ); + header = null; + matcher = null; + message = null; + return mail.getRecipients(); + } + } + //log( "FALSE matched header " + headerName + ": " + header + ": with " + patternStr + ":" ); + header = null; + matcher = null; + message = null; + return null; + } + + + + public String getMatcherInfo() + { + return "HasHeaderWithRegex"; + } + +} Index: src/java/org/apache/james/services/MatcherLoader.java =================================================================== --- src/java/org/apache/james/services/MatcherLoader.java (revision 898405) +++ src/java/org/apache/james/services/MatcherLoader.java (working copy) @@ -20,7 +20,6 @@ package org.apache.james.services; import org.apache.mailet.Matcher; - import javax.mail.MessagingException; public interface MatcherLoader { @@ -31,14 +30,23 @@ String ROLE = "org.apache.james.services.MatcherLoader"; /** - * Get a new Matcher with the specified name acting - * in the specified context. + * Get a new Matcher with the specified match class name acting + * in the acquired context. * * @param matchName the name of the matcher to be loaded - * @param context the MailetContext to be passed to the new - * matcher * @throws MessagingException if an error occurs */ public Matcher getMatcher(String matchName) throws MessagingException; + + + /** + * Get a new Matcher with the specified match class name with an optional alias name acting + * in the acquired context. + * + * @param matchName the name of the matcher to be loaded e.g. And, Or, Not, Xor. + * @param aliasName e.g. spam-matcher + * @throws MessagingException if an error occurs + */ + public Matcher getMatcher(String matchName,String aliasName) throws MessagingException; } Index: src/java/org/apache/james/transport/JamesSpoolManager.java =================================================================== --- src/java/org/apache/james/transport/JamesSpoolManager.java (revision 898405) +++ src/java/org/apache/james/transport/JamesSpoolManager.java (working copy) @@ -19,6 +19,7 @@ package org.apache.james.transport; +import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.activity.Initializable; import org.apache.avalon.framework.configuration.Configurable; @@ -37,6 +38,7 @@ import org.apache.mailet.Mailet; import org.apache.mailet.MailetException; import org.apache.mailet.Matcher; +import org.apache.mailet.CompositeMatcher; import javax.mail.MessagingException; @@ -139,7 +141,7 @@ MailetLoader mailetLoader = (MailetLoader) compMgr.lookup(MailetLoader.ROLE); - MatcherLoader matchLoader + MatcherLoader matcherLoader = (MatcherLoader) compMgr.lookup(MatcherLoader.ROLE); //A processor is a Collection of @@ -156,9 +158,21 @@ processor.setSpool(spool); processor.initialize(); processors.put(processorName, processor); + + final Configuration[] mailetConfs = processorConf.getChildren( "mailet" ); + + // @uthor Ralph Holland 2010-01-11 + // obtain all the top-level matchers underneath this processor + // and create their tree structure in ready for use in a mailet match expression + Configuration[] matchers = processorConf.getChildren( "matcher" ); + if (matchers != null) + { + for ( int m=0; m < matchers.length; m++ ) + { + setupMatcher( matcherLoader, processorName, matchers[m], true ); + } + } - final Configuration[] mailetConfs - = processorConf.getChildren( "mailet" ); // Loop through the mailet configuration, load // all of the matcher and mailets, and add // them to the processor. @@ -170,7 +184,7 @@ Mailet mailet = null; Matcher matcher = null; try { - matcher = matchLoader.getMatcher(matcherName); + matcher = matcherLoader.getMatcher(matcherName); //The matcher itself should log that it's been inited. if (getLogger().isInfoEnabled()) { StringBuffer infoBuffer = @@ -190,9 +204,9 @@ .append(": ") .append(ex.toString()); getLogger().error( errorBuffer.toString(), ex ); - if (ex.getNextException() != null) { - getLogger().error( "Caused by nested exception: ", ex.getNextException()); - } + if (ex.getNextException() != null) { + getLogger().error( "Caused by nested exception: ", ex.getNextException()); + } } System.err.println("Unable to init matcher " + matcherName); System.err.println("Check spool manager logs for more details."); @@ -278,8 +292,132 @@ reader.start(); } } + + /** + * Internal routine to absorb ConfigurationException when attempting to get the value of + * a non-existant attribute i.e. an optional attribute + * @param configuration class containing the xml for the matcher + * @param name of attribute + * @return String attribute value or null if it does not exist + */ + private static String getOptionalAttribute(Configuration configuration,String name) + { + try + { + return configuration.getAttribute(name); + } + catch(ConfigurationException ce) + { + return null; + } + } + + /** + * This is used to setup a Matcher outside the declaration of a maillet + * @param matcherConfig + * @param outer boolean flag set to true when the config is directly underneath a processor element. + * @return CompositeMatcher that has been constructed and initialized before the mailet is created. + * @throws ConfigurationException, MessagingException + */ + private Matcher setupMatcher(MatcherLoader matcherLoader,String processorName,Configuration matcherConfig,boolean outer) throws MessagingException, ConfigurationException + { + Matcher matcher = null; + String matchAttribute = matcherConfig.getAttribute("match"); + String aliasName = getOptionalAttribute(matcherConfig,"name"); + getLogger().info("setupMatcher match="+matchAttribute+" name="+aliasName); + if (outer && aliasName==null) + { + StringBuffer errorBuffer = new StringBuffer(256) + .append("In processor ") + .append(processorName) + .append(": ") + .append(processorName) + .append("Outer level matchers must be named so they can be referred to from a mailet"); + if (getLogger().isErrorEnabled()) { + getLogger().error( errorBuffer.toString() ); + } + throw new MessagingException(errorBuffer.toString()); + } + if (matchAttribute == null) + { + StringBuffer errorBuffer = new StringBuffer(256) + .append("In processor ") + .append(processorName) + .append(": ") + .append(processorName) + .append("All matchers must have a match with the class name [and an optionally condition following the = sign)."); + if (getLogger().isErrorEnabled()) { + getLogger().error( errorBuffer.toString() ); + } + throw new MessagingException(errorBuffer.toString()); + } + // load a matcher instance + // if an alias is supplied, then that matcher will be assigned the alias as a name and + // its instance will be available via that name in a mailet match expression + matcher = matcherLoader.getMatcher(matchAttribute,aliasName); + if (matcher==null) + { + if (aliasName != null) + { + StringBuffer errorBuffer = new StringBuffer(256) + .append("In processor ") + .append(processorName) + .append(": ") + .append("Unable to find matcher named ") + .append(aliasName); + if (getLogger().isErrorEnabled()) { + getLogger().error( errorBuffer.toString() ); + } + throw new MessagingException(errorBuffer.toString()); + } + else if (matchAttribute != null) + { + StringBuffer errorBuffer = new StringBuffer(256) + .append("In processor ") + .append(processorName) + .append(": ") + .append("Unable to find matcher with match ") + .append(matchAttribute); + if (getLogger().isErrorEnabled()) { + getLogger().error( errorBuffer.toString() ); + } + throw new MessagingException(errorBuffer.toString()); + } + } + if (matcher instanceof CompositeMatcher) + { + getLogger().info("setupMatcher recurring to setup children"); + // use recursion to establish the children of a composite + setupMatcherChildren( matcherLoader, processorName, (CompositeMatcher)matcher, matcherConfig.getChildren("matcher") ); + getLogger().info("setupMatcher done setupMatcherChildren"); + } + return matcher; + } + /** + * The setup for the matcher children which are iteratively and recursively setup via the + * setupMatcher() method for any composite matchers immediately declared under this compositeMatcher. + * @param matchLoader the MatcherLoader used to load classes dynamically. + * @param processorName String value of the processor element's name. + * @param compositeMatcher CompositeMatcher that child Matchers are setup underneath. + * @param childConfigs Configuration of each child Matcher. + * @throws ConfigurationException, MessagingException + */ + private void setupMatcherChildren(MatcherLoader matchLoader, String processorName, CompositeMatcher compositeMatcher,Configuration[] childConfigs) + throws MessagingException, ConfigurationException + { + if (childConfigs != null) + { + for ( int m=0; m < childConfigs.length; m++ ) + { + // iterate across all the child matchers and set them up + compositeMatcher.add( setupMatcher( matchLoader, processorName, childConfigs[m], false ) ); + } + } + } + + /** * This routinely checks the message spool for messages, and processes * them as necessary */ Index: src/java/org/apache/james/transport/matchers/Xor.java =================================================================== --- src/java/org/apache/james/transport/matchers/Xor.java (revision 0) +++ src/java/org/apache/james/transport/matchers/Xor.java (revision 0) @@ -0,0 +1,78 @@ +package org.apache.james.transport.matchers; + +import org.apache.mailet.CompositeMatcherBase; +import java.util.Collection; +import java.util.Iterator; +import java.util.ArrayList; +import org.apache.mailet.MailAddress; +import org.apache.mailet.Mail; +import javax.mail.MessagingException; +import org.apache.mailet.Matcher; + +public class Xor extends CompositeMatcherBase { + + /** + * This is the Xor CompositeMatcher - consider it to be the inequality operator for recipients. + * If any recipients match other matcher results then the result does not include that recipient. + * @return Collection of Recipients from the Xor composition of the child matchers. + */ + public Collection match(Mail mail) throws MessagingException + { + Collection finalResult = null; + Matcher matcher = null; + boolean first = true; + for (Iterator matcherIter = iterator(); matcherIter.hasNext(); ) + { + matcher = (Matcher)(matcherIter.next()); + Collection result = matcher.match(mail); + if (result==null) + { + result = new ArrayList(0); + } + //log("Matching with " + matcher.getMatcherConfig().getMatcherName() + " result="+result.toString() ); + + if (first) + { + finalResult = result; + first = false; + } + else + { + // Check if we need to Xor ... + // if the finalResult and the subsequent result are the same collection, then it contains the same recipients + // so we can short-circuit building the XOR and return an empty set + if (finalResult==result) + { + // the XOR of the same collection is empty + finalResult.clear(); + // log("same collection - so clear"); + } + else + { + // the two results are different collections, so we XOR them + // Ensure that the finalResult does not contain recipients in the result collection + MailAddress recipient = null; + for (Iterator i = result.iterator(); i.hasNext(); ) + { + recipient =(MailAddress)(i.next()); + if (!finalResult.contains(recipient)) + { + finalResult.add(recipient); + } + else + { + finalResult.remove(recipient); + } + } + recipient = null; + // log("xor recipients into new finalResult="+finalResult); + } + // basically the finalResult gets replaced with a smaller result + // otherwise finalResult would have been equal to result (in all cases) + } + result = null; + } + return finalResult; + } + +} Index: src/test/org/apache/james/transport/matchers/AndTest.java =================================================================== --- src/test/org/apache/james/transport/matchers/AndTest.java (revision 0) +++ src/test/org/apache/james/transport/matchers/AndTest.java (revision 0) @@ -0,0 +1,96 @@ +package org.apache.james.transport.matchers; + +import org.apache.james.test.mock.mailet.MockMail; +import org.apache.james.test.mock.mailet.MockMailContext; +import org.apache.james.test.mock.mailet.MockMatcherConfig; + +import org.apache.mailet.MailAddress; +import org.apache.mailet.Matcher; +import org.apache.mailet.CompositeMatcher; + +import javax.mail.MessagingException; +import javax.mail.internet.ParseException; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collection; + +import junit.framework.TestCase; + +public class AndTest extends TestCase { + + private MockMailContext context; + private MockMail mockedMail; + + private CompositeMatcher matcher; + + public AndTest(String arg0) throws UnsupportedEncodingException { + super(arg0); + } + + private void setupMockedMail() throws ParseException { + mockedMail = new MockMail(); + mockedMail.setRecipients(Arrays.asList(new MailAddress[] { + new MailAddress("test@james.apache.org"), + new MailAddress("test2@james.apache.org") })); + + } + + /** + * Setup a composite Or matcher and test it + * @throws MessagingException + */ + private void setupMatcher() throws MessagingException { + context = new MockMailContext(); + matcher = new And(); + MockMatcherConfig mci = new MockMatcherConfig("And",context); + matcher.init(mci); + } + + private void setupChild(String match) throws MessagingException + { + Matcher child = null; + if (match.equals("All")) + { + child = new All(); + } + else + { + child = new RecipientIsRegex(); + } + MockMatcherConfig sub = new MockMatcherConfig(match,context); + child.init(sub); + matcher.add(child); + + } + + // test if all recipients was returned + public void testAndIntersectSameTwice() throws MessagingException { + setupMockedMail(); + setupMatcher(); + setupChild("RecipientIsRegex=test@james.apache.org"); + setupChild("RecipientIsRegex=test@james.apache.org"); + setupChild("All"); + + Collection matchedRecipients = matcher.match(mockedMail); + + assertNotNull(matchedRecipients); + assertEquals(1,matchedRecipients.size()); + MailAddress address = (MailAddress)matchedRecipients.iterator().next(); + assertEquals(address,"test@james.apache.org"); + } + + public void testAndNoIntersect() throws MessagingException { + setupMockedMail(); + setupMatcher(); + setupChild("RecipientIsRegex=test@james.apache.org"); + setupChild("RecipientIsRegex=test2@james.apache.org"); + + Collection matchedRecipients = matcher.match(mockedMail); + + assertNotNull(matchedRecipients); + assertEquals(0,matchedRecipients.size()); + } + + +} Index: src/java/org/apache/james/transport/JamesMatcherLoader.java =================================================================== --- src/java/org/apache/james/transport/JamesMatcherLoader.java (revision 898405) +++ src/java/org/apache/james/transport/JamesMatcherLoader.java (working copy) @@ -26,11 +26,14 @@ import org.apache.james.services.MatcherLoader; import org.apache.mailet.MailetException; import org.apache.mailet.Matcher; +import org.apache.mailet.CompositeMatcher; +import java.util.HashMap; /** * Loads Matchers for use inside James. * */ public class JamesMatcherLoader extends Loader implements MatcherLoader { + /** * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration) */ @@ -39,9 +42,21 @@ } /* (non-Javadoc) - * @see org.apache.james.transport.MatcherLoader#getMatcher(java.lang.String) - */ - public Matcher getMatcher(String matchName) throws MessagingException { + * @see org.apache.james.transport.MatcherLoader#getMatcher(java.lang.String) + * Original signature skeleton invokes original loader code with additional aliasName set to null + */ + public Matcher getMatcher(String matchName) throws MessagingException + { + return getMatcher(matchName,null); + } + + /* (non-Javadoc) + * @see org.apache.james.transport.MatcherLoader#getMatcher(java.lang.String,java.lang.String) + * modified to include aliasName by Ralph Holland 2010-01-11 + */ + public Matcher getMatcher(String matchName,String aliasName) throws MessagingException { + + Matcher matcher = null; try { String condition = (String) null; int i = matchName.indexOf('='); @@ -49,16 +64,36 @@ condition = matchName.substring(i + 1); matchName = matchName.substring(0, i); } + // modified to lookup in the namedMatchers collection first - Ralph Holland + matcher = (Matcher)namedMatchers.get(matchName); + if (matcher!=null) + { + return matcher; + } for (i = 0; i < packages.size(); i++) { - String className = (String) packages.elementAt(i) + matchName; + String fullClassName = (String) packages.elementAt(i) + matchName; try { + // attempt to load it + matcher = (Matcher) Thread.currentThread().getContextClassLoader().loadClass(fullClassName).newInstance(); + + // modified by Ralph Holland to store with the name as the key + if (aliasName != null) + { + namedMatchers.put(aliasName,matcher); + } + else + { + aliasName = matchName; + } + + // initialize the matcher after sucessfully loading it MatcherConfigImpl configImpl = new MatcherConfigImpl(); - configImpl.setMatcherName(matchName); + configImpl.setMatcherName(aliasName); configImpl.setCondition(condition); configImpl.setMailetContext(mailetContext); - Matcher matcher = (Matcher) Thread.currentThread().getContextClassLoader().loadClass(className).newInstance(); matcher.init(configImpl); return matcher; + } catch (ClassNotFoundException cnfe) { //do this so we loop through all the packages } @@ -78,5 +113,8 @@ ")"); throw new MailetException(exceptionBuffer.toString(), e); } - } + } + + private HashMap namedMatchers = new HashMap(); + } Index: src/java/org/apache/mailet/CompositeMatcherBase.java =================================================================== --- src/java/org/apache/mailet/CompositeMatcherBase.java (revision 0) +++ src/java/org/apache/mailet/CompositeMatcherBase.java (revision 0) @@ -0,0 +1,39 @@ +package org.apache.mailet; + +import java.util.Collection; +import java.util.Iterator; +import java.util.ArrayList; +import org.apache.mailet.MailAddress; +import org.apache.mailet.Mail; +import javax.mail.MessagingException; +import org.apache.mailet.Matcher; +import org.apache.mailet.CompositeMatcher; +import org.apache.mailet.GenericMatcher; +import org.apache.james.core.MatcherConfigImpl; + +/** + * Abstract base class for CompositeMatchers. This class handles the child collection of Matchers associated with the CompositeMatcher. + */ +public abstract class CompositeMatcherBase extends GenericMatcher implements CompositeMatcher +{ + /** + * This lets the SpoolManager configuration code build up the composition (which might be composed of other composites). + * @param matcher Matcher child of the CompositeMatcher. + */ + public void add(Matcher matcher) + { + matchers.add(matcher); + } + + /** + * @return Iterator for the child matchers + */ + public Iterator iterator() + { + return matchers.iterator(); + } + + // the collection used to store the child-matchers + private Collection matchers = new ArrayList(); + +} Index: src/java/org/apache/james/transport/matchers/Or.java =================================================================== --- src/java/org/apache/james/transport/matchers/Or.java (revision 0) +++ src/java/org/apache/james/transport/matchers/Or.java (revision 0) @@ -0,0 +1,92 @@ +package org.apache.james.transport.matchers; + +import org.apache.mailet.CompositeMatcherBase; +import org.apache.mailet.MailAddress; +import org.apache.mailet.Mail; +import org.apache.mailet.Matcher; +import java.util.Collection; +import java.util.Iterator; +import java.util.ArrayList; +import javax.mail.MessagingException; + +public class Or extends CompositeMatcherBase { + + + /** + * This is the Or CompositeMatcher - consider it to be a union of the results. + * If any match results in a full set of recipients the matching is short-circuited. + * @return Collection of Recipient from the Or composition results of the child matchers. + */ + public Collection match(Mail mail) throws MessagingException + { + Collection finalResult = null; + Matcher matcher = null; + boolean first = true; + + // the size of the complete set of recipients + int size = mail.getRecipients().size(); + + // Loop through until the finalResult is full or all the child matchers have been executed + for (Iterator matcherIter = iterator(); matcherIter.hasNext(); ) + { + matcher = (Matcher)matcherIter.next(); +// log("Matching with " +// + matcher +// .getMatcherConfig() +// .getMatcherName() +// ); + Collection result = matcher.match(mail); + if (first) + { + if (result==null) + { + result = new ArrayList(0); + } + finalResult = result; + first = false; + } + else + { + // Check if we need to Or ... + // if the finalResult and the subsequent result are the same collection, then it contains the same recipients + // so we can short-circuit building the OR of the two + if (finalResult!=result) + { + if (result!=null) + { + if (finalResult==null) + { + finalResult = result; + } + else + { + // the two results are different collections, so we must OR them + // Ensure that the finalResult only contains one copy of the recipients in the result collection + MailAddress recipient = null; + for (Iterator i = result.iterator(); i.hasNext(); ) + { + recipient = (MailAddress)i.next(); + if (!finalResult.contains(recipient)) + { + finalResult.add(recipient); + } + } + recipient = null; + } + } + } + } + if (finalResult.size()==size) + { + // we have a complete set of recipients, no need to OR in anymore + // i.e. short-circuit the Or + break; + } + result = null; + matcher = null; + } +// log("OrMatch: end."); + return finalResult; + } + +}