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.sql.Connection;
21 import java.sql.DriverManager;
22 import java.sql.PreparedStatement;
23 import java.sql.ResultSet;
24 import java.sql.Statement;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.List;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
30 import org.sonews.config.Config;
31 import org.sonews.feed.Subscription;
32 import org.sonews.storage.Article;
33 import org.sonews.storage.ArticleHead;
34 import org.sonews.storage.DrupalArticle;
35 import org.sonews.storage.DrupalMessage;
36 import static org.sonews.storage.DrupalMessage.parseArticleID;
37 import static org.sonews.storage.DrupalMessage.parseGroupID;
38 import org.sonews.storage.Group;
39 import org.sonews.storage.Storage;
40 import org.sonews.storage.StorageBackendException;
41 import org.sonews.util.Pair;
45 * @author František Kučera (frantovo.cz)
47 public class DrupalDatabase implements Storage {
49 private static final Logger log = Logger.getLogger(DrupalDatabase.class.getName());
50 public static final String CHARSET = "UTF-8";
51 public static final String CRLF = "\r\n";
52 protected Connection conn = null;
53 // TODO: správná doména
54 private String myDomain = "nntp.i1984.cz";
56 public DrupalDatabase() throws StorageBackendException {
60 private void connectDatabase() throws StorageBackendException {
62 // Load database driver
63 String driverClass = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DBMSDRIVER, "java.lang.Object");
64 Class.forName(driverClass);
66 // Establish database connection
67 String url = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_DATABASE, "<not specified>");
68 String username = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_USER, "root");
69 String password = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "");
70 conn = DriverManager.getConnection(url, username, password);
74 * SET NAMES utf8 → dobrá čeština
75 * Client characterset: utf8
76 * Conn. characterset: utf8
77 * SET CHARACTER SET utf8; → dobrá čeština jen pro SLECT, ale při volání funkce se zmrší
78 * Client characterset: utf8
79 * Conn. characterset: latin1
82 * V JDBC URL musí být: ?useUnicode=true&characterEncoding=UTF-8
84 conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
85 if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) {
86 log.warning("Database is NOT fully serializable!");
88 } catch (Exception e) {
89 throw new StorageBackendException(e);
93 protected static void close(Connection connection, Statement statement, ResultSet resultSet) {
94 if (resultSet != null) {
97 } catch (Exception e) {
100 if (statement != null) {
103 } catch (Exception e) {
106 if (connection != null) {
109 } catch (Exception e) {
115 public List<Group> getGroups() throws StorageBackendException {
116 PreparedStatement ps = null;
119 ps = conn.prepareStatement("SELECT * FROM nntp_group");
120 rs = ps.executeQuery();
121 List<Group> skupiny = new ArrayList<Group>();
124 skupiny.add(new Group(rs.getString("name"), rs.getInt("id"), Group.READONLY));
128 } catch (Exception e) {
129 throw new StorageBackendException(e);
136 public Group getGroup(String name) throws StorageBackendException {
137 PreparedStatement ps = null;
140 ps = conn.prepareStatement("SELECT * FROM nntp_group WHERE name = ?");
141 ps.setString(1, name);
142 rs = ps.executeQuery();
145 return new Group(rs.getString("name"), rs.getInt("id"), Group.READONLY);
149 } catch (Exception e) {
150 throw new StorageBackendException(e);
157 public boolean isGroupExisting(String groupname) throws StorageBackendException {
158 return getGroup(groupname) != null;
162 public Article getArticle(String messageID) throws StorageBackendException {
163 Long articleID = parseArticleID(messageID);
164 Long groupID = parseGroupID(messageID);
166 if (articleID == null || groupID == null) {
167 log.log(Level.SEVERE, "Invalid messageID: {0}", new Object[]{messageID});
170 return getArticle(articleID, groupID);
175 public Article getArticle(long articleID, long groupID) throws StorageBackendException {
176 PreparedStatement ps = null;
179 ps = conn.prepareStatement("SELECT * FROM nntp_article WHERE id = ? AND group_id = ?");
180 ps.setLong(1, articleID);
181 ps.setLong(2, groupID);
182 rs = ps.executeQuery();
185 DrupalMessage m = new DrupalMessage(rs, myDomain, true);
186 return new DrupalArticle(m);
190 } catch (Exception e) {
191 throw new StorageBackendException(e);
198 public List<Pair<Long, ArticleHead>> getArticleHeads(Group group, long first, long last) throws StorageBackendException {
199 PreparedStatement ps = null;
202 ps = conn.prepareStatement("SELECT * FROM nntp_article WHERE group_id = ? AND id >= ? AND id <= ? ORDER BY id");
203 ps.setLong(1, group.getInternalID());
204 ps.setLong(2, first);
206 rs = ps.executeQuery();
208 List<Pair<Long, ArticleHead>> heads = new ArrayList<Pair<Long, ArticleHead>>();
211 DrupalMessage m = new DrupalMessage(rs, myDomain, false);
212 String headers = m.getHeaders();
213 heads.add(new Pair<Long, ArticleHead>(rs.getLong("id"), new ArticleHead(headers)));
217 } catch (Exception e) {
218 throw new StorageBackendException(e);
225 public long getArticleIndex(Article article, Group group) throws StorageBackendException {
226 Long id = parseArticleID(article.getMessageID());
228 throw new StorageBackendException("Invalid messageID: " + article.getMessageID());
235 public List<Long> getArticleNumbers(long groupID) throws StorageBackendException {
236 PreparedStatement ps = null;
239 ps = conn.prepareStatement("SELECT id FROM nntp_article WHERE group_id = ?");
240 ps.setLong(1, groupID);
241 rs = ps.executeQuery();
242 List<Long> articleNumbers = new ArrayList<Long>();
244 articleNumbers.add(rs.getLong(1));
246 return articleNumbers;
247 } catch (Exception e) {
248 throw new StorageBackendException(e);
255 public int getFirstArticleNumber(Group group) throws StorageBackendException {
256 PreparedStatement ps = null;
259 ps = conn.prepareStatement("SELECT min(id) FROM nntp_article WHERE group_id = ?");
260 ps.setLong(1, group.getInternalID());
261 rs = ps.executeQuery();
264 } catch (Exception e) {
265 throw new StorageBackendException(e);
272 public int getLastArticleNumber(Group group) throws StorageBackendException {
273 PreparedStatement ps = null;
276 ps = conn.prepareStatement("SELECT max(id) FROM nntp_article WHERE group_id = ?");
277 ps.setLong(1, group.getInternalID());
278 rs = ps.executeQuery();
281 } catch (Exception e) {
282 throw new StorageBackendException(e);
289 public boolean isArticleExisting(String messageID) throws StorageBackendException {
290 Long articleID = parseArticleID(messageID);
291 Long groupID = parseGroupID(messageID);
293 if (articleID == null || groupID == null) {
296 PreparedStatement ps = null;
299 ps = conn.prepareStatement("SELECT count(*) FROM nntp_article WHERE id = ? AND group_id = ?");
300 ps.setLong(1, articleID);
301 ps.setLong(2, groupID);
302 rs = ps.executeQuery();
305 return rs.getInt(1) == 1;
306 } catch (Exception e) {
307 throw new StorageBackendException(e);
315 public int countArticles() throws StorageBackendException {
316 PreparedStatement ps = null;
319 ps = conn.prepareStatement("SELECT count(*) FROM nntp_article");
320 rs = ps.executeQuery();
323 } catch (Exception e) {
324 throw new StorageBackendException(e);
331 public int countGroups() throws StorageBackendException {
332 PreparedStatement ps = null;
335 ps = conn.prepareStatement("SELECT count(*) FROM nntp_group");
336 rs = ps.executeQuery();
339 } catch (Exception e) {
340 throw new StorageBackendException(e);
347 public int getPostingsCount(String groupname) throws StorageBackendException {
348 PreparedStatement ps = null;
351 ps = conn.prepareStatement("SELECT count(*) FROM nntp_article WHERE group_name = ?");
352 ps.setString(1, groupname);
353 rs = ps.executeQuery();
356 } catch (Exception e) {
357 throw new StorageBackendException(e);
364 public List<Pair<Long, String>> getArticleHeaders(Group group, long start, long end, String header, String pattern) throws StorageBackendException {
365 log.log(Level.SEVERE, "TODO: getArticleHeaders {0} / {1} / {2} / {3} / {4}", new Object[]{group, start, end, header, pattern});
367 return Collections.emptyList();
371 * Checks username and password.
374 * @return true if credentials are valid | false otherwise
375 * @throws StorageBackendException it there is any error during authentication process
376 * (but should not be thrown if only bad thing is wrong username or password)
379 public boolean authenticateUser(String username,
380 char[] password) throws StorageBackendException {
381 PreparedStatement ps = null;
384 ps = conn.prepareStatement("SELECT nntp_login(?, ?)");
385 ps.setString(1, username);
386 ps.setString(2, String.copyValueOf(password));
387 rs = ps.executeQuery();
389 return rs.getInt(1) == 1;
390 } catch (Exception e) {
391 throw new StorageBackendException(e);
398 * Validates article and if OK, calls {@link #insertArticle(java.lang.String, java.lang.String, java.lang.String, java.lang.Long, java.lang.Long) }
400 * @throws StorageBackendException
403 public void addArticle(Article article) throws StorageBackendException {
404 if (article.getAuthenticatedUser() == null) {
405 log.log(Level.SEVERE, "User was not authenticated, so his article was rejected.");
406 throw new StorageBackendException("User must be authenticated to post articles");
409 DrupalMessage m = new DrupalMessage(article);
411 Long parentID = m.getParentID();
412 Long groupID = m.getGroupID();
414 if (parentID == null || groupID == null) {
415 throw new StorageBackendException("No valid In-Reply-To header was found → rejecting posted message.");
418 String subject = m.getSubject();
419 String text = m.getBodyXhtmlFragment();
421 if (subject == null || subject.length() < 1) {
422 String plainText = m.getBodyPlainText();
423 subject = plainText.substring(0, Math.min(32, plainText.length()));
424 if (subject.length() < plainText.length()) {
425 subject = subject + "…";
429 insertArticle(article.getAuthenticatedUser(), subject, text, parentID, groupID);
430 log.log(Level.INFO, "User ''{0}'' has posted an article", article.getAuthenticatedUser());
432 } catch (Exception e) {
433 throw new StorageBackendException(e);
439 * Physically stores article in database.
445 private void insertArticle(String sender, String subject, String text, Long parentID, Long groupID) throws StorageBackendException {
446 PreparedStatement ps = null;
449 ps = conn.prepareStatement("SELECT nntp_post_article(?, ?, ?, ?, ?)");
451 ps.setString(1, sender);
452 ps.setString(2, subject);
453 ps.setString(3, text);
454 ps.setLong(4, parentID);
455 ps.setLong(5, groupID);
457 rs = ps.executeQuery();
460 Long articleID = rs.getLong(1);
461 log.log(Level.INFO, "Article was succesfully stored as {0}", articleID);
462 } catch (Exception e) {
463 throw new StorageBackendException(e);
470 public void addEvent(long timestamp, int type, long groupID) throws StorageBackendException {
471 log.log(Level.SEVERE, "TODO: addEvent {0} / {1} / {2}", new Object[]{timestamp, type, groupID});
475 public void addGroup(String groupname, int flags) throws StorageBackendException {
476 log.log(Level.SEVERE, "TODO: addGroup {0} / {1}", new Object[]{groupname, flags});
480 public void delete(String messageID) throws StorageBackendException {
481 log.log(Level.SEVERE, "TODO: delete {0}", new Object[]{messageID});
485 public String getConfigValue(String key) throws StorageBackendException {
486 //log.log(Level.SEVERE, "TODO: getConfigValue {0}", new Object[]{key});
491 public void setConfigValue(String key, String value) throws StorageBackendException {
492 log.log(Level.SEVERE, "TODO: setConfigValue {0} = {1}", new Object[]{key, value});
496 public int getEventsCount(int eventType, long startTimestamp, long endTimestamp, Group group) throws StorageBackendException {
497 log.log(Level.SEVERE, "TODO: getEventsCount {0} / {1} / {2} / {3}", new Object[]{eventType, startTimestamp, endTimestamp, group});
502 public double getEventsPerHour(int key, long gid) throws StorageBackendException {
503 log.log(Level.SEVERE, "TODO: getEventsPerHour {0} / {1}", new Object[]{key, gid});
508 public List<String> getGroupsForList(String listAddress) throws StorageBackendException {
509 log.log(Level.SEVERE, "TODO: getGroupsForList {0}", new Object[]{listAddress});
510 return Collections.emptyList();
514 public List<String> getListsForGroup(String groupname) throws StorageBackendException {
515 log.log(Level.SEVERE, "TODO: getListsForGroup {0}", new Object[]{groupname});
516 return Collections.emptyList();
520 public String getOldestArticle() throws StorageBackendException {
521 log.log(Level.SEVERE, "TODO: getOldestArticle");
526 public List<Subscription> getSubscriptions(int type) throws StorageBackendException {
527 log.log(Level.SEVERE, "TODO: getSubscriptions {0}", new Object[]{type});
528 return Collections.emptyList();
532 public void purgeGroup(Group group) throws StorageBackendException {
533 log.log(Level.SEVERE, "TODO: purgeGroup {0}", new Object[]{group});
537 public boolean update(Article article) throws StorageBackendException {
538 log.log(Level.SEVERE, "TODO: update {0}", new Object[]{article});
539 throw new StorageBackendException("Not implemented yet.");
543 public boolean update(Group group) throws StorageBackendException {
544 log.log(Level.SEVERE, "TODO: update {0}", new Object[]{group});
545 throw new StorageBackendException("Not implemented yet.");