You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ay...@apache.org on 2014/05/27 18:26:53 UTC

[2/2] git commit: CAMEL-7468: Make xmlTokenizer more xml-aware so that it can handle more flexible structures

CAMEL-7468: Make xmlTokenizer more xml-aware so that it can handle more flexible structures


Project: http://git-wip-us.apache.org/repos/asf/camel/repo
Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/de56e0bd
Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/de56e0bd
Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/de56e0bd

Branch: refs/heads/master
Commit: de56e0bddfaa007e6c224ed5a7e5abb085a95cc6
Parents: db80b93
Author: Akitoshi Yoshida <ay...@apache.org>
Authored: Tue May 27 18:25:09 2014 +0200
Committer: Akitoshi Yoshida <ay...@apache.org>
Committed: Tue May 27 18:26:06 2014 +0200

----------------------------------------------------------------------
 camel-core/pom.xml                              |   8 +
 .../apache/camel/builder/ExpressionBuilder.java |   9 +-
 .../apache/camel/builder/ExpressionClause.java  |   8 +
 .../camel/builder/ExpressionClauseSupport.java  |  13 +
 .../language/tokenizer/XMLTokenizeLanguage.java | 129 ++++++
 .../model/language/XMLTokenizerExpression.java  |  96 ++++
 .../camel/support/RecordableInputStream.java    |  87 ++++
 .../support/TokenXMLExpressionIterator.java     |  42 --
 .../support/XMLTokenExpressionIterator.java     | 460 +++++++++++++++++++
 .../org/apache/camel/language/xtokenize         |  18 +
 .../tokenizer/XMLTokenizeLanguageTest.java      | 123 +++++
 .../tokenizer/XMLTokenizeWrapLanguageTest.java  | 133 ++++++
 .../support/XMLTokenExpressionIteratorTest.java | 228 +++++++++
 13 files changed, 1311 insertions(+), 43 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/pom.xml
----------------------------------------------------------------------
diff --git a/camel-core/pom.xml b/camel-core/pom.xml
index f968cc6..905b5fe 100644
--- a/camel-core/pom.xml
+++ b/camel-core/pom.xml
@@ -169,6 +169,14 @@
       <scope>test</scope>
     </dependency>
 
+    <!-- xmltokenizer using woodstox -->
+    <dependency>
+      <groupId>org.codehaus.woodstox</groupId>
+      <artifactId>woodstox-core-asl</artifactId>
+      <version>${woodstox-version}</version>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
 

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/builder/ExpressionBuilder.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/builder/ExpressionBuilder.java b/camel-core/src/main/java/org/apache/camel/builder/ExpressionBuilder.java
index ea5c97d..8af5f57 100644
--- a/camel-core/src/main/java/org/apache/camel/builder/ExpressionBuilder.java
+++ b/camel-core/src/main/java/org/apache/camel/builder/ExpressionBuilder.java
@@ -25,6 +25,7 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Scanner;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
@@ -49,6 +50,7 @@ import org.apache.camel.spi.UnitOfWork;
 import org.apache.camel.support.ExpressionAdapter;
 import org.apache.camel.support.TokenPairExpressionIterator;
 import org.apache.camel.support.TokenXMLExpressionIterator;
+import org.apache.camel.support.XMLTokenExpressionIterator;
 import org.apache.camel.util.ExchangeHelper;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.GroupIterator;
@@ -1227,10 +1229,15 @@ public final class ExpressionBuilder {
                 inheritNamespaceTagName = inheritNamespaceTagName + ">";
             }
         }
-
         return new TokenXMLExpressionIterator(tagName, inheritNamespaceTagName);
     }
 
+    public static Expression tokenizeXMLAwareExpression(String path, boolean wrap) {
+        ObjectHelper.notEmpty(path, "path");
+
+        return new XMLTokenExpressionIterator(path, wrap);
+    }
+
     /**
      * Returns a tokenize expression which will tokenize the string with the
      * given regex

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/builder/ExpressionClause.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/builder/ExpressionClause.java b/camel-core/src/main/java/org/apache/camel/builder/ExpressionClause.java
index 787ff78..8830961 100644
--- a/camel-core/src/main/java/org/apache/camel/builder/ExpressionClause.java
+++ b/camel-core/src/main/java/org/apache/camel/builder/ExpressionClause.java
@@ -569,6 +569,14 @@ public class ExpressionClause<T> extends ExpressionDefinition {
         return delegate.tokenizeXMLPair(tagName, inheritNamespaceTagName, group);
     }
 
+    public T tokenizeXML(String path, boolean wrap, Namespaces namespaces) {
+        return tokenizeXML(path, wrap, namespaces, 0);
+    }
+
+    public T tokenizeXML(String path, boolean wrap, Namespaces namespaces, int group) {
+        return delegate.tokenizeXML(path, wrap, namespaces, group);
+    }
+
     /**
      * Evaluates an <a href="http://camel.apache.org/vtdxml.html">XPath
      * expression using the VTD-XML library</a>

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/builder/ExpressionClauseSupport.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/builder/ExpressionClauseSupport.java b/camel-core/src/main/java/org/apache/camel/builder/ExpressionClauseSupport.java
index db3acfd..e647122 100644
--- a/camel-core/src/main/java/org/apache/camel/builder/ExpressionClauseSupport.java
+++ b/camel-core/src/main/java/org/apache/camel/builder/ExpressionClauseSupport.java
@@ -43,6 +43,7 @@ import org.apache.camel.model.language.SpELExpression;
 import org.apache.camel.model.language.SqlExpression;
 import org.apache.camel.model.language.TokenizerExpression;
 import org.apache.camel.model.language.VtdXmlExpression;
+import org.apache.camel.model.language.XMLTokenizerExpression;
 import org.apache.camel.model.language.XPathExpression;
 import org.apache.camel.model.language.XQueryExpression;
 
@@ -603,6 +604,18 @@ public class ExpressionClauseSupport<T> {
         return result;
     }
 
+    public T tokenizeXML(String path, boolean wrap, Namespaces namespaces, int group) {
+        XMLTokenizerExpression expression = new XMLTokenizerExpression(path);
+        expression.setWrap(wrap);
+        expression.setNamespaces(namespaces.getNamespaces());
+
+        if (group > 0) {
+            expression.setGroup(group);
+        }
+        setExpressionType(expression);
+        return result;
+    }
+
     /**
      * Evaluates an <a href="http://camel.apache.org/vtdxml.html">XPath
      * expression using the VTD-XML library</a>

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguage.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguage.java b/camel-core/src/main/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguage.java
new file mode 100644
index 0000000..46b06f7
--- /dev/null
+++ b/camel-core/src/main/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguage.java
@@ -0,0 +1,129 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.language.tokenizer;
+
+import java.util.Map;
+
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.builder.ExpressionBuilder;
+import org.apache.camel.support.LanguageSupport;
+import org.apache.camel.util.ExpressionToPredicateAdapter;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * A language for tokenizer expressions.
+ * <p/>
+ * This xmltokenizer language can operate in the following modes:
+ * <ul>
+ *     <li>wrap - wrapping the extracted token in its ancestor context</li>
+ *     <li>injected - injecting the contextual namespace bindings into the extracted token</li>
+ * </ul>
+ */
+public class XMLTokenizeLanguage extends LanguageSupport {
+
+    private String path;
+    private String headerName;
+    private boolean wrap;
+    private int group;
+
+    public static Expression tokenize(String token) {
+        return tokenize(token, false);
+    }
+
+    public static Expression tokenize(String token, boolean wrap) {
+        XMLTokenizeLanguage language = new XMLTokenizeLanguage();
+        language.setPath(token);
+        language.setWrap(wrap);
+        return language.createExpression(null);
+    }
+
+    public static Expression tokenize(String headerName, String token) {
+        return tokenize(headerName, token, false);
+    }
+
+    public static Expression tokenize(String headerName, String token, boolean wrap) {
+        XMLTokenizeLanguage language = new XMLTokenizeLanguage();
+        language.setHeaderName(headerName);
+        language.setPath(token);
+        language.setWrap(wrap);
+        return language.createExpression(null);
+    }
+
+    public Predicate createPredicate(String expression) {
+        return ExpressionToPredicateAdapter.toPredicate(createExpression(expression));
+    }
+
+    /**
+     * Creates a tokenize expression.
+     */
+    public Expression createExpression() {
+        ObjectHelper.notNull(path, "token");
+
+        Expression answer = ExpressionBuilder.tokenizeXMLAwareExpression(path, wrap);
+
+        // if group then wrap answer in group expression
+        if (group > 0) {
+            //REVISIT wrap the xml tokens with a group element to turn the result into xml?
+            answer = ExpressionBuilder.groupIteratorExpression(answer, null, group);
+        }
+
+        return answer;
+    }
+
+    public Expression createExpression(String expression) {
+        if (ObjectHelper.isNotEmpty(expression)) {
+            this.path = expression;
+        }
+        return createExpression();
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public String getHeaderName() {
+        return headerName;
+    }
+
+    public void setHeaderName(String headerName) {
+        this.headerName = headerName;
+    }
+
+    public boolean isWrap() {
+        return wrap;
+    }
+
+    public void setWrap(boolean wrap) {
+        this.wrap = wrap;
+    }
+    public int getGroup() {
+        return group;
+    }
+
+    public void setGroup(int group) {
+        this.group = group;
+    }
+
+    public boolean isSingleton() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/model/language/XMLTokenizerExpression.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/model/language/XMLTokenizerExpression.java b/camel-core/src/main/java/org/apache/camel/model/language/XMLTokenizerExpression.java
new file mode 100644
index 0000000..a9e36fa
--- /dev/null
+++ b/camel-core/src/main/java/org/apache/camel/model/language/XMLTokenizerExpression.java
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.model.language;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.language.tokenizer.XMLTokenizeLanguage;
+import org.apache.camel.util.ExpressionToPredicateAdapter;
+
+/**
+ * For expressions and predicates using a body or header tokenizer.
+ *
+ * @see XMLTokenizeLanguage
+ */
+@XmlRootElement(name = "xmlTokenize")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class XMLTokenizerExpression extends NamespaceAwareExpression {
+    @XmlAttribute
+    private String headerName;
+    @XmlAttribute
+    private Boolean wrap;
+    @XmlAttribute
+    private Integer group;
+
+    public XMLTokenizerExpression() {
+    }
+
+    public XMLTokenizerExpression(String expression) {
+        super(expression);
+    }
+
+    @Override
+    public String getLanguage() {
+        return "xtokenize";
+    }
+
+    public String getHeaderName() {
+        return headerName;
+    }
+
+    public void setHeaderName(String headerName) {
+        this.headerName = headerName;
+    }
+
+    public void setWrap(boolean wrap) {
+        this.wrap = wrap;
+    }
+
+    public Boolean getWrap() {
+        return wrap;
+    }
+
+    public Integer getGroup() {
+        return group;
+    }
+
+    public void setGroup(Integer group) {
+        this.group = group;
+    }
+
+    @Override
+    protected void configureExpression(CamelContext camelContext, Expression expression) {
+        super.configureExpression(camelContext, expression);
+        if (wrap != null) {
+            setProperty(expression, "wrap", wrap);
+        }
+    }
+
+    @Override
+    protected void configurePredicate(CamelContext camelContext, Predicate predicate) {
+        super.configurePredicate(camelContext, predicate);
+        if (wrap != null) {
+            setProperty(predicate, "wrap", wrap);
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/support/RecordableInputStream.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/support/RecordableInputStream.java b/camel-core/src/main/java/org/apache/camel/support/RecordableInputStream.java
new file mode 100644
index 0000000..1a061d8
--- /dev/null
+++ b/camel-core/src/main/java/org/apache/camel/support/RecordableInputStream.java
@@ -0,0 +1,87 @@
+/**
+ * 
+ */
+package org.apache.camel.support;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * This class is used by the toknizer to extract data while reading from the stream.
+ * REVIST it is used package internally but may be moved to some common package.
+ */
+class RecordableInputStream extends FilterInputStream {
+    private TrimmableByteArrayOutputStream buf;
+    private String charset;
+    private boolean recording;
+    protected RecordableInputStream(InputStream in, String charset) {
+        super(in);
+        this.buf = new TrimmableByteArrayOutputStream();
+        this.charset = charset;
+        this.recording = true;
+    }
+
+    @Override
+    public int read() throws IOException {
+        int c = super.read();
+        if (c > 0 && recording) {
+            buf.write(c);
+        }
+        return c;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        int n = super.read(b, off, len);
+        if (n > 0 && recording) {
+            buf.write(b, off, n);
+        }
+        return n;
+    }
+
+    public String getText(int pos) {
+        String t = null;
+        recording = false;
+        final byte[] ba = buf.toByteArray(pos);
+        buf.trim(pos, 0);
+        try {        
+            if (charset == null) {
+                t = new String(ba);
+            } else {
+                t = new String(ba, charset);
+            }
+        } catch (UnsupportedEncodingException e) {
+            // ignore it as this encoding exception should have been caught earlier while scanning.
+        }
+
+        return t;
+    }
+    
+    public byte[] getBytes(int pos) {
+        recording = false;
+        byte[] b = buf.toByteArray(pos);
+        buf.trim(pos, 0);
+        return b;
+    }
+    
+    public void record() {
+        recording = true;
+    }
+
+    private static class TrimmableByteArrayOutputStream extends ByteArrayOutputStream {
+        public void trim(int head, int tail) {
+            System.arraycopy(buf, head, buf, 0, count - head - tail);
+            count -= head + tail;
+        }
+        
+        public byte[] toByteArray(int len) {
+            byte[] b = new byte[len];
+            System.arraycopy(buf, 0, b, 0, len);
+            return b;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/support/TokenXMLExpressionIterator.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/support/TokenXMLExpressionIterator.java b/camel-core/src/main/java/org/apache/camel/support/TokenXMLExpressionIterator.java
index f6ac6be..4192acf 100644
--- a/camel-core/src/main/java/org/apache/camel/support/TokenXMLExpressionIterator.java
+++ b/camel-core/src/main/java/org/apache/camel/support/TokenXMLExpressionIterator.java
@@ -325,46 +325,4 @@ public class TokenXMLExpressionIterator extends ExpressionAdapter {
         }
         return sb.toString();
     }
-
-    // this input stream records the stream until the first text extraction occurs.
-    private static class RecordableInputStream extends FilterInputStream {
-        private ByteArrayOutputStream buf;
-        private String charset;
-        private boolean recording;
-        protected RecordableInputStream(InputStream in, String charset) {
-            super(in);
-            this.buf = new ByteArrayOutputStream();
-            this.charset = charset;
-            this.recording = true;
-        }
-
-        @Override
-        public int read() throws IOException {
-            int c = super.read();
-            if (c > 0 && recording) {
-                buf.write(c);
-            }
-            return c;
-        }
-
-        @Override
-        public int read(byte[] b, int off, int len) throws IOException {
-            int n = super.read(b, off, len);
-            if (n > 0 && recording) {
-                buf.write(b, off, n);
-            }
-            return n;
-        }
-
-        public String getText(int pos) {
-            String t = null;
-            recording = false;
-            try {
-                t = new String(buf.toByteArray(), 0, pos, charset);
-            } catch (UnsupportedEncodingException e) {
-                // ignore it as this should have be caught while scanning.
-            }
-            return t;
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/java/org/apache/camel/support/XMLTokenExpressionIterator.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/support/XMLTokenExpressionIterator.java b/camel-core/src/main/java/org/apache/camel/support/XMLTokenExpressionIterator.java
new file mode 100644
index 0000000..63a3ba0
--- /dev/null
+++ b/camel-core/src/main/java/org/apache/camel/support/XMLTokenExpressionIterator.java
@@ -0,0 +1,460 @@
+/**
+ * 
+ */
+package org.apache.camel.support;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.converter.jaxp.StaxConverter;
+import org.apache.camel.spi.NamespaceAware;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+public class XMLTokenExpressionIterator extends ExpressionAdapter implements NamespaceAware {
+    protected final String path;
+    protected boolean wrap;
+    protected Map<String, String> nsmap;
+
+    public XMLTokenExpressionIterator(String path, boolean wrap) {
+        ObjectHelper.notEmpty(path, "path");
+        this.path = path;
+        this.wrap = wrap;
+    }
+
+    @Override
+    public void setNamespaces(Map<String, String> nsmap) {
+        this.nsmap = nsmap;
+    }
+
+    public void setWrap(boolean wrap) {
+        this.wrap = wrap;
+    }
+    
+    protected Iterator<?> createIterator(InputStream in, Exchange exchange) throws XMLStreamException {
+        XMLTokenIterator iterator = new XMLTokenIterator(path, nsmap, wrap, in, exchange);
+        return iterator;
+    }
+
+    @Override
+    public boolean matches(Exchange exchange) {
+        // as a predicate we must close the stream, as we do not return an iterator that can be used
+        // afterwards to iterate the input stream
+        Object value = doEvaluate(exchange, true);
+        return ObjectHelper.evaluateValuePredicate(value);
+    }
+
+    @Override
+    public Object evaluate(Exchange exchange) {
+        // as we return an iterator to access the input stream, we should not close it
+        return doEvaluate(exchange, false);
+    }
+
+    /**
+     * Strategy to evaluate the exchange
+     *
+     * @param exchange   the exchange
+     * @param closeStream whether to close the stream before returning from this method.
+     * @return the evaluated value
+     */
+    protected Object doEvaluate(Exchange exchange, boolean closeStream) {
+        InputStream in = null;
+        try {
+            in = exchange.getIn().getMandatoryBody(InputStream.class);
+            return createIterator(in, exchange);
+        } catch (InvalidPayloadException e) {
+            exchange.setException(e);
+            // must close input stream
+            IOHelper.close(in);
+            return null;
+        } catch (XMLStreamException e) {
+            exchange.setException(e);
+            // must close input stream
+            IOHelper.close(in);
+            return null;
+        } finally {
+            if (closeStream) {
+                IOHelper.close(in);
+            }
+        }
+    }
+    
+
+    static class XMLTokenIterator implements Iterator<Object>, Closeable {
+        private static final Logger LOG = LoggerFactory.getLogger(XMLTokenIterator.class);
+        private static final Pattern NAMESPACE_PATTERN = Pattern.compile("xmlns(:\\w+|)\\s*=\\s*('[^']+'|\"[^\"]+\")");
+
+        private QName[] splitpath;
+        private int index;
+        private boolean wrap;
+        private RecordableInputStream in;
+        private XMLStreamReader reader;
+        private List<QName> path;
+        private List<Map<String, String>> namespaces;
+        private List<String> segments;
+        private List<QName> segmentlog;
+        private int code;
+        private int consumed;
+        private boolean backtrack;
+        private int trackdepth = -1;
+        private int depth;
+        
+        private Object nextToken;
+        
+        public XMLTokenIterator(String path, Map<String, String> nsmap, boolean wrap, InputStream in, Exchange exchange) throws XMLStreamException {
+            final String[] sl = path.substring(1).split("/");
+            this.splitpath = new QName[sl.length];
+            for (int i = 0; i < sl.length; i++) {
+                String s = sl[i];
+                if (s.length() > 0) {
+                    int d = s.indexOf(':');
+                    String pfx = d > 0 ? s.substring(0, d) : "";
+                    this.splitpath[i] = new QName(nsmap.get(pfx), d > 0 ? s.substring(d + 1) : s, pfx);
+                }
+            }
+            
+            this.wrap = wrap;
+            String charset = IOHelper.getCharsetName(exchange, false);
+            this.in = new RecordableInputStream(in, charset);
+            this.reader = new StaxConverter().createXMLStreamReader(this.in, exchange);
+
+            LOG.trace("reader.class: {}", reader.getClass());
+            if (reader.getLocation().getCharacterOffset() < 0) {
+                LOG.error("XMLStreamReader {} not supporting Location");
+                throw new XMLStreamException("reader not supporting Location");
+            }
+
+            this.path = new ArrayList<QName>();
+            
+            // wrapped mode needs the segments and the injected mode needs the namespaces
+            if (wrap) {
+                this.segments = new ArrayList<String>();
+                this.segmentlog = new ArrayList<QName>();
+            } else {
+                this.namespaces = new ArrayList<Map<String, String>>();
+            }
+                        
+            this.nextToken = getNextToken();
+
+        }
+        
+        private boolean isDoS() {
+            return splitpath[index] == null;
+        }
+        
+        private QName current() {
+            return splitpath[index + (isDoS() ? 1 : 0)];
+        }
+        
+        private QName ancestor() {
+            return index == 0 ? null : splitpath[index - 1];
+        }
+
+        private void down() {
+            if (isDoS()) {
+                index++;
+            }
+            index++;
+        }
+        
+        private void up() {
+            index--;
+        }
+        
+        private boolean isBottom() {
+            return index == splitpath.length - (isDoS() ? 2 : 1);
+        }
+        
+        private boolean isTop() {
+            return index == 0;
+        }
+        
+        private int readNext() throws XMLStreamException {
+            int c = code;
+            if (c > 0) {
+                code = 0;
+            } else {
+                c = reader.next();
+            }
+            return c;
+        }
+        
+        private String getCurrenText() {
+            int pos = reader.getLocation().getCharacterOffset();
+            String txt = in.getText(pos - consumed);
+            consumed = pos;
+            // keep recording
+            in.record();
+            return txt;
+        }
+
+        private void pushName(QName name) {
+            path.add(name);
+        }
+
+        private QName popName() {
+            return path.remove(path.size() - 1);
+        }
+
+        private void pushSegment(QName qname, String token) {
+            segments.add(token);
+            segmentlog.add(qname);
+        }
+
+        private String popSegment() {
+            return segments.remove(segments.size() - 1);
+        }
+        
+        private QName peekLog() {
+            return segmentlog.get(segmentlog.size() - 1);
+        }
+        
+        private QName popLog() {
+            return segmentlog.remove(segmentlog.size() - 1);
+        }
+
+        private void pushNamespaces(XMLStreamReader reader) {
+            Map<String, String> m = new HashMap<String, String>();
+            if (namespaces.size() > 0) {
+                m.putAll(namespaces.get(namespaces.size() - 1));
+            }
+            for (int i = 0; i < reader.getNamespaceCount(); i++) {
+                m.put(reader.getNamespacePrefix(i), reader.getNamespaceURI(i));
+            }
+            namespaces.add(m);
+        }
+
+        private void popNamespaces() {
+            namespaces.remove(namespaces.size() - 1);
+        }
+
+        private Map<String, String> getCurrentNamespaceBindings() {
+            return namespaces.get(namespaces.size() - 1);
+        }
+
+        private void readCurrent(boolean incl) throws XMLStreamException {
+            int d = depth;
+            while (d <= depth) {
+                int code = reader.next();
+                if (code == XMLStreamReader.START_ELEMENT) {
+                    depth++;
+                } else if (code == XMLStreamReader.END_ELEMENT) {
+                    depth--;
+                }
+            }
+            // either look ahead to the next token or stay at the end element token
+            if (incl) {
+                code = reader.next();
+            } else {
+                code = reader.getEventType();
+                if (code == XMLStreamReader.END_ELEMENT) {
+                    // revert the depth count to avoid double counting the up event
+                    depth++;
+                }
+            }
+        }
+
+        private String getCurrentToken() throws XMLStreamException {
+            readCurrent(true);
+            popName();
+            
+            String token = createContextualToken(getCurrenText());
+            if (!wrap) {
+                popNamespaces();
+            }
+            
+            return token;
+        }
+
+        private String createContextualToken(String token){
+            StringBuilder sb = new StringBuilder();
+            if (wrap) {
+                for (int i = 0; i < segments.size(); i++) {
+                    sb.append(segments.get(i));
+                }
+                sb.append(token);
+                for (int i = path.size() - 1; i >= 0; i--) {
+                    QName q = path.get(i);
+                    sb.append("</").append(makeName(q)).append(">");
+                }
+
+            } else {
+                final String stag = token.substring(0, token.indexOf('>') + 1);
+                Set<String> skip = new HashSet<String>();
+                Matcher matcher = NAMESPACE_PATTERN.matcher(stag);
+                char quote = 0;
+                while (matcher.find()) {
+                    String prefix = matcher.group(1);
+                    if (prefix.length() > 0) {
+                        prefix = prefix.substring(1);
+                    }
+                    skip.add(prefix);
+                    if (quote == 0) {
+                        quote = matcher.group(2).charAt(0);
+                    }
+                }
+                if (quote == 0) {
+                    quote = '"';
+                }
+                boolean empty = stag.endsWith("/>"); 
+                sb.append(token.substring(0, stag.length() - (empty ? 2 : 1)));
+                for (Entry<String, String> e : getCurrentNamespaceBindings().entrySet()) {
+                    if (!skip.contains(e.getKey())) {
+                        sb.append(" xmlns:").append(e.getKey()).append("=").append(quote).append(e.getValue()).append(quote);
+                    }
+                }
+                sb.append(token.substring(stag.length() - (empty ? 2 : 1)));
+            }
+
+            return sb.toString();
+        }
+
+        private String getNextToken() throws XMLStreamException {
+            int code = 0;
+            while (code != XMLStreamReader.END_DOCUMENT) {
+                code = readNext();
+
+                switch (code) {
+                case XMLStreamReader.START_ELEMENT:
+                    depth++;
+                    QName name = reader.getName();
+                    if (LOG.isTraceEnabled()) {
+                        LOG.trace("se={}; depth={}; trackdepth={}", new Object[]{name, depth, trackdepth});
+                    }
+                    
+                    String token = getCurrenText();
+                    LOG.trace("token={}", token);
+                    if (!backtrack && wrap) {
+                        pushSegment(name, token);
+                    }
+                    pushName(name);
+                    if (!wrap) {
+                        pushNamespaces(reader);
+                    }
+                    backtrack = false;
+                    if (matches(name, current())) {
+                        // mark the position of the match in the segments list
+                        if (isBottom()) {
+                            // final match
+                            token = getCurrentToken();
+                            backtrack = true;
+                            trackdepth = depth;
+                            return token;
+                        } else {
+                            // intermediary match
+                            down();
+                        }
+                    } else if (isDoS()){
+                        // continue
+                    } else {
+                        // skip
+                        readCurrent(false);
+                    }
+                    break;
+                case XMLStreamReader.END_ELEMENT:
+                    depth--;
+                    QName endname = reader.getName();
+                    LOG.trace("ee={}", endname);
+                    
+                    popName();
+                    if (!wrap) {
+                        popNamespaces();
+                    }
+                    
+                    int pc = 0;
+                    if (backtrack || (trackdepth > 0 && depth == trackdepth - 1)) {
+                        // reactive backtrack if not backtracking and update the track depth
+                        backtrack = true;
+                        trackdepth--;
+                        if (wrap) {
+                            while (!endname.equals(peekLog())) {
+                                pc++;
+                                popLog();
+                            }
+                        }
+                    }
+
+                    if (backtrack) {
+                        if (wrap) {
+                            for (int i = 0; i < pc; i++) {
+                                popSegment();
+                            }
+                        }
+
+                        if ((ancestor() == null && !isTop())
+                            || (ancestor() != null && matches(endname, ancestor()))) {
+                            up();
+                        }
+                    }
+                    break;
+                case XMLStreamReader.END_DOCUMENT:
+                    LOG.trace("depth={}", depth);
+                    break;
+                }
+            }
+            return null;
+        }
+
+        private static String makeName(QName qname) {
+            String pfx = qname.getPrefix();
+            return pfx.length() == 0 ? qname.getLocalPart() : qname.getPrefix() + ":" + qname.getLocalPart();
+        }
+
+        private static boolean matches(QName a, QName b) {
+            return a.getNamespaceURI().equals(b.getNamespaceURI()) && a.getLocalPart().equals(b.getLocalPart());
+        }
+
+        @Override
+        public boolean hasNext() {
+            return nextToken != null;
+        }
+
+        @Override
+        public Object next() {
+            Object o = nextToken;
+            try {
+                nextToken = getNextToken();
+            } catch (XMLStreamException e) {
+                //
+            }
+            return o;
+        }
+
+        @Override
+        public void remove() {
+            // noop
+        }
+
+        @Override
+        public void close() throws IOException {
+            try {
+                reader.close();
+            } catch (XMLStreamException e) {
+                throw new IOException(e);
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/main/resources/META-INF/services/org/apache/camel/language/xtokenize
----------------------------------------------------------------------
diff --git a/camel-core/src/main/resources/META-INF/services/org/apache/camel/language/xtokenize b/camel-core/src/main/resources/META-INF/services/org/apache/camel/language/xtokenize
new file mode 100644
index 0000000..e9a9898
--- /dev/null
+++ b/camel-core/src/main/resources/META-INF/services/org/apache/camel/language/xtokenize
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+class=org.apache.camel.language.tokenizer.XMLTokenizeLanguage

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguageTest.java
----------------------------------------------------------------------
diff --git a/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguageTest.java b/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguageTest.java
new file mode 100644
index 0000000..99c450f
--- /dev/null
+++ b/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeLanguageTest.java
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.language.tokenizer;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.builder.xml.Namespaces;
+
+public class XMLTokenizeLanguageTest extends ContextTestSupport {
+
+    public void testSendClosedTagMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("<c:child some_attr='a' anotherAttr='a' xmlns:c=\"urn:c\"></c:child>", "<c:child some_attr='b' anotherAttr='b' xmlns:c=\"urn:c\"></c:child>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a'></c:child><c:child some_attr='b' anotherAttr='b'></c:child></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendClosedTagWithLineBreaksMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("<c:child some_attr='a' anotherAttr='a' xmlns:c=\"urn:c\">\n</c:child>", "<c:child some_attr='b' anotherAttr='b' xmlns:c=\"urn:c\">\n</c:child>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?>\n"
+                + "<c:parent xmlns:c='urn:c'>\n"
+                + "<c:child some_attr='a' anotherAttr='a'>\n"
+                + "</c:child>\n"
+                + "<c:child some_attr='b' anotherAttr='b'>\n"
+                + "</c:child>\n"
+                + "</c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendSelfClosingTagMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("<c:child some_attr='a' anotherAttr='a'  xmlns:c=\"urn:c\"/>", "<c:child some_attr='b' anotherAttr='b'  xmlns:c=\"urn:c\"/>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a' /><c:child some_attr='b' anotherAttr='b' /></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendMixedClosingTagMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<c:child some_attr='a' anotherAttr='a' xmlns:c=\"urn:c\">ha</c:child>", "<c:child some_attr='b' anotherAttr='b'  xmlns:c=\"urn:c\"/>", "<c:child some_attr='c' xmlns:c=\"urn:c\"></c:child>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a'>ha</c:child><c:child some_attr='b' anotherAttr='b' /><c:child some_attr='c'></c:child></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendMixedClosingTagInsideMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<c:child name='child1' xmlns:c=\"urn:c\"><grandchild name='grandchild1'/> <grandchild name='grandchild2'/></c:child>",
+            "<c:child name='child2' xmlns:c=\"urn:c\"><grandchild name='grandchild1'></grandchild><grandchild name='grandchild2'></grandchild></c:child>");
+
+        template.sendBody("direct:start",
+            "<c:parent xmlns:c='urn:c'><c:child name='child1'><grandchild name='grandchild1'/> <grandchild name='grandchild2'/></c:child>"
+            + "<c:child name='child2'><grandchild name='grandchild1'></grandchild><grandchild name='grandchild2'></grandchild></c:child></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendNamespacedChildMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<c:child xmlns:c='urn:c' some_attr='a' anotherAttr='a'></c:child>", "<c:child xmlns:c='urn:c' some_attr='b' anotherAttr='b' />");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child xmlns:c='urn:c' some_attr='a' anotherAttr='a'></c:child><c:child xmlns:c='urn:c' some_attr='b' anotherAttr='b' /></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendNamespacedParentMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<c:child some_attr='a' anotherAttr='a' xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"></c:child>", "<c:child some_attr='b' anotherAttr='b' xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"/>");
+        
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\"><c:child some_attr='a' anotherAttr='a'></c:child><c:child some_attr='b' anotherAttr='b'/></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendMoreParentsMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<c:child some_attr='a' anotherAttr='a' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"></c:child>", "<c:child some_attr='b' anotherAttr='b' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"/>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child><c:child some_attr='b' anotherAttr='b'/></c:parent></grandparent></g:greatgrandparent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            Namespaces ns = new Namespaces("C", "urn:c");
+            public void configure() {
+                from("direct:start")
+                    .split().tokenizeXML("//C:child", false, ns)
+                        .to("mock:result")
+                    .end();
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeWrapLanguageTest.java
----------------------------------------------------------------------
diff --git a/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeWrapLanguageTest.java b/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeWrapLanguageTest.java
new file mode 100644
index 0000000..f18e709
--- /dev/null
+++ b/camel-core/src/test/java/org/apache/camel/language/tokenizer/XMLTokenizeWrapLanguageTest.java
@@ -0,0 +1,133 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.camel.language.tokenizer;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.builder.xml.Namespaces;
+
+public class XMLTokenizeWrapLanguageTest extends ContextTestSupport {
+
+    public void testSendClosedTagMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a'></c:child></c:parent>", 
+                                                              "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='b' anotherAttr='b'></c:child></c:parent>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a'></c:child><c:child some_attr='b' anotherAttr='b'></c:child></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendClosedTagWithLineBreaksMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("<?xml version='1.0' encoding='UTF-8'?>\n<c:parent xmlns:c='urn:c'>\n<c:child some_attr='a' anotherAttr='a'>\n</c:child></c:parent>", 
+                                                              "<?xml version='1.0' encoding='UTF-8'?>\n<c:parent xmlns:c='urn:c'>\n<c:child some_attr='b' anotherAttr='b'>\n</c:child></c:parent>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?>\n"
+                + "<c:parent xmlns:c='urn:c'>\n"
+                + "<c:child some_attr='a' anotherAttr='a'>\n"
+                + "</c:child>\n"
+                + "<c:child some_attr='b' anotherAttr='b'>\n"
+                + "</c:child>\n"
+                + "</c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendSelfClosingTagMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a' /></c:parent>", 
+                                                              "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='b' anotherAttr='b' /></c:parent>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a' /><c:child some_attr='b' anotherAttr='b' /></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendMixedClosingTagMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a'>ha</c:child></c:parent>", 
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='b' anotherAttr='b' /></c:parent>", 
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='c'></c:child></c:parent>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child some_attr='a' anotherAttr='a'>ha</c:child><c:child some_attr='b' anotherAttr='b' /><c:child some_attr='c'></c:child></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendMixedClosingTagInsideMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<c:parent xmlns:c='urn:c'><c:child name='child1'><grandchild name='grandchild1'/> <grandchild name='grandchild2'/></c:child></c:parent>",
+            "<c:parent xmlns:c='urn:c'><c:child name='child2'><grandchild name='grandchild1'></grandchild><grandchild name='grandchild2'></grandchild></c:child></c:parent>");
+
+        template.sendBody("direct:start",
+            "<c:parent xmlns:c='urn:c'><c:child name='child1'><grandchild name='grandchild1'/> <grandchild name='grandchild2'/></c:child>"
+            + "<c:child name='child2'><grandchild name='grandchild1'></grandchild><grandchild name='grandchild2'></grandchild></c:child></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendNamespacedChildMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child xmlns:c='urn:c' some_attr='a' anotherAttr='a'></c:child></c:parent>", 
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child xmlns:c='urn:c' some_attr='b' anotherAttr='b' /></c:parent>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c'><c:child xmlns:c='urn:c' some_attr='a' anotherAttr='a'></c:child><c:child xmlns:c='urn:c' some_attr='b' anotherAttr='b' /></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendNamespacedParentMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\"><c:child some_attr='a' anotherAttr='a'></c:child></c:parent>", 
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\"><c:child some_attr='b' anotherAttr='b'/></c:parent>");
+        
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\"><c:child some_attr='a' anotherAttr='a'></c:child><c:child some_attr='b' anotherAttr='b'/></c:parent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    public void testSendMoreParentsMessageToTokenize() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived(
+            "<?xml version='1.0' encoding='UTF-8'?><g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child></c:parent></grandparent></g:greatgrandparent>", 
+            "<?xml version='1.0' encoding='UTF-8'?><g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='b' anotherAttr='b'/></c:parent></grandparent></g:greatgrandparent>");
+
+        template.sendBody("direct:start",
+            "<?xml version='1.0' encoding='UTF-8'?><g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt><c:parent xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child><c:child some_attr='b' anotherAttr='b'/></c:parent></grandparent></g:greatgrandparent>");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            Namespaces ns = new Namespaces("C", "urn:c");
+            public void configure() {
+                from("direct:start")
+                    .split().tokenizeXML("//C:child", true, ns)
+                        .to("mock:result")
+                    .end();
+            }
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/de56e0bd/camel-core/src/test/java/org/apache/camel/support/XMLTokenExpressionIteratorTest.java
----------------------------------------------------------------------
diff --git a/camel-core/src/test/java/org/apache/camel/support/XMLTokenExpressionIteratorTest.java b/camel-core/src/test/java/org/apache/camel/support/XMLTokenExpressionIteratorTest.java
new file mode 100644
index 0000000..eedc7f7
--- /dev/null
+++ b/camel-core/src/test/java/org/apache/camel/support/XMLTokenExpressionIteratorTest.java
@@ -0,0 +1,228 @@
+/**
+ * 
+ */
+package org.apache.camel.support;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.stream.XMLStreamException;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.ExchangeBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+
+import junit.framework.TestCase;
+
+/**
+ *
+ */
+public class XMLTokenExpressionIteratorTest extends TestCase {
+    private static final byte[] TEST_BODY = 
+        ("<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='1' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child>"
+            + "<c:child some_attr='b' anotherAttr='b'/>"
+            + "</c:parent>"
+            + "<c:parent some_attr='2' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='c' anotherAttr='c'></c:child>"
+            + "<c:child some_attr='d' anotherAttr='d'/>"
+            + "</c:parent>"
+            + "</grandparent>"
+            + "<grandparent><uncle>ben</uncle><aunt/>"
+            + "<c:parent some_attr='3' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='e' anotherAttr='e'></c:child>"
+            + "<c:child some_attr='f' anotherAttr='f'/>"
+            + "</c:parent>"
+            + "</grandparent>"
+            + "</g:greatgrandparent>").getBytes();
+    
+    private static final String[] RESULTS_CHILD_WRAPPED = {
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='1' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='1' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='b' anotherAttr='b'/>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='2' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='c' anotherAttr='c'></c:child>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='2' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='d' anotherAttr='d'/>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle>ben</uncle><aunt/>"
+            + "<c:parent some_attr='3' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='e' anotherAttr='e'></c:child>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle>ben</uncle><aunt/>"
+            + "<c:parent some_attr='3' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='f' anotherAttr='f'/>"
+            + "</c:parent></grandparent></g:greatgrandparent>"
+    };
+
+    private static final String[] RESULTS_CHILD = {
+        "<c:child some_attr='a' anotherAttr='a' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"></c:child>",
+        "<c:child some_attr='b' anotherAttr='b' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"/>",
+        "<c:child some_attr='c' anotherAttr='c' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"></c:child>",
+        "<c:child some_attr='d' anotherAttr='d' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"/>",
+        "<c:child some_attr='e' anotherAttr='e' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"></c:child>",
+        "<c:child some_attr='f' anotherAttr='f' xmlns:g=\"urn:g\" xmlns:d=\"urn:d\" xmlns:c=\"urn:c\"/>"
+    };
+
+    private static final String[] RESULTS_PARENT_WRAPPED = {
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='1' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child>"
+            + "<c:child some_attr='b' anotherAttr='b'/>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "<c:parent some_attr='2' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='c' anotherAttr='c'></c:child>"
+            + "<c:child some_attr='d' anotherAttr='d'/>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle>ben</uncle><aunt/>"
+            + "<c:parent some_attr='3' xmlns:c='urn:c' xmlns:d=\"urn:d\">"
+            + "<c:child some_attr='e' anotherAttr='e'></c:child>"
+            + "<c:child some_attr='f' anotherAttr='f'/>"
+            + "</c:parent></grandparent></g:greatgrandparent>",
+    };
+    
+    private static final String[] RESULTS_PARENT = {
+        "<c:parent some_attr='1' xmlns:c='urn:c' xmlns:d=\"urn:d\" xmlns:g='urn:g'>"
+            + "<c:child some_attr='a' anotherAttr='a'></c:child>"
+            + "<c:child some_attr='b' anotherAttr='b'/>"
+            + "</c:parent>",
+        "<c:parent some_attr='2' xmlns:c='urn:c' xmlns:d=\"urn:d\" xmlns:g='urn:g'>"
+            + "<c:child some_attr='c' anotherAttr='c'></c:child>"
+            + "<c:child some_attr='d' anotherAttr='d'/>"
+            + "</c:parent>",
+        "<c:parent some_attr='3' xmlns:c='urn:c' xmlns:d=\"urn:d\" xmlns:g='urn:g'>"
+            + "<c:child some_attr='e' anotherAttr='e'></c:child>"
+            + "<c:child some_attr='f' anotherAttr='f'/>"
+            + "</c:parent>",
+    };
+    
+    private static final String[] RESULTS_AUNT_WRAPPED = {
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle/><aunt>emma</aunt>"
+            + "</grandparent></g:greatgrandparent>",
+        "<?xml version='1.0' encoding='UTF-8'?>"
+            + "<g:greatgrandparent xmlns:g='urn:g'><grandparent><uncle>ben</uncle><aunt/>"
+            + "</grandparent></g:greatgrandparent>"
+    };    
+    
+    private static final String[] RESULTS_AUNT = {
+        "<aunt xmlns:g=\"urn:g\">emma</aunt>",
+        "<aunt xmlns:g=\"urn:g\"/>"
+    };    
+    
+
+    private Map<String, String> nsmap;
+    private Exchange exchange;
+
+    @Override
+    protected void setUp() throws Exception {
+        nsmap = new HashMap<String, String>();
+        nsmap.put("G", "urn:g");
+        nsmap.put("C", "urn:c");
+        
+        exchange = ExchangeBuilder.anExchange(new DefaultCamelContext()).build();
+    }
+
+
+    public void testExtractChild() throws Exception {
+        invokeAndVerify("//C:child", true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+
+    public void testExtractChildInjected() throws Exception {
+        invokeAndVerify("//C:child", false, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD);
+    }
+
+    public void testExtractChildWithAncestorGGP_GP() throws Exception {
+        invokeAndVerify("/G:greatgrandparent/grandparent//C:child", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+
+    public void testExtractChildWithAncestorGGP_P() throws Exception {
+        invokeAndVerify("/G:greatgrandparent//C:parent/C:child", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+
+    public void testExtractChildWithAncestorGP__P() throws Exception {
+        invokeAndVerify("//grandparent//C:parent/C:child", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+
+    public void testExtractChildWithAncestorGP_P() throws Exception {
+        invokeAndVerify("//grandparent/C:parent/C:child", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+
+    public void testExtractChildWithAncestorP() throws Exception {
+        invokeAndVerify("//C:parent/C:child", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+
+    public void testExtractChildWithAncestorGGP_GP_P() throws Exception {
+        invokeAndVerify("/G:greatgrandparent/grandparent/C:parent/C:child", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_CHILD_WRAPPED);
+    }
+    
+    public void textExtractParent() throws Exception {
+        invokeAndVerify("//C:parent", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_PARENT_WRAPPED);
+    }
+    
+    public void textExtractParentInjected() throws Exception {
+        invokeAndVerify("//C:parent", 
+                        false, new ByteArrayInputStream(TEST_BODY), RESULTS_PARENT);
+    }
+    
+    public void textExtractAunt() throws Exception {
+        invokeAndVerify("//aunt", 
+                        true, new ByteArrayInputStream(TEST_BODY), RESULTS_AUNT_WRAPPED);
+    }
+
+    public void textExtractAuntInjected() throws Exception {
+        invokeAndVerify("//aunt", 
+                        false, new ByteArrayInputStream(TEST_BODY), RESULTS_AUNT);
+    }
+
+    private void invokeAndVerify(String path, boolean wrap, InputStream in, String[] expected) throws Exception {
+        XMLTokenExpressionIterator xtei = new XMLTokenExpressionIterator(path, wrap);
+        xtei.setNamespaces(nsmap);
+        
+        Iterator<?> it = xtei.createIterator(in, exchange);
+        List<String> results = new ArrayList<String>();
+        while (it.hasNext()) {
+            results.add((String)it.next());
+        }
+        ((Closeable)it).close();
+
+        assertEquals("token count", expected.length, results.size());
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("mismatch [" + i + "]", expected[i], results.get(i));
+        }
+
+    }
+}