1 //
2 //  SurveyForum.java
3 //
4 //  Created by Steven R. Loomis on 27/10/2006.
5 //  Copyright 2006-2013 IBM. All rights reserved.
6 //
7 
8 package org.unicode.cldr.web;
9 
10 import java.sql.Connection;
11 import java.sql.PreparedStatement;
12 import java.sql.ResultSet;
13 import java.sql.SQLException;
14 import java.sql.Statement;
15 import java.sql.Timestamp;
16 import java.util.Date;
17 import java.util.HashMap;
18 import java.util.HashSet;
19 import java.util.Hashtable;
20 import java.util.Map;
21 import java.util.Set;
22 
23 import org.json.JSONArray;
24 import org.json.JSONException;
25 import org.json.JSONObject;
26 import org.unicode.cldr.util.CLDRConfig;
27 import org.unicode.cldr.util.CLDRLocale;
28 import org.unicode.cldr.util.CLDRURLS;
29 import org.unicode.cldr.util.VoteResolver;
30 import org.unicode.cldr.web.SurveyException.ErrorCode;
31 import org.unicode.cldr.web.UserRegistry.User;
32 
33 import com.ibm.icu.dev.util.ElapsedTimer;
34 import com.ibm.icu.text.DateFormat;
35 import com.ibm.icu.util.ULocale;
36 
37 /**
38  * This class implements a discussion forum per language (ISO code)
39  */
40 public class SurveyForum {
41 
42     private static final String FLAGGED_FOR_REVIEW_HTML = " <p>[This item was flagged for CLDR TC review.]";
43 
44     private static java.util.logging.Logger logger;
45 
46     private static String DB_FORA = "sf_fora"; // forum name -> id
47 
48     private static String DB_LOC2FORUM = "sf_loc2forum"; // locale -> forum.. for selects.
49 
50     private static final String F_FORUM = "forum";
51 
52     public static final String F_XPATH = "xpath";
53 
54     /**
55      * Make an "html-safe" version of the given string
56      *
57      * @param s
58      * @return the possibly-modified string
59      */
HTMLSafe(String s)60     public static String HTMLSafe(String s) {
61         if (s == null) {
62             return null;
63         }
64         return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
65     }
66 
67     /**
68      * Oops. HTML escaped into the DB
69      *
70      * @param s
71      * @return the possibly-modified string
72      */
HTMLUnsafe(String s)73     private static String HTMLUnsafe(String s) {
74         return s.replaceAll("<p>", "\n")
75             .replaceAll("&quot;", "\"")
76             .replaceAll("&gt;", ">")
77             .replaceAll("&lt;", "<")
78             .replaceAll("&amp;", "&");
79     }
80 
81     private Hashtable<String, Integer> nameToNum = new Hashtable<>();
82 
83     private static final int BAD_FORUM = -1;
84     private static final int NO_FORUM = -2;
85 
86     /**
87      * A post with this value for "parent" is the first post in its
88      * thread; that is, it has no real parent post.
89      */
90     public static final int NO_PARENT = -1;
91 
getForumNumber(CLDRLocale locale)92     private synchronized int getForumNumber(CLDRLocale locale) {
93         String forum = localeToForum(locale);
94         if (forum.length() == 0) {
95             return NO_FORUM; // all forums
96         }
97         // make sure it is a valid src!
98         if ((forum == null) || (forum.indexOf('_') >= 0) || !sm.isValidLocale(CLDRLocale.getInstance(forum))) {
99             return BAD_FORUM;
100         }
101         Integer i = nameToNum.get(forum);
102         if (i == null) {
103             return createForum(forum);
104         } else {
105             return i.intValue();
106         }
107     }
108 
getForumNumberFromDB(String forum)109     private int getForumNumberFromDB(String forum) {
110         try {
111             Connection conn = null;
112             PreparedStatement fGetByLoc = null;
113             try {
114                 conn = sm.dbUtils.getDBConnection();
115                 fGetByLoc = prepare_fGetByLoc(conn);
116                 fGetByLoc.setString(1, forum);
117                 ResultSet rs = fGetByLoc.executeQuery();
118                 if (!rs.next()) {
119                     rs.close();
120                     return BAD_FORUM;
121                 } else {
122                     int j = rs.getInt(1);
123                     rs.close();
124                     return j;
125                 }
126             } finally {
127                 DBUtils.close(fGetByLoc, conn);
128             }
129         } catch (SQLException se) {
130             String complaint = "SurveyForum:  Couldn't add forum " + forum + " - " + DBUtils.unchainSqlException(se)
131                 + " - fGetByLoc";
132             logger.severe(complaint);
133             throw new RuntimeException(complaint);
134         }
135     }
136 
137     /**
138      *
139      * @param forum
140      * @return the forum number
141      *
142      * Called only by getForumNumber.
143      */
createForum(String forum)144     private int createForum(String forum) {
145         int num = getForumNumberFromDB(forum);
146         if (num == BAD_FORUM) {
147             try {
148                 Connection conn = null;
149                 PreparedStatement fAdd = null;
150                 try {
151                     conn = sm.dbUtils.getDBConnection();
152                     fAdd = prepare_fAdd(conn);
153                     fAdd.setString(1, forum);
154                     fAdd.executeUpdate();
155                     conn.commit();
156                 } finally {
157                     DBUtils.close(fAdd, conn);
158                 }
159             } catch (SQLException se) {
160                 String complaint = "SurveyForum:  Couldn't add forum " + forum + " - " + DBUtils.unchainSqlException(se)
161                     + " - fAdd";
162                 logger.severe(complaint);
163                 throw new RuntimeException(complaint);
164             }
165             num = getForumNumberFromDB(forum);
166         }
167 
168         if (num == BAD_FORUM) {
169             throw new RuntimeException("Couldn't query ID for forum " + forum);
170         }
171         // Add to list
172         Integer i = new Integer(num);
173         nameToNum.put(forum, i);
174         return num;
175     }
176 
gatherInterestedUsers(String forum, Set<Integer> cc_emails, Set<Integer> bcc_emails)177     private int gatherInterestedUsers(String forum, Set<Integer> cc_emails, Set<Integer> bcc_emails) {
178         int emailCount = 0;
179         try {
180             Connection conn = null;
181             PreparedStatement pIntUsers = null;
182             try {
183                 conn = sm.dbUtils.getDBConnection();
184                 pIntUsers = prepare_pIntUsers(conn);
185                 pIntUsers.setString(1, forum);
186 
187                 ResultSet rs = pIntUsers.executeQuery();
188 
189                 while (rs.next()) {
190                     int uid = rs.getInt(1);
191 
192                     UserRegistry.User u = sm.reg.getInfo(uid);
193                     if (u != null && u.email != null && u.email.length() > 0
194                             && !(UserRegistry.userIsLocked(u) || UserRegistry.userIsExactlyAnonymous(u))) {
195                         if (UserRegistry.userIsVetter(u)) {
196                             cc_emails.add(u.id);
197                         } else {
198                             bcc_emails.add(u.id);
199                         }
200                         emailCount++;
201                     }
202                 }
203             } finally {
204                 DBUtils.close(pIntUsers, conn);
205             }
206         } catch (SQLException se) {
207             String complaint = "SurveyForum:  Couldn't gather interested users for " + forum + " - "
208                 + DBUtils.unchainSqlException(se) + " - pIntUsers";
209             logger.severe(complaint);
210             throw new RuntimeException(complaint);
211         }
212 
213         return emailCount;
214     }
215 
216     /**
217      * Send email notification to a set of users
218      *
219      * @param ctx
220      * @param forum
221      * @param base_xpath
222      * @param subj
223      * @param text
224      * @param postId
225      *
226      * Called by doPostInternal
227      */
emailNotify(UserRegistry.User user, CLDRLocale locale, int base_xpath, String subj, String text, Integer postId)228     private void emailNotify(UserRegistry.User user, CLDRLocale locale, int base_xpath, String subj, String text, Integer postId) {
229         String forum = localeToForum(locale);
230         ElapsedTimer et = new ElapsedTimer("Sending email to " + forum);
231         // Do email-
232         Set<Integer> cc_emails = new HashSet<>();
233         Set<Integer> bcc_emails = new HashSet<>();
234 
235         // Collect list of users to send to.
236         gatherInterestedUsers(forum, cc_emails, bcc_emails);
237 
238         String subject = "CLDR forum post (" + locale.getDisplayName() + " - " + locale + "): " + subj;
239 
240         String body = "Do not reply to this message, instead go to <"
241             + CLDRConfig.getInstance().absoluteUrls().forSpecial(CLDRURLS.Special.Forum, locale, (String) null, Integer.toString(postId))
242 
243             + ">\n====\n\n"
244             + text;
245 
246         if (MailSender.getInstance().DEBUG) {
247             System.out.println(et + ": Forum notify: u#" + user.id + " x" + base_xpath + " queueing cc:" + cc_emails.size() + " and bcc:" + bcc_emails.size());
248         }
249 
250         MailSender.getInstance().queue(user.id, cc_emails, bcc_emails, HTMLUnsafe(subject), HTMLUnsafe(body), locale, base_xpath, postId);
251     }
252 
253     /**
254      * Is the current user allowed to post with the given PostType in this context?
255      * This was already checked on the client, but don't trust the client too much.
256      * Check on server as well, at least to prevent someone closing a post who shouldn't be allowed to.
257      *
258      * @param user the current user
259      * @param postType the PostType
260      * @param replyTo the post id of the parent, or NO_PARENT
261      * @return true or false
262      *
263      * @throws SurveyException
264      */
userCanUsePostType(User user, PostType postType, int replyTo)265     private boolean userCanUsePostType(User user, PostType postType, int replyTo) throws SurveyException {
266         if (SurveyMain.isPhaseReadonly()) {
267             return false;
268         }
269         if (postType != PostType.CLOSE) {
270             return true;
271         }
272         if (replyTo == NO_PARENT) {
273             return false; // first post can't begin as closed
274         }
275         if (getFirstPosterInThread(replyTo) == user.id) {
276             return true;
277         }
278         if (UserRegistry.userIsTC(user)) {
279             return true;
280         }
281         return false;
282     }
283 
284     /**
285      * Get the user id of the first poster in the thread containing this post
286      *
287      * @param postId
288      * @return the user id, or UserRegistry.NO_USER
289      *
290      * @throws SurveyException
291      */
getFirstPosterInThread(int postId)292     private int getFirstPosterInThread(int postId) throws SurveyException {
293         int posterId = UserRegistry.NO_USER;
294         Connection conn = null;
295         PreparedStatement pList = null;
296         try {
297             conn = sm.dbUtils.getDBConnection();
298             pList = DBUtils.prepareStatement(conn, "pList", "SELECT parent,poster FROM " + DBUtils.Table.FORUM_POSTS.toString()
299                 + " WHERE id=?");
300             for (;;) {
301                 pList.setInt(1, postId);
302                 ResultSet rs = pList.executeQuery();
303                 int parentId = NO_PARENT;
304                 while (rs.next()) {
305                     parentId = rs.getInt(1);
306                     posterId = rs.getInt(2);
307                 }
308                 if (parentId == NO_PARENT) {
309                     break;
310                 }
311                 postId = parentId;
312             }
313          } catch (SQLException se) {
314             String complaint = "SurveyForum: Couldn't get parent for post - " + DBUtils.unchainSqlException(se);
315             SurveyLog.logException(se, complaint);
316             throw new SurveyException(ErrorCode.E_INTERNAL, complaint);
317         } finally {
318             DBUtils.close(pList, conn);
319         }
320         return posterId;
321     }
322 
323     /**
324      * If this user posts to the forum, will it cause this xpath+locale to be flagged
325      * (if not already flagged)?
326      *
327      * Return true if the user has made a losing vote, and the VoteResolver.canFlagOnLosing
328      * (i.e., path is locked and/or requires VoteResolver.HIGH_BAR votes).
329      *
330      * @param user
331      * @param xpath
332      * @param locale
333      * @return true or false
334      */
couldFlagOnLosing(UserRegistry.User user, String xpath, CLDRLocale locale)335     private boolean couldFlagOnLosing(UserRegistry.User user, String xpath, CLDRLocale locale) {
336         BallotBox<User> bb = sm.getSTFactory().ballotBoxForLocale(locale);
337         if (bb.userDidVote(user, xpath)) {
338             String userValue = bb.getVoteValue(user, xpath);
339             if (userValue != null) {
340                 VoteResolver<String> vr = bb.getResolver(xpath);
341                 if (!userValue.equals(vr.getWinningValue())) {
342                     return vr.canFlagOnLosing();
343                 }
344             }
345         }
346         return false;
347     }
348 
349     /**
350      * Called by SM to create the reg
351      *
352      * @param xlogger the logger to use
353      * @param ourConn the conn to use
354      * @return the SurveyForum
355      */
createTable(java.util.logging.Logger xlogger, Connection ourConn, SurveyMain sm)356     public static SurveyForum createTable(java.util.logging.Logger xlogger, Connection ourConn, SurveyMain sm)
357         throws SQLException {
358         SurveyForum reg = new SurveyForum(xlogger, sm);
359         try {
360             reg.setupDB(ourConn); // always call - we can figure it out.
361         } finally {
362             DBUtils.closeDBConnection(ourConn);
363         }
364         return reg;
365     }
366 
SurveyForum(java.util.logging.Logger xlogger, SurveyMain ourSm)367     private SurveyForum(java.util.logging.Logger xlogger, SurveyMain ourSm) {
368         logger = xlogger;
369         sm = ourSm;
370     }
371 
372     private Date oldOnOrBefore = null;
373 
374     /**
375      * Re-create DB_LOC2FORUM table from scratch, called at start-up
376      *
377      * @param conn
378      * @throws SQLException
379      */
reloadLocales(Connection conn)380     private void reloadLocales(Connection conn) throws SQLException {
381         String sql = "";
382         synchronized (conn) {
383             Statement s = conn.createStatement();
384             if (!DBUtils.hasTable(conn, DB_LOC2FORUM)) { // user attribute
385                 sql = "CREATE TABLE " + DB_LOC2FORUM + " ( " + " locale VARCHAR(255) NOT NULL, "
386                     + " forum VARCHAR(255) NOT NULL" + " )";
387                 s.execute(sql);
388                 sql = "CREATE UNIQUE INDEX " + DB_LOC2FORUM + "_loc ON " + DB_LOC2FORUM + " (locale) ";
389                 s.execute(sql);
390                 sql = "CREATE INDEX " + DB_LOC2FORUM + "_f ON " + DB_LOC2FORUM + " (forum) ";
391                 s.execute(sql);
392             } else {
393                 s.executeUpdate("delete from " + DB_LOC2FORUM);
394             }
395             s.close();
396 
397             PreparedStatement initbl = DBUtils.prepareStatement(conn, "initbl", "INSERT INTO " + DB_LOC2FORUM
398                 + " (locale,forum) VALUES (?,?)");
399             int errs = 0;
400             for (CLDRLocale l : SurveyMain.getLocalesSet()) {
401                 initbl.setString(1, l.toString());
402                 String forum = localeToForum(l);
403                 initbl.setString(2, forum);
404                 try {
405                     initbl.executeUpdate();
406                 } catch (SQLException se) {
407                     if (errs == 0) {
408                         System.err.println("While updating " + DB_LOC2FORUM + " -  " + DBUtils.unchainSqlException(se)
409                             + " - " + l + ":" + forum + ",  [This and further errors, ignored]");
410                     }
411                     errs++;
412                 }
413             }
414             initbl.close();
415             conn.commit();
416         }
417     }
418 
419     /**
420      * internal - called to setup db
421      */
setupDB(Connection conn)422     private void setupDB(Connection conn) throws SQLException {
423         String onOrBefore = CLDRConfig.getInstance().getProperty("CLDR_OLD_POSTS_BEFORE", "12/31/69");
424         DateFormat sdf = DateFormat.getDateInstance(DateFormat.SHORT, ULocale.US);
425         try {
426             oldOnOrBefore = sdf.parse(onOrBefore);
427         } catch (Throwable t) {
428             System.err.println("Error in parsing CLDR_OLD_POSTS_BEFORE : " + onOrBefore + " - err " + t.toString());
429             t.printStackTrace();
430             oldOnOrBefore = null;
431         }
432         if (oldOnOrBefore == null) {
433             oldOnOrBefore = new Date(0);
434         }
435         System.err.println("CLDR_OLD_POSTS_BEFORE: date: " + sdf.format(oldOnOrBefore) + " (format: mm/dd/yy)");
436         String sql = null;
437         String locindex = "loc";
438         if (DBUtils.db_Mysql) {
439             locindex = "loc(122)";
440         }
441 
442         if (!DBUtils.hasTable(conn, DB_FORA)) { // user attribute
443             Statement s = conn.createStatement();
444             sql = "CREATE TABLE " + DB_FORA + " ( " + " id INT NOT NULL " + DBUtils.DB_SQL_IDENTITY
445                 + ", "
446                 + " loc VARCHAR(122) NOT NULL, "
447                 + // interest locale
448                 " first_time " + DBUtils.DB_SQL_TIMESTAMP0 + " NOT NULL " + DBUtils.DB_SQL_WITHDEFAULT + " "
449                 + DBUtils.DB_SQL_CURRENT_TIMESTAMP0 + ", " + " last_time TIMESTAMP NOT NULL " + DBUtils.DB_SQL_WITHDEFAULT
450                 + " CURRENT_TIMESTAMP" + " )";
451             s.execute(sql);
452             sql = "";
453             s.close();
454             conn.commit();
455         }
456         if (!DBUtils.hasTable(conn, DBUtils.Table.FORUM_POSTS.toString())) {
457             Statement s = conn.createStatement();
458             sql = "CREATE TABLE " + DBUtils.Table.FORUM_POSTS + " ( " + " id INT NOT NULL "
459                 + DBUtils.DB_SQL_IDENTITY + ", "
460                 + " forum INT NOT NULL, " // which forum (DB_FORA), i.e. de
461                 + " poster INT NOT NULL, " + " subj " + DBUtils.DB_SQL_UNICODE + ", " + " text " + DBUtils.DB_SQL_UNICODE
462                 + " NOT NULL, " + " parent INT " + DBUtils.DB_SQL_WITHDEFAULT
463                 + " -1, "
464                 + " loc VARCHAR(122), " // specific locale, i.e. de_CH
465                 + " xpath INT, " // base xpath
466                 + " last_time TIMESTAMP NOT NULL " + DBUtils.DB_SQL_WITHDEFAULT + " CURRENT_TIMESTAMP, "
467                 + " version VARCHAR(122), " // CLDR version
468                 + " root INT NOT NULL,"
469                 + " type INT NOT NULL,"
470                 + " is_open BOOLEAN NOT NULL,"
471                 + " value " + DBUtils.DB_SQL_UNICODE
472                 + " )";
473             s.execute(sql);
474             sql = "CREATE UNIQUE INDEX " + DBUtils.Table.FORUM_POSTS + "_id ON " + DBUtils.Table.FORUM_POSTS + " (id) ";
475             s.execute(sql);
476             sql = "CREATE INDEX " + DBUtils.Table.FORUM_POSTS + "_ut ON " + DBUtils.Table.FORUM_POSTS + " (poster, last_time) ";
477             s.execute(sql);
478             sql = "CREATE INDEX " + DBUtils.Table.FORUM_POSTS + "_utt ON " + DBUtils.Table.FORUM_POSTS + " (id, last_time) ";
479             s.execute(sql);
480             sql = "CREATE INDEX " + DBUtils.Table.FORUM_POSTS + "_chil ON " + DBUtils.Table.FORUM_POSTS + " (parent) ";
481             s.execute(sql);
482             sql = "CREATE INDEX " + DBUtils.Table.FORUM_POSTS + "_loc ON " + DBUtils.Table.FORUM_POSTS + " (" + locindex + ") ";
483             s.execute(sql);
484             sql = "CREATE INDEX " + DBUtils.Table.FORUM_POSTS + "_x ON " + DBUtils.Table.FORUM_POSTS + " (xpath) ";
485             s.execute(sql);
486             sql = "";
487             s.close();
488             conn.commit();
489         }
490         reloadLocales(conn);
491     }
492 
493     private SurveyMain sm = null;
494 
prepare_fGetByLoc(Connection conn)495     private static PreparedStatement prepare_fGetByLoc(Connection conn) throws SQLException {
496         return DBUtils.prepareStatement(conn, "fGetByLoc", "SELECT id FROM " + DB_FORA + " where loc=?");
497     }
498 
prepare_fAdd(Connection conn)499     private static PreparedStatement prepare_fAdd(Connection conn) throws SQLException {
500         return DBUtils.prepareStatement(conn, "fAdd", "INSERT INTO " + DB_FORA + " (loc) values (?)");
501     }
502 
503     /**
504      * Prepare a statement for adding a new post to the forum table.
505      *
506      * @param conn the Connection
507      * @return the PreparedStatement
508      * @throws SQLException
509      *
510      * Called only by savePostToDb
511      */
prepare_pAdd(Connection conn)512     private static PreparedStatement prepare_pAdd(Connection conn) throws SQLException {
513         return DBUtils.prepareStatement(conn, "pAdd", "INSERT INTO "
514             + DBUtils.Table.FORUM_POSTS.toString()
515             + " (poster,subj,text,forum,parent,loc,xpath,version,root,type,is_open,value)"
516             + " values (?,?,?,?,?,?,?,?,?,?,?,?)");
517     }
518 
519     /**
520      * Prepare a statement for closing a thread of posts in the forum table.
521      *
522      * @param conn the Connection
523      * @return the PreparedStatement
524      * @throws SQLException
525      *
526      * Called only by savePostToDb
527      */
prepare_pCloseThread(Connection conn)528     private static PreparedStatement prepare_pCloseThread(Connection conn) throws SQLException {
529         return DBUtils.prepareStatement(conn, "pAdd", "UPDATE "
530             + DBUtils.Table.FORUM_POSTS.toString()
531             + " SET is_open=false WHERE id=? OR root=?");
532     }
533 
prepare_pIntUsers(Connection conn)534     private static PreparedStatement prepare_pIntUsers(Connection conn) throws SQLException {
535         return DBUtils.prepareStatement(conn, "pIntUsers", "SELECT uid from " + UserRegistry.CLDR_INTEREST + " where forum=?");
536     }
537 
localeToForum(ULocale locale)538     private static String localeToForum(ULocale locale) {
539         return locale.getLanguage();
540     }
541 
localeToForum(CLDRLocale locale)542     private static String localeToForum(CLDRLocale locale) {
543         return localeToForum(locale.toULocale());
544     }
545 
546     /**
547      *
548      * @param ctx
549      * @param forum
550      * @return
551      *
552      * Possibly called by tmpl/usermenu.jsp -- maybe dead code?
553      */
forumLink(WebContext ctx, String forum)554     public static String forumLink(WebContext ctx, String forum) {
555         String url = ctx.base() + "?" + F_FORUM + "=" + forum;
556         return "<a " + ctx.atarget(WebContext.TARGET_DOCS) + " class='forumlink' href='" + url + "' >" // title='"+title+"'
557             + "Forum" + "</a>";
558     }
559 
560     /**
561      * How many forum posts are there for the given locale and xpath?
562      *
563      * @param locale
564      * @param xpathId
565      * @return the number of posts
566      *
567      * Called by STFactory.PerLocaleData.voteForValue and SurveyAjax.processRequest (WHAT_FORUM_COUNT)
568      */
postCountFor(CLDRLocale locale, int xpathId)569     public int postCountFor(CLDRLocale locale, int xpathId) {
570         Connection conn = null;
571         PreparedStatement ps = null;
572         String tableName = DBUtils.Table.FORUM_POSTS.toString();
573         try {
574             conn = DBUtils.getInstance().getDBConnection();
575 
576             ps = DBUtils.prepareForwardReadOnly(conn, "select count(*) from " + tableName + " where loc=? and xpath=?");
577             ps.setString(1, locale.getBaseName());
578             ps.setInt(2, xpathId);
579 
580             return DBUtils.sqlCount(null, conn, ps);
581         } catch (SQLException e) {
582             SurveyLog.logException(e, "postCountFor for " + tableName + " " + locale + ":" + xpathId);
583             return 0;
584         } finally {
585             DBUtils.close(ps, conn);
586         }
587     }
588 
589     /**
590      * Gather forum post information into a JSONArray, in preparation for
591      * displaying it to the user.
592      *
593      * @param session
594      * @param locale
595      * @param base_xpath Base XPath of the item being viewed, if positive; or XPathTable.NO_XPATH
596      * @param ident If nonzero - select only this item. If zero, select all items.
597      * @return the JSONArray
598      * @throws JSONException
599      * @throws SurveyException
600      */
toJSON(CookieSession session, CLDRLocale locale, int base_xpath, int ident)601     public JSONArray toJSON(CookieSession session, CLDRLocale locale, int base_xpath, int ident) throws JSONException, SurveyException {
602         assertCanAccessForum(session, locale);
603 
604         JSONArray ret = new JSONArray();
605 
606         int forumNumber = getForumNumber(locale);
607 
608         try {
609             Connection conn = null;
610             try {
611                 conn = sm.dbUtils.getDBConnection();
612                 Object[][] o = null;
613                 final String forumPosts = DBUtils.Table.FORUM_POSTS.toString();
614                 if (ident == 0) {
615                     if (base_xpath == 0) {
616                         // all posts
617                         o = DBUtils.sqlQueryArrayArrayObj(conn, "select " + getPallresultfora(forumPosts) + "  FROM " + forumPosts
618                             + " WHERE (" + forumPosts + ".forum =? ) ORDER BY "
619                             + forumPosts
620                             + ".last_time DESC", forumNumber);
621                     } else {
622                         // all posts for xpath
623                         o = DBUtils.sqlQueryArrayArrayObj(conn, "select " + getPallresultfora(forumPosts) + "  FROM " + forumPosts
624                             + " WHERE (" + forumPosts + ".forum =? AND " + forumPosts + " .xpath =? and "
625                             + forumPosts + ".loc=? ) ORDER BY "
626                             + forumPosts
627                             + ".last_time DESC", forumNumber, base_xpath, locale);
628                     }
629                 } else {
630                     // specific POST
631                     if (base_xpath <= 0) {
632                         o = DBUtils.sqlQueryArrayArrayObj(conn, "select " + getPallresultfora(forumPosts) + "  FROM " + forumPosts
633                                 + " WHERE (" + forumPosts + ".forum =? AND "
634                                 + forumPosts + " .id =?) ORDER BY "
635                                 + forumPosts
636                                 + ".last_time DESC",
637                             forumNumber, /* base_xpath,*/ident);
638                     } else {
639                         // just a restriction - specific post, specific xpath
640                         o = DBUtils.sqlQueryArrayArrayObj(conn, "select " + getPallresultfora(forumPosts) + "  FROM " + forumPosts
641                             + " WHERE (" + forumPosts + ".forum =? AND " + forumPosts + " .xpath =? AND "
642                             + forumPosts + " .id =?) ORDER BY " + forumPosts
643                             + ".last_time DESC", forumNumber, base_xpath, ident);
644                     }
645                 }
646                 if (o != null) {
647                     for (int i = 0; i < o.length; i++) {
648                         int poster = (Integer) o[i][0];
649                         String subj2 = (String) o[i][1];
650                         String text2 = (String) o[i][2];
651                         Timestamp lastDate = (Timestamp) o[i][3];
652                         int id = (Integer) o[i][4];
653                         int parent = (Integer) o[i][5];
654                         int xpath = (Integer) o[i][6];
655                         String loc = (String) o[i][7];
656                         String version = (String) o[i][8];
657                         int root = (int) o[i][9];
658                         int typeInt = (int) o[i][10];
659                         boolean open = (boolean) o[i][11];
660                         String value = (String) o[i][12];
661 
662                         PostType type = PostType.fromInt(typeInt, PostType.DISCUSS);
663 
664                         if (lastDate.after(oldOnOrBefore)) {
665                             JSONObject post = new JSONObject();
666                             post.put("poster", poster)
667                                 .put("subject", subj2)
668                                 .put("text", text2)
669                                 .put("postType", type.toName())
670                                 .put("date", lastDate)
671                                 .put("date_long", lastDate.getTime())
672                                 .put("id", id)
673                                 .put("parent", parent);
674                             if (loc != null) {
675                                 post.put("locale", loc);
676                             }
677                             if (version != null) {
678                                 post.put("version", version);
679                             }
680                             if (value != null) {
681                                 post.put("value", value);
682                             }
683                             post.put("open", open);
684                             post.put("root", root);
685                             post.put("xpath_id", xpath);
686                             if (xpath > 0) {
687                                 post.put("xpath", sm.xpt.getStringIDString(xpath));
688                             }
689                             UserRegistry.User posterUser = sm.reg.getInfo(poster);
690                             if (posterUser != null) {
691                                 JSONObject posterInfoJson = SurveyAjax.JSONWriter.wrap(posterUser);
692                                 if (posterInfoJson != null) {
693                                     post.put("posterInfo", posterInfoJson);
694                                 }
695                             }
696                             ret.put(post);
697                         }
698                     }
699                 }
700                 return ret;
701             } finally {
702                 DBUtils.close(conn);
703             }
704         } catch (SQLException se) {
705             // When query fails, set breakpoint here and look at se.detailMessage for clues
706             String complaint = "SurveyForum:  Couldn't show posts in forum "
707                 + locale
708                 + " - " + DBUtils.unchainSqlException(se)
709                 + " - fGetByLoc";
710             logger.severe(complaint);
711             throw new RuntimeException(complaint);
712         }
713     }
714 
assertCanAccessForum(CookieSession session, CLDRLocale locale)715     private void assertCanAccessForum(CookieSession session, CLDRLocale locale) throws SurveyException {
716         if (session == null || session.user == null) {
717             throw new SurveyException(ErrorCode.E_NOT_LOGGED_IN);
718         }
719         assertCanAccessForum(session.user, locale);
720     }
721 
assertCanAccessForum(UserRegistry.User user, CLDRLocale locale)722     private void assertCanAccessForum(UserRegistry.User user, CLDRLocale locale) throws SurveyException {
723         boolean canModify = (UserRegistry.userCanAccessForum(user, locale));
724         if (!canModify) {
725             throw new SurveyException(ErrorCode.E_NO_PERMISSION, "You do not have permission to access that locale");
726         }
727     }
728 
729     /**
730      * Construct a portion of an sql query for getting all needed columns from the forum posts table.
731      *
732      * @param forumPosts the table name
733      * @return the string to be used as part of a query
734      */
getPallresultfora(String forumPosts)735     private static String getPallresultfora(String forumPosts) {
736         return forumPosts + ".poster,"
737             + forumPosts + ".subj,"
738             + forumPosts + ".text,"
739             + forumPosts + ".last_time,"
740             + forumPosts + ".id,"
741             + forumPosts + ".parent,"
742             + forumPosts + ".xpath, "
743             + forumPosts + ".loc,"
744             + forumPosts + ".version,"
745             + forumPosts + ".root,"
746             + forumPosts + ".type,"
747             + forumPosts + ".is_open,"
748             + forumPosts + ".value";
749     }
750 
751     /**
752      * Respond when the user adds a new forum post.
753      *
754      * @param mySession the CookieSession
755      * @param xpath of the form "stringid" or "#1234"
756      * @param l the CLDRLocale
757      * @param subj the subject of the post
758      * @param text the text of the post
759      * @param postTypeStr the PostType string such as "Close", or null
760      * @param replyTo the id of the post to which this is a reply; {@link #NO_PARENT} if there is no parent
761      * @return the post id
762      *
763      * @throws SurveyException
764      */
doPost(CookieSession mySession, PostInfo postInfo)765     public int doPost(CookieSession mySession, PostInfo postInfo) throws SurveyException {
766         CLDRLocale locale = postInfo.getLocale();
767         assertCanAccessForum(mySession, locale);
768         int replyTo = postInfo.getReplyTo();
769         int base_xpath;
770         if (replyTo < 0) {
771             replyTo = NO_PARENT;
772             base_xpath = sm.xpt.getXpathIdOrNoneFromStringID(postInfo.getPathStr());
773         } else {
774             base_xpath = DBUtils.sqlCount("select xpath from " + DBUtils.Table.FORUM_POSTS + " where id=?", replyTo); // default to -1
775         }
776         postInfo.setPath(base_xpath);
777         final boolean couldFlag = couldFlagOnLosing(postInfo.getUser(), sm.xpt.getById(base_xpath), locale)
778                 && !sm.getSTFactory().getFlag(locale, base_xpath);
779         postInfo.setCouldFlagOnLosing(couldFlag);
780         if (couldFlag) {
781             postInfo.setText(postInfo.getText() + FLAGGED_FOR_REVIEW_HTML);
782         }
783         return doPostInternal(postInfo);
784     }
785 
786     /**
787      * Update the forum as appropriate after a vote has been accepted
788      *
789      * @param locale
790      * @param user
791      * @param distinguishingXpath
792      * @param xpathId
793      * @param value
794      * @param didClearFlag
795      */
doForumAfterVote(CLDRLocale locale, User user, String distinguishingXpath, int xpathId, String value, boolean didClearFlag)796     public void doForumAfterVote(CLDRLocale locale, User user, String distinguishingXpath, int xpathId,
797             String value, boolean didClearFlag) {
798         if (didClearFlag) {
799             try {
800                 int newPostId = postFlagRemoved(xpathId, locale, user);
801                 System.out.println("NOTE: flag was removed from "
802                     + locale + " " + distinguishingXpath + " - post ID=" + newPostId
803                     + " by " + user.toString());
804             } catch (SurveyException e) {
805                 SurveyLog.logException(e, "Error trying to post that a flag was removed from "
806                     + locale + " " + distinguishingXpath);
807             }
808         }
809         final boolean ENABLE_AUTO_POSTING = true;
810         if (ENABLE_AUTO_POSTING) {
811             if (value != null) {
812                 autoPostAgree(locale, user, xpathId, value);
813             }
814             autoPostDecline(locale, user, xpathId, value);
815             autoPostClose(locale, user, xpathId, value);
816         }
817     }
818 
819     /**
820      * Make a special post for flag removal
821      *
822      * @param xpathId
823      * @param locale
824      * @param user
825      * @return the post id
826      *
827      * @throws SurveyException
828      */
postFlagRemoved(int xpathId, CLDRLocale locale, User user)829     private int postFlagRemoved(int xpathId, CLDRLocale locale, User user) throws SurveyException {
830         PostInfo postInfo = new PostInfo(locale, PostType.CLOSE.toName(), "(The flag was removed.)");
831         postInfo.setSubj("Flag Removed");
832         postInfo.setPath(xpathId);
833         postInfo.setUser(user);
834         return doPostInternal(postInfo);
835     }
836 
837     /**
838      * Auto-post Agree for each open Request post for this locale+path+value by other users
839      *
840      * @param locale
841      * @param user
842      * @param xpathId
843      * @param value
844      */
autoPostAgree(CLDRLocale locale, User user, int xpathId, String value)845     private void autoPostAgree(CLDRLocale locale, User user, int xpathId, String value) {
846         Connection conn = null;
847         PreparedStatement pList = null;
848         String tableName = DBUtils.Table.FORUM_POSTS.toString();
849         Map<Integer, String> posts = new HashMap<>();
850         try {
851             conn = sm.dbUtils.getDBConnection();
852             pList = DBUtils.prepareStatement(conn, "pList",
853                 "SELECT id,subj FROM " + tableName
854                 + " WHERE is_open=true AND type=? AND loc=? AND xpath=? AND value=? AND NOT poster=?");
855             pList.setInt(1, PostType.REQUEST.toInt());
856             pList.setString(2, locale.toString());
857             pList.setInt(3, xpathId);
858             DBUtils.setStringUTF8(pList, 4, value);
859             pList.setInt(5, user.id);
860             ResultSet rs = pList.executeQuery();
861             while (rs.next()) {
862                 posts.put(rs.getInt(1), DBUtils.getStringUTF8(rs, 2));
863             }
864             posts.forEach((root, subject) -> autoPostReplyAgree(root, subject, locale, user, xpathId, value));
865          } catch (SQLException se) {
866             String complaint = "SurveyForum: autoPostAgree - " + DBUtils.unchainSqlException(se);
867             SurveyLog.logException(se, complaint);
868         } finally {
869             DBUtils.close(pList, conn);
870         }
871     }
872 
autoPostReplyAgree(int root, String subject, CLDRLocale locale, User user, int xpathId, String value)873     private void autoPostReplyAgree(int root, String subject, CLDRLocale locale, User user,
874             int xpathId, String value) {
875         String text = "(Auto-generated:) I voted for “" + value + "”";
876         PostInfo postInfo = new PostInfo(locale, PostType.AGREE.toName(), text);
877         postInfo.setSubj(subject);
878         postInfo.setPath(xpathId);
879         postInfo.setUser(user);
880         postInfo.setReplyTo(root /* replyTo */);
881         postInfo.setRoot(root);
882         postInfo.setValue(value);
883         postInfo.setSendEmail(false);
884         try {
885             doPostInternal(postInfo);
886         } catch (SurveyException e) {
887             SurveyLog.logException(e, "SurveyForum: autoPostReplyAgree root " + root);
888         }
889     }
890 
891     /**
892      * For each open AGREE post by this user, for this locale+path, where
893      * the given value is NOT the same as the requested value, generate a new DECLINE post.
894      *
895      * @param locale
896      * @param user
897      * @param xpathId
898      * @param value
899      */
autoPostDecline(CLDRLocale locale, User user, int xpathId, String value)900     private void autoPostDecline(CLDRLocale locale, User user, int xpathId, String value) {
901         String dbValue = value == null ? "" : value;
902         Connection conn = null;
903         PreparedStatement pList = null;
904         String tableName = DBUtils.Table.FORUM_POSTS.toString();
905         Map<Integer, String> posts = new HashMap<>();
906         try {
907             conn = sm.dbUtils.getDBConnection();
908             pList = DBUtils.prepareStatement(conn, "pList",
909                 "SELECT root,subj FROM " + tableName
910                 + " WHERE is_open=true AND type=? AND loc=? AND xpath=? AND poster=? AND NOT value=?");
911             pList.setInt(1, PostType.AGREE.toInt());
912             pList.setString(2, locale.toString());
913             pList.setInt(3, xpathId);
914             pList.setInt(4, user.id);
915             DBUtils.setStringUTF8(pList, 5, dbValue);
916             ResultSet rs = pList.executeQuery();
917             while (rs.next()) {
918                 posts.put(rs.getInt(1), DBUtils.getStringUTF8(rs, 2));
919             }
920             posts.forEach((root, subject) -> autoPostReplyDecline(root, subject, locale, user, xpathId, value));
921          } catch (SQLException se) {
922             String complaint = "SurveyForum: autoPostDecline - " + DBUtils.unchainSqlException(se);
923             SurveyLog.logException(se, complaint);
924         } finally {
925             DBUtils.close(pList, conn);
926         }
927     }
928 
autoPostReplyDecline(int root, String subject, CLDRLocale locale, User user, int xpathId, String value)929     private void autoPostReplyDecline(int root, String subject, CLDRLocale locale, User user,
930             int xpathId, String value) {
931         String text = (value == null)
932                 ? "(Auto-generated:) I changed my vote to Abstain, and will reconsider my vote."
933                 : "(Auto-generated:) I changed my vote to “" + value + "”, which now disagrees with the request.";
934         PostInfo postInfo = new PostInfo(locale, PostType.DECLINE.toName(), text);
935         postInfo.setSubj(subject);
936         postInfo.setPath(xpathId);
937         postInfo.setUser(user);
938         postInfo.setReplyTo(root /* replyTo */);
939         postInfo.setRoot(root);
940         postInfo.setValue(value == null ? "Abstain" : value); /* NOT the same as the requested value */
941         try {
942             doPostInternal(postInfo);
943         } catch (SurveyException e) {
944             SurveyLog.logException(e, "SurveyForum: autoPostReplyDecline root " + root);
945         }
946     }
947 
948     /**
949      * I request a vote for “X”, then I change to “Y” (or abstain)
950      * Auto-generated: I changed my vote to “Y”, disagreeing with my request. This topic is being closed. ⇒ Close
951      *
952      * @param locale
953      * @param user
954      * @param xpathId
955      * @param value
956      */
autoPostClose(CLDRLocale locale, User user, int xpathId, String value)957     private void autoPostClose(CLDRLocale locale, User user, int xpathId, String value) {
958         String dbValue = value == null ? "" : value;
959         Connection conn = null;
960         PreparedStatement pList = null;
961         String tableName = DBUtils.Table.FORUM_POSTS.toString();
962         Map<Integer, String> posts = new HashMap<>();
963         try {
964             conn = sm.dbUtils.getDBConnection();
965             pList = DBUtils.prepareStatement(conn, "pList",
966                 "SELECT id,subj FROM " + tableName
967                 + " WHERE is_open=true AND type=? AND loc=? AND xpath=? AND poster=? AND NOT value=?");
968             pList.setInt(1, PostType.REQUEST.toInt());
969             pList.setString(2, locale.toString());
970             pList.setInt(3, xpathId);
971             pList.setInt(4, user.id);
972             DBUtils.setStringUTF8(pList, 5, dbValue);
973             ResultSet rs = pList.executeQuery();
974             while (rs.next()) {
975                 posts.put(rs.getInt(1), DBUtils.getStringUTF8(rs, 2));
976             }
977             posts.forEach((root, subject) -> autoPostReplyClose(root, subject, locale, user, xpathId, value));
978          } catch (SQLException se) {
979             String complaint = "SurveyForum: autoPostClose - " + DBUtils.unchainSqlException(se);
980             SurveyLog.logException(se, complaint);
981         } finally {
982             DBUtils.close(pList, conn);
983         }
984     }
985 
autoPostReplyClose(int root, String subject, CLDRLocale locale, User user, int xpathId, String value)986     private void autoPostReplyClose(int root, String subject, CLDRLocale locale, User user,
987             int xpathId, String value) {
988         String abstainOrQuotedValue = value == null ? "Abstain" : "“" + value + "”";
989         String text = "(Auto-generated:) I changed my vote to " + abstainOrQuotedValue
990                 + ", disagreeing with my request. This topic is being closed.";
991         PostInfo postInfo = new PostInfo(locale, PostType.CLOSE.toName(), text);
992         postInfo.setSubj(subject);
993         postInfo.setPath(xpathId);
994         postInfo.setUser(user);
995         postInfo.setReplyTo(root /* replyTo */);
996         postInfo.setRoot(root);
997         postInfo.setValue(value == null ? "Abstain" : value); /* NOT the same as the requested value */
998         postInfo.setSendEmail(false);
999         try {
1000             doPostInternal(postInfo);
1001         } catch (SurveyException e) {
1002             SurveyLog.logException(e, "SurveyForum: autoPostReplyClose root " + root);
1003         }
1004     }
1005 
1006     /**
1007      * Respond to the user making a new forum post. Save the post in the database,
1008      * and send an email if appropriate.
1009      *
1010      * @param postInfo the post info
1011      *
1012      * @return the new post id, or <= 0 for failure
1013      * @throws SurveyException
1014      */
doPostInternal(PostInfo postInfo)1015     private Integer doPostInternal(PostInfo postInfo) throws SurveyException {
1016         if (!postInfo.isValid()) {
1017             SurveyLog.errln("Invalid postInfo in SurveyForum.doPostInternal");
1018             return 0;
1019         }
1020         PostType postType = postInfo.getType();
1021         User user = postInfo.getUser();
1022         if (!userCanUsePostType(user, postType, postInfo.getReplyTo())) {
1023             SurveyLog.errln("Post not allowed in SurveyForum.doPostInternal");
1024             return 0;
1025         }
1026         int postId = savePostToDb(postInfo);
1027 
1028         if (postInfo.getSendEmail()) {
1029             emailNotify(user, postInfo.getLocale(), postInfo.getPath(), postInfo.getSubj(), postInfo.getText(), postId);
1030         }
1031         return postId;
1032     }
1033 
1034     /**
1035      * Save a new post to the FORUM_POSTS table; if it's a CLOSE post,
1036      * also set is_open=false for all posts in this thread
1037      *
1038      * @param PostInfo the post info
1039      * @return the new post id, or <= 0 for failure
1040      * @throws SurveyException
1041      */
savePostToDb(PostInfo postInfo)1042     private int savePostToDb(PostInfo postInfo) throws SurveyException {
1043         int postId = 0;
1044         final CLDRLocale locale = postInfo.getLocale();
1045         final String localeStr = locale.toString();
1046         final int forumNumber = getForumNumber(locale);
1047         final User user = postInfo.getUser();
1048         final PostType type = postInfo.getType();
1049         final boolean open = (type == PostType.CLOSE) ? false : postInfo.getOpen();
1050         final int root = postInfo.getRoot();
1051         final String text = postInfo.getText().replaceAll("\r", "").replaceAll("\n", "<p>");
1052         try {
1053             Connection conn = null;
1054             PreparedStatement pAdd = null, pCloseThread = null;
1055             try {
1056                 conn = sm.dbUtils.getDBConnection();
1057                 if (type == PostType.CLOSE) {
1058                     pCloseThread = prepare_pCloseThread(conn);
1059                     pCloseThread.setInt(1, root);
1060                     pCloseThread.setInt(2, root);
1061                     pCloseThread.executeUpdate();
1062                 }
1063                 pAdd = prepare_pAdd(conn);
1064                 pAdd.setInt(1, user.id);
1065                 DBUtils.setStringUTF8(pAdd, 2, postInfo.getSubj());
1066                 DBUtils.setStringUTF8(pAdd, 3, text);
1067                 pAdd.setInt(4, forumNumber);
1068                 pAdd.setInt(5, postInfo.getReplyTo()); // record parent
1069                 pAdd.setString(6, localeStr); // real locale of item, not forum #
1070                 pAdd.setInt(7, postInfo.getPath());
1071                 pAdd.setString(8, SurveyMain.getNewVersion()); // version
1072                 pAdd.setInt(9, root);
1073                 pAdd.setInt(10, type.toInt());
1074                 pAdd.setBoolean(11, open);
1075                 DBUtils.setStringUTF8(pAdd, 12, postInfo.getValue());
1076 
1077                 int n = pAdd.executeUpdate();
1078                 if (postInfo.couldFlagOnLosing()) {
1079                     sm.getSTFactory().setFlag(conn, locale, postInfo.getPath(), user);
1080                     System.out.println("NOTE: flag was set on " + localeStr + " by " + user.toString());
1081                 }
1082 
1083                 conn.commit();
1084                 postId = DBUtils.getLastId(pAdd);
1085 
1086                 if (n != 1) {
1087                     throw new RuntimeException("Couldn't post to " + localeStr + " - update failed.");
1088                 }
1089             } finally {
1090                 DBUtils.close(pAdd, pCloseThread, conn);
1091             }
1092         } catch (SQLException se) {
1093             String complaint = "SurveyForum:  Couldn't add post to " + localeStr + " - " + DBUtils.unchainSqlException(se)
1094                 + " - pAdd";
1095             SurveyLog.logException(se, complaint);
1096             throw new SurveyException(ErrorCode.E_INTERNAL, complaint);
1097         }
1098         return postId;
1099     }
1100 
1101     public class PostInfo {
1102         private final CLDRLocale locale;
1103         private PostType type;
1104         private String text;
1105         private int xpathId = XPathTable.NO_XPATH;
1106         private String pathString = null;
1107         private int replyTo = NO_PARENT;
1108         private int root = NO_PARENT;
1109         private String subj = null;
1110         private String value = null;
1111         private boolean open = true;
1112         private boolean couldFlag = false;
1113         private UserRegistry.User user = null;
1114         private boolean sendEmail = true;
1115 
PostInfo(CLDRLocale locale, String postTypeStr, String text)1116         public PostInfo(CLDRLocale locale, String postTypeStr, String text) {
1117             this.locale = locale;
1118             this.type = PostType.fromName(postTypeStr, null);
1119             this.text = text;
1120         }
1121 
isValid()1122         public boolean isValid() {
1123             if (locale == null
1124                 || type == null
1125                 || text == null
1126                 || subj == null
1127                 || user == null) {
1128                 return false;
1129             }
1130             if ((replyTo == NO_PARENT) != (root == NO_PARENT)) {
1131                 return false;
1132             }
1133             if (value == null && (type == PostType.REQUEST || type == PostType.AGREE || type == PostType.DECLINE)) {
1134                 return false;
1135             }
1136             return true;
1137         }
1138 
1139         /*
1140          * Getters
1141          */
1142 
getPathStr()1143         public String getPathStr() {
1144             return pathString;
1145         }
1146 
getValue()1147         public String getValue() {
1148             return value;
1149         }
1150 
getOpen()1151         public boolean getOpen() {
1152             return open;
1153         }
1154 
getType()1155         public PostType getType() {
1156             return type;
1157         }
1158 
getRoot()1159         public int getRoot() {
1160             return root;
1161         }
1162 
getLocale()1163         public CLDRLocale getLocale() {
1164             return locale;
1165         }
1166 
couldFlagOnLosing()1167         public boolean couldFlagOnLosing() {
1168             return couldFlag;
1169         }
1170 
getPath()1171         public int getPath() {
1172             return xpathId;
1173         }
1174 
getText()1175         public String getText() {
1176             return text;
1177         }
1178 
getSubj()1179         public String getSubj() {
1180             return subj;
1181         }
1182 
getReplyTo()1183         public int getReplyTo() {
1184             return replyTo;
1185         }
1186 
getUser()1187         public User getUser() {
1188             return user;
1189         }
1190 
getSendEmail()1191         public boolean getSendEmail() {
1192             return sendEmail;
1193         }
1194 
1195         /*
1196          * Setters
1197          */
1198 
setRoot(int root)1199         public void setRoot(int root) {
1200             this.root = root;
1201         }
1202 
setSubj(String subj)1203         public void setSubj(String subj) {
1204             this.subj = subj;
1205         }
1206 
setPathString(String xpathStr)1207         public void setPathString(String xpathStr) {
1208             this.pathString = xpathStr;
1209         }
1210 
setReplyTo(int replyTo)1211         public void setReplyTo(int replyTo) {
1212             this.replyTo = replyTo;
1213         }
1214 
setUser(User user)1215         public void setUser(User user) {
1216             this.user = user;
1217         }
1218 
setPath(int base_xpath)1219         public void setPath(int base_xpath) {
1220             this.xpathId = base_xpath;
1221         }
1222 
setCouldFlagOnLosing(boolean couldFlag)1223         public void setCouldFlagOnLosing(boolean couldFlag) {
1224             this.couldFlag = couldFlag;
1225         }
1226 
setText(String text)1227         public void setText(String text) {
1228             this.text = text;
1229         }
1230 
setOpen(boolean open)1231         public void setOpen(boolean open) {
1232             this.open = open;
1233         }
1234 
setValue(String value)1235         public void setValue(String value) {
1236             this.value = value;
1237         }
1238 
setSendEmail(boolean sendEmail)1239         public void setSendEmail(boolean sendEmail) {
1240             this.sendEmail = sendEmail;
1241         }
1242     }
1243 
1244     /**
1245      * Status values associated with forum posts and threads
1246      */
1247     enum PostType {
1248         CLOSE(0, "Close"),
1249         DISCUSS(1, "Discuss"),
1250         REQUEST(2, "Request"),
1251         AGREE(3, "Agree"),
1252         DECLINE(4, "Decline");
1253 
PostType(int id, String name)1254         PostType(int id, String name) {
1255             this.id = id;
1256             this.name = name;
1257         }
1258 
1259         private final int id;
1260         private final String name;
1261 
1262         /**
1263          * Get the integer id for this PostType
1264          *
1265          * @return the id
1266          */
toInt()1267         public int toInt() {
1268             return id;
1269         }
1270 
1271         /**
1272          * Get the name for this PostType
1273          *
1274          * @return the name
1275          */
toName()1276         public String toName() {
1277             return name;
1278         }
1279 
1280         /**
1281          * Get a PostType value from its name, or if the name is not associated with
1282          * a PostType value, use the given default PostType
1283          *
1284          * @param i
1285          * @param defaultStatus
1286          * @return the PostType
1287          */
fromInt(int i, PostType defaultStatus)1288         public static PostType fromInt(int i, PostType defaultStatus) {
1289             for (PostType s : PostType.values()) {
1290                 if (s.id == i) {
1291                     return s;
1292                 }
1293             }
1294             return defaultStatus;
1295         }
1296 
1297         /**
1298          * Get a PostType value from its name, or if the name is not associated with
1299          * a PostType value, use the given default PostType
1300          *
1301          * @param name
1302          * @param defaultStatus
1303          * @return the PostType
1304          */
fromName(String name, PostType defaultStatus)1305         public static PostType fromName(String name, PostType defaultStatus) {
1306             if (name != null) {
1307                 for (PostType s : PostType.values()) {
1308                     if (s.name.equals(name)) {
1309                         return s;
1310                     }
1311                 }
1312             }
1313             return defaultStatus;
1314         }
1315     }
1316 }
1317