001/*
002 *  $Id: $
003 *  $HeadURL: svn://svn.webarts.bc.ca/open/trunk/projects/WebARTS/ca/bc/webarts/widgets/ResultSetConverter.java $
004 *  $Revision: 1055 $
005 *  $LastChangedDate: 2016-04-25 20:53:42 -0700 (Mon, 25 Apr 2016) $
006 *  $LastChangedBy: tgutwin $
007 *  Copyright (c) 2014-2016 Tom B. Gutwin P.Eng. North Vancouver BC Canada
008 *
009 *  This program is free software; you can redistribute it and/or
010 *  modify it under the terms of the GNU General Public License
011 *  as published by the Free Software Foundation; either version 3
012 *  of the License, or any later version.
013 *
014 *  This program is distributed in the hope that it will be useful,
015 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
016 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 *  GNU General Public License for more details.
018 *
019 *  You should have received a copy of the GNU General Public License
020 *  along with this program; If not, see <http://www.gnu.org/licenses/>.
021 */
022package ca.bc.webarts.widgets;
023
024import org.json.JSONArray;
025import org.json.JSONObject;
026import org.json.JSONException;
027
028import nu.xom.Attribute;
029import nu.xom.Builder;
030import nu.xom.Document;
031import nu.xom.Element;
032import nu.xom.Elements;
033import nu.xom.Node;
034import nu.xom.Nodes;
035import nu.xom.ParsingException;
036import nu.xom.ValidityException;
037import nu.xom.Serializer;
038import nu.xom.Text;
039import nu.xom.XMLException;
040
041import au.com.bytecode.opencsv.CSVReader;
042import au.com.bytecode.opencsv.CSVWriter;
043//import jxl.*;
044//import jxl.write.*;
045
046import java.io.IOException;
047import java.io.InputStream;
048import java.io.Reader;
049import java.lang.Exception;
050import java.sql.Array;
051import java.sql.Blob;
052import java.sql.Clob;
053import java.sql.ResultSet;
054import java.sql.ResultSetMetaData;
055import java.sql.SQLException;
056import java.sql.Types;
057
058
059/** Helper class that converts SQL ResultSets into XML (XOM) object, JSON (org.json) object, CSV file, Delimited StringBuilder, or a Delimited String.
060  * It relys on nu.xom, au.com.bytecode.opencsv,  and org.json libraries .
061**/
062public class ResultSetConverter
063{
064  public static final String SYSTEM_FILE_SEPERATOR = java.io.File.separator;
065
066  /**  A holder for this clients System line termination separator.  */
067  public static final String SYSTEM_LINE_SEPERATOR = System.getProperty("line.separator");
068  /** The Default char ("|") used for delimiting the ResultSet columns. It equals "|".**/
069  public static final String DEFAULT_COLUMN_DELIMITOR = "|";
070
071  /** The class ResultSet that gets converted. **/
072  private ResultSet rs_ = null;
073  // private Builder xmlBuilder_ = new Builder();
074  private Document rsDoc_ = null;
075  /** The class columnDelimitorchar (defualt = "|") used for delimiting the ResultSet columns during conversions to CSV, StringBuilder or String . **/
076  protected static String columnDelimitor = DEFAULT_COLUMN_DELIMITOR;
077
078  /** Constructor that initializes the class resultset. **/
079  public ResultSetConverter(ResultSet rs)
080  {
081    this.rs_ = rs;
082  }
083
084  /**
085   * Set Method for this classes ResultSet.
086   *
087   * @param rs is the value to set this class field to.
088   *
089   **/
090  public void setRs(ResultSet rs)
091  {
092    this.rs_ = rs;
093  }  // setRs Method
094
095
096  /**
097   * Get Method for this classes ResultSet.
098   *
099   * @return ResultSet - The value the class field 'rs'.
100   *
101   **/
102  public ResultSet getRs()
103  {
104    return this.rs_;
105  }  // getRs Method
106
107
108  /**
109    * Set Method for class field 'columnDelimitor' that gets used in deliminating the CSV, StringBuilder or String conversions.
110    * If not set, the DEFAULT_COLUMN_DELIMITOR is used.
111    *
112    * @param columnDelimitor is the value to set this class field to.
113    *
114    **/
115  public  void setColumnDelimitor(String columnDelimitor)
116  {
117    this.columnDelimitor = columnDelimitor;
118  }  // setColumnDelimitor Method
119
120
121  /**
122    * Get Method for class field 'columnDelimitor' that gets used in deliminating the CSV, StringBuilder or String conversions.
123    * If not set, the DEFAULT_COLUMN_DELIMITOR is used.
124    *
125    * @return String - The value the class field 'columnDelimitor'.
126    *
127    **/
128  public String getColumnDelimitor()
129  {
130    return columnDelimitor;
131  }  // getColumnDelimitor Method
132
133
134  /** Converts the class ResultSet into a fully described XML Document (nu.xom.Documnet)
135   * @return the xom Document representing the class ResultSet
136   **/
137  public Document convertToXml() throws IOException, SQLException, JSONException, Exception
138  {
139    if (this.rs_!=null)
140    {
141      System.out.println("convertToXML()");
142      try
143      {
144        ResultSetMetaData metadata = rs_.getMetaData();
145        int numFields = metadata.getColumnCount();
146        System.out.println("convertToXML: numFields="+numFields);
147
148        Element table = new Element("table");
149
150        Element record = null;
151        Element fieldElement = null;
152        int type = 0;
153        String typeName = "";
154        String name = "";
155        Object value = null;
156        int rowCount = 0;
157        while (rs_.next()) // && rowCount < 10)
158        {
159          rowCount++;
160          record = new Element("record");
161          for (int field = 1; field <= numFields; field++)
162          {
163
164            fieldElement = new Element("field");
165            type = metadata.getColumnType(field);
166            typeName = getXmlSchemaType(type);
167            fieldElement.addAttribute(new Attribute("xsi:type", "http://www.w3.org/2001/XMLSchema-instance",
168                                      typeName,
169                                      Attribute.Type.NMTOKEN));
170            name = metadata.getColumnName(field);
171            fieldElement.addAttribute(new Attribute("name", name));
172
173            // Convert nulls to empty elements with xsi:nil="true"
174            value = rs_.getObject(field);
175            if (value == null)
176            {            // null value in database
177              fieldElement.addAttribute(new Attribute("xsi:nil", "http://www.w3.org/2001/XMLSchema-instance", "true"));
178            }
179            else
180            {            // non-null value
181              fieldElement.appendChild(convertToXML(rs_, field, type));
182            }
183            record.appendChild(fieldElement);
184          }
185          table.appendChild(record);
186        }      // end while
187        rsDoc_ =  new Document(table);
188      }
189      catch (SQLException ex)
190      {      // convert exception type
191        throw new XMLException("SQL error", ex);
192      }
193    }
194    else
195    {
196      System.out.println("ERROR: Null or empty ResultSet.");
197      throw new Exception("Null or empty ResultSet.");
198    }
199    return rsDoc_;
200  }
201
202
203  /**
204   * converts the rs data field to a hex encoded Text xml node.
205   * I want the XML document to store values in the standard W3C
206   * XML Schema Language forms. This requires certain conversions
207   * depending on the type of the data
208   **/
209  private Node convertToXML(ResultSet data, int field, int type) throws SQLException, IOException
210  {
211    Node retVal = new Text("null");;
212    //System.out.println("convertToXML(ResultSet , int , int)");
213    //System.out.print(".");
214    switch (type)
215    {
216      case Types.BINARY:
217      case Types.VARBINARY:
218      case Types.LONGVARBINARY:
219        retVal = hexEncode(data.getBinaryStream(field));
220      case Types.BLOB:
221        Blob blob = data.getBlob(field);
222        retVal = hexEncode(blob.getBinaryStream());
223        // String types may contain C0 control characters that are
224        // not legal in XML. If so an exception is thrown.
225      case Types.CLOB:
226        Clob clob = data.getClob(field);
227        Reader r = clob.getCharacterStream();
228        char[] text = new char[1024];
229        int numRead;
230        StringBuffer result = new StringBuffer();
231        while ((numRead = r.read(text, 0, 1024)) != -1)
232        {
233          result.append(text, 0, numRead);
234        }
235        retVal = new Text(result.toString());
236      case Types.ARRAY:
237        Array array = data.getArray(field);
238        retVal = writeXmlArray(array);
239      default:        // All other types can be handled as strings
240        Object o = data.getObject(field);
241        if (o == null)
242        {
243          return new Text("");
244        }
245        retVal = new Text(o.toString());
246    }
247
248    return retVal;
249  }
250
251
252  /** Crerates a HEX encoded Text (String ) object. **/
253  private Text hexEncode(InputStream in) throws IOException
254  {
255
256    StringBuffer result = new StringBuffer();
257
258    int octet;
259    while ((octet = in.read()) != -1)
260    {
261      if (octet < 16)
262      {
263        result.append('0');
264      }
265      result.append(Integer.toHexString(octet));
266    }
267    return new Text(result.toString());
268
269  }
270
271
272  /** creates an XML Array element. **/
273  private Element writeXmlArray(Array array) throws IOException, SQLException
274  {
275
276    Element holder = new Element("array");
277    ResultSet data = array.getResultSet();
278    int type = array.getBaseType();
279    String typeName = getXmlSchemaType(type);
280
281    while (data.next())
282    {
283      Element component = new Element("component");
284      component.addAttribute(new Attribute("xsi:type", "http://www.w3.org/2001/XMLSchema-instance", typeName));
285      component.appendChild(convertToXML(data, 2, type));
286      holder.appendChild(component);
287    }
288    return holder;
289
290  }
291
292
293  /** lookup method to convert the passed in java.sql.Types int into a XML xsi:type, http://www.w3.org/2001/XMLSchema-instance String. **/
294  public static String getXmlSchemaType(int type)
295  {
296    switch (type)
297    {
298      case Types.ARRAY: return "array";
299      case Types.BIGINT: return "xsd:long";
300      case Types.BINARY: return "xsd:hexBinary";
301      case Types.BIT: return "xsd:boolean";
302      case Types.BLOB: return "xsd:hexBinary";
303      case Types.CHAR: return "xsd:string";
304      case Types.CLOB: return "xsd:string";
305      case Types.DATE: return "xsd:date";
306      case Types.DECIMAL: return "xsd:decimal";
307      case Types.DOUBLE: return "xsd:double";
308      case Types.FLOAT: return "xsd:decimal";
309      case Types.INTEGER: return "xsd:int";
310      case Types.JAVA_OBJECT: return "xsd:string";
311      case Types.LONGVARBINARY: return "xsd:hexBinary";
312      case Types.LONGVARCHAR: return "xsd:string";
313      case Types.NUMERIC: return "xsd:decimal";
314      case Types.REAL: return "xsd:float";
315      case Types.REF: return "xsd:IDREF";
316      case Types.SMALLINT: return "xsd:short";
317      case Types.STRUCT: return "struct";
318      case Types.TIME: return "xsd:time";
319      case Types.TIMESTAMP: return "xsd:dateTime";
320      case Types.TINYINT: return "xsd:byte";
321      case Types.VARBINARY: return "xsd:hexBinary";
322        // most general type
323      default: return "xsd:string";
324    }
325
326  }
327
328
329  /** Converts the class ResultSet into a fully described JSON Array Object (org.json.JSONArray)
330   * @return the org.json.JSONArray representing the class ResultSet or an Exception if ResultSet is null
331   **/
332  public JSONArray convertToJson() throws SQLException, JSONException, Exception
333  {
334    JSONArray json = null;
335
336    if (this.rs_!=null)
337    {
338      json = new JSONArray();
339      ResultSetMetaData rsmd = rs_.getMetaData();
340      int numColumns = rsmd.getColumnCount();
341
342      while (rs_.next())
343      {
344
345        JSONObject obj = new JSONObject();
346
347        for (int i = 1; i <= numColumns; i++)
348        {
349          String column_name = rsmd.getColumnName(i);
350
351          if (rsmd.getColumnType(i) == java.sql.Types.ARRAY)
352          {
353            obj.put(column_name, rs_.getArray(i));
354          }
355          else if (rsmd.getColumnType(i) == java.sql.Types.BIGINT)
356          {
357            obj.put(column_name, rs_.getLong(i));
358          }
359          else if (rsmd.getColumnType(i) == java.sql.Types.REAL)
360          {
361            obj.put(column_name, rs_.getFloat(i));
362          }
363          else if (rsmd.getColumnType(i) == java.sql.Types.BOOLEAN)
364          {
365            obj.put(column_name, rs_.getBoolean(i));
366          }
367          else if (rsmd.getColumnType(i) == java.sql.Types.BLOB)
368          {
369            obj.put(column_name, rs_.getBlob(i));
370          }
371          else if (rsmd.getColumnType(i) == java.sql.Types.DOUBLE)
372          {
373            obj.put(column_name, rs_.getDouble(i));
374          }
375          else if (rsmd.getColumnType(i) == java.sql.Types.FLOAT)
376          {
377            obj.put(column_name, rs_.getDouble(i));
378          }
379          else if (rsmd.getColumnType(i) == java.sql.Types.INTEGER)
380          {
381            obj.put(column_name, rs_.getInt(i));
382          }
383          else if (rsmd.getColumnType(i) == java.sql.Types.NVARCHAR)
384          {
385            obj.put(column_name, rs_.getNString(i));
386          }
387          else if (rsmd.getColumnType(i) == java.sql.Types.VARCHAR)
388          {
389            obj.put(column_name, rs_.getString(i));
390          }
391          else if (rsmd.getColumnType(i) == java.sql.Types.CHAR)
392          {
393            obj.put(column_name, rs_.getString(i));
394          }
395          else if (rsmd.getColumnType(i) == java.sql.Types.NCHAR)
396          {
397            obj.put(column_name, rs_.getNString(i));
398          }
399          else if (rsmd.getColumnType(i) == java.sql.Types.LONGNVARCHAR)
400          {
401            obj.put(column_name, rs_.getNString(i));
402          }
403          else if (rsmd.getColumnType(i) == java.sql.Types.LONGVARCHAR)
404          {
405            obj.put(column_name, rs_.getString(i));
406          }
407          else if (rsmd.getColumnType(i) == java.sql.Types.TINYINT)
408          {
409            obj.put(column_name, rs_.getByte(i));
410          }
411          else if (rsmd.getColumnType(i) == java.sql.Types.SMALLINT)
412          {
413            obj.put(column_name, rs_.getShort(i));
414          }
415          else if (rsmd.getColumnType(i) == java.sql.Types.DATE)
416          {
417            obj.put(column_name, rs_.getDate(i));
418          }
419          else if (rsmd.getColumnType(i) == java.sql.Types.TIME)
420          {
421            obj.put(column_name, rs_.getTime(i));
422          }
423          else if (rsmd.getColumnType(i) == java.sql.Types.TIMESTAMP)
424          {
425            obj.put(column_name, rs_.getTimestamp(i));
426          }
427          else if (rsmd.getColumnType(i) == java.sql.Types.BINARY)
428          {
429            obj.put(column_name, rs_.getBytes(i));
430          }
431          else if (rsmd.getColumnType(i) == java.sql.Types.VARBINARY)
432          {
433            obj.put(column_name, rs_.getBytes(i));
434          }
435          else if (rsmd.getColumnType(i) == java.sql.Types.LONGVARBINARY)
436          {
437            obj.put(column_name, rs_.getBinaryStream(i));
438          }
439          else if (rsmd.getColumnType(i) == java.sql.Types.BIT)
440          {
441            obj.put(column_name, rs_.getBoolean(i));
442          }
443          else if (rsmd.getColumnType(i) == java.sql.Types.CLOB)
444          {
445            obj.put(column_name, rs_.getClob(i));
446          }
447          else if (rsmd.getColumnType(i) == java.sql.Types.NUMERIC)
448          {
449            obj.put(column_name, rs_.getBigDecimal(i));
450          }
451          else if (rsmd.getColumnType(i) == java.sql.Types.DECIMAL)
452          {
453            obj.put(column_name, rs_.getBigDecimal(i));
454          }
455          else if (rsmd.getColumnType(i) == java.sql.Types.DATALINK)
456          {
457            obj.put(column_name, rs_.getURL(i));
458          }
459          else if (rsmd.getColumnType(i) == java.sql.Types.REF)
460          {
461            obj.put(column_name, rs_.getRef(i));
462          }
463          else if (rsmd.getColumnType(i) == java.sql.Types.STRUCT)
464          {
465            obj.put(column_name, rs_.getObject(i));          // must be a custom mapping consists of a class that implements the interface SQLData and an entry in a java.util.Map object.
466          }
467          else if (rsmd.getColumnType(i) == java.sql.Types.DISTINCT)
468          {
469            obj.put(column_name, rs_.getObject(i));          // must be a custom mapping consists of a class that implements the interface SQLData and an entry in a java.util.Map object.
470          }
471          else if (rsmd.getColumnType(i) == java.sql.Types.JAVA_OBJECT)
472          {
473            obj.put(column_name, rs_.getObject(i));
474          }
475          else
476          {
477            obj.put(column_name, rs_.getString(i));
478          }
479        }
480
481        json.put(obj);
482      }
483    }
484    else
485    {
486      System.out.println("ERROR: Null or empty ResultSet.");
487      throw new Exception("Null or empty ResultSet.");
488    }
489    return json;
490  }
491
492
493  /**
494   *  Converts a ResultSet StringBuffer to a saved file that holds one result per line.
495   *
496   * @param fileName is the name of the file to save
497   * @return a String containing the filename if suucessful, null if not.
498   **/
499  public String convertToCSVFile(String fileName, boolean includeColumnsHeaders)
500  {
501    String retVal = "";
502
503    try
504    {
505      CSVWriter csvW = new CSVWriter(new java.io.FileWriter(fileName), columnDelimitor.charAt(0));
506      if (rs_ != null)
507      {
508        csvW.writeAll(rs_, includeColumnsHeaders);
509      }
510
511      if (csvW != null)
512      {
513        csvW.close();
514      }
515
516      retVal = fileName;
517    }
518    catch (Exception ex)
519    {
520      System.out.println("Could Not write CSV File: " + fileName);
521    }
522
523    return retVal;
524  }
525
526
527  public String convertToCSV(){ return convertToCSV(true);}
528  /**
529   *  Converts a ResultSet  to a csv String that holds one result per line.
530   *
531   * @return a String containing the csv if suucessful, null if not.
532   **/
533  public String convertToCSV(boolean includeColumnsHeaders)
534  {
535    String retVal = "";
536
537    try
538    {
539      java.io.StringWriter sw = new java.io.StringWriter();
540      CSVWriter csvW = new CSVWriter(sw, columnDelimitor.charAt(0), CSVWriter.NO_QUOTE_CHARACTER);
541      if (rs_ != null)
542      {
543        csvW.writeAll(rs_, includeColumnsHeaders);
544      }
545
546      if (csvW != null)
547      {
548        csvW.flush();
549        csvW.close();
550      }
551
552      retVal = sw.toString();
553    }
554    catch (Exception ex)
555    {
556      System.out.println("Could Not parse resultSet to CSV");
557      ex.printStackTrace();
558    }
559
560    return retVal;
561  }
562
563
564  /** Convert to a Delimited StringBuilder.
565   * @return the Delimited StringBuilder representing the class ResultSet
566   **/
567  public StringBuilder convertToStringBuilder()
568  {
569    StringBuilder retVal = new StringBuilder("");
570    if (rs_ != null)
571    {
572      int numRows = 0;
573      try
574      {
575        ResultSetMetaData rsmd = rs_.getMetaData();
576        int numCols = rsmd.getColumnCount();
577        for (int i =1; i <= numCols; i++)
578        {
579          retVal.append(rsmd.getColumnName(i));
580          if (i < numCols)
581          {
582            retVal.append(columnDelimitor);
583          }
584        }
585        retVal.append("\n");
586        while (rs_.next())
587        {
588          for (int i =1; i <= numCols; i++)
589          {
590            retVal.append(rs_.getString(i));
591            if (i < numCols)
592            {
593              retVal.append(columnDelimitor);
594            }
595          }
596          numRows++;
597          retVal.append("\n");
598        }
599      }
600      catch (SQLException noResultsEx)
601      {
602        System.out.println(noResultsEx.toString());
603        retVal = new StringBuilder(noResultsEx.toString());
604      }
605      catch (java.lang.OutOfMemoryError meapMemEx)
606      {
607        System.out.println(meapMemEx.toString());
608        System.out.println("Truncating RS to " + numRows + " rows");
609      }
610    }
611    return retVal;
612  }
613
614  /** Convert to a Delimited String.
615   * @return the Delimited String representing the class ResultSet
616   **/
617  public String toString()
618  { return convertToStringBuilder().toString(); }
619
620}