/** * 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.sax.dog.DataType; import jlibs.xml.sax.dog.Scope; import jlibs.xml.sax.dog.expr.Expression; import jlibs.xml.sax.dog.expr.Literal; import jlibs.xml.sax.dog.expr.Variable; import jlibs.xml.sax.dog.expr.func.Function; import jlibs.xml.sax.dog.expr.func.FunctionCall; import jlibs.xml.sax.dog.expr.func.Functions; import jlibs.xml.sax.dog.expr.nodset.ExactPosition; import jlibs.xml.sax.dog.expr.nodset.Language; import jlibs.xml.sax.dog.expr.nodset.Last; import jlibs.xml.sax.dog.expr.nodset.Position; import jlibs.xml.sax.dog.path.*; import jlibs.xml.sax.dog.path.tests.*; import org.jaxen.saxpath.Operator; import org.jaxen.saxpath.SAXPathException; import org.jaxen.saxpath.XPathHandler; import org.jaxen.saxpath.XPathReader; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPathFunction; import javax.xml.xpath.XPathFunctionResolver; import javax.xml.xpath.XPathVariableResolver; import java.util.ArrayDeque; import java.util.ArrayList; /** * @author Santhosh Kumar T */ public final class XPathParser implements XPathHandler{ private final NamespaceContext nsContext; private final XPathVariableResolver variableResolver; private final XPathFunctionResolver functionResolver; private final XPathReader reader = new org.jaxen.saxpath.base.XPathReader(); public XPathParser(NamespaceContext nsContext, XPathVariableResolver variableResolver, XPathFunctionResolver functionResolver){ this.nsContext = nsContext; this.variableResolver = variableResolver; this.functionResolver = functionResolver; reader.setXPathHandler(this); } private boolean allowDefaultPrefixMapping = false; public boolean isAllowDefaultPrefixMapping(){ return allowDefaultPrefixMapping; } public void setAllowDefaultPrefixMapping(boolean allow){ allowDefaultPrefixMapping = allow; } private boolean documentContext; public Expression parse(String xpath, boolean documentContext) throws SAXPathException{ this.documentContext = documentContext; frames.clear(); peekFrame = null; stepStack.clear(); predicateDepth = 0; expr = null; reader.parse(xpath); return expr; } /*-------------------------------------------------[ XPath ]---------------------------------------------------*/ @Override public void startXPath(){ pushFrame(); } /** * the expression object created for currently parsing xpath is saved * in this variable by endXPath() */ private Expression expr; @Override public void endXPath(){ Object current = pop(); if(current instanceof Expression) expr = (Expression)current; else expr = ((LocationPath)current).typeCast(DataType.NODESET).simplify(); } /*-------------------------------------------------[ LocationPath ]---------------------------------------------------*/ @Override public void startAbsoluteLocationPath(){ pushFrame(); } @Override public void endAbsoluteLocationPath(){ endLocationPath(Scope.DOCUMENT, popFrame()); } @Override public void startRelativeLocationPath(){ pushFrame(); } @Override public void endRelativeLocationPath(){ ArrayDeque steps = popFrame(); int scope; if(documentContext){ if(peekFrame.size()==2 && peekFrame.getFirst()==PATH_FLAG && peekFrame.getLast() instanceof LocationPath) scope = Scope.LOCAL; else scope = predicateDepth==0 ? Scope.DOCUMENT : Scope.LOCAL; }else scope = Scope.LOCAL; endLocationPath(scope, steps); } @SuppressWarnings({"unchecked"}) private void endLocationPath(int scope, ArrayDeque steps){ LocationPath path = new LocationPath(scope, steps.size()); steps.toArray(path.steps); push(LocationPathAnalyzer.simplify(path)); } /*-------------------------------------------------[ Steps ]---------------------------------------------------*/ /** * This array is used to convert jaxen's axis constant to xmldog's axis constant.<br> * for example:<br><blockquote> * <code>axisMap[org.jaxen.saxpath.Axis.CHILD]</code> returns <code>jlibs.xml.sax.dog.path.Axis.CHILD</code> * </blockquote> * <p><br> * it contains <code>-1</code> if that axis is not supported by xmldog.<br> * for example:<br><blockquote> * <code>axisMap[org.jaxen.saxpath.Axis.PARENT]</code> returns <code>-1</code> * </blockquote> */ private static final int axisMap[] = { -1, //INVALID_AXIS Axis.CHILD, Axis.DESCENDANT, -1, //PARENT -1, //ANCESTOR Axis.FOLLOWING_SIBLING, -1, //PRECEDING_SIBLING Axis.FOLLOWING, -1, //PRECEDING Axis.ATTRIBUTE, Axis.NAMESPACE, Axis.SELF, Axis.DESCENDANT_OR_SELF, -1, //ANCESTOR_OR_SELF }; /** * This is used to track the step we are in. When step starts it is pushed and popped when it ends. * <p><br> * {@link jlibs.xml.sax.dog.sniff.XPathParser#createFunction(String, int) createFunction(String, int)} uses current step * to do some optimizations for position and last functions. */ private ArrayDeque<Step> stepStack = new ArrayDeque<Step>(); private void startStep(int axis, Constraint constraint){ int dogAxis = axisMap[axis]; if(dogAxis==-1) throw new UnsupportedOperationException("Axis "+org.jaxen.saxpath.Axis.lookup(axis)+" is not supported"); Step step = new Step(dogAxis, constraint); push(step); stepStack.addLast(step); } private void endStep(){ stepStack.pollLast(); } @Override public void startNameStep(int axis, String prefix, String localName) throws SAXPathException{ Constraint constraint; boolean star = localName.equals("*"); if(star && prefix.length()==0) constraint = Star.INSTANCE; else{ String uri = !allowDefaultPrefixMapping && prefix.length()==0 ? "" : nsContext.getNamespaceURI(prefix); if(uri==null) throw new SAXPathException("undeclared prefix: " + prefix); if(star) constraint = namespaceURIStub.get(uri); else{ if("*".equals(uri)) constraint = localNameStub.get(localName); else constraint = qnameStub.get(uri, localName); } } startStep(axis, constraint); } @Override public void endNameStep(){ endStep(); } @Override public void startAllNodeStep(int axis){ startStep(axis, Node.INSTANCE); } @Override public void endAllNodeStep(){ endStep(); } @Override public void startTextNodeStep(int axis){ startStep(axis, Text.INSTANCE); } @Override public void endTextNodeStep(){ endStep(); } @Override public void startCommentNodeStep(int axis){ startStep(axis, Comment.INSTANCE); } @Override public void endCommentNodeStep(){ endStep(); } @Override public void startProcessingInstructionNodeStep(int axis, String name){ startStep(axis, name.length()==0 ? PI.INSTANCE : piTargetStub.get(name)); } @Override public void endProcessingInstructionNodeStep(){ endStep(); } /*-------------------------------------------------[ Predicate ]---------------------------------------------------*/ /** * represents depth of current predicate.<br> * when predicate starts it is incremented. and decrement on end.<br> * * All relative location-paths that are depth 0 should be treated as absolute. * for example: * "book/chapter/name" should be treated as "/book/chapter/name" * "sum(book/chapter/pages, book/chapter)" should be treated as "sum(/book/chapter/pages, /book/chapter)" * * i.e top level location paths should always be treated as absolute */ private int predicateDepth; @Override public void startPredicate(){ predicateDepth++; } @Override public void endPredicate(){ predicateDepth--; Object predicate = pop(); Predicated predicated = (Predicated)peek(); Expression predicateExpr; if(predicate instanceof Expression){ predicateExpr = (Expression)predicate; if(predicateExpr.resultType==DataType.NUMBER){ if(predicate instanceof Literal){ Double d = (Double)predicateExpr.getResult(); int pos = d.intValue(); if(d!=pos) predicateExpr = new Literal(Boolean.FALSE, DataType.BOOLEAN); else{ Step step = predicated instanceof Step ? (Step)predicated : null; if(step!=null && ( step.axis==Axis.SELF || ((step.axis==Axis.ATTRIBUTE || step.axis==Axis.NAMESPACE) && step.constraint instanceof QName) || step.predicateSet.getPredicate() instanceof ExactPosition)) predicateExpr = new Literal(pos==1, DataType.BOOLEAN); else predicateExpr = new ExactPosition(pos); } }else{ FunctionCall equals = new FunctionCall(Functions.NUMBER_EQUALS_NUMBER); equals.addValidMember(new Position(), 0); equals.addValidMember(predicateExpr, 1); predicateExpr = equals; } }else predicateExpr = Functions.typeCast(predicateExpr, DataType.BOOLEAN); }else predicateExpr = Functions.typeCast(predicate, DataType.BOOLEAN); if(predicated instanceof LocationPath){ LocationPath path = (LocationPath)predicated; if(path.contexts.size()==0){ LocationPath newPath = new LocationPath(Scope.LOCAL, 0); newPath.contexts.add(path); pop(); push(newPath); predicated = newPath; } } predicated.addPredicate(predicateExpr); } /*-------------------------------------------------[ Literals ]---------------------------------------------------*/ @Override public void literal(String literal){ push(new Literal(literal, DataType.STRING)); } @Override @SuppressWarnings({"UnnecessaryBoxing"}) public void number(int number){ push(new Literal(new Double(number), DataType.NUMBER)); } @Override @SuppressWarnings({"UnnecessaryBoxing"}) public void number(double number){ push(new Literal(new Double(number), DataType.NUMBER)); } /*-------------------------------------------------[ Operators ]---------------------------------------------------*/ @Override public void startUnaryExpr(){} @Override public void endUnaryExpr(int operator){ FunctionCall functionCall = new FunctionCall(Functions.MULTIPLY); functionCall.addValidMember(new Literal(-1d, DataType.NUMBER), 0); functionCall.addMember(pop(), 1); push(functionCall.simplify()); } @Override public void startEqualityExpr(){} @Override public void endEqualityExpr(int operator){ endBinaryOperator(operator==Operator.EQUALS ? Functions.EQUALS : Functions.NOT_EQUALS); } @Override public void startAdditiveExpr(){} @Override public void endAdditiveExpr(int operator){ endBinaryOperator(operator==Operator.ADD ? Functions.ADD : Functions.SUBSTRACT); } @Override public void startMultiplicativeExpr(){} @Override public void endMultiplicativeExpr(int operator){ Function function; if(operator==Operator.MULTIPLY) function = Functions.MULTIPLY; else if(operator==Operator.DIV) function = Functions.DIV; else function = Functions.MOD; endBinaryOperator(function); } @Override public void startRelationalExpr(){} @Override public void endRelationalExpr(int operator){ Function function; if(operator==Operator.LESS_THAN) function = Functions.LESS_THAN; else if(operator==Operator.LESS_THAN_EQUALS) function = Functions.LESS_THAN_EQUAL; else if(operator==Operator.GREATER_THAN) function = Functions.GREATER_THAN; else function = Functions.GREATER_THAN_EQUAL; endBinaryOperator(function); } @Override public void startAndExpr(){} @Override public void endAndExpr(boolean create){ if(create) endBinaryOperator(Functions.AND); } @Override public void startOrExpr(){} @Override public void endOrExpr(boolean create){ if(create) endBinaryOperator(Functions.OR); } private void endBinaryOperator(Function function){ FunctionCall functionCall = new FunctionCall(function); Object member2 = pop(); Object member1 = pop(); functionCall.addMember(member1, 0); functionCall.addMember(member2, 1); push(functionCall.simplify()); } @Override public void startFunction(String prefix, String name) throws SAXPathException{ String uri = prefix.length()==0 ? "": nsContext.getNamespaceURI(prefix); if(uri==null) throw new SAXPathException("undeclared prefix: " + prefix); if(uri.length()>0 && functionResolver==null) throw new SAXPathException("FunctionResolver is required"); pushFrame(); push(new javax.xml.namespace.QName(uri, name)); } @Override public void endFunction() throws SAXPathException{ ArrayDeque params = popFrame(); javax.xml.namespace.QName name = (javax.xml.namespace.QName)params.pollFirst(); if(name.getNamespaceURI().length()==0) push(createFunction(name.getLocalPart(), params).simplify()); else{ int noOfParams = params.size(); XPathFunction function = functionResolver.resolveFunction(name, noOfParams); if(function==null) throw new SAXPathException("Unknown Function: "+name); FunctionCall functionCall = new FunctionCall(new Functions.UserFunction(name.getNamespaceURI(), name.getLocalPart(), function), noOfParams); for(int i=0; i<noOfParams; i++) functionCall.addMember(params.pollFirst(), i); push(functionCall); } } /** * Tells whether any xpath parsed by this parser uses lang() function.<br> * This is used by xpath engine to avoid tracking of language when none of xpaths * used lang() function. */ public boolean langInterested; private Expression createFunction(String name, ArrayDeque params) throws SAXPathException{ int noOfParams = params.size(); switch(noOfParams){ case 0:{ LocationPath locationPath = predicateDepth==0 ? LocationPath.DOCUMENT_CONTEXT : LocationPath.LOCAL_CONTEXT; Expression expr = locationPath.apply(name); if(expr!=null) return expr; return createFunction(name, 0); } case 1:{ if(name.equals("lang")){ langInterested = true; FunctionCall functionCall = new FunctionCall(Functions.LANGUAGE_MATCH); functionCall.addValidMember(new Language(), 0); functionCall.addMember(params.pollFirst(), 1); return functionCall; } else{ Object current = params.pollFirst(); if(current instanceof LocationPath){ Expression expr = ((LocationPath)current).apply(name); if(expr!=null) return expr; } FunctionCall functionCall = (FunctionCall)createFunction(name, 1); functionCall.addMember(current, 0); return functionCall; } } default: FunctionCall functionCall = (FunctionCall)createFunction(name, noOfParams); for(int i = 0; i<noOfParams; i++) functionCall.addMember(params.pollFirst(), i); return functionCall; } } private Expression createFunction(String name, int noOfMembers) throws SAXPathException{ if(noOfMembers==0){ if(name.equals("position")){ Step step = stepStack.peekLast(); if(step!=null){ int axis = step.axis; if(axis==Axis.SELF || ((axis==Axis.ATTRIBUTE || axis==Axis.NAMESPACE) && step.constraint instanceof QName) || step.predicateSet.getPredicate() instanceof ExactPosition) return new Literal(DataType.ONE, DataType.NUMBER); } return new Position(); } else if(name.equals("last")){ Step step = stepStack.peekLast(); if(step!=null){ int axis = step.axis; if(axis==Axis.SELF || ((axis==Axis.ATTRIBUTE || axis==Axis.NAMESPACE) && step.constraint instanceof QName) || step.predicateSet.getPredicate() instanceof ExactPosition) return new Literal(DataType.ONE, DataType.NUMBER); } return new Last(); } else if(name.equals("true")) return new Literal(Boolean.TRUE, DataType.BOOLEAN); else if(name.equals("false")) return new Literal(Boolean.FALSE, DataType.BOOLEAN); } Function function = Functions.library.get(name); if(function==null) throw new SAXPathException("Unknown function: " + name); return new FunctionCall(function, noOfMembers); } /*-------------------------------------------------[ Pending ]---------------------------------------------------*/ @Override public void startUnionExpr(){ pushFrame(); } @Override public void endUnionExpr(boolean create){ ArrayDeque stack = popFrame(); if(create){ LocationPath result = new LocationPath(Scope.LOCAL, 0); result.addToContext((LocationPath)stack.pollFirst()); result.addToContext((LocationPath)stack.pollFirst()); push(result); }else push(stack.peek()); } private static final Object FILTER_FLAG = new Object(); @Override public void startFilterExpr(){ push(FILTER_FLAG); } @Override public void endFilterExpr(){ Object obj = pop(); if(pop()!=FILTER_FLAG) throw new NotImplementedException("FilterExpression"); push(obj); } private static final Object PATH_FLAG = "PATH_FLAG"; @Override public void startPathExpr(){ push(PATH_FLAG); } @Override public void endPathExpr(){ Object relative = pop(); Object context = pop(); if(relative instanceof LocationPath && context instanceof LocationPath){ ((LocationPath)relative).pathExpression = true; ((LocationPath)relative).addToContext((LocationPath)context); context = pop(); } if(context!=PATH_FLAG) throw new NotImplementedException("Path"); push(relative); } @Override public void variableReference(String prefix, String variableName) throws SAXPathException{ String uri = prefix.length()==0 ? "": nsContext.getNamespaceURI(prefix); if(uri==null) throw new SAXPathException("undeclared prefix: " + prefix); if(variableResolver==null) throw new SAXPathException("VariableResolver is required"); push(new Variable(variableResolver, new javax.xml.namespace.QName(uri, variableName))); } /*-------------------------------------------------[ Context ]---------------------------------------------------*/ /** * This manages stack of frames. on location-path and function start a frame is pushed, and on its end * it is popped. When a frame is popped, based on its contents Expression is created. * * During the start of xpath parsing a frame is pushed. on xpath parsing end, this frame contains the Expression * created for the given xpath. * * the frame is again a stack into which Expression or LocationPaths are pushed/popped. */ private ArrayDeque<ArrayDeque> frames = new ArrayDeque<ArrayDeque>(); /** * the peek frame is maintained in this variable, to improve performance. */ private ArrayDeque peekFrame; private void pushFrame(){ frames.addLast(peekFrame=new ArrayDeque()); } private ArrayDeque popFrame(){ ArrayDeque frame = frames.pollLast(); peekFrame = frames.peekLast(); return frame; } /** pushes into current frame */ @SuppressWarnings("unchecked") private void push(Object obj){ peekFrame.addLast(obj); } /** pops from current frame */ private Object pop(){ return peekFrame.pollLast(); } /** peeks into current frame */ private Object peek(){ return peekFrame.peekLast(); } /*-------------------------------------------------[ Stubs ]----------------------------------------------------------- XPath Parser reuses non-signleton Constraint instances. for example it never creates two QName instances with same value. This is achieved using Stub classes. For each non-singleton Constraint type, these is one stub implementation below. These stubs assign a unique id to each constraint they created. */ /** * this list contains all constraints created by parser. when a constraint is created its id is computed from * the size of this list. * * Stub classes also use this list to find whether the requested constraint is already created or not */ public ArrayList constraints = new ArrayList(); private NamespaceURIStub namespaceURIStub = new NamespaceURIStub(); private LocalNameStub localNameStub = new LocalNameStub(); private QNameStub qnameStub = new QNameStub(); private PITargetStub piTargetStub = new PITargetStub(); class NamespaceURIStub{ private String namespaceURI; @Override public boolean equals(Object obj){ return obj instanceof NamespaceURI && ((NamespaceURI)obj).namespaceURI.equals(namespaceURI); } @SuppressWarnings({"unchecked"}) public NamespaceURI get(String namespaceURI){ this.namespaceURI = namespaceURI; int index = constraints.indexOf(this); if(index!=-1) return (NamespaceURI)constraints.get(index); else{ NamespaceURI constraint = new NamespaceURI(Constraint.ID_START +constraints.size(), namespaceURI); constraints.add(constraint); return constraint; } } } class LocalNameStub{ private String localName; @Override public boolean equals(Object obj){ return obj instanceof LocalName && ((LocalName)obj).localName.equals(localName); } @SuppressWarnings({"unchecked"}) public LocalName get(String localName){ this.localName = localName; int index = constraints.indexOf(this); if(index!=-1) return (LocalName)constraints.get(index); else{ LocalName constraint = new LocalName(Constraint.ID_START +constraints.size(), localName); constraints.add(constraint); return constraint; } } } class QNameStub{ private String namespaceURI; private String localName; @Override public boolean equals(Object obj){ if(obj instanceof QName){ QName qname = (QName)obj; return qname.localName.equals(localName) && qname.namespaceURI.equals(namespaceURI); }else return false; } @SuppressWarnings({"unchecked"}) public QName get(String namespaceURI, String localName){ this.namespaceURI = namespaceURI; this.localName = localName; int index = constraints.indexOf(this); if(index!=-1) return (QName)constraints.get(index); else{ QName constraint = new QName(Constraint.ID_START +constraints.size(), namespaceURI, localName); constraints.add(constraint); return constraint; } } } class PITargetStub{ private String target; @Override public boolean equals(Object obj){ return obj instanceof PITarget && ((PITarget)obj).target.equals(target); } @SuppressWarnings({"unchecked"}) public PITarget get(String target){ this.target = target; int index = constraints.indexOf(this); if(index!=-1) return (PITarget)constraints.get(index); else{ PITarget constraint = new PITarget(Constraint.ID_START +constraints.size(), target); constraints.add(constraint); return constraint; } } } }