3 * see AUTHORS for the list of contributors
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 package org.sonews.storage.impl;
20 import java.io.UnsupportedEncodingException;
21 import java.sql.Connection;
22 import java.sql.DriverManager;
23 import java.sql.PreparedStatement;
24 import java.sql.ResultSet;
25 import java.sql.SQLException;
26 import java.sql.Statement;
27 import java.text.SimpleDateFormat;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.Date;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 import javax.mail.internet.MailDateFormat;
36 import javax.mail.internet.MimeUtility;
37 import org.apache.commons.codec.net.BCodec;
38 import org.apache.commons.codec.net.QuotedPrintableCodec;
39 import org.sonews.config.Config;
40 import org.sonews.feed.Subscription;
41 import org.sonews.storage.Article;
42 import org.sonews.storage.ArticleHead;
43 import org.sonews.storage.Group;
44 import org.sonews.storage.Storage;
45 import org.sonews.storage.StorageBackendException;
46 import org.sonews.util.Pair;
50 * @author František Kučera (frantovo.cz)
52 public class DrupalDatabase implements Storage {
54 private static final Logger log = Logger.getLogger(DrupalDatabase.class.getName());
55 public static final String CHARSET = "UTF-8";
56 public static final String CRLF = "\r\n";
57 public static final int MAX_RESTARTS = 2;
58 /** How many times the database connection was reinitialized */
59 protected int restarts = 0;
60 protected Connection conn = null;
61 private QuotedPrintableCodec qpc = new QuotedPrintableCodec(CHARSET);
62 private SimpleDateFormat RFC822_DATE = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US);
63 // TODO: správná doména
64 private String myDomain = "kinderporno.cz";
66 public DrupalDatabase() throws StorageBackendException {
70 private void connectDatabase() throws StorageBackendException {
72 // Load database driver
73 String driverClass = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DBMSDRIVER, "java.lang.Object");
74 Class.forName(driverClass);
76 // Establish database connection
77 String url = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, "<not specified>");
78 String username = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root");
79 String password = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "");
80 conn = DriverManager.getConnection(url, username, password);
82 conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
83 if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) {
84 log.warning("Database is NOT fully serializable!");
86 } catch (Exception e) {
87 throw new StorageBackendException(e);
91 protected static void close(Connection connection, Statement statement, ResultSet resultSet) {
92 if (resultSet != null) {
95 } catch (Exception e) {
98 if (statement != null) {
101 } catch (Exception e) {
104 if (connection != null) {
107 } catch (Exception e) {
114 * @param messageID <{0}-{1}-{2}@domain.tld> where {0} is nntp_id and {1} is group_id and {2} is group_name
115 * @return array where [0] = nntp_id and [1] = group_id and [2] = group_name or returns null if messageID is invalid
117 private static String[] parseMessageID(String messageID) {
118 if (messageID.matches("<[0-9]+\\-[0-9]+\\-[a-z0-9\\.]+@.+>")) {
119 return messageID.substring(1).split("@")[0].split("\\-");
125 private static Long parseArticleID(String messageID) {
126 String[] localPart = parseMessageID(messageID);
127 if (localPart == null) {
130 return Long.parseLong(localPart[0]);
134 private static Long parseGroupID(String messageID) {
135 String[] localPart = parseMessageID(messageID);
136 if (localPart == null) {
139 return Long.parseLong(localPart[1]);
143 private static String parseGroupName(String messageID) {
144 String[] localPart = parseMessageID(messageID);
145 if (localPart == null) {
152 private static String constructMessageId(int articleID, int groupID, String groupName, String domainName) {
153 StringBuilder sb = new StringBuilder();
155 sb.append(articleID);
159 sb.append(groupName);
161 sb.append(domainName);
163 return sb.toString();
168 * @param sb header list to be appended with new header. List must be terminated by line end.
169 * @param key header name (without : and space)
170 * @param value header value
171 * @param encode true if value should be encoded/escaped before appending
172 * @throws UnsupportedEncodingException
174 private static void addHeader(StringBuilder sb, String key, String value, boolean encode) throws UnsupportedEncodingException {
178 sb.append(MimeUtility.encodeWord(value));
185 private String constructHeaders(ResultSet rs) throws SQLException, UnsupportedEncodingException {
186 StringBuilder sb = new StringBuilder();
188 addHeader(sb, "Message-id", constructMessageId(rs.getInt("id"), rs.getInt("group_id"), rs.getString("group_name"), myDomain), false);
189 addHeader(sb, "From", MimeUtility.encodeWord(rs.getString("sender_name")) + " <>", false);
190 addHeader(sb, "Subject", rs.getString("subject"), true);
191 /** TODO: správný formát data: */
192 addHeader(sb, "Date", RFC822_DATE.format(new Date(rs.getLong("created"))), false);
193 addHeader(sb, "Content-Type", "text/html; charset=" + CHARSET, false);
194 addHeader(sb, "Content-Transfer-Encoding", "quoted-printable", false);
195 //addHeader(sb, "Content-Transfer-Encoding", "base64", false);
197 Integer parentID = rs.getInt("parent_id");
198 if (parentID != null && parentID > 0) {
199 String parentMessageID = constructMessageId(parentID, rs.getInt("group_id"), rs.getString("group_name"), myDomain);
200 addHeader(sb, "In-Reply-To", parentMessageID, false);
201 addHeader(sb, "References", parentMessageID, false);
204 return sb.toString();
208 public List<Group> getGroups() throws StorageBackendException {
209 PreparedStatement ps = null;
212 ps = conn.prepareStatement("SELECT * FROM nntp_group");
213 rs = ps.executeQuery();
214 List<Group> skupiny = new ArrayList<Group>();
217 skupiny.add(new Group(rs.getString("name"), rs.getInt("id"), Group.READONLY));
221 } catch (Exception e) {
222 throw new StorageBackendException(e);
229 public Group getGroup(String name) throws StorageBackendException {
230 PreparedStatement ps = null;
233 ps = conn.prepareStatement("SELECT * FROM nntp_group WHERE name = ?");
234 ps.setString(1, name);
235 rs = ps.executeQuery();
238 return new Group(rs.getString("name"), rs.getInt("id"), Group.READONLY);
242 } catch (Exception e) {
243 throw new StorageBackendException(e);
250 public boolean isGroupExisting(String groupname) throws StorageBackendException {
251 return getGroup(groupname) != null;
255 public Article getArticle(String messageID) throws StorageBackendException {
256 Long articleID = parseArticleID(messageID);
257 Long groupID = parseGroupID(messageID);
259 if (articleID == null || groupID == null) {
260 log.log(Level.SEVERE, "Invalid messageID: {0}", new Object[]{messageID});
263 return getArticle(articleID, groupID);
268 public Article getArticle(long articleID, long groupID) throws StorageBackendException {
269 PreparedStatement ps = null;
272 ps = conn.prepareStatement("SELECT * FROM nntp_article WHERE id = ? AND group_id = ?");
273 ps.setLong(1, articleID);
274 ps.setLong(2, groupID);
275 rs = ps.executeQuery();
278 String headers = constructHeaders(rs);
280 BCodec bc = new BCodec(CHARSET);
281 byte[] body = qpc.encode(rs.getString("text")).getBytes();
282 //byte[] body = bc.encode(rs.getString("text")).getBytes();
284 return new Article(headers, body);
288 } catch (Exception e) {
289 throw new StorageBackendException(e);
296 public List<Pair<Long, ArticleHead>> getArticleHeads(Group group, long first, long last) throws StorageBackendException {
297 PreparedStatement ps = null;
300 // TODO: je nutné řazení?
301 ps = conn.prepareStatement("SELECT * FROM nntp_article WHERE group_id = ? AND id >= ? AND id <= ? ORDER BY id");
302 ps.setLong(1, group.getInternalID());
303 ps.setLong(2, first);
305 rs = ps.executeQuery();
307 List<Pair<Long, ArticleHead>> heads = new ArrayList<Pair<Long, ArticleHead>>();
310 String headers = constructHeaders(rs);
311 heads.add(new Pair<Long, ArticleHead>(rs.getLong("id"), new ArticleHead(headers)));
315 } catch (Exception e) {
316 throw new StorageBackendException(e);
323 public List<Pair<Long, String>> getArticleHeaders(Group group, long start, long end, String header, String pattern) throws StorageBackendException {
324 log.log(Level.SEVERE, "TODO: getArticleHeaders {0} / {1} / {2} / {3} / {4}", new Object[]{group, start, end, header, pattern});
326 return Collections.emptyList();
330 public long getArticleIndex(Article article, Group group) throws StorageBackendException {
331 Long id = parseArticleID(article.getMessageID());
333 throw new StorageBackendException("Invalid messageID: " + article.getMessageID());
340 public List<Long> getArticleNumbers(long groupID) throws StorageBackendException {
341 PreparedStatement ps = null;
344 ps = conn.prepareStatement("SELECT id FROM nntp_article WHERE group_id = ?");
345 ps.setLong(1, groupID);
346 rs = ps.executeQuery();
347 List<Long> articleNumbers = new ArrayList<Long>();
349 articleNumbers.add(rs.getLong(1));
351 return articleNumbers;
352 } catch (Exception e) {
353 throw new StorageBackendException(e);
360 public int getFirstArticleNumber(Group group) throws StorageBackendException {
361 PreparedStatement ps = null;
364 ps = conn.prepareStatement("SELECT min(id) FROM nntp_article WHERE group_id = ?");
365 ps.setLong(1, group.getInternalID());
366 rs = ps.executeQuery();
369 } catch (Exception e) {
370 throw new StorageBackendException(e);
377 public int getLastArticleNumber(Group group) throws StorageBackendException {
378 PreparedStatement ps = null;
381 ps = conn.prepareStatement("SELECT max(id) FROM nntp_article WHERE group_id = ?");
382 ps.setLong(1, group.getInternalID());
383 rs = ps.executeQuery();
386 } catch (Exception e) {
387 throw new StorageBackendException(e);
394 public boolean isArticleExisting(String messageID) throws StorageBackendException {
395 Long articleID = parseArticleID(messageID);
396 Long groupID = parseGroupID(messageID);
398 if (articleID == null || groupID == null) {
401 PreparedStatement ps = null;
404 ps = conn.prepareStatement("SELECT count(*) FROM nntp_article WHERE id = ? AND group_id = ?");
405 ps.setLong(1, articleID);
406 ps.setLong(2, groupID);
407 rs = ps.executeQuery();
410 return rs.getInt(1) == 1;
411 } catch (Exception e) {
412 throw new StorageBackendException(e);
420 // --- zatím neimplementovat ---
423 public void addArticle(Article art) throws StorageBackendException {
424 log.log(Level.SEVERE, "TODO: addArticle {0}", new Object[]{art});
428 public void addEvent(long timestamp, int type, long groupID) throws StorageBackendException {
429 log.log(Level.SEVERE, "TODO: addEvent {0} / {1} / {2}", new Object[]{timestamp, type, groupID});
433 public void addGroup(String groupname, int flags) throws StorageBackendException {
434 log.log(Level.SEVERE, "TODO: addGroup {0} / {1}", new Object[]{groupname, flags});
438 public int countArticles() throws StorageBackendException {
439 PreparedStatement ps = null;
442 ps = conn.prepareStatement("SELECT count(*) FROM nntp_article");
443 rs = ps.executeQuery();
446 } catch (Exception e) {
447 throw new StorageBackendException(e);
454 public int countGroups() throws StorageBackendException {
455 PreparedStatement ps = null;
458 ps = conn.prepareStatement("SELECT count(*) FROM nntp_group");
459 rs = ps.executeQuery();
462 } catch (Exception e) {
463 throw new StorageBackendException(e);
470 public void delete(String messageID) throws StorageBackendException {
471 log.log(Level.SEVERE, "TODO: delete {0}", new Object[]{messageID});
475 public String getConfigValue(String key) throws StorageBackendException {
476 //log.log(Level.SEVERE, "TODO: getConfigValue {0}", new Object[]{key});
481 public int getEventsCount(int eventType, long startTimestamp, long endTimestamp, Group group) throws StorageBackendException {
482 log.log(Level.SEVERE, "TODO: getEventsCount {0} / {1} / {2} / {3}", new Object[]{eventType, startTimestamp, endTimestamp, group});
487 public double getEventsPerHour(int key, long gid) throws StorageBackendException {
488 log.log(Level.SEVERE, "TODO: getEventsPerHour {0} / {1}", new Object[]{key, gid});
493 public List<String> getGroupsForList(String listAddress) throws StorageBackendException {
494 log.log(Level.SEVERE, "TODO: getGroupsForList {0}", new Object[]{listAddress});
495 return Collections.emptyList();
499 public List<String> getListsForGroup(String groupname) throws StorageBackendException {
500 log.log(Level.SEVERE, "TODO: getListsForGroup {0}", new Object[]{groupname});
501 return Collections.emptyList();
505 public String getOldestArticle() throws StorageBackendException {
506 log.log(Level.SEVERE, "TODO: getOldestArticle");
511 public int getPostingsCount(String groupname) throws StorageBackendException {
512 PreparedStatement ps = null;
515 ps = conn.prepareStatement("SELECT count(*) FROM nntp_article WHERE group_name = ?");
516 ps.setString(1, groupname);
517 rs = ps.executeQuery();
520 } catch (Exception e) {
521 throw new StorageBackendException(e);
528 public List<Subscription> getSubscriptions(int type) throws StorageBackendException {
529 log.log(Level.SEVERE, "TODO: getSubscriptions {0}", new Object[]{type});
530 return Collections.emptyList();
534 public void purgeGroup(Group group) throws StorageBackendException {
535 log.log(Level.SEVERE, "TODO: purgeGroup {0}", new Object[]{group});
539 public void setConfigValue(String key, String value) throws StorageBackendException {
540 log.log(Level.SEVERE, "TODO: setConfigValue {0} = {1}", new Object[]{key, value});
544 public boolean update(Article article) throws StorageBackendException {
545 log.log(Level.SEVERE, "TODO: update {0}", new Object[]{article});
546 throw new StorageBackendException("Not implemented yet.");
550 public boolean update(Group group) throws StorageBackendException {
551 log.log(Level.SEVERE, "TODO: update {0}", new Object[]{group});
552 throw new StorageBackendException("Not implemented yet.");