Unitils-使用實踐

簡介

Unitils這個Java開源類包的目的是讓單元測試變得更加容易和可維護。Unitils構建在DBUnit與EasyMock項目之上并與JUnit和TestNG相結合朗和。支持數(shù)據(jù)庫測試,支持利用mock對象進行測試并提供與Spring和Hibernate相集成。Unitils設計成以一種高度可配置和松散偶合的方式來添加這些服務到單元測試中寿桨。
我們設計的測試實踐大體如下:

  1. JUnit4 進行單元測試
  1. EasyMock 構造對象
  2. DBUnit 進行數(shù)據(jù)庫的管理
  3. Unitils 進行整合

Unitils可以完成如下的功能:

  1. 自動維護和強制關閉單元測試數(shù)據(jù)庫(支持Oracle,Hsqldb强戴,MySql亭螟,DB2),
  2. 簡化單元測試數(shù)據(jù)庫連接的設置骑歹,
  3. 簡化利用DBUnit測試數(shù)據(jù)的插入预烙,
  4. 簡化Hibernate session管理,
  5. 自動測試與數(shù)據(jù)庫相映射的Hibernate映射對象陵刹。
  6. 在利用Mock對象進行測試時能夠:
  7. 簡化EasyMock mock對象創(chuàng)建默伍,
  8. 簡化mock對象注入,利用反射等式匹配EasyMock參數(shù)衰琐。
  9. 在與Spring集成時易于把spring管理的bean注入到單元測試中,支持在單元測試中使用一個用Spring配置的Hibernate SessionFactory炼蹦。

起步

  1. 配置文件簡介
    unitils-default.properties 默認的配置羡宙,在unitils發(fā)行包中。我們沒有必要對這個文件進行修改掐隐,但它可以用來作參考狗热。
    unitils.properties 可包含項目的全部配置钞馁,它是我們需要進行配置的文件,并且能覆寫缺省的配置匿刮。這個文件并不是必須的僧凰,但是一旦你創(chuàng)建了一個,你就需要將該文件放置在項目的classpath下熟丸。
    unitils-local.properties 可以包含用戶特定配置训措,是可選的配置文件,它可以覆寫項目的配置光羞,用來定義開發(fā)者的具體設置绩鸣,舉個例子來說,如果每個開發(fā)者都使用自己的數(shù)據(jù)庫schema纱兑,你就可以創(chuàng)建一個unitils-local.properties為每個用戶配置自己的數(shù)據(jù)庫賬號呀闻、密碼和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
我主要配置了使用模塊,數(shù)據(jù)庫連接铐炫,數(shù)據(jù)集加載策略垒手。另外,為了簡化配置驳遵,把一些自己用的配置文件也放置在這里淫奔,在測試代碼中讀取。

#啟用unitils所需模塊
unitils.modules=database,dbunit,spring

#自定義擴展模塊堤结,詳見實例源碼
#unitils.module.dbunit.className=org.unitils.dbunit.DbUnitModule
#unitils.module.dbunit.className=com.candle.util.MySqlDbUnitModule

#配置數(shù)據(jù)庫連接
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!
#配置為數(shù)據(jù)庫名稱
database.schemaNames=HCM_TEST
#配置數(shù)據(jù)庫方言
database.dialect=oracle

#配置數(shù)據(jù)庫維護策略.請注意下面這段描述
# 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

#配置數(shù)據(jù)庫表創(chuàng)建策略唆迁,是否自動建表以及建表sql腳本存放目錄
#dbMaintainer.autoCreateExecutedScriptsTable=false
#dbMaintainer.script.locations=D:\workspace\unit-demo\src\test\java\com\candle\dao

#數(shù)據(jù)集加載策略
#CleanInsertLoadStrategy:先刪除dateSet中有關表的數(shù)據(jù),然后再插入數(shù)據(jù) 
#InsertLoadStrategy:只插入數(shù)據(jù) 
#RefreshLoadStrategy:有同樣key的數(shù)據(jù)更新竞穷,沒有的插入 
#UpdateLoadStrategy:有同樣key的數(shù)據(jù)更新唐责,沒有的不做任何操作 
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.impl.CleanInsertLoadStrategy

#配置數(shù)據(jù)集工廠
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

#配置數(shù)據(jù)集結構模式XSD生成路徑看政,可以自定義目錄朴恳,但不能為空
dataSetStructureGenerator.xsd.dirName=/
dbMaintainer.generateDataSetStructure.enabled=true

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

整合

這里因為工作的需要,要整合Spring允蚣。其他都不進行介紹于颖。
我構造了一個基類,用于進行Spring容器的初始化嚷兔,為了安全測試環(huán)境與開發(fā)環(huán)境是分開的森渐,特意使用了不同的配置文件做入。測試類繼承自此基類,容器只初始化一次

//@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:    獲取數(shù)據(jù)庫連接 
     * @return  
     * @throws
     */
    public DataSource getDataSource(){
        return dataSource;
    }
}

數(shù)據(jù)

這一部分是我最看重的同衣,在測試的過程中如何構造初始化的數(shù)據(jù)竟块,進行數(shù)據(jù)的驗證等數(shù)據(jù)庫相關操作都是很繁瑣的事情。這里是將數(shù)據(jù)放置在XML文件中耐齐,通過注解方便的進行導入和驗證浪秘。
注意:數(shù)據(jù)使用的是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>

下面看一下具體使用方法

#表示初始化構造數(shù)據(jù)蚪缀,使用ME_SCHEDULE.xml文件中的數(shù)據(jù)構造
#構造策略可以在配置文件中設置秫逝,我使用的是先清除在插入的方式
@DataSet("ME_SCHEDULE.xml")
#預期的數(shù)據(jù)
@ExpectedDataSet("ME_SCHEDULE_except.xml")

工具

為了準備數(shù)據(jù),開發(fā)一些小工具進行數(shù)據(jù)的導入和導出询枚。

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);

        // 配置數(shù)據(jù)庫為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;
    }
}

以上代碼是在一段網(wǎng)絡代碼的基礎上進行整理得到违帆。注意幾個問題:

  1. 我使用Oracle數(shù)據(jù)庫,在DatabaseConnection構造的時候需要指定Schema金蜀,否則會報錯
  2. 修改了部分輸出的代碼刷后,主要解決兩個問題:輸出的內(nèi)容里面漢字顯示為#&;格式的轉義序列渊抄;數(shù)據(jù)庫字段為null的情況下不輸出尝胆,導致了導入數(shù)據(jù)的時候出現(xiàn)問題。所以修改了部分代碼护桦,增加了下面的幾個類

Utf8FlatXmlDataSet繼承自FlatXmlDataSet

public class Utf8FlatXmlDataSet extends FlatXmlDataSet{
    public Utf8FlatXmlDataSet(FlatXmlProducer flatXmlProducer)
            throws DataSetException {
        super(flatXmlProducer);
    }
    /**
     * @Title:          write 
     * @Description:    重寫了輸出函數(shù)含衔,主要是加載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:    增加對空數(shù)據(jù)的處理 
     * @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;
    }

}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末催享,一起剝皮案震驚了整個濱河市杭隙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌因妙,老刑警劉巖痰憎,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異攀涵,居然都是意外死亡铣耘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門以故,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涡拘,“玉大人,你說我怎么就攤上這事据德■Γ” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵棘利,是天一觀的道長橱野。 經(jīng)常有香客問我,道長善玫,這世上最難降的妖魔是什么水援? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮茅郎,結果婚禮上蜗元,老公的妹妹穿的比我還像新娘。我一直安慰自己系冗,他們只是感情好奕扣,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掌敬,像睡著了一般惯豆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上奔害,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天楷兽,我揣著相機與錄音,去河邊找鬼华临。 笑死芯杀,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的雅潭。 我是一名探鬼主播揭厚,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寻馏!你這毒婦竟也來了棋弥?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤诚欠,失蹤者是張志新(化名)和其女友劉穎顽染,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體轰绵,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡粉寞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了左腔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唧垦。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖液样,靈堂內(nèi)的尸體忽然破棺而出振亮,到底是詐尸還是另有隱情巧还,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布坊秸,位于F島的核電站麸祷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏褒搔。R本人自食惡果不足惜阶牍,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望星瘾。 院中可真熱鬧走孽,春花似錦、人聲如沸琳状。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽算撮。三九已至生宛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肮柜,已是汗流浹背陷舅。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留审洞,地道東北人莱睁。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像芒澜,于是被迫代替她去往敵國和親仰剿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容