FlatXmlProducer.java

  1. /*
  2.  *
  3.  * The DbUnit Database Testing Framework
  4.  * Copyright (C)2002-2004, DbUnit.org
  5.  *
  6.  * This library is free software; you can redistribute it and/or
  7.  * modify it under the terms of the GNU Lesser General Public
  8.  * License as published by the Free Software Foundation; either
  9.  * version 2.1 of the License, or (at your option) any later version.
  10.  *
  11.  * This library is distributed in the hope that it will be useful,
  12.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14.  * Lesser General Public License for more details.
  15.  *
  16.  * You should have received a copy of the GNU Lesser General Public
  17.  * License along with this library; if not, write to the Free Software
  18.  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  19.  *
  20.  */
  21. package org.dbunit.dataset.xml;

  22. import java.io.IOException;
  23. import java.io.StringReader;
  24. import java.util.ArrayList;
  25. import java.util.Iterator;
  26. import java.util.List;

  27. import javax.xml.parsers.ParserConfigurationException;
  28. import javax.xml.parsers.SAXParserFactory;

  29. import org.dbunit.dataset.Column;
  30. import org.dbunit.dataset.DataSetException;
  31. import org.dbunit.dataset.DefaultTableMetaData;
  32. import org.dbunit.dataset.IDataSet;
  33. import org.dbunit.dataset.ITableMetaData;
  34. import org.dbunit.dataset.NoSuchColumnException;
  35. import org.dbunit.dataset.OrderedTableNameMap;
  36. import org.dbunit.dataset.datatype.DataType;
  37. import org.dbunit.dataset.stream.BufferedConsumer;
  38. import org.dbunit.dataset.stream.DefaultConsumer;
  39. import org.dbunit.dataset.stream.IDataSetConsumer;
  40. import org.dbunit.dataset.stream.IDataSetProducer;
  41. import org.slf4j.Logger;
  42. import org.slf4j.LoggerFactory;
  43. import org.xml.sax.Attributes;
  44. import org.xml.sax.ContentHandler;
  45. import org.xml.sax.EntityResolver;
  46. import org.xml.sax.InputSource;
  47. import org.xml.sax.SAXException;
  48. import org.xml.sax.SAXParseException;
  49. import org.xml.sax.XMLReader;
  50. import org.xml.sax.helpers.DefaultHandler;

  51. /**
  52.  * @author Manuel Laflamme
  53.  * @author Last changed by: $Author$
  54.  * @version $Revision$ $Date$
  55.  * @since 1.5 (Apr 18, 2003)
  56.  */
  57. public class FlatXmlProducer extends DefaultHandler implements IDataSetProducer, ContentHandler
  58. {

  59.     /**
  60.      * Logger for this class
  61.      */
  62.     private static final Logger logger = LoggerFactory.getLogger(FlatXmlProducer.class);

  63.     private static final IDataSetConsumer EMPTY_CONSUMER = new DefaultConsumer();
  64.     private static final String DATASET = "dataset";

  65.     private final InputSource _inputSource;
  66.     private final EntityResolver _resolver;
  67.     private boolean _validating = false;
  68.     /**
  69.      * The dataset used to retrieve the metadata for the tables via {@link IDataSet#getTableMetaData(String)}.
  70.      * Can be null
  71.      */
  72.     private IDataSet _metaDataSet;
  73.     /**
  74.      * The DTD handler which is used to parse a DTD if available. The result of the parsing is stored in
  75.      * {@link #_metaDataSet}.
  76.      */
  77.     private FlatDtdHandler _dtdHandler;
  78.    
  79.     /**
  80.      * The current line number in the current table
  81.      */
  82.     private int _lineNumber = 0;
  83.     /**
  84.      * The current line number
  85.      */
  86.     private int _lineNumberGlobal = 0;
  87.     /**
  88.      * Whether the column sensing feature should be used to dynamically recognize new columns
  89.      * during the parse process.
  90.      */
  91.     private boolean _columnSensing = false;
  92.     private boolean _caseSensitiveTableNames;

  93.     /**
  94.      * The consumer which is responsible for creating the datasets and tables
  95.      */
  96.     private IDataSetConsumer _consumer = EMPTY_CONSUMER;
  97.     /**
  98.      * The ordered table name map which also holds the currently active {@link ITableMetaData}
  99.      */
  100.     private OrderedTableNameMap _orderedTableNameMap;

  101.    
  102.     public FlatXmlProducer(InputSource xmlSource)
  103.     {
  104.         this(xmlSource, true);
  105.     }

  106.     public FlatXmlProducer(InputSource xmlSource, boolean dtdMetadata)
  107.     {
  108.         this(xmlSource, dtdMetadata, false);
  109.     }

  110.     public FlatXmlProducer(InputSource xmlSource, IDataSet metaDataSet)
  111.     {
  112.         _inputSource = xmlSource;
  113.         _metaDataSet = metaDataSet;
  114.         _resolver = this;
  115.         _caseSensitiveTableNames = metaDataSet.isCaseSensitiveTableNames();
  116.         initialize(false);
  117.     }

  118.     public FlatXmlProducer(InputSource xmlSource, EntityResolver resolver)
  119.     {
  120.         _inputSource = xmlSource;
  121.         _resolver = resolver;
  122.         initialize(true);
  123.     }
  124.    
  125.     /**
  126.      * @param xmlSource The input datasource
  127.      * @param dtdMetadata Whether or not DTD metadata is available to parse via a DTD handler
  128.      * @param columnSensing Whether or not the column sensing feature should be used (see FAQ)
  129.      */
  130.     public FlatXmlProducer(InputSource xmlSource, boolean dtdMetadata, boolean columnSensing)
  131.     {
  132.         this(xmlSource, dtdMetadata, columnSensing, false);
  133.     }
  134.    
  135.     /**
  136.      * @param xmlSource The input datasource
  137.      * @param dtdMetadata Whether or not DTD metadata is available to parse via a DTD handler
  138.      * @param columnSensing Whether or not the column sensing feature should be used (see FAQ)
  139.      * @param caseSensitiveTableNames Whether or not this dataset should use case sensitive table names
  140.      * @since 2.4.2
  141.      */
  142.     public FlatXmlProducer(InputSource xmlSource, boolean dtdMetadata, boolean columnSensing, boolean caseSensitiveTableNames)
  143.     {
  144.         _inputSource = xmlSource;
  145.         _columnSensing = columnSensing;
  146.         _caseSensitiveTableNames = caseSensitiveTableNames;
  147.         _resolver = this;
  148.         initialize(dtdMetadata);
  149.     }


  150.    
  151.     private void initialize(boolean dtdMetadata)
  152.     {
  153.         if (dtdMetadata)
  154.         {
  155.             this._dtdHandler = new FlatDtdHandler(this);
  156.         }
  157.     }
  158.    
  159.     /**
  160.      * @return Whether or not this producer works case sensitively
  161.      * @since 2.4.7
  162.      */
  163.     public boolean isCaseSensitiveTableNames() {
  164.         return _caseSensitiveTableNames;
  165.     }

  166.     private ITableMetaData createTableMetaData(String tableName, Attributes attributes) throws DataSetException
  167.     {
  168.         if (logger.isDebugEnabled())
  169.             logger.debug("createTableMetaData(tableName={}, attributes={}) - start", tableName, attributes);

  170.         // First try to find it in the DTD's dataset
  171.         if (_metaDataSet != null)
  172.         {
  173.             return _metaDataSet.getTableMetaData(tableName);
  174.         }

  175.         // Create metadata from attributes
  176.         Column[] columns = new Column[attributes.getLength()];
  177.         for (int i = 0; i < attributes.getLength(); i++)
  178.         {
  179.             columns[i] = new Column(attributes.getQName(i), DataType.UNKNOWN);
  180.         }

  181.         return new DefaultTableMetaData(tableName, columns);
  182.     }
  183.    
  184.    
  185.     /**
  186.      * merges the existing columns with the potentially new ones.
  187.      * @param columnsToMerge List of extra columns found, which need to be merge back into the metadata.
  188.      * @return ITableMetaData The merged metadata object containing the new columns
  189.      * @throws DataSetException
  190.      */
  191.     private ITableMetaData mergeTableMetaData(List columnsToMerge, ITableMetaData originalMetaData) throws DataSetException
  192.     {
  193.         Column[] columns = new Column[originalMetaData.getColumns().length + columnsToMerge.size()];
  194.         System.arraycopy(originalMetaData.getColumns(), 0, columns, 0, originalMetaData.getColumns().length);
  195.        
  196.         for (int i = 0; i < columnsToMerge.size(); i++)
  197.         {
  198.             Column column = (Column)columnsToMerge.get(i);
  199.             columns[columns.length - columnsToMerge.size() + i] = column;
  200.         }
  201.        
  202.         return new DefaultTableMetaData(originalMetaData.getTableName(), columns);
  203.     }
  204.    
  205.     /**
  206.      * @return The currently active table metadata or <code>null</code> if no active
  207.      * metadata exists.
  208.      */
  209.     private ITableMetaData getActiveMetaData()
  210.     {
  211.         if(_orderedTableNameMap != null)
  212.         {
  213.             String lastTableName = _orderedTableNameMap.getLastTableName();
  214.             if(lastTableName != null)
  215.             {
  216.                 return (ITableMetaData) _orderedTableNameMap.get(lastTableName);
  217.             }
  218.             else
  219.             {
  220.                 return null;
  221.             }
  222.         }
  223.         else
  224.         {
  225.             return null;
  226.         }
  227.        
  228.     }
  229.    
  230.     /**
  231.      * @param tableName
  232.      * @return <code>true</code> if the given tableName is a new one
  233.      * which means that it differs from the last active table name.
  234.      */
  235.     private boolean isNewTable(String tableName)
  236.     {
  237.         return !_orderedTableNameMap.isLastTable(tableName);
  238.     }


  239.     /**
  240.      * parses the attributes in the current row, and checks whether a new column
  241.      * is found.
  242.      *
  243.      * <p>Depending on the value of the <code>columnSensing</code> flag, the appropriate
  244.      * action is taken:</p>
  245.      *
  246.      * <ul>
  247.      *   <li>If it is true, the new column is merged back into the metadata;</li>
  248.      *   <li>If not, a warning message is displayed.</li>
  249.      * </ul>
  250.      *
  251.      * @param attributes  Attributed for the current row.
  252.      * @throws DataSetException
  253.      */
  254.     protected void handleMissingColumns(Attributes attributes)
  255.             throws DataSetException
  256.     {
  257.         List columnsToMerge = new ArrayList();
  258.        
  259.         ITableMetaData activeMetaData = getActiveMetaData();
  260.         // Search all columns that do not yet exist and collect them
  261.         int attributeLength = attributes.getLength();
  262.         for (int i = 0 ; i < attributeLength; i++)
  263.         {
  264.             try {
  265.                 activeMetaData.getColumnIndex(attributes.getQName(i));
  266.             }
  267.             catch (NoSuchColumnException e) {
  268.                 columnsToMerge.add(new Column(attributes.getQName(i), DataType.UNKNOWN));
  269.             }
  270.         }
  271.        
  272.         if (!columnsToMerge.isEmpty())
  273.         {
  274.             if (_columnSensing)
  275.             {
  276.                 logger.debug("Column sensing enabled. Will create a new metaData with potentially new columns if needed");
  277.                 activeMetaData = mergeTableMetaData(columnsToMerge, activeMetaData);
  278.                 _orderedTableNameMap.update(activeMetaData.getTableName(), activeMetaData);
  279.                 // We also need to recreate the table, copying the data already collected from the old one to the new one
  280.                 _consumer.startTable(activeMetaData);
  281.             }
  282.             else
  283.             {
  284.                 final StringBuilder extraColumnNames = new StringBuilder();
  285.                 for (Iterator i = columnsToMerge.iterator(); i.hasNext();) {
  286.                     Column col = (Column) i.next();
  287.                     extraColumnNames.append(extraColumnNames.length() > 0 ? "," : "").append(col.getColumnName());
  288.                 }
  289.                 String msg = "Extra columns (" + extraColumnNames.toString() + ") on line " + (_lineNumber + 1)
  290.                         + " for table " + activeMetaData.getTableName() + " (global line number is "
  291.                         + _lineNumberGlobal + "). Those columns will be ignored.";
  292.                 msg += "\n\tPlease add the extra columns to line 1,"
  293.                     + " or use a DTD to make sure the value of those columns are populated"
  294.                     + " or specify 'columnSensing=true' for your FlatXmlProducer.";
  295.                 msg += "\n\tSee FAQ for more details.";
  296.                 logger.warn(msg);
  297.             }
  298.         }
  299.     }
  300.    
  301.     public void setColumnSensing(boolean columnSensing)
  302.     {
  303.         _columnSensing = columnSensing;
  304.     }

  305.     public void setValidating(boolean validating)
  306.     {
  307.         _validating = validating;
  308.     }

  309.     ////////////////////////////////////////////////////////////////////////////
  310.     // IDataSetProducer interface

  311.     public void setConsumer(IDataSetConsumer consumer) throws DataSetException
  312.     {
  313.         logger.debug("setConsumer(consumer) - start");

  314.         if(this._columnSensing) {
  315.             _consumer = new BufferedConsumer(consumer);
  316.         }
  317.         else {
  318.             _consumer = consumer;
  319.         }
  320.     }

  321.     public void produce() throws DataSetException
  322.     {
  323.         logger.debug("produce() - start");

  324.         try
  325.         {
  326.             SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
  327.             saxParserFactory.setValidating(_validating);
  328.             XMLReader xmlReader = saxParserFactory.newSAXParser().getXMLReader();

  329.             if(_dtdHandler != null)
  330.             {
  331.                 FlatDtdHandler.setLexicalHandler(xmlReader, _dtdHandler);
  332.                 FlatDtdHandler.setDeclHandler(xmlReader, _dtdHandler);
  333.             }

  334.             xmlReader.setContentHandler(this);
  335.             xmlReader.setErrorHandler(this);
  336.             xmlReader.setEntityResolver(_resolver);
  337.             xmlReader.parse(_inputSource);
  338.         }
  339.         catch (ParserConfigurationException e)
  340.         {
  341.             throw new DataSetException(e);
  342.         }
  343.         catch (SAXException e)
  344.         {
  345.             DataSetException exceptionToRethrow = XmlProducer.buildException(e);
  346.             throw exceptionToRethrow;
  347.         }
  348.         catch (IOException e)
  349.         {
  350.             throw new DataSetException(e);
  351.         }
  352.     }

  353.     ////////////////////////////////////////////////////////////////////////////
  354.     // EntityResolver interface

  355.     public InputSource resolveEntity(String publicId, String systemId)
  356.             throws SAXException
  357.     {
  358.         logger.debug("resolveEntity(publicId={}, systemId={}) - start", publicId, systemId);

  359.         // No DTD metadata wanted/available
  360.         if (_dtdHandler == null || !_dtdHandler.isDtdPresent())
  361.         {
  362.             return new InputSource(new StringReader(""));
  363.         }
  364.         return null;
  365.     }

  366.     ////////////////////////////////////////////////////////////////////////////
  367.     // ErrorHandler interface

  368.     public void error(SAXParseException e) throws SAXException
  369.     {
  370.         throw e;

  371.     }

  372.     ////////////////////////////////////////////////////////////////////////
  373.     // ContentHandler interface

  374.     public void startElement(String uri, String localName, String qName,
  375.             Attributes attributes) throws SAXException
  376.     {
  377.         if (logger.isDebugEnabled())
  378.             logger.debug("startElement(uri={}, localName={}, qName={}, attributes={}) - start",
  379.                     new Object[] { uri, localName, qName, attributes });

  380.         try
  381.         {
  382.             ITableMetaData activeMetaData = getActiveMetaData();
  383.             // Start of dataset
  384.             if (activeMetaData == null && qName.equals(DATASET))
  385.             {
  386.                 _consumer.startDataSet();
  387.                 _orderedTableNameMap = new OrderedTableNameMap(_caseSensitiveTableNames);
  388.                 return;
  389.             }

  390.             // New table
  391.             if (isNewTable(qName))
  392.             {
  393.                 // If not first table, notify end of previous table to consumer
  394.                 if (activeMetaData != null)
  395.                 {
  396.                     _consumer.endTable();
  397.                 }

  398.                 // In FlatXML the table might have appeared before already, so check for this
  399.                 if(_orderedTableNameMap.containsTable(qName))
  400.                 {
  401.                     activeMetaData = (ITableMetaData)_orderedTableNameMap.get(qName);
  402.                     _orderedTableNameMap.setLastTable(qName);
  403.                 }
  404.                 else
  405.                 {
  406.                     activeMetaData = createTableMetaData(qName, attributes);
  407.                     _orderedTableNameMap.add(activeMetaData.getTableName(), activeMetaData);
  408.                 }
  409.                
  410.                 // Notify start of new table to consumer
  411.                 _consumer.startTable(activeMetaData);
  412.                 _lineNumber = 0;
  413.             }

  414.             // Row notification
  415.             int attributesLength = attributes.getLength();
  416.             if (attributesLength > 0)
  417.             {
  418.                 // If we do not have a DTD
  419.                 if (_dtdHandler == null || !_dtdHandler.isDtdPresent())
  420.                 {
  421.                     handleMissingColumns(attributes);
  422.                     // Since a new MetaData object was created assign it to the local variable
  423.                     activeMetaData = getActiveMetaData();
  424.                 }

  425.                 _lineNumber++;
  426.                 _lineNumberGlobal++;
  427.                 Column[] columns = activeMetaData.getColumns();
  428.                 Object[] rowValues = new Object[columns.length];
  429.                 for (int i = 0; i < attributesLength; i++)
  430.                 {
  431.                     determineAndSetRowValue(attributes, activeMetaData,
  432.                             rowValues, i);
  433.                 }
  434.                 _consumer.row(rowValues);
  435.             }
  436.         }
  437.         catch (DataSetException e)
  438.         {
  439.             throw new SAXException(e);
  440.         }
  441.     }

  442.     protected void determineAndSetRowValue(Attributes attributes,
  443.             ITableMetaData activeMetaData, Object[] rowValues, int i)
  444.             throws DataSetException, NoSuchColumnException
  445.     {
  446.         String attributeQName = attributes.getQName(i);
  447.         String attributeValue = attributes.getValue(i);
  448.         try
  449.         {
  450.             int colIndex = activeMetaData.getColumnIndex(attributeQName);
  451.             rowValues[colIndex] = attributeValue;
  452.         }
  453.         catch (NoSuchColumnException e)
  454.         {
  455.             // since missing columns have already been handled above, we will only need
  456.             // to care about NoSuchColumnExceptions if something's gone wrong and we're
  457.             // looking for a nonexistant column that should have been sensed
  458.             if (_columnSensing)
  459.             {
  460.                 throw e;
  461.             }
  462.         }
  463.     }

  464.     public void endElement(String uri, String localName, String qName) throws SAXException
  465.     {
  466.         if (logger.isDebugEnabled())
  467.             logger.debug("endElement(uri={}, localName={}, qName={}) - start",
  468.                     new Object[]{ uri, localName, qName });

  469.         // End of dataset
  470.         if (qName.equals(DATASET))
  471.         {
  472.             try
  473.             {
  474.                 // Notify end of active table to consumer
  475.                 if (getActiveMetaData() != null)
  476.                 {
  477.                     _consumer.endTable();
  478.                 }

  479.                 // Notify end of dataset to consumer
  480.                 _consumer.endDataSet();
  481.             }
  482.             catch (DataSetException e)
  483.             {
  484.                 throw new SAXException(e);
  485.             }
  486.         }
  487.     }

  488.     private static class FlatDtdHandler extends FlatDtdProducer
  489.     {
  490.         /**
  491.          * Logger for this class
  492.          */
  493.         private final Logger logger = LoggerFactory.getLogger(FlatDtdHandler.class);

  494.         private boolean _dtdPresent = false;
  495.         private FlatXmlProducer xmlProducer;

  496.         public FlatDtdHandler(FlatXmlProducer xmlProducer)
  497.         {
  498.             this.xmlProducer = xmlProducer;
  499.         }

  500.         public boolean isDtdPresent()
  501.         {
  502.             return _dtdPresent;
  503.         }
  504.        
  505.         ////////////////////////////////////////////////////////////////////////////
  506.         // LexicalHandler interface

  507.         public void startDTD(String name, String publicId, String systemId)
  508.                 throws SAXException
  509.         {
  510.             if (logger.isDebugEnabled())
  511.                 logger.debug("startDTD(name={}, publicId={}, systemId={}) - start",
  512.                         new Object[] { name, publicId, systemId });

  513.             _dtdPresent = true;
  514.             try
  515.             {
  516.                 // Cache the DTD content to use it as metadata
  517.                 FlatDtdDataSet metaDataSet = new FlatDtdDataSet();
  518.                 this.setConsumer(metaDataSet);
  519.                 // Set the metaData on the xmlProducer
  520.                 xmlProducer._metaDataSet = metaDataSet;

  521.                 super.startDTD(name, publicId, systemId);
  522.             }
  523.             catch (DataSetException e)
  524.             {
  525.                 throw new SAXException(e);
  526.             }
  527.         }
  528.     }
  529. }