/* * eXist Native XML Database * Copyright (C) 2001-06, Wolfgang M. Meier (wolfgang@exist-db.org) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * $Id$ */ package org.exist.xquery; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Namespaces; import org.exist.dom.QName; import org.exist.dom.memtree.MemTreeBuilder; import org.exist.dom.memtree.NodeImpl; import org.exist.util.XMLChar; import org.exist.xquery.util.ExpressionDumper; import org.exist.xquery.value.Item; import org.exist.xquery.value.QNameValue; import org.exist.xquery.value.Sequence; import org.exist.xquery.value.StringValue; import org.exist.xquery.value.Type; import org.xml.sax.helpers.AttributesImpl; import javax.xml.XMLConstants; import java.util.Iterator; /** * Constructor for element nodes. This class handles both, direct and dynamic * element constructors. * * @author wolf */ public class ElementConstructor extends NodeConstructor { private Expression qnameExpr; private PathExpr content = null; private AttributeConstructor attributes[] = null; private QName namespaceDecls[] = null; protected final static Logger LOG = LogManager.getLogger(ElementConstructor.class); public ElementConstructor(XQueryContext context) { super(context); } public ElementConstructor(XQueryContext context, String qname) { super(context); this.qnameExpr = new LiteralValue(context, new StringValue(qname)); } public void setContent(PathExpr path) { this.content = path; this.content.setUseStaticContext(true); } public PathExpr getContent() { return content; } public AttributeConstructor[] getAttributes() { return attributes; } public void setNameExpr(Expression expr) { //Deferred atomization (we could have a QNameValue) //this.qnameExpr = new Atomize(context, expr); this.qnameExpr = expr; } public Expression getNameExpr() { return qnameExpr; } public void addAttribute(AttributeConstructor attr) throws XPathException { if(attr.isNamespaceDeclaration()) { if("xmlns".equals(attr.getQName())) {addNamespaceDecl("", attr.getLiteralValue());} else {addNamespaceDecl(QName.extractLocalName(attr.getQName()), attr.getLiteralValue());} } else if(attributes == null) { attributes = new AttributeConstructor[1]; attributes[0] = attr; } else { AttributeConstructor natts[] = new AttributeConstructor[attributes.length + 1]; System.arraycopy(attributes, 0, natts, 0, attributes.length); natts[attributes.length] = attr; attributes = natts; } } public void addNamespaceDecl(String name, String uri) throws XPathException { final QName qn = new QName(name, uri, "xmlns"); if ( ("xml".equals(name) && !Namespaces.XML_NS.equals(uri)) || ("xmlns".equals(name) && ! "".equals(uri))) { throw new XPathException(this, ErrorCodes.XQST0070, "can not redefine '" + qn + "'"); } if (Namespaces.XML_NS.equals(uri) && !"xml".equals(name)) {throw new XPathException(this, ErrorCodes.XQST0070, "'"+Namespaces.XML_NS+"' can bind only to 'xml' prefix");} if (Namespaces.XMLNS_NS.equals(uri) && !"xmlns".equals(name)) {throw new XPathException(this, ErrorCodes.XQST0070, "'"+Namespaces.XMLNS_NS+"' can bind only to 'xmlns' prefix");} if (name.length()!=0 && uri.trim().length()==0) { throw new XPathException(this, ErrorCodes.XQST0085, "cannot undeclare a prefix "+name+"."); } addNamespaceDecl(qn); } private void addNamespaceDecl(QName qn) throws XPathException { if(namespaceDecls == null) { namespaceDecls = new QName[1]; namespaceDecls[0] = qn; } else { for(int i = 0; i < namespaceDecls.length; i++) { if (qn.equals(namespaceDecls[i])) {throw new XPathException(this, ErrorCodes.XQST0071, "duplicate definition for '" + qn + "'");} } QName decls[] = new QName[namespaceDecls.length + 1]; System.arraycopy(namespaceDecls, 0, decls, 0, namespaceDecls.length); decls[namespaceDecls.length] = qn; namespaceDecls = decls; } //context.inScopeNamespaces.put(qn.getLocalPart(), qn.getNamespaceURI()); } /* (non-Javadoc) * @see org.exist.xquery.Expression#analyze(org.exist.xquery.AnalyzeContextInfo) */ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { super.analyze(contextInfo); context.pushInScopeNamespaces(); // declare namespaces if(namespaceDecls != null) { for(int i = 0; i < namespaceDecls.length; i++) { if ("".equals(namespaceDecls[i].getNamespaceURI())) { // TODO: the specs are unclear here: should we throw XQST0085 or not? context.inScopeNamespaces.remove(namespaceDecls[i].getLocalPart()); // if (context.inScopeNamespaces.remove(namespaceDecls[i].getLocalPart()) == null) // throw new XPathException(getASTNode(), "XQST0085 : can not undefine '" + namespaceDecls[i] + "'"); } else {context.declareInScopeNamespace(namespaceDecls[i].getLocalPart(), namespaceDecls[i].getNamespaceURI());} } } final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); newContextInfo.setParent(this); newContextInfo.addFlag(IN_NODE_CONSTRUCTOR); qnameExpr.analyze(newContextInfo); if(attributes != null) { for(int i = 0; i < attributes.length; i++) { attributes[i].analyze(newContextInfo); } } if(content != null) {content.analyze(newContextInfo);} context.popInScopeNamespaces(); } /* (non-Javadoc) * @see org.exist.xquery.Expression#eval(org.exist.xquery.value.Sequence, org.exist.xquery.value.Item) */ public Sequence eval( Sequence contextSequence, Item contextItem) throws XPathException { context.expressionStart(this); context.pushInScopeNamespaces(); if (newDocumentContext) {context.pushDocumentContext();} try { final MemTreeBuilder builder = context.getDocumentBuilder(); // declare namespaces if(namespaceDecls != null) { for(int i = 0; i < namespaceDecls.length; i++) { //if ("".equals(namespaceDecls[i].getNamespaceURI())) { // TODO: the specs are unclear here: should we throw XQST0085 or not? // context.inScopeNamespaces.remove(namespaceDecls[i].getLocalPart()); // if (context.inScopeNamespaces.remove(namespaceDecls[i].getLocalPart()) == null) // throw new XPathException(getAS TNode(), "XQST0085 : can not undefine '" + namespaceDecls[i] + "'"); //} else context.declareInScopeNamespace(namespaceDecls[i].getLocalPart(), namespaceDecls[i].getNamespaceURI()); } } // process attributes final AttributesImpl attrs = new AttributesImpl(); if(attributes != null) { AttributeConstructor constructor; Sequence attrValues; QName attrQName; // first, search for xmlns attributes and declare in-scope namespaces for (int i = 0; i < attributes.length; i++) { constructor = attributes[i]; if(constructor.isNamespaceDeclaration()) { final int p = constructor.getQName().indexOf(':'); if(p == Constants.STRING_NOT_FOUND) {context.declareInScopeNamespace("", constructor.getLiteralValue());} else { final String prefix = constructor.getQName().substring(p + 1); context.declareInScopeNamespace(prefix, constructor.getLiteralValue()); } } } String v = null; // process the remaining attributes for (int i = 0; i < attributes.length; i++) { context.proceed(this, builder); constructor = attributes[i]; attrValues = constructor.eval(contextSequence, contextItem); attrQName = QName.parse(context, constructor.getQName(), ""); final String namespaceURI = attrQName.getNamespaceURI(); if (namespaceURI != null && !namespaceURI.isEmpty() && attrQName.getPrefix() == null) { String prefix = context.getPrefixForURI(namespaceURI); if (prefix != null) { attrQName = new QName(attrQName.getLocalPart(), attrQName.getNamespaceURI(), prefix); } else { //generate prefix for (final int n = 1; i < 100; i++) { prefix = "eXnsp"+n; if (context.getURIForPrefix(prefix) == null) { attrQName = new QName(attrQName.getLocalPart(), attrQName.getNamespaceURI(), prefix); break; } prefix = null; } if (prefix == null) {throw new XPathException(this, "Prefix can't be generate.");} } } if (attrs.getIndex(attrQName.getNamespaceURI(), attrQName.getLocalPart()) != -1) {throw new XPathException(this, ErrorCodes.XQST0040, "'" + attrQName.getLocalPart() + "' is a duplicate attribute name");} v = DynamicAttributeConstructor.normalize(this, attrQName, attrValues.getStringValue()); attrs.addAttribute(attrQName.getNamespaceURI(), attrQName.getLocalPart(), attrQName.getStringValue(), "CDATA", v); } } context.proceed(this, builder); // create the element final Sequence qnameSeq = qnameExpr.eval(contextSequence, contextItem); if(!qnameSeq.hasOne()) {throw new XPathException(this, ErrorCodes.XPTY0004, "Type error: the node name should evaluate to a single item");} final Item qnitem = qnameSeq.itemAt(0); QName qn; if (qnitem instanceof QNameValue) { qn = ((QNameValue)qnitem).getQName(); } else { //Do we have the same result than Atomize there ? -pb try { qn = QName.parse(context, qnitem.getStringValue()); } catch (final IllegalArgumentException e) { throw new XPathException(this, ErrorCodes.XPTY0004, "" + qnitem.getStringValue() + "' is not a valid element name"); } catch (final XPathException e) { e.setLocation(getLine(), getColumn(), getSource()); throw e; } //Use the default namespace if specified /* if (qn.getPrefix() == null && context.inScopeNamespaces.get("xmlns") != null) { qn.setNamespaceURI((String)context.inScopeNamespaces.get("xmlns")); } */ if (qn.getPrefix() == null && context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX) != null) { qn = new QName(qn.getLocalPart(), context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX), qn.getPrefix()); } } //Not in the specs but... makes sense if(!XMLChar.isValidName(qn.getLocalPart())) {throw new XPathException(this, ErrorCodes.XPTY0004, "'" + qnitem.getStringValue() + "' is not a valid element name");} // add namespace declaration nodes final int nodeNr = builder.startElement(qn, attrs); if(namespaceDecls != null) { for(int i = 0; i < namespaceDecls.length; i++) { builder.namespaceNode(namespaceDecls[i]); } } // do we need to add a namespace declaration for the current node? if (qn.hasNamespace()) { if (context.getInScopePrefix(qn.getNamespaceURI()) == null) { String prefix = qn.getPrefix(); if (prefix == null || prefix.length() == 0) {prefix = "";} context.declareInScopeNamespace(prefix, qn.getNamespaceURI()); builder.namespaceNode(new QName(prefix, qn.getNamespaceURI(), "xmlns")); } } else if ((qn.getPrefix() == null || qn.getPrefix().length() == 0) && context.getInheritedNamespace("") != null) { context.declareInScopeNamespace("", ""); builder.namespaceNode(new QName("", "", "xmlns")); } // process element contents if(content != null) { content.eval(contextSequence, contextItem); } builder.endElement(); final NodeImpl node = builder.getDocument().getNode(nodeNr); return node; } finally { context.popInScopeNamespaces(); if (newDocumentContext) {context.popDocumentContext();} context.expressionEnd(this); } } /* (non-Javadoc) * @see org.exist.xquery.Expression#dump(org.exist.xquery.util.ExpressionDumper) */ public void dump(ExpressionDumper dumper) { dumper.display("element "); //TODO : remove curly braces if Qname dumper.display("{"); qnameExpr.dump(dumper); dumper.display("} "); dumper.display("{"); dumper.startIndent(); if(attributes != null) { AttributeConstructor attr; for(int i = 0; i < attributes.length; i++) { if(i > 0) {dumper.nl();} attr = attributes[i]; attr.dump(dumper); } dumper.endIndent(); dumper.startIndent(); } if(content != null) { for(final Iterator<Expression> i = content.steps.iterator(); i.hasNext(); ) { final Expression expr = i.next(); expr.dump(dumper); if(i.hasNext()) {dumper.nl();} } dumper.endIndent().nl(); } dumper.display("} "); } public String toString() { final StringBuilder result = new StringBuilder(); result.append("element "); //TODO : remove curly braces if Qname result.append("{"); result.append(qnameExpr.toString()); result.append("} "); result.append("{"); if(attributes != null) { AttributeConstructor attr; for(int i = 0; i < attributes.length; i++) { if(i > 0) {result.append(" ");} attr = attributes[i]; result.append(attr.toString()); } } if(content != null) { for(final Iterator<Expression> i = content.steps.iterator(); i.hasNext(); ) { final Expression expr = i.next(); result.append(expr.toString()); if(i.hasNext()) {result.append(" ");} } } result.append("} "); return result.toString(); } /* (non-Javadoc) * @see org.exist.xquery.AbstractExpression#setPrimaryAxis(int) */ public void setPrimaryAxis(int axis) { } @Override public int getPrimaryAxis() { if (content != null) { return content.getPrimaryAxis(); } return Constants.UNKNOWN_AXIS; } /* (non-Javadoc) * @see org.exist.xquery.AbstractExpression#resetState() */ public void resetState(boolean postOptimization) { super.resetState(postOptimization); qnameExpr.resetState(postOptimization); if(content != null) {content.resetState(postOptimization);} if(attributes != null) for(int i = 0; i < attributes.length; i++) { final Expression next = attributes[i]; next.resetState(postOptimization); } } public void accept(ExpressionVisitor visitor) { visitor.visitElementConstructor(this); } public int returnsType() { return Type.ELEMENT; } }