001package bradleyross.j2ee.servlets;
002import java.io.*;
003import java.sql.SQLException;
004import java.sql.Types;
005import java.sql.ResultSet;
006import java.sql.ResultSetMetaData;
007import javax.servlet.*;
008import javax.servlet.http.*;
009import bradleyross.library.database.DatabaseProperties;
010/**
011 * Serves as a template for writing servlets that
012 * display the contains of a java.sql.ResultSet object
013 * as a CSV (comma separated values) page.
014 * 
015 * <p>The methods getResultSet and makeConnection are
016 *    abstract and will need to be overridden before
017 *    this class can be used.  The user may also wish to
018 *    override several of the methods for formatting
019 *    information.</p>
020 * <p>This code needs to be changed since many of the fields that belong
021 *    to the instance must be moved within the service method since there
022 *    can be multiple copies of the service method running at the same time.</p>
023 * @see #getResultSet(HttpServletRequest, DatabaseProperties)
024 * @author Bradley Ross
025 */
026public abstract class CsvWriter extends HttpServlet
027{
028        /**
029         * Provided for compliance with Serializable interface.
030         */
031        private static final long serialVersionUID = 1L;
032        /**
033         * ServletConfig object as passed to the init method.
034         * <p>Must be moved to inner class</p>
035         */
036        protected ServletConfig config = null;
037        /**
038         * Called by the servlet container when the servlet object is first placed
039         * in service.
040         */
041        public void init(ServletConfig configIn) throws ServletException
042        { 
043                super.init(configIn); 
044                config = configIn;
045        }
046        /**
047         * Called by the servlet container when the servlet object is being taken out
048         * of service.
049         */
050        public void destroy()
051        { super.destroy(); }
052        /**
053         * Encapsulates methods and properties for a given instance of the
054         * service method.
055         * 
056         * <p>Methods and properties in this class will normally not be
057         *    overridden.</p>
058         * @author Bradley Ross
059         *
060         */
061        protected class Instance extends Object
062        {
063                /**
064                 * Object containing HTTP request.
065                 */
066                protected HttpServletRequest request = null;
067                /**
068                 * Object used for preparing HTTP response.
069                 */
070                protected HttpServletResponse response = null;
071                public Instance (HttpServletRequest req, HttpServletResponse res)
072                {
073                        request = req;
074                        response = res;
075                        try 
076                        {
077                                output = res.getWriter();
078                        }
079                        catch (IOException e) 
080                        {
081                                e.printStackTrace();
082                        }
083                }
084                /**
085                 * File name to be used when creating file.  File will be
086                 * suffixed with .csv.
087                 * <p>Changes to this field can be made using the setValues method of the
088                 *    enclosing class.
089                 *    </p>
090                 * @see #setOutputFilename(String)
091                 * @see #getOutputFilename()
092                 * @see CsvWriter#setValues(Instance)
093                 * 
094                 */
095                protected String outputFilename = "default";
096                /**
097                 * String to be placed in CSV file as the first line in the file.
098                 * <p>Changes to this field can be made by using the setValues
099                 *    method of the enclosing class.</p>
100                 * @see CsvWriter#setValues(Instance)
101                 * 
102                 */
103                protected String message = null;
104                /**
105                 * Object containing database connection information.
106                 * <p>This field is set by the makeConnection method.</p>
107                 * @see CsvWriter#makeConnection()
108                 */
109                protected bradleyross.library.database.DatabaseProperties data = null;
110                /**
111                 * Object containing the result set.
112                 * <p>This object is created by the getResultSet method which
113                 *    is called by the service method.</p>
114                 * @see CsvWriter#getResultSet(HttpServletRequest, DatabaseProperties)
115                 */
116                protected java.sql.ResultSet rs = null;
117                /**
118                 * Object containing meta data on result set.
119                 * <p>This object is created by the service method after the
120                 *    execution of the getResultSet method and before the
121                 *    execution of the printHeader method.</p>
122                 *    <p>Must be moved to inner class</p>
123                 */
124                protected java.sql.ResultSetMetaData rsmd = null;  
125                /**
126                 * Object representing the output to the HTTP response.
127                 */
128                protected java.io.PrintWriter output = null;
129                /**
130                 * Number of columns in result set
131                 */
132                protected int columnCount;
133                /**
134                 * Getter for rs.
135                 * @return ResultSet object
136                 * @see #rs
137                 */
138                public ResultSet getResultSet()
139                {
140                        return rs;
141                }
142                /**
143                 * Setter for rs.
144                 * <p>This would not normally be called from outside classes.</p>
145                 * @param value Value to be used for field
146                 * @see #rs
147                 */
148                public void setResultSet(ResultSet value)
149                {
150                        try 
151                        {
152                                rs = value;
153                                rsmd = rs.getMetaData();
154                                columnCount = rsmd.getColumnCount();
155                        } 
156                        catch (SQLException e) 
157                        {
158
159                                e.printStackTrace();
160                        }
161                }
162                /**
163                 * Getter for rsmd.
164                 * 
165                 * @return ResultSetMetaData object
166                 * @see #rsmd
167                 */
168                public ResultSetMetaData getResultSetMetaData()
169                {
170                        return rsmd;
171                }
172                /**
173                 * Setter for rsmd.
174                 * 
175                 * <p>This would not normally be called by external classes.</p>
176                 * 
177                 * @param value ResultSetMetaData object
178                 * @see #rsmd
179                 */
180                public void setResultSetMetaData(ResultSetMetaData value)
181                {
182                        rsmd = value;
183                }
184                public DatabaseProperties getDatabaseProperties()
185                {
186                        return data;
187                }
188                public void setDatabaseProperties (DatabaseProperties value)
189                {
190                        data = value;
191                }
192                public String getOutputFilename()
193                { 
194                        return outputFilename;
195                }
196                public void setOutputFilename(String value)
197                {
198                        outputFilename = value;
199                }
200                /**
201                 * Obtains number of columns in result set
202                 * @return Number of columns
203                 * @see #columnCount
204                 */
205                public int getColumnCount()
206                {
207                        return columnCount;
208                }
209                /**
210                 * Sets number of columns in result set.
211                 * <p>This method would not normally be used.</p>
212                 * @param value Value to be used
213                 * @see #columnCount
214                 */
215                public void setColumnCount(int value)
216                {
217                        columnCount = value;
218                }
219                /**
220                 * Getter for output.
221                 * @return PrintWriter object.
222                 * @see #output
223                 */
224                public PrintWriter getPrintWriter()
225                {
226                        return output;
227                }
228                /**
229                 * Getter for message.
230                 * 
231                 * @return Message text
232                 * @see #message
233                 */
234                public String getMessage()
235                { 
236                        return message; 
237                }
238                /**
239                 * Getter for request.
240                 * 
241                 * @return HttpServletRequest object
242                 * @see #request
243                 */
244                public HttpServletRequest getRequest()
245                {
246                        return request;
247                }
248                /**
249                 * Getter for response.
250                 * @return HttpServletResponse object
251                 * @see #response
252                 */
253                public HttpServletResponse getResponse()
254                {
255                        return response;
256                }
257        }
258        /**
259         * Called by servlet container when an HTTP transaction requests a response
260         * from the server.
261         * 
262         * @param req Object containing request information
263         * @param res Object containing response information
264         */
265        public void service (HttpServletRequest req,
266                        HttpServletResponse res) throws IOException
267                        {
268                Instance instance = new Instance(req, res);
269                PrintWriter output = instance.getPrintWriter();
270                try
271                {
272                        instance.setDatabaseProperties(makeConnection());
273                        instance.getDatabaseProperties().connect();
274                        ResultSet rs = getResultSet(req, instance.getDatabaseProperties());
275                        instance.setResultSet(rs);
276                        setValues(instance);
277                        res.setContentType("text/csv");
278                        res.setHeader("Content-Disposition", "attachment;filename=" +
279                                        instance.getOutputFilename() + ".csv");
280                        if (instance.getMessage() != null)
281                        { output.print(instance.getMessage() + "\r\n"); }
282                        if (rs == null)
283                        { 
284                                output.print("No result set provided\r\n"); 
285                                return;
286                        }
287                        boolean moreRecords = rs.next();
288                        if (!moreRecords)
289                        {
290                                output.print("Result set is empty\r\n");
291                                return;
292                        }
293                        else 
294                        { printHeader(instance); }
295                        while (true)
296                        {
297                                printLine(instance);
298                                moreRecords = rs.next();
299                                if (!moreRecords) { break; }
300                        }
301                        instance.getResultSet().close();
302                        instance.getDatabaseProperties().close();
303                }
304                catch (java.io.IOException e)
305                { throw new java.io.IOException("Error in processing file: " +
306                                e.getClass().getName() + " : " + e.getMessage()); }
307                catch (java.sql.SQLException e)
308                { 
309                        System.out.println("SQL error in processing CSV file");
310                        throw new java.io.IOException("SQL error: " + e.getClass().getName() + 
311                                        " : " + e.getMessage());
312                }
313                        }
314        /**
315         * Obtain the database connection information.
316         */
317        protected abstract bradleyross.library.database.DatabaseProperties
318        makeConnection() throws IOException;
319        /**
320         * Will be overridden with the method for obtaining the SQL
321         * result set.
322         * <p>This should be kept separate from the getResultSet method within the
323         *    Instance class. The {@link Instance#getResultSet() } method returns the
324         *    ResultSet object stored in the Instance object.  This method actually generates
325         *    the ResultSet object.  The service method then stores the result of this method 
326         *    in the Instance object.</p>
327         * @param req Request information
328         * @return Result set
329         * @see Instance#message
330         * @see Instance#outputFilename
331         */
332        protected abstract java.sql.ResultSet getResultSet(HttpServletRequest req, DatabaseProperties data)
333        throws java.io.IOException;
334        /**
335         * Sets values in Instance object.
336         * <p>This must be overridden in a subclass in order to set parameters.</p>
337         * @param instance Instance object being affected
338         * @throws java.io.IOException
339         */
340        protected void setValues(Instance instance) throws java.io.IOException
341        { ; }
342        /**
343         * Will print header line for CSV file.
344         */
345        protected void printHeader (Instance instance)
346        throws java.io.IOException
347        {
348                ResultSetMetaData rsmd = instance.getResultSetMetaData();
349                PrintWriter output = instance.getPrintWriter();
350                try
351                {
352                        for (int i = 1; i <= rsmd.getColumnCount(); i++)
353                        {
354                                if (i != 1) { output.print(","); }
355                                output.print(rsmd.getColumnName(i));
356                        }
357                        output.print("\r\n");
358                }
359                catch (Exception e)
360                { throw new java.io.IOException("Error detected: " + e.getClass().getName() +
361                                " : " + e.getMessage()); }
362        }
363        /**
364         * Will print one line of CSV file.
365         * <p>This method will be changed at a later time to allow the
366         *    specification of overriding methods for formatting timestamps,
367         *    integers, floating point numbers, character strings, etc.</p>
368         */
369        protected void printLine (Instance instance)
370        throws java.io.IOException
371        {
372                int columnCount = 0;
373                ResultSet rs = instance.getResultSet();
374                ResultSetMetaData rsmd = instance.getResultSetMetaData();
375                PrintWriter output = instance.getPrintWriter();
376                try 
377                {
378                        columnCount = rsmd.getColumnCount();
379                } 
380                catch (SQLException e1) 
381                {
382                        e1.printStackTrace();
383                }
384                for (int i = 1; i <= columnCount; i++)
385                {
386                        try
387                        {
388                                if (i != 1) { output.print(","); } 
389                                int columnType = rsmd.getColumnType(i);
390                                if (columnType == Types.VARCHAR || columnType == Types.CHAR ||
391                                                columnType == Types.LONGVARCHAR)
392                                {
393                                        String working = rs.getString(i);
394                                        if (rs.wasNull()) { working = new String(); }
395                                        output.print(formatString(working, i)); 
396                                }
397                                else if (columnType == Types.FLOAT || columnType == Types.REAL ||
398                                                columnType == Types.DOUBLE)
399                                {
400                                        double working = rs.getDouble(i);
401                                        if (rs.wasNull()) 
402                                        { output.print(new String()); } 
403                                        else
404                                        { output.print(formatDouble(working)); }
405                                }
406                                else if (columnType == Types.TINYINT || columnType == Types.SMALLINT ||
407                                                columnType == Types.INTEGER || columnType == Types.BIGINT)
408                                {
409                                        int working = rs.getInt(i);
410                                        if (rs.wasNull())
411                                        { output.print(new String()); }
412                                        else
413                                        { output.print(formatInteger(working)); }
414                                }
415                                else if (columnType == Types.TIMESTAMP)
416                                { 
417                                        java.sql.Timestamp working = rs.getTimestamp(i);
418                                        if (rs.wasNull())
419                                        { output.print(new String()); }
420                                        else
421                                        {
422                                                java.util.Date date = new java.util.Date(working.getTime());
423                                                output.print(formatDateTime(date)); 
424                                        }
425                                }
426                                else
427                                        /*
428                                         ** Haven't decided how to handle Types.DECIMAL and
429                                         ** TYPES.GENERIC.  However, this may be academic as I don't
430                                         ** think that these will occur in the databases
431                                         ** that I will be using.
432                                         */
433                                         {
434                                        String working = rs.getString(i);
435                                        if (rs.wasNull())
436                                        { output.print(new String()); }
437                                        else
438                                        { output.print(working); }
439                                         }
440                        }
441                        catch (Exception e)
442                        { throw new java.io.IOException("Error detected in column " +
443                                        Integer.toString(i) + ":" +
444                                        e.getClass().getName() + " : " +
445                                        e.getMessage()); }
446                }
447                try
448                { output.print("\r\n"); }
449                catch (Exception e)
450                { throw new java.io.IOException("Error detected: " +
451                                e.getClass().getName() + " : " +
452                                e.getMessage()); }
453        }
454        /**
455         * Method of calling formatString when it doesn't
456         * depend on the column in the result set.
457         * @param valueIn String to be processed
458         * @return Processed string
459         */
460        protected final String formatString(String valueIn)
461        { return formatString(valueIn, -1); }
462        /**
463         * Double up on all double quotes in string and surround the string with
464         * double quotes so that it will be in the proper format for the CSV file.
465         * @param valueIn String to be processed
466         * @param column Column number of item - This will allow different formatting to be used
467         *        for each column.  This parameter is not used unless the method is
468         *        overridden.
469         * @return Processed string
470         */
471        protected String formatString(String valueIn, int column)
472        {
473                String working = null;
474                if (valueIn == null)
475                { working = " "; }
476                else if (valueIn.length() == 0)
477                { working = " "; }
478                else
479                {
480                        java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\\"");
481                        java.util.regex.Matcher matcher = pattern.matcher(valueIn);
482                        working = matcher.replaceAll("\"\"");
483                }
484                if (working == null) { working = " "; }
485                return "\"" + working + "\"";
486        }
487        /**
488         * Alternate calling sequence, leaving out the column number.
489         * @param valueIn Floating point number to be formatted
490         * @return Formatted string
491         */
492        protected final String formatDouble (double valueIn)
493        { return formatDouble (valueIn, -1); }
494        /**
495         * Formatter for floating point numbers.
496         * <p>This method can be overridden to control printing of
497         *    the values in the CSV file.</p>
498         * @param valueIn Floating point number to be processed
499         * @param column Column number - This will allow different formatting to be used
500         *        for each column.  This parameter is not used unless the method
501         *        is overridden.
502         * @return Formatted string
503         */
504        protected String formatDouble (double valueIn, int column)
505        { return Double.toString(valueIn); }
506        /**
507         * Alternate calling sequence, omitting the column number.
508         * @param valueIn Integer value to be formatted
509         * @return Formatted string
510         */
511        protected final String formatInteger (int valueIn)
512        { return formatInteger(valueIn, -1); }
513        /**
514         * Formatter for integer values.
515         * <p>This method can be overridden to control printing of
516         *    the values in the CSV file.</p>
517         * @param valueIn Integer to be formatted
518         * @param column Column number - This will allow different formatting to be used
519         *        for each column.  This parameter is not used unless the method is 
520         *        overridden.
521         * @return Formatted string
522         */
523        protected String formatInteger (int valueIn, int column)
524        { return Integer.toString(valueIn); }
525        /**
526         * Alternate calling sequence, leaving out the column number
527         * @param valueIn java.util.Date object containing date and time
528         * @return Formatted string
529         */
530        protected final String formatDateTime (java.util.Date valueIn)
531        { return formatDateTime(valueIn, 1); }
532        /**
533         * Formatter for values involving date and time.
534         * <p>This method can be overridden to control printing of
535         *    the values in the CSV file.</p>
536         * @param valueIn java.util.Date object containing date and time
537         * @param column Column number - This will allow different formatting to be used
538         *        for each column.  This parameter is not used unless the method is
539         *        overridden.
540         * @return Formatted string
541         */
542        protected String formatDateTime (java.util.Date valueIn, int column)
543        {
544                java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("MM/dd/yyyy HH:mm");
545                return format.format(valueIn);
546        }
547}