org.ballerinalang.model.values.BXMLItem.java Source code

Java tutorial

Introduction

Here is the source code for org.ballerinalang.model.values.BXMLItem.java

Source

/*
 * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * Licensed 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.ballerinalang.model.values;

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMComment;
import org.apache.axiom.om.OMDocument;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMProcessingInstruction;
import org.apache.axiom.om.OMText;
import org.apache.axiom.om.OMXMLBuilderFactory;
import org.apache.axiom.om.impl.common.OMChildrenQNameIterator;
import org.apache.axiom.om.impl.common.OMNamespaceImpl;
import org.apache.axiom.om.impl.dom.CommentImpl;
import org.apache.axiom.om.impl.dom.TextImpl;
import org.apache.axiom.om.impl.llom.OMDocumentImpl;
import org.apache.axiom.om.impl.llom.OMElementImpl;
import org.apache.axiom.om.impl.llom.OMProcessingInstructionImpl;
import org.apache.commons.lang3.StringEscapeUtils;
import org.ballerinalang.bre.bvm.BVM;
import org.ballerinalang.model.types.BMapType;
import org.ballerinalang.model.types.BType;
import org.ballerinalang.model.types.BTypes;
import org.ballerinalang.model.util.XMLNodeType;
import org.ballerinalang.model.util.XMLUtils;
import org.ballerinalang.model.util.XMLValidationUtils;
import org.ballerinalang.util.exceptions.BallerinaException;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;

import static org.ballerinalang.model.util.FreezeUtils.handleInvalidUpdate;
import static org.ballerinalang.model.util.FreezeUtils.isOpenForFreeze;
import static org.ballerinalang.util.BLangConstants.STRING_NULL_VALUE;

/**
 * {@code BXML} represents a single XML item in Ballerina.
 * XML item could be one of:
 * <ul>
 * <li>element</li>
 * <li>text</li>
 * <li>comment</li>
 * <li>processing instruction</li>
 * </ul>
 *
 * @since 0.88
 */
public final class BXMLItem extends BXML<OMNode> {

    private OMNode omNode;
    private XMLNodeType nodeType;

    /**
     * Create an empty XMLValue.
     */
    public BXMLItem() {
        omNode = new OMElementImpl();
        setXMLNodeType();
    }

    /**
     * Initialize a {@link BXMLItem} from a XML string.
     *
     * @param xmlValue A XML string
     */
    public BXMLItem(String xmlValue) {
        if (xmlValue == null) {
            return;
        }

        try {
            omNode = XMLUtils.stringToOM(xmlValue);
            setXMLNodeType();
        } catch (Throwable t) {
            handleXmlException("failed to create xml: ", t);
        }
    }

    /**
     * Initialize a {@link BXMLItem} from a {@link org.apache.axiom.om.OMNode} object.
     *
     * @param value xml object
     */
    public BXMLItem(OMNode value) {
        this.omNode = value;
        setXMLNodeType();
    }

    /**
     * Create a {@link BXMLItem} from a {@link InputStream}.
     *
     * @param inputStream Input Stream
     */
    public BXMLItem(InputStream inputStream) {
        if (inputStream == null) {
            return;
        }

        try {
            omNode = OMXMLBuilderFactory.createOMBuilder(XMLUtils.STAX_PARSER_CONFIGURATION, inputStream)
                    .getDocumentElement();
            setXMLNodeType();
        } catch (Throwable t) {
            handleXmlException("failed to create xml: ", t);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public XMLNodeType getNodeType() {
        return nodeType;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BBoolean isEmpty() {
        return new BBoolean(omNode == null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BBoolean isSingleton() {
        return new BBoolean(true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BString getItemType() {
        return new BString(nodeType.value());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BString getElementName() {
        if (nodeType == XMLNodeType.ELEMENT) {
            return new BString(((OMElement) omNode).getQName().toString());
        }

        return BTypes.typeString.getEmptyValue();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BString getTextValue() {
        return new BString(getTextValue(omNode));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getAttribute(String localName, String namespace) {
        return getAttribute(localName, namespace, XMLConstants.DEFAULT_NS_PREFIX);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getAttribute(String localName, String namespace, String prefix) {
        if (nodeType != XMLNodeType.ELEMENT || localName == null || localName.isEmpty()) {
            return STRING_NULL_VALUE;
        }
        QName attributeName = getQName(localName, namespace, prefix);
        OMAttribute attribute = ((OMElement) omNode).getAttribute(attributeName);

        if (attribute != null) {
            return attribute.getAttributeValue();
        }

        OMNamespace ns = ((OMElement) omNode).findNamespaceURI(localName);
        return ns == null ? STRING_NULL_VALUE : ns.getNamespaceURI();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setAttribute(String localName, String namespaceUri, String prefix, String value) {
        if (nodeType != XMLNodeType.ELEMENT) {
            return;
        }

        if (localName == null || localName.isEmpty()) {
            throw new BallerinaException("localname of the attribute cannot be empty");
        }

        // Validate whether the attribute name is an XML supported qualified name, according to the XML recommendation.
        XMLValidationUtils.validateXMLName(localName);
        XMLValidationUtils.validateXMLName(prefix);

        // If the attribute already exists, update the value.
        OMElement node = (OMElement) omNode;
        QName qname = getQName(localName, namespaceUri, prefix);
        OMAttribute attr = node.getAttribute(qname);
        if (attr != null) {
            attr.setAttributeValue(value);
            return;
        }

        // If the prefix is 'xmlns' then this is a namespace addition
        if (prefix != null && prefix.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
            node.declareNamespace(value, localName);
            return;
        }

        createAttribute(localName, namespaceUri, prefix, value, node);
    }

    private void createAttribute(String localName, String namespaceUri, String prefix, String value,
            OMElement node) {
        // If the namespace is null/empty, only the local part exists. Therefore add a simple attribute.
        if (namespaceUri == null || namespaceUri.isEmpty()) {
            node.addAttribute(localName, value, null);
            return;
        }

        if (!(prefix == null || prefix.isEmpty())) {
            OMNamespace existingNs = node.findNamespaceURI(prefix);

            // If a namespace exists with the same prefix but a different uri, then do not add the new attribute.
            if (existingNs != null && !namespaceUri.equals(existingNs.getNamespaceURI())) {
                throw new BallerinaException("failed to add attribute '" + prefix + ":" + localName + "'. prefix '"
                        + prefix + "' is already bound to namespace '" + existingNs.getNamespaceURI() + "'");
            }

            node.addAttribute(localName, value, new OMNamespaceImpl(namespaceUri, prefix));
            return;
        }

        // We reach here if the namespace prefix is null/empty, and a namespace uri exists.
        // Find a prefix that has the same namespaceUri, out of the defined namespaces
        Iterator<String> prefixes = node.getNamespaceContext(false).getPrefixes(namespaceUri);
        if (prefixes.hasNext()) {
            prefix = prefixes.next();
            if (prefix.isEmpty()) {
                node.addAttribute(localName, value, null);
                return;
            }
            if (prefix.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
                // If found, and if its the default namespace, add a namespace decl
                node.declareNamespace(value, localName);
                return;
            }
        }

        // Else use the prefix. If the prefix is null, a random prefix will be generated.
        node.addAttribute(localName, value, new OMNamespaceImpl(namespaceUri, prefix));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BMap<?, ?> getAttributesMap() {
        BXmlAttrMap attrMap = new BXmlAttrMap(this);

        if (nodeType != XMLNodeType.ELEMENT) {
            return attrMap;
        }

        OMNamespace defaultNs = ((OMElement) omNode).getDefaultNamespace();
        String namespaceOfPrefix = '{'
                + (defaultNs == null ? XMLConstants.XMLNS_ATTRIBUTE_NS_URI : defaultNs.getNamespaceURI()) + '}';

        Iterator<OMNamespace> namespaceIterator = ((OMElement) omNode).getAllDeclaredNamespaces();
        while (namespaceIterator.hasNext()) {
            OMNamespace namespace = namespaceIterator.next();
            String prefix = namespace.getPrefix();
            if (prefix.isEmpty()) {
                continue;
            }
            attrMap.put(namespaceOfPrefix + prefix, new BString(namespace.getNamespaceURI()));
        }

        Iterator<OMAttribute> attrIterator = ((OMElement) omNode).getAllAttributes();
        while (attrIterator.hasNext()) {
            OMAttribute attr = attrIterator.next();
            attrMap.put(attr.getQName().toString(), new BString(attr.getAttributeValue()));
        }

        attrMap.finishConstruction();
        return attrMap;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setAttributes(BMap<String, ?> attributes) {
        synchronized (this) {
            if (freezeStatus.getState() != BVM.FreezeStatus.State.UNFROZEN) {
                handleInvalidUpdate(freezeStatus.getState());
            }
        }

        if (nodeType != XMLNodeType.ELEMENT || attributes == null) {
            return;
        }

        // Remove existing attributes
        OMElement omElement = ((OMElement) omNode);
        Iterator<OMAttribute> attrIterator = omElement.getAllAttributes();
        while (attrIterator.hasNext()) {
            omElement.removeAttribute(attrIterator.next());
        }

        // Remove existing namespace declarations
        Iterator<OMNamespace> namespaceIterator = omElement.getAllDeclaredNamespaces();
        while (namespaceIterator.hasNext()) {
            namespaceIterator.next();
            namespaceIterator.remove();
        }

        String localName, uri;
        for (String qname : attributes.keys()) {
            if (qname.startsWith("{") && qname.indexOf('}') > 0) {
                localName = qname.substring(qname.indexOf('}') + 1, qname.length());
                uri = qname.substring(1, qname.indexOf('}'));
            } else {
                localName = qname;
                uri = STRING_NULL_VALUE;
            }

            // Validate whether the attribute name is an XML supported qualified name,
            // according to the XML recommendation.
            XMLValidationUtils.validateXMLName(localName);
            setAttribute(localName, uri, STRING_NULL_VALUE, attributes.get(qname).stringValue());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> elements() {
        BValueArray elementsSeq = new BValueArray();
        switch (nodeType) {
        case ELEMENT:
            elementsSeq.add(0, this);
            break;
        default:
            break;
        }
        return new BXMLSequence(elementsSeq);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> elements(String qname) {
        BValueArray elementsSeq = new BValueArray();
        switch (nodeType) {
        case ELEMENT:
            if (getElementName().stringValue().equals(getQname(qname).toString())) {
                elementsSeq.add(0, this);
            }
            break;
        default:
            break;
        }
        return new BXMLSequence(elementsSeq);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> children() {
        BValueArray elementsSeq = new BValueArray();
        switch (nodeType) {
        case ELEMENT:
            Iterator<OMNode> childrenItr = ((OMElement) omNode).getChildren();
            int i = 0;
            while (childrenItr.hasNext()) {
                elementsSeq.add(i++, new BXMLItem(childrenItr.next()));
            }
            break;
        default:
            break;
        }

        return new BXMLSequence(elementsSeq);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> children(String qname) {
        BValueArray elementsSeq = new BValueArray();
        switch (nodeType) {
        case ELEMENT:
            /*
             * Here we are not using "((OMElement) omNode).getChildrenWithName(qname))" method, since as per the
             * documentation of AxiomContainer.getChildrenWithName, if the namespace part of the qname is empty, it
             * will look for the elements which matches only the local part and returns. i.e: It will not match the
             * namespace. This is not the behavior we want. Hence we are explicitly creating an iterator which
             * will return elements that will match both namespace and the localName, regardless whether they are
             * empty or not.
             */
            Iterator<OMNode> childrenItr = new OMChildrenQNameIterator(((OMElement) omNode).getFirstOMChild(),
                    getQname(qname));
            int i = 0;
            while (childrenItr.hasNext()) {
                OMNode node = childrenItr.next();
                elementsSeq.add(i++, new BXMLItem(node));
            }
            break;
        default:
            break;
        }
        return new BXMLSequence(elementsSeq);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setChildren(BXML<?> seq) {
        synchronized (this) {
            if (freezeStatus.getState() != BVM.FreezeStatus.State.UNFROZEN) {
                handleInvalidUpdate(freezeStatus.getState());
            }
        }

        if (seq == null) {
            return;
        }

        OMElement currentNode;
        switch (nodeType) {
        case ELEMENT:
            currentNode = ((OMElement) omNode);
            break;
        default:
            throw new BallerinaException("not an " + XMLNodeType.ELEMENT);
        }

        currentNode.removeChildren();
        addChildren(seq);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addChildren(BXML<?> seq) {
        synchronized (this) {
            if (freezeStatus.getState() != BVM.FreezeStatus.State.UNFROZEN) {
                handleInvalidUpdate(freezeStatus.getState());
            }
        }

        if (seq == null) {
            return;
        }

        OMElement currentNode;
        switch (nodeType) {
        case ELEMENT:
            currentNode = ((OMElement) omNode);
            break;
        default:
            throw new BallerinaException("not an " + XMLNodeType.ELEMENT);
        }

        if (seq.getNodeType() == XMLNodeType.SEQUENCE) {
            BValueArray childSeq = ((BXMLSequence) seq).value();
            for (int i = 0; i < childSeq.size(); i++) {
                currentNode.addChild((OMNode) childSeq.getRefValue(i).value());
            }
        } else {
            currentNode.addChild((OMNode) seq.value());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> strip() {
        if (omNode == null || (nodeType == XMLNodeType.TEXT && ((OMText) omNode).getText().trim().isEmpty())) {
            return new BXMLSequence();
        }

        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> slice(long startIndex, long endIndex) {
        if (startIndex > 1 || endIndex > 1 || startIndex < -1 || endIndex < -1) {
            throw new BallerinaException("index out of range: [" + startIndex + "," + endIndex + "]");
        }

        if (startIndex == -1) {
            startIndex = 0;
        }

        if (endIndex == -1) {
            endIndex = 1;
        }

        if (startIndex == endIndex) {
            return new BXMLSequence();
        }

        if (startIndex > endIndex) {
            throw new BallerinaException("invalid indices: " + startIndex + " < " + endIndex);
        }

        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> descendants(String qname) {
        List<BXML<?>> descendants = new ArrayList<BXML<?>>();
        switch (nodeType) {
        case ELEMENT:
            addDescendants(descendants, (OMElement) omNode, getQname(qname).toString());
            break;
        default:
            break;
        }

        return new BXMLSequence(new BValueArray(descendants.toArray(new BXML[descendants.size()]), BTypes.typeXML));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void serialize(OutputStream outputStream) {
        try {
            this.omNode.serializeAndConsume(outputStream);
        } catch (Throwable t) {
            handleXmlException("error occurred during writing the message to the output stream: ", t);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OMNode value() {
        return this.omNode;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String stringValue() {
        try {
            switch (nodeType) {
            case COMMENT:
                return COMMENT_START + ((OMComment) omNode).getValue() + COMMENT_END;
            case TEXT:
                return getTextValue(omNode);
            case PI:
                return PI_START + ((OMProcessingInstruction) omNode).getTarget() + " "
                        + ((OMProcessingInstruction) omNode).getValue() + PI_END;
            default:
                return this.omNode.toString();
            }
        } catch (Throwable t) {
            handleXmlException("failed to get xml as string: ", t);
        }
        return STRING_NULL_VALUE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXMLItem copy(Map<BValue, BValue> refs) {
        if (isFrozen()) {
            return this;
        }

        OMNode clonedNode;
        switch (nodeType) {
        case ELEMENT:
            clonedNode = ((OMElement) omNode).cloneOMElement();
            break;
        case TEXT:
            TextImpl text = new TextImpl();
            text.setTextContent(((OMText) omNode).getText());
            clonedNode = text;
            break;
        case COMMENT:
            CommentImpl comment = new CommentImpl();
            comment.setTextContent(((OMComment) omNode).getValue());
            clonedNode = comment;
            break;
        case PI:
            OMProcessingInstructionImpl pi = new OMProcessingInstructionImpl();
            pi.setTarget(((OMProcessingInstruction) omNode).getTarget());
            pi.setValue(((OMProcessingInstruction) omNode).getValue());
            clonedNode = pi;
            break;
        default:
            clonedNode = omNode;
            break;
        }

        // adding the document element as parent, to get xpPaths work
        OMDocument doc = new OMDocumentImpl();
        doc.addChild(clonedNode);
        return new BXMLItem(clonedNode);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BXML<?> getItem(long index) {
        if (index != 0) {
            throw new BallerinaException("index out of range: index: " + index + ", size: 1");
        }

        return this;
    }

    /**
     * {@inheritDoc}
     */
    public long size() {
        if (getNodeType() == XMLNodeType.TEXT) {
            String textContent = ((OMText) this.omNode).getText();
            return textContent.codePointCount(0, textContent.length());
        }
        return this.omNode == null ? 0 : 1;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void build() {
        this.omNode.build();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeAttribute(String qname) {
        synchronized (this) {
            if (freezeStatus.getState() != BVM.FreezeStatus.State.UNFROZEN) {
                handleInvalidUpdate(freezeStatus.getState());
            }
        }

        if (nodeType != XMLNodeType.ELEMENT || qname.isEmpty()) {
            return;
        }

        OMElement omElement = (OMElement) omNode;
        OMAttribute attribute = omElement.getAttribute(getQname(qname));

        if (attribute == null) {
            return;
        }

        omElement.removeAttribute(attribute);
    }

    @Override
    public BIterator newIterator() {
        return new BXMLItemIterator(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeChildren(String qname) {
        synchronized (this) {
            if (freezeStatus.getState() != BVM.FreezeStatus.State.UNFROZEN) {
                handleInvalidUpdate(freezeStatus.getState());
            }
        }

        switch (nodeType) {
        case ELEMENT:
            /*
             * Here we are not using "((OMElement) omNode).getChildrenWithName(qname))" method, since as per the
             * documentation of AxiomContainer.getChildrenWithName, if the namespace part of the qname is empty, it
             * will look for the elements which matches only the local part and returns. i.e: It will not match the
             * namespace. This is not the behavior we want. Hence we are explicitly creating an iterator which
             * will return elements that will match both namespace and the localName, regardless whether they are
             * empty or not.
             */
            Iterator<OMNode> childrenItr = new OMChildrenQNameIterator(((OMElement) omNode).getFirstOMChild(),
                    getQname(qname));
            while (childrenItr.hasNext()) {
                childrenItr.next();
                childrenItr.remove();
            }
            break;
        default:
            break;
        }
    }

    // private methods

    private void setXMLNodeType() {
        switch (omNode.getType()) {
        case OMNode.ELEMENT_NODE:
            nodeType = XMLNodeType.ELEMENT;
            break;
        case OMNode.TEXT_NODE:
        case OMNode.SPACE_NODE:
            nodeType = XMLNodeType.TEXT;
            break;
        case OMNode.COMMENT_NODE:
            nodeType = XMLNodeType.COMMENT;
            break;
        case OMNode.PI_NODE:
            nodeType = XMLNodeType.PI;
            break;
        default:
            nodeType = XMLNodeType.SEQUENCE;
            break;
        }
    }

    private String getTextValue(OMNode node) {
        switch (node.getType()) {
        case OMNode.ELEMENT_NODE:
            StringBuilder sb = new StringBuilder();
            Iterator<OMNode> children = ((OMElement) node).getChildren();
            while (children.hasNext()) {
                sb.append(getTextValue(children.next()));
            }
            return sb.toString();
        case OMNode.TEXT_NODE:
            String text = ((OMText) node).getText();
            return StringEscapeUtils.escapeXml11(text);
        case OMNode.COMMENT_NODE:
            return STRING_NULL_VALUE;
        case OMNode.PI_NODE:
            return STRING_NULL_VALUE;
        default:
            return STRING_NULL_VALUE;
        }
    }

    private QName getQName(String localName, String namespaceUri, String prefix) {
        QName qname;
        if (prefix != null) {
            qname = new QName(namespaceUri, localName, prefix);
        } else {
            qname = new QName(namespaceUri, localName);
        }
        return qname;
    }

    /**
     * {@code {@link BXMLItemIterator}} provides iterator for xml items.
     *
     * @since 0.96.0
     */
    static class BXMLItemIterator implements BIterator {

        BXMLItem value;
        int cursor = 0;
        BXMLCodePointIterator codePointIterator;

        BXMLItemIterator(BXMLItem bxmlItem) {
            value = bxmlItem;
        }

        @Override
        public BValue getNext() {
            if (value.getNodeType() == XMLNodeType.TEXT) {
                if (codePointIterator == null) {
                    codePointIterator = createCodePointIterator(value);
                }
                cursor++;
                return codePointIterator.getNext();
            } else if (hasNext()) {
                cursor++;
                return value;
            }
            return null;
        }

        @Override
        public boolean hasNext() {
            if (value.getNodeType() == XMLNodeType.TEXT) {
                if (codePointIterator == null) {
                    codePointIterator = createCodePointIterator(value);
                }
                return codePointIterator.hasNext();
            }
            return cursor == 0;
        }

        private BXMLCodePointIterator createCodePointIterator(BXMLItem value) {
            return new BXMLCodePointIterator(((OMText) value.omNode).getText());
        }

        @Override
        public void stamp(BType type, List<BVM.TypeValuePair> unresolvedValues) {

        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void attemptFreeze(BVM.FreezeStatus freezeStatus) {
        if (isOpenForFreeze(this.freezeStatus, freezeStatus)) {
            this.freezeStatus = freezeStatus;
        }
    }

    private static class BXmlAttrMap extends BMap {
        private final BXMLItem bXmlItem;
        private boolean constructed = false;

        BXmlAttrMap(BXMLItem bXmlItem) {
            super(new BMapType(BTypes.typeString));
            this.bXmlItem = bXmlItem;
        }

        void finishConstruction() {
            constructed = true;
        }

        @Override
        @SuppressWarnings("unchecked")
        public void put(Object key, BValue value) {
            super.put(key, value);
            if (constructed) {
                setAttribute((String) key, value.stringValue());
            }
        }

        private void setAttribute(String key, String value) {
            String url = null;
            String localName = key;

            int endOfUrl = key.lastIndexOf('}');
            if (endOfUrl != -1) {
                int startBrace = key.indexOf('{');
                if (startBrace == 0) {
                    url = key.substring(startBrace + 1, endOfUrl);
                    localName = key.substring(endOfUrl + 1);
                }
            }
            bXmlItem.setAttribute(localName, url, null, value);
        }
    }
}