Drupal: posílání zpráv do skupiny.
1.1 --- a/src/org/sonews/storage/DrupalMessage.java Wed Oct 19 21:40:51 2011 +0200
1.2 +++ b/src/org/sonews/storage/DrupalMessage.java Thu Oct 20 09:59:04 2011 +0200
1.3 @@ -18,6 +18,7 @@
1.4 package org.sonews.storage;
1.5
1.6 import java.io.BufferedReader;
1.7 +import java.io.ByteArrayInputStream;
1.8 import java.io.ByteArrayOutputStream;
1.9 import java.io.IOException;
1.10 import java.io.InputStream;
1.11 @@ -45,6 +46,7 @@
1.12 import javax.xml.transform.TransformerFactory;
1.13 import javax.xml.transform.stream.StreamResult;
1.14 import javax.xml.transform.stream.StreamSource;
1.15 +import org.sonews.daemon.NNTPConnection;
1.16 import org.sonews.util.io.Resource;
1.17
1.18 /**
1.19 @@ -66,6 +68,8 @@
1.20 private static final String XHTML_CONTENT_TYPE = "text/html; charset=" + CHARSET;
1.21 private static final String ZNAKČKA_KONCE_ŘÁDKU = "◆";
1.22 private String messageID;
1.23 + private Long parentID;
1.24 + private Long groupID;
1.25
1.26 /**
1.27 * Constructs MIME message from SQL result.
1.28 @@ -75,13 +79,14 @@
1.29 public DrupalMessage(ResultSet rs, String myDomain, boolean constructBody) throws SQLException, UnsupportedEncodingException, MessagingException {
1.30 super(Session.getDefaultInstance(System.getProperties()));
1.31
1.32 - addHeader("Message-id", constructMessageId(rs.getInt("id"), rs.getInt("group_id"), rs.getString("group_name"), myDomain));
1.33 + groupID = rs.getLong("group_id");
1.34 + addHeader("Message-id", constructMessageId(rs.getInt("id"), groupID, rs.getString("group_name"), myDomain));
1.35 addHeader("Newsgroups", rs.getString("group_name"));
1.36 setFrom(new InternetAddress(rs.getString("sender_email"), rs.getString("sender_name")));
1.37 setSubject(rs.getString("subject"));
1.38 setSentDate(new Date(rs.getLong("created")));
1.39
1.40 - int parentID = rs.getInt("parent_id");
1.41 + parentID = rs.getLong("parent_id");
1.42 if (parentID > 0) {
1.43 String parentMessageID = constructMessageId(parentID, rs.getInt("group_id"), rs.getString("group_name"), myDomain);
1.44 addHeader("In-Reply-To", parentMessageID);
1.45 @@ -116,6 +121,38 @@
1.46 }
1.47 }
1.48
1.49 + /**
1.50 + * Constructs MIME message from article posted by user.
1.51 + * @param article article that came through NNTP.
1.52 + * @throws MessagingException
1.53 + */
1.54 + public DrupalMessage(Article article) throws MessagingException {
1.55 + super(Session.getDefaultInstance(System.getProperties()), serializeArticle(article));
1.56 +
1.57 + String[] parentHeaders = getHeader("In-Reply-To");
1.58 + if (parentHeaders.length == 1) {
1.59 + String parentMessageID = parentHeaders[0];
1.60 + parentID = parseArticleID(parentMessageID);
1.61 + groupID = parseGroupID(parentMessageID);
1.62 + } else {
1.63 + throw new MessagingException("Message posted by user must have exactly one In-Reply-To header.");
1.64 + }
1.65 + }
1.66 +
1.67 + private static InputStream serializeArticle(Article a) {
1.68 + byte articleHeaders[] = a.getHeaderSource().getBytes();
1.69 + byte delimiter[] = (NNTPConnection.NEWLINE + NNTPConnection.NEWLINE).getBytes();
1.70 + byte body[] = a.getBody();
1.71 +
1.72 + byte message[] = new byte[articleHeaders.length + delimiter.length + body.length];
1.73 +
1.74 + System.arraycopy(articleHeaders, 0, message, 0, articleHeaders.length);
1.75 + System.arraycopy(delimiter, 0, message, articleHeaders.length, delimiter.length);
1.76 + System.arraycopy(body, 0, message, articleHeaders.length + delimiter.length, body.length);
1.77 +
1.78 + return new ByteArrayInputStream(message);
1.79 + }
1.80 +
1.81 private String readPlainText(ResultSet rs, String xhtmlText) {
1.82 try {
1.83 TransformerFactory tf = TransformerFactory.newInstance();
1.84 @@ -255,7 +292,7 @@
1.85 return výsledek.toString();
1.86 }
1.87
1.88 - private static String constructMessageId(int articleID, int groupID, String groupName, String domainName) {
1.89 + public static String constructMessageId(long articleID, long groupID, String groupName, String domainName) {
1.90 StringBuilder sb = new StringBuilder();
1.91 sb.append("<");
1.92 sb.append(articleID);
1.93 @@ -269,6 +306,54 @@
1.94 return sb.toString();
1.95 }
1.96
1.97 + /**
1.98 + * @return article ID of parent of this message | or null, if this is root article and not reply to another one
1.99 + */
1.100 + public Long getParentID() {
1.101 + return parentID;
1.102 + }
1.103 +
1.104 + /**
1.105 + * @return group ID of this message | or null, if this message is not reply to any other one – which is wrong because we have to know the group
1.106 + */
1.107 + public Long getGroupID() {
1.108 + return groupID;
1.109 + }
1.110 +
1.111 + /**
1.112 + *
1.113 + * @param messageID <{0}-{1}-{2}@domain.tld> where {0} is nntp_id and {1} is group_id and {2} is group_name
1.114 + * @return array where [0] = nntp_id and [1] = group_id and [2] = group_name or returns null if messageID is invalid
1.115 + */
1.116 + private static String[] parseMessageID(String messageID) {
1.117 + if (messageID.matches("<[0-9]+\\-[0-9]+\\-[a-z0-9\\.]+@.+>")) {
1.118 + return messageID.substring(1).split("@")[0].split("\\-");
1.119 + } else {
1.120 + return null;
1.121 + }
1.122 + }
1.123 +
1.124 + public static Long parseArticleID(String messageID) {
1.125 + String[] localPart = parseMessageID(messageID);
1.126 + if (localPart == null) {
1.127 + return null;
1.128 + } else {
1.129 + return Long.parseLong(localPart[0]);
1.130 + }
1.131 + }
1.132 +
1.133 + public static Long parseGroupID(String messageID) {
1.134 + String[] localPart = parseMessageID(messageID);
1.135 + if (localPart == null) {
1.136 + return null;
1.137 + } else {
1.138 + return Long.parseLong(localPart[1]);
1.139 + // If needed:
1.140 + // parseGroupName() will be same as this method, just with:
1.141 + // return localPart[2];
1.142 + }
1.143 + }
1.144 +
1.145 @Override
1.146 public void setHeader(String name, String value) throws MessagingException {
1.147 super.setHeader(name, value);
2.1 --- a/src/org/sonews/storage/impl/DrupalDatabase.java Wed Oct 19 21:40:51 2011 +0200
2.2 +++ b/src/org/sonews/storage/impl/DrupalDatabase.java Thu Oct 20 09:59:04 2011 +0200
2.3 @@ -28,12 +28,13 @@
2.4 import java.util.logging.Level;
2.5 import java.util.logging.Logger;
2.6 import org.sonews.config.Config;
2.7 -import org.sonews.daemon.Connections;
2.8 import org.sonews.feed.Subscription;
2.9 import org.sonews.storage.Article;
2.10 import org.sonews.storage.ArticleHead;
2.11 import org.sonews.storage.DrupalArticle;
2.12 import org.sonews.storage.DrupalMessage;
2.13 +import static org.sonews.storage.DrupalMessage.parseArticleID;
2.14 +import static org.sonews.storage.DrupalMessage.parseGroupID;
2.15 import org.sonews.storage.Group;
2.16 import org.sonews.storage.Storage;
2.17 import org.sonews.storage.StorageBackendException;
2.18 @@ -68,6 +69,18 @@
2.19 String password = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "");
2.20 conn = DriverManager.getConnection(url, username, password);
2.21
2.22 + /**
2.23 + * Kódování češtiny:
2.24 + * SET NAMES utf8 → dobrá čeština
2.25 + * Client characterset: utf8
2.26 + * Conn. characterset: utf8
2.27 + * SET CHARACTER SET utf8; → dobrá čeština jen pro SLECT, ale při volání funkce se zmrší
2.28 + * Client characterset: utf8
2.29 + * Conn. characterset: latin1
2.30 + *
2.31 + * Správné řešení:
2.32 + * V JDBC URL musí být: ?useUnicode=true&characterEncoding=UTF-8
2.33 + */
2.34 conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
2.35 if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) {
2.36 log.warning("Database is NOT fully serializable!");
2.37 @@ -98,40 +111,6 @@
2.38 }
2.39 }
2.40
2.41 - /**
2.42 - *
2.43 - * @param messageID <{0}-{1}-{2}@domain.tld> where {0} is nntp_id and {1} is group_id and {2} is group_name
2.44 - * @return array where [0] = nntp_id and [1] = group_id and [2] = group_name or returns null if messageID is invalid
2.45 - */
2.46 - private static String[] parseMessageID(String messageID) {
2.47 - if (messageID.matches("<[0-9]+\\-[0-9]+\\-[a-z0-9\\.]+@.+>")) {
2.48 - return messageID.substring(1).split("@")[0].split("\\-");
2.49 - } else {
2.50 - return null;
2.51 - }
2.52 - }
2.53 -
2.54 - private static Long parseArticleID(String messageID) {
2.55 - String[] localPart = parseMessageID(messageID);
2.56 - if (localPart == null) {
2.57 - return null;
2.58 - } else {
2.59 - return Long.parseLong(localPart[0]);
2.60 - }
2.61 - }
2.62 -
2.63 - private static Long parseGroupID(String messageID) {
2.64 - String[] localPart = parseMessageID(messageID);
2.65 - if (localPart == null) {
2.66 - return null;
2.67 - } else {
2.68 - return Long.parseLong(localPart[1]);
2.69 - // If needed:
2.70 - // parseGroupName() will be same as this method, just with:
2.71 - // return localPart[2];
2.72 - }
2.73 - }
2.74 -
2.75 @Override
2.76 public List<Group> getGroups() throws StorageBackendException {
2.77 PreparedStatement ps = null;
2.78 @@ -397,7 +376,8 @@
2.79 * (but should not be thrown if only bad thing is wrong username or password)
2.80 */
2.81 @Override
2.82 - public boolean authenticateUser(String username, char[] password) throws StorageBackendException {
2.83 + public boolean authenticateUser(String username,
2.84 + char[] password) throws StorageBackendException {
2.85 PreparedStatement ps = null;
2.86 ResultSet rs = null;
2.87 try {
2.88 @@ -414,14 +394,81 @@
2.89 }
2.90 }
2.91
2.92 + /**
2.93 + * Validates article and if OK, calls {@link #insertArticle(java.lang.String, java.lang.String, java.lang.String, java.lang.Long, java.lang.Long) }
2.94 + * @param article
2.95 + * @throws StorageBackendException
2.96 + */
2.97 @Override
2.98 - public void addArticle(Article art) throws StorageBackendException {
2.99 - if (art.getAuthenticatedUser() == null) {
2.100 + public void addArticle(Article article) throws StorageBackendException {
2.101 + if (article.getAuthenticatedUser() == null) {
2.102 log.log(Level.SEVERE, "User was not authenticated, so his article was rejected.");
2.103 throw new StorageBackendException("User must be authenticated to post articles");
2.104 } else {
2.105 + try {
2.106 + DrupalMessage m = new DrupalMessage(article);
2.107
2.108 - log.log(Level.INFO, "User ''{0}'' has posted an article", art.getAuthenticatedUser());
2.109 + Long parentID = m.getParentID();
2.110 + Long groupID = m.getGroupID();
2.111 +
2.112 + if (parentID == null || groupID == null) {
2.113 + throw new StorageBackendException("No valid In-Reply-To header was found → rejecting posted message.");
2.114 + } else {
2.115 + if (m.isMimeType("text/plain")) {
2.116 + Object content = m.getContent();
2.117 + if (content instanceof String) {
2.118 + String subject = m.getSubject();
2.119 + String text = (String) content;
2.120 +
2.121 + /**
2.122 + * TODO: validovat a transformovat text
2.123 + * (v současné době se o to stará až Drupal při výstupu)
2.124 + */
2.125 + if (subject == null || subject.length() < 1) {
2.126 + subject = text.substring(0, Math.min(10, text.length()));
2.127 + }
2.128 +
2.129 + insertArticle(article.getAuthenticatedUser(), subject, text, parentID, groupID);
2.130 + log.log(Level.INFO, "User ''{0}'' has posted an article", article.getAuthenticatedUser());
2.131 + }
2.132 + } else {
2.133 + throw new StorageBackendException("Only text/plain messages are supported for now – post it as plain text please.");
2.134 + }
2.135 + }
2.136 + } catch (Exception e) {
2.137 + throw new StorageBackendException(e);
2.138 + }
2.139 + }
2.140 + }
2.141 +
2.142 + /**
2.143 + * Physically stores article in database.
2.144 + * @param subject
2.145 + * @param text
2.146 + * @param parentID
2.147 + * @param groupID
2.148 + */
2.149 + private void insertArticle(String sender, String subject, String text, Long parentID, Long groupID) throws StorageBackendException {
2.150 + PreparedStatement ps = null;
2.151 + ResultSet rs = null;
2.152 + try {
2.153 + ps = conn.prepareStatement("SELECT nntp_post_article(?, ?, ?, ?, ?)");
2.154 +
2.155 + ps.setString(1, sender);
2.156 + ps.setString(2, subject);
2.157 + ps.setString(3, text);
2.158 + ps.setLong(4, parentID);
2.159 + ps.setLong(5, groupID);
2.160 +
2.161 + rs = ps.executeQuery();
2.162 + rs.next();
2.163 +
2.164 + Long articleID = rs.getLong(1);
2.165 + log.log(Level.INFO, "Article was succesfully stored as {0}", articleID);
2.166 + } catch (Exception e) {
2.167 + throw new StorageBackendException(e);
2.168 + } finally {
2.169 + close(null, ps, rs);
2.170 }
2.171 }
2.172