Unitils-使用实践

简介

Unitils这个Java开源类包的目的是让单元测试变得更加容易和可维护。Unitils构建在DBUnit与EasyMock项目之上并与JUnit和TestNG相结合。支持数据库测试,支持利用mock对象进行测试并提供与Spring和Hibernate相集成。Unitils设计成以一种高度可配置和松散偶合的方式来添加这些服务到单元测试中。
我们设计的测试实践大体如下:

  1. JUnit4 进行单元测试
  1. EasyMock 构造对象
  2. DBUnit 进行数据库的管理
  3. Unitils 进行整合

Unitils可以完成如下的功能:

  1. 自动维护和强制关闭单元测试数据库(支持Oracle,Hsqldb,MySql,DB2),
  2. 简化单元测试数据库连接的设置,
  3. 简化利用DBUnit测试数据的插入,
  4. 简化Hibernate session管理,
  5. 自动测试与数据库相映射的Hibernate映射对象。
  6. 在利用Mock对象进行测试时能够:
  7. 简化EasyMock mock对象创建,
  8. 简化mock对象注入,利用反射等式匹配EasyMock参数。
  9. 在与Spring集成时易于把spring管理的bean注入到单元测试中,支持在单元测试中使用一个用Spring配置的Hibernate SessionFactory。

起步

  1. 配置文件简介
    unitils-default.properties 默认的配置,在unitils发行包中。我们没有必要对这个文件进行修改,但它可以用来作参考。
    unitils.properties 可包含项目的全部配置,它是我们需要进行配置的文件,并且能覆写缺省的配置。这个文件并不是必须的,但是一旦你创建了一个,你就需要将该文件放置在项目的classpath下。
    unitils-local.properties 可以包含用户特定配置,是可选的配置文件,它可以覆写项目的配置,用来定义开发者的具体设置,举个例子来说,如果每个开发者都使用自己的数据库schema,你就可以创建一个unitils-local.properties为每个用户配置自己的数据库账号、密码和schema。每个unitils-local.properties文件应该放置在对应的用户文件夹(System.getProperty("user.home"))

  2. 配置Maven(Unitils集成dbunit、Spring所必须jar包)

<!-- Unitils -dbunit、Spring -->
        <dependency>
            <groupId>org.unitils</groupId>
            <artifactId>unitils-dbunit</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.unitils</groupId>
            <artifactId>unitils-io</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.unitils</groupId>
            <artifactId>unitils-database</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.unitils</groupId>
            <artifactId>unitils-spring</artifactId>
            <version>3.4.2</version>
        </dependency>

配置

默认的配置文件是unitils.properties
我主要配置了使用模块,数据库连接,数据集加载策略。另外,为了简化配置,把一些自己用的配置文件也放置在这里,在测试代码中读取。

#启用unitils所需模块
unitils.modules=database,dbunit,spring

#自定义扩展模块,详见实例源码
#unitils.module.dbunit.className=org.unitils.dbunit.DbUnitModule
#unitils.module.dbunit.className=com.candle.util.MySqlDbUnitModule

#配置数据库连接
database.driverClassName=oracle.jdbc.driver.OracleDriver
database.url=jdbc:oracle:thin:@192.168.24.15:1521:orcl
database.userName=HCM_TEST
database.password=hcm_test*1#2!
#配置为数据库名称
database.schemaNames=HCM_TEST
#配置数据库方言
database.dialect=oracle

#配置数据库维护策略.请注意下面这段描述
# If set to true, the DBMaintainer will be used to update the unit test database schema. This is done once for each
# test run, when creating the DataSource that provides access to the unit test database.
updateDataBaseSchema.enabled=false

#配置数据库表创建策略,是否自动建表以及建表sql脚本存放目录
#dbMaintainer.autoCreateExecutedScriptsTable=false
#dbMaintainer.script.locations=D:\workspace\unit-demo\src\test\java\com\candle\dao

#数据集加载策略
#CleanInsertLoadStrategy:先删除dateSet中有关表的数据,然后再插入数据 
#InsertLoadStrategy:只插入数据 
#RefreshLoadStrategy:有同样key的数据更新,没有的插入 
#UpdateLoadStrategy:有同样key的数据更新,没有的不做任何操作 
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.impl.CleanInsertLoadStrategy

#配置数据集工厂
DbUnitModule.DataSet.factory.default=org.unitils.dbunit.datasetfactory.impl.MultiSchemaXmlDataSetFactory
DbUnitModule.ExpectedDataSet.factory.default=org.unitils.dbunit.datasetfactory.impl.MultiSchemaXmlDataSetFactory

#配置事务策略 commit、rollback 和disabled;或者在代码的方法上标记@Transactional(value=TransactionMode.ROLLBACK)
#commit 是单元测试方法过后提交事务
#rollback 是回滚事务
#disabled 是没有事务,默认情况下,事务管理是disabled
DatabaseModule.Transactional.value.default=disabled

#配置数据集结构模式XSD生成路径,可以自定义目录,但不能为空
dataSetStructureGenerator.xsd.dirName=/
dbMaintainer.generateDataSetStructure.enabled=true

#工作流配置(其他地方使用)
diagram.activityFontName=\u5B8B\u4F53
diagram.labelFontName=\u5B8B\u4F53

整合

这里因为工作的需要,要整合Spring。其他都不进行介绍。
我构造了一个基类,用于进行Spring容器的初始化,为了安全测试环境与开发环境是分开的,特意使用了不同的配置文件。测试类继承自此基类,容器只初始化一次

//@RunWith(UnitilsJUnit4TestClassRunner.class)
@SpringApplicationContext({
    "classpath:test-hcm-context.xml", 
    "classpath:test-hcm-jpa.xml", 
    "classpath:test-hcm-servlet.xml",
    "classpath:test-hcm-workflow.xml"}) 
public class BaseUtilsTestCase extends UnitilsJUnit4{

    @SpringApplicationContext
    private ApplicationContext applicationContext;

    @TestDataSource
    private DataSource dataSource;

    /**
     * @Title:          getContext 
     * @Description:    所有子类可以使用此方法获取Spring容器 
     * @return  
     * @throws
     */
    public ApplicationContext getContext(){
        return applicationContext;
    }

    /**
     * @Title:          getDataSource 
     * @Description:    获取数据库连接 
     * @return  
     * @throws
     */
    public DataSource getDataSource(){
        return dataSource;
    }
}

数据

这一部分是我最看重的,在测试的过程中如何构造初始化的数据,进行数据的验证等数据库相关操作都是很繁琐的事情。这里是将数据放置在XML文件中,通过注解方便的进行导入和验证。
注意:数据使用的是DBUnit的FlatXml格式,样子如下

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <sys_user id="7000" LOGIN_ID="999888" REAL_NAME="测试1" PASSWORD="1" USER_TYPE="0" USER_STATUS="0" ORG_ID="22" SEX="0" POSITION="" PHONE="" MAIL="" ADDRESS="" PUSH_ID=""/>
    <sys_user id="7001" LOGIN_ID="999777" REAL_NAME="测试2" PASSWORD="1" USER_TYPE="0" USER_STATUS="0" ORG_ID="22" SEX="1" POSITION="" PHONE="" MAIL="" ADDRESS="" PUSH_ID=""/>
</dataset>

下面看一下具体使用方法

#表示初始化构造数据,使用ME_SCHEDULE.xml文件中的数据构造
#构造策略可以在配置文件中设置,我使用的是先清除在插入的方式
@DataSet("ME_SCHEDULE.xml")
#预期的数据
@ExpectedDataSet("ME_SCHEDULE_except.xml")

工具

为了准备数据,开发一些小工具进行数据的导入和导出。

public class DbunitHelper extends BaseUtilsTestCase{
    @TestDataSource
    private DataSource dataSource;

    private String schemaName;
    
    private static IDatabaseConnection conn;

    public static final String ROOT_URL = System.getProperty("user.dir")
            + "/src/test/resources/";

    @Before
    public void setup() throws Exception {
        InputStream in3 = DbunitHelper.class.getClassLoader()
                .getResourceAsStream("unitils.properties");
        
        Properties p = new Properties();
        p.load(in3);
        schemaName = (String)p.get("database.schemaNames");
        
        conn = new DatabaseConnection(DataSourceUtils.getConnection(dataSource),schemaName);

        // 配置数据库为Oracle
        DatabaseConfig dbConfig = conn.getConfig();
        dbConfig.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
                new OracleDataTypeFactory());
    }

    @After
    public void teardown() throws Exception {
        if (conn != null) {
            conn.close();
        }
    }

    /**
     * 
     * @Title: getXmlDataSet
     * @param name
     * @return
     * @throws DataSetException
     * @throws IOException
     */
    protected IDataSet getXmlDataSet(String name) throws DataSetException,
            IOException {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        builder.setColumnSensing(true);
        return builder.build(new FileInputStream(new File(ROOT_URL + name)));
    }

    /**
     * Get DB DataSet
     * 
     * @Title: getDBDataSet
     * @return
     * @throws SQLException
     */
    protected IDataSet getDBDataSet() throws SQLException {
        return conn.createDataSet();
    }

    /**
     * Get Query DataSet
     * 
     * @Title: getQueryDataSet
     * @return
     * @throws SQLException
     */
    protected QueryDataSet getQueryDataSet() throws SQLException {
        return new QueryDataSet(conn);
    }

    /**
     * Get Excel DataSet
     * 
     * @Title: getXlsDataSet
     * @param name
     * @return
     * @throws SQLException
     * @throws DataSetException
     * @throws IOException
     */
    protected XlsDataSet getXlsDataSet(String name) throws SQLException,
            DataSetException, IOException {
        InputStream is = new FileInputStream(new File(ROOT_URL + name));

        return new XlsDataSet(is);
    }

    /**
     * backup the whole DB
     * 
     * @Title: backupAll
     * @throws Exception
     */
    protected void backupAll(String file) throws Exception {
        IDataSet ds = conn.createDataSet();
        
        Writer writer = new FileWriter(file);  
        
        Utf8FlatXmlDataSet.write(ds, writer, "UTF-8");
        writer.flush();  
        writer.close();
        
    }

    protected void backupTable(String[] tableNames,String file) throws Exception {
        QueryDataSet dataSet = new QueryDataSet(conn);  
        if (null != tableNames && 0 < tableNames.length) {  
            int tableNamesLength = tableNames.length;  
            for (int i = 0; i < tableNamesLength; i++) {  
                dataSet.addTable(tableNames[i]);  
            }  
        }  
        Writer writer = new FileWriter(file);  
        
        Utf8FlatXmlDataSet.write(dataSet, writer, "UTF-8");
        writer.flush();  
        writer.close();  
    }
    
    /**
     * back specified DB table
     * 
     * @Title: backupCustom
     * @param tableName
     * @throws Exception
     */
    protected void backupCustom(String... tableName) throws Exception {
        // back up specific files
        QueryDataSet qds = new QueryDataSet(conn);
        for (String str : tableName) {

            qds.addTable(str);
        }
        Writer writer = new FileWriter("temp.xml");  
        
        Utf8FlatXmlDataSet.write(qds, writer, "UTF-8");
        writer.flush();  
        writer.close(); 

    }

    /**
     * Clear data of table
     * 
     * @param tableName
     * @throws Exception
     */
    protected void clearTable(String tableName) throws Exception {
        DefaultDataSet dataset = new DefaultDataSet();
        dataset.addTable(new DefaultTable(tableName));
        DatabaseOperation.DELETE_ALL.execute(conn, dataset);
    }

    /**
     * verify Table is Empty
     * 
     * @param tableName
     * @throws DataSetException
     * @throws SQLException
     */
    protected void verifyTableEmpty(String tableName) throws DataSetException,
            SQLException {
        Assert.assertEquals(0, conn.createDataSet().getTable(tableName)
                .getRowCount());
    }

    /**
     * verify Table is not Empty
     * 
     * @Title: verifyTableNotEmpty
     * @param tableName
     * @throws DataSetException
     * @throws SQLException
     */
    protected void verifyTableNotEmpty(String tableName)
            throws DataSetException, SQLException {
        Assert.assertNotEquals(0, conn.createDataSet().getTable(tableName)
                .getRowCount());
    }

    /**
     * 
     * @Title: createReplacementDataSet
     * @param dataSet
     * @return
     */
    protected ReplacementDataSet createReplacementDataSet(IDataSet dataSet) {
        ReplacementDataSet replacementDataSet = new ReplacementDataSet(dataSet);

        // Configure the replacement dataset to replace '[NULL]' strings with
        // null.
        replacementDataSet.addReplacementObject("[null]", null);

        return replacementDataSet;
    }
}

以上代码是在一段网络代码的基础上进行整理得到。注意几个问题:

  1. 我使用Oracle数据库,在DatabaseConnection构造的时候需要指定Schema,否则会报错
  2. 修改了部分输出的代码,主要解决两个问题:输出的内容里面汉字显示为#&;格式的转义序列;数据库字段为null的情况下不输出,导致了导入数据的时候出现问题。所以修改了部分代码,增加了下面的几个类

Utf8FlatXmlDataSet继承自FlatXmlDataSet

public class Utf8FlatXmlDataSet extends FlatXmlDataSet{
    public Utf8FlatXmlDataSet(FlatXmlProducer flatXmlProducer)
            throws DataSetException {
        super(flatXmlProducer);
    }
    /**
     * @Title:          write 
     * @Description:    重写了输出函数,主要是加载writer有改变 
     * @param dataSet
     * @param writer
     * @param encoding
     * @throws IOException
     * @throws DataSetException  
     * @throws
     */
    public static void write(IDataSet dataSet, Writer writer, String encoding)
            throws IOException, DataSetException
    {
        Utf8FlatXmlWriter datasetWriter = new Utf8FlatXmlWriter(writer, encoding);
        datasetWriter.setIncludeEmptyTable(true);
        datasetWriter.write(dataSet);
    }

}

Utf8FlatXmlWriter基本上和FlatXmlWriter一致,主要的不同点在于xmlWriter使用的是自己定义的Utf8FlatXmlWriter

public class Utf8FlatXmlWriter implements IDataSetConsumer{

    /**
     * Logger for this class
     */
    private static final Logger logger = LoggerFactory.getLogger(FlatXmlWriter.class);

    private static final String DATASET = "dataset";

    private Utf8XmlWriter _xmlWriter;
    private ITableMetaData _activeMetaData;
    private int _activeRowCount;
    private boolean _includeEmptyTable = false;
    private String _systemId = null;

    public Utf8FlatXmlWriter(OutputStream out) throws IOException
    {
        this(out, null);
    }

    /**
     * @param outputStream The stream to which the XML will be written.
     * @param encoding The encoding to be used for the {@link XmlWriter}.
     * Can be null. See {@link XmlWriter#XmlWriter(OutputStream, String)}.
     * @throws UnsupportedEncodingException
     */
    public Utf8FlatXmlWriter(OutputStream outputStream, String encoding) 
    throws UnsupportedEncodingException
    {
        _xmlWriter = new Utf8XmlWriter(outputStream, encoding);
        _xmlWriter.enablePrettyPrint(true);
    }

    public Utf8FlatXmlWriter(Writer writer)
    {
        _xmlWriter = new Utf8XmlWriter(writer);
        _xmlWriter.enablePrettyPrint(true);
    }

    public Utf8FlatXmlWriter(Writer writer, String encoding)
    {
        _xmlWriter = new Utf8XmlWriter(writer, encoding);
        _xmlWriter.enablePrettyPrint(true);
    }

    public void setIncludeEmptyTable(boolean includeEmptyTable)
    {
        _includeEmptyTable = includeEmptyTable;
    }

    public void setDocType(String systemId)
    {
        _systemId = systemId;
    }

    /**
     * Enable or disable pretty print of the XML.
     * @param enabled <code>true</code> to enable pretty print (which is the default). 
     * <code>false</code> otherwise.
     * @since 2.4
     */
    public void setPrettyPrint(boolean enabled)
    {
        _xmlWriter.enablePrettyPrint(enabled);
    }
    
    /**
     * Writes the given {@link IDataSet} using this writer.
     * @param dataSet The {@link IDataSet} to be written
     * @throws DataSetException
     */
    public void write(IDataSet dataSet) throws DataSetException
    {
        logger.debug("write(dataSet={}) - start", dataSet);

        DataSetProducerAdapter provider = new DataSetProducerAdapter(dataSet);
        provider.setConsumer(this);
        provider.produce();
    }

    ////////////////////////////////////////////////////////////////////////////
    // IDataSetConsumer interface

    public void startDataSet() throws DataSetException
    {
        logger.debug("startDataSet() - start");

        try
        {
            _xmlWriter.writeDeclaration();
            _xmlWriter.writeDoctype(_systemId, null);
            _xmlWriter.writeElement(DATASET);
        }
        catch (IOException e)
        {
            throw new DataSetException(e);
        }
    }

    public void endDataSet() throws DataSetException
    {
        logger.debug("endDataSet() - start");

        try
        {
            _xmlWriter.endElement();
            _xmlWriter.close();
        }
        catch (IOException e)
        {
            throw new DataSetException(e);
        }
    }

    public void startTable(ITableMetaData metaData) throws DataSetException
    {
        logger.debug("startTable(metaData={}) - start", metaData);

        _activeMetaData = metaData;
        _activeRowCount = 0;
    }

    public void endTable() throws DataSetException
    {
        logger.debug("endTable() - start");

        if (_includeEmptyTable && _activeRowCount == 0)
        {
            try
            {
                String tableName = _activeMetaData.getTableName();
                _xmlWriter.writeEmptyElement(tableName);
            }
            catch (IOException e)
            {
                throw new DataSetException(e);
            }
        }

        _activeMetaData = null;
    }

    public void row(Object[] values) throws DataSetException
    {
        logger.debug("row(values={}) - start", values);

        try
        {
            String tableName = _activeMetaData.getTableName();
            _xmlWriter.writeElement(tableName);

            Column[] columns = _activeMetaData.getColumns();
            for (int i = 0; i < columns.length; i++)
            {
                String columnName = columns[i].getColumnName();
                Object value = values[i];

                // Skip null value
                if (value == null)
                {
                    _xmlWriter.writeNullAttribute(columnName);
                    continue;
                }

                try
                {
                    String stringValue = DataType.asString(value);
                    _xmlWriter.writeAttribute(columnName, stringValue, true);
                }
                catch (TypeCastException e)
                {
                    throw new DataSetException("table=" +
                            _activeMetaData.getTableName() + ", row=" + i +
                            ", column=" + columnName +
                            ", value=" + value, e);
                }
            }

            _activeRowCount++;
            _xmlWriter.endElement();
        }
        catch (IOException e)
        {
            throw new DataSetException(e);
        }
    }
}

Utf8XmlWriter自己书写的xmlWriter,主体代码与XmlWriter一致。增加了writeNullAttribute方法,修改了escapeXml方法(直接输出中文)

public class Utf8XmlWriter{

    /**
     * CDATA start tag: {@value}
     */
    public static final String CDATA_START = "<![CDATA[";
    /**
     * CDATA end tag: {@value}
     */
    public static final String CDATA_END = "]]>";

    /**
     * Default encoding value which is {@value}
     */
    public static final String DEFAULT_ENCODING = "UTF-8";
    
    /**
     * Logger for this class
     */
    private static final Logger logger = LoggerFactory.getLogger(Utf8XmlWriter.class);

    private Writer out;      // underlying writer
    private String encoding; // the encoding to be written into the XML header/metatag
    private Stack<String> stack = new Stack<String>();        // of xml element names
    private StringBuffer attrs; // current attribute string
    private boolean empty;      // is the current node empty
    private boolean closed = true;     // is the current node closed...

    private boolean pretty = true;    // is pretty printing enabled?
    /**
     * was text the last thing output?
     */
    private boolean wroteText = false;
    /**
     * output this to indent one level when pretty printing
     */
    private String indent = "  ";
    /**
     * output this to end a line when pretty printing
     */
    private String newline = "\n";

    
    /**
     * Create an XmlWriter on top of an existing java.io.Writer.
     */
    public Utf8XmlWriter(Writer writer)
    {
        this(writer, null);
    }

    /**
     * Create an XmlWriter on top of an existing java.io.Writer.
     */
    public Utf8XmlWriter(Writer writer, String encoding)
    {
        setWriter(writer, encoding);
    }

    /**
     * Create an XmlWriter on top of an existing {@link java.io.OutputStream}.
     * @param outputStream
     * @param encoding The encoding to be used for writing to the given output
     * stream. Can be <code>null</code>. If it is <code>null</code> the 
     * {@link #DEFAULT_ENCODING} is used.
     * @throws UnsupportedEncodingException 
     * @since 2.4
     */
    public Utf8XmlWriter(OutputStream outputStream, String encoding) 
    throws UnsupportedEncodingException
    {
        if(encoding==null)
        {
            encoding = DEFAULT_ENCODING;            
        }
        OutputStreamWriter writer = new OutputStreamWriter(outputStream, encoding);
        setWriter(writer, encoding);
    }


    /**
     * Turn pretty printing on or off.
     * Pretty printing is enabled by default, but it can be turned off
     * to generate more compact XML.
     *
     * @param enable true to enable, false to disable pretty printing.
     */
    public void enablePrettyPrint(boolean enable)
    {
        if(logger.isDebugEnabled())
            logger.debug("enablePrettyPrint(enable={}) - start", String.valueOf(enable));

        this.pretty = enable;
    }

    /**
     * Specify the string to prepend to a line for each level of indent.
     * It is 2 spaces ("  ") by default. Some may prefer a single tab ("\t")
     * or a different number of spaces. Specifying an empty string will turn
     * off indentation when pretty printing.
     *
     * @param indent representing one level of indentation while pretty printing.
     */
    public void setIndent(String indent)
    {
        logger.debug("setIndent(indent={}) - start", indent);

        this.indent = indent;
    }

    /**
     * Specify the string used to terminate each line when pretty printing.
     * It is a single newline ("\n") by default. Users who need to read
     * generated XML documents in Windows editors like Notepad may wish to
     * set this to a carriage return/newline sequence ("\r\n"). Specifying
     * an empty string will turn off generation of line breaks when pretty
     * printing.
     *
     * @param newline representing the newline sequence when pretty printing.
     */
    public void setNewline(String newline)
    {
        logger.debug("setNewline(newline={}) - start", newline);

        this.newline = newline;
    }

    /**
     * A helper method. It writes out an element which contains only text.
     *
     * @param name String name of tag
     * @param text String of text to go inside the tag
     */
    public Utf8XmlWriter writeElementWithText(String name, String text) throws IOException
    {
        logger.debug("writeElementWithText(name={}, text={}) - start", name, text);

        writeElement(name);
        writeText(text);
        return endElement();
    }

    /**
     * A helper method. It writes out empty entities.
     *
     * @param name String name of tag
     */
    public Utf8XmlWriter writeEmptyElement(String name) throws IOException
    {
        logger.debug("writeEmptyElement(name={}) - start", name);

        writeElement(name);
        return endElement();
    }

    /**
     * Begin to write out an element. Unlike the helper tags, this tag
     * will need to be ended with the endElement method.
     *
     * @param name String name of tag
     */
    public Utf8XmlWriter writeElement(String name) throws IOException
    {
        logger.debug("writeElement(name={}) - start", name);

        return openElement(name);
    }

    /**
     * Begin to output an element.
     *
     * @param name name of element.
     */
    private Utf8XmlWriter openElement(String name) throws IOException
    {
        logger.debug("openElement(name={}) - start", name);

        boolean wasClosed = this.closed;
        closeOpeningTag();
        this.closed = false;
        if (this.pretty)
        {
            //   ! wasClosed separates adjacent opening tags by a newline.
            // this.wroteText makes sure an element embedded within the text of
            // its parent element begins on a new line, indented to the proper
            // level. This solves only part of the problem of pretty printing
            // entities which contain both text and child entities.
            if (!wasClosed || this.wroteText)
            {
                this.out.write(newline);
            }
            for (int i = 0; i < this.stack.size(); i++)
            {
                this.out.write(indent); // Indent opening tag to proper level
            }
        }
        this.out.write("<");
        this.out.write(name);
        stack.add(name);
        this.empty = true;
        this.wroteText = false;
        return this;
    }

    // close off the opening tag
    private void closeOpeningTag() throws IOException
    {
        logger.debug("closeOpeningTag() - start");

        if (!this.closed)
        {
            writeAttributes();
            this.closed = true;
            this.out.write(">");
        }
    }

    // write out all current attributes
    private void writeAttributes() throws IOException
    {
        logger.debug("writeAttributes() - start");

        if (this.attrs != null)
        {
            this.out.write(this.attrs.toString());
            this.attrs.setLength(0);
            this.empty = false;
        }
    }

    /**
     * Write an attribute out for the current element.
     * Any XML characters in the value are escaped.
     * Currently it does not actually throw the exception, but
     * the API is set that way for future changes.
     *
     * @param attr name of attribute.
     * @param value value of attribute.
     * @see #writeAttribute(String, String, boolean)
     */
    public Utf8XmlWriter writeAttribute(String attr, String value) throws IOException
    {
        logger.debug("writeAttribute(attr={}, value={}) - start", attr, value);
        return this.writeAttribute(attr, value, false);
    }

    /**
     * Write an attribute out for the current element.
     * Any XML characters in the value are escaped.
     * Currently it does not actually throw the exception, but
     * the API is set that way for future changes.
     *
     * @param attr name of attribute.
     * @param value value of attribute.
     * @param literally If the writer should be literally on the given value
     * which means that meta characters will also be preserved by escaping them. 
     * Mainly preserves newlines and tabs.
     */
    public Utf8XmlWriter writeAttribute(String attr, String value, boolean literally) throws IOException
    {
        if(logger.isDebugEnabled())
            logger.debug("writeAttribute(attr={}, value={}, literally={}) - start", 
                    new Object[] {attr, value, String.valueOf(literally)} );

        if(this.wroteText==true) {
            throw new IllegalStateException("The text for the current element has already been written. Cannot add attributes afterwards.");
        }

        if (this.attrs == null)
        {
            this.attrs = new StringBuffer();
        }
        this.attrs.append(" ");
        this.attrs.append(attr);
        this.attrs.append("=\"");
        this.attrs.append(escapeXml(value, literally));
        this.attrs.append("\"");
        return this;
    }

    /**
     * @Title:          writeNullAttribute 
     * @Description:    增加对空数据的处理 
     * @param attr
     * @return  
     * @throws
     */
    public Utf8XmlWriter writeNullAttribute(String attr){
        if (this.attrs == null)
        {
            this.attrs = new StringBuffer();
        }
        this.attrs.append(" ");
        this.attrs.append(attr);
        this.attrs.append("=\"");
        this.attrs.append("[null]");
        this.attrs.append("\"");
        return this;
    }
    
    /**
     * End the current element. This will throw an exception
     * if it is called when there is not a currently open
     * element.
     */
    public Utf8XmlWriter endElement() throws IOException
    {
        logger.debug("endElement() - start");

        if (this.stack.empty())
        {
            throw new IOException("Called endElement too many times. ");
        }
        String name = (String)this.stack.pop();
        if (name != null)
        {
            if (this.empty)
            {
                writeAttributes();
                this.out.write("/>");
            }
            else
            {
                if (this.pretty && !this.wroteText)
                {
                    for (int i = 0; i < this.stack.size(); i++)
                    {
                        this.out.write(indent); // Indent closing tag to proper level
                    }
                }
                this.out.write("</");
                this.out.write(name);
                this.out.write(">");
            }
            if (this.pretty)
                this.out.write(newline); // Add a newline after the closing tag
            this.empty = false;
            this.closed = true;
            this.wroteText = false;
        }
        return this;
    }

    /**
     * Close this writer. It does not close the underlying
     * writer, but does throw an exception if there are
     * as yet unclosed tags.
     */
    public void close() throws IOException
    {
        logger.debug("close() - start");

        this.out.flush();

        if (!this.stack.empty())
        {
            throw new IOException("Tags are not all closed. " +
                    "Possibly, " + this.stack.pop() + " is unclosed. ");
        }
    }

    /**
     * Output body text. Any XML characters are escaped.
     * @param text The text to be written
     * @return This writer
     * @throws IOException
     * @see #writeText(String, boolean)
     */
    public Utf8XmlWriter writeText(String text) throws IOException
    {
        logger.debug("writeText(text={}) - start", text);
        return this.writeText(text, false);
    }

    /**
     * Output body text. Any XML characters are escaped.
     * @param text The text to be written
     * @param literally If the writer should be literally on the given value
     * which means that meta characters will also be preserved by escaping them. 
     * Mainly preserves newlines and tabs.
     * @return This writer
     * @throws IOException
     */
    public Utf8XmlWriter writeText(String text, boolean literally) throws IOException
    {
        if(logger.isDebugEnabled())
            logger.debug("writeText(text={}, literally={}) - start", text, String.valueOf(literally));

        closeOpeningTag();
        this.empty = false;
        this.wroteText = true;

        this.out.write(escapeXml(text, literally));
        return this;
    }

    /**
     * Write out a chunk of CDATA. This helper method surrounds the
     * passed in data with the CDATA tag.
     *
     * @param cdata of CDATA text.
     */
    public Utf8XmlWriter writeCData(String cdata) throws IOException
    {
        logger.debug("writeCData(cdata={}) - start", cdata);

        closeOpeningTag();
        
        boolean hasAlreadyEnclosingCdata = cdata.startsWith(CDATA_START) && cdata.endsWith(CDATA_END);
        
        // There may already be CDATA sections inside the data.
        // But CDATA sections can't be nested - can't have ]]> inside a CDATA section. 
        // (See http://www.w3.org/TR/REC-xml/#NT-CDStart in the W3C specs)
        // The solutions is to replace any occurrence of "]]>" by "]]]]><![CDATA[>",
        // so that the top CDATA section is split into many valid CDATA sections (you
        // can look at the "]]]]>" as if it was an escape sequence for "]]>").
        if(!hasAlreadyEnclosingCdata) {
            cdata = cdata.replaceAll(CDATA_END, "]]]]><![CDATA[>");
        }
        
        this.empty = false;
        this.wroteText = true;
        if(!hasAlreadyEnclosingCdata)
            this.out.write(CDATA_START);
        this.out.write(cdata);
        if(!hasAlreadyEnclosingCdata)
            this.out.write(CDATA_END);
        return this;
    }

    /**
     * Write out a chunk of comment. This helper method surrounds the
     * passed in data with the XML comment tag.
     *
     * @param comment of text to comment.
     */
    public Utf8XmlWriter writeComment(String comment) throws IOException
    {
        logger.debug("writeComment(comment={}) - start", comment);

        writeChunk("<!-- " + comment + " -->");
        return this;
    }

    private void writeChunk(String data) throws IOException
    {
        logger.debug("writeChunk(data={}) - start", data);

        closeOpeningTag();
        this.empty = false;
        if (this.pretty && !this.wroteText)
        {
            for (int i = 0; i < this.stack.size(); i++)
            {
                this.out.write(indent);
            }
        }

        this.out.write(data);

        if (this.pretty)
        {
            this.out.write(newline);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // Added for DbUnit

    /**
     * Escapes some meta characters like \n, \r that should be preserved in the XML
     * so that a reader will not filter out those symbols.  This code is modified
     * from xmlrpc:
     * https://svn.apache.org/repos/asf/webservices/xmlrpc/branches/XMLRPC_1_2_BRANCH/src/java/org/apache/xmlrpc/XmlWriter.java
     *
     * @param str The string to be escaped
     * @param literally If the writer should be literally on the given value
     * which means that meta characters will also be preserved by escaping them. 
     * Mainly preserves newlines and carriage returns.
     * @return The escaped string
     */
    private String escapeXml(String str, boolean literally)
    {
        logger.debug("escapeXml(str={}, literally={}) - start", str, Boolean.toString(literally));

        char [] block = null;
        int last = 0;
        StringBuffer buffer = null;
        int strLength = str.length();
        int index = 0;

        for (index=0; index<strLength; index++)
        {
            String entity = null;
            char currentChar = str.charAt(index);
            switch (currentChar)
            {
                case '\t':
                    entity = "  ";
                    break;
                case '\n':
                    if (literally) { entity = "
"; }
                    break;
                case '\r':
                    if (literally) { entity = "
"; }
                    break;
                case '&':
                    entity = "&";
                    break;
                case '<':
                    entity = "<";
                    break;
                case '>':
                    entity = ">";
                    break;
                case '\"':
                    entity = """;
                    break;
                case '\'':
                    entity = "'";
                    break;
                default:
                    if ((currentChar > 0x7f) || !isValidXmlChar(currentChar))
                    {
                        entity = "" + currentChar;
                    }
                    break;
            }

            // If we found something to substitute, then copy over previous
            // data then do the substitution.
            if (entity != null)
            {
                if (block == null)
                {
                    block = str.toCharArray();
                }
                if (buffer == null)
                {
                    buffer = new StringBuffer();
                }
                buffer.append(block, last, index - last);
                buffer.append(entity);
                last = index + 1;
            }
        }

        // nothing found, just return source
        if (last == 0)
        {
            return str;
        }

        if (last < strLength)
        {
            if (block == null)
            {
                block = str.toCharArray();
            }
            if (buffer == null)
            {
                buffer = new StringBuffer();
            }
            buffer.append(block, last, index - last);
        }

        return buffer.toString();
    }

    /**
     * Section 2.2 of the XML spec describes which Unicode code points
     * are valid in XML:
     *
     * <blockquote><code>#x9 | #xA | #xD | [#x20-#xD7FF] |
     * [#xE000-#xFFFD] | [#x10000-#x10FFFF]</code></blockquote>
     *
     * Code points outside this set must be entity encoded to be
     * represented in XML.
     *
     * @param c The character to inspect.
     * @return Whether the specified character is valid in XML.
     */
    private static final boolean isValidXmlChar(char c)
    {
        switch (c)
        {
            case 0x9:
            case 0xa:  // line feed, '\n'
            case 0xd:  // carriage return, '\r'
                return true;

            default:
                return ( (0x20 <= c && c <= 0xd7ff) ||
                    (0xe000 <= c && c <= 0xfffd) ||
                    (0x10000 <= c && c <= 0x10ffff) );
        }
    }

    private void setEncoding(String encoding)
    {
        logger.debug("setEncoding(encoding={}) - start", encoding);

        if (encoding == null && out instanceof OutputStreamWriter)
            encoding = ((OutputStreamWriter)out).getEncoding();

        if (encoding != null)
        {
            encoding = encoding.toUpperCase();

            // Use official encoding names where we know them,
            // avoiding the Java-only names.  When using common
            // encodings where we can easily tell if characters
            // are out of range, we'll escape out-of-range
            // characters using character refs for safety.

            // I _think_ these are all the main synonyms for these!
            if ("UTF8".equals(encoding))
            {
                encoding = "UTF-8";
            }
            else if ("US-ASCII".equals(encoding)
                    || "ASCII".equals(encoding))
            {
//                dangerMask = (short)0xff80;
                encoding = "US-ASCII";
            }
            else if ("ISO-8859-1".equals(encoding)
                    || "8859_1".equals(encoding)
                    || "ISO8859_1".equals(encoding))
            {
//                dangerMask = (short)0xff00;
                encoding = "ISO-8859-1";
            }
            else if ("UNICODE".equals(encoding)
                    || "UNICODE-BIG".equals(encoding)
                    || "UNICODE-LITTLE".equals(encoding))
            {
                encoding = "UTF-16";

                // TODO: UTF-16BE, UTF-16LE ... no BOM; what
                // release of JDK supports those Unicode names?
            }

//            if (dangerMask != 0)
//                stringBuf = new StringBuffer();
        }

        this.encoding = encoding;
    }


    /**
     * Resets the handler to write a new text document.
     *
     * @param writer XML text is written to this writer.
     * @param encoding if non-null, and an XML declaration is written,
     *  this is the name that will be used for the character encoding.
     *
     * @exception IllegalStateException if the current
     *  document hasn't yet ended (i.e. the output stream {@link #out} is not null)
     */
    final public void setWriter(Writer writer, String encoding)
    {
        logger.debug("setWriter(writer={}, encoding={}) - start", writer, encoding);

        if (this.out != null)
            throw new IllegalStateException(
                    "can't change stream in mid course");
        this.out = writer;
        if (this.out != null)
            setEncoding(encoding);
//        if (!(this.out instanceof BufferedWriter))
//            this.out = new BufferedWriter(this.out);
    }

    public Utf8XmlWriter writeDeclaration() throws IOException
    {
        logger.debug("writeDeclaration() - start");

        if (this.encoding != null)
        {
            this.out.write("<?xml version='1.0'");
            this.out.write(" encoding='" + this.encoding + "'");
            this.out.write("?>");
            this.out.write(this.newline);
        }

        return this;
    }

    public Utf8XmlWriter writeDoctype(String systemId, String publicId) throws IOException
    {
        logger.debug("writeDoctype(systemId={}, publicId={}) - start", systemId, publicId);

        if (systemId != null || publicId != null)
        {
            this.out.write("<!DOCTYPE dataset");

            if (systemId != null)
            {
                this.out.write(" SYSTEM \"");
                this.out.write(systemId);
                this.out.write("\"");
            }

            if (publicId != null)
            {
                this.out.write(" PUBLIC \"");
                this.out.write(publicId);
                this.out.write("\"");
            }

            this.out.write(">");
            this.out.write(this.newline);
        }

        return this;
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容