Обзор
Здравствуйте! В этой статье я хочу описать программу валидации XML с помощью Spring Framework. Наиболее очевидная область применения такой валидации — это программирование web-сервисов.
Валидация производится через преобразование XML-Java (unmarshalling) по соответствующей XSD-схеме. XML-файл считается прошедшим проверку, если преобразование XML в объект Java прошло успешно.
Проект компилируется в jar файл и запускается в командной строке. Для красоты прикручен Apache ANSI Printer, в котором можно задавать шрифт, цвет и выделение текста в cmd.
Исходный код доступен в GitHub по ссылке XmlProcessor.
Итак, приступим.
1. Исходные файлы и схемы
В качестве входных данных определим 2 XML файла Address.xml и Client.xml.
Address.xml:
<?xml version="1.0" encoding="UTF-8"?>
<ns:Request xmlns:ns="http://www.tempuri.org/types">
<Version>V001.000.00</Version>
<Address>
<Apartment>50</Apartment>
<House>7</House>
<Street>Sadovaya</Street>
<City>SPB</City>
<Country>Russia</Country>
<Index>123456</Index>
</Address>
</ns:Request>
Client.xml:
<?xml version="1.0" encoding="UTF-8"?>
<ns:Request xmlns:ns="http://www.tempuri.org/types">
<Version>V001.000.00</Version>
<Client>
<Id>12</Id>
<Name>A</Name>
</Client>
<Client>
<Id>34</Id>
<Name>B</Name>
</Client>
</ns:Request>
Далее определим XSD-схемы XmlValidator.xsd, ComplexTypes.xsd и SimpleTypes.xsd с
описанием контейнера CombinedType и объектов типа Address и Client:
XmlValidator.xsd:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsd:schema xmlns:ns="http://www.tempuri.org/types"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:ict="complextypes"
targetNamespace="http://www.tempuri.org/types"
elementFormDefault="qualified">
<xsd:import namespace="complextypes" schemaLocation="complextypes.xsd"/>
<xsd:annotation>
<xsd:documentation>XSD structure</xsd:documentation>
</xsd:annotation>
<xsd:element name="Combined" type="ict:CombinedType">
<xsd:annotation>
<xsd:documentation>XML definition</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:schema>
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="complextypes"
xmlns:ist="simpletypes"
targetNamespace="complextypes">
<xsd:import namespace="simpletypes" schemaLocation="simpletypes.xsd"/>
<xsd:complexType name="CombinedType">
<xsd:sequence>
<xsd:element name="Version" type="ist:VersionType">
<xsd:annotation>
<xsd:documentation>The version</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:choice>
<xsd:element name="Address" type="AddressType" maxOccurs="1">
<xsd:annotation>
<xsd:documentation>Address type</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="Client" type="ClientType" maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>Client type</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="AddressType">
<xsd:sequence>
<xsd:element name="Apartment" type="ist:ApartmentType">
<xsd:annotation>
<xsd:documentation>Apartment number</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="House" type="ist:HouseNumberType">
<xsd:annotation>
<xsd:documentation>House number</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="Street" type="ist:StreetType">
<xsd:annotation>
<xsd:documentation>Street name</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="City" type="ist:CityType">
<xsd:annotation>
<xsd:documentation>City name</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="Country" type="ist:CountryType">
<xsd:annotation>
<xsd:documentation>Country name</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="Index" type="ist:IndexType" minOccurs="0">
<xsd:annotation>
<xsd:documentation>Postal index</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="ClientType">
<xsd:sequence>
<xsd:element name="Id" type="ist:IdType">
<xsd:annotation>
<xsd:documentation>The id</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="Name" type="ist:NameType">
<xsd:annotation>
<xsd:documentation>The name</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="simpletypes"
targetNamespace="simpletypes">
<xsd:simpleType name="VersionType">
<xsd:annotation>
<xsd:documentation>V000.000.00</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:maxLength value="11"/>
<xsd:pattern value="V[0-9]{3}.[0-9]{3}.[0-9]{2}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="IdType">
<xsd:annotation>
<xsd:documentation>Int, 10 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:nonNegativeInteger">
<xsd:totalDigits value="10"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="NameType">
<xsd:annotation>
<xsd:documentation>String, 50 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:maxLength value="50"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ApartmentType">
<xsd:annotation>
<xsd:documentation>Int, 4 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:nonNegativeInteger">
<xsd:totalDigits value="4"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="HouseNumberType">
<xsd:annotation>
<xsd:documentation>Int, 3 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:nonNegativeInteger">
<xsd:totalDigits value="3"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="StreetType">
<xsd:annotation>
<xsd:documentation>String, 40 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:maxLength value="40"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="CityType">
<xsd:annotation>
<xsd:documentation>City, 40 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:maxLength value="40"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="CountryType">
<xsd:annotation>
<xsd:documentation>Country, 30 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:maxLength value="30"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="IndexType">
<xsd:annotation>
<xsd:documentation>Int, 10 max</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:nonNegativeInteger">
<xsd:totalDigits value="10"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
Обратите внимание на элемент <xsd:choice> в CombinedType схемы ComplexTypes.xsd. Он означает, что xml-файл должен содержать либо один элемент типа Address, либо один или несколько элементов типа Client.
2. Конфигурация Spring
В файле spring-config.xml находится описание используемых в проекте бинов Printer,
FileReader, Marshaller, XMLService. Помимо указанных бинов, определен фильтр расширений
FileNameExtensionFilter, чтобы рассматривать только xml-файлы.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd">
<!-- Printer -->
<bean id="printer" class="com.xmlprocessor.service.impl.AnsiConsolePrinter"/>
<!-- FilenameExtensionFilter -->
<bean id="filenameExtensionFilter"class=
"com.xmlprocessor.util.FilenameExtensionFilter">
<constructor-arg index="0">
<list>
<value>xml</value>
</list>
</constructor-arg>
</bean>
<!-- FileReader -->
<bean id="fileReader" class="com.xmlprocessor.service.impl.FileReader">
<property name="filenameFilter" ref="filenameExtensionFilter" />
</bean>
<!-- Marshaller -->
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="classesToBeBound">
<list>
<value>com.xmlprocessor.types.CombinedType</value>
</list>
</property>
<property name="schemas">
<list>
<value>xmlvalidator.xsd</value>
<value>complextypes.xsd</value>
<value>simpletypes.xsd</value>
</list>
</property>
<property name="marshallerProperties">
<map>
<entry>
<key>
<util:constant static-field=
"javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT"/>
</key>
<value type="java.lang.Boolean">true</value>
</entry>
</map>
</property>
</bean>
<!-- XmlService -->
<bean id="xmlService" class="com.xmlprocessor.service.impl.XmlService">
<property name="printer" ref="printer" />
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
</bean>
</beans>
3. Преобразование XML-Java
В точке входа приложения XmlProcessorDrv сначала считываем и проверяем аргументы
командной строки, затем через вызов методов класса Compositor создаем бины printer,
fileReader и xmlService. Далее считываем xml-файлы и выполняем валидацию файлов в
директории, указанной в CLI_OPTION_DIRECTORY.
Самое интересное происходит при вызове
xmlService.validate(xmlFiles)
package com.xmlprocessor.main;
import java.io.File;
import java.util.List;
import com.xmlprocessor.config.Compositor;
import com.xmlprocessor.service.api.PrinterInt;
import com.xmlprocessor.service.api.XmlServiceInt;
import com.xmlprocessor.util.CommandLineArgs;
public class XmlProcessorDrv {
/** Name of the program */
private static final String PROG_NAME = XmlProcessorDrv.class.getSimpleName();
/** Version of the Program */
private static final String PROG_VERSION = "1.0 (XmlProcessor v1.000)";
/** Exit Status {@value} for OK. */
private static final int EXIT_STATUS_OK = 0;
/** Exit Status {@value} for not OK. */
private static final int EXIT_STATUS_NOT_OK = -1;
/**
* Main entry point.
* Evaluates command line args and validates provided xml files
*
* @param args
* Command line arguments
*/
public static void main(String[] args) {
// execution status
int exitStatus;
// get printer object
PrinterInt printer = Compositor.getPrinter();
// read command line args
CommandLineArgs cmdLineArgs = new CommandLineArgs(args);
// Show version
if (cmdLineArgs.hasOption(CommandLineArgs.CLI_OPTION_VERSION)) {
printer.printf("%s v%s\n", PROG_NAME, PROG_VERSION);
}
// Show help
if (cmdLineArgs.hasOption(CommandLineArgs.CLI_OPTION_HELP)) {
cmdLineArgs.printHelp(PROG_NAME);
}
// Check if the directory name is passed in args
if (!cmdLineArgs.hasOption(CommandLineArgs.CLI_OPTION_DIRECTORY)) {
cmdLineArgs.printHelp(PROG_NAME);
return;
}
String dir = cmdLineArgs.getOptionValue(CommandLineArgs.CLI_OPTION_DIRECTORY);
printer.printf("\n%s %s","Folder with XML files: ", dir);
List<File> xmlFiles;
XmlServiceInt xmlService = Compositor.getXmlService();
try {
xmlFiles = Compositor.getFileReader().readFiles(dir);
printer.bold("\n\nStart validating XML files:\n");
xmlService.validate(xmlFiles);
exitStatus = EXIT_STATUS_OK;
} catch (Exception ex) {
printer.errorln("\n" + ex.getMessage());
exitStatus = EXIT_STATUS_NOT_OK;
}
System.exit(exitStatus);
} // main
}
Код метода validate представлен ниже.
...
/** {@inheritDoc} */
public void validate(List<File> xmlFiles) throws Exception {
int fileCount = xmlFiles.size();
File currentFile;
FileInputStream fileInputStream = null;
Source xmlFileSource;
CombinedType combinedType;
AddressType addressType;
for (int count = 0; count < fileCount; count++) {
currentFile = xmlFiles.get(count);
printer.boldln("Current file: ").println(currentFile.getPath());
try {
fileInputStream = new FileInputStream(currentFile);
xmlSource = new StreamSource(fileInputStream);
combinedType = (CombinedType)unmarshaller.unmarshal(xmlSource);
printer.boldln("Xml file [" + currentFile.getName() + "] validation success!\n");
printer.boldln("Version: ").println(combinedType.getVersion());
addressType = combinedType.getAddress();
if (addressType != null) {
printer.boldln("Address: ").println(addressType.toString());
} else if (combinedType.getClients() != null) {
int i=0;
for (ClientType client : combinedType.getClients()) {
printer.boldln("Client").println("[" + ++i + "]" +
client.toString());
}
}
} catch(Exception e) {
printer.fatalln("Xml file [" + currentFile.getName() + "] validation error: \n" + e.getMessage());
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
printer.boldln("Validating complete.");
}
Ключевое преобразование XML-Java или unmarshalling происходит в строке
combinedType = (CombinedType)unmarshaller.unmarshal(xmlSource);
Как уже упоминалось, если удалось из XML получить java-объект типа CombinedType, то
XML признается корректным.
Unmarshaller-у должен быть заранее известен конечный объект преобразования. Для
этого с помощью JAXB создадим файлы AddressType.java, ClientType.java, CombinedType.java
В IDE Eclipse: правый клик по XSD -> Generate -> JAXB Classes…
В итоге:
package com.xmlprocessor.types;
import java.math.BigInteger;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
/**
* <p>Java class for AddressType complex type.</p>
*/
@SuppressWarnings("restriction")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "AddressType", propOrder = {
"apartment",
"street",
"house",
"city",
"country",
"index"
})
public class AddressType {
@XmlElement(name = "Apartment", required = true)
protected Integer apartment;
@XmlElement(name = "House", required = true)
protected BigInteger house;
@XmlElement(name = "Street", required = true)
protected String street;
@XmlElement(name = "City", required = true)
protected String city;
@XmlElement(name = "Country", required = true)
protected String country;
@XmlElement(name = "Index")
protected BigInteger index;
public Integer getApartment() {
return apartment;
}
public void setApartment(Integer value) {
this.apartment = value;
}
public String getStreet() {
return street;
}
public void setStreet(String value) {
this.street = value;
}
public BigInteger getHouse() {
return house;
}
public void setHouse(BigInteger value) {
this.house = value;
}
public String getCity() {
return city;
}
public void setCity(String value) {
this.city = value;
}
public String getCountry() {
return country;
}
public void setCountry(String value) {
this.country = value;
}
public BigInteger getIndex() {
return index;
}
public void setIndex(BigInteger value) {
this.index = value;
}
public boolean isSetIndex() {
return (this.index!= null);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\nApartment#: " + apartment);
sb.append("\nHouse#: " + house);
sb.append("\nStreet: " + street);
sb.append("\nCity: " + city);
sb.append("\nCountry: " + country);
if (this.isSetIndex()) {
sb.append("\nIndex: " + index);
}
return sb.toString();
}
}
package com.xmlprocessor.types;
import java.math.BigInteger;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
@SuppressWarnings("restriction")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "ClientType", namespace = "http://www.tempuri.org/complextypes",
propOrder = {
"id",
"name"
})
public class ClientType {
@XmlElement(name = "Id", required = true)
protected BigInteger id;
@XmlElement(name = "Name", required = true)
protected String name;
public BigInteger getId() {
return id;
}
public void setId(BigInteger value) {
this.id = value;
}
public String getName() {
return name;
}
public void setName(String value) {
this.name = value;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\nId: " + id);
sb.append("\nName: " + name);
return sb.toString();
}
}
package com.xmlprocessor.types;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
/**
* <p>Java class for CombinedType complex type.
*/
@SuppressWarnings("restriction")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "CombinedType", propOrder = {
"version",
"clients",
"address"
})
@XmlRootElement(name = "Combined", namespace = "http://www.tempuri.org/types")
public class CombinedType {
@XmlElement(name = "Version", required = true)
protected String version;
@XmlElement(name = "Client")
protected List<ClientType> clients;
@XmlElement(name = "Address")
protected AddressType address;
public String getVersion() {
return version;
}
public void setVersion(String value) {
this.version = value;
}
public List<ClientType> getClients() {
if (clients == null) {
clients = new ArrayList<ClientType>();
}
return this.clients;
}
public AddressType getAddress() {
return address;
}
public void setAddress(AddressType value) {
this.address = value;
}
}
4. Демонстрация работы программы
В проект включен файл Readme с описанием доступных опций и аргументов командной
строки:
XmlProcessor v1.0
Usage: java -jar XmlProcessorDrv [-d <Dir>] [-h] [-v]
-h,--help Display this help
-v Version of the program
-d <Dir> Folder with XML files to be validated
Example: java -jar xmlProcessor.jar --help -v -d "C:\\XmlSample"
Для запуска проекта из cmd скомпилируем jar, вызвав Maven build package на pom.xml, и в
cmd введем вышеописанную команду
java -jar xmlProcessor.jar -h -v -d «C:\XmlSample»
Директория C:\XmlSample должна содержать один или несколько файлов вида Address.xml и
Client.xml. Файлы с расширением не xml будут проигнорированы.
Пример корректной работы программы:
Программа выдаст ошибку, если длина поля превышает установленный в SimpleTypes.xsd предел, имеется опечатка в имени узла, итд. Для примера допустим, что первый элемент адреса написан как AApartment:
<?xml version="1.0" encoding="UTF-8"?>
<ns:Combined xmlns:ns="http://www.tempuri.org/types">
<Version>V001.000.00</Version>
<Address>
<AApartment>50</Apartment>
<House>7</House>
<Street>Sadovaya</Street>
<City>Saint Petersburg</City>
<Country>Russia</Country>
<Index>123456</Index>
</Address>
</ns:Combined>
В этом случае программа выдаст ошибку:
Спасибо за внимание.