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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnection;
import be.iminds.ilabt.jfed.util.TextUtil;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import java.io.StringReader;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * BasicStringRspec provides some very basic RSpec parsing, without requiring the use of ModelRspec (which is in the "rspec" module).
 * <p/>
 * See the rspec module for advanced RSpec parsing.
 * (the rspec module uses java7 (or later) with javaFX (for Property and other javaFX "model" classes, it does not use any javaFX GUI code).
 * But since it uses javaFX, it does require that any jar that references the rspec module jar, correctly includes the javaFX classpath.
 * This can only be done using a javaFX packager. See http://stackoverflow.com/questions/15577735/manifest-classpath-for-jar-files )
 */
public class BasicStringRspec {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(BasicStringRspec.class);
    protected final String xmlString;
    protected boolean wellFormed;
    protected boolean validRspec;
    protected Boolean isPossiblyStitching = null;
    protected final List<Set<String>> stitchingCms = new ArrayList<>();
            //only links with linktype "lan" are added to this
    protected List<String> allComponentManagers;
    private String rspecType;

    public BasicStringRspec(String xmlString) {
        this.xmlString = xmlString;
        try {
            wellFormed = true;
            validRspec = true;
            parse();
        } catch (Exception e) {
            LOG.warn("XML is not well-formed and cannot be parsed: " + e.getMessage(), e);
            wellFormed = false;
        }
    }

    //TODO: move RspecXmlConstanst to where I can statically import these from it:
    public final static String NAMESPACE_RSPEC = "http://www.geni.net/resources/rspec/3";
    public final static String NAMESPACE_PROXY = "http://jfed.iminds.be/proxy/1.0";
    public final static String NAMESPACE_OPENFLOW = "http://www.geni.net/resources/rspec/ext/openflow/3";
    public final static QName Q_RSPEC = new QName(NAMESPACE_RSPEC, "rspec");
    public final static QName Q_ATT_RSPEC_TYPE = new QName("type");
    public final static QName Q_NODE = new QName(NAMESPACE_RSPEC, "node");
    public final static QName Q_NODE_AVAILABLE = new QName(NAMESPACE_RSPEC, "available");
    public final static QName Q_ATT_NODE_AVAILABLE_NOW = new QName("now");
    public final static QName Q_LINK = new QName(NAMESPACE_RSPEC, "link");
    public final static QName Q_INTERFACE = new QName(NAMESPACE_RSPEC, "interface");
    public final static QName Q_ATT_CLIENT_ID = new QName("client_id");
    public final static QName Q_ATT_NODE_EXCLUSIVE = new QName("exclusive");
    public final static QName Q_NODE_SLIVERTYPE = new QName(NAMESPACE_RSPEC, "sliver_type");
    public final static QName Q_ATT_NODE_SLIVERTYPE_NAME = new QName("name");
    public final static QName Q_ATT_COM_MAN_ID = new QName("component_manager_id");
    public final static QName Q_ATT_COM_ID = new QName("component_id");
    public final static QName Q_LINK_COM_MAN = new QName(NAMESPACE_RSPEC, "component_manager");
    public final static QName Q_ATT_LINK_COM_MAN_NAME = new QName("name");
    public final static QName Q_LINK_TYPE = new QName(NAMESPACE_RSPEC, "link_type");
    public final static QName Q_ATT_LINK_TYPE_NAME = new QName("name");
    public final static QName Q_NODE_SERVICES = new QName(NAMESPACE_RSPEC, "services");
    public final static QName Q_NODE_SERVICES_LOGIN = new QName(NAMESPACE_RSPEC, "login");
    //<ns:proxy xmlns:ns="http://jfed.iminds.be/proxy/1.0" proxy="U0000001@150.254.160.199:22" for="llopat@10.134.0.8:22"/>
    public final static QName Q_NODE_SERVICES_LOGIN_PROXY = new QName(NAMESPACE_PROXY, "proxy");
    public final static QName Q_ATT_NODE_SERVICES_LOGIN_PROXY_PROXY = new QName("proxy");
    public final static QName Q_ATT_NODE_SERVICES_LOGIN_PROXY_FOR = new QName("for");
    public final static QName Q_ATT_NODE_SERVICES_LOGIN_AUTH = new QName("authentication");
    public final static QName Q_ATT_NODE_SERVICES_LOGIN_HOSTNAME = new QName("hostname");
    public final static QName Q_ATT_NODE_SERVICES_LOGIN_PORT = new QName("port");
    public final static QName Q_ATT_NODE_SERVICES_LOGIN_USERNAME = new QName("username");

    /**
     * Very fault tolerant parsing.
     * <p/>
     * Parses ONLY openflow component managers
     */
    public static List<String> parseOpenflowComponentManagers(String xmlString) {
        //LOG.trace("parseOpenflowComponentManagers starts");
        Set<String> allComponentManagersSet = new HashSet<>();

        //namespace: "http://www.geni.net/resources/rspec/ext/openflow/3"

        //found 2 componentManagerUrns:
        // <rspec><openflow:sliver><openflow:group><openflow:datapath component_manager_id="urn:publicid:IDN+openflow:foam:foam.instageni.gpolab.bbn.com+authority+am"...
        // <rspec><openflow:datapath component_manager_id="

        //we could simply do: look for any openflow node, and find component_manager_id in it.
        //                    then look for any datapath node, and find component_manager_id in it.
        //
        //But we do it even moer simple: look for any element in the openflow namespace, and find component_manager_id in it.

        try {
            XMLInputFactory xif = XMLInputFactory.newFactory();
            StreamSource xml = new StreamSource(new StringReader(xmlString));
            XMLStreamReader streamReader = xif.createXMLStreamReader(xml);

            while (streamReader.hasNext()) {
                streamReader.next();
                switch (streamReader.getEventType()) {
                    case XMLStreamReader.START_ELEMENT: {
                        if (Objects.equals(streamReader.getName().getNamespaceURI(), NAMESPACE_OPENFLOW)) {
                            String comManId = streamReader.getAttributeValue(null, Q_ATT_COM_MAN_ID.getLocalPart());
                            if (comManId != null)
                                allComponentManagersSet.add(comManId);
                        }
                        break;
                    }
                    case XMLStreamReader.CHARACTERS: {
                        break;
                    }
                    default:
                        break;
                }
            }
        } catch (XMLStreamException e) {
            //ignore any error parsing RSpec
            LOG.trace("Error parsing openflow RSpec: ignored", e);
        }

        return new ArrayList<>(allComponentManagersSet);
    }

    public String getXmlString() {
        return xmlString;
    }

    public boolean isWellFormed() {
        return wellFormed;
    }

    public boolean isValidRspec() {
        return wellFormed && validRspec;
    }

    /**
     * Very fault tolerant parsing.
     * <p/>
     * This initialises:
     * - wellFormed
     * - isStitching
     * - allComponentManagers
     */
    protected void parse() {
        Set<String> allComponentManagersSet = new HashSet<>();

        wellFormed = true;
        validRspec = true;
        int rspecCount = 0;
        try {
            XMLInputFactory xif = XMLInputFactory.newFactory();
            StreamSource xml = new StreamSource(new StringReader(xmlString));
            XMLStreamReader streamReader = xif.createXMLStreamReader(xml);

            int link = 0;
            Set<String> curLinkComMan = new HashSet<>();
            int rspecLevel = 0;

            boolean currentLinkCannotBeStitching = false;

            while (streamReader.hasNext()) {
                streamReader.next();
                switch (streamReader.getEventType()) {
                    case XMLStreamReader.END_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_RSPEC)) {
                            rspecLevel--;
                        }
                        if (Objects.equals(streamReader.getName(), Q_LINK)) {
                            if (curLinkComMan.size() > 1) {
                                if (!currentLinkCannotBeStitching) {
                                    isPossiblyStitching = true;
                                    stitchingCms.add(curLinkComMan);
                                }
                            }
                            curLinkComMan = new HashSet<>();
                        }
                        break;
                    }
                    case XMLStreamReader.START_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_RSPEC)) {
                            //rspecs without any rspec node are not wel formed.
                            rspecCount++;
                            rspecLevel++;

                            if (rspecLevel == 1) {
                                rspecType =
                                        streamReader.getAttributeValue(Q_ATT_RSPEC_TYPE.getNamespaceURI(), Q_ATT_RSPEC_TYPE.getLocalPart());
                            }
                        }
                        if (rspecLevel > 0 && Objects.equals(streamReader.getName(), Q_NODE)) {
                            String comManId = streamReader.getAttributeValue(null, Q_ATT_COM_MAN_ID.getLocalPart());
                            if (comManId != null)
                                allComponentManagersSet.add(comManId);
                            break;
                        }
                        if (rspecLevel > 0 && Objects.equals(streamReader.getName(), Q_LINK)) {
                            link++;
                            currentLinkCannotBeStitching = false;
                            break;
                        }
                        if (rspecLevel > 0 && Objects.equals(streamReader.getName(), Q_LINK_TYPE)) {
                            String linkTypeName =
                                    streamReader.getAttributeValue(null, Q_ATT_LINK_TYPE_NAME.getLocalPart());
                            if (linkTypeName != null && !Objects.equals(linkTypeName, "lan") && !Objects.equals(linkTypeName, "vlan"))
                                currentLinkCannotBeStitching = true;
                        }
                        if (rspecLevel > 0 && Objects.equals(streamReader.getName(), Q_LINK_COM_MAN)) {
                            String comManId =
                                    streamReader.getAttributeValue(null, Q_ATT_LINK_COM_MAN_NAME.getLocalPart());
                            if (comManId != null) {
                                allComponentManagersSet.add(comManId);
                                curLinkComMan.add(comManId);
                            }
                            break;
                        }
                        break;
                    }
                    case XMLStreamReader.CHARACTERS: {
                        break;
                    }
                    default:
                        break;
                }
            }
        } catch (XMLStreamException e) {
            LOG.error("Error parsing rspec. Will be ignored: " + e.getMessage(), e);
            wellFormed = false;
            return;
        }
        if (rspecCount <= 0)
            validRspec = false;
        if (isPossiblyStitching == null)
            isPossiblyStitching = false;

        allComponentManagersSet.addAll(parseOpenflowComponentManagers(xmlString));

        allComponentManagers = new ArrayList<>(allComponentManagersSet);

        assert stitchingCms.isEmpty() == !isPossiblyStitching;
    }

    public List<String> getAllComponentManagerUrns() {
        if (allComponentManagers != null) return allComponentManagers;
        if (!isWellFormed()) return null;
        parse();
        return allComponentManagers;
    }

    public List<Server> getAllComponentManager(TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder, AuthorityFinder.Purpose purpose) {
        List<String> allComponentManagerUrns = getAllComponentManagerUrns();
        if (allComponentManagerUrns == null) return null;
        List<Server> res = new ArrayList<>();
        for (String cm : allComponentManagerUrns) {
            Server auth = authorityFinder.findByUrn(cm, purpose);
            res.add(auth);
        }
        return res;
    }

    /**
     * does any link have multiple component managers?
     * <p>
     * TestbedInfoSource is used to determine if two different URNs are actually the same authority.
     * This happens for exogeni for example. In that case, a link with multiple component managers can be a non-stitched link!
     * <p>
     * if testbedInfoSource is null, it is assumed that any link with multiple component managers is a stitched link
     */
    public Boolean isStitching(TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder) {
        if (isPossiblyStitching == null) {
            if (!isWellFormed()) return null;
            parse();
        }
        if (!isPossiblyStitching) return false;
        if (testbedInfoSource == null) return isPossiblyStitching;
        //possibly stitching, we need to check

        assert !stitchingCms.isEmpty();
        for (Set<String> urns : stitchingCms) {
            Server firstServer = null;
            for (String urn : urns) {
                //TODO this is actually complexer, because it will depend on exogeni settings...
                Server server =
                        testbedInfoSource.getByUrn(urn, TestbedInfoSource.SubAuthMatchAllowed.ALLOW_TOPLEVEL, TestbedInfoSource.SubAuthMatchPreference.PREFER_TOPLEVEL);
                if (server == null) {
                    LOG.warn("Authority not found for " + urn + " during stitch check -> falling back to assuming link is stitched");
                    return true; //fall back to assuming it is stitched, if any authority is not found
                }
                assert server.getTestbed() != null;
                assert server.getTestbed().getId() != null;
                if (firstServer == null)
                    firstServer = server;
                else {
                    if (!Objects.equals(firstServer.getTestbed().getId(), server.getTestbed().getId()))
                        return true; //authorities from different testbeds, so stitching
                }
            }
        }
        return false;
    }


    public static String makeUniqueNodeId(String clientId, String componentId, String componentManagerId) {
        String compManPart = "@" + (componentManagerId == null ? "unspecified" : componentManagerId);

        if (clientId != null)
            return clientId + compManPart;
        else
            return (componentId == null ? "nodeWithoutAnyId" : componentId) + compManPart;
    }

    public static String makeUniqueLinkId(String clientId, String componentId, Collection<String> componentManagerIds) {
        String compManPart;
        if (componentManagerIds != null && !componentManagerIds.isEmpty()) {
            compManPart = "@";
            List<String> sortedComponentManagerIds = new ArrayList<>(componentManagerIds);
            Collections.sort(sortedComponentManagerIds);
            boolean first = true;
            for (String componentManagerId : sortedComponentManagerIds) {
                if (first)
                    compManPart += "" + componentManagerId;
                else
                    compManPart += "&" + componentManagerId;
                first = false;
            }
        } else
            compManPart = "@unspecified";

        if (clientId != null)
            return clientId + compManPart;
        else
            return (componentId == null ? "linkWithoutAnyId" : componentId) + compManPart;
    }

    public static String makeUniqueInterfaceId(String clientId, String componentId, String nodeUniqueId) {
        String atPart = nodeUniqueId == null ? "@unknownNode" : "@" + nodeUniqueId;

        if (clientId != null)
            return clientId + atPart;
        else
            return (componentId == null ? "ifaceWithoutAnyId" : componentId) + atPart;
    }

    private static final Pattern PROXY_PATTERN = Pattern.compile("([^@]*)@([^:]*):([0-9]*)");
    private static final Pattern PROXY_PATTERN_2 = Pattern.compile("([^@]*)@([^:]*)");

    private static JFedConnection.SshProxyInfo parseProxy(String proxyString) {
        try {
            Matcher mProxy = PROXY_PATTERN.matcher(proxyString);
            if (mProxy.matches()) {
                int proxyPort = Integer.parseInt(mProxy.group(3));
                return new JFedConnection.SshProxyInfo(mProxy.group(2), proxyPort, mProxy.group(1),
                        null/*sshKeyInfo*/, null/*hostkey*/, false/*limitName*/);
            }
        } catch (NumberFormatException e) {
            LOG.error("Failed to parse port in \"" + proxyString + "\"");
            return null;
        }

        Matcher mProxy2 = PROXY_PATTERN_2.matcher(proxyString);
        if (mProxy2.matches()) {
            return new JFedConnection.SshProxyInfo(mProxy2.group(2), 22/*proxyPort*/, mProxy2.group(1),
                    null/*sshKeyInfo*/, null/*hostkey*/, false/*limitName*/);
        }

        return null;
    }

    /**
     * helper to only extract login info for a specific node, without full parsing and thus no need for ModelRspec
     *
     * @param nodeUniqueId the client_id of the node
     */
    public List<LoginService> findNodeLoginInfoByUniqueId(String nodeUniqueId) {
        assert nodeUniqueId != null;
        if (!isWellFormed()) return null;

        List<LoginService> res = new ArrayList<>();

        try {
            XMLInputFactory xif = XMLInputFactory.newFactory();
            StreamSource xml = new StreamSource(new StringReader(xmlString));
            XMLStreamReader streamReader = xif.createXMLStreamReader(xml);

            Map<LoginService, JFedConnection.SshProxyInfo> proxyMapping = new HashMap<>();
            boolean atCorrectNode = false;
            while (streamReader.hasNext()) {
                streamReader.next();
                switch (streamReader.getEventType()) {
                    case XMLStreamReader.START_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_NODE)) {
                            String clientId = streamReader.getAttributeValue(null, Q_ATT_CLIENT_ID.getLocalPart());
                            String compManId = streamReader.getAttributeValue(null, Q_ATT_COM_MAN_ID.getLocalPart());
                            String uniqueId = makeUniqueNodeId(
                                    clientId == null ? null : clientId.trim(),
                                    null,
                                    compManId == null ? null : compManId.trim()
                            );
                            if (Objects.equals(uniqueId, nodeUniqueId.trim())) {
                                atCorrectNode = true;
                            }
                            break;
                        }
                        if (atCorrectNode && Objects.equals(streamReader.getName(), Q_NODE_SERVICES_LOGIN_PROXY)) {
                            String attProxy =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_PROXY_PROXY.getLocalPart());
                            String attFor =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_PROXY_FOR.getLocalPart());

                            LOG.debug("Found proxy in service attProxy=" + attProxy + " attFor=" + attFor);

                            if (attProxy != null && attFor != null && !attProxy.trim().isEmpty() && !attFor.trim().isEmpty()) {
                                JFedConnection.SshProxyInfo proxy = parseProxy(attProxy);
                                JFedConnection.SshProxyInfo proxyFor = parseProxy(attFor);

                                if (proxy != null && proxyFor != null) {
                                    LoginService forLoginService =
                                            new LoginService("ssh-keys", proxyFor.getHostname(), proxyFor.getPort(), proxyFor.getUsername(), proxy);

                                    proxyMapping.put(forLoginService, proxy);
                                    LOG.debug("Found and registered proxy " + attProxy + " (=" + proxy + ") for " + attFor + " (=" + forLoginService + ")");
                                } else {
                                    LOG.warn("Invalid SSH proxy attribute: attProxy=" + attProxy + " attFor=" + attFor);
                                }
                            } else {
                                LOG.warn("Invalid SSH proxy attribute (empty): attProxy=" + attProxy + " attFor=" + attFor);
                            }
                        }
                        if (atCorrectNode && Objects.equals(streamReader.getName(), Q_NODE_SERVICES_LOGIN)) {
                            String attAuth =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_AUTH.getLocalPart());
                            String attHostname =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_HOSTNAME.getLocalPart());
                            String attPort =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_PORT.getLocalPart());
                            String attUsername =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_USERNAME.getLocalPart());

                            try {
                                res.add(new LoginService(attAuth, attHostname,
                                        attPort == null ? 22 : Integer.parseInt(attPort), attUsername, null));
                            } catch (NumberFormatException e) {
                                LOG.error("Found matching login details but with non numeric port (\"" + attPort + "\") -> These login details will be ignored.", e);
                            }
                            break;
                        }
                        break;
                    }
                    case XMLStreamReader.END_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_NODE)) {
                            atCorrectNode = false;
                        }
                    }
                    default:
                        break;
                }
            }

            //check proxy mapping and adjust result LoginServices
            if (!proxyMapping.isEmpty())
                for (int i = 0; i < res.size(); i++) {
                    LoginService ls = res.get(i);

                    LoginService matchLs = null;
                    JFedConnection.SshProxyInfo matchProxy = null;
                    for (Map.Entry<LoginService, JFedConnection.SshProxyInfo> e : proxyMapping.entrySet()) {
                        LoginService curLs = e.getKey();
                        if (Objects.equals(curLs.getHostname(), ls.getHostname()) && Objects.equals(curLs.getUsername(), ls.getUsername()) && curLs.getPort() == ls.getPort()) {
                            matchLs = curLs;
                            matchProxy = e.getValue();
                            break;
                        }
                    }

                    if (matchLs != null && matchProxy != null) {
                        LoginService updatedLs =
                                new LoginService(matchLs.getAuthentication(), matchLs.getHostname(), matchLs.getPort(), matchLs.getUsername(), matchProxy);
                        res.set(i, updatedLs);
                        LOG.debug("Found proxy for " + ls + " : " + matchProxy);
                    } else {
                        LOG.debug("Did not find proxy for " + ls);
                    }
                }
        } catch (XMLStreamException e) {
            LOG.error("Error parsing rspec. Will be ignored: " + e.getMessage(), e);
        }

        if (!res.isEmpty() || nodeUniqueId.indexOf('@') < 0) {
            return res;
        } else {
            return findNodeLoginInfoByClientId(nodeUniqueId.substring(0, nodeUniqueId.indexOf('@')));
        }
    }

    /**
     * helper to only extract login info for a specific node, without full parsing and thus no need for ModelRspec
     *
     * DIRTY COPY from findNodeLoginByUniqueId, this should be improved
     *
     * @param nodeClientId the client_id of the node
     */
    public List<LoginService> findNodeLoginInfoByClientId(String nodeClientId) {
        assert nodeClientId != null;
        if (!isWellFormed()) return null;

        List<LoginService> res = new ArrayList<>();

        try {
            XMLInputFactory xif = XMLInputFactory.newFactory();
            StreamSource xml = new StreamSource(new StringReader(xmlString));
            XMLStreamReader streamReader = xif.createXMLStreamReader(xml);

            Map<LoginService, JFedConnection.SshProxyInfo> proxyMapping = new HashMap<>();
            boolean atCorrectNode = false;
            while (streamReader.hasNext()) {
                streamReader.next();
                switch (streamReader.getEventType()) {
                    case XMLStreamReader.START_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_NODE)) {
                            String clientId = streamReader.getAttributeValue(null, Q_ATT_CLIENT_ID.getLocalPart());
                            if (Objects.equals(clientId, nodeClientId.trim())) {
                                atCorrectNode = true;
                            }
                            break;
                        }
                        if (atCorrectNode && Objects.equals(streamReader.getName(), Q_NODE_SERVICES_LOGIN_PROXY)) {
                            String attProxy =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_PROXY_PROXY.getLocalPart());
                            String attFor =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_PROXY_FOR.getLocalPart());

                            LOG.debug("Found proxy in service attProxy=" + attProxy + " attFor=" + attFor);

                            if (attProxy != null && attFor != null && !attProxy.trim().isEmpty() && !attFor.trim().isEmpty()) {
                                JFedConnection.SshProxyInfo proxy = parseProxy(attProxy);
                                JFedConnection.SshProxyInfo proxyFor = parseProxy(attFor);

                                if (proxy != null && proxyFor != null) {
                                    LoginService forLoginService =
                                            new LoginService("ssh-keys", proxyFor.getHostname(), proxyFor.getPort(), proxyFor.getUsername(), proxy);

                                    proxyMapping.put(forLoginService, proxy);
                                    LOG.debug("Found and registered proxy " + attProxy + " (=" + proxy + ") for " + attFor + " (=" + forLoginService + ")");
                                } else {
                                    LOG.warn("Invalid SSH proxy attribute: attProxy=" + attProxy + " attFor=" + attFor);
                                }
                            } else {
                                LOG.warn("Invalid SSH proxy attribute (empty): attProxy=" + attProxy + " attFor=" + attFor);
                            }
                        }
                        if (atCorrectNode && Objects.equals(streamReader.getName(), Q_NODE_SERVICES_LOGIN)) {
                            String attAuth =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_AUTH.getLocalPart());
                            String attHostname =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_HOSTNAME.getLocalPart());
                            String attPort =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_PORT.getLocalPart());
                            String attUsername =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SERVICES_LOGIN_USERNAME.getLocalPart());

                            try {
                                res.add(new LoginService(attAuth, attHostname,
                                        attPort == null ? 22 : Integer.parseInt(attPort), attUsername, null));
                            } catch (NumberFormatException e) {
                                LOG.error("Found matching login details but with non numeric port (\"" + attPort + "\") -> These login details will be ignored.", e);
                            }
                            break;
                        }
                        break;
                    }
                    case XMLStreamReader.END_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_NODE)) {
                            atCorrectNode = false;
                        }
                    }
                    default:
                        break;
                }
            }

            //check proxy mapping and adjust result LoginServices
            if (!proxyMapping.isEmpty())
                for (int i = 0; i < res.size(); i++) {
                    LoginService ls = res.get(i);

                    LoginService matchLs = null;
                    JFedConnection.SshProxyInfo matchProxy = null;
                    for (Map.Entry<LoginService, JFedConnection.SshProxyInfo> e : proxyMapping.entrySet()) {
                        LoginService curLs = e.getKey();
                        if (Objects.equals(curLs.getHostname(), ls.getHostname()) && Objects.equals(curLs.getUsername(), ls.getUsername()) && curLs.getPort() == ls.getPort()) {
                            matchLs = curLs;
                            matchProxy = e.getValue();
                            break;
                        }
                    }

                    if (matchLs != null && matchProxy != null) {
                        LoginService updatedLs =
                                new LoginService(matchLs.getAuthentication(), matchLs.getHostname(), matchLs.getPort(), matchLs.getUsername(), matchProxy);
                        res.set(i, updatedLs);
                        LOG.debug("Found proxy for " + ls + " : " + matchProxy);
                    } else {
                        LOG.debug("Did not find proxy for " + ls);
                    }
                }
        } catch (XMLStreamException e) {
            LOG.error("Error parsing rspec. Will be ignored: " + e.getMessage(), e);
        }

        return res;
    }


    public List<BasicNodeInfo> getBasicNodeInfo() {
        if (!isWellFormed()) return null;

        List<BasicNodeInfo> res = new ArrayList<>();
        try {
            XMLInputFactory xif = XMLInputFactory.newFactory();
            StreamSource xml = new StreamSource(new StringReader(xmlString));
            XMLStreamReader streamReader = xif.createXMLStreamReader(xml);

            String comManId = null;
            String comId = null;
            String clientId = null;
            Boolean available = null;
            Boolean exclusive = null;
            List<String> sliverTypes = new ArrayList<>();

            while (streamReader.hasNext()) {
                streamReader.next();
                switch (streamReader.getEventType()) {
                    case XMLStreamReader.START_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_NODE)) {
                            comManId = streamReader.getAttributeValue(null, Q_ATT_COM_MAN_ID.getLocalPart());
                            comId = streamReader.getAttributeValue(null, Q_ATT_COM_ID.getLocalPart());
                            clientId = streamReader.getAttributeValue(null, Q_ATT_CLIENT_ID.getLocalPart());
                            exclusive =
                                    TextUtil.objectToBoolean(streamReader.getAttributeValue(null, Q_ATT_NODE_EXCLUSIVE.getLocalPart()));
                        }
                        if (Objects.equals(streamReader.getName(), Q_NODE_AVAILABLE)) {
                            available =
                                    TextUtil.objectToBoolean(streamReader.getAttributeValue(null, Q_ATT_NODE_AVAILABLE_NOW.getLocalPart()));
                        }
                        if (Objects.equals(streamReader.getName(), Q_NODE_SLIVERTYPE)) {
                            String sliverType =
                                    streamReader.getAttributeValue(null, Q_ATT_NODE_SLIVERTYPE_NAME.getLocalPart());
                            if (sliverType != null)
                                sliverTypes.add(sliverType);
                        }
                        break;
                    }
                    case XMLStreamReader.END_ELEMENT: {
                        if (Objects.equals(streamReader.getName(), Q_NODE)) {
                            if (clientId != null || comId != null || comManId != null)
                                res.add(new BasicNodeInfo(clientId, comId, comManId, available, exclusive, sliverTypes));

                            comManId = null;
                            comId = null;
                            clientId = null;
                            available = null;
                            exclusive = null;
                            sliverTypes = new ArrayList<>();
                        }
                    }
                    default:
                        break;
                }
            }
        } catch (XMLStreamException e) {
            LOG.error("Error parsing rspec. Will be ignored: " + e.getMessage(), e);
        }

        return res;
    }

    public String getRspecType() {
        return rspecType;
    }

    public static class LoginService {
        private final String authentication;
        private final String hostname;
        private final int port;
        private final String username;
        private final JFedConnection.SshProxyInfo sshProxy;

        public LoginService(String authentication, String hostname, int port, String username, JFedConnection.SshProxyInfo sshProxy) {
            assert (authentication != null);
            assert (hostname != null);
            assert (username != null);
            this.authentication = authentication;
            this.hostname = hostname;
            this.port = port <= 0 ? 22 : port;
            this.username = username;
            this.sshProxy = sshProxy;
        }

        public String getAuthentication() {
            return authentication;
        }

        public String getHostname() {
            return hostname;
        }

        public int getPort() {
            return port;
        }

        public String getUsername() {
            return username;
        }

        public JFedConnection.SshProxyInfo getSshProxy() {
            return sshProxy;
        }

        @Override
        public String toString() {
            return "LoginService{" +
                    "authentication='" + authentication + '\'' +
                    ", hostname='" + hostname + '\'' +
                    ", port=" + port +
                    ", username='" + username + '\'' +
                    ", sshProxy='" + sshProxy + '\'' +
                    '}';
        }
    }

    public static class BasicNodeInfo {
        private final String clientId;
        private final String componentId;
        private final String componentManagerId;
        private final Collection<String> sliverTypes;
        private final Boolean available;
        private final Boolean exclusive;

        public BasicNodeInfo(@Nullable String clientId, @Nullable String componentId, @Nullable String componentManagerId,
                             @Nullable Boolean available,
                             @Nullable Boolean exclusive,
                             Collection<String> sliverTypes) {
            this.clientId = clientId;
            this.componentId = componentId;
            this.componentManagerId = componentManagerId;
            this.available = available;
            this.exclusive = exclusive;
            this.sliverTypes = sliverTypes == null ? Collections.emptyList() : sliverTypes;
        }

        public String getClientId() {
            return clientId;
        }

        public String getComponentId() {
            return componentId;
        }

        public String getComponentManagerId() {
            return componentManagerId;
        }

        public Boolean isAvailable() {
            return available;
        }

        public Collection<String> getSliverTypes() {
            return sliverTypes;
        }

        public Boolean isExclusive() {
            return exclusive;
        }

        public String getUniqueId() {
            return makeUniqueNodeId(
                    clientId == null ? null : clientId.trim(),
                    componentId == null ? null : componentId.trim(),
                    componentManagerId == null ? null : componentManagerId.trim()
            );
        }
    }
}
