initial versions.
authorErik Brakkee <erik@brakkee.org>
Sat, 23 Jul 2011 20:13:47 +0000 (22:13 +0200)
committerErik Brakkee <erik@brakkee.org>
Sat, 23 Jul 2011 20:13:47 +0000 (22:13 +0200)
27 files changed:
common/pom.xml [new file with mode: 0644]
common/src/main/java/org/wamblee/xmlrouter/common/Id.java [new file with mode: 0644]
config/pom.xml [new file with mode: 0644]
config/src/main/java/org/wamblee/xmlrouter/config/Config.java [new file with mode: 0644]
config/src/main/java/org/wamblee/xmlrouter/config/DocumentType.java [new file with mode: 0644]
config/src/main/java/org/wamblee/xmlrouter/config/Filter.java [new file with mode: 0644]
config/src/main/java/org/wamblee/xmlrouter/config/Rule.java [new file with mode: 0644]
config/src/main/java/org/wamblee/xmlrouter/config/Transformation.java [new file with mode: 0644]
impl/pom.xml [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/Constants.java [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/RobustDestination.java [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/RobustFilter.java [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/RobustTransformation.java [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/TransformationPath.java [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/Transformations.java [new file with mode: 0644]
impl/src/main/java/org/wamblee/xmlrouter/impl/XMLRouter.java [new file with mode: 0644]
impl/src/test/java/org/wamblee/xmlrouter/impl/RobustDestinationTest.java [new file with mode: 0644]
impl/src/test/java/org/wamblee/xmlrouter/impl/RobustFilterTest.java [new file with mode: 0644]
impl/src/test/java/org/wamblee/xmlrouter/impl/RobustTransformationTest.java [new file with mode: 0644]
impl/src/test/java/org/wamblee/xmlrouter/impl/TransformationsTest.java [new file with mode: 0644]
impl/src/test/java/org/wamblee/xmlrouter/impl/XMLRouterTest.java [new file with mode: 0644]
publish/pom.xml [new file with mode: 0644]
publish/src/main/java/org/wamblee/xmlrouter/publish/Gateway.java [new file with mode: 0644]
subscribe/pom.xml [new file with mode: 0644]
subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/Destination.java [new file with mode: 0644]
subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/DestinationRegistry.java [new file with mode: 0644]
subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/RegistrationException.java [new file with mode: 0644]

diff --git a/common/pom.xml b/common/pom.xml
new file mode 100644 (file)
index 0000000..d2c06b5
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.wamblee.xmlrouter</groupId>
+        <artifactId>xmlrouter-root</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xmlrouter-common</artifactId>
+    <packaging>jar</packaging>
+    <name>/xmlrouter/common</name>
+    <url>http://wamblee.org</url>
+    
+    
+    <distributionManagement>
+        <site>
+            <id>xmlrouter-site</id>
+            <url>file:${distrib}/cache</url>
+        </site>
+    </distributionManagement>
+
+</project>
diff --git a/common/src/main/java/org/wamblee/xmlrouter/common/Id.java b/common/src/main/java/org/wamblee/xmlrouter/common/Id.java
new file mode 100644 (file)
index 0000000..504459a
--- /dev/null
@@ -0,0 +1,35 @@
+package org.wamblee.xmlrouter.common;
+
+public class Id<T> {
+
+    private int id;
+
+    public Id(int aId) {
+        id = aId;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    @Override
+    public int hashCode() {
+        return id;
+    }
+
+    @Override
+    public boolean equals(Object aObj) {
+        if (aObj == null) {
+            return false;
+        }
+        if (!(aObj instanceof Id)) {
+            return false;
+        }
+        return id == ((Id<T>) aObj).id;
+    }
+
+    @Override
+    public String toString() {
+        return id + "";
+    }
+}
diff --git a/config/pom.xml b/config/pom.xml
new file mode 100644 (file)
index 0000000..26fa8b2
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.wamblee.xmlrouter</groupId>
+        <artifactId>xmlrouter-root</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xmlrouter-config</artifactId>
+    <packaging>jar</packaging>
+    <name>/xmlrouter/config</name>
+    <url>http://wamblee.org</url>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.wamblee.xmlrouter</groupId>
+            <artifactId>xmlrouter-common</artifactId>
+            <version>0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.wamblee</groupId>
+            <artifactId>wamblee-support-general</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+        </dependency>        
+    </dependencies>
+    
+    <distributionManagement>
+        <site>
+            <id>xmlrouter-site</id>
+            <url>file:${distrib}/cache</url>
+        </site>
+    </distributionManagement>
+
+</project>
diff --git a/config/src/main/java/org/wamblee/xmlrouter/config/Config.java b/config/src/main/java/org/wamblee/xmlrouter/config/Config.java
new file mode 100644 (file)
index 0000000..940b346
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * 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.wamblee.xmlrouter.config;
+
+import java.util.Collection;
+
+import org.wamblee.xmlrouter.common.Id;
+
+/**
+ * Configuration API for the XML router.
+ * 
+ * @author Erik Brakkee
+ */
+public interface Config {
+
+    Id<DocumentType> addDocumentType(DocumentType aType);
+
+    void removeDocumentType(Id<DocumentType> aId);
+
+    Collection<DocumentType> getDocumentTypes();
+
+    Id<Transformation> addTransformation(Transformation aTransformation);
+
+    void removeTransformation(Id<Transformation> aId);
+
+    Collection<Transformation> getTransformations();
+
+    Id<Filter> addFilter(Filter aFilter);
+
+    void removeFilter(Id<Filter> aId);
+
+    Collection<Filter> getFilters();
+
+}
diff --git a/config/src/main/java/org/wamblee/xmlrouter/config/DocumentType.java b/config/src/main/java/org/wamblee/xmlrouter/config/DocumentType.java
new file mode 100644 (file)
index 0000000..edcf294
--- /dev/null
@@ -0,0 +1,27 @@
+package org.wamblee.xmlrouter.config;
+
+import javax.xml.transform.dom.DOMSource;
+
+public interface DocumentType {
+    
+    /**
+     * Symbolic name for the document type. 
+     * @return Name. 
+     */
+    String getName();
+
+    /**
+     * Checks if a document is of the given type. 
+     * @param aSource Document
+     * @return True iff the document is of the given type. 
+     */
+    boolean isInstance(DOMSource aSource);
+    
+    /**
+     * Validates the document. Implementations that do not validate should simply
+     * return true always. 
+     * @param aSource Document.
+     * @return True iff the document is valid. 
+     */
+    boolean validate(DOMSource aSource);
+}
diff --git a/config/src/main/java/org/wamblee/xmlrouter/config/Filter.java b/config/src/main/java/org/wamblee/xmlrouter/config/Filter.java
new file mode 100644 (file)
index 0000000..db9dee9
--- /dev/null
@@ -0,0 +1,13 @@
+package org.wamblee.xmlrouter.config;
+
+import javax.xml.transform.dom.DOMSource;
+
+public interface Filter {
+
+    /**
+     * Determines if a given document will be processed or not. 
+     * @param aSource Source document. 
+     * @return
+     */
+    boolean isAllowed(String aDocumentType, DOMSource aSource);
+}
diff --git a/config/src/main/java/org/wamblee/xmlrouter/config/Rule.java b/config/src/main/java/org/wamblee/xmlrouter/config/Rule.java
new file mode 100644 (file)
index 0000000..e8bbfb4
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * 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.wamblee.xmlrouter.config;
+
+import org.wamblee.xml.XMLDocument;
+import org.wamblee.xml.XMLException;
+import org.wamblee.xml.XMLProcessor;
+import org.wamblee.xml.XPathExpression;
+
+/**
+ * Basic rule for XML routing. 
+ * 
+ * @author Erik Brakkee
+ */
+public class Rule {
+     
+    private XPathExpression condition; 
+    private XMLProcessor processor; 
+    private String destination; 
+
+    /**
+     * Constructs the rule. 
+     * @param aCondition Condition to be satisfied. 
+     * @param aProcessor XML Processor to be applied when the condition is valid. 
+     * @param aDestination Destination for the result of this rule. 
+     */
+    public Rule(XPathExpression aCondition, XMLProcessor aProcessor, String aDestination) {
+        condition = aCondition;
+        processor = aProcessor;
+        destination = aDestination;
+    }
+    
+    /**
+     * Applies the rule to a document. 
+     * @param aDocument Document to apply rule ot. 
+     * @return Transformed document or null if the rule did not apply. 
+     * @throws XMLException In case the application of the rule gives an error. 
+     */
+    public XMLDocument apply(XMLDocument aDocument) throws XMLException {
+        if ( !condition.booleanEval(aDocument)) { 
+            return null; 
+        }
+        return aDocument.process(processor); 
+    }
+    
+    public String getDestination() {
+        return destination;
+    }
+}
diff --git a/config/src/main/java/org/wamblee/xmlrouter/config/Transformation.java b/config/src/main/java/org/wamblee/xmlrouter/config/Transformation.java
new file mode 100644 (file)
index 0000000..b681afc
--- /dev/null
@@ -0,0 +1,28 @@
+package org.wamblee.xmlrouter.config;
+
+import javax.xml.transform.dom.DOMSource;
+
+public interface Transformation {
+
+    /**
+     * From type that can be transformed.
+     * 
+     * @return From document type.
+     */
+    String getFromType();
+
+    /**
+     * To document type.
+     * 
+     * @return Document type.
+     */
+    String getToType();
+
+    /**
+     * @param aDocument
+     *            Document to transform.
+     * @return Resulting document.
+     */
+    DOMSource transform(DOMSource aDocument);
+
+}
diff --git a/impl/pom.xml b/impl/pom.xml
new file mode 100644 (file)
index 0000000..a20b1e4
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.wamblee.xmlrouter</groupId>
+        <artifactId>xmlrouter-root</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xmlrouter-impl</artifactId>
+    <packaging>jar</packaging>
+    <name>/xmlrouter/impl</name>
+    <url>http://wamblee.org</url>
+    
+    <dependencies>
+           <dependency>
+                   <groupId>org.wamblee.xmlrouter</groupId>
+                       <artifactId>xmlrouter-config</artifactId>
+                       <version>0.1-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+            <groupId>org.wamblee.xmlrouter</groupId>
+            <artifactId>xmlrouter-publish</artifactId>
+            <version>0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.wamblee.xmlrouter</groupId>
+            <artifactId>xmlrouter-subscribe</artifactId>
+            <version>0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+        </dependency>        
+    </dependencies>
+    
+    <distributionManagement>
+        <site>
+            <id>xmlrouter-site</id>
+            <url>file:${distrib}/cache</url>
+        </site>
+    </distributionManagement>
+
+</project>
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/Constants.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/Constants.java
new file mode 100644 (file)
index 0000000..057c90f
--- /dev/null
@@ -0,0 +1,5 @@
+package org.wamblee.xmlrouter.impl;
+
+public enum Constants {
+    UNKNOWN_DOCUMENT_TYPE, UNKNOWN_DESTINATION_NAME
+}
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/RobustDestination.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/RobustDestination.java
new file mode 100644 (file)
index 0000000..6de2c19
--- /dev/null
@@ -0,0 +1,130 @@
+package org.wamblee.xmlrouter.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.wamblee.xml.XMLDocument;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.subscribe.Destination;
+
+/**
+ * Provides robustness for the use of externally provided destinations by
+ * catching null return values and exceptions and providing usable default
+ * return values.
+ * 
+ * @author Erik Brakkee
+ * 
+ */
+public class RobustDestination implements Destination {
+
+    private static final Logger LOGGER = Logger
+        .getLogger(RobustDestination.class.getName());
+
+    private Id<Destination> id;
+    private Destination destination;
+
+    public RobustDestination(Id<Destination> aId, Destination aDestination) {
+        id = aId;
+        destination = aDestination;
+    }
+
+    @Override
+    public String getName() {
+        try {
+            String res = destination.getName();
+            if (res == null) {
+                logNameReturnedNull();
+                return Constants.UNKNOWN_DESTINATION_NAME.toString();
+            }
+            return res;
+        } catch (Exception e) {
+            logNameThrewException(e);
+            return Constants.UNKNOWN_DESTINATION_NAME.toString();
+        }
+    }
+
+    @Override
+    public Collection<String> chooseFromTargetTypes(
+        Collection<String> aPossibleTargetTypes) {
+        try {
+            Collection<String> res = destination
+                .chooseFromTargetTypes(aPossibleTargetTypes);
+            if (res == null) {
+                logChooseFromTargetTypesReturnedNull(aPossibleTargetTypes);
+                return Collections.EMPTY_LIST;
+            }
+            Collection<String> finalRes = new ArrayList<String>();
+            for (String type : res) {
+                if (!aPossibleTargetTypes.contains(type)) {
+                    logChooseFromTargetTypesReturnedInvalidType(
+                        aPossibleTargetTypes, type);
+                } else {
+                    finalRes.add(type);
+                }
+            }
+            return finalRes;
+        } catch (Exception e) {
+            logChooseFromTargetTypesThrewException(aPossibleTargetTypes, e);
+            return Collections.EMPTY_LIST;
+        }
+    }
+
+    @Override
+    public boolean receive(DOMSource aEvent) {
+        try {
+            return destination.receive(aEvent);
+        } catch (Exception e) {
+            logReceiveThrewException(aEvent, e);
+            return false;
+        }
+    }
+
+    private void logNameThrewException(Exception aE) {
+        LOGGER.log(Level.WARNING,
+            "getName() threw exception for destination id " + id +
+                " returning name " + Constants.UNKNOWN_DESTINATION_NAME +
+                " instead", aE);
+    }
+
+    private void logNameReturnedNull() {
+        LOGGER.log(Level.WARNING,
+            "getName() returned null for destination id " + id +
+                " returning name " + Constants.UNKNOWN_DESTINATION_NAME +
+                " instead");
+    }
+
+    private void logChooseFromTargetTypesReturnedInvalidType(
+        Collection<String> aPossibleTargetTypes, String aType) {
+        LOGGER
+            .log(
+                Level.WARNING,
+                "chooseFromTargetTypes() for destination {0} returned invalid type ''{1}'' which is not in the offered set of types {2}",
+                new Object[] { id, aType, aPossibleTargetTypes.toString() });
+    }
+
+    private void logChooseFromTargetTypesReturnedNull(
+        Collection<String> aPossibleTargetTypes) {
+        LOGGER.log(Level.WARNING,
+            "chooseFromTargetTypes() returned null for destination " + id);
+    }
+
+    private void logChooseFromTargetTypesThrewException(
+        Collection<String> aPossibleTargetTypes, Exception aE) {
+        LOGGER
+            .log(
+                Level.WARNING,
+                "chooseFromTargetTypes() threw exception for destination " + id,
+                aE);
+    }
+
+    private void logReceiveThrewException(DOMSource aEvent, Exception aE) {
+        LOGGER.log(Level.WARNING, "Receive threw exception for destination " +
+            id + " for event " + new XMLDocument(aEvent).print(true), aE);
+    }
+
+}
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/RobustFilter.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/RobustFilter.java
new file mode 100644 (file)
index 0000000..413ae92
--- /dev/null
@@ -0,0 +1,34 @@
+package org.wamblee.xmlrouter.impl;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Filter;
+
+public class RobustFilter implements Filter {
+
+    private static final Logger LOGGER = Logger.getLogger(RobustFilter.class
+        .getName());
+
+    private Id<Filter> id;
+    private Filter filter;
+
+    public RobustFilter(Id<Filter> aId, Filter aFilter) {
+        id = aId;
+        filter = aFilter;
+    }
+
+    @Override
+    public boolean isAllowed(String aDocumentType, DOMSource aSource) {
+        try {
+            return filter.isAllowed(aDocumentType, aSource);
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Filter " + id +
+                " threw exception, assuming filter returns true", e);
+            return true;
+        }
+    }
+}
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/RobustTransformation.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/RobustTransformation.java
new file mode 100644 (file)
index 0000000..fb028ae
--- /dev/null
@@ -0,0 +1,94 @@
+package org.wamblee.xmlrouter.impl;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.wamblee.xml.XMLDocument;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Transformation;
+
+public class RobustTransformation implements Transformation {
+
+    private static final Logger LOGGER = Logger
+        .getLogger(RobustTransformation.class.getName());
+
+    private Id<Transformation> id;
+    private Transformation transformation;
+
+    public RobustTransformation(Id<Transformation> aId,
+        Transformation aTransformation) {
+        id = aId;
+        transformation = aTransformation;
+    }
+
+    @Override
+    public String getFromType() {
+        try {
+            String from = transformation.getFromType();
+            if (from == null) {
+                logTypeReturnedNull("from");
+                return Constants.UNKNOWN_DOCUMENT_TYPE.toString();
+            }
+            return from;
+        } catch (Exception e) {
+            logTypeThrewException("from", e);
+            return Constants.UNKNOWN_DOCUMENT_TYPE.toString();
+        }
+    }
+
+    @Override
+    public String getToType() {
+        try {
+            String to = transformation.getToType();
+            if (to == null) {
+                logTypeReturnedNull("to");
+                return Constants.UNKNOWN_DOCUMENT_TYPE.toString();
+            }
+            return to;
+        } catch (Exception e) {
+            logTypeThrewException("to", e);
+            return Constants.UNKNOWN_DOCUMENT_TYPE.toString();
+        }
+    }
+
+    @Override
+    public DOMSource transform(DOMSource aDocument) {
+        try {
+            DOMSource res = transformation.transform(aDocument);
+            if (res == null) {
+                logTransformationReturnedNull(aDocument);
+                return null;
+            }
+            return res;
+        } catch (Exception e) {
+            logTranformationThrewException(aDocument, e);
+            return null;
+        }
+    }
+
+    private void logTypeThrewException(String aFromTo, Exception aE) {
+        LOGGER.log(Level.WARNING, "get" + aFromTo +
+            " threw exception, returning default value " +
+            Constants.UNKNOWN_DOCUMENT_TYPE, aE);
+    }
+
+    private void logTypeReturnedNull(String aFromTo) {
+        LOGGER.log(Level.WARNING, "get" + aFromTo +
+            " returned null, returning default value " +
+            Constants.UNKNOWN_DOCUMENT_TYPE);
+    }
+
+    private void logTranformationThrewException(DOMSource aEvent, Exception aE) {
+        LOGGER.log(Level.WARNING,
+            "transformation " + id + " threw exception for event " +
+                new XMLDocument(aEvent).print(true), aE);
+    }
+
+    private void logTransformationReturnedNull(DOMSource aEvent) {
+        LOGGER.log(Level.WARNING, "transformation " + id +
+            " returned null for event " + new XMLDocument(aEvent).print(true));
+    }
+
+}
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/TransformationPath.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/TransformationPath.java
new file mode 100644 (file)
index 0000000..c054f90
--- /dev/null
@@ -0,0 +1,79 @@
+package org.wamblee.xmlrouter.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.wamblee.xmlrouter.config.Transformation;
+
+public class TransformationPath {
+
+    private List<Transformation> transformations;
+
+    public TransformationPath() {
+        transformations = new ArrayList<Transformation>();
+    }
+
+    public TransformationPath(Transformation aTransformation) {
+        this();
+        transformations.add(aTransformation);
+    }
+
+    public TransformationPath(List<Transformation> aTransformations) {
+        this();
+        transformations.addAll(aTransformations);
+    }
+
+    public TransformationPath appendPath(TransformationPath aSequence) {
+        if (transformations.isEmpty()) {
+            return new TransformationPath(aSequence.transformations);
+        }
+        if (aSequence.transformations.isEmpty()) {
+            // nothing to append
+            return new TransformationPath(transformations);
+        }
+        // both are non-empty
+        if (!getToType().equals(aSequence.getFromType())) {
+            throw new RuntimeException(
+                "error concatening sequence, destination of first differs from source of second");
+        }
+        List<Transformation> t = new ArrayList<Transformation>(transformations);
+        t.addAll(aSequence.transformations);
+        return new TransformationPath(t);
+    }
+
+    public int size() {
+        return transformations.size();
+    }
+
+    public String getFromType() {
+        if (transformations.isEmpty()) {
+            return null;
+        }
+        return transformations.get(0).getFromType();
+    }
+
+    public String getToType() {
+        if (transformations.isEmpty()) {
+            return null;
+        }
+        return transformations.get(transformations.size() - 1).getToType();
+    }
+
+    public List<Transformation> getTransformations() {
+        return transformations;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("TransformationPath(");
+        for (int i = 0; i < transformations.size(); i++) {
+            if (i > 0) {
+                buf.append(", ");
+            }
+            buf.append(transformations.get(i).toString());
+        }
+        buf.append(")");
+        return buf.toString();
+    }
+}
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/Transformations.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/Transformations.java
new file mode 100644 (file)
index 0000000..07d086b
--- /dev/null
@@ -0,0 +1,159 @@
+package org.wamblee.xmlrouter.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Transformation;
+
+public class Transformations {
+
+    private AtomicInteger sequenceNumber;
+    private Map<Integer, Transformation> transformations;
+    private List<String> vertices;
+    private TransformationPath[][] matrix;
+
+    private Map<String, List<TransformationPath>> sequences;
+
+    public Transformations() {
+        sequenceNumber = new AtomicInteger(1);
+        transformations = new LinkedHashMap<Integer, Transformation>();
+        vertices = new ArrayList<String>();
+        matrix = new TransformationPath[0][0];
+    }
+
+    public Id<Transformation> addTransformation(Transformation aTransformation) {
+        int seqno = sequenceNumber.getAndIncrement();
+        Id<Transformation> id = new Id<Transformation>(seqno);
+        transformations.put(seqno,
+            new RobustTransformation(id, aTransformation));
+        computeTransformationSequences();
+        return id;
+    }
+
+    public Collection<String> getPossibleTargetTypes(String aType) {
+        int index = vertices.indexOf(aType);
+        Set<String> res = new HashSet<String>();
+        for (int j = 0; j < vertices.size(); j++) {
+            if (matrix[index][j] != null) {
+                String value = matrix[index][j].getToType();
+                if (value == null) {
+                    value = aType;
+                }
+                res.add(value);
+            }
+        }
+        res.add(aType);
+        return res;
+    }
+
+    /**
+     * Gets the transformation path from A to B.
+     * 
+     * @param aFrom
+     *            From
+     * @param aTo
+     *            To
+     * @return Transformatkon path or null if not found.
+     */
+    public TransformationPath getPath(String aFrom, String aTo) {
+        int i = vertices.indexOf(aFrom);
+        if (i == -1) {
+            if (aFrom.equals(aTo)) {
+                return new TransformationPath();
+            }
+            return null;
+        }
+
+        int j = vertices.indexOf(aTo);
+        return matrix[i][j];
+    }
+
+    private void computeTransformationSequences() {
+        vertices = new ArrayList<String>();
+
+        // Obtain possible starting points.
+        Set<String> v = new HashSet<String>();
+        for (Transformation transformation : transformations.values()) {
+            v.add(transformation.getFromType());
+            v.add(transformation.getToType());
+        }
+
+        vertices.addAll(v);
+
+        matrix = new TransformationPath[vertices.size()][vertices.size()];
+
+        // Floyd's algorithm.
+        int nvertices = vertices.size();
+        for (int i = 0; i < nvertices; i++) {
+            matrix[i][i] = new TransformationPath();
+        }
+        for (Transformation transformation : transformations.values()) {
+            int from = vertices.indexOf(transformation.getFromType());
+            int to = vertices.indexOf(transformation.getToType());
+            TransformationPath path = new TransformationPath(transformation);
+            matrix[from][to] = path;
+        }
+
+        for (int k = 0; k < nvertices; k++) {
+            for (int i = 0; i < nvertices; i++) {
+                for (int j = 0; j < nvertices; j++) {
+                    // if the path from i to j through k is shorter then the
+                    // existing path then
+                    // replace it.
+                    int lij = getPathLength(i, j);
+                    int lik = getPathLength(i, k);
+                    int lkj = getPathLength(k, j);
+                    if (lik + lkj < lij) {
+                        matrix[i][j] = matrix[i][k].appendPath(matrix[k][j]);
+                    }
+                }
+            }
+        }
+    }
+
+    private int getPathLength(int i, int j) {
+        // We use MAX_INT/3 as infinity. This ensures that the default integer
+        // comparison in Floyd's algorithm does not lead to overflow.
+        return matrix[i][j] == null ? Integer.MAX_VALUE / 3 : matrix[i][j]
+            .size();
+    }
+
+    public void removeTransformation(Id<Transformation> aId) {
+        transformations.remove(aId.getId());
+        computeTransformationSequences();
+    }
+
+    public Collection<Transformation> getTransformations() {
+        return Collections.unmodifiableCollection(transformations.values());
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("Transformations(");
+        int nvertices = vertices.size();
+        for (int i = 0; i < nvertices; i++) {
+            for (int j = 0; j < nvertices; j++) {
+                TransformationPath path = matrix[i][j];
+                if (path != null) {
+                    buf.append(vertices.get(i));
+                    buf.append(" -> ");
+                    buf.append(vertices.get(j));
+                    buf.append(": ");
+                    buf.append(path);
+                    buf.append("\n");
+                }
+            }
+        }
+        buf.append(")");
+        return buf.toString();
+    }
+}
\ No newline at end of file
diff --git a/impl/src/main/java/org/wamblee/xmlrouter/impl/XMLRouter.java b/impl/src/main/java/org/wamblee/xmlrouter/impl/XMLRouter.java
new file mode 100644 (file)
index 0000000..85a51df
--- /dev/null
@@ -0,0 +1,265 @@
+package org.wamblee.xmlrouter.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.wamblee.xml.XMLDocument;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Config;
+import org.wamblee.xmlrouter.config.DocumentType;
+import org.wamblee.xmlrouter.config.Filter;
+import org.wamblee.xmlrouter.config.Transformation;
+import org.wamblee.xmlrouter.publish.Gateway;
+import org.wamblee.xmlrouter.subscribe.Destination;
+import org.wamblee.xmlrouter.subscribe.DestinationRegistry;
+
+// TODO concurrency.
+
+public class XMLRouter implements Config, Gateway, DestinationRegistry {
+
+    private static final Logger LOGGER = Logger.getLogger(XMLRouter.class
+        .getName());
+
+    private AtomicInteger sequenceNumbers;
+    private Map<Integer, DocumentType> documentTypes;
+    private Transformations transformations;
+    private Map<Integer, Filter> filters;
+    private Map<Integer, Destination> destinations;
+
+    public XMLRouter() {
+        sequenceNumbers = new AtomicInteger(1);
+        documentTypes = new LinkedHashMap<Integer, DocumentType>();
+        transformations = new Transformations();
+        filters = new LinkedHashMap<Integer, Filter>();
+        destinations = new LinkedHashMap<Integer, Destination>();
+    }
+
+    @Override
+    public Id<DocumentType> addDocumentType(DocumentType aType) {
+        int seqno = sequenceNumbers.getAndIncrement();
+        documentTypes.put(seqno, aType);
+        return new Id<DocumentType>(seqno);
+    }
+
+    @Override
+    public void removeDocumentType(Id<DocumentType> aId) {
+        documentTypes.remove(aId);
+    }
+
+    @Override
+    public Collection<DocumentType> getDocumentTypes() {
+        return Collections.unmodifiableCollection(documentTypes.values());
+    }
+
+    @Override
+    public Id<Transformation> addTransformation(Transformation aTransformation) {
+        return transformations.addTransformation(aTransformation);
+    }
+
+    @Override
+    public void removeTransformation(Id<Transformation> aId) {
+        transformations.removeTransformation(aId);
+    }
+
+    @Override
+    public Collection<Transformation> getTransformations() {
+        return transformations.getTransformations();
+    }
+
+    @Override
+    public Id<Filter> addFilter(Filter aFilter) {
+        int seqno = sequenceNumbers.getAndIncrement();
+        filters.put(seqno, aFilter);
+        return new Id<Filter>(seqno);
+    }
+
+    @Override
+    public void removeFilter(Id<Filter> aId) {
+        filters.remove(aId);
+    }
+
+    @Override
+    public Collection<Filter> getFilters() {
+        return Collections.unmodifiableCollection(filters.values());
+    }
+
+    @Override
+    public boolean publish(String aSource, DOMSource aEvent) {
+
+        boolean delivered = false;
+        try {
+
+            List<String> filteredInputTypes = determineFilteredInputTypes(aEvent);
+            if (filteredInputTypes.isEmpty()) {
+                if (LOGGER.isLoggable(Level.FINE)) {
+                    String doc = new XMLDocument(aEvent).print(true);
+                    LOGGER
+                        .log(
+                            Level.FINE,
+                            "Event ''0}'' from source {1} removed because of filters.",
+                            new Object[] { doc, aSource });
+                }
+                return delivered;
+            }
+
+            // get the reachable target types through transformations.
+
+            // It is possible that a given event belongs to multiple input
+            // types.
+            // This is however certainly not the main case.
+
+            for (String inputType : filteredInputTypes) {
+                boolean result = deliverEvent(aSource, aEvent, inputType);
+                delivered = delivered || result;
+            }
+        } finally {
+            if (!delivered) {
+                destinationNotFound(aSource, aEvent);
+            }
+            return delivered;
+        }
+    }
+
+    private boolean deliverEvent(String aSource, DOMSource aEvent,
+        String aInputType) {
+
+        boolean delivered = false;
+        Set<String> possibleTargetTypes = new HashSet<String>();
+        possibleTargetTypes.addAll(transformations
+            .getPossibleTargetTypes(aInputType));
+
+        // ask each destination what target types, if any they want to have.
+        for (Destination destination : destinations.values()) {
+            Collection<String> requested = destination
+                .chooseFromTargetTypes(possibleTargetTypes);
+            if (!requested.isEmpty()) {
+                // Deliver to the destination.
+                for (String targetType : requested) {
+                    TransformationPath path = transformations.getPath(
+                        aInputType, targetType);
+                    List<Transformation> ts = path.getTransformations();
+                    int i = 0;
+                    boolean allowed = true;
+                    DOMSource transformed = aEvent;
+                    while (i < ts.size() && allowed && transformed != null) {
+                        Transformation t = ts.get(i);
+                        DOMSource orig = transformed;
+                        transformed = t.transform(transformed);
+                        if (transformed == null) {
+                            transformationReturnedNull(aSource, aEvent,
+                                aInputType, t, orig);
+                        }
+
+                        if (!isAllowedByFilters(t.getToType(), transformed)) {
+                            allowed = false;
+                        }
+                        i++;
+                    }
+                    if (allowed && transformed != null) {
+                        // all transformations done and all filters still
+                        // allow the event.
+                        boolean result = destination.receive(transformed);
+                        delivered = delivered || result;
+
+                    }
+                }
+            }
+        }
+        return delivered;
+    }
+
+    private List<String> determineFilteredInputTypes(DOMSource aEvent) {
+        List<String> types = determineDocumentTypes(aEvent);
+        // apply filters to the input
+        List<String> filteredTypes = new ArrayList<String>();
+        for (String type : types) {
+            boolean allowed = isAllowedByFilters(type, aEvent);
+            if (allowed) {
+                filteredTypes.add(type);
+            }
+        }
+        return filteredTypes;
+    }
+
+    private boolean isAllowedByFilters(String aType, DOMSource aEvent) {
+        boolean allowed = true;
+        for (Filter filter : filters.values()) {
+            if (!filter.isAllowed(aType, aEvent)) {
+                allowed = false;
+            }
+        }
+        return allowed;
+    }
+
+    private List<String> determineDocumentTypes(DOMSource aEvent) {
+        List<String> res = new ArrayList<String>();
+        for (DocumentType type : documentTypes.values()) {
+            if (type.isInstance(aEvent)) {
+                res.add(type.getName());
+            }
+        }
+        return res;
+    }
+
+    private void logEvent(String aMessage, String aSource, DOMSource aEvent,
+        Exception aException) {
+        LOGGER.log(Level.WARNING, aMessage + ": source '" + aSource +
+            "': Event: '" + new XMLDocument(aEvent).print(true) + "'",
+            aException);
+    }
+
+    private void logEvent(String aMessage, String aSource, DOMSource aEvent) {
+        LOGGER.log(Level.WARNING,
+            aMessage + ": " + eventToString(aSource, aEvent));
+    }
+
+    private String eventToString(String aSource, DOMSource aEvent) {
+        return "source '" + aSource + "': Event: '" +
+            new XMLDocument(aEvent).print(true) + "'";
+    }
+
+    private void transformationReturnedNull(String aSource, DOMSource aEvent,
+        String aInputType, Transformation aT, DOMSource aTransformed) {
+        LOGGER.log(Level.WARNING, "Transformation returned null for event " +
+            eventToString(aSource, aEvent) + " inputType '" + aInputType +
+            "', transformation '" + aT + "' document to transform " +
+            new XMLDocument(aTransformed).print(true));
+    }
+
+    private void destinationNotFound(String aSource, DOMSource aEvent) {
+        LOGGER.log(Level.WARNING, "No destination found for event: " +
+            eventToString(aSource, aEvent));
+    }
+
+    @Override
+    public Id<Destination> registerDestination(Destination aDestination) {
+        notNull("destination", aDestination);
+        int seqno = sequenceNumbers.getAndIncrement();
+        Id<Destination> id = new Id<Destination>(seqno);
+        destinations.put(seqno, new RobustDestination(id, aDestination));
+        return id;
+    }
+
+    @Override
+    public void unregisterDestination(Id<Destination> aId) {
+        destinations.remove(aId.getId());
+    }
+
+    private void notNull(String aName, Object aValue) {
+        if (aValue == null) {
+            throw new IllegalArgumentException("Parameter '" + aName +
+                "' may not be null");
+        }
+    }
+}
diff --git a/impl/src/test/java/org/wamblee/xmlrouter/impl/RobustDestinationTest.java b/impl/src/test/java/org/wamblee/xmlrouter/impl/RobustDestinationTest.java
new file mode 100644 (file)
index 0000000..397f7b2
--- /dev/null
@@ -0,0 +1,96 @@
+package org.wamblee.xmlrouter.impl;
+
+import static junit.framework.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.subscribe.Destination;
+
+public class RobustDestinationTest {
+
+    private Destination destination;
+    private Destination robust;
+    private DOMSource source;
+
+    @Before
+    public void setUp() {
+        destination = mock(Destination.class);
+        robust = new RobustDestination(new Id<Destination>(100), destination);
+        source = mock(DOMSource.class);
+    }
+
+    @Test
+    public void testNormalFlow() {
+        when(destination.getName()).thenReturn("name");
+        when(
+            destination.chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(Arrays.asList("x"));
+
+        assertEquals("name", robust.getName());
+        assertEquals(Arrays.asList("x"),
+            robust.chooseFromTargetTypes(Arrays.asList("x", "y")));
+        when(destination.receive(same(source))).thenReturn(true);
+        assertTrue(robust.receive(source));
+        verify(destination).receive(same(source));
+
+        when(destination.receive(same(source))).thenReturn(false);
+        assertFalse(robust.receive(source));
+
+    }
+
+    @Test
+    public void testNameReturnsNull() {
+        when(destination.getName()).thenReturn(null);
+        assertEquals(Constants.UNKNOWN_DESTINATION_NAME.toString(),
+            robust.getName());
+    }
+
+    @Test
+    public void testNameThrowsException() {
+        doThrow(new RuntimeException("x")).when(destination).getName();
+        assertEquals(Constants.UNKNOWN_DESTINATION_NAME.toString(),
+            robust.getName());
+    }
+
+    @Test
+    public void testChooseFromTargetTypesReturnsNull() {
+        when(
+            destination.chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(null);
+        assertEquals(Collections.EMPTY_LIST,
+            robust.chooseFromTargetTypes(Arrays.asList("x")));
+    }
+
+    @Test
+    public void testChooseFromTargetTypesThrowsException() {
+        doThrow(new RuntimeException("x")).when(destination)
+            .chooseFromTargetTypes(anyList());
+        assertEquals(Collections.EMPTY_LIST,
+            robust.chooseFromTargetTypes(Arrays.asList("x")));
+    }
+
+    @Test
+    public void testChooseFromTargetTypesReturnsInvalidType() {
+        when(
+            destination.chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(Arrays.asList("x", "y"));
+        assertEquals(Arrays.asList("y"),
+            robust.chooseFromTargetTypes(Arrays.asList("y", "z")));
+    }
+
+    @Test
+    public void testReceiveThrowsException() {
+        doThrow(new RuntimeException("x")).when(destination).receive(
+            any(DOMSource.class));
+        assertEquals(false, robust.receive(source));
+    }
+}
diff --git a/impl/src/test/java/org/wamblee/xmlrouter/impl/RobustFilterTest.java b/impl/src/test/java/org/wamblee/xmlrouter/impl/RobustFilterTest.java
new file mode 100644 (file)
index 0000000..7c10282
--- /dev/null
@@ -0,0 +1,49 @@
+package org.wamblee.xmlrouter.impl;
+
+import static junit.framework.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Filter;
+
+public class RobustFilterTest {
+
+    private Filter filter;
+    private Filter robust;
+    private DOMSource source;
+
+    @Before
+    public void setUp() {
+        filter = mock(Filter.class);
+        robust = new RobustFilter(new Id<Filter>(10), filter);
+        source = mock(DOMSource.class);
+    }
+
+    @Test
+    public void testNoEzception() {
+        when(filter.isAllowed(anyString(), any(DOMSource.class))).thenReturn(
+            true);
+        assertTrue(robust.isAllowed("xx", source));
+        verify(filter).isAllowed(eq("xx"), same(source));
+
+        reset(filter);
+        when(filter.isAllowed(anyString(), any(DOMSource.class))).thenReturn(
+            false);
+        assertFalse(robust.isAllowed("xx", source));
+        verify(filter).isAllowed(eq("xx"), same(source));
+    }
+
+    @Test
+    public void testException() {
+        doThrow(new RuntimeException("bla")).when(filter).isAllowed(
+            anyString(), any(DOMSource.class));
+        assertTrue(robust.isAllowed("xx", source));
+        verify(filter).isAllowed(eq("xx"), same(source));
+
+    }
+}
diff --git a/impl/src/test/java/org/wamblee/xmlrouter/impl/RobustTransformationTest.java b/impl/src/test/java/org/wamblee/xmlrouter/impl/RobustTransformationTest.java
new file mode 100644 (file)
index 0000000..ea76c54
--- /dev/null
@@ -0,0 +1,82 @@
+package org.wamblee.xmlrouter.impl;
+
+import static junit.framework.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Transformation;
+
+public class RobustTransformationTest {
+    private Transformation transformation;
+    private Transformation robust;
+    private DOMSource source;
+    private DOMSource resSource;
+
+    @Before
+    public void setUp() {
+        transformation = mock(Transformation.class);
+        robust = new RobustTransformation(new Id<Transformation>(100),
+            transformation);
+        source = mock(DOMSource.class);
+        resSource = mock(DOMSource.class);
+    }
+
+    @Test
+    public void testNormalFlow() {
+        when(transformation.getFromType()).thenReturn("from");
+        when(transformation.getToType()).thenReturn("to");
+
+        when(transformation.transform(same(source))).thenReturn(resSource);
+
+        assertEquals("from", robust.getFromType());
+        assertEquals("to", robust.getToType());
+
+        assertSame(resSource, robust.transform(source));
+    }
+
+    @Test
+    public void testFromReturnsNull() {
+        when(transformation.getFromType()).thenReturn(null);
+        assertEquals(Constants.UNKNOWN_DOCUMENT_TYPE.toString(),
+            robust.getFromType());
+    }
+
+    @Test
+    public void testToReturnsNull() {
+        when(transformation.getToType()).thenReturn(null);
+        assertEquals(Constants.UNKNOWN_DOCUMENT_TYPE.toString(),
+            robust.getToType());
+    }
+
+    @Test
+    public void testFromThrowsException() {
+        doThrow(new RuntimeException("x")).when(transformation).getFromType();
+        assertEquals(Constants.UNKNOWN_DOCUMENT_TYPE.toString(),
+            robust.getFromType());
+    }
+
+    @Test
+    public void testToThrowsException() {
+        doThrow(new RuntimeException("x")).when(transformation).getFromType();
+        assertEquals(Constants.UNKNOWN_DOCUMENT_TYPE.toString(),
+            robust.getFromType());
+    }
+
+    @Test
+    public void testTransformReturnsNull() {
+        when(transformation.transform(any(DOMSource.class))).thenReturn(null);
+        assertEquals(null, robust.transform(source));
+    }
+
+    @Test
+    public void testTransformationThrowsException() {
+        doThrow(new RuntimeException("x")).when(transformation).transform(
+            any(DOMSource.class));
+        assertEquals(null, robust.transform(source));
+    }
+}
diff --git a/impl/src/test/java/org/wamblee/xmlrouter/impl/TransformationsTest.java b/impl/src/test/java/org/wamblee/xmlrouter/impl/TransformationsTest.java
new file mode 100644 (file)
index 0000000..8489f89
--- /dev/null
@@ -0,0 +1,102 @@
+package org.wamblee.xmlrouter.impl;
+
+import static junit.framework.Assert.*;
+
+import java.util.Collection;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.Transformation;
+
+public class TransformationsTest {
+
+    public static class MyTransformation implements Transformation {
+
+        private String from;
+        private String to;
+
+        public MyTransformation(String aFrom, String aTo) {
+            from = aFrom;
+            to = aTo;
+        }
+
+        @Override
+        public String getFromType() {
+            return from;
+        }
+
+        @Override
+        public String getToType() {
+            return to;
+        }
+
+        @Override
+        public DOMSource transform(DOMSource aDocument) {
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return from + "->" + to;
+        }
+    }
+
+    private Transformations transformations;
+
+    @Before
+    public void setUp() {
+        transformations = new Transformations();
+    }
+
+    @Test
+    public void testOneTransformation() {
+        Id<Transformation> seqno = transformations
+            .addTransformation(new MyTransformation("A", "B"));
+        System.out.println(transformations.toString());
+        TransformationPath path = transformations.getPath("A", "B");
+        assertEquals(1, path.size());
+        assertNull(transformations.getPath("B", "A"));
+
+        assertEquals(0, transformations.getPath("A", "A").size());
+        assertEquals(0, transformations.getPath("B", "B").size());
+
+        Collection<String> possibleTargets = transformations
+            .getPossibleTargetTypes("A");
+        assertEquals(2, possibleTargets.size());
+        assertTrue(possibleTargets.contains("A"));
+        assertTrue(possibleTargets.contains("B"));
+    }
+
+    @Test
+    public void testMultipleTransformations() {
+        Id<Transformation> seqno1 = transformations
+            .addTransformation(new MyTransformation("A", "B"));
+        Id<Transformation> seqno2 = transformations
+            .addTransformation(new MyTransformation("B", "C"));
+        Id<Transformation> seqno3 = transformations
+            .addTransformation(new MyTransformation("C", "A"));
+        System.out.println(transformations);
+        assertEquals(2, transformations.getPath("C", "B").size());
+        assertFalse(seqno1.equals(seqno2));
+        assertFalse(seqno2.equals(seqno3));
+        assertFalse(seqno1.equals(seqno3));
+
+        transformations.removeTransformation(seqno1);
+        assertNull(transformations.getPath("C", "B"));
+    }
+
+    @Test
+    public void testWithoutTransformations() {
+        Collection<String> res = transformations.getPossibleTargetTypes("a");
+        assertEquals(1, res.size());
+
+        TransformationPath path = transformations.getPath("A", "A");
+        assertNotNull(path);
+        assertEquals(0, path.size());
+
+        assertNull(transformations.getPath("B", "C"));
+    }
+}
diff --git a/impl/src/test/java/org/wamblee/xmlrouter/impl/XMLRouterTest.java b/impl/src/test/java/org/wamblee/xmlrouter/impl/XMLRouterTest.java
new file mode 100644 (file)
index 0000000..c4a73ba
--- /dev/null
@@ -0,0 +1,318 @@
+package org.wamblee.xmlrouter.impl;
+
+import static junit.framework.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.xml.transform.dom.DOMSource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wamblee.xmlrouter.common.Id;
+import org.wamblee.xmlrouter.config.DocumentType;
+import org.wamblee.xmlrouter.config.Transformation;
+import org.wamblee.xmlrouter.subscribe.Destination;
+
+public class XMLRouterTest {
+
+    public static class MyDestination implements Destination {
+
+        private boolean receiveResult;
+        private Collection<String> types;
+
+        public MyDestination(boolean aReceiveResult, Collection<String> aTypes) {
+            receiveResult = aReceiveResult;
+            types = aTypes;
+        }
+
+        @Override
+        public Collection<String> chooseFromTargetTypes(
+            Collection<String> aPossibleTargetTypes) {
+            return types;
+        }
+
+        @Override
+        public String getName() {
+            return "xxx";
+        }
+
+        @Override
+        public boolean receive(DOMSource aEvent) {
+            return receiveResult;
+        }
+    }
+
+    private XMLRouter router;
+    private DOMSource source1;
+    private DOMSource source2;
+    private DOMSource source3;
+
+    private Destination destinationSpy;
+    private Id<Destination> destinationId;
+
+    @Before
+    public void setUp() {
+        router = new XMLRouter();
+        source1 = mock(DOMSource.class);
+        source2 = mock(DOMSource.class);
+        source3 = mock(DOMSource.class);
+    }
+
+    @Test
+    public void testNoInputDocumentsRegistered() {
+        Destination destination = new MyDestination(true, Arrays.asList("any"));
+        destinationSpy = spy(destination);
+
+        destinationId = router.registerDestination(destinationSpy);
+        assertFalse(router.publish("any", source1));
+    }
+
+    @Test
+    public void testOneDestinationNoTransformationSuccess() {
+        destinationSpy = registerDestination(true, "any");
+        registerDocumentType("any");
+
+        assertTrue(router.publish("any", source1));
+        verify(destinationSpy).receive(same(source1));
+
+        // Unregister the destination.
+        router.unregisterDestination(destinationId);
+        reset(destinationSpy);
+        assertFalse(router.publish("any", source2));
+        verifyNoMoreInteractions(destinationSpy);
+    }
+
+    private void registerDocumentType(String aType) {
+        DocumentType type = mock(DocumentType.class);
+        when(type.isInstance(any(DOMSource.class))).thenReturn(true);
+        when(type.getName()).thenReturn(aType);
+        Id<DocumentType> typeId = router.addDocumentType(type);
+    }
+
+    private void registerDocumentType(String aType, DOMSource aSource) {
+        DocumentType type = mock(DocumentType.class);
+        when(type.isInstance(same(aSource))).thenReturn(true);
+        when(type.getName()).thenReturn(aType);
+        Id<DocumentType> typeId = router.addDocumentType(type);
+    }
+
+    private Destination registerDestination(boolean aResult, String... types) {
+        Destination destination = new MyDestination(aResult,
+            Arrays.asList(types));
+        Destination myspy = spy(destination);
+        destinationId = router.registerDestination(myspy);
+        return myspy;
+    }
+
+    @Test
+    public void testOneDestinationNotMatches() {
+        destinationSpy = registerDestination(true);
+        registerDocumentType("any");
+
+        assertFalse(router.publish("any", source1));
+        verify(destinationSpy, never()).receive(any(DOMSource.class));
+    }
+
+    @Test
+    public void testOneDestinationThrowsException() {
+        destinationSpy = registerDestination(true, "any");
+        registerDocumentType("any");
+
+        doThrow(new RuntimeException()).when(destinationSpy).receive(
+            any(DOMSource.class));
+
+        assertFalse(router.publish("any", source1));
+        verify(destinationSpy).receive(same(source1));
+    }
+
+    @Test
+    public void testOneDestinationThrowsExceptionSecondDestinationStillHandled() {
+        testOneDestinationThrowsException();
+        Destination destination2 = new MyDestination(true, Arrays.asList("any"));
+        Destination destinationSpy2 = spy(destination2);
+        Id<Destination> destinationId2 = router
+            .registerDestination(destinationSpy2);
+
+        assertTrue(router.publish("any", source1));
+        verify(destinationSpy2).receive(same(source1));
+    }
+
+    @Test
+    public void testDestinationChooseFromTargetTypesThrowsException() {
+        destinationSpy = registerDestination(true, "any");
+        registerDocumentType("any");
+
+        doThrow(new RuntimeException()).when(destinationSpy)
+            .chooseFromTargetTypes((Collection<String>) anyObject());
+
+        assertFalse(router.publish("any", source1));
+        verify(destinationSpy, never()).receive(same(source1));
+    }
+
+    @Test
+    public void testDestinationChooseFromTargetTypesThrowsExceptionSecondDestinationStillOk() {
+        testDestinationChooseFromTargetTypesThrowsException();
+
+        Destination destination2 = new MyDestination(true, Arrays.asList("any"));
+        Destination destinationSpy2 = spy(destination2);
+        Id<Destination> destinationId2 = router
+            .registerDestination(destinationSpy2);
+        assertTrue(router.publish("any", source1));
+        verify(destinationSpy, never()).receive(same(source1));
+        verify(destinationSpy2).receive(same(source1));
+    }
+
+    @Test
+    public void testDestinationChooseFromTargetTypesReturnsNull() {
+        destinationSpy = registerDestination(true, "any");
+        registerDocumentType("any");
+
+        when(
+            destinationSpy
+                .chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(null);
+
+        assertFalse(router.publish("any", source1));
+        verify(destinationSpy, never()).receive(same(source1));
+    }
+
+    @Test
+    public void testDestinationChooseFromTargetTypesReturnsNullSecondDestinationStillOk() {
+        testDestinationChooseFromTargetTypesReturnsNull();
+
+        Destination destination2 = new MyDestination(true, Arrays.asList("any"));
+        Destination destinationSpy2 = spy(destination2);
+        Id<Destination> destinationId2 = router
+            .registerDestination(destinationSpy2);
+        assertTrue(router.publish("any", source1));
+        verify(destinationSpy, never()).receive(same(source1));
+        verify(destinationSpy2).receive(same(source1));
+    }
+
+    @Test
+    public void testOneTransformationOneDestination() {
+        registerDocumentType("any");
+        Transformation transformation = mock(Transformation.class);
+        when(transformation.getFromType()).thenReturn("any");
+        when(transformation.getToType()).thenReturn("bla");
+        when(transformation.transform(same(source1))).thenReturn(source2);
+        router.addTransformation(transformation);
+
+        Destination destination = mock(Destination.class);
+        when(
+            destination.chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(Arrays.asList("bla"));
+
+        router.registerDestination(destination);
+
+        when(destination.receive(any(DOMSource.class))).thenReturn(true);
+        assertTrue(router.publish("bla", source1));
+
+        verify(transformation).transform(source1);
+        verify(destination).receive(same(source2));
+
+        // now the same when the destination rejects the event.
+        when(destination.receive(any(DOMSource.class))).thenReturn(false);
+        assertFalse(router.publish("bla", source1));
+    }
+
+    private Transformation createTransformation(String aFrom, String aTo,
+        DOMSource aSource, DOMSource aTarget) {
+        Transformation transformation = mock(Transformation.class);
+        when(transformation.getFromType()).thenReturn(aFrom);
+        when(transformation.getToType()).thenReturn(aTo);
+        when(transformation.transform(same(aSource))).thenReturn(aTarget);
+        return transformation;
+    }
+
+    @Test
+    public void testOneTransformationReturnsNull() {
+        registerDocumentType("any");
+        Transformation transformation = createTransformation("any", "bla",
+            source1, null);
+
+        router.addTransformation(transformation);
+
+        Destination destination = mock(Destination.class);
+        when(
+            destination.chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(Arrays.asList("bla"));
+        router.registerDestination(destination);
+
+        assertFalse(router.publish("bla", source1));
+
+        verify(transformation).transform(source1);
+        verify(destination, never()).receive(any(DOMSource.class));
+
+        // add second transformation that behaves normally
+        Transformation transformation2 = createTransformation("any", "bla2",
+            source1, source2);
+
+        router.addTransformation(transformation2);
+        when(
+            destination.chooseFromTargetTypes((Collection<String>) anyObject()))
+            .thenReturn(Arrays.asList("bla", "bla2"));
+
+        reset(transformation);
+        when(transformation.getFromType()).thenReturn("any");
+        when(transformation.getToType()).thenReturn("bla");
+        when(transformation.transform(same(source1))).thenReturn(null);
+
+        when(destination.receive(any(DOMSource.class))).thenReturn(true);
+        assertTrue(router.publish("bla", source1));
+
+        verify(transformation).transform(source1);
+        verify(transformation2).transform(source1);
+
+        verify(destination).receive(same(source2));
+
+    }
+
+    @Test
+    public void testChooseMultipleDestinationsOneType() {
+        Destination dest1 = registerDestination(true, "any");
+        Destination dest2 = registerDestination(true, "any");
+        registerDocumentType("any");
+
+        assertTrue(router.publish("source", source1));
+
+        verify(dest1).receive(same(source1));
+        verify(dest2).receive(same(source1));
+    }
+
+    @Test
+    public void testMultipleDeliveryToOneDestination() {
+        Destination dest = registerDestination(true, "any", "other");
+        registerDocumentType("any", source1);
+        registerDocumentType("other", source2);
+        Transformation transformation = createTransformation("any", "other",
+            source1, source2);
+        router.addTransformation(transformation);
+
+        assertTrue(router.publish("source", source1));
+
+        verify(dest).receive(same(source1));
+        verify(dest).receive(same(source2));
+    }
+
+    @Test
+    public void testMultipleTransformations() {
+        Destination dest = registerDestination(true, "any", "other");
+        registerDocumentType("any", source1);
+        registerDocumentType("other", source3);
+
+        Transformation t1 = createTransformation("any", "intermediate",
+            source1, source2);
+        router.addTransformation(t1);
+        Transformation t2 = createTransformation("intermediate", "other",
+            source2, source3);
+        router.addTransformation(t2);
+
+        assertTrue(router.publish("source", source1));
+
+        verify(dest).receive(same(source3));
+    }
+}
diff --git a/publish/pom.xml b/publish/pom.xml
new file mode 100644 (file)
index 0000000..9f96ec8
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.wamblee.xmlrouter</groupId>
+        <artifactId>xmlrouter-root</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xmlrouter-publish</artifactId>
+    <packaging>jar</packaging>
+    <name>/xmlrouter/publish</name>
+    <url>http://wamblee.org</url>
+    
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+        </dependency>        
+    </dependencies>
+    
+    <distributionManagement>
+        <site>
+            <id>xmlrouter-site</id>
+            <url>file:${distrib}/cache</url>
+        </site>
+    </distributionManagement>
+
+</project>
diff --git a/publish/src/main/java/org/wamblee/xmlrouter/publish/Gateway.java b/publish/src/main/java/org/wamblee/xmlrouter/publish/Gateway.java
new file mode 100644 (file)
index 0000000..f181a27
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * 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.wamblee.xmlrouter.publish;
+
+import javax.xml.transform.dom.DOMSource;
+
+/**
+ * XML router interface for publishing events.
+ * 
+ * @author Erik Brakkee
+ */
+public interface Gateway {
+
+    /**
+     * Publishes an event.
+     * 
+     * @param aSource
+     *            Source.
+     * @param aEvent
+     *            Event.
+     * @return True iff th event was delivered to at least one destination.
+     */
+    boolean publish(String aSource, DOMSource aEvent);
+}
diff --git a/subscribe/pom.xml b/subscribe/pom.xml
new file mode 100644 (file)
index 0000000..a429e9b
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.wamblee.xmlrouter</groupId>
+        <artifactId>xmlrouter-root</artifactId>
+        <version>0.1-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>xmlrouter-subscribe</artifactId>
+    <packaging>jar</packaging>
+    <name>/xmlrouter/subscribe</name>
+    <url>http://wamblee.org</url>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.wamblee.xmlrouter</groupId>
+            <artifactId>xmlrouter-common</artifactId>
+            <version>0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+        </dependency>        
+    </dependencies>
+    
+    <distributionManagement>
+        <site>
+            <id>xmlrouter-site</id>
+            <url>file:${distrib}/cache</url>
+        </site>
+    </distributionManagement>
+
+</project>
diff --git a/subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/Destination.java b/subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/Destination.java
new file mode 100644 (file)
index 0000000..a0c7b94
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * 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.wamblee.xmlrouter.subscribe;
+
+import java.util.Collection;
+
+import javax.xml.transform.dom.DOMSource;
+
+public interface Destination {
+
+    String getName();
+
+    Collection<String> chooseFromTargetTypes(
+        Collection<String> aPossibleTargetTypes);
+
+    boolean receive(DOMSource aEvent);
+}
diff --git a/subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/DestinationRegistry.java b/subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/DestinationRegistry.java
new file mode 100644 (file)
index 0000000..87f9265
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * 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.wamblee.xmlrouter.subscribe;
+
+import org.wamblee.xmlrouter.common.Id;
+
+public interface DestinationRegistry {
+
+    /**
+     * @param aDestination
+     *            Destination.
+     * @return Registration id.
+     * @throws RegistrationException
+     *             In case registration fails.
+     */
+    Id<Destination> registerDestination(Destination aDestination);
+
+    /**
+     * 
+     * @param aRegistration
+     *            Registration id.
+     * @throws RegistrationException
+     *             In case the unregistration could not be done.
+     */
+    void unregisterDestination(Id<Destination> aRegistration);
+}
diff --git a/subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/RegistrationException.java b/subscribe/src/main/java/org/wamblee/xmlrouter/subscribe/RegistrationException.java
new file mode 100644 (file)
index 0000000..7b57e8b
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2005-2011 the original author or authors.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * 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.wamblee.xmlrouter.subscribe;
+
+
+public class RegistrationException extends RuntimeException {
+
+    public RegistrationException(String aMsg) { 
+        super(aMsg);
+    }
+    
+    public RegistrationException(String aMsg, Throwable aCause) { 
+        super(aMsg, aCause);
+    }
+}