package be.iminds.ilabt.jfed.rspec.parser;

import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.util.*;
import org.slf4j.LoggerFactory;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.stream.StreamSource;
import java.io.StringReader;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static be.iminds.ilabt.jfed.rspec.parser.RspecXmlConstants.*;

public class StaxRspecParser implements Callable<ModelRspec>, ProgressProvider {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(StaxRspecParser.class);
    private final List<ProgressHandler> progressHandlers =
            Collections.synchronizedList(new ArrayList<ProgressHandler>());

    private final RspecFactory rspecFactory;

    private static final long PROGRESS_LOSING_DATA = 20;
    private final String inputRspec;

    private boolean useNitosENodeBHack;

    public StaxRspecParser(String inputRspec, RspecFactory rspecFactory) {
        assert inputRspec != null;
        assert rspecFactory != null;
        this.inputRspec = inputRspec;
        this.rspecFactory = rspecFactory;

        this.useNitosENodeBHack = true;

        if (rspecFactory == null)
            throw new IllegalArgumentException("rspecFactory may not be null");
    }

    public StaxRspecParser setUseNitosENodeBHack(boolean useNitosENodeBHack) {
        this.useNitosENodeBHack = useNitosENodeBHack;
        return this;
    }

    private static boolean isType(ModelRspec modelRspec, String... type) {
        if (modelRspec == null)
            return false;
        if (modelRspec.getType() == null)
            return false;
        for (String t : type)
            if (modelRspec.getType().equalsIgnoreCase(t))
                return true;
        return false;
    }

    @Override
    public ModelRspec call() throws RspecParseException {
        long startTime = System.currentTimeMillis();
        //handle empty String
        if (this.inputRspec == null || this.inputRspec.equals(""))
            throw new RspecParseException(null, "Received an empty inputRspec");

        //FEDIBBTDEV-793 force compatability with RSpec2
        String inputRspec =
                this.inputRspec.replace("http://www.protogeni.net/resources/rspec/2", "http://www.protogeni.net/resources/rspec/3");

        inputRspec = inputRspec.replaceFirst("<\\?xml .*\\?>", "<?xml version=\"1.0\" ?>");

        try {
            XMLInputFactory xif = XMLInputFactory.newFactory();
            StreamSource xml = new StreamSource(new StringReader(inputRspec));
            XMLEventReader eventReader = xif.createXMLEventReader(xml);

            XMLEvent e = eventReader.nextEvent();
            assert e.isStartDocument();

            //it can happen that extra XML is detected before the ModelRspec-object is created. Therefore, we nee this extra variable.
            List<ExtraXml> topLevelExtraXml = new ArrayList<>();
            ModelRspec res = null;

            String rspecType = null;

            //read
            while (eventReader.hasNext()) {
                e = eventReader.nextEvent();
                //dirty hack to make sure that characterOffset never exceeds the length
                updateProgress(Math.min(e.getLocation().getCharacterOffset(), inputRspec.length()), inputRspec.length());

                if (e.isStartElement()) {
                    boolean handled = false;
                    if (Objects.equals(e.asStartElement().getName(), Q_RSPEC)) {
                        assert res == null;

                        Attribute typeAtt = e.asStartElement().getAttributeByName(Q_ATT_RSPEC_TYPE);
                        if (typeAtt != null) {
                            res = rspecFactory.createModelRspec(typeAtt.getValue());
                        } else {
                            LOG.error("No type set in RSpec.");
                            res = rspecFactory.createModelRspec(null);
                        }

                        Attribute expiresAtt = e.asStartElement().getAttributeByName(Q_ATT_RSPEC_EXPIRES);
                        if (expiresAtt != null)
                            res.setExpires(expiresAtt.getValue());

                        String schemaLocation = null;
                        String defaultNamespace = null;
                        Map<String, String> namespaces = new HashMap<>();

                        Attribute schemaLocationAtt = e.asStartElement().getAttributeByName(Q_XSI_SCHEMALOCATION);
                        if (schemaLocationAtt != null)
                            schemaLocation = schemaLocationAtt.getValue();

                        @SuppressWarnings("unchecked")
                        Iterator<Namespace> nsIt = e.asStartElement().getNamespaces();
                        while (nsIt.hasNext()) {
                            Namespace ns = nsIt.next();
                            if (ns.isDefaultNamespaceDeclaration())
                                defaultNamespace = ns.getNamespaceURI();
                            else
                                namespaces.put(ns.getPrefix(), ns.getNamespaceURI());
                        }

                        res.setParseData(schemaLocation, defaultNamespace, namespaces);

                        handled = true;
                    }
                    /*if we are processing something other than the start-element,
                    we assume that we already have the ModelRspec-object available  */

                    assert res != null :
                            "ModelRspec-object should have been created by now! Wanted to process event " + e;
                    if (Objects.equals(e.asStartElement().getName(), Q_NODE)) {
                        RspecNode n = readNode(res, e, eventReader);
                        if (n != null)
                            res.addNode(n);
                        handled = true;
                    }

                    //HACK: FEDIBBTDEV-2591 support for nitos flex:e_node_b
                    //   (convert it to a regular node with a fixed hardware type)
                    if (useNitosENodeBHack && Objects.equals(e.asStartElement().getName(), Q_NITOS_E_NODE_B)) {
                        RspecNode n = readFlexENodeB(res, e, eventReader);
                        LOG.debug("Read flex:e_node_b as normal node null="+(n==null));
                        if (n != null) {
                            LOG.debug("                   name="+n.getComponentName()+" avail"+n.getAvailable());
                            res.addNode(n);
                        }
                        handled = true;
                    }

                    if (Objects.equals(e.asStartElement().getName(), Q_LINK)) {
                        RspecLink l = readLink(res, e, eventReader, rspecType);
                        if (l != null)
                            res.addLink(l);
                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_OPENFLOW_DATAPATH)) {
                        //This stores openflow datapaths. BUT ONLY INFORMATIONAL!  (= they are not recreated in the Writer)
                        OpenflowDataPath dp = readOpenflowDataPath(res, e, eventReader);
                        if (dp != null)
                            res.addOpenflowDataPath(dp);

                        //but still treat it as unknown XML
                        handled = true;
                        topLevelExtraXml.add(dp.getAsExtraXml());
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_LEASE)) {

                        Lease lease = readLease(res, e, eventReader);
                        if (lease != null) {
                            res.addLease(lease);
                        }
                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_CHANNEL)) {
                        Channel channel = readChannel(res, e, eventReader);
                        if (channel != null) {
                            res.addChannel(channel);
                        }
                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_NODE_ROUTABLEADRESSES)) {
                        Attribute availableAtt =
                                e.asStartElement().getAttributeByName(Q_ATT_ROUTABLEADRESSES_AVAILABLE);
                        if (availableAtt != null) {
                            res.setRoutableAddressesAvailable(Integer.parseInt(availableAtt.getValue()));
                        } else {
                            res.setRoutableAddressesAvailable(null);
                        }

                        Attribute configuredAtt =
                                e.asStartElement().getAttributeByName(Q_ATT_ROUTABLEADRESSES_CONFIGURED);
                        if (configuredAtt != null) {
                            res.setRoutableAddressesConfigured(Integer.parseInt(configuredAtt.getValue()));
                        } else {
                            res.setRoutableAddressesConfigured(null);
                        }

                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_COLLOCATE_FACTOR)) {
                        if (res.getMultiplexFactor() != null)
                            LOG.warn("Detected duplicate <emulab:collocate_factor> @ {}", e.getLocation());

                        Attribute countAtt = e.asStartElement().getAttributeByName(Q_ATT_COLLOCATE_FACTOR_COUNT);
                        if (countAtt != null) {
                            try {
                                res.setMultiplexFactor(Integer.parseInt(countAtt.getValue()));
                            } catch (NumberFormatException ex) {
                                LOG.error("Could not parse collocate factor count '{}' @ location {}",
                                        countAtt.getValue(), e.getLocation());
                            }
                        } else {
                            LOG.warn("No attribute 'count' could be found for <emulab:collocate_factor> @ {}",
                                    e.getLocation());
                        }

                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_GEANT_TESTBED_TYPE)) {
                        GeantTestbedType geantTestbedType = readGeantTestbedType(res, e, eventReader);
                        if (geantTestbedType != null) {
                            res.addGeantTestbedType(geantTestbedType);
                        }
                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_ROUTABLE_POOL)) {
                        AddressPool addressPool = readAddressPool(res, e, eventReader);
                        if (addressPool != null)
                            res.addAddressPool(addressPool);
                        handled = true;
                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK)) {
                        ExecuteAnsiblePlaybook executeAnsiblePlaybook = readExecuteAnsibleCookbook(res, e, eventReader);
                        if (executeAnsiblePlaybook != null)
                            res.addExecuteAnsiblePlaybook(executeAnsiblePlaybook);
                        handled = true;
                    }

                    if (Objects.equals(e.asStartElement().getName(), Q_JFED_DISTRIBUTE_SSH_KEYPAIR)) {
                        DistributeSshKeypair distributeSshKeypair = readDistributeSshKeypair(res, e, eventReader);
                        if (distributeSshKeypair != null)
                            res.addDistributeSshKeypair(distributeSshKeypair);
                        handled = true;
                    }

//                    if (e.asStartElement().getName().equals(Q_JFEDSSH_USER_SSH_KEYS_USER_LIST)) {
//                        EventListExtraXml fullXml = new EventListExtraXml(e.asStartElement(), eventReader);
//                        UserSpecList userSpecList = (UserSpecList) parseWithJaxb(e.getLocation(), fullXml.getAsEventReader(), UserSpecList.class);
//                        res.getSshKeys().addAll(userSpecList.getUserSpecList());
//                        handled = true;
//                    }
                    if (Objects.equals(e.asStartElement().getName(), Q_JFEDSSH_USER_SSH_KEYS)) {
                        UserSpec userSpec =
                                be.iminds.ilabt.jfed.rspec.parser.extensions.UserSpecParser.parseJfedSshUserSshKeys(e.asStartElement(), eventReader);

                        res.getSshKeys().add(userSpec);
//
//                        EventListExtraXml fullXml = new EventListExtraXml(e.asStartElement(), eventReader);
//                        UserSpec userSpec =
//                                (UserSpec) parseWithJaxb(e.getLocation(), fullXml.getAsEventReader(), UserSpec.class);
//                        res.getSshKeys().add(userSpec);
                        handled = true;
                    }
                    if (!handled) {
                        EventListExtraXml extraXml = new EventListExtraXml(e.asStartElement(), eventReader);

                        //check if the factory knows how to store this, otherwise, store it in the ExtraXML-fields
                        if (!rspecFactory.processExtraRspecXml(res, extraXml))
                            //res.getExtraXml().add(extraXml);
                            topLevelExtraXml.add(extraXml);
                    }
                }

                if (e.getEventType() == XMLStreamConstants.COMMENT) {
                    topLevelExtraXml.add(new SingleElementExtraXml(e));
                }
            }

            if (res == null)
                throw new RspecParseException(null, "Received an invalid Rspec-input. Did not create an ModelRspec-object!");

            res.getExtraXml().addAll(topLevelExtraXml);

            rspecFactory.finishParsing(res);

            //check if all interfaces are bound.
            //this does not delete interfaces that are not bound (users of the RSpecModel have to take into account that interfaces can be partially unbound).
            for (RspecInterface rspecInterface : interfacesByLinkId.values()) {
                if (rspecInterface.isLinkUnbound()) {
                    if (!isType(res, "advertisement"))
                        ignorableParseError(null, "interface \"" + rspecInterface.getClientId() + "\" is not bound to any link");
//                    rspecInterface.delete();
                } else {
                    if (rspecInterface.isNodeUnbound()) {
                        if (!isType(res, "advertisement"))
                            ignorableParseError(null, "interface_ref \"" + rspecInterface.getClientId() + "\" is not bound to any node");
//                        rspecInterface.delete();
                    }
                }
            }

            //Performance monitoring. Mostly, it is important that there aren't many of these messages in the log.
            // If you see lots of these messages, the parsing is not correctly cached somehow.   (as was the case in FEDIBBTDEV-894)
            //Additionally, this is useful to check if parse time for a certain rspec isn't high for some reason.
            long stopTime = System.currentTimeMillis();
            LOG.info("Performance monitoring: PARSED RSPEC in " + (stopTime - startTime) + " ms");
            return res;
        } catch (XMLStreamException e) {
            throw new RspecParseException(e.getLocation(), "Problem parsing RSpec: " + e.getMessage(), e);
        } catch (AssertionError | Exception e) {
            throw new RspecParseException(null, "Problem parsing RSpec", e);
        }
    }

    private RspecLink readLink(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader, String rspecType) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_LINK);

        Attribute idAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        if (idAtt == null) {
            if (rspecType == null || !rspecType.trim().equalsIgnoreCase("advertisement")) {
                //warn for non advertisement RSpecs without client_id in link
                LOG.warn("link@{} has no client_id. ignoring.", startNodeEvent.getLocation());
            }
            return null;
        }
        RspecLink res = rspecFactory.createLinkWithClientId(rspec, idAtt.getValue(), null);
        Attribute sliverIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);
        if (sliverIdAtt != null)
            res.setSliverId(sliverIdAtt.getValue());
        Attribute vlantagAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LINK_VLANTAG);
        if (vlantagAtt != null) {
            try {
                res.setVlanTag(Integer.parseInt(vlantagAtt.getValue()));
            } catch (NumberFormatException e) {
                LOG.error("Error while parsing vlantag '{}': {}", vlantagAtt.getValue(), e.getMessage(), e);
            }
        }

        List<XMLEventReader> linkPropertyEventsList = new ArrayList<>();

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_TYPE)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_ATT_LINK_TYPE_NAME);
                    if (att.getValue() != null)
                        res.getLinkTypes().add(att.getValue());
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_SHAREDLAN_LINKSHAREDVLAN)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_LINK_SHAREDLAN_ATT_NAME);
                    if (att.getValue() != null)
                        res.setSharedLan(att.getValue());
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_VLAN_TAGGING)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_LINK_VLAN_TAGGING_ENABLED_ATT_NAME);
                    if (att.getValue() != null)
                        res.setVlanTagging(TextUtil.objectToBoolean(att.getValue()));
                    else
                        res.setVlanTagging(null);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_BEST_EFFORT)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_LINK_BEST_EFFORT_ENABLED_ATT_NAME);
                    if (att.getValue() != null)
                        res.setBestEffort(TextUtil.objectToBoolean(att.getValue()));
                    else
                        res.setBestEffort(null);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_JFED_LINK_AUTOSHARELAN)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_ATT_JFED_LINK_AUTOSHARELAN_NAME);
                    if (att.getValue() != null)
                        res.getAutoShareLanNames().add(att.getValue());
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_LINK_MULTIPLEXING)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_LINK_LINK_MULTIPLEXING_ENABLED_ATT_NAME);
                    if (att.getValue() != null)
                        res.setLinkMultiplexing(TextUtil.objectToBoolean(att.getValue()));
                    else
                        res.setLinkMultiplexing(null);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_INTERSWITCH)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_LINK_INTERSWITCH_ALLOW);
                    if (att.getValue() != null)
                        res.setInterswitchDisallow(!TextUtil.objectToBoolean(att.getValue()));
                    else
                        res.setInterswitchDisallow(null);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_LINKATTRIBUTE)) {
                    Attribute attKey = e.asStartElement().getAttributeByName(Q_LINK_LINKATTRIBUTE_KEY);
                    Attribute attVal = e.asStartElement().getAttributeByName(Q_LINK_LINKATTRIBUTE_VALUE);
                    if (attKey.getValue() != null && attVal.getValue() != null && Objects.equals(attKey.getValue(), Q_LINK_LINKATTRIBUTE_KEY_NO_MAC_LEARNING))
                        res.setNoMacLearning(attVal.getValue()); //pass the String, so "yep" can be reconstructed
                    else
                        res.setNoMacLearning((Boolean) null);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_COM_MAN)
                        || Objects.equals(e.asStartElement().getName(), Q_LINK_COM_MAN2) ) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_ATT_LINK_COM_MAN_NAME);
                    if (att.getValue() != null && GeniUrn.parse(att.getValue()) != null)
                        res.getComponentManagerUrns().add(GeniUrn.parse(att.getValue()));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LINK_PROPERTY)) {
                    //store them for processing after interface_ref is handled
                    linkPropertyEventsList.add(StaxHelper.getEventsUntilEndTag(e, eventReader));
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_INTERFACE_REF)) {
                    readInterfaceRef(res, e, eventReader);
                    continue;
                }
                //if we got here, the element was not handled above.
                EventListExtraXml extraXml = new EventListExtraXml(e.asStartElement(), eventReader);

                //check if the factory knows how to store this
                if (rspecFactory.processExtraLinkXml(res, extraXml))
                    continue;

                res.getExtraXml().add(extraXml);
                continue;
            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_LINK)) {
                    //process linkPropertyEvents now that interface_ref is handled
                    for (XMLEventReader linkPropertyEvents : linkPropertyEventsList) {
                        assert linkPropertyEvents.hasNext();
                        XMLEvent startEvent = linkPropertyEvents.nextEvent();
                        readLinkProperty(res, startEvent, linkPropertyEvents);
                    }
                    return res;
                }
            }
            if (e.isAttribute()) {
                //attributes are handled when their start element is handled
                continue;
            }
            if (e.isNamespace()) {
                //namespaces are handled when their start element is handled
                //todo: check if more namespace handling is needed
                continue;
            }
            if (e.isProcessingInstruction()) {
                //todo ?
            }
            if (e.isCharacters()) {
                if (e.asCharacters().isWhiteSpace())
                    continue; //ignore whitespace
            }
            if (e.isEntityReference()) {
                //todo ?
            }
            if (e.getEventType() == XMLStreamConstants.COMMENT) {
                res.getExtraXml().add(new SingleElementExtraXml(e));
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<link> tag was never closed");
    }

    private Lease readLease(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_LEASE);

        String leaseId = null;
        Attribute leaseIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_ID);
        if (leaseIdAtt != null)
            leaseId = leaseIdAtt.getValue();

        String clientId = null;
        Attribute cliendIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        if (cliendIdAtt != null)
            clientId = cliendIdAtt.getValue();

        Date validFrom = null;
        Attribute validFromAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_VALID_FROM);
        if (validFromAtt != null) {

            try {
                validFrom = RFC3339Util.rfc3339StringToDate(validFromAtt.getValue());
            } catch (ParseException ex) {
                throw new RspecParseException(validFromAtt.getLocation(),
                        "Could not properly parse validFrom attribute '" + validFromAtt.getValue() + "'. Is it a valid date?");
            }
        }

        Date validUntil = null;
        Attribute validUntilAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_VALID_UNTIL);
        if (validUntilAtt != null) {
            try {
                validUntil = RFC3339Util.rfc3339StringToDate(validUntilAtt.getValue());
            } catch (ParseException ex) {
                throw new RspecParseException(validUntilAtt.getLocation(),
                        "Could not properly parse validUntil attribute '" + validUntilAtt.getValue() + "'. Is it a valid date?");
            }
        }

        discardRestOfTag(startNodeEvent, eventReader);

        return rspecFactory.createLease(rspec, leaseId, clientId, validFrom, validUntil,
                validFromAtt != null ? validFromAtt.getValue() : null,
                validUntilAtt != null ? validUntilAtt.getValue() : null);

    }

    private Channel readChannel(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_CHANNEL);

        Channel channel = rspecFactory.createChannel(rspec);

        Attribute comIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_ID);
        if (comIdAtt != null) {
            channel.setComponentId(comIdAtt.getValue());
        }

        Attribute comManIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_MAN_ID);
        if (comManIdAtt != null) {
            channel.setComponentManagerId(comManIdAtt.getValue());
        }
        Attribute conNameAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_NAME);
        if (conNameAtt != null) {
            channel.setComponentName(conNameAtt.getValue());
        }
        Attribute freqAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_FREQUENCY);
        if (freqAtt != null) {
            channel.setFrequency(freqAtt.getValue());
        }


        Attribute editorXAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_X);
        if (editorXAtt != null) {
            try {
                channel.setEditorX(Double.parseDouble(editorXAtt.getValue()));
            } catch (NumberFormatException ex) {
                LOG.warn("Could not parse {} into a valid double as editorX-location", editorXAtt.getValue());
            }
        }

        Attribute editorYAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_Y);
        if (editorYAtt != null) {
            try {
                channel.setEditorY(Double.parseDouble(editorYAtt.getValue()));
            } catch (NumberFormatException ex) {
                LOG.warn("Could not parse {} into a valid double as editorY-location", editorYAtt.getValue());
            }
        }

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_LEASE_REF)) {
                    Attribute leaseRefId = e.asStartElement().getAttributeByName(Q_ATT_ID_REF);
                    if (leaseRefId != null) {
                        channel.addLeaseIdRef(leaseRefId.getValue());
                    }
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }

                //if we got here, the element was not handled above.
                EventListExtraXml extraXml = new EventListExtraXml(e.asStartElement(), eventReader);

                channel.getExtraXml().add(extraXml);
                continue;

            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_CHANNEL)) {
                    return channel;
                }
            }
            if (e.isAttribute()) {
                //attributes are handled when their start element is handled
                continue;
            }
            if (e.isNamespace()) {
                //namespaces are handled when their start element is handled
                //todo: check if more namespace handling is needed
                continue;
            }
            if (e.isProcessingInstruction()) {
                //todo ?
            }
            if (e.isCharacters()) {
                if (e.asCharacters().isWhiteSpace())
                    continue; //ignore whitespace
            }
            if (e.isEntityReference()) {
                //todo ?
            }
            if (e.getEventType() == XMLStreamConstants.COMMENT) {
                channel.getExtraXml().add(new SingleElementExtraXml(e));
            }

        }
        throw new RspecParseException(startNodeEvent.getLocation(), "<channel> tag was never closed");
    }

    private GeantTestbedType readGeantTestbedType(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_GEANT_TESTBED_TYPE);


        String name = null;
        Attribute nameAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_GTT_NAME);
        if (nameAtt != null) {
            name = nameAtt.getValue();
        }

        GeantTestbedType gtt = rspecFactory.createGeantTestbedType(name);

        Attribute editorXAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_X);
        if (editorXAtt != null) {
            try {
                gtt.setEditorX(Double.parseDouble(editorXAtt.getValue()));
            } catch (NumberFormatException ex) {
                LOG.warn("Could not parse {} into a valid double as editorX-location", editorXAtt.getValue());
            }
        }

        Attribute editorYAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_Y);
        if (editorYAtt != null) {
            try {
                gtt.setEditorY(Double.parseDouble(editorYAtt.getValue()));
            } catch (NumberFormatException ex) {
                LOG.warn("Could not parse {} into a valid double as editorY-location", editorYAtt.getValue());
            }
        }

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_GTS_DSL)) {

                    e = eventReader.nextEvent();

                    if (e.isCharacters()) {
                        gtt.setDsl(e.asCharacters().getData());
                    } else if (e.isEndElement()) {
                        //there was no data in the dsl-element
                    } else {
                        LOG.warn("Unexpected element while reading GTS DSL: {}", e);
                    }
                    continue;
                }

//                if(e.asStartElement().getName().equals(Q_GTS_PORT_BINDING)){
//                    gtt.getPortBindings().put(e.asStartElement().getAttributeByName(Q_ATT_GTS_PORT).getValue(),
//                            new GeantTestbedType.RspecInterfaceBinding(
//                                    e.asStartElement().getAttributeByName(Q_ATT_RSPEC_NODE).getValue(),
//                                    e.asStartElement().getAttributeByName(Q_ATT_RSPEC_INTERFACE).getValue()
//                            ));
//                }

                //if we got here, the element was not handled above.
                LOG.warn("Unexpected tag '{}' in <geantTestbedType>", e.asStartElement().getName());
            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_GEANT_TESTBED_TYPE)) {
                    return gtt;
                }
            }
            if (e.isAttribute()) {
                //attributes are handled when their start element is handled
                continue;
            }
            if (e.isNamespace()) {
                //namespaces are handled when their start element is handled
                //todo: check if more namespace handling is needed
                continue;
            }
            if (e.isProcessingInstruction()) {
                //todo ?
            }
            if (e.isCharacters()) {
                if (e.asCharacters().isWhiteSpace())
                    continue; //ignore whitespace
            }
            if (e.isEntityReference()) {
                //todo ?
            }
            if (e.getEventType() == XMLStreamConstants.COMMENT) {
                LOG.warn("Cannot save comment in GTT-type");
            }

        }
        throw new RspecParseException(startNodeEvent.getLocation(), "<geantTestbedType> tag was never closed");
    }

    private AddressPool readAddressPool(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_ROUTABLE_POOL);

        AddressPool addressPool = rspecFactory.createAddressPool(rspec);

        Attribute clientIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        if (clientIdAtt != null) {
            addressPool.setClientId(clientIdAtt.getValue());
        }

        Attribute comManIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_MAN_ID);
        if (comManIdAtt != null) {
            try {
                addressPool.setComponentManagerId(new GeniUrn(comManIdAtt.getValue()));
            } catch (GeniUrn.GeniUrnParseException e) {
                LOG.warn("Could not parse component_manager_id from address_pool {}",
                        clientIdAtt != null ? clientIdAtt.getValue() : "<NO_CLIENT_ID>",
                        e);
            }
        }
        Attribute typeAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_ROUTABLE_POOL_TYPE);
        if (typeAtt != null) {
            addressPool.setType(typeAtt.getValue());
        }
        Attribute countAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COUNT);
        if (countAtt != null) {
            try {
                addressPool.setCount(Integer.parseInt(countAtt.getValue()));
            } catch (NumberFormatException e) {
                LOG.warn("Could not parse count from address_pool {}",
                        clientIdAtt != null ? clientIdAtt.getValue() : "<NO_CLIENT_ID>",
                        e);
            }
        }

        Attribute editorXAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_X);
        if (editorXAtt != null) {
            try {
                addressPool.setEditorX(Double.parseDouble(editorXAtt.getValue()));
            } catch (NumberFormatException ex) {
                LOG.warn("Could not parse {} into a valid double as editorX-location for addresspool", editorXAtt.getValue());
            }
        }

        Attribute editorYAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_Y);
        if (editorYAtt != null) {
            try {
                addressPool.setEditorY(Double.parseDouble(editorYAtt.getValue()));
            } catch (NumberFormatException ex) {
                LOG.warn("Could not parse {} into a valid double as editorY-location for addresspool", editorYAtt.getValue());
            }
        }

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_IPV4)) {
                    String address = null, netmask = null;
                    Attribute addressAtt = e.asStartElement().getAttributeByName(Q_ATT_IPV4_ADDRESS);
                    if (addressAtt != null) {
                        address = addressAtt.getValue();
                    }
                    Attribute netmaskAtt = e.asStartElement().getAttributeByName(Q_ATT_IPV4_NETMASK);
                    if (netmaskAtt != null) {
                        netmask = netmaskAtt.getValue();
                    }

                    addressPool.addIPv4(rspecFactory.createIPv4(rspec, address, netmask));

                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                } else {
                    LOG.warn("Unexpected element in <routable_pool>: {}", e.asStartElement().getName());
                }
            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_ROUTABLE_POOL)) {
                    return addressPool;
                }
            }
            if (e.isAttribute()) {
                //attributes are handled when their start element is handled
                continue;
            }
            if (e.isNamespace()) {
                //namespaces are handled when their start element is handled
                //todo: check if more namespace handling is needed
                continue;
            }
            if (e.isProcessingInstruction()) {
                //todo ?
            }
            if (e.isCharacters()) {
                if (e.asCharacters().isWhiteSpace())
                    continue; //ignore whitespace
            }
            if (e.isEntityReference()) {
                //todo ?
            }
        }
        throw new RspecParseException(startNodeEvent.getLocation(), "<routable_pool> tag was never closed");
    }


    private ExecuteAnsiblePlaybook readExecuteAnsibleCookbook(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK);

        Attribute sourceAtt =
                startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE);
        if (sourceAtt == null) {
            LOG.warn(Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK + " has no " + Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE + ". ignoring.");
            discardRestOfTag(startNodeEvent, eventReader);
            return null;
        }
        Attribute outputFileAtt =
                startNodeEvent.asStartElement().getAttributeByName(Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE);

        discardRestOfTag(startNodeEvent, eventReader);

        return rspecFactory.createExecuteAnsibleCookbook(
                rspec,
                sourceAtt == null ? null : sourceAtt.getValue(),
                outputFileAtt == null ? null : outputFileAtt.getValue());
    }

    private DistributeSshKeypair readDistributeSshKeypair(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_JFED_DISTRIBUTE_SSH_KEYPAIR);

        Attribute locationAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LOCATION);
        Attribute userAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_USER);

        String location = locationAtt != null ? locationAtt.getValue() : null;
        GeniUrn userUrn;
        if (userAtt != null) {
            try {
                userUrn = new GeniUrn(userAtt.getValue());
            } catch (GeniUrn.GeniUrnParseException ex) {
                LOG.warn("Could not parse user-urn {} in {}. Ignoring.", userAtt.getValue(), Q_JFED_DISTRIBUTE_SSH_KEYPAIR);
                userUrn = null;
            }
        } else {
            userUrn = null;
        }

        discardRestOfTag(startNodeEvent, eventReader);

        return rspecFactory.createDistributeSshKeypair(
                rspec,
                location,
                userUrn);
    }

    private OpenflowDataPath readOpenflowDataPath(ModelRspec rspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_OPENFLOW_DATAPATH);

        Attribute idAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_OPENFLOW_COM_ID);
//        if (idAtt == null) {
//            LOG.warn("openflow datapath  has no component_id. ignoring.");
//            return null;
//        }
        Attribute compManAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_OPENFLOW_COM_MAN_ID);
//        if (compManAtt == null) {
//            LOG.warn("openflow datapath  has no component_manager_id. ignoring.");
//            return null;
//        }
        Attribute dpidAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_OPENFLOW_DPID);
//        if (dpidAtt == null) {
//            LOG.warn("openflow datapath has no dpid. ignoring.");
//            return null;
//        }

        EventListExtraXml allXml = new EventListExtraXml(startNodeEvent.asStartElement(), eventReader);

        return rspecFactory.createOpenflowDataPath(rspec, allXml, compManAtt.getValue(), idAtt.getValue(), dpidAtt.getValue());
    }

    private static void discardRestOfTag(XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        QName name = startNodeEvent.asStartElement().getName();

        int depth = 1; //= 0 + 1 for startNodeEvent
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();
            if (e.isStartElement() && Objects.equals(e.asStartElement().getName(), name)) {
                //shouldn't happen
                depth++;
            }

            if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), name) && --depth == 0) {
                //done processing property
                return;
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<" + name.getLocalPart() + "> tag was never closed");
    }

    private void readLinkProperty(RspecLink link, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        Attribute attSource = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LINK_PROPERTY_SOURCE);
        Attribute attDest = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LINK_PROPERTY_DEST);
        Attribute attCapacity = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LINK_PROPERTY_CAPACITY);
        Attribute attLatency = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LINK_PROPERTY_LATENCY);
        Attribute attLoss = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_LINK_PROPERTY_LOSS);

        if (attSource == null || attDest == null) {
            ignorableParseError(startNodeEvent.getLocation(), "Error parsing link property: both source and destination are needed");
            //ignore link property
            return;
        }

//        RspecInterface fromIface = link.getInterfaceById(attSource.getValue());
//        RspecInterface toIface = link.getInterfaceById(attDest.getValue());

        LinkSetting linkSetting =
                link.getLinkSettingByClientIds(attSource.getValue(), attDest.getValue());//new RspecLink.LinkSetting(fromIface, toIface);
        if (linkSetting == null) {
            ignorableParseError(startNodeEvent.getLocation(), "Error parsing link property: source and/or destination == unknown link interface");
            //ignore link property
            return;
        }

        if (attCapacity != null) linkSetting.setCapacity_Kbps(Long.parseLong(attCapacity.getValue()));
        if (attLatency != null) linkSetting.setLatency_ms(Integer.parseInt(attLatency.getValue()));
        if (attLoss != null) linkSetting.setPacketLoss(Double.parseDouble(attLoss.getValue()));

        //this helps reconstruct the RSpec in the exact same way, as empty properties will not get skipped due to this.
        if (!linkSetting.isActive())
            linkSetting.setShownIfNotActive(true);

//        link.getLinkSettings().add(linkSetting);

        discardRestOfTag(startNodeEvent, eventReader);
    }

    private static class LinkId {
        private final String clientId;
        private final String compId;
        private final String sliverId;

        public LinkId(String clientId, String compId, String sliverId) {
            this.clientId = clientId;
            this.compId = compId;
            this.sliverId = sliverId;
        }

        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        @Override
        public boolean equals(Object o) {
            return equals(o, false, false);
        }

        public boolean equals(Object o, boolean ignoreCompId, boolean ignoreSliverId) {
            if (this == o) return true;
            if (!(o instanceof LinkId)) return false;

            LinkId linkId = (LinkId) o;

            if (clientId != null ? !Objects.equals(clientId, linkId.clientId) : linkId.clientId != null) return false;
            if (!ignoreCompId && (compId != null ? !Objects.equals(compId, linkId.compId) : linkId.compId != null))
                return false;
            if (!ignoreSliverId && (sliverId != null ? !Objects.equals(sliverId, linkId.sliverId)
                    : linkId.sliverId != null))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = clientId != null ? clientId.hashCode() : 0;
            result = 31 * result + (compId != null ? compId.hashCode() : 0);
            result = 31 * result + (sliverId != null ? sliverId.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            String res = "LinkId{";
            if (clientId != null) res += "\"" + clientId + "\"";
            if (compId != null) res += " @ " + compId + "";
            if (sliverId != null) res += " sliver=" + sliverId + "";
            res += "}";
            return res;
        }
    }

    private final Map<LinkId, RspecInterface> interfacesByLinkId = new HashMap<>();

    private RspecInterface findPartialMatchingInterface(LinkId linkId) {
        //Possibility: Either this interface_red or the interface has partial info (no compId or sliverId), and the other has not
        List<RspecInterface> match1 = new ArrayList<>();
        List<RspecInterface> match2 = new ArrayList<>();
        List<RspecInterface> match3 = new ArrayList<>();
        for (Map.Entry<LinkId, RspecInterface> e : interfacesByLinkId.entrySet()) {
            LinkId li = e.getKey();
            RspecInterface iface = e.getValue();
            boolean thisHasOnlyClientId = linkId.compId == null && linkId.sliverId == null;
            boolean otherHasOnlyClientId = li.compId == null && li.sliverId == null;

            if ((linkId.compId == null || li.compId == null) && li.equals(linkId, true, false)) match1.add(iface);
            if ((linkId.sliverId == null || li.sliverId == null) && li.equals(linkId, false, true)) match2.add(iface);
            if ((thisHasOnlyClientId || otherHasOnlyClientId) && li.equals(linkId, true, true)) match3.add(iface);
        }
        RspecInterface res = null;
        //note: if there are multiple matches, we can't be sure, so we won't use that result.
        if (match1.size() == 1) res = match1.get(0);
        else if (match2.size() == 1) res = match2.get(0);
        else if (match3.size() == 1) res = match3.get(0);

        LOG.debug("    Did not find interface_ref by full link id " + linkId + ". " +
                "Result after partial match (" + match1.size() + " " + match2.size() + " " + match3.size() + "): "
                + (res == null ? "still not found" : "found"));
        if (LOG.isTraceEnabled() && res == null)
            LOG.trace("        Known links (" + interfacesByLinkId.size() + "): " + interfacesByLinkId.keySet());

        return res;
    }

    private RspecInterface readInterfaceRef(RspecLink parentLink, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        Attribute idAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        if (idAtt == null) {
            ignorableParseError(startNodeEvent.getLocation(), "Error parsing interface_ref property: client_id was not specified");
            return null;
        }
        String clientId = idAtt.getValue();

        Attribute comIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_ID);
        Attribute sliverIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);
        Attribute felixVlanAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_FELIX_VLAN);
        String comId = comIdAtt != null ? comIdAtt.getValue() : null;
        String sliverId = sliverIdAtt != null ? sliverIdAtt.getValue() : null;
        Integer felixVlan = null;
        if (felixVlanAtt != null) {
            try {
                felixVlan = Integer.parseInt(felixVlanAtt.getValue());
            } catch (NumberFormatException ex) {
                ignorableParseError(startNodeEvent.getLocation(), "Error parsing interface_ref property 'felix:vlan': " +
                        "the value '" + felixVlanAtt.getValue() + "' is not a valid number", ex);
            }
        }

        LinkId linkId = new LinkId(clientId, comId, sliverId);
        LOG.debug("Parsing interface_ref " + linkId);

        RspecInterface res = interfacesByLinkId.get(linkId);
        if (res == null) {
            res = findPartialMatchingInterface(linkId);
        }
        if (res == null) {
            res = rspecFactory.createInterface(parentLink, clientId);
            interfacesByLinkId.put(linkId, res);
        } else {
            if (!res.isLinkUnbound()) {
                ignorableParseError(startNodeEvent.getLocation(), "Error parsing interface_ref property: " +
                        "interface has already been used in another link " +
                        "(this_link=" + parentLink.getUniqueId() + " other_link=" + res.getLink().getUniqueId() + ")");
                return null;
            }
            res.bindLink(parentLink);
        }

        if (comId != null)
            res.setComponentId(comId);
        if (sliverId != null)
            res.setSliverId(sliverId);
        if (felixVlan != null)
            res.setFelixVlan(felixVlan);

        res.getRefExtraXml().addAll(EventListExtraXml.readExtraElements(startNodeEvent, eventReader));
        return res;
    }

    private RspecInterface readInterface(RspecNode parentNode, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_INTERFACE);

        Attribute clientIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        Attribute comIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_ID);
        Attribute sliverIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);

        String clientId = clientIdAtt == null ? null : clientIdAtt.getValue();
        String comId = comIdAtt != null ? comIdAtt.getValue() : null;
        String sliverId = sliverIdAtt != null ? sliverIdAtt.getValue() : null;

        if (clientId == null && comId == null) {
            ignorableParseError(startNodeEvent.getLocation(), "Error parsing interface property: both client_id and component_id are not specified");
            discardRestOfTag(startNodeEvent, eventReader);
            return null;
        }
        LinkId linkId = new LinkId(clientId, comId, sliverId);
        LOG.debug("Parsing interface " + linkId);

        RspecInterface res = interfacesByLinkId.get(linkId);
        if (res == null) {
            res = findPartialMatchingInterface(linkId);
        }
        if (res == null) {
            res = rspecFactory.createInterface(parentNode, clientId);
            interfacesByLinkId.put(linkId, res);
        } else {
            if (!res.isNodeUnbound()) {
                ignorableParseError(startNodeEvent.getLocation(),
                        "Error parsing interface property: interface has already been used in another node " +
                                "(this_node=" + parentNode.getUniqueId() + " other_node=" + res.getNode().getUniqueId() + ")");
                return null;
            }
            res.bindNode(parentNode);
        }

        if (clientIdAtt != null)
            res.setClientId(clientIdAtt.getValue());
        if (comId != null)
            res.setComponentId(comId);
        if (sliverId != null)
            res.setSliverId(sliverId);

        Attribute macAddressAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_INTERFACE_MAC);
        if (macAddressAtt != null)
            res.setMacAddress(macAddressAtt.getValue());

        Attribute comNameAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_NAME);
        Attribute roleAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_IFACE_ROLE);
        Attribute publicIpv4Att = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_IFACE_PUBIP4);
        if (comNameAtt != null)
            res.setComponentName(comNameAtt.getValue());
        if (roleAtt != null)
            res.setRole(roleAtt.getValue());
        if (publicIpv4Att != null)
            res.setPublicIpv4(publicIpv4Att.getValue());


        int depth = 1;
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_INTERFACE_IP)) {
                    Attribute attAddress = e.asStartElement().getAttributeByName(Q_ATT_INTERFACE_IP_ADDRESS);
                    Attribute attNetmask = e.asStartElement().getAttributeByName(Q_ATT_INTERFACE_IP_NETMASK);
                    Attribute attType = e.asStartElement().getAttributeByName(Q_ATT_INTERFACE_IP_TYPE);
                    assert attAddress != null;
                    RspecInterface.IpAddress ip = new RspecInterface.IpAddress(
                            attAddress.getValue(),
                            attNetmask == null ? null : attNetmask.getValue(),
                            attType == null ? null : attType.getValue()
                    );
                    res.getIpAddresses().add(ip);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_INTERFACE)) {
                    depth++;
                    //should not happen!
                }

                //if we got here, the element was not handled above.
                res.getExtraXml().add(new EventListExtraXml(e.asStartElement(), eventReader));
                continue;
            }
            if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_INTERFACE) && --depth == 0) {
                return res;
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<interface> tag was never closed");
    }

    private void readServices(RspecNode node, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_NODE_SERVICES);
        int depth = 1;

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SERVICES_EXECUTE)) {
                    Attribute attCommand = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_EXECUTE_COMMAND);
                    Attribute attShell = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_EXECUTE_SHELL);
                    final ExecuteService executeService = rspecFactory.createExecuteService(
                            attShell == null ? null : attShell.getValue(),
                            attCommand == null ? null : attCommand.getValue()
                    );

                    Attribute attFinishedFile = e.asStartElement().getAttributeByName(Q_ATT_JFED_EXECUTE_FINISHED_FLAG);
                    if (attFinishedFile != null) {
                        executeService.setFinishedFile(attFinishedFile.getValue());
                    }

                    node.addExecuteService(executeService);
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SERVICES_INSTALL)) {
                    Attribute attPath = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_INSTALL_PATH);
                    Attribute attUrl = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_INSTALL_URL);
                    node.addInstallService(rspecFactory.createInstallService(
                            attPath == null ? null : attPath.getValue(),
                            attUrl == null ? null : attUrl.getValue()
                    ));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SERVICES_LOGIN)) {
                    Attribute attAuth = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_LOGIN_AUTH);
                    Attribute attHostname = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_LOGIN_HOSTNAME);
                    Attribute attPort = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_LOGIN_PORT);
                    Attribute attUsername = e.asStartElement().getAttributeByName(Q_ATT_NODE_SERVICES_LOGIN_USERNAME);

                    node.getLoginServices().add(new LoginService(
                            attAuth == null ? null : attAuth.getValue(),
                            attHostname == null ? null : attHostname.getValue(),
                            attPort == null ? null : attPort.getValue(),
                            attUsername == null ? null : attUsername.getValue()
                    ));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_PROXY_PROXY)) {
                    Attribute attFor = e.asStartElement().getAttributeByName(Q_ATT_FOR);
                    Attribute attProxy = e.asStartElement().getAttributeByName(Q_ATT_PROXY);
                    Attribute attHostKey = e.asStartElement().getAttributeByName(Q_ATT_HOSTKEY);

                    node.getProxyServices().add(new ProxyService(attProxy == null ? null : attProxy.getValue(),
                            attFor == null ? null : attFor.getValue(),
                            attHostKey == null ? null : attHostKey.getValue()
                    ));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_JFED_ANSIBLE_SERVICE)) {
                    AnsibleService as = rspecFactory.createAnsibleService();

                    Attribute attInstallRequirements =
                            e.asStartElement().getAttributeByName(Q_ATT_INSTALL_REQUIREMENTS);
                    if (attInstallRequirements != null)
                        as.setInstallRequirements(attInstallRequirements.getValue());

                    Attribute attExecutePlaybook = e.asStartElement().getAttributeByName(Q_ATT_EXECUTE_PLAYBOOK);
                    if (attExecutePlaybook != null)
                        as.setExecutePlaybook(attExecutePlaybook.getValue());

                    Attribute attPlaybookCommand = e.asStartElement().getAttributeByName(Q_ATT_PLAYBOOK_COMMAND);
                    if (attPlaybookCommand != null)
                        as.setPlaybookCommand(attPlaybookCommand.getValue());

                    Attribute attGalaxyCommand = e.asStartElement().getAttributeByName(Q_ATT_GALAXY_COMMAND);
                    if (attGalaxyCommand != null)
                        as.setGalaxyCommand(attGalaxyCommand.getValue());

                    Attribute attLogfile = e.asStartElement().getAttributeByName(Q_ATT_LOGFILE);
                    if (attLogfile != null)
                        as.setLogfile(attLogfile.getValue());

                    Attribute attDebug = e.asStartElement().getAttributeByName(Q_ATT_DEBUG);
                    if (attDebug != null)
                        as.setDebug(Boolean.parseBoolean(attDebug.getValue()));

                    Attribute attInventory = e.asStartElement().getAttributeByName(Q_ATT_INVENTORY);
                    if (attInventory != null)
                        as.setInventory(attInventory.getValue());

                    node.addAnsibleService(as);

                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }

                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SERVICES)) {
                    depth++;
                    //should not happen!
                }
                //unknown extra XML in <services>
                node.getExtraServicesXml().add(new EventListExtraXml(e.asStartElement(), eventReader));
            }
            if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_NODE_SERVICES) && --depth == 0) {
                if (node.getLoginServices().isEmpty() && node.getInstallServices().isEmpty() && node.getExecuteServices().isEmpty())
                    node.addEmptyTag(RspecNode.EmptyTag.SERVICES);
                return;
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<services> tag was never closed");
    }

    private SliverType readSliverType(RspecNode node, StartElement startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert Objects.equals(startNodeEvent.getName(), Q_NODE_SLIVERTYPE);

        Attribute att = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_NODE_SLIVERTYPE_NAME);
        SliverTypeBuilder sliverTypeBuilder = new SliverTypeBuilder(att == null ? null : att.getValue());

        int depth = 1;
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SLIVERTYPE_DISKIMAGE)) {
                    Attribute attName = e.asStartElement().getAttributeByName(Q_ATT_DISKIMAGE_NAME);
                    Attribute attUrl = e.asStartElement().getAttributeByName(Q_ATT_DISKIMAGE_URL);
                    Attribute attOs = e.asStartElement().getAttributeByName(Q_ATT_DISKIMAGE_OS);
                    Attribute attVer = e.asStartElement().getAttributeByName(Q_ATT_DISKIMAGE_VERSION);
                    Attribute attDesc = e.asStartElement().getAttributeByName(Q_ATT_DISKIMAGE_DESCRIPTION);
                    Attribute attDefault = e.asStartElement().getAttributeByName(Q_ATT_DISKIMAGE_DEFAULT);

                    sliverTypeBuilder.addDiskImage(new DiskImage(
                            attName == null ? null : attName.getValue(),
                            attUrl == null ? null : attUrl.getValue(),
                            attOs == null ? null : attOs.getValue(),
                            attVer == null ? null : attVer.getValue(),
                            attDesc == null ? null : attDesc.getValue(),
                            attDefault == null ? null : TextUtil.objectToBoolean(attDefault.getValue())
                    ));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SLIVERTYPE_XEN)) {
                    Attribute attCores = e.asStartElement().getAttributeByName(Q_ATT_XEN_CORES);
                    if (attCores != null) {
                        try {
                            sliverTypeBuilder.xenCores(Integer.parseInt(attCores.getValue()));
                        } catch (NumberFormatException e1) {
                            LOG.warn("Invalid integer in parsed Rspec xen cores", e1);
                        }
                    } else
                        sliverTypeBuilder.xenCores(null);

                    Attribute attRam = e.asStartElement().getAttributeByName(Q_ATT_XEN_RAM);
                    if (attRam != null) {
                        try {
                            sliverTypeBuilder.xenRam(Integer.parseInt(attRam.getValue()));
                        } catch (NumberFormatException e1) {
                            LOG.warn("Invalid integer in parsed Rspec xen ram", e1);
                        }
                    } else
                        sliverTypeBuilder.xenRam(null);

                    Attribute attDisk = e.asStartElement().getAttributeByName(Q_ATT_XEN_DISK);
                    if (attDisk != null) {
                        try {
                            sliverTypeBuilder.xenDisk(Integer.parseInt(attDisk.getValue()));
                        } catch (NumberFormatException e1) {
                            LOG.warn("Invalid integer in parsed Rspec xen disk", e1);
                        }
                    } else
                        sliverTypeBuilder.xenDisk(null);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SLIVERTYPE)) {
                    LOG.warn("Warning: nested <sliver_type> at " + e.getLocation());
                    depth++;
                    continue;
                    //should not happen!
                }

                //store unknown XML
                sliverTypeBuilder.addExtraXml(new EventListExtraXml(e.asStartElement(), eventReader));
                continue;
            }
            if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_NODE_SLIVERTYPE) && --depth == 0) {
                if (!sliverTypeBuilder.isValid()) {
                    LOG.warn("Invalid sliver_type in Rspec: name is required. Will be set to a default name (\"pc\").");
                    sliverTypeBuilder.name("pc");
                }

                return sliverTypeBuilder.build();
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<sliver_type> tag was never closed");
    }

    private HardwareType readHardwareType(RspecNode node, StartElement startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert Objects.equals(startNodeEvent.getName(), Q_NODE_HARDWARETYPE);
        Attribute attHwTypeNameAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_NODE_HARDWARETYPE_NAME);
        assert attHwTypeNameAtt != null;

        Integer typeSlotsInt = null;
        String typeSlotsString = null;

        int depth = 1;
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_HARDWARETYPE_NODETYPE)) {
                    Attribute attTypeSlots =
                            e.asStartElement().getAttributeByName(Q_ATT_NODE_HARDWARETYPE_NODETYPE_TYPESLOTS);
                    if (attTypeSlots != null) {
                        assert typeSlotsString == null :
                                "multiple " + Q_ATT_NODE_HARDWARETYPE_NODETYPE_TYPESLOTS + " seen in same " + Q_NODE_HARDWARETYPE;
                        typeSlotsString = attTypeSlots.getValue();
                        try {
                            typeSlotsInt = Integer.parseInt(typeSlotsString);
                        } catch (NumberFormatException ex) {
                            //not a problem, because typeSlots may be "unlimited"
//                            ignorableParseError(startNodeEvent.getLocation(),
//                                    "Exception parsing " + Q_ATT_NODE_HARDWARETYPE_NODETYPE_TYPESLOTS + " as int \"" + typeSlotsString + "\"", ex);
                        }
                    }
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }

                //handle unknown XML
                //res.addExtraXml(new EventListExtraXml(e.asStartElement(), eventReader));
                ignorableParseError(startNodeEvent.getLocation(),
                        "Unsupported extra XML in " + Q_NODE_HARDWARETYPE + ": " + startNodeEvent.getName());
                discardRestOfTag(e, eventReader);

                continue;
            }
            if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), Q_NODE_HARDWARETYPE) && --depth == 0) {
                return new HardwareType(
                        attHwTypeNameAtt == null ? null : attHwTypeNameAtt.getValue(), typeSlotsInt, typeSlotsString);
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<" + Q_NODE_HARDWARETYPE + "> tag was never closed");
    }

    private RspecNode readNode(ModelRspec modelRspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_NODE);

        Attribute idAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        RspecNode res = rspecFactory.createNodeWithClientId(modelRspec, idAtt == null ? null : idAtt.getValue());
        Attribute comIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_ID);
        Attribute comManIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_MAN_ID);
        Attribute exclusiveAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_NODE_EXCLUSIVE);
        Attribute sliverIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);
        Attribute conNameAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_NAME);
        if (comManIdAtt != null)
            res.setComponentManagerId(comManIdAtt.getValue());
        if (comIdAtt != null)
            res.setComponentId(comIdAtt.getValue());
        if (exclusiveAtt != null)
            res.setExclusive(TextUtil.stringToBoolean(exclusiveAtt.getValue()));
        if (sliverIdAtt != null)
            res.setSliverId(sliverIdAtt.getValue());
        if (conNameAtt != null)
            res.setComponentName(conNameAtt.getValue());

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SLIVERTYPE)) {
                    res.getSliverTypes().add(readSliverType(res, e.asStartElement(), eventReader));
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_FLACK_NODE_INFO)) {
                    Attribute attX = e.asStartElement().getAttributeByName(Q_ATT_FLACK_NODE_INFO_X);
                    Attribute attY = e.asStartElement().getAttributeByName(Q_ATT_FLACK_NODE_INFO_Y);
                    if (attX != null) res.setFlackEditorX(Double.parseDouble(attX.getValue()));
                    if (attY != null) res.setFlackEditorY(Double.parseDouble(attY.getValue()));
//                    readAllExpectingNothingMore(e, eventReader);
                    //treat as unknown XML  (as this is read only anyway, we don't allow changing this)
                    res.getExtraXml().add(new EventListExtraXml(e.asStartElement(), eventReader));
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_INTERFACE)) {
                    readInterface(res, e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_JFED_LOCATION)) {
                    Attribute attX = e.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_X);
                    Attribute attY = e.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_Y);
                    if (attX != null) res.setEditorX(Double.parseDouble(attX.getValue()));
                    if (attY != null) res.setEditorY(Double.parseDouble(attY.getValue()));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_SERVICES)) {
                    readServices(res, e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_HARDWARETYPE)) {
                    res.getHardwareTypes().add(readHardwareType(res, e.asStartElement(), eventReader));
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_ROUTABLECONTROLIP)) {
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    res.setRoutableControlIp(true);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_AVAILABLE)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_ATT_NODE_AVAILABLE_NOW);
                    if (att != null) res.setAvailable(TextUtil.stringToBoolean(att.getValue()));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_LOCATION)) {
                    Attribute attCountry = e.asStartElement().getAttributeByName(Q_ATT_NODE_LOCATION_COUNTRY);
                    Attribute attLat = e.asStartElement().getAttributeByName(Q_ATT_NODE_LOCATION_LAT);
                    Attribute attLong = e.asStartElement().getAttributeByName(Q_ATT_NODE_LOCATION_LONG);
                    res.setLocation(new NodeLocation(
                            attCountry == null ? null : attCountry.getValue(),
                            attLat == null ? null : attLat.getValue(),
                            attLong == null ? null : attLong.getValue()
                    ));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LEASE_REF)) {
                    Attribute leaseRefId = e.asStartElement().getAttributeByName(Q_ATT_ID_REF);
                    if (leaseRefId != null) {
                        res.addLeaseIdRef(leaseRefId.getValue());
                    }
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_JFED_ANSIBLE_GROUP)) {
                    Attribute attName = e.asStartElement().getAttributeByName(Q_ATT_ANSIBLE_GROUP_NAME);
                    if (attName != null) {
                        res.getAnsibleGroups().add(attName.getValue());
                    } else {
                        LOG.warn("Encountered ansible_group-tag without name-attribute. Ignoring");
                    }
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }

                //if we got here, the element was not handled above.
                EventListExtraXml extraXml = new EventListExtraXml(e.asStartElement(), eventReader);

                //check if the factory knows how to store this
                if (rspecFactory.processExtraNodeXml(res, extraXml))
                    continue;

                res.getExtraXml().add(extraXml);
                continue;

            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_NODE)) {
                    return rspecFactory.convertToSpecialNode(res);
                }
            }
            if (e.isAttribute()) {
                //attributes are handled when their start element is handled
                continue;
            }
            if (e.isNamespace()) {
                //namespaces are handled when their start element is handled
                //todo: check if more namespace handling is needed
                continue;
            }
            if (e.isProcessingInstruction()) {
                //todo ?
            }
            if (e.isCharacters()) {
                if (e.asCharacters().isWhiteSpace())
                    continue; //ignore whitespace
            }
            if (e.isEntityReference()) {
                //todo ?
            }

            if (e.getEventType() == XMLStreamConstants.COMMENT) {
                res.getExtraXml().add(new SingleElementExtraXml(e));
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<node> tag was never closed");
    }

    private static final String E_NODE_B_URN_REGEX =
            "urn:publicid:IDN\\+([0-9a-zA-Z._:-]*)\\+(e_node_b)\\+([+;%/0-9a-zA-Z._:-]*)";
    private static final Pattern E_NODE_B_URN_PATTERN = Pattern.compile(E_NODE_B_URN_REGEX);

    private RspecNode readFlexENodeB(ModelRspec modelRspec, XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        LOG.debug("NITOS flex:e_node_b HACK: readFlexENodeB");

        assert startNodeEvent.isStartElement();
        assert Objects.equals(startNodeEvent.asStartElement().getName(), Q_NITOS_E_NODE_B);

        Attribute idAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
        RspecNode res = rspecFactory.createNodeWithClientId(modelRspec, idAtt == null ? null : idAtt.getValue());
        Attribute comIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_ID);
        Attribute comManIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_MAN_ID);
        Attribute exclusiveAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_NODE_EXCLUSIVE);
        Attribute sliverIdAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);
        Attribute conNameAtt = startNodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_NAME);
        if (comManIdAtt != null)
            res.setComponentManagerId(comManIdAtt.getValue());
        if (comIdAtt != null) {
            if (comIdAtt.getValue() != null) {
                Matcher urnMatcher = E_NODE_B_URN_PATTERN.matcher(comIdAtt.getValue().trim());
                if (urnMatcher.matches()) {
                    String topLevelAuthority_enc = urnMatcher.group(1);
                    String resourceType_enc = urnMatcher.group(2);
                    String resourceName_enc = urnMatcher.group(3);

                    assert resourceType_enc.equals("e_node_b");

                    res.setComponentId(
                            GeniUrn.createGeniUrnFromEncodedParts(
                                    topLevelAuthority_enc,
                                    "node",
                                    resourceName_enc
                            )
                    );

                    LOG.debug("NITOS flex:e_node_b HACK: Changed URN from \""+comIdAtt.getValue()+"\" to \""+res.getComponentId()+"\"");
                } else {
                    LOG.debug("NITOS flex:e_node_b HACK: non e_node_b urn: \""+comIdAtt.getValue()+"\"");
                    res.setComponentId(comIdAtt.getValue());
                }
            } else {
                LOG.debug("NITOS flex:e_node_b HACK: no urn: (\""+comIdAtt.getValue()+"\")");
                res.setComponentId(comIdAtt.getValue());
            }
        } else {
            LOG.debug("NITOS flex:e_node_b HACK: comIdAtt == null");
        }
        if (exclusiveAtt != null)
            res.setExclusive(TextUtil.stringToBoolean(exclusiveAtt.getValue()));
        if (sliverIdAtt != null)
            res.setSliverId(sliverIdAtt.getValue());
        if (conNameAtt != null)
            res.setComponentName(conNameAtt.getValue());


        //HACK: FEDIBBTDEV-2591 fixed hardware type
        res.getHardwareTypes().add(new HardwareType(HACK_FIXED_HARDWARETYPE_E_NODE_B));

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();

            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName(), Q_JFED_LOCATION)) {
                    Attribute attX = e.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_X);
                    Attribute attY = e.asStartElement().getAttributeByName(Q_ATT_JFED_LOCATION_Y);
                    if (attX != null) res.setEditorX(Double.parseDouble(attX.getValue()));
                    if (attY != null) res.setEditorY(Double.parseDouble(attY.getValue()));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_NODE_AVAILABLE)) {
                    Attribute att = e.asStartElement().getAttributeByName(Q_ATT_NODE_AVAILABLE_NOW);
                    if (att != null) res.setAvailable(TextUtil.stringToBoolean(att.getValue()));
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }
                if (Objects.equals(e.asStartElement().getName(), Q_LEASE_REF)) {
                    Attribute leaseRefId = e.asStartElement().getAttributeByName(Q_ATT_ID_REF);
                    if (leaseRefId != null) {
                        res.addLeaseIdRef(leaseRefId.getValue());
                    }
                    StaxHelper.readAllExpectingNothingMore(e, eventReader);
                    continue;
                }

                //if we got here, the element was not handled above.
                EventListExtraXml extraXml = new EventListExtraXml(e.asStartElement(), eventReader);

                //check if the factory knows how to store this
                if (rspecFactory.processExtraNodeXml(res, extraXml))
                    continue;

                res.getExtraXml().add(extraXml);
                continue;
            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName(), Q_NITOS_E_NODE_B)) {
                    return rspecFactory.convertToSpecialNode(res);
                }
            }
            if (e.isAttribute()) {
                //attributes are handled when their start element is handled
                continue;
            }
            if (e.isNamespace()) {
                //namespaces are handled when their start element is handled
                continue;
            }
            if (e.isProcessingInstruction()) {
                //?
            }
            if (e.isCharacters()) {
                if (e.asCharacters().isWhiteSpace())
                    continue; //ignore whitespace
            }
            if (e.isEntityReference()) {
                //?
            }

            if (e.getEventType() == XMLStreamConstants.COMMENT) {
                res.getExtraXml().add(new SingleElementExtraXml(e));
            }
        }

        throw new RspecParseException(startNodeEvent.getLocation(), "<flex:e_node_b> tag was never closed");
    }


    public static Object parseWithJaxb(Location location, XMLEventReader eventReader, Class<?> clazz) throws RspecParseException {
        try {
            JAXBContext jc = JAXBContext.newInstance(clazz);
            Unmarshaller unmarshaller = jc.createUnmarshaller();
            JAXBElement jb = unmarshaller.unmarshal(eventReader, clazz);
            eventReader.close();

            return jb.getValue();
        } catch (XMLStreamException | JAXBException e) {
            throw new RspecParseException(location, "Error parsing " + clazz.getName() + ": " + e.getMessage(), e);
        }
    }

    private void ignorableParseError(Location location, String message) throws RspecParseException {
        ignorableParseError(location, message, null);
    }

    private void ignorableParseError(Location location, String message, Exception e) throws RspecParseException {
        LOG.error("Ignorable RspecParsingException: " + message + " @ " + RspecParseException.locationToString(location), e);
//        throw new RspecParseException(location, message, e);
    }

    private volatile double totalWork;
    private volatile double progress;


    public double getTotalWork() {
        return totalWork;
    }

    public double getProgress() {
        return progress;
    }

    @Override
    public void addProgressHandler(ProgressHandler progressHandler) {
        progressHandlers.add(progressHandler);
    }

    @Override
    public void removeProgressHandler(ProgressHandler progressHandler) {
        progressHandlers.remove(progressHandler);
    }


    private void updateProgress(long currentProgress, long totalProgress) {
        this.progress = currentProgress;
        this.totalWork = totalProgress;

        for (ProgressHandler progressHandler : progressHandlers)
            progressHandler.onProgressUpdate(currentProgress, totalProgress);
    }
}
