/** * Copyright 2015 Santhosh Kumar Tekuri * * The JLibs authors license 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 jlibs.xml.sax.dog.sniff; import jlibs.core.lang.NotImplementedException; import jlibs.xml.DefaultNamespaceContext; import jlibs.xml.sax.dog.DataType; import jlibs.xml.sax.dog.NodeItem; import jlibs.xml.sax.dog.NodeType; import jlibs.xml.sax.dog.Scope; import jlibs.xml.sax.dog.expr.*; import jlibs.xml.sax.dog.expr.nodset.NodeSet; import jlibs.xml.sax.dog.expr.nodset.NodeSetListener; import jlibs.xml.sax.dog.expr.nodset.PositionTracker; import jlibs.xml.sax.dog.expr.nodset.StringEvaluation; import jlibs.xml.sax.dog.path.EventID; import jlibs.xml.sax.helpers.MyNamespaceSupport; import org.xml.sax.Attributes; import javax.xml.namespace.NamespaceContext; import javax.xml.stream.XMLStreamReader; import java.util.*; /** * @author Santhosh Kumar T */ public final class Event extends EvaluationListener implements NodeSetListener{ private final List<Expression> exprList; private final List<Expression> globalExprList; private final EventID.ConstraintEntry listenersArray[][]; private final SAXHandler handler; @SuppressWarnings({"unchecked"}) public Event(NamespaceContext givenNSContext, List<Expression> globalExprList, List<Expression> exprList, int noOfConstraints, boolean langInterested){ this.givenNSContext = givenNSContext; this.globalExprList = globalExprList; this.exprList = exprList; int noOfXPaths = exprList.size(); results = new Object[noOfXPaths]; pendingInstantResults = new int[noOfXPaths]; listeners = new List[noOfXPaths]; instantListenersCount = new int[noOfXPaths]; finished = new BitSet(noOfXPaths); listenersArray = new EventID.ConstraintEntry[6][noOfConstraints]; handler = new SAXHandler(this, langInterested); } public NamespaceContext getNamespaceContext(){ return nsContext; } public SAXHandler getSAXHandler(){ return handler; } /*-------------------------------------------------[ Information ]---------------------------------------------------*/ private long order; private int type; private String namespaceURI; private String localName; private String qualifiedName; private String value; public long order(){ return order; } public int type(){ return type; } public String namespaceURI(){ return namespaceURI; } public String localName(){ return localName; } public String qualifiedName(){ return qualifiedName; } public String value(){ if(type==NodeType.TEXT && value==null) value = buff.length()>0 ? buff.toString() : null; return value; } public String language(){ return tailInfo==null ? "" : tailInfo.lang; } private StringBuilder elementLocation = new StringBuilder(); private Info locationInfo; public String location(){ if(type==NodeType.DOCUMENT) return "/"; StringBuilder elementLocation = this.elementLocation; // update elementLocation Info info = locationInfo.next; while(info!=null){ assert info.slash==-1; info.slash = elementLocation.length(); elementLocation.append('/'); elementLocation.append(info.elem).append('[').append(info.elemntPos).append(']'); info = info.next; } locationInfo = tailInfo; int len; switch(type){ case NodeType.ELEMENT: return elementLocation.toString(); case NodeType.ATTRIBUTE: len = elementLocation.length(); elementLocation.append("/@").append(qname(namespaceURI, localName)); break; case NodeType.NAMESPACE: len = elementLocation.length(); elementLocation.append("/namespace::").append(localName); break; case NodeType.TEXT: len = elementLocation.length(); elementLocation.append("/text()[").append(tailInfo.textCount).append(']'); break; case NodeType.COMMENT: len = elementLocation.length(); elementLocation.append("/comment()[").append(tailInfo.commentCount).append(']'); break; case NodeType.PI: len = elementLocation.length(); elementLocation.append("/processing-instruction('").append(localName).append("')[").append(tailInfo.piMap.get(localName).value).append(']'); break; default: throw new NotImplementedException(); } String location = elementLocation.toString(); elementLocation.setLength(len); return location; } @Override public String toString(){ return location(); } /*-------------------------------------------------[ IDList ]---------------------------------------------------*/ private EventID current; public EventID getID(){ if(current==null){ current = new EventID(type, listenersArray); // current.location = location(); } return current; } private boolean interestedInAttributes; private boolean interestedInNamespaces; private boolean interestedInText; private void fireEvent(){ EventID id = current; current = null; EventID firstID = null; EventID activeID = null; boolean interestedInAttributes = false; boolean interestedInNamespaces = false; boolean interestedInText = false; do{ if(!id.onEvent(this)){ if(firstID==null) firstID = id; else activeID.previous = id; activeID = id; interestedInAttributes |= id.interestedInAttributes; interestedInNamespaces |= id.interestedInNamespaces; interestedInText |= id.interestedInText>0; } id = id.previous; }while(id!=null); if(activeID!=null) activeID.previous = null; EventID current = this.current; if(current!=null && current.axisEntryCount!=0){ current.previous = firstID; current.listenersAdded(); interestedInAttributes |= current.interestedInAttributes; interestedInNamespaces |= current.interestedInNamespaces; interestedInText |= current.interestedInText>0; }else this.current = firstID; this.interestedInAttributes = interestedInAttributes; this.interestedInNamespaces = interestedInNamespaces; this.interestedInText = interestedInText; } private void firePush(){ EventID id = current; EventID firstID = null; EventID activeID = null; boolean interestedInAttributes = false; boolean interestedInNamespaces = false; boolean interestedInText = false; do{ if(!id.push()){ if(firstID==null) firstID = id; else activeID.previous = id; activeID = id; interestedInAttributes |= id.interestedInAttributes; interestedInNamespaces |= id.interestedInNamespaces; interestedInText |= id.interestedInText>0; } id = id.previous; }while(id!=null); if(activeID!=null) activeID.previous = null; current = firstID; this.interestedInAttributes = interestedInAttributes; this.interestedInNamespaces = interestedInNamespaces; this.interestedInText = interestedInText; } private void firePop(){ EventID id = current; EventID firstID = null; EventID activeID = null; boolean interestedInAttributes = false; boolean interestedInNamespaces = false; boolean interestedInText = false; boolean doc = tailInfo==null; do{ if(!id.pop(doc)){ if(firstID==null) firstID = id; else activeID.previous = id; activeID = id; interestedInAttributes |= id.interestedInAttributes; interestedInNamespaces |= id.interestedInNamespaces; interestedInText |= id.interestedInText>0; } id = id.previous; }while(id!=null); if(activeID!=null) activeID.previous = null; current = firstID; this.interestedInAttributes = interestedInAttributes; this.interestedInNamespaces = interestedInNamespaces; this.interestedInText = interestedInText; } /*-------------------------------------------------[ NodeItem ]---------------------------------------------------*/ private NodeItem nodeItem; public NodeItem nodeItem(){ if(nodeItem==null) nodeItem = new NodeItem(this); return nodeItem; } /*-------------------------------------------------[ XMLBuilder ]---------------------------------------------------*/ private XMLBuilder xmlBuilder = null; public void setXMLBuilder(XMLBuilder xmlBuilder){ this.xmlBuilder = xmlBuilder; } @SuppressWarnings({"SimplifiableIfStatement"}) private boolean isXMLRequired(){ if(xmlBuilder==null) return false; else if(xmlBuilder.active) return true; else return xmlBuilder.active = nodeItem!=null; } private void notifyXMLBuilder(){ if(xmlBuilder==null) return; else if(!xmlBuilder.active && nodeItem==null) return; Object xml = xmlBuilder.onEvent(this); if(nodeItem!=null){ nodeItem.xml = xml; nodeItem.xmlBuilt = true; finishedXMLBuild(nodeItem); } } /*-------------------------------------------------[ NodeSetListener ]---------------------------------------------------*/ @Override public void mayHit(){ nodeItem.refCount++; } @Override public void discard(long order){ xmlBuilder.discard(order); } @Override public void finished(){} /*-------------------------------------------------[ Results ]---------------------------------------------------*/ private final Object results[]; private final int pendingInstantResults[]; private final BitSet finished; private int pendingExpressions; public static final RuntimeException STOP_PARSING = new RuntimeException("STOP_PARSING"); private boolean stopped; @Override public void finished(Evaluation evaluation){ assert evaluation.expression.scope()==Scope.DOCUMENT; assert hasInstantListener(evaluation.expression) ? evaluation.getResult()==null : evaluation.getResult()!=null; assert pendingExpressions>0; int id = evaluation.expression.id; assert results[id]==null || results[id]==evaluation; // null for StaticEvaluation // store result if this doc expression is used in some predicate boolean needEvaluation = false; finished.set(id); List<EvaluationListener> listeners = this.listeners[id]; if(listeners!=null){ boolean hasPendingInstantResults = pendingInstantResults[id]!=0; boolean clearListeners = true; for(EvaluationListener listener: listeners){ if(!listener.disposed){ if(listener instanceof InstantEvaluationListener && hasPendingInstantResults){ clearListeners = false; continue; } listener.finished(evaluation); } } if(clearListeners) this.listeners[id] = null; else needEvaluation = true; } if(!needEvaluation) results[id] = evaluation.expression.storeResult ? evaluation.getResult() : null; if(--pendingExpressions==0 && tailInfo!=null){ tailInfo = null; if(xmlBuilder==null) throw STOP_PARSING; stopped = true; } } public static final Object DUMMY_VALUE = new Object(); public void onInstantResult(Expression expression, NodeItem nodeItem){ BitSet bitSet = nodeItem.expressions; if(bitSet==null){ nodeItem.expressions = bitSet = new BitSet(); bitSet.set(expression.id); }else if(bitSet.get(expression.id)) return; else bitSet.set(expression.id); if(xmlBuilder==null || nodeItem.xmlBuilt) fireInstantResult(expression, nodeItem); else pendingInstantResults[expression.id]++; } private void fireInstantResult(Expression expression, NodeItem nodeItem){ if(expression.getXPath()==null) // non-user given absolute xpath return; List<EvaluationListener> listeners = this.listeners[expression.id]; for(EvaluationListener listener: listeners){ if(!listener.disposed && listener instanceof InstantEvaluationListener) ((InstantEvaluationListener)listener).onNodeHit(expression, nodeItem); } } public void finishedXMLBuild(NodeItem nodeItem){ nodeItem.xmlBuilt = true; BitSet bitSet = nodeItem.expressions; if(bitSet==null) return; int i=0; while(true){ i = bitSet.nextSetBit(i); if(i==-1) return; fireInstantResult(exprList.get(i), nodeItem); pendingInstantResults[i]--; if(finished.get(i) && pendingInstantResults[i]==0){ List<EvaluationListener> listeners = this.listeners[i]; if(listeners!=null){ this.listeners[i] = null; for(EvaluationListener listener: listeners){ if(!listener.disposed && listener instanceof InstantEvaluationListener) listener.finished((Evaluation)results[i]); } results[i] = null; } } i++; } } private EvaluationListener listener; public void setListener(EvaluationListener listener){ if(this.listener!=null) throw new IllegalStateException("EvaluationListener can be set only once"); this.listener = listener; for(Expression expr: exprList) addListener(expr, this.listener); } public EvaluationListener getListener(){ return listener; } private final List<EvaluationListener> listeners[]; private final int instantListenersCount[]; public Evaluation addListener(Expression expr, EvaluationListener evaluationListener){ assert expr.scope()==Scope.DOCUMENT; int id = expr.id; List<EvaluationListener> listeners = this.listeners[id]; if(listeners==null) this.listeners[id] = listeners=new ArrayList<EvaluationListener>(); listeners.add(evaluationListener); if(supportsInstantResults(expr) && evaluationListener instanceof InstantEvaluationListener) instantListenersCount[id]++; Object value = results[id]; if(value instanceof Evaluation) return (Evaluation)value; else{ if(value==null) return null; else throw new IllegalStateException(); } } public void removeListener(Expression expr, EvaluationListener evaluationListener){ List<EvaluationListener> listeners = this.listeners[expr.id]; if(listeners!=null){ if(listeners.remove(evaluationListener)){ if(supportsInstantResults(expr) && evaluationListener instanceof InstantEvaluationListener) instantListenersCount[expr.id]--; } }else evaluationListener.disposed = true; } private boolean supportsInstantResults(Expression expr){ return expr instanceof NodeSet; } public boolean hasInstantListener(Expression expr){ return supportsInstantResults(expr) && instantListenersCount[expr.id]>0; } public Object result(Expression expr){ assert expr.scope()==Scope.DOCUMENT; return results[expr.id]; } /*-------------------------------------------------[ OnEvent ]---------------------------------------------------*/ void setData(int type, String namespaceURI, String localName, String qualifiedName, String value){ nodeItem = null; this.type = type; this.namespaceURI = namespaceURI; this.localName = localName; this.qualifiedName = qualifiedName; this.value = value; } private void onEvent(int type, String namespaceURI, String localName, String qualifiedName, String value){ nodeItem = null; order++; this.type = type; this.namespaceURI = namespaceURI; this.localName = localName; this.qualifiedName = qualifiedName; this.value = value; if(!stopped && type!=NodeType.ELEMENT) fireEvent(); } public void onStartDocument(){ if(listener!=null){ for(Expression expr: globalExprList) listener.finished(new StaticEvaluation<Expression>(expr, -1, expr.getResult())); } int noOfXPaths = exprList.size(); if(noOfXPaths==0) throw STOP_PARSING; pendingExpressions = noOfXPaths; nsContext = new DefaultNamespaceContext(); locationInfo = tailInfo = new Info(); tailInfo.lang = ""; tailInfo.slash = 0; order = 0L; type = NodeType.DOCUMENT; value = namespaceURI = localName = qualifiedName = ""; Object results[] = this.results; for(int i=noOfXPaths-1; i>=0; i--){ Expression expression = exprList.get(i); Object result = expression.getResult(this); if(result instanceof Evaluation){ results[i] = result; Evaluation eval = (Evaluation)result; eval.addListener(this); if(xmlBuilder!=null && expression.resultType==DataType.NODESET && eval instanceof NodeSetListener.Support) ((Support)eval).setNodeSetListener(this); eval.start(); }else{ Evaluation eval; if(expression.resultType==DataType.NODESET && hasInstantListener(expression)){ onInstantResult(expression, nodeItem); results[i] = eval = new StaticEvaluation<Expression>(expression, order, null); }else eval = new StaticEvaluation<Expression>(expression, order, result); finished(eval); } } current.listenersAdded(); firePush(); if(isXMLRequired()) nodeItem.xml = xmlBuilder.doStartDocument(nodeItem); else if(stopped) throw STOP_PARSING; } public void onEndDocument(){ if(!stopped) pop(); assert pendingExpressions==0; assert tailInfo==null; if(xmlBuilder!=null) xmlBuilder.doEndDocument(this); } public void onStartElement(String uri, String localName, String qualifiedName, String lang){ onEvent(NodeType.ELEMENT, uri, localName, qualifiedName, null); if(!stopped){ Info info = new Info(); info.elem = qname(uri, localName); info.elemntPos = tailInfo.updateElementPosition(info.elem); info.lang = lang!=null ? lang : language(); push(info); fireEvent(); } if(!stopped) firePush(); if(isXMLRequired()){ Object xml = xmlBuilder.doStartElement(this, nodeItem); if(nodeItem!=null) nodeItem.xml = xml; }else if(stopped) throw STOP_PARSING; } public void onEndElement(){ if(!stopped) pop(); if(xmlBuilder!=null && xmlBuilder.active){ if(xmlBuilder.doEndElement(this)==null && stopped) throw STOP_PARSING; } } public void onText(){ if(buff.length()>0){ if(!stopped) tailInfo.textCount++; if(xmlBuilder!=null || interestedInText) onEvent(NodeType.TEXT, "", "", "", null); notifyXMLBuilder(); buff.setLength(0); } } public void onComment(char[] ch, int start, int length){ if(!stopped) tailInfo.commentCount++; onEvent(NodeType.COMMENT, "", "", "", new String(ch, start, length)); notifyXMLBuilder(); } public void onPI(String target, String data){ if(!stopped) tailInfo.updatePIPosition(target); onEvent(NodeType.PI, "", target, target, data); notifyXMLBuilder(); } public void onAttributes(Attributes attrs){ if(interestedInAttributes){ int len = attrs.getLength(); for(int i=0; i<len; i++){ onEvent(NodeType.ATTRIBUTE, attrs.getURI(i), attrs.getLocalName(i), attrs.getQName(i), attrs.getValue(i)); notifyXMLBuilder(); } }else if(xmlBuilder!=null && xmlBuilder.active) xmlBuilder.onAttributes(this, attrs); } public void onAttributes(XMLStreamReader reader){ if(interestedInAttributes){ int len = reader.getAttributeCount(); for(int i=0; i<len; i++){ String prefix = reader.getAttributePrefix(i); String localName = reader.getAttributeLocalName(i); String qname = prefix.length()==0 ? localName : prefix+':'+localName; String uri = reader.getAttributeNamespace(i); if(uri==null) uri = ""; onEvent(NodeType.ATTRIBUTE, uri, localName, qname, reader.getAttributeValue(i)); notifyXMLBuilder(); } }else if(xmlBuilder!=null && xmlBuilder.active) xmlBuilder.onAttributes(this, reader); } public void onNamespaces(MyNamespaceSupport nsSupport){ if(interestedInNamespaces){ Enumeration<String> prefixes = nsSupport.getPrefixes(); while(prefixes.hasMoreElements()){ String prefix = prefixes.nextElement(); String uri = nsSupport.getURI(prefix); onEvent(NodeType.NAMESPACE, "", prefix, prefix, uri); notifyXMLBuilder(); } }else if(xmlBuilder!=null && xmlBuilder.active) xmlBuilder.onNamespaces(this, nsSupport); } /*-------------------------------------------------[ Stack ]---------------------------------------------------*/ private Info tailInfo; private void push(Info info){ info.prev = tailInfo; tailInfo.next = info; tailInfo = info; } private void pop(){ Info curTailInfo = tailInfo; if(curTailInfo.slash!=-1) elementLocation.setLength(curTailInfo.slash); if(locationInfo==curTailInfo) locationInfo = curTailInfo.prev; tailInfo = curTailInfo.prev; if(tailInfo!=null) tailInfo.next = null; firePop(); } static final class IntWrapper{ int value = 1; } static final class Info{ Info prev; Info next; int slash = -1; String elem; String lang; int elemntPos = 1; private static int updatePosition(Map<String, IntWrapper> map, String key){ IntWrapper position = map.get(key); if(position==null){ map.put(key, new IntWrapper()); return 1; }else return ++position.value; } Map<String, IntWrapper> elemMap; public int updateElementPosition(String qname){ if(elemMap==null) elemMap = new HashMap<String, IntWrapper>(); return updatePosition(elemMap, qname); } Map<String, IntWrapper> piMap; public int updatePIPosition(String target){ if(piMap==null) piMap = new HashMap<String, IntWrapper>(); return updatePosition(piMap, target); } int textCount; int commentCount; } /*-------------------------------------------------[ NamespaceContext ]---------------------------------------------------*/ private DefaultNamespaceContext nsContext; public final NamespaceContext givenNSContext; private String qname(String uri, String name){ String prefix = nsContext.getPrefix(uri); if(prefix==null){ prefix = givenNSContext.getPrefix(uri); if(prefix!=null) nsContext.declarePrefix(prefix, uri); else prefix = nsContext.declarePrefix(uri); } if(prefix.length()==0) return name; else{ // doing: // return prefix+';'+name; // manually to avoid StringBuilder creation int prefixLen = prefix.length(); int nameLen = name.length(); char ch[] = new char[prefixLen+1+nameLen]; prefix.getChars(0, prefixLen, ch, 0); ch[prefixLen] = ':'; name.getChars(0, name.length(), ch, prefixLen+1); assert new String(ch).equals(prefix+':'+name); return new String(ch); } } /*-------------------------------------------------[ StringContent ]---------------------------------------------------*/ public final StringBuilder buff = new StringBuilder(500); public void appendText(char[] ch, int start, int length){ if(xmlBuilder!=null || interestedInText) buff.append(ch, start, length); else if(buff.length()==0) buff.append('x'); } /*-------------------------------------------------[ Evaluation Helper ]---------------------------------------------------*/ public Evaluation evaluation; public Object evaluate(Expression expr){ evaluation = null; switch(expr.scope()){ case Scope.GLOBAL: return expr.getResult(); case Scope.DOCUMENT: Object value = results[expr.id]; return value instanceof Evaluation ? null : value; default: assert expr.scope()==Scope.LOCAL; Object result = expr.getResult(this); assert result!=null; if(result instanceof Evaluation){ evaluation = (Evaluation)result; return null; }else return result; } } public ArrayDeque<PositionTracker> positionTrackerStack = new ArrayDeque<PositionTracker>(); public StringEvaluation stringEvaluation; }