From aac457735f24129b9113c53ae93a1a11f524b6c0 Mon Sep 17 00:00:00 2001 From: Manuel Bentele Date: Fri, 8 Jan 2021 13:44:15 +0100 Subject: Add support for QCOW2 images for QEMU --- src/main/java/org/openslx/util/vm/DiskImage.java | 156 +++++++++++++++++++++-- 1 file changed, 146 insertions(+), 10 deletions(-) (limited to 'src/main/java/org/openslx') diff --git a/src/main/java/org/openslx/util/vm/DiskImage.java b/src/main/java/org/openslx/util/vm/DiskImage.java index 15b3800..1602926 100644 --- a/src/main/java/org/openslx/util/vm/DiskImage.java +++ b/src/main/java/org/openslx/util/vm/DiskImage.java @@ -26,8 +26,7 @@ public class DiskImage public enum ImageFormat { - VMDK( "vmdk" ), QCOW2( "qcow2" ), VDI( "vdi" ), - DOCKER("dockerfile"); + VMDK( "vmdk" ), QCOW2( "qcow2" ), VDI( "vdi" ), DOCKER( "dockerfile" ); public final String extension; @@ -53,7 +52,7 @@ public class DiskImage return VDI; if ( virtId.equals( TConst.VIRT_QEMU ) ) return QCOW2; - if ( virtId.equals( TConst.VIRT_DOCKER) ) + if ( virtId.equals( TConst.VIRT_DOCKER ) ) return DOCKER; return null; } @@ -166,17 +165,154 @@ public class DiskImage return; } - // TODO: qcow + // qcow2 disk image file.seek( 0 ); if ( file.readInt() == QEMU_MAGIC ) { - // dummy values - this.isStandalone = true; - this.isCompressed = false; - this.isSnapshot = false; + // + // qcow2 (version 2 and 3) header format: + // + // magic (4 byte) + // version (4 byte) + // backing_file_offset (8 byte) + // backing_file_size (4 byte) + // cluster_bits (4 byte) + // size (8 byte) + // crypt_method (4 byte) + // l1_size (4 byte) + // l1_table_offset (8 byte) + // refcount_table_offset (8 byte) + // refcount_table_clusters (4 byte) + // nb_snapshots (4 byte) + // snapshots_offset (8 byte) + // incompatible_features (8 byte) [*] + // compatible_features (8 byte) [*] + // autoclear_features (8 byte) [*] + // refcount_order (8 byte) [*] + // header_length (4 byte) [*] + // + // [*] these fields are only available in the qcow2 version 3 header format + // + + // + // check qcow2 file format version + // + file.seek( 4 ); + final int qcowVersion = file.readInt(); + if ( qcowVersion < 2 || qcowVersion > 3 ) { + // disk image format is not a qcow2 disk format + throw new UnknownImageFormatException(); + } else { + // disk image format is a valid qcow2 disk format + this.hwVersion = qcowVersion; + this.subFormat = null; + } + + // + // check if qcow2 image does not refer to any backing file + // + file.seek( 8 ); + this.isStandalone = ( file.readLong() == 0 ) ? true : false; + + // + // check if qcow2 image does not contain any snapshot + // + file.seek( 56 ); + this.isSnapshot = ( file.readInt() == 0 ) ? true : false; + + // + // check if qcow2 image uses extended L2 tables + // + boolean qcowUseExtendedL2 = false; + + // extended L2 tables are only possible in qcow2 version 3 header format + if ( qcowVersion == 3 ) { + // read incompatible feature bits + file.seek( 72 ); + final long qcowIncompatibleFeatures = file.readLong(); + + // support for extended L2 tables is enabled if bit 4 is set + qcowUseExtendedL2 = ( ( ( qcowIncompatibleFeatures & 0x000000000010 ) >>> 4 ) == 1 ); + } + + // + // check if qcow2 image contains compressed clusters + // + boolean qcowCompressed = false; + + // get number of entries in L1 table + file.seek( 36 ); + final int qcowL1TableSize = file.readInt(); + + // check if a valid L1 table is present + if ( qcowL1TableSize > 0 ) { + // qcow2 image contains active L1 table with more than 0 entries: l1_size > 0 + // search for first L2 table and its first entry to get compression bit + + // get cluster bits to calculate the cluster size + file.seek( 20 ); + final int qcowClusterBits = file.readInt(); + final int qcowClusterSize = ( 1 << qcowClusterBits ); + + // entries of a L1 table have always the size of 8 byte (64 bit) + final int qcowL1TableEntrySize = 8; + + // entries of a L2 table have either the size of 8 or 16 byte (64 or 128 bit) + final int qcowL2TableEntrySize = ( qcowUseExtendedL2 ) ? 16 : 8; + + // calculate number of L2 table entries + final int qcowL2TableSize = qcowClusterSize / qcowL2TableEntrySize; + + // get offset of L1 table + file.seek( 40 ); + long qcowL1TableOffset = file.readLong(); + + // check for each L2 table referenced from an L1 table its entries + // until a compressed cluster descriptor is found + for ( long i = 0; i < qcowL1TableSize; i++ ) { + // get offset of current L2 table from the current L1 table entry + long qcowL1TableEntryOffset = qcowL1TableOffset + ( i * qcowL1TableEntrySize ); + file.seek( qcowL1TableEntryOffset ); + long qcowL1TableEntry = file.readLong(); + + // extract offset (bits 9 - 55) from L1 table entry + long qcowL2TableOffset = ( qcowL1TableEntry & 0x00fffffffffffe00L ); + + if ( qcowL2TableOffset == 0 ) { + // L2 table and all clusters described by this L2 table are unallocated + continue; + } + + // get each L2 table entry and check if it is a compressed cluster descriptor + for ( long j = 0; j < qcowL2TableSize; j++ ) { + // get current L2 table entry + long qcowL2TableEntryOffset = qcowL2TableOffset + ( j * qcowL2TableEntrySize ); + file.seek( qcowL2TableEntryOffset ); + long qcowL2TableEntry = file.readLong(); + + // extract cluster type (standard or compressed) (bit 62) + boolean qcowClusterCompressed = ( ( ( qcowL2TableEntry & 0x4000000000000000L ) >>> 62 ) == 1 ); + + // check if qcow2 disk image contains at least one compressed cluster descriptor + if ( qcowClusterCompressed ) { + qcowCompressed = true; + break; + } + } + + // terminate if one compressed cluster descriptor is already found + if ( qcowCompressed ) { + break; + } + } + } else { + // qcow2 image does not contain an active L1 table with any entry: l1_size = 0 + qcowCompressed = false; + } + + this.isCompressed = qcowCompressed; this.format = ImageFormat.QCOW2; - this.subFormat = null; this.diskDescription = null; - this.hwVersion = 0; + return; } } -- cgit v1.2.3-55-g7522 From 10567e0145ce651c327267d8f4ea31d82bc7e239 Mon Sep 17 00:00:00 2001 From: Manuel Bentele Date: Fri, 29 Jan 2021 12:22:05 +0100 Subject: Add base classes and utilites to represent Libvirt XML documents --- .../openslx/libvirt/xml/LibvirtXmlCreatable.java | 26 ++ .../openslx/libvirt/xml/LibvirtXmlDocument.java | 374 +++++++++++++++++++++ .../libvirt/xml/LibvirtXmlDocumentException.java | 25 ++ .../openslx/libvirt/xml/LibvirtXmlEditable.java | 241 +++++++++++++ .../org/openslx/libvirt/xml/LibvirtXmlNode.java | 356 ++++++++++++++++++++ .../openslx/libvirt/xml/LibvirtXmlResources.java | 52 +++ .../libvirt/xml/LibvirtXmlSchemaValidator.java | 283 ++++++++++++++++ .../libvirt/xml/LibvirtXmlSerializable.java | 57 ++++ .../xml/LibvirtXmlSerializationException.java | 25 ++ .../openslx/libvirt/xml/LibvirtXmlValidatable.java | 18 + .../libvirt/xml/LibvirtXmlValidationException.java | 25 ++ .../libvirt/xml/LibvirtXmlDocumentTest.java | 262 +++++++++++++++ .../libvirt/xml/LibvirtXmlTestResources.java | 29 ++ .../xml/qemu-kvm_default-archlinux-vm-cdrom.xml | 144 ++++++++ .../xml/qemu-kvm_default-archlinux-vm-floppy.xml | 145 ++++++++ .../xml/qemu-kvm_default-archlinux-vm-no-hdd.xml | 134 ++++++++ .../xml/qemu-kvm_default-archlinux-vm-no-nic.xml | 134 ++++++++ .../xml/qemu-kvm_default-archlinux-vm-no-sound.xml | 136 ++++++++ .../xml/qemu-kvm_default-archlinux-vm-no-usb.xml | 127 +++++++ .../libvirt/xml/qemu-kvm_default-archlinux-vm.xml | 140 ++++++++ .../qemu-kvm_default-ubuntu-20-04-vm-invalid.xml | 164 +++++++++ .../xml/qemu-kvm_default-ubuntu-20-04-vm.xml | 164 +++++++++ 22 files changed, 3061 insertions(+) create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlCreatable.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocument.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocumentException.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlEditable.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlNode.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlResources.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlSchemaValidator.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializable.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializationException.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidatable.java create mode 100644 src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidationException.java create mode 100644 src/test/java/org/openslx/libvirt/xml/LibvirtXmlDocumentTest.java create mode 100644 src/test/java/org/openslx/libvirt/xml/LibvirtXmlTestResources.java create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-cdrom.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-floppy.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-no-hdd.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-no-nic.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-no-sound.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-no-usb.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-ubuntu-20-04-vm-invalid.xml create mode 100644 src/test/resources/libvirt/xml/qemu-kvm_default-ubuntu-20-04-vm.xml (limited to 'src/main/java/org/openslx') diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlCreatable.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlCreatable.java new file mode 100644 index 0000000..e799ace --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlCreatable.java @@ -0,0 +1,26 @@ +package org.openslx.libvirt.xml; + +import org.w3c.dom.Node; + +/** + * Serializability of a Libvirt XML object from/to its XML representation. + * + * @author Manuel Bentele + * @version 1.0 + */ +public interface LibvirtXmlCreatable +{ + /** + * Serializing an object from its XML representation. + * + * @param xmlNode The object's XML representation. + */ + void fromXmlNode( Node xmlNode ); + + /** + * Serializing the object to its XML representation. + * + * @return XML representation of the object. + */ + Node toXmlNode(); +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocument.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocument.java new file mode 100644 index 0000000..abab162 --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocument.java @@ -0,0 +1,374 @@ +package org.openslx.libvirt.xml; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.io.IOUtils; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * A generic representation of a Libvirt XML file. + * + * @implNote Base class to derive the representation of specific Libvirt XML files. + * + * @author Manuel Bentele + * @version 1.0 + */ +public abstract class LibvirtXmlDocument implements LibvirtXmlSerializable, LibvirtXmlValidatable +{ + /** + * Document builder to parse Libvirt XML document from file. + */ + private DocumentBuilder domBuilder = null; + + /** + * Representation of a Libvirt XML document. + */ + private Document xmlDocument = null; + + /** + * XML transformer to transform Libvirt XML document to a file. + */ + private Transformer xmlTransformer = null; + + /** + * XML root node of the Libvirt XML document. + */ + private LibvirtXmlNode rootXmlNode = null; + + /** + * RNG schema validator to validate the Libvirt XML document content. + */ + private LibvirtXmlSchemaValidator rngValidator = null; + + /** + * Creates and initializes XML context to create and transform a Libvirt XML file from/to a file. + * + * @param rngSchema RNG schema to validate the Libvirt XML document content. + * + * @throws LibvirtXmlDocumentException error occured during setup of the XML context to read and + * write from/to a Libvirt XML file. + */ + private void createXmlContext( Source rngSchema ) throws LibvirtXmlDocumentException + { + // used for XML input + try { + DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); + domFactory.setIgnoringElementContentWhitespace( true ); + domFactory.setNamespaceAware( true ); + this.domBuilder = domFactory.newDocumentBuilder(); + } catch ( ParserConfigurationException e ) { + String errorMsg = new String( "Setting up XML context for reading from the Libvirt XML document failed." ); + throw new LibvirtXmlDocumentException( errorMsg ); + } + + // used for XML output + try { + // use hack to load specific transformer factory implementation for XSLT + System.setProperty( TransformerFactory.class.getName(), + "org.apache.xalan.processor.TransformerFactoryImpl" ); + + // create XML transformer factory to create XML transformer with specific indentation + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + + // create XML transformer and apply settings for output XML transformation + InputStream xslOutputSchemaStream = LibvirtXmlResources.getLibvirtXsl( "xml-output-transformation.xsl" ); + StreamSource xslOutputSchema = new StreamSource( xslOutputSchemaStream ); + this.xmlTransformer = transformerFactory.newTransformer( xslOutputSchema ); + } catch ( TransformerConfigurationException e ) { + String errorMsg = new String( "Setting up XML context for writing to the Libvirt XML document failed." ); + throw new LibvirtXmlDocumentException( errorMsg ); + } + + // used for XML validation with RNG schema files + if ( rngSchema != null ) { + try { + this.rngValidator = new LibvirtXmlSchemaValidator( rngSchema ); + } catch ( SAXException e ) { + String errorMsg = new String( "Setting up XML context for validating to the Libvirt XML document failed." ); + e.printStackTrace(); + throw new LibvirtXmlDocumentException( errorMsg ); + } + } + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link String}. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( String xml ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this( xml, null ); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link String}. + * @param rngSchema RNG schema to validate XML content. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( String xml, Source rngSchema ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this.createXmlContext( rngSchema ); + this.fromXml( xml ); + this.validateXml(); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link File}. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( File xml ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this( xml, null ); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link File}. + * @param rngSchema RNG schema to validate XML content. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( File xml, Source rngSchema ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this.createXmlContext( rngSchema ); + this.fromXml( xml ); + this.validateXml(); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link InputStream}. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( InputStream xml ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this( xml, null ); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link InputStream}. + * @param rngSchema RNG schema to validate XML content. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( InputStream xml, Source rngSchema ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this.createXmlContext( rngSchema ); + this.fromXml( xml ); + this.validateXml(); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link InputSource}. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( InputSource xml ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this( xml, null ); + } + + /** + * Creates a Libvirt XML document from a given XML content. + * + * @param xml XML content as {@link InputSource}. + * @param rngSchema RNG schema to validate XML content. + * + * @throws LibvirtXmlDocumentException creation of XML context failed. + * @throws LibvirtXmlSerializationException serialization of the XML content failed. + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public LibvirtXmlDocument( InputSource xml, Source rngSchema ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + this.createXmlContext( rngSchema ); + this.fromXml( xml ); + this.validateXml(); + } + + /** + * Returns the XML root node of the Libvirt XML document. + * + * @return root node of the Libvirt XML document. + */ + public LibvirtXmlNode getRootXmlNode() + { + return this.rootXmlNode; + } + + @Override + public void fromXml( String xml ) throws LibvirtXmlSerializationException + { + try { + this.xmlDocument = this.domBuilder.parse( xml ); + this.xmlDocument.getDocumentElement().normalize(); + } catch ( SAXException e ) { + e.printStackTrace(); + } catch ( IOException e ) { + e.printStackTrace(); + } + + this.rootXmlNode = new LibvirtXmlNode( this.xmlDocument, this.xmlDocument.getDocumentElement() ); + } + + @Override + public void fromXml( File xml ) throws LibvirtXmlSerializationException + { + try { + this.xmlDocument = this.domBuilder.parse( xml ); + this.xmlDocument.getDocumentElement().normalize(); + } catch ( SAXException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } catch ( IOException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } + + this.rootXmlNode = new LibvirtXmlNode( this.xmlDocument, this.xmlDocument.getDocumentElement() ); + } + + @Override + public void fromXml( InputStream xml ) throws LibvirtXmlSerializationException + { + try { + this.xmlDocument = this.domBuilder.parse( xml ); + this.xmlDocument.getDocumentElement().normalize(); + } catch ( SAXException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } catch ( IOException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } + + this.rootXmlNode = new LibvirtXmlNode( this.xmlDocument, this.xmlDocument.getDocumentElement() ); + } + + @Override + public void fromXml( InputSource xml ) throws LibvirtXmlSerializationException + { + try { + this.xmlDocument = this.domBuilder.parse( xml ); + this.xmlDocument.getDocumentElement().normalize(); + } catch ( SAXException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } catch ( IOException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } + + this.rootXmlNode = new LibvirtXmlNode( this.xmlDocument, this.xmlDocument.getDocumentElement() ); + } + + @Override + @SuppressWarnings( "deprecation" ) + public String toXml() throws LibvirtXmlSerializationException + { + StringWriter xmlWriter = null; + String xml = null; + + try { + xmlWriter = new StringWriter(); + DOMSource source = new DOMSource( this.xmlDocument ); + StreamResult xmlString = new StreamResult( xmlWriter ); + this.xmlTransformer.transform( source, xmlString ); + xml = xmlWriter.toString() + System.lineSeparator(); + xmlWriter.close(); + } catch ( TransformerException | IOException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } finally { + IOUtils.closeQuietly( xmlWriter ); + } + + return xml; + } + + @Override + @SuppressWarnings( "deprecation" ) + public void toXml( File xml ) throws LibvirtXmlSerializationException + { + FileWriter xmlWriter = null; + + try { + xmlWriter = new FileWriter( xml ); + DOMSource source = new DOMSource( this.xmlDocument ); + StreamResult xmlStream = new StreamResult( xmlWriter ); + this.xmlTransformer.transform( source, xmlStream ); + xmlWriter.append( System.lineSeparator() ); + xmlWriter.close(); + } catch ( TransformerException | IOException e ) { + throw new LibvirtXmlSerializationException( e.getLocalizedMessage() ); + } finally { + IOUtils.closeQuietly( xmlWriter ); + } + } + + @Override + public void validateXml() throws LibvirtXmlValidationException + { + if ( this.rngValidator != null ) { + this.rngValidator.validate( this.xmlDocument ); + } + } + + @Override + public String toString() + { + try { + return this.toXml(); + } catch ( LibvirtXmlSerializationException e ) { + return null; + } + } +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocumentException.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocumentException.java new file mode 100644 index 0000000..f8605ed --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlDocumentException.java @@ -0,0 +1,25 @@ +package org.openslx.libvirt.xml; + +/** + * An exception of a Libvirt XML document. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class LibvirtXmlDocumentException extends Exception +{ + /** + * Version number for serialization. + */ + private static final long serialVersionUID = -7423926322035713576L; + + /** + * Creates an document exception including an error message. + * + * @param errorMsg message to describe a specific document error. + */ + public LibvirtXmlDocumentException( String errorMsg ) + { + super( errorMsg ); + } +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlEditable.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlEditable.java new file mode 100644 index 0000000..1ddddce --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlEditable.java @@ -0,0 +1,241 @@ +package org.openslx.libvirt.xml; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Editability of XML nodes based on {@link XPath} expressions. + * + * @author Manuel Bentele + * @version 1.0 + */ +public interface LibvirtXmlEditable +{ + /** + * Returns XML node selected by a {@link XPath} expression + * + * @param expression {@link XPath} expression to select XML node. + * @return selected XML node. + */ + public Node getXmlNode( String expression ); + + /** + * Returns XML nodes selected by a {@link XPath} expression + * + * @param expression {@link XPath} expression to select XML nodes. + * @return selected XML nodes. + */ + public NodeList getXmlNodes( String expression ); + + /** + * Return current XML root element. + * + * @return current XML root element. + */ + public default Node getXmlElement() + { + return this.getXmlElement( null ); + } + + /** + * Returns XML element from selection by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + * @return selected XML element. + */ + public Node getXmlElement( String expression ); + + /** + * Sets an XML element selected by a {@link XPath} expression. + * + * If the XML element selected by the given {@link XPath} expression does not exists, the XML + * element will be created. + * + * @param expression {@link XPath} expression to select XML element. + */ + public default void setXmlElement( String expression ) + { + this.setXmlElement( expression, null ); + } + + /** + * Sets a XML element selected by a {@link XPath} expression and appends child XML node. + * + * If the XML element selected by the given {@link XPath} expression does not exists, the XML + * element will be created and the given XML child node is appended. + * + * @param expression {@link XPath} expression to select XML element. + * @param child XML node that will be appended to the selected XML element. + */ + public void setXmlElement( String expression, Node child ); + + /** + * Returns the text value of a XML element selected by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + * @return Text value of the selected XML element. + */ + public String getXmlElementValue( String expression ); + + /** + * Sets the text value of a XML element selected by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + * @param value text value to set selected XML element's text. + */ + public void setXmlElementValue( String expression, String value ); + + /** + * Removes a XML element and all its childs selected by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + */ + public void removeXmlElement( String expression ); + + /** + * Removes all child elements of a XML element selected by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + */ + public void removeXmlElementChilds( String expression ); + + /** + * Returns the text value of a XML attribute from the current XML root element. + * + * @param attributeName name to select XML attribute of the current XML root element. + * @return attribute text of the XML attribute from the current XML root element as + * {@link String}. + */ + public default String getXmlElementAttributeValue( String attributeName ) + { + return this.getXmlElementAttributeValue( null, attributeName ); + } + + /** + * Returns the binary choice of a XML attribute from the current XML root element. + * + * If the text value of the XML attribute equals to yes, the returned {@link boolean} + * value is set to true. Otherwise, if the text value of the XML attribute equals to + * no, the returned {@link boolean} value is set to false. + * + * @param attributeName name to select XML attribute of the current XML root element. + * @return attribute value of the XML attribute from the current XML root element as + * {@link boolean}. + */ + public default boolean getXmlElementAttributeValueAsBool( String attributeName ) + { + return "yes".equals( this.getXmlElementAttributeValue( attributeName ) ); + } + + /** + * Returns the binary choice of a XML attribute from a XML element selected by a + * {@link XPath}expression. + * + * If the text value of the XML attribute equals to yes, the returned {@link boolean} + * value is set to true. Otherwise, if the text value of the XML attribute equals to + * no, the returned {@link boolean} value is set to false. + * + * @param expression {@link XPath} expression to select XML element. + * @param attributeName name to select XML attribute of the current XML root element. + * @return attribute value of the XML attribute from the current XML root element as + * {@link boolean}. + */ + public default boolean getXmlElementAttributeValueAsBool( String expression, String attributeName ) + { + return "yes".equals( this.getXmlElementAttributeValue( expression, attributeName ) ); + } + + /** + * Returns the text value of a XML attribute from a XML element selected by a + * {@link XPath}expression. + * + * @param expression {@link XPath} expression to select XML element. + * @param attributeName name to select XML attribute of the selected XML element. + * @return attribute text of the XML attribute from the selected XML element. + */ + public String getXmlElementAttributeValue( String expression, String attributeName ); + + /** + * Sets the text value of a XML attribute from the current XML root element. + * + * @param attributeName name to select XML attribute of the current XML root element. + * @param value XML attribute value for the selected XML attribute from the current XML root + * element. + */ + public default void setXmlElementAttributeValue( String attributeName, String value ) + { + this.setXmlElementAttributeValue( null, attributeName, value ); + } + + /** + * Sets the binary choice value of a XML attribute from the current XML root element. + * + * If the binary choice value for the XML attribute equals to true, the text value of the + * selected XML attribute is set to yes. Otherwise, if the binary choice value for the + * selected XML attribute equals to false, the text value of the selected XML attribute is + * set to no. + * + * @param attributeName name to select XML attribute of the selected XML element. + * @param value binary choice value for the selected XML attribute from the selected XML element. + */ + public default void setXmlElementAttributeValue( String attributeName, boolean value ) + { + final String valueYesNo = value ? "yes" : "no"; + this.setXmlElementAttributeValue( attributeName, valueYesNo ); + } + + /** + * Sets the binary choice value of a XML attribute from a XML element selected by a + * {@link XPath} expression. + * + * If the binary choice value for the XML attribute equals to true, the text value of the + * selected XML attribute is set to yes. Otherwise, if the binary choice value for the + * selected XML attribute equals to false, the text value of the selected XML attribute is + * set to no. + * + * @param expression {@link XPath} expression to select XML element. + * @param attributeName name to select XML attribute of the selected XML element. + * @param value binary choice value for the selected XML attribute from the selected XML element. + */ + public default void setXmlElementAttributeValue( String expression, String attributeName, boolean value ) + { + final String valueYesNo = value ? "yes" : "no"; + this.setXmlElementAttributeValue( expression, attributeName, valueYesNo ); + } + + /** + * Sets the text value of a XML attribute from a XML element selected by a + * {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + * @param attributeName name to select XML attribute of the selected XML element. + * @param value XML attribute value for the selected XML attribute from the selected XML element. + */ + public void setXmlElementAttributeValue( String expression, String attributeName, String value ); + + /** + * Removes an XML attribute from the current XML root element. + * + * @param attributeName name of the attribute which should be deleted. + */ + public default void removeXmlElementAttribute( String attributeName ) + { + this.removeXmlElementAttribute( null, attributeName ); + } + + /** + * Removes an XML attribute from a XML element selected by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + * @param attributeName name of the attribute which should be deleted. + */ + public void removeXmlElementAttribute( String expression, String attributeName ); + + /** + * Removes all XML attributes from a XML element selected by a {@link XPath} expression. + * + * @param expression {@link XPath} expression to select XML element. + */ + public void removeXmlElementAttributes( String expression ); + +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlNode.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlNode.java new file mode 100644 index 0000000..93e28de --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlNode.java @@ -0,0 +1,356 @@ +package org.openslx.libvirt.xml; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * A representation of a XML node as part of a {@link LibvirtXMLDocument}. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class LibvirtXmlNode implements LibvirtXmlCreatable, LibvirtXmlEditable +{ + /** + * Separation character for internal {@link XPath} expressions. + */ + private static final String XPATH_EXPRESSION_SEPARATOR = "/"; + + /** + * Current XML node selection character for internal {@link XPath} expressions. + */ + private static final String XPATH_EXPRESSION_CURRENT_NODE = "."; + + /** + * Factory to create {@link XPath} objects. + */ + private XPathFactory xPathFactory = null; + + /** + * Representation of the XML document, in which this {@link LibvirtXmlNode} is part of. + */ + private Document xmlDocument = null; + + /** + * Current XML base node as XML root anchor for relative internal {@link XPath} expressions. + */ + private Node xmlBaseNode = null; + + /** + * Create and initialize {@link XPath} context to define and compile custom {@link XPath} + * expressions. + */ + private void createXPathContext() + { + this.xPathFactory = XPathFactory.newInstance(); + } + + /** + * Creates empty Libvirt XML node, which does not belong to any XML document and does not specify + * any XML base node. + * + * @implNote Please call {@link LibvirtXmlNode#setXmlDocument(Document)} and + * {@link LibvirtXmlNode#setXmlBaseNode(Node)} manually to obtain a functional Libvirt + * XML node. + */ + public LibvirtXmlNode() + { + this( null, null ); + } + + /** + * Creates Libvirt XML node from a existing Libvirt XML node by reference. + * + * @param xmlNode existing Libvirt XML node. + */ + public LibvirtXmlNode( LibvirtXmlNode xmlNode ) + { + this( xmlNode.getXmlDocument(), xmlNode.getXmlBaseNode() ); + } + + /** + * Creates Libvirt XML node as part of a existing XML document. + * + * @param xmlDocument existing XML document. + * + * @implNote Please call {@link LibvirtXmlNode#setXmlBaseNode(Node)} manually to obtain a + * functional Libvirt XML node. + */ + public LibvirtXmlNode( Document xmlDocument ) + { + this( xmlDocument, null ); + } + + /** + * Creates Libvirt XML node with a specific XML base node. + * + * @param xmlBaseNode existing XML base node. + * + * @implNote Please call {@link LibvirtXmlNode#setXmlDocument(Document)} manually to obtain a + * functional Libvirt XML node. + */ + public LibvirtXmlNode( Node xmlBaseNode ) + { + this( null, xmlBaseNode ); + } + + /** + * Creates Libvirt XML node with a specific XML base node as part of a XML document. + * + * @param xmlDocument existing XML document. + * @param xmlBaseNode existing XML base node. + */ + public LibvirtXmlNode( Document xmlDocument, Node xmlBaseNode ) + { + this.createXPathContext(); + + this.setXmlDocument( xmlDocument ); + this.setXmlBaseNode( xmlBaseNode ); + } + + /** + * Returns referenced XML document. + * + * @return referenced XML document. + */ + public Document getXmlDocument() + { + return this.xmlDocument; + } + + /** + * Sets existing XML document for Libvirt XML node. + * + * @param xmlDocument existing XML document. + */ + public void setXmlDocument( Document xmlDocument ) + { + this.xmlDocument = xmlDocument; + } + + /** + * Returns current XML base node. + * + * @return current XML base node as XML root anchor of relative internal {@link XPath} + * expressions. + */ + public Node getXmlBaseNode() + { + return this.xmlBaseNode; + } + + /** + * Sets existing XML base node for Libvirt XML node. + * + * @param xmlBaseNode existing XML base node as XML root anchor for relative internal + * {@link XPath} expressions. + */ + public void setXmlBaseNode( Node xmlBaseNode ) + { + this.xmlBaseNode = xmlBaseNode; + } + + @Override + public Node getXmlNode( String expression ) + { + NodeList nodes = this.getXmlNodes( expression ); + return nodes.item( 0 ); + } + + @Override + public NodeList getXmlNodes( String expression ) + { + Object nodes = null; + + try { + XPath xPath = this.xPathFactory.newXPath(); + XPathExpression xPathExpr = xPath.compile( expression ); + nodes = xPathExpr.evaluate( this.xmlBaseNode, XPathConstants.NODESET ); + } catch ( XPathExpressionException e ) { + e.printStackTrace(); + } + + return NodeList.class.cast( nodes ); + } + + @Override + public Node getXmlElement( String expression ) + { + String completeExpression = null; + + if ( expression == null ) { + completeExpression = XPATH_EXPRESSION_CURRENT_NODE; + } else if ( expression.isEmpty() ) { + completeExpression = XPATH_EXPRESSION_CURRENT_NODE; + } else { + completeExpression = XPATH_EXPRESSION_CURRENT_NODE + XPATH_EXPRESSION_SEPARATOR + expression; + } + + Node node = this.getXmlNode( completeExpression ); + + if ( node != null && node.getNodeType() == Node.ELEMENT_NODE ) { + return node; + } else { + return null; + } + } + + private Node createXmlElement( String expression ) + { + Node parentNode = this.xmlBaseNode; + Node currentNode = parentNode; + + if ( expression != null && !expression.isEmpty() ) { + String[] nodeNames = expression.split( XPATH_EXPRESSION_SEPARATOR ); + String partialExpression = XPATH_EXPRESSION_CURRENT_NODE; + + for ( int i = 0; i < nodeNames.length; i++ ) { + partialExpression += XPATH_EXPRESSION_SEPARATOR + nodeNames[i]; + currentNode = this.getXmlNode( partialExpression ); + + if ( currentNode == null ) { + parentNode.appendChild( this.xmlDocument.createElement( nodeNames[i] ) ); + currentNode = parentNode.getLastChild(); + } + + parentNode = currentNode; + } + } + + return currentNode; + } + + @Override + public void setXmlElement( String expression, Node child ) + { + Node node = this.createXmlElement( expression ); + + if ( child != null ) { + node.appendChild( child ); + } + } + + @Override + public String getXmlElementValue( String expression ) + { + Node node = this.getXmlElement( expression ); + + if ( node != null ) { + return node.getTextContent(); + } else { + return null; + } + } + + @Override + public void setXmlElementValue( String expression, String value ) + { + Node node = this.createXmlElement( expression ); + node.setTextContent( value ); + } + + @Override + public void removeXmlElement( String expression ) + { + Node node = this.getXmlElement( expression ); + + if ( node != null ) { + node.getParentNode().removeChild( node ); + } + } + + @Override + public void removeXmlElementChilds( String expression ) + { + Node node = this.getXmlElement( expression ); + + if ( node != null ) { + for ( int i = 0; i < node.getChildNodes().getLength(); i++ ) { + Node child = node.getChildNodes().item( 0 ); + node.removeChild( child ); + } + } + } + + @Override + public String getXmlElementAttributeValue( String expression, String attributeName ) + { + Node node = null; + + if ( expression != null && !expression.isEmpty() ) { + node = this.getXmlElement( expression ); + } else { + node = this.xmlBaseNode; + } + + if ( node == null ) { + return null; + } else { + Node attribute = node.getAttributes().getNamedItem( attributeName ); + + if ( attribute == null ) { + return null; + } else { + return attribute.getNodeValue(); + } + } + } + + @Override + public void setXmlElementAttributeValue( String expression, String attributeName, String value ) + { + Node node = this.createXmlElement( expression ); + Node attribute = node.getAttributes().getNamedItem( attributeName ); + + if ( attribute == null ) { + Element element = Element.class.cast( node ); + element.setAttribute( attributeName, value ); + } else { + attribute.setNodeValue( value ); + } + } + + @Override + public void removeXmlElementAttribute( String expression, String attributeName ) + { + Node node = this.getXmlElement( expression ); + + if ( node != null ) { + Node attribute = node.getAttributes().getNamedItem( attributeName ); + node.getAttributes().removeNamedItem( attribute.getNodeName() ); + } + } + + @Override + public void removeXmlElementAttributes( String expression ) + { + Node node = this.getXmlElement( expression ); + + if ( node != null ) { + for ( int i = 0; i < node.getAttributes().getLength(); i++ ) { + Node attribute = node.getAttributes().item( 0 ); + node.getAttributes().removeNamedItem( attribute.getNodeName() ); + } + } + } + + @Override + public void fromXmlNode( Node xmlNode ) + { + this.setXmlBaseNode( xmlNode ); + } + + @Override + public Node toXmlNode() + { + return this.getXmlBaseNode(); + } +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlResources.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlResources.java new file mode 100644 index 0000000..38c818b --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlResources.java @@ -0,0 +1,52 @@ +package org.openslx.libvirt.xml; + +import java.io.File; +import java.io.InputStream; + +/** + * Collection of resource utils for a Libvirt XML document. + * + * @author Manuel Bentele + * @version 1.0 + */ +public final class LibvirtXmlResources +{ + /** + * File path prefix of the absolute path to the libvirt resource folder in a *.jar file. + */ + private static final String LIBVIRT_PREFIX_PATH = File.separator + "libvirt"; + + /** + * File path prefix of the absolute path to the libvirt XSL resource folder in a *.jar file. + */ + private static final String LIBVIRT_PREFIX_PATH_XSL = LIBVIRT_PREFIX_PATH + File.separator + "xsl"; + + /** + * File path prefix of the absolute path to the libvirt RNG resource folder in a *.jar file. + */ + private static final String LIBVIRT_PREFIX_PATH_RNG = LIBVIRT_PREFIX_PATH + File.separator + "rng"; + + /** + * Returns a Libvirt XSL resource as stream. + * + * @param libvirtXslFileName file name of the XSL resource in the resources *.jar folder. + * @return Libvirt XSL resource as stream. + */ + public static InputStream getLibvirtXsl( String libvirtXslFileName ) + { + String libvirtXslPath = LibvirtXmlResources.LIBVIRT_PREFIX_PATH_XSL + File.separator + libvirtXslFileName; + return LibvirtXmlResources.class.getResourceAsStream( libvirtXslPath ); + } + + /** + * Returns a Libvirt RNG schema resource as stream. + * + * @param libvirtRngFileName file name of the RNG schema resource in the resources *.jar folder. + * @return Libvirt RNG schema resource as stream. + */ + public static InputStream getLibvirtRng( String libvirtRngFileName ) + { + String libvirtRngPath = LibvirtXmlResources.LIBVIRT_PREFIX_PATH_RNG + File.separator + libvirtRngFileName; + return LibvirtXmlResources.class.getResourceAsStream( libvirtRngPath ); + } +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSchemaValidator.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSchemaValidator.java new file mode 100644 index 0000000..01e8adb --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSchemaValidator.java @@ -0,0 +1,283 @@ +package org.openslx.libvirt.xml; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import javax.xml.XMLConstants; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.w3c.dom.Document; +import org.w3c.dom.ls.LSInput; +import org.w3c.dom.ls.LSResourceResolver; +import org.xml.sax.SAXException; + +/** + * Resource resolver input for RelaxNG schemas. + * + * @author Manuel Bentele + * @version 1.0 + */ +class LibvirtXmlSchemaResourceInput implements LSInput +{ + /** + * Stores the public identification of the schema resource. + */ + private String publicId; + + /** + * Stores the system identification of the schema resource. + */ + private String systemId; + + /** + * Stream to process and read the schema resource. + */ + private BufferedInputStream inputStream; + + /** + * Creates a resource resolver input for a RelaxNG schema. + * + * @param publicId public identification of the schema resource. + * @param sysId system identification of the schema resource. + * @param input stream of the schema resource. + */ + public LibvirtXmlSchemaResourceInput( String publicId, String sysId, InputStream input ) + { + this.publicId = publicId; + this.systemId = sysId; + this.inputStream = new BufferedInputStream( input ); + } + + @Override + public String getBaseURI() + { + return null; + } + + @Override + public InputStream getByteStream() + { + return null; + } + + @Override + public boolean getCertifiedText() + { + return false; + } + + @Override + public Reader getCharacterStream() + { + return null; + } + + @Override + public String getEncoding() + { + return null; + } + + @Override + public String getPublicId() + { + return this.publicId; + } + + @Override + public String getStringData() + { + String data = null; + + synchronized ( this.inputStream ) { + try { + int inputLength = this.inputStream.available(); + byte[] input = new byte[ inputLength ]; + this.inputStream.read( input ); + data = new String( input ); + } catch ( IOException e ) { + e.printStackTrace(); + } + } + + return data; + } + + @Override + public String getSystemId() + { + return this.systemId; + } + + @Override + public void setBaseURI( String arg0 ) + { + } + + @Override + public void setByteStream( InputStream arg0 ) + { + } + + @Override + public void setCertifiedText( boolean arg0 ) + { + } + + @Override + public void setCharacterStream( Reader arg0 ) + { + } + + @Override + public void setEncoding( String arg0 ) + { + } + + @Override + public void setPublicId( String arg0 ) + { + this.publicId = arg0; + } + + @Override + public void setStringData( String arg0 ) + { + } + + @Override + public void setSystemId( String arg0 ) + { + this.systemId = arg0; + } +} + +/** + * Resource resolver for RelaxNG schemas. + * + * @author Manuel Bentele + * @version 1.0 + */ +class LibvirtXmlSchemaResourceResolver implements LSResourceResolver +{ + @Override + public LSInput resolveResource( String type, String namespaceURI, String publicId, String systemId, String baseURI ) + { + InputStream rngResourceStream = LibvirtXmlResources.getLibvirtRng( systemId ); + return new LibvirtXmlSchemaResourceInput( publicId, systemId, rngResourceStream ); + } +} + +/** + * Validator for validation of Libvirt XML documents with RelaxNG schemas. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class LibvirtXmlSchemaValidator +{ + /** + * RelaxNG based validator for validation of Libvirt XML documents. + */ + private Validator rngSchemaValidator; + + /** + * Creates a validator for validation of Libvirt XML documents with RelaxNG schemas. + * + * @param rngSchema + * @throws SAXException + */ + public LibvirtXmlSchemaValidator( Source rngSchema ) throws SAXException + { + this.createValidationContext( rngSchema ); + } + + /** + * Creates context for validation of Libvirt XML documents with a RelaxNG schema. + * + * @param rngSchema RelaxNG schema used for validation with {@link #validate(Document)}. + * + * @throws SAXException Loading, creation and processing of rngSchema has failed. + */ + private void createValidationContext( Source rngSchema ) throws SAXException + { + // use hack to load specific schema factory implementation for RelaxNG schemas + System.setProperty( SchemaFactory.class.getName() + ":" + XMLConstants.RELAXNG_NS_URI, + "com.thaiopensource.relaxng.jaxp.XMLSyntaxSchemaFactory" ); + + // create schema resource resolver to resolve schema resources during parsing and validation + LibvirtXmlSchemaResourceResolver schemaResolver = new LibvirtXmlSchemaResourceResolver(); + + // create schema factory to be able to create a RelaxNG schema validator + SchemaFactory factory = SchemaFactory.newInstance( XMLConstants.RELAXNG_NS_URI ); + factory.setResourceResolver( schemaResolver ); + Schema schema = factory.newSchema( rngSchema ); + + // create the RelaxNG schema validator + this.rngSchemaValidator = schema.newValidator(); + this.rngSchemaValidator.setResourceResolver( schemaResolver ); + } + + /** + * Transforms a DOM source to a Stream source. + * + * @param domSource DOM source of a Libvirt XML document. + * @return Stream source of a Libvirt XML document. + * + * @throws TransformerException Transformation of DOM source to a Stream source has failed. + * + * @implNote This utility method is necessary in {@link #validate(Document)} to be able to + * validate a DOM Libvirt XML document with the schema and validator implementation for + * RelaxNG schema files from + * com.thaiopensource.relaxng.jaxp.XMLSyntaxSchemaFactory. + */ + private static StreamSource toStreamSource( DOMSource domSource ) throws TransformerException + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + StreamResult result = new StreamResult( outputStream ); + + // create identity transformer + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.transform( domSource, result ); + + ByteArrayInputStream inputStream = new ByteArrayInputStream( outputStream.toByteArray() ); + return new StreamSource( inputStream ); + } + + /** + * Validates a given (and parsed) DOM Libvirt XML document. + * + * Validation takes place if the specified xmlDocument is non-null, otherwise the + * validation succeeds immediately. If the validation of the xmlDocument fails, a + * validation exception is thrown. + * + * @param xmlDocument Libvirt XML document. + * + * @throws LibvirtXmlValidationException Validation of Libvirt XML document failed. + */ + public void validate( Document xmlDocument ) throws LibvirtXmlValidationException + { + if ( xmlDocument != null ) { + try { + DOMSource domSource = new DOMSource( xmlDocument ); + StreamSource source = LibvirtXmlSchemaValidator.toStreamSource( domSource ); + this.rngSchemaValidator.validate( source ); + } catch ( SAXException | TransformerException | IOException e ) { + throw new LibvirtXmlValidationException( e.getLocalizedMessage() ); + } + } + } +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializable.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializable.java new file mode 100644 index 0000000..6f11ce5 --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializable.java @@ -0,0 +1,57 @@ +package org.openslx.libvirt.xml; + +import java.io.File; +import java.io.InputStream; + +import org.xml.sax.InputSource; + +/** + * Serializability of a Libvirt XML document from/to a XML file. + * + * @author Manuel Bentele + * @version 1.0 + */ +public abstract interface LibvirtXmlSerializable +{ + /** + * Serialize Libvirt XML document from {@link String}. + * + * @param xml {@link String} containing XML content. + */ + public void fromXml( String xml ) throws LibvirtXmlSerializationException; + + /** + * Serialize Libvirt XML document from {@link File}. + * + * @param xml {@link File} containing XML content. + */ + public void fromXml( File xml ) throws LibvirtXmlSerializationException; + + /** + * Serialize Libvirt XML document from {@link InputStream}. + * + * @param xml {@link InputStream} providing XML content. + */ + void fromXml( InputStream xml ) throws LibvirtXmlSerializationException; + + /** + * Serialize Libvirt XML document from {@link InputSource}. + * + * @param xml {@link InputSource} providing XML content. + */ + public void fromXml( InputSource xml ) throws LibvirtXmlSerializationException; + + /** + * Serialize Libvirt XML document to {@link String}. + * + * @return XML {@link String} containing Libvirt XML document content. + */ + public String toXml() throws LibvirtXmlSerializationException; + + /** + * Serialize Libvirt XML document to {@link File}. + * + * @param xml XML {@link File} containing Libvirt XML document content. + */ + public void toXml( File xml ) throws LibvirtXmlSerializationException; +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializationException.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializationException.java new file mode 100644 index 0000000..d522107 --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlSerializationException.java @@ -0,0 +1,25 @@ +package org.openslx.libvirt.xml; + +/** + * An exception of an serialization error during Libvirt XML serialization. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class LibvirtXmlSerializationException extends Exception +{ + /** + * Version number for serialization. + */ + private static final long serialVersionUID = 7995955592221349949L; + + /** + * Creates a XML serialization exception including an error message. + * + * @param errorMsg message to describe a specific XML serialization error. + */ + public LibvirtXmlSerializationException( String errorMsg ) + { + super( errorMsg ); + } +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidatable.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidatable.java new file mode 100644 index 0000000..8574cc4 --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidatable.java @@ -0,0 +1,18 @@ +package org.openslx.libvirt.xml; + +/** + * Validatability of Libvirt XML document. + * + * @author Manuel Bentele + * @version 1.0 + */ +public abstract interface LibvirtXmlValidatable +{ + /** + * Validates the XML document's content and report error if document is not a valid Libvirt XML + * document. + * + * @throws LibvirtXmlValidationException XML content is not a valid Libvirt XML. + */ + public void validateXml() throws LibvirtXmlValidationException; +} diff --git a/src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidationException.java b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidationException.java new file mode 100644 index 0000000..24e9db7 --- /dev/null +++ b/src/main/java/org/openslx/libvirt/xml/LibvirtXmlValidationException.java @@ -0,0 +1,25 @@ +package org.openslx.libvirt.xml; + +/** + * An exception of an unsuccessful Libvirt XML validation. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class LibvirtXmlValidationException extends Exception +{ + /** + * Version number for serialization. + */ + private static final long serialVersionUID = 2299967599483742777L; + + /** + * Creates a validation exception including an error message. + * + * @param errorMsg message to describe a specific Libvirt XML error. + */ + public LibvirtXmlValidationException( String errorMsg ) + { + super( errorMsg ); + } +} diff --git a/src/test/java/org/openslx/libvirt/xml/LibvirtXmlDocumentTest.java b/src/test/java/org/openslx/libvirt/xml/LibvirtXmlDocumentTest.java new file mode 100644 index 0000000..1b6e5a5 --- /dev/null +++ b/src/test/java/org/openslx/libvirt/xml/LibvirtXmlDocumentTest.java @@ -0,0 +1,262 @@ +package org.openslx.libvirt.xml; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; + +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Level; +import org.apache.log4j.LogManager; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +class LibvirtXmlDocumentStub extends LibvirtXmlDocument +{ + public LibvirtXmlDocumentStub( File xml ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + super( xml ); + } + + public LibvirtXmlDocumentStub( File xml, Source rngSchema ) + throws LibvirtXmlDocumentException, LibvirtXmlSerializationException, LibvirtXmlValidationException + { + super( xml, rngSchema ); + } +} + +public class LibvirtXmlDocumentTest +{ + private static final String EMPTY = new String(); + + @BeforeAll + public static void setUp() + { + // disable logging with log4j + LogManager.getRootLogger().setLevel( Level.OFF ); + } + + private LibvirtXmlDocument newLibvirtXmlDocumentInstance( String xmlFileName ) + { + LibvirtXmlDocument document = null; + + try { + File xmlFile = LibvirtXmlTestResources.getLibvirtXmlFile( xmlFileName ); + document = new LibvirtXmlDocumentStub( xmlFile ); + } catch ( LibvirtXmlDocumentException | LibvirtXmlSerializationException | LibvirtXmlValidationException e ) { + String errorMsg = new String( "Cannot prepare requested Libvirt XML file from the resources folder" ); + fail( errorMsg ); + } + + return document; + } + + private LibvirtXmlDocument newLibvirtXmlDocumentValidationInstance( String xmlFileName, String rngSchemaFileName ) + throws LibvirtXmlValidationException + { + LibvirtXmlDocument document = null; + + try { + File xmlFile = LibvirtXmlTestResources.getLibvirtXmlFile( xmlFileName ); + Source rngSchemaSource = new StreamSource( LibvirtXmlResources.getLibvirtRng( rngSchemaFileName ) ); + document = new LibvirtXmlDocumentStub( xmlFile, rngSchemaSource ); + } catch ( LibvirtXmlDocumentException | LibvirtXmlSerializationException e ) { + String errorMsg = new String( "Cannot prepare requested Libvirt XML file from the resources folder" ); + fail( errorMsg ); + } + + return document; + } + + @Test + @DisplayName( "Read libvirt XML file to String" ) + public void testReadXmlFileToString() throws LibvirtXmlSerializationException, IOException + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + File originalXmlFile = LibvirtXmlTestResources.getLibvirtXmlFile( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + + final String readXmlContent = vm.toXml(); + final String originalXmlContent = FileUtils.readFileToString( originalXmlFile, StandardCharsets.UTF_8 ); + + assertNotNull( readXmlContent ); + + final int lengthReadXmlContent = readXmlContent.split( System.lineSeparator() ).length; + final int lengthOriginalXmlContent = originalXmlContent.split( System.lineSeparator() ).length; + + assertEquals( lengthOriginalXmlContent, lengthReadXmlContent ); + } + + @Test + @DisplayName( "Read libvirt XML file to file" ) + public void testReadXmlFileToFile() throws LibvirtXmlSerializationException, IOException + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + File originalXmlFile = LibvirtXmlTestResources.getLibvirtXmlFile( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + File readXmlFile = LibvirtXmlTestResources.createLibvirtXmlTempFile(); + + vm.toXml( readXmlFile ); + + final String readXmlContent = FileUtils.readFileToString( readXmlFile, StandardCharsets.UTF_8 ); + final String originalXmlContent = FileUtils.readFileToString( originalXmlFile, StandardCharsets.UTF_8 ); + + assertNotNull( readXmlContent ); + + final int lengthReadXmlContent = readXmlContent.split( System.lineSeparator() ).length; + final int lengthOriginalXmlContent = originalXmlContent.split( System.lineSeparator() ).length; + + assertEquals( lengthOriginalXmlContent, lengthReadXmlContent ); + } + + @Test + @DisplayName( "Validate correct libvirt XML file" ) + public void testValidateCorrectXmlFile() + { + Executable validateXmlDocument = () -> { + this.newLibvirtXmlDocumentValidationInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml", "domain.rng" ); + }; + + assertDoesNotThrow( validateXmlDocument ); + } + + @Test + @DisplayName( "Validate incorrect libvirt XML file" ) + public void testValidateIncorrectXmlFile() + { + Executable validateXmlDocument = () -> { + this.newLibvirtXmlDocumentValidationInstance( "qemu-kvm_default-ubuntu-20-04-vm-invalid.xml", "domain.rng" ); + }; + + assertThrows( LibvirtXmlValidationException.class, validateXmlDocument ); + } + + @Test + @DisplayName( "Get non-existent node from libvirt XML file" ) + public void testGetNonExistentElement() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertNull( vm.getRootXmlNode().getXmlElement( "info" ) ); + } + + @Test + @DisplayName( "Set non-existent node in libvirt XML file" ) + public void testSetNonExistentElement() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElement( "info" ); + assertNotNull( vm.getRootXmlNode().getXmlElement( "info" ) ); + } + + @Test + @DisplayName( "Get non-existent element's value in libvirt XML file" ) + public void testGetNonExistentElementValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertNull( vm.getRootXmlNode().getXmlElementValue( "info" ) ); + } + + @Test + @DisplayName( "Set non-existent element's value in libvirt XML file" ) + public void testSetNonExistentElementValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElementValue( "info", "content" ); + assertEquals( "content", vm.getRootXmlNode().getXmlElementValue( "info" ) ); + } + + @Test + @DisplayName( "Get empty element from libvirt XML file" ) + public void testGetEmptyElement() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertNotNull( vm.getRootXmlNode().getXmlElement( "features/acpi" ) ); + } + + @Test + @DisplayName( "Set empty element in libvirt XML file" ) + public void testSetEmptyElement() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElement( "features/acpi" ); + assertNotNull( vm.getRootXmlNode().getXmlElement( "features/acpi" ) ); + } + + @Test + @DisplayName( "Get empty element's value from libvirt XML file" ) + public void testGetEmptyElementValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertEquals( EMPTY, vm.getRootXmlNode().getXmlElementValue( "features/acpi" ) ); + } + + @Test + @DisplayName( "Set empty element's value in libvirt XML file" ) + public void testSetEmptyElementValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElementValue( "features/acpi", "content" ); + assertEquals( "content", vm.getRootXmlNode().getXmlElementValue( "features/acpi" ) ); + } + + @Test + @DisplayName( "Get non-existent element's attribute value from libvirt XML file" ) + public void testGetNonExistentElementAttributeValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertNull( vm.getRootXmlNode().getXmlElementAttributeValue( "info", "test" ) ); + } + + @Test + @DisplayName( "Set non-existent element's attribute value from libvirt XML file" ) + public void testSetNonExistentElementAttributeValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElementAttributeValue( "info", "test", "info" ); + assertEquals( "info", vm.getRootXmlNode().getXmlElementAttributeValue( "info", "test" ) ); + } + + @Test + @DisplayName( "Get element's non-existent attribute value from libvirt XML file" ) + public void testGetElementNonExistentAttributeValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertNull( vm.getRootXmlNode().getXmlElementAttributeValue( "features/acpi", "test" ) ); + } + + @Test + @DisplayName( "Set element's non-existent attribute value from libvirt XML file" ) + public void testSetElementNonExistentAttributeValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElementAttributeValue( "features/acpi", "test", "info" ); + assertEquals( "info", vm.getRootXmlNode().getXmlElementAttributeValue( "features/acpi", "test" ) ); + } + + @Test + @DisplayName( "Get element's attribute value from libvirt XML file" ) + public void testGetElementAttributeValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + assertEquals( "partial", vm.getRootXmlNode().getXmlElementAttributeValue( "cpu", "check" ) ); + } + + @Test + @DisplayName( "Set element's attribute value from libvirt XML file" ) + public void testSetElementAttributeValue() + { + LibvirtXmlDocument vm = this.newLibvirtXmlDocumentInstance( "qemu-kvm_default-ubuntu-20-04-vm.xml" ); + vm.getRootXmlNode().setXmlElementAttributeValue( "cpu", "check", "full" ); + assertEquals( "full", vm.getRootXmlNode().getXmlElementAttributeValue( "cpu", "check" ) ); + } +} diff --git a/src/test/java/org/openslx/libvirt/xml/LibvirtXmlTestResources.java b/src/test/java/org/openslx/libvirt/xml/LibvirtXmlTestResources.java new file mode 100644 index 0000000..6cc0360 --- /dev/null +++ b/src/test/java/org/openslx/libvirt/xml/LibvirtXmlTestResources.java @@ -0,0 +1,29 @@ +package org.openslx.libvirt.xml; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +public final class LibvirtXmlTestResources +{ + private static final String LIBVIRT_PREFIX_PATH = File.separator + "libvirt"; + private static final String LIBVIRT_PREFIX_PATH_XML = LIBVIRT_PREFIX_PATH + File.separator + "xml"; + + private static final String LIBVIRT_TEMP_PREFIX = "libvirt-"; + private static final String LIBVIRT_TEMP_SUFFIX = ".xml"; + + public static File getLibvirtXmlFile( String libvirtXmlFileName ) + { + String libvirtXmlPath = LibvirtXmlTestResources.LIBVIRT_PREFIX_PATH_XML + File.separator + libvirtXmlFileName; + URL libvirtXml = LibvirtXmlTestResources.class.getResource( libvirtXmlPath ); + return new File( libvirtXml.getFile() ); + } + + public static File createLibvirtXmlTempFile() throws IOException + { + File tempFile = File.createTempFile( LibvirtXmlTestResources.LIBVIRT_TEMP_PREFIX, + LibvirtXmlTestResources.LIBVIRT_TEMP_SUFFIX ); + tempFile.deleteOnExit(); + return tempFile; + } +} diff --git a/src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-cdrom.xml b/src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-cdrom.xml new file mode 100644 index 0000000..617e20b --- /dev/null +++ b/src/test/resources/libvirt/xml/qemu-kvm_default-archlinux-vm-cdrom.xml @@ -0,0 +1,144 @@ + + archlinux + 22bbd81f-b31b-4242-9907-8840844944bf + + + + + + 4194304 + 4194304 + 2 + + hvm + + + + + + + + + + + + + + destroy + restart + destroy + + + + + + /usr/bin/qemu-system-x86_64 + + + + +
+ + + + + + + +
+ + +
+ + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + +
+ + + + + + + + + + + +
+ + + +
+ + +
+ + + + + + + + +
+ +