diff options
Diffstat (limited to 'src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java')
-rw-r--r-- | src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java | 771 |
1 files changed, 771 insertions, 0 deletions
diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java new file mode 100644 index 0000000..b5b3180 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java @@ -0,0 +1,771 @@ +package org.openslx.virtualization.configuration; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.util.Resources; +import org.openslx.util.Util; +import org.openslx.util.XmlHelper; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.configuration.VirtualizationConfiguration.DriveBusType; +import org.openslx.virtualization.configuration.VirtualizationConfiguration.HardDisk; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Class handling the parsing of a .vbox machine description file + */ +public class VirtualizationConfigurationVirtualboxFileFormat +{ + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfigurationVirtualboxFileFormat.class ); + + // key information set during initial parsing of the XML file + private String osName = ""; + private ArrayList<HardDisk> hddsArray = new ArrayList<HardDisk>(); + + // XPath and DOM parsing related members + private Document doc = null; + + /** + * Version of the configuration file format. + */ + private Version version = null; + + /** + * File names of XML schema files for different file format versions. + */ + private final static HashMap<Version, String> FILE_FORMAT_SCHEMA_VERSIONS = new HashMap<Version, String>() { + + private static final long serialVersionUID = -3163681758191475625L; + + { + put( Version.valueOf( "1.15" ), "VirtualBox-settings_v1-15.xsd" ); + put( Version.valueOf( "1.16" ), "VirtualBox-settings_v1-16.xsd" ); + put( Version.valueOf( "1.17" ), "VirtualBox-settings_v1-17.xsd" ); + put( Version.valueOf( "1.18" ), "VirtualBox-settings_v1-18.xsd" ); + } + }; + + /** + * Path to the VirtualBox file format schemas within the *.jar file. + */ + private final static String FILE_FORMAT_SCHEMA_PREFIX_PATH = Resources.PATH_SEPARATOR + "virtualbox" + + Resources.PATH_SEPARATOR + "xsd"; + + public static final String DUMMY_VALUE = "[dummy]"; + + // list of nodes to automatically remove when reading the vbox file + private static final String[] BLACKLIST = { + "/VirtualBox/Machine/Hardware/GuestProperties", + "/VirtualBox/Machine/Hardware/VideoCapture", + "/VirtualBox/Machine/Hardware/HID", + "/VirtualBox/Machine/Hardware/LPT", + "/VirtualBox/Machine/Hardware/SharedFolders", + "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='0']/*", + "/VirtualBox/Machine/ExtraData", + "/VirtualBox/Machine/StorageControllers/StorageController/AttachedDevice[not(@type='HardDisk')]", + "/VirtualBox/Machine/Hardware/StorageControllers/StorageController/AttachedDevice[not(@type='HardDisk')]", + "/VirtualBox/Machine/MediaRegistry/FloppyImages", + "/VirtualBox/Machine/MediaRegistry/DVDImages" }; + + public static enum MatchMode + { + EXACTLY_ONE, + MULTIPLE, + FIRST_ONLY, + }; + + /** + * Creates a vbox configuration by constructing a DOM from the given VirtualBox machine + * configuration file. + * Will validate the given file against the VirtualBox XSD schema and only proceed if it is + * valid. + * + * @param file the VirtualBox machine configuration file + * @throws IOException if an error occurs while reading the file + * @throws VirtualizationConfigurationException if the given file is not a valid VirtualBox + * configuration file. + */ + public VirtualizationConfigurationVirtualboxFileFormat( File file ) throws IOException, VirtualizationConfigurationException + { + doc = XmlHelper.parseDocumentFromStream( new FileInputStream( file ) ); + doc = XmlHelper.removeFormattingNodes( doc ); + if ( doc == null ) + throw new VirtualizationConfigurationException( "Could not parse given VirtualBox machine configuration file!" ); + + this.parseConfigurationVersion(); + this.init(); + } + + /** + * Creates an vbox configuration by constructing a DOM from the XML content given as a byte + * array. + * + * @param machineDescription content of the XML file saved as a byte array. + * @param length of the machine description byte array. + * @throws VirtualizationConfigurationException creation of VirtualBox configuration file representation failed. + */ + public VirtualizationConfigurationVirtualboxFileFormat( byte[] machineDescription, int length ) throws VirtualizationConfigurationException + { + ByteArrayInputStream is = new ByteArrayInputStream( machineDescription ); + + doc = XmlHelper.parseDocumentFromStream( is ); + if ( doc == null ) { + final String errorMsg = "Could not parse given VirtualBox machine description from byte array!"; + LOGGER.debug( errorMsg ); + throw new VirtualizationConfigurationException( errorMsg ); + } + + this.parseConfigurationVersion(); + this.init(); + } + + public void validate() throws VirtualizationConfigurationException + { + this.validateFileFormatVersion( this.getVersion() ); + } + + public void validateFileFormatVersion( Version version ) throws VirtualizationConfigurationException + { + if ( this.getVersion() != null && this.doc != null ) { + // check if specified version is supported + final String fileName = FILE_FORMAT_SCHEMA_VERSIONS.get( version ); + + if ( fileName == null ) { + final String errorMsg = "File format version " + version.toString() + " is not supported!"; + LOGGER.debug( errorMsg ); + throw new VirtualizationConfigurationException( errorMsg ); + } else { + // specified version is supported, so validate document with corresponding schema file + final InputStream schemaResource = VirtualizationConfigurationVirtualboxFileFormat + .getSchemaResource( fileName ); + + if ( schemaResource != null ) { + try { + final SchemaFactory factory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI ); + final Schema schema = factory.newSchema( new StreamSource( schemaResource ) ); + final Validator validator = schema.newValidator(); + validator.validate( new DOMSource( this.doc ) ); + } catch ( SAXException | IOException e ) { + final String errorMsg = "XML configuration is not a valid VirtualBox v" + version.toString() + + " configuration: " + e.getLocalizedMessage(); + LOGGER.debug( errorMsg ); + throw new VirtualizationConfigurationException( errorMsg ); + } + } + } + } + } + + private static InputStream getSchemaResource( String fileName ) + { + final String schemaFilePath = FILE_FORMAT_SCHEMA_PREFIX_PATH + File.separator + fileName; + return VirtualizationConfigurationVirtualboxFileFormat.class.getResourceAsStream( schemaFilePath ); + } + + /** + * Main initialization functions parsing the document created during the constructor. + * @throws VirtualizationConfigurationException + */ + private void init() throws VirtualizationConfigurationException + { + try { + this.validate(); + } catch ( VirtualizationConfigurationException e ) { + // do not print output of failed validation if placeholders are available + // since those placeholder values violate the defined UUID pattern + if ( !this.checkForPlaceholders() ) { + final String errorMsg = "XML configuration is not a valid VirtualBox v" + version.toString() + + " configuration: " + e.getLocalizedMessage(); + LOGGER.debug( errorMsg ); + } + } + + if ( Util.isEmptyString( getDisplayName() ) ) { + throw new VirtualizationConfigurationException( "Machine doesn't have a name" ); + } + try { + ensureHardwareUuid(); + setOsType(); + fixUsb(); // Since we now support selecting specific speed + removeUnusedHdds(); + if ( checkForPlaceholders() ) { + return; + } + setHdds(); + removeBlacklistedElements(); + addPlaceHolders(); + } catch ( XPathExpressionException e ) { + LOGGER.debug( "Could not initialize VBoxConfig", e ); + return; + } + } + + private void parseConfigurationVersion() throws VirtualizationConfigurationException + { + String versionText; + try { + versionText = XmlHelper.compileXPath( "/VirtualBox/@version" ).evaluate( this.doc ); + } catch ( XPathExpressionException e ) { + throw new VirtualizationConfigurationException( + "Failed to parse the version number of the configuration file" ); + } + + if ( versionText == null || versionText.isEmpty() ) { + throw new VirtualizationConfigurationException( "Configuration file does not contain any version number!" ); + } else { + // parse version information from textual description + final Pattern versionPattern = Pattern.compile( "^(\\d+\\.\\d+).*$" ); + final Matcher versionMatcher = versionPattern.matcher( versionText ); + + if ( versionMatcher.find() ) { + this.version = Version.valueOf( versionMatcher.group( 1 ) ); + } + + if ( this.version == null ) { + throw new VirtualizationConfigurationException( "Configuration file version number is not valid!" ); + } + } + } + + private void fixUsb() + { + NodeList list = findNodes( "/VirtualBox/Machine/Hardware/USB/Controllers/Controller" ); + if ( list != null && list.getLength() != 0 ) { + LOGGER.info( "USB present, not fixing anything" ); + return; + } + // If there's no USB section, this can mean two things: + // 1) Old config that would always default to USB 2.0 for "USB enabled" or nothing for disabled + // 2) New config with USB disabled + list = findNodes( "/VirtualBox/OpenSLX/USB[@disabled]" ); + if ( list != null && list.getLength() != 0 ) { + LOGGER.info( "USB explicitly disabled" ); + return; // Explicitly marked as disabled, do nothing + } + // We assume case 1) and add USB 2.0 + LOGGER.info( "Fixing USB: Adding USB 2.0" ); + Element controller; + Element node = createNodeRecursive( "/VirtualBox/Machine/Hardware/USB/Controllers" ); + controller = addNewNode( node, "Controller" ); + controller.setAttribute( "name", "OHCI" ); + controller.setAttribute( "type", "OHCI" ); + controller = addNewNode( node, "Controller" ); + controller.setAttribute( "name", "EHCI" ); + controller.setAttribute( "type", "EHCI" ); + } + + /** + * Remove any HDDs from MediaRegistry that aren't being used + */ + private void removeUnusedHdds() + { + Set<String> existing = new HashSet<>(); + NodeList list = findNodes( storageControllersPath() + "/StorageController/AttachedDevice/Image" ); + if ( list != null && list.getLength() != 0 ) { + for ( int i = 0; i < list.getLength(); ++i ) { + Node item = list.item( i ); + if ( ! ( item instanceof Element ) ) + continue; + Element e = (Element)item; + String uuid = e.getAttribute( "uuid" ); + if ( Util.isEmptyString( uuid ) ) + continue; + existing.add( uuid ); + } + } + // Now check registry + list = findNodes( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk" ); + if ( list != null && list.getLength() != 0 ) { + for ( int i = 0; i < list.getLength(); ++i ) { + Node item = list.item( i ); + if ( ! ( item instanceof Element ) ) + continue; + Element e = (Element)item; + String uuid = e.getAttribute( "uuid" ); + if ( !existing.contains( uuid ) ) { + LOGGER.info( "Removing unused HDD " + uuid + " from MediaRegistry" ); + e.getParentNode().removeChild( e ); + } + } + } + } + + public String storageControllersPath() + { + if ( this.getVersion().isSmallerThan( Version.valueOf( "1.17" ) ) ) + return "/VirtualBox/Machine/StorageControllers"; + return "/VirtualBox/Machine/Hardware/StorageControllers"; + } + + /** + * Saves the machine's uuid as hardware uuid to prevent VMs from + * believing in a hardware change. + * + * @throws XPathExpressionException + * @throws VirtualizationConfigurationException + */ + private void ensureHardwareUuid() throws XPathExpressionException, VirtualizationConfigurationException + { + // we will need the machine uuid, so get it + String machineUuid = XmlHelper.compileXPath( "/VirtualBox/Machine/@uuid" ).evaluate( this.doc ); + if ( machineUuid.isEmpty() ) { + LOGGER.error( "Machine UUID empty, should never happen!" ); + throw new VirtualizationConfigurationException( "XML doesn't contain a machine uuid" ); + } + + NodeList hwNodes = findNodes( "/VirtualBox/Machine/Hardware" ); + int count = hwNodes.getLength(); + if ( count != 1 ) { + throw new VirtualizationConfigurationException( "Zero or > 1 '/VirtualBox/Machine/Hardware' node were found, should never happen!" ); + } + Element hw = (Element)hwNodes.item( 0 ); + String hwUuid = hw.getAttribute( "uuid" ); + if ( !hwUuid.isEmpty() ) { + LOGGER.info( "Found hardware uuid: " + hwUuid ); + return; + } else { + if ( !addAttributeToNode( hw, "uuid", machineUuid ) ) { + LOGGER.error( "Failed to set machine UUID '" + machineUuid + "' as hardware UUID." ); + return; + } + LOGGER.info( "Saved machine UUID as hardware UUID." ); + } + } + + public Version getVersion() + { + return this.version; + } + + /** + * Self-explanatory. + */ + public void addPlaceHolders() + { + // placeholder for the location of the virtual hdd + changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk", "location", DUMMY_VALUE, MatchMode.MULTIPLE ); + // in case it already has a snapshot + changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk/HardDisk", "location", DUMMY_VALUE, MatchMode.MULTIPLE ); + changeAttribute( "/VirtualBox/Machine", "snapshotFolder", DUMMY_VALUE, MatchMode.FIRST_ONLY ); + } + + /** + * Function checks if the placeholders are present + * + * @return true if the placeholders are present, false otherwise + */ + private boolean checkForPlaceholders() + { + // TODO this should be more robust... + NodeList hdds = findNodes( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk" ); + for ( int i = 0; i < hdds.getLength(); i++ ) { + Element hdd = (Element)hdds.item( i ); + if ( hdd == null ) + continue; + if ( hdd.getAttribute( "location" ).equals( DUMMY_VALUE ) ) { + return true; + } + } + return false; + } + + /** + * Called during init(), prunes the DOM from the elements blacklisted defined + * in the member blacklist, a list of XPath expressions as String + * + * @throws XPathExpressionException + */ + private void removeBlacklistedElements() throws XPathExpressionException + { + // iterate over the blackList + for ( String blackedTag : BLACKLIST ) { + XPathExpression blackedExpr = XmlHelper.compileXPath( blackedTag ); + NodeList blackedNodes = (NodeList)blackedExpr.evaluate( this.doc, XPathConstants.NODESET ); + for ( int i = 0; i < blackedNodes.getLength(); i++ ) { + // go through the child nodes of the blacklisted ones -> why? + Element child = (Element)blackedNodes.item( i ); + removeNode( child ); + } + } + } + + /** + * Getter for the display name + * + * @return the display name of this VM + */ + public String getDisplayName() + { + try { + return XmlHelper.compileXPath( "/VirtualBox/Machine/@name" ).evaluate( this.doc ); + } catch ( XPathExpressionException e ) { + return ""; + } + } + + /** + * Function finds and saves the name of the guest OS + * + * @throws XPathExpressionException failed to find and retrieve name of the guest OS. + */ + public void setOsType() throws XPathExpressionException + { + String os = XmlHelper.compileXPath( "/VirtualBox/Machine/@OSType" ).evaluate( this.doc ); + if ( os != null && !os.isEmpty() ) { + osName = os; + } + } + + /** + * Getter for the parsed guest OS name + * + * @return name of the guest OS + */ + public String getOsName() + { + return osName; + } + + /** + * Search for attached hard drives and determine their controller and their path. + * + * @throws XPathExpressionException failed to find attached hard drives and their controllers. + */ + public void setHdds() throws XPathExpressionException + { + final XPathExpression hddsExpr = XmlHelper.compileXPath( storageControllersPath() + + "/StorageController/AttachedDevice[@type='HardDisk']/Image" ); + + NodeList nodes = (NodeList)hddsExpr.evaluate( this.doc, XPathConstants.NODESET ); + if ( nodes == null ) { + LOGGER.error( "Failed to find attached hard drives." ); + return; + } + for ( int i = 0; i < nodes.getLength(); i++ ) { + Element hddElement = (Element)nodes.item( i ); + if ( hddElement == null ) + continue; + String uuid = hddElement.getAttribute( "uuid" ); + if ( uuid.isEmpty() ) + continue; + // got uuid, check if it was registered + XPathExpression hddsRegistered = XmlHelper.compileXPath( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk[@uuid='" + uuid + "']" ); + NodeList hddsRegisteredNodes = (NodeList)hddsRegistered.evaluate( this.doc, XPathConstants.NODESET ); + if ( hddsRegisteredNodes == null || hddsRegisteredNodes.getLength() != 1 ) { + LOGGER.error( "Found hard disk with uuid '" + uuid + "' which does not appear (unique) in the Media Registry. Skipping." ); + continue; + } + Element hddElementReg = (Element)hddsRegisteredNodes.item( 0 ); + if ( hddElementReg == null ) + continue; + String fileName = hddElementReg.getAttribute( "location" ); + String type = hddElementReg.getAttribute( "type" ); + if ( !type.equals( "Normal" ) && !type.equals( "Writethrough" ) ) { + LOGGER.warn( "Type of the disk file is neither 'Normal' nor 'Writethrough' but: " + type ); + LOGGER.warn( "This makes the image not directly modificable, which might lead to problems when editing it locally." ); + } + // search if it is also attached to a controller + Node hddDevice = hddElement.getParentNode(); + if ( hddDevice == null ) { + LOGGER.error( "HDD node had a null parent, shouldn't happen" ); + continue; + } + Element hddController = (Element)hddDevice.getParentNode(); + if ( hddController == null ) { + LOGGER.error( "HDD node had a null parent, shouldn't happen" ); + continue; + } + String controllerMode = hddController.getAttribute( "type" ); + String controllerType = hddController.getAttribute( "name" ); + DriveBusType busType; + if ( controllerType.equals( "NVMe" ) ) { + busType = DriveBusType.NVME; + } else { + try { + // This assumes the type in the xml matches our enum constants. + busType = DriveBusType.valueOf( controllerType ); + } catch (Exception e) { + LOGGER.warn( "Skipping unknown HDD controller type '" + controllerType + "'" ); + continue; + } + } + LOGGER.info( "Adding hard disk with controller: " + busType + " (" + controllerMode + ") from file '" + fileName + "'." ); + hddsArray.add( new HardDisk( controllerMode, busType, fileName ) ); + } + } + + /** + * Getter for the list of detected hard drives. + * + * @return list of disk drives. + */ + public ArrayList<HardDisk> getHdds() + { + return hddsArray; + } + + /** + * Detect if the vbox file has any machine snapshot by looking at + * the existance of '/VirtualBox/Machine/Snapshot' elements. + * + * @return true if a machine snapshot is present, false otherwise. + */ + public boolean isMachineSnapshot() + { + // check if the vbox configuration file contains some machine snapshots. + // by looking at the existance of /VirtualBox/Machine/Snapshot + NodeList machineSnapshots = findNodes( "/VirtualBox/Machine/Snapshot" ); + return machineSnapshots != null && machineSnapshots.getLength() > 0; + } + + /** + * Searches the DOM for the elements matching the given XPath expression. + * + * @param xpath expression to search the DOM with + * @return nodes found by evaluating given XPath expression + */ + public NodeList findNodes( String xpath ) + { + NodeList nodes = null; + try { + XPathExpression expr = XmlHelper.compileXPath( xpath ); + Object nodesObject = expr.evaluate( this.doc, XPathConstants.NODESET ); + nodes = (NodeList)nodesObject; + } catch ( XPathExpressionException e ) { + LOGGER.error( "Could not build path", e ); + } + return nodes; + } + + /** + * Function used to change the value of an attribute of given element(s). + * + * @param elementXPath given as an xpath expression + * @param attribute attribute to change + * @param value to set the attribute to + * @param mode what to do if multiple nodes match XPath + * @return state of the change operation whether the attribute was changed successfully or not. + */ + public boolean changeAttribute( String elementXPath, String attribute, String value, MatchMode mode ) + { + NodeList nodes = findNodes( elementXPath ); + if ( nodes == null || nodes.getLength() == 0 ) { + if ( mode != MatchMode.MULTIPLE ) { + LOGGER.error( "No node could be found for: " + elementXPath ); + } + return false; + } + if ( nodes.getLength() != 1 && ( mode == MatchMode.EXACTLY_ONE ) ) { + LOGGER.error( "Multiple nodes found for: " + elementXPath ); + return false; + } + boolean ret = true; + for ( int i = 0; i < nodes.getLength(); ++i ) { + if ( !addAttributeToNode( nodes.item( i ), attribute, value ) ) { + ret = false; + } + if ( mode == MatchMode.FIRST_ONLY ) + break; + } + return ret; + } + + /** + * Add given attribute with given value to the given node. + * NOTE: this will overwrite the attribute of the node if it already exists. + * + * @param node to add the attribute to + * @param attribute attribute to add to the node + * @param value of the attribute + * @return true if successful, false otherwise + */ + public boolean addAttributeToNode( Node node, String attribute, String value ) + { + if ( node == null || node.getNodeType() != Node.ELEMENT_NODE ) { + LOGGER.error( "Trying to change attribute of a non element node!" ); + return false; + } + try { + ( (Element)node ).setAttribute( attribute, value ); + } catch ( DOMException e ) { + LOGGER.error( "Failed set '" + attribute + "' to '" + value + "' of xml node '" + node.getNodeName() + "': ", e ); + return false; + } + return true; + } + + /** + * Adds a new node named nameOfNewNode to the given parent found by parentXPath. + * + * @param parentXPath XPath expression to the parent + * @param childName name of the node to be added + * @return the newly added Node + */ + public Element addNewNode( String parentXPath, String childName ) + { + NodeList possibleParents = findNodes( parentXPath ); + if ( possibleParents == null || possibleParents.getLength() != 1 ) { + LOGGER.error( "Could not find unique parent node to add new node to: " + parentXPath ); + return null; + } + return addNewNode( possibleParents.item( 0 ), childName ); + } + + public Element createNodeRecursive( String xPath ) + { + String[] nodeNames = xPath.split( "/" ); + Node parent = this.doc; + Element latest = null; + for ( int nodeIndex = 0; nodeIndex < nodeNames.length; ++nodeIndex ) { + if ( nodeNames[nodeIndex].length() == 0 ) + continue; + Node node = skipNonElementNodes( parent.getFirstChild() ); + while ( node != null ) { + if ( node.getNodeType() == Node.ELEMENT_NODE && nodeNames[nodeIndex].equals( node.getNodeName() ) ) + break; // Found existing + // Check next on same level + node = skipNonElementNodes( node.getNextSibling() ); + } + if ( node == null ) { + node = doc.createElement( nodeNames[nodeIndex] ); + parent.appendChild( node ); + } + parent = node; + latest = (Element)node; + } + return latest; + } + + private Element skipNonElementNodes( Node nn ) + { + while ( nn != null && nn.getNodeType() != Node.ELEMENT_NODE ) { + nn = nn.getNextSibling(); + } + return (Element)nn; + } + + public void setExtraData( String key, String value ) + { + NodeList nl = findNodes( "/VirtualBox/Machine/ExtraData/ExtraDataItem" ); + Element e = null; + if ( nl != null ) { + for ( int i = 0; i < nl.getLength(); ++i ) { + Node n = nl.item( i ); + if ( n.getNodeType() == Node.ELEMENT_NODE ) { + final Element ne = (Element)n; + final String keyValue = ne.getAttribute( "name" ); + if ( keyValue != null && keyValue.equals( key ) ) { + e = ne; + break; + } + } + } + } + if ( e == null ) { + Element p = createNodeRecursive( "/VirtualBox/Machine/ExtraData" ); + e = addNewNode( p, "ExtraDataItem" ); + e.setAttribute( "name", key ); + } + e.setAttribute( "value", value ); + } + + /** + * Creates a new element to the given parent node. + * + * @param parent to add the new element to + * @param childName name of the new element to create + * @return the newly created node + */ + public Element addNewNode( Node parent, String childName ) + { + if ( parent == null || parent.getNodeType() != Node.ELEMENT_NODE ) { + return null; + } + Element newNode = null; + try { + newNode = doc.createElement( childName ); + parent.appendChild( newNode ); + } catch ( DOMException e ) { + LOGGER.error( "Failed to add '" + childName + "' to '" + parent.getNodeName() + "'." ); + } + return newNode; + } + + /** + * Helper to remove given node from the DOM. + * + * @param node Node object to remove. + */ + private void removeNode( Node node ) + { + if ( node == null ) + return; + Node parent = node.getParentNode(); + if ( parent != null ) + parent.removeChild( node ); + } + + /** + * Helper to output the DOM as a String. + * + * @param prettyPrint sets whether to indent the output + * @return (un-)formatted XML + */ + public String toString( boolean prettyPrint ) + { + return XmlHelper.getXmlFromDocument( doc, prettyPrint ); + } + + /** + * Remove all nodes with name childName from parentPath + * @param parentPath XPath to parent node of where child nodes are to be deleted + * @param childName Name of nodes to delete + */ + public void removeNodes( String parentPath, String childName ) + { + NodeList parentNodes = findNodes( parentPath ); + // XPath might match multiple nodes + for ( int i = 0; i < parentNodes.getLength(); ++i ) { + Node parent = parentNodes.item( i ); + List<Node> delList = new ArrayList<>( 0 ); + // Iterate over child nodes + for ( Node child = parent.getFirstChild(); child != null; child = child.getNextSibling() ) { + if ( childName.equals( child.getNodeName() ) ) { + // Remember all to be deleted (don't delete while iterating) + delList.add( child ); + } + } + // Now delete them all + for ( Node child : delList ) { + parent.removeChild( child ); + } + } + } +} |