View Javadoc
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  
23  import java.io.IOException;
24  import java.io.StringReader;
25  import java.util.HashMap;
26  import java.util.Iterator;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.StringTokenizer;
31  
32  import javax.xml.parsers.ParserConfigurationException;
33  import javax.xml.parsers.SAXParser;
34  import javax.xml.parsers.SAXParserFactory;
35  
36  import org.dbunit.dataset.Column;
37  import org.dbunit.dataset.DataSetException;
38  import org.dbunit.dataset.DefaultTableMetaData;
39  import org.dbunit.dataset.datatype.DataType;
40  import org.dbunit.dataset.stream.DefaultConsumer;
41  import org.dbunit.dataset.stream.IDataSetConsumer;
42  import org.dbunit.dataset.stream.IDataSetProducer;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  import org.xml.sax.EntityResolver;
46  import org.xml.sax.InputSource;
47  import org.xml.sax.SAXException;
48  import org.xml.sax.SAXNotRecognizedException;
49  import org.xml.sax.SAXNotSupportedException;
50  import org.xml.sax.XMLReader;
51  import org.xml.sax.ext.DeclHandler;
52  import org.xml.sax.ext.LexicalHandler;
53  
54  /**
55   * Produces a DataSet from a flat DTD.
56   *
57   * Only external DTDs are supported and for the root element only the following
58   * declarations are supported.
59   * <ul>
60   *   <li>ANY: like &lt;!Element dataset ANY&gt;</li>
61   *   <li>sequences: like &lt;!Element dataset (first*,second,third?)gt;</li>
62   *   <li>choices: like &lt;!Element dataset (first|second+|third)&gt;</li>
63   * </ul>
64   * Combinations of sequences and choices are not support nor are #PCDATA or
65   * EMPTY declarations.
66   *
67   * @author Manuel Laflamme
68   * @author Last changed by: $Author$
69   * @version $Revision$ $Date$
70   * @since Apr 27, 2003
71   */
72  public class FlatDtdProducer implements IDataSetProducer, EntityResolver, DeclHandler, LexicalHandler
73  {
74      /**
75       * Constant for the value {@value}
76       */
77      public static final String REQUIRED = "#REQUIRED";
78  
79      /**
80       * Constant for the value {@value}
81       */
82      public static final String IMPLIED = "#IMPLIED";
83  
84      /**
85       * Constant for the value {@value}
86       */
87      public static final String ANY = "ANY";
88  
89      /**
90       * Logger for this class
91       */
92      private static final Logger logger = LoggerFactory.getLogger(FlatDtdProducer.class);
93  
94      private static final IDataSetConsumer EMPTY_CONSUMER = new DefaultConsumer();
95  
96      private static final String XML_CONTENT =
97              "<?xml version=\"1.0\"?>" +
98                      "<!DOCTYPE dataset SYSTEM \"urn:/dummy.dtd\">" +
99                      "<dataset/>";
100     private static final String DECL_HANDLER_PROPERTY_NAME =
101             "http://xml.org/sax/properties/declaration-handler";
102     private static final String LEXICAL_HANDLER_PROPERTY_NAME =
103             "http://xml.org/sax/properties/lexical-handler";
104 
105     private InputSource _inputSource;
106     private IDataSetConsumer _consumer = EMPTY_CONSUMER;
107 
108     private String _rootName;
109     private String _rootModel;
110     private final Map _columnListMap = new HashMap();
111 
112     public FlatDtdProducer()
113     {
114     }
115 
116     public FlatDtdProducer(final InputSource inputSource)
117     {
118         _inputSource = inputSource;
119     }
120 
121     public static void setDeclHandler(final XMLReader xmlReader, final DeclHandler handler)
122             throws SAXNotRecognizedException, SAXNotSupportedException
123     {
124         logger.debug("setDeclHandler(xmlReader={}, handler={}) - start", xmlReader, handler);
125         xmlReader.setProperty(DECL_HANDLER_PROPERTY_NAME, handler);
126     }
127 
128     public static void setLexicalHandler(final XMLReader xmlReader, final LexicalHandler handler)
129             throws SAXNotRecognizedException, SAXNotSupportedException
130     {
131         logger.debug("setLexicalHandler(xmlReader={}, handler={}) - start", xmlReader, handler);
132         xmlReader.setProperty(LEXICAL_HANDLER_PROPERTY_NAME, handler);
133     }
134 
135     private List createColumnList()
136     {
137         return new LinkedList();
138     }
139 
140     ////////////////////////////////////////////////////////////////////////////
141     // IDataSetProducer interface
142 
143     @Override
144     public void setConsumer(final IDataSetConsumer consumer) throws DataSetException
145     {
146         _consumer = consumer;
147     }
148 
149     @Override
150     public void produce() throws DataSetException
151     {
152         logger.debug("produce() - start");
153 
154         try
155         {
156 
157             final SAXParser saxParser = SAXParserFactory.newInstance("com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", null).newSAXParser();
158             final XMLReader xmlReader = saxParser.getXMLReader();
159 
160             setDeclHandler(xmlReader, this);
161             setLexicalHandler(xmlReader, this);
162             xmlReader.setEntityResolver(this);
163             xmlReader.parse(new InputSource(new StringReader(XML_CONTENT)));
164         }
165         catch (final ParserConfigurationException e)
166         {
167             throw new DataSetException(e);
168         }
169         catch (final SAXException e)
170         {
171             final Exception exception = e.getException() == null ? e : e.getException();
172             if(exception instanceof DataSetException)
173             {
174                 throw (DataSetException)exception;
175             }
176             else
177             {
178                 throw new DataSetException(exception);
179             }
180         }
181         catch (final IOException e)
182         {
183             throw new DataSetException(e);
184         }
185     }
186 
187     ////////////////////////////////////////////////////////////////////////////
188     // EntityResolver interface
189 
190     @Override
191     public InputSource resolveEntity(final String publicId, final String systemId)
192             throws SAXException
193     {
194         return _inputSource;
195     }
196 
197     ////////////////////////////////////////////////////////////////////////////
198     // DeclHandler interface
199 
200     @Override
201     public void elementDecl(final String name, final String model) throws SAXException
202     {
203         logger.debug("elementDecl(name={}, model={}) - start", name, model);
204 
205         // Root element
206         if (name.equals(_rootName))
207         {
208             // The root model defines the table sequence. Keep it for later used!
209             _rootModel = model;
210         }
211         else if (!_columnListMap.containsKey(name))
212         {
213             _columnListMap.put(name, createColumnList());
214         }
215     }
216 
217     @Override
218     public void attributeDecl(final String elementName, final String attributeName,
219             final String type, final String mode, final String value) throws SAXException
220     {
221         if (logger.isDebugEnabled())
222         {
223             logger.debug("attributeDecl(elementName={}, attributeName={}, type={}, mode={}, value={}) - start",
224                     new Object[]{ elementName, attributeName, type, mode, value });
225         }
226 
227         // Each element attribute represent a table column
228         final Column.Nullable nullable = (REQUIRED.equals(mode)) ?
229                 Column.NO_NULLS : Column.NULLABLE;
230         final Column column = new Column(attributeName, DataType.UNKNOWN, nullable);
231 
232         if (!_columnListMap.containsKey(elementName))
233         {
234             _columnListMap.put(elementName, createColumnList());
235         }
236         final List columnList = (List)_columnListMap.get(elementName);
237         columnList.add(column);
238     }
239 
240     @Override
241     public void internalEntityDecl(final String name, final String value) throws SAXException
242     {
243         // Not used!
244     }
245 
246     @Override
247     public void externalEntityDecl(final String name, final String publicId,
248             final String systemId) throws SAXException
249     {
250         // Not used!
251     }
252 
253     ////////////////////////////////////////////////////////////////////////////
254     // LexicalHandler interface
255 
256     @Override
257     public void startDTD(final String name, final String publicId, final String systemId)
258             throws SAXException
259     {
260         if (logger.isDebugEnabled())
261         {
262             logger.debug("startDTD(name={}, publicId={}, systemId={}) - start",
263                     new Object[]{ name, publicId, systemId });
264         }
265 
266         try
267         {
268             _rootName = name;
269             _consumer.startDataSet();
270         }
271         catch (final DataSetException e)
272         {
273             throw new SAXException(e);
274         }
275     }
276 
277     @Override
278     public void endDTD() throws SAXException
279     {
280         logger.debug("endDTD() - start");
281 
282         try
283         {
284             if(_rootModel == null)
285             {
286                 logger.info("The rootModel is null. Cannot add tables.");
287             }
288             else
289             {
290                 if (ANY.equalsIgnoreCase(_rootModel))
291                 {
292                     final Iterator i = _columnListMap.keySet().iterator();
293                     while (i.hasNext()) {
294                         final String tableName = (String) i.next();
295                         addTable(tableName);
296                     }
297                 }
298                 else {
299                     // Remove enclosing model parenthesis
300                     final String rootModel = _rootModel.substring(1, _rootModel.length() - 1);
301 
302                     // Parse the root element model to determine the table sequence.
303                     // Support all sequence or choices model but not the mix of both.
304                     final String delim = (rootModel.indexOf(",") != -1) ? "," : "|";
305                     final StringTokenizer tokenizer = new StringTokenizer(rootModel, delim);
306                     while (tokenizer.hasMoreTokens()) {
307                         String tableName = tokenizer.nextToken();
308                         tableName = cleanupTableName(tableName);
309                         addTable(tableName);
310                     }
311                 }
312             }
313 
314             _consumer.endDataSet();
315         }
316         catch (final DataSetException e)
317         {
318             throw new SAXException(e);
319         }
320     }
321 
322     private void addTable(final String tableName) throws DataSetException
323     {
324         final Column[] columns = getColumns(tableName);
325         _consumer.startTable(new DefaultTableMetaData(tableName, columns));
326         _consumer.endTable();
327     }
328 
329     private Column[] getColumns(final String tableName) throws DataSetException
330     {
331         final List columnList = (List)_columnListMap.get(tableName);
332         if(columnList==null){
333             throw new DataSetException("ELEMENT/ATTRIBUTE declaration for '" + tableName + "' is missing. " +
334                     "Every table must have an element describing the table.");
335         }
336         final Column[] columns = (Column[])columnList.toArray(new Column[0]);
337         return columns;
338     }
339 
340     protected String cleanupTableName(final String tableName)
341     {
342         String cleaned = tableName;
343         // Remove beginning parenthesis.
344         while (cleaned.startsWith("(")) {
345             cleaned = cleaned.substring(1);
346         }
347         // Remove ending parenthesis and occurrence operators
348         while (cleaned.endsWith(")")
349                 || cleaned.endsWith("*")
350                 || cleaned.endsWith("?")
351                 || cleaned.endsWith("+")) {
352             cleaned = cleaned.substring(0, cleaned.length() - 1);
353         }
354         return cleaned;
355     }
356 
357     @Override
358     public void startEntity(final String name) throws SAXException
359     {
360         // Not used!
361     }
362 
363     @Override
364     public void endEntity(final String name) throws SAXException
365     {
366         // Not used!
367     }
368 
369     @Override
370     public void startCDATA() throws SAXException
371     {
372         // Not used!
373     }
374 
375     @Override
376     public void endCDATA() throws SAXException
377     {
378         // Not used!
379     }
380 
381     @Override
382     public void comment(final char ch[], final int start, final int length) throws SAXException
383     {
384         // Not used!
385     }
386 }