1    package geekblog;
2    
3    
4    import java.sql.*;
5    import java.util.*;
6    import javax.servlet.*;
7    
8    
9    /**
10    * Expects a database looking something like the following:
11    *
12    * <pre>
13    * CREATE TABLE entries (ID long identity, posted datetime, title varchar, text varchar)
14    * CREATE TABLE articles (entryID long)
15    * CREATE TABLE comments (entryID long, name varchar, email varchar, comment varchar)
16    * CREATE TABLE links (linkID long identity, linkText varchar, linkURL varchar)
17    * </pre>
18    *
19    * <p>
20    * TODO: put close() calls around any open ResultSets
21    *
22    */
23   public class JDBCDataManager
24     implements DataManager
25   {
26     protected Connection conn;
27     protected Map statements = new HashMap();
28   
29   
30     /**
31      * Constructor placeholder; all real work done in init()
32      */
33     public JDBCDataManager()
34     {
35     }
36   
37   
38     /**
39      * Initializer.
40      */
41     public void init(ServletContext context)
42     {
43       // TODO: Look up database.url, database.userid, and database.password 
44       // properties from ServletContext (or Preferences node)
45       //
46     }
47     /**
48      * Shut everything down.
49      */
50     public void destroy(ServletContext context)
51     {
52       try
53       {
54         conn.close();
55       }
56       catch (SQLException sqlEx)
57       {
58         context.log(sqlEx, "*** Error shutting down connection ***");
59       }
60     }
61   
62   
63     // ================================================================
64     //                                              JDBC-specific API
65     // ================================================================
66     /**
67      * Return the JDBC Connection used by this DataManager. Intended solely
68      * for use by component-internal routines, like that of util.jsp.
69      */
70     public Connection getConnection()
71     {
72       return conn;
73     }
74   
75   
76     // ================================================================
77     //                                               Weblog entry API
78     // ================================================================
79     /**
80      * Retrieve an entry by ID.
81      */
82     public BlogEntry getEntry(long entryID)
83     {
84       BlogEntry retval = null;
85   
86       try
87       {
88         PreparedStatement ps;
89         if ((ps = (PreparedStatement)statements.get("getEntry")) == null)
90         {
91           String sql = 
92             "SELECT posted, title, text FROM entries WHERE id = ?";
93   
94           ps = conn.prepareStatement(sql);
95           statements.put("getEntry", ps);
96         }
97   
98         ps.setLong(1, entryID);
99   
100        ResultSet rs = ps.executeQuery();
101        if (rs.next())
102          retval = new BlogEntry(rs.getTimestamp(1), rs.getString(2), rs.getString(3));
103        if (rs.next())
104        {
105          // More than one? This is an error.
106          throw new RuntimeException("More than one entry " + entryID + " found.");
107        }
108        
109        return retval;
110      }
111      catch (SQLException sqlEx)
112      {
113        throw new RuntimeException(sqlEx);
114      }
115    }
116    /**
117     * Retrieve every single last one.
118     */
119    public Collection getAllEntries()
120    {
121      ArrayList retval = new ArrayList();
122  
123      try
124      {
125        PreparedStatement ps;
126        if ((ps = (PreparedStatement)statements.get("getLastNEntries")) == null)
127        {
128          String sql = 
129            "SELECT id, posted, title, text FROM entries ORDER BY posted DESC";
130  
131          ps = conn.prepareStatement(sql);
132          statements.put("getLastNEntries", ps);
133        }
134  
135        ResultSet rs = ps.executeQuery();
136        while (rs.next())
137        {
138          retval.add(new BlogEntry(rs.getTimestamp(2), rs.getString(3), rs.getString(4)));
139        }
140  
141        return retval;
142      }
143      catch (SQLException sqlEx)
144      {
145        throw new RuntimeException(sqlEx);
146      }
147    }
148    /**
149     * Retrieve the last "n" entries.
150     */
151    public Collection getLastNEntries(int n)
152    {
153      ArrayList retval = new ArrayList();
154  
155      try
156      {
157        PreparedStatement ps;
158        if ((ps = (PreparedStatement)statements.get("getLastNEntries")) == null)
159        {
160          String sql = 
161            "SELECT id, posted, title, text FROM entries ORDER BY posted DESC";
162  
163          ps = conn.prepareStatement(sql);
164          statements.put("getLastNEntries", ps);
165        }
166  
167        ResultSet rs = ps.executeQuery();
168        while (rs.next() && (n-- > 0))
169        {
170          retval.add(new BlogEntry(rs.getTimestamp(2), rs.getString(3), rs.getString(4)));
171        }
172  
173        return retval;
174      }
175      catch (SQLException sqlEx)
176      {
177        throw new RuntimeException(sqlEx);
178      }
179    }
180    /**
181     * Retrieve a list of entries for a given date.
182     *
183     * @param date Day/month/year tuple to find entries for.
184     *             Ignores hour, minutes, seconds.
185     */
186    public Collection getEntriesForDate(java.util.Date date)
187    {
188      ArrayList retval = new ArrayList();
189  
190      try
191      {
192        PreparedStatement ps;
193        if ((ps = (PreparedStatement)statements.get("getEntriesForDate")) == null)
194        {
195          String sql = 
196            "SELECT posted, title, text FROM entries WHERE DAYOFYEAR(posted) = DAYOFYEAR(?) AND YEAR(posted) = YEAR(?) ORDER BY posted DESC";
197  
198          ps = conn.prepareStatement(sql);
199          statements.put("getEntriesForDate", ps);
200        }
201  
202        ps.setDate(1, new java.sql.Date(date.getTime()));
203        ps.setDate(2, new java.sql.Date(date.getTime()));
204  
205        ResultSet rs = ps.executeQuery();
206        while (rs.next())
207        {
208          retval.add(new BlogEntry(rs.getTimestamp(1), rs.getString(2), rs.getString(3)));
209        }
210  
211        return retval;
212      }
213      catch (SQLException sqlEx)
214      {
215        throw new RuntimeException(sqlEx);
216      }
217    }
218    /**
219     * Retrieve a list of entries for a given month/year.
220     *
221     * @param date Month/year tuple to find entries for.
222     *             Ignores day, hour, minutes, seconds.
223     */
224    public Collection getEntriesForMonth(java.util.Date date)
225    {
226      ArrayList retval = new ArrayList();
227  
228      try
229      {
230        PreparedStatement ps;
231        if ((ps = (PreparedStatement)statements.get("getEntriesForMonth")) == null)
232        {
233          String sql = 
234            "SELECT posted, title, text FROM entries WHERE MONTH(posted) = MONTH(?) AND YEAR(posted) = YEAR(?) ORDER BY posted DESC";
235  
236          ps = conn.prepareStatement(sql);
237          statements.put("getEntriesForMonth", ps);
238        }
239  
240        ps.setDate(1, new java.sql.Date(date.getTime()));
241        ps.setDate(2, new java.sql.Date(date.getTime()));
242  
243        ResultSet rs = ps.executeQuery();
244        while (rs.next())
245        {
246          retval.add(new BlogEntry(rs.getTimestamp(1), rs.getString(2), rs.getString(3)));
247        }
248  
249        return retval;
250      }
251      catch (SQLException sqlEx)
252      {
253        throw new RuntimeException(sqlEx);
254      }
255    }
256    /**
257     * Retrieve a count of the entries for a given date.
258     *
259     * @param date Day/month/year to find count of entries for. 
260     *             Ignores hour, minutes, seconds.
261     */
262    public int getEntryCountForDate(java.util.Date date)
263    {
264      try
265      {
266        PreparedStatement ps;
267        if ((ps = (PreparedStatement)statements.get("getEntryCountForDate")) == null)
268        {
269          String sql = 
270            "SELECT COUNT(*) FROM entries WHERE DAYOFYEAR(posted) = DAYOFYEAR(?) AND YEAR(posted) = YEAR(?)";
271  
272          ps = conn.prepareStatement(sql);
273          statements.put("getEntryCountForDate", ps);
274        }
275  
276        ps.setDate(1, new java.sql.Date(date.getTime()));
277        ps.setDate(2, new java.sql.Date(date.getTime()));
278  
279        ResultSet rs = ps.executeQuery();
280        if (rs.next())
281          return rs.getInt(1);
282        else
283          throw new RuntimeException("How can an aggregate query not return at least one row?!?");
284      }
285      catch (SQLException sqlEx)
286      {
287        throw new RuntimeException(sqlEx);
288      }
289    }
290    /**
291     * Retrieve a count of the entries for a given month.
292     *
293     * @param date Month and year to find count of entries for. 
294     *             Ignores day, hour, minutes, seconds.
295     */
296    public int getEntryCountForMonth(java.util.Date date)
297    {
298      try
299      {
300        PreparedStatement ps;
301        if ((ps = (PreparedStatement)statements.get("getEntryCountForMonth")) == null)
302        {
303          String sql = 
304            "SELECT COUNT(*) FROM entries WHERE MONTH(posted) = MONTH(?) AND YEAR(posted) = YEAR(?)";
305  
306          ps = conn.prepareStatement(sql);
307          statements.put("getEntryCountForMonth", ps);
308        }
309  
310        ps.setDate(1, new java.sql.Date(date.getTime()));
311        ps.setDate(2, new java.sql.Date(date.getTime()));
312  
313        ResultSet rs = ps.executeQuery();
314        if (rs.next())
315        {
316          int retval = rs.getInt(1);
317          return retval;
318        }
319        else
320          throw new RuntimeException("How can an aggregate query not return at least one row?!?");
321      }
322      catch (SQLException sqlEx)
323      {
324        throw new RuntimeException(sqlEx);
325      }
326    }
327    /**
328     * Mark an entry as an article.
329     */
330    public void markAsArticle(BlogEntry entry)
331    {
332      try
333      {
334        PreparedStatement cps;
335        if ((cps = (PreparedStatement)statements.get("markAsArticleCheck")) == null)
336        {
337          String sql =
338            "SELECT * FROM articles WHERE entryID = ?";
339  
340          cps = conn.prepareStatement(sql);
341          statements.put("markAsArticleCheck", cps);
342        }
343        cps.setLong(1, entry.getID());
344        ResultSet rs = cps.executeQuery();
345        if (rs.next())
346        {
347          // This entry has already been marked as an article; don't mark it again
348          return;
349        }
350  
351        PreparedStatement ps;
352        if ((ps = (PreparedStatement)statements.get("markAsArticle")) == null)
353        {
354          String sql = 
355            "INSERT INTO articles VALUES(?)";
356  
357          ps = conn.prepareStatement(sql);
358          statements.put("markAsArticle", ps);
359        }
360  
361        ps.setLong(1, entry.getID());
362        int updateCt = ps.executeUpdate();
363        if (updateCt != 1)
364        {
365          throw new RuntimeException("Couldn't insert entry as article: " + entry.getID());
366        }
367      }
368      catch (SQLException sqlEx)
369      {
370        throw new RuntimeException(sqlEx);
371      }
372    }
373    /**
374     * Retrieve a list of all the entries marked as articles.
375     */
376    public Collection getArticleEntries()
377    {
378      ArrayList retval = new ArrayList();
379  
380      try
381      {
382        PreparedStatement ps;
383        if ((ps = (PreparedStatement)statements.get("getArticleEntries")) == null)
384        {
385          String sql = 
386            "SELECT posted, title, text FROM entries e, articles a WHERE a.entryID = e.id";
387  
388          ps = conn.prepareStatement(sql);
389          statements.put("getArticleEntries", ps);
390        }
391  
392        ResultSet rs = ps.executeQuery();
393        while (rs.next())
394        {
395          retval.add(new BlogEntry(rs.getTimestamp(1), rs.getString(2), rs.getString(3)));
396        }
397  
398        return retval;
399      }
400      catch (SQLException sqlEx)
401      {
402        throw new RuntimeException(sqlEx);
403      }
404    }
405    /**
406     * Find all entries containing some text expression
407     */
408    public Collection findEntries(String text)
409    {
410      ArrayList retval = new ArrayList();
411  
412      try
413      {
414        String searchParam = "%" + text + "%";
415        
416        PreparedStatement ps;
417        if ((ps = (PreparedStatement)statements.get("findEntries")) == null)
418        {
419          String sql = 
420            "SELECT id, posted, title, text FROM entries WHERE text LIKE ? OR title LIKE ? ORDER BY posted DESC";
421  
422          ps = conn.prepareStatement(sql);
423          statements.put("findEntries", ps);
424        }
425  
426        ps.setString(1, searchParam);
427        ps.setString(2, searchParam);
428  
429        ResultSet rs = ps.executeQuery();
430        while (rs.next())
431          retval.add(new BlogEntry(rs.getTimestamp(2), rs.getString(3), rs.getString(4)));
432        rs.close();
433  
434        return retval;
435      }
436      catch (SQLException sqlEx)
437      {
438        throw new RuntimeException(sqlEx);
439      }
440    }
441    /**
442     * Post an entry (either new or edited).
443     */
444    public void postEntry(BlogEntry entry)
445    {
446      try
447      {
448        // First find if this entry already exists
449        if (getEntry(entry.getID()) == null)
450        {
451          PreparedStatement ps;
452          if ((ps = (PreparedStatement)statements.get("postEntry.pt1")) == null)
453          {
454            String sql = 
455              "INSERT INTO entries VALUES (?, ?, ?, ?)";
456  
457            ps = conn.prepareStatement(sql);
458            statements.put("postEntry.pt1", ps);
459          }
460  
461          ps.setLong(1, entry.getID());
462          ps.setTimestamp(2, new java.sql.Timestamp(entry.getTimestamp().getTime()));
463          ps.setString(3, entry.getTitle());
464          ps.setString(4, entry.getText());
465  
466          int updateCt = ps.executeUpdate();
467          if (updateCt != 1)
468            throw new RuntimeException("Update count wasn't 1?!?");
469        }
470        else
471        {
472          PreparedStatement ps;
473          if ((ps = (PreparedStatement)statements.get("postEntry.pt2")) == null)
474          {
475            String sql = 
476              "UPDATE entries SET title = ?, text = ? WHERE ID = ?";
477  
478            ps = conn.prepareStatement(sql);
479            statements.put("postEntry.pt2", ps);
480          }
481  
482          ps.setString(1, entry.getTitle());
483          ps.setString(2, entry.getText());
484          ps.setLong(3, entry.getID());
485  
486          int updateCt = ps.executeUpdate();
487          if (updateCt != 1)
488            throw new RuntimeException("Update count wasn't 1?!?");
489        }
490      }
491      catch (SQLException sqlEx)
492      {
493        throw new RuntimeException(sqlEx);
494      }
495    }
496    /**
497     * Remove an entry. Also removes all comments and articles associated with 
498     * that entry.
499     */
500    public void removeEntry(long entryID)
501    {
502      // TODO Transact both under a single transaction for atomicity
503      //
504      try
505      {
506        PreparedStatement ps;
507        if ((ps = (PreparedStatement)statements.get("removeEntry.pt1")) == null)
508        {
509          String sql = "DELETE FROM entries WHERE ID = ?";
510          
511          ps = conn.prepareStatement(sql);
512          statements.put("removeEntry.pt1", ps);
513        }
514        ps.setLong(1, entryID);
515        ps.executeUpdate();
516          // Ignore possibility we tried to remove an entry that doesn't exist
517      
518        PreparedStatement ps2;
519        if ((ps2 = (PreparedStatement)statements.get("removeEntry.pt2")) == null)
520        {
521          String sql = "DELETE FROM comments WHERE entryID = ?";
522          
523          ps2 = conn.prepareStatement(sql);
524          statements.put("removeEntry.pt2", ps2);
525        }
526        ps2.setLong(1, entryID);
527        ps2.executeUpdate();
528          // Ignore possibility we tried to remove an entry that doesn't exist
529      
530        PreparedStatement ps3;
531        if ((ps3 = (PreparedStatement)statements.get("removeEntry.pt3")) == null)
532        {
533          String sql = "DELETE FROM articles WHERE entryID = ?";
534          
535          ps3 = conn.prepareStatement(sql);
536          statements.put("removeEntry.pt3", ps3);
537        }
538        ps3.setLong(1, entryID);
539        ps3.executeUpdate();
540          // Ignore possibility we tried to remove an entry that doesn't exist
541      }
542      catch (SQLException sqlEx)
543      {
544        throw new RuntimeException(sqlEx);
545      }
546    }
547    
548    
549    // ================================================================
550    //                                            Weblog comments API
551    // ================================================================
552    /**
553     * Retrieve the list of comments for a given entry.
554     */
555    public int getCommentCountForEntry(long entryID)
556    {
557      if (getEntry(entryID) == null)
558        return -1;
559  
560      try
561      {
562        PreparedStatement ps;
563        if ((ps = (PreparedStatement)statements.get("getCommentCountForEntry")) == null)
564        {
565          String sql = 
566            "SELECT COUNT(*) FROM comments WHERE entryID = ?";
567  
568          ps = conn.prepareStatement(sql);
569          statements.put("getCommentCountForEntry", ps);
570        }
571  
572        ps.setLong(1, entryID);
573  
574        ResultSet rs = ps.executeQuery();
575        if (rs.next())
576          return rs.getInt(1);
577        
578        return -1;
579      }
580      catch (Exception sqlEx)
581      {
582        throw new RuntimeException(sqlEx);
583      }
584    }
585    /**
586     * Retrieve the list of comments for a given entry.
587     */
588    public Collection getCommentsForEntry(long entryID)
589    {
590      if (getEntry(entryID) == null)
591        return null;
592  
593      ArrayList retval = new ArrayList();
594  
595      try
596      {
597        PreparedStatement ps;
598        if ((ps = (PreparedStatement)statements.get("getCommentsForEntry")) == null)
599        {
600          String sql = 
601            "SELECT name, email, comment FROM comments WHERE entryID = ?";
602  
603          ps = conn.prepareStatement(sql);
604          statements.put("getCommentsForEntry", ps);
605        }
606  
607        ps.setLong(1, entryID);
608  
609        ResultSet rs = ps.executeQuery();
610        while (rs.next())
611        {
612          retval.add(new BlogComment(entryID, rs.getString(1), rs.getString(2), rs.getString(3)));
613        }
614  
615        return retval;
616      }
617      catch (Exception sqlEx)
618      {
619        throw new RuntimeException(sqlEx);
620      }
621    }
622    /**
623     * Post a comment to an entry.
624     */
625    public void postComment(long entryID, BlogComment comment)
626    {
627      try
628      {
629        PreparedStatement ps;
630        if ((ps = (PreparedStatement)statements.get("postComment")) == null)
631        {
632          String sql = 
633            "INSERT INTO comments VALUES (?, ?, ?, ?)";
634  
635          ps = conn.prepareStatement(sql);
636          statements.put("postComment", ps);
637        }
638  
639        ps.setLong(1, entryID);
640        ps.setString(2, comment.getName());
641        ps.setString(3, comment.getEMail());
642        ps.setString(4, comment.getComment());
643  
644        int updateCt = ps.executeUpdate();
645        if (updateCt != 1)
646          throw new RuntimeException("Update count wasn't 1?!?");
647      }
648      catch (Exception sqlEx)
649      {
650        throw new RuntimeException(sqlEx);
651      }
652    }
653  }
654