blob: 19cc2a62b793ad8304fe8f9f79ea9733fb11f3c0 [file] [log] [blame]
/**
* 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.support;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
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.XMLStreamConstants;
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 char mode;
protected int group;
protected Map<String, String> nsmap;
public XMLTokenExpressionIterator(String path, char mode) {
this(path, mode, 1);
}
public XMLTokenExpressionIterator(String path, char mode, int group) {
ObjectHelper.notEmpty(path, "path");
this.path = path;
this.mode = mode;
this.group = group > 1 ? group : 1;
}
@Override
public void setNamespaces(Map<String, String> nsmap) {
this.nsmap = nsmap;
}
public void setMode(char mode) {
this.mode = mode;
}
public void setMode(String mode) {
this.mode = mode != null ? mode.charAt(0) : 0;
}
public int getGroup() {
return group;
}
public void setGroup(int group) {
this.group = group;
}
protected Iterator<?> createIterator(InputStream in, String charset) throws XMLStreamException, UnsupportedEncodingException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(in);
} else {
reader = new InputStreamReader(in, charset);
}
XMLTokenIterator iterator = new XMLTokenIterator(path, nsmap, mode, group, reader);
return iterator;
}
protected Iterator<?> createIterator(Reader in) throws XMLStreamException {
XMLTokenIterator iterator = new XMLTokenIterator(path, nsmap, mode, group, in);
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);
String charset = IOHelper.getCharsetName(exchange);
return createIterator(in, charset);
} 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;
} catch (UnsupportedEncodingException 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 AttributedQName[] splitpath;
private int index;
private char mode;
private int group;
private RecordableReader in;
private XMLStreamReader reader;
private List<QName> path;
private List<Map<String, String>> namespaces;
private List<String> segments;
private List<QName> segmentlog;
private List<String> tokens;
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, char mode, InputStream in, String charset)
throws XMLStreamException, UnsupportedEncodingException {
// woodstox's getLocation().etCharOffset() does not return the offset correctly for InputStream, so use Reader instead.
this(path, nsmap, mode, 1, new InputStreamReader(in, charset));
}
public XMLTokenIterator(String path, Map<String, String> nsmap, char mode, int group, InputStream in, String charset)
throws XMLStreamException, UnsupportedEncodingException {
// woodstox's getLocation().etCharOffset() does not return the offset correctly for InputStream, so use Reader instead.
this(path, nsmap, mode, new InputStreamReader(in, charset));
}
public XMLTokenIterator(String path, Map<String, String> nsmap, char mode, Reader in) throws XMLStreamException {
this(path, nsmap, mode, 1, in);
}
public XMLTokenIterator(String path, Map<String, String> nsmap, char mode, int group, Reader in) throws XMLStreamException {
final String[] sl = path.substring(1).split("/");
this.splitpath = new AttributedQName[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 AttributedQName(
"*".equals(pfx) ? "*" : nsmap == null ? "" : nsmap.get(pfx), d > 0 ? s.substring(d + 1) : s, pfx);
}
}
this.mode = mode != 0 ? mode : 'i';
this.group = group > 0 ? group : 1;
this.in = new RecordableReader(in);
this.reader = new StaxConverter().createXMLStreamReader(this.in);
LOG.trace("reader.class: {}", reader.getClass());
int coff = reader.getLocation().getCharacterOffset();
if (coff != 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 (this.mode == 'w') {
this.segments = new ArrayList<String>();
this.segmentlog = new ArrayList<QName>();
} else if (this.mode == 'i') {
this.namespaces = new ArrayList<Map<String, String>>();
}
// when grouping the tokens, allocate the storage to temporarily store tokens.
if (this.group > 1) {
this.tokens = new ArrayList<String>();
}
this.nextToken = getNextToken();
}
private boolean isDoS() {
return splitpath[index] == null;
}
private AttributedQName current() {
return splitpath[index + (isDoS() ? 1 : 0)];
}
private AttributedQName 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 == XMLStreamConstants.START_ELEMENT) {
depth++;
} else if (code == XMLStreamConstants.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 == XMLStreamConstants.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 (mode == 'i') {
popNamespaces();
}
return token;
}
private String createContextualToken(String token) {
StringBuilder sb = new StringBuilder();
if (mode == 'w' && group == 1) {
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 if (mode == 'i') {
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(e.getKey().length() == 0 ? " xmlns" : " xmlns:")
.append(e.getKey()).append("=").append(quote).append(e.getValue()).append(quote);
}
}
sb.append(token.substring(stag.length() - (empty ? 2 : 1)));
} else if (mode == 'u') {
int bp = token.indexOf(">");
int ep = token.lastIndexOf("</");
if (bp > 0 && ep > 0) {
sb.append(token.substring(bp + 1, ep));
}
} else if (mode == 't') {
int bp = 0;
for (;;) {
int ep = token.indexOf('>', bp);
bp = token.indexOf('<', ep);
if (bp < 0) {
break;
}
sb.append(token.substring(ep + 1, bp));
}
} else {
return token;
}
return sb.toString();
}
private String getGroupedToken() {
StringBuilder sb = new StringBuilder();
if (mode == 'w') {
// for wrapped
for (int i = 0; i < segments.size(); i++) {
sb.append(segments.get(i));
}
for (String s : tokens) {
sb.append(s);
}
for (int i = path.size() - 1; i >= 0; i--) {
QName q = path.get(i);
sb.append("</").append(makeName(q)).append(">");
}
} else {
// for injected, unwrapped, text
sb.append("<group>");
for (String s : tokens) {
sb.append(s);
}
sb.append("</group>");
}
tokens.clear();
return sb.toString();
}
private String getNextToken() throws XMLStreamException {
int xcode = 0;
while (xcode != XMLStreamConstants.END_DOCUMENT) {
xcode = readNext();
switch (xcode) {
case XMLStreamConstants.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 && mode == 'w') {
pushSegment(name, token);
}
pushName(name);
if (mode == 'i') {
pushNamespaces(reader);
}
backtrack = false;
if (current().matches(name)) {
// mark the position of the match in the segments list
if (isBottom()) {
// final match
token = getCurrentToken();
backtrack = true;
trackdepth = depth;
if (group > 1) {
tokens.add(token);
if (group == tokens.size()) {
return getGroupedToken();
}
} else {
return token;
}
} else {
// intermediary match
down();
}
} else if (isDoS()) {
// continue
} else {
// skip
readCurrent(false);
}
break;
case XMLStreamConstants.END_ELEMENT:
if ((backtrack || (trackdepth > 0 && depth == trackdepth))
&& (mode == 'w' && group > 1 && tokens.size() > 0)) {
// flush the left over using the current context
code = XMLStreamConstants.END_ELEMENT;
return getGroupedToken();
}
depth--;
QName endname = reader.getName();
LOG.trace("ee={}", endname);
popName();
if (mode == 'i') {
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 (mode == 'w') {
while (!endname.equals(peekLog())) {
pc++;
popLog();
}
}
}
if (backtrack) {
if (mode == 'w') {
for (int i = 0; i < pc; i++) {
popSegment();
}
}
if ((ancestor() == null && !isTop())
|| (ancestor() != null && ancestor().matches(endname))) {
up();
}
}
break;
case XMLStreamConstants.END_DOCUMENT:
LOG.trace("depth={}", depth);
if (group > 1 && tokens.size() > 0) {
// flush the left over before really going EoD
code = XMLStreamConstants.END_DOCUMENT;
return getGroupedToken();
}
break;
default:
break;
}
}
return null;
}
private static String makeName(QName qname) {
String pfx = qname.getPrefix();
return pfx.length() == 0 ? qname.getLocalPart() : qname.getPrefix() + ":" + qname.getLocalPart();
}
@Override
public boolean hasNext() {
return nextToken != null;
}
@Override
public Object next() {
Object o = nextToken;
try {
nextToken = getNextToken();
} catch (XMLStreamException e) {
nextToken = null;
throw new RuntimeException(e);
}
return o;
}
@Override
public void remove() {
// noop
}
@Override
public void close() throws IOException {
try {
reader.close();
} catch (XMLStreamException e) {
throw new IOException(e);
}
}
}
static class AttributedQName extends QName {
private static final long serialVersionUID = 9878370226894144L;
private Pattern lcpattern;
private boolean nsany;
public AttributedQName(String localPart) {
super(localPart);
checkWildcard("", localPart);
}
public AttributedQName(String namespaceURI, String localPart, String prefix) {
super(namespaceURI, localPart, prefix);
checkWildcard(namespaceURI, localPart);
}
public AttributedQName(String namespaceURI, String localPart) {
super(namespaceURI, localPart);
checkWildcard(namespaceURI, localPart);
}
public boolean matches(QName qname) {
return (nsany || getNamespaceURI().equals(qname.getNamespaceURI()))
&& (lcpattern != null
? lcpattern.matcher(qname.getLocalPart()).matches()
: getLocalPart().equals(qname.getLocalPart()));
}
private void checkWildcard(String nsa, String lcp) {
nsany = "*".equals(nsa);
boolean wc = false;
for (int i = 0; i < lcp.length(); i++) {
char c = lcp.charAt(i);
if (c == '?' || c == '*') {
wc = true;
break;
}
}
if (wc) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lcp.length(); i++) {
char c = lcp.charAt(i);
switch (c) {
case '.':
sb.append("\\.");
break;
case '*':
sb.append(".*");
break;
case '?':
sb.append('.');
break;
default:
sb.append(c);
break;
}
}
lcpattern = Pattern.compile(sb.toString());
}
}
}
}