Drupal: zprávy jsou multipart/alternative – jak prostý text, tak XHTML. TODO: filtry prostého textu a XHTML
zatím se do všech zpráv vkládá stejný vycpávkový text.
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/src/org/sonews/storage/DrupalArticle.java Wed Oct 12 00:11:25 2011 +0200
1.3 @@ -0,0 +1,46 @@
1.4 +/*
1.5 + * SONEWS News Server
1.6 + * see AUTHORS for the list of contributors
1.7 + *
1.8 + * This program is free software: you can redistribute it and/or modify
1.9 + * it under the terms of the GNU General Public License as published by
1.10 + * the Free Software Foundation, either version 3 of the License, or
1.11 + * (at your option) any later version.
1.12 + *
1.13 + * This program is distributed in the hope that it will be useful,
1.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
1.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1.16 + * GNU General Public License for more details.
1.17 + *
1.18 + * You should have received a copy of the GNU General Public License
1.19 + * along with this program. If not, see <http://www.gnu.org/licenses/>.
1.20 + */
1.21 +package org.sonews.storage;
1.22 +
1.23 +import java.io.IOException;
1.24 +import java.util.Enumeration;
1.25 +import javax.mail.Header;
1.26 +import javax.mail.MessagingException;
1.27 +import javax.mail.internet.InternetHeaders;
1.28 +
1.29 +/**
1.30 + *
1.31 + * @author František Kučera (frantovo.cz)
1.32 + */
1.33 +public class DrupalArticle extends Article {
1.34 +
1.35 + public DrupalArticle(DrupalMessage msg) throws MessagingException, IOException {
1.36 + headers = new InternetHeaders();
1.37 +
1.38 + /** In order to have all headers (like MIME type) */
1.39 + msg.saveChanges();
1.40 +
1.41 + for (Enumeration e = msg.getAllHeaders(); e.hasMoreElements();) {
1.42 + final Header header = (Header) e.nextElement();
1.43 + this.headers.addHeader(header.getName(), header.getValue());
1.44 + }
1.45 +
1.46 + setBody(msg.getBody());
1.47 + validateHeaders();
1.48 + }
1.49 +}
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/src/org/sonews/storage/DrupalMessage.java Wed Oct 12 00:11:25 2011 +0200
2.3 @@ -0,0 +1,193 @@
2.4 +/*
2.5 + * SONEWS News Server
2.6 + * see AUTHORS for the list of contributors
2.7 + *
2.8 + * This program is free software: you can redistribute it and/or modify
2.9 + * it under the terms of the GNU General Public License as published by
2.10 + * the Free Software Foundation, either version 3 of the License, or
2.11 + * (at your option) any later version.
2.12 + *
2.13 + * This program is distributed in the hope that it will be useful,
2.14 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
2.15 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2.16 + * GNU General Public License for more details.
2.17 + *
2.18 + * You should have received a copy of the GNU General Public License
2.19 + * along with this program. If not, see <http://www.gnu.org/licenses/>.
2.20 + */
2.21 +package org.sonews.storage;
2.22 +
2.23 +import java.io.ByteArrayOutputStream;
2.24 +import java.io.IOException;
2.25 +import java.io.UnsupportedEncodingException;
2.26 +import java.sql.ResultSet;
2.27 +import java.sql.SQLException;
2.28 +import java.util.ArrayList;
2.29 +import java.util.Date;
2.30 +import java.util.Enumeration;
2.31 +import javax.mail.Header;
2.32 +import javax.mail.MessagingException;
2.33 +import javax.mail.Multipart;
2.34 +import javax.mail.Session;
2.35 +import javax.mail.internet.InternetAddress;
2.36 +import javax.mail.internet.MimeBodyPart;
2.37 +import javax.mail.internet.MimeMessage;
2.38 +import javax.mail.internet.MimeMultipart;
2.39 +
2.40 +/**
2.41 + * This is MimeMessage which enables custom Message-ID header
2.42 + * (this header will not be overwritten by the default one like in MimeMessage).
2.43 + *
2.44 + * Also add header and body separate serialization.
2.45 + *
2.46 + * And can be deserialized from SQL ResultSet
2.47 + *
2.48 + * @author František Kučera (frantovo.cz)
2.49 + */
2.50 +public class DrupalMessage extends MimeMessage {
2.51 +
2.52 + private static final String MESSAGE_ID_HEADER = "Message-ID";
2.53 + private static final String CRLF = "\r\n";
2.54 + public static final String CHARSET = "UTF-8";
2.55 + private static final String XHTML_CONTENT_TYPE = "text/html; charset=" + CHARSET;
2.56 + private String messageID;
2.57 +
2.58 + /**
2.59 + * Constructs MIME message from SQL result.
2.60 + * @param rs ResultSet containing message data. No {@link ResultSet#next()} will be called, just values from current row will be read.
2.61 + * @param constructBody true if whole message should be constructed | false if we need only message headers (body will be dummy).
2.62 + */
2.63 + public DrupalMessage(ResultSet rs, String myDomain, boolean constructBody) throws SQLException, UnsupportedEncodingException, MessagingException {
2.64 + super(Session.getDefaultInstance(System.getProperties()));
2.65 +
2.66 + addHeader("Message-id", constructMessageId(rs.getInt("id"), rs.getInt("group_id"), rs.getString("group_name"), myDomain));
2.67 + addHeader("Newsgroups", rs.getString("group_name"));
2.68 + setFrom(new InternetAddress("anonym@example.com", rs.getString("sender_name")));
2.69 + setSubject(rs.getString("subject"));
2.70 + setSentDate(new Date(rs.getLong("created")));
2.71 +
2.72 + Integer parentID = rs.getInt("parent_id");
2.73 + if (parentID != null && parentID > 0) {
2.74 + String parentMessageID = constructMessageId(parentID, rs.getInt("group_id"), rs.getString("group_name"), myDomain);
2.75 + addHeader("In-Reply-To", parentMessageID);
2.76 + addHeader("References", parentMessageID);
2.77 + }
2.78 +
2.79 + if (constructBody) {
2.80 + Multipart multipart = new MimeMultipart("alternative");
2.81 + setContent(multipart);
2.82 +
2.83 + /** TODO: Plain text part */
2.84 + MimeBodyPart textPart = new MimeBodyPart();
2.85 + multipart.addBodyPart(textPart);
2.86 + textPart.setText(readPlainText(rs));
2.87 +
2.88 + /** TODO: XHTML part */
2.89 + MimeBodyPart htmlPart = new MimeBodyPart();
2.90 + multipart.addBodyPart(htmlPart);
2.91 + htmlPart.setContent(readXhtmlText(rs), XHTML_CONTENT_TYPE);
2.92 + } else {
2.93 + setText("");
2.94 + }
2.95 + }
2.96 +
2.97 + private String readPlainText(ResultSet rs) {
2.98 + /**
2.99 + * TODO: převést na prostý text
2.100 + */
2.101 + return "TODO: obyčejný text";
2.102 + }
2.103 +
2.104 + private String readXhtmlText(ResultSet rs) {
2.105 + /**
2.106 + * TODO: převést na XHTML
2.107 + */
2.108 + return "<html><body>TODO: tady bude nějaký <strong>(X)HTML</strong></body></html>";
2.109 + }
2.110 +
2.111 + private static String constructMessageId(int articleID, int groupID, String groupName, String domainName) {
2.112 + StringBuilder sb = new StringBuilder();
2.113 + sb.append("<");
2.114 + sb.append(articleID);
2.115 + sb.append("-");
2.116 + sb.append(groupID);
2.117 + sb.append("-");
2.118 + sb.append(groupName);
2.119 + sb.append("@");
2.120 + sb.append(domainName);
2.121 + sb.append(">");
2.122 + return sb.toString();
2.123 + }
2.124 +
2.125 + @Override
2.126 + public void setHeader(String name, String value) throws MessagingException {
2.127 + super.setHeader(name, value);
2.128 +
2.129 + if (MESSAGE_ID_HEADER.equalsIgnoreCase(name)) {
2.130 + messageID = value;
2.131 + }
2.132 + }
2.133 +
2.134 + @Override
2.135 + public final void addHeader(String name, String value) throws MessagingException {
2.136 + super.addHeader(name, value);
2.137 +
2.138 + if (MESSAGE_ID_HEADER.equalsIgnoreCase(name)) {
2.139 + messageID = value;
2.140 + }
2.141 + }
2.142 +
2.143 + @Override
2.144 + public void removeHeader(String name) throws MessagingException {
2.145 + super.removeHeader(name);
2.146 +
2.147 + if (MESSAGE_ID_HEADER.equalsIgnoreCase(name)) {
2.148 + messageID = null;
2.149 + }
2.150 + }
2.151 +
2.152 + public void setMessageID(String messageID) {
2.153 + this.messageID = messageID;
2.154 + }
2.155 +
2.156 + @Override
2.157 + protected void updateMessageID() throws MessagingException {
2.158 + if (messageID == null) {
2.159 + super.updateMessageID();
2.160 + } else {
2.161 + setHeader(MESSAGE_ID_HEADER, messageID);
2.162 + }
2.163 + }
2.164 +
2.165 + /**
2.166 + * Call {@link #saveChanges()} before this method, if you want all headers including such ones like:
2.167 + *
2.168 + * <pre>MIME-Version: 1.0
2.169 + *Content-Type: multipart/alternative;</pre>
2.170 + *
2.171 + * @return serialized headers
2.172 + * @throws MessagingException if getAllHeaders() fails
2.173 + */
2.174 + public String getHeaders() throws MessagingException {
2.175 + StringBuilder sb = new StringBuilder();
2.176 + for (Enumeration eh = getAllHeaderLines(); eh.hasMoreElements();) {
2.177 + sb.append(eh.nextElement());
2.178 + sb.append(CRLF);
2.179 + }
2.180 + return sb.toString();
2.181 + }
2.182 +
2.183 + public byte[] getBody() throws IOException, MessagingException {
2.184 + saveChanges();
2.185 +
2.186 + ArrayList<String> skipHeaders = new ArrayList<String>();
2.187 + for (Enumeration eh = getAllHeaders(); eh.hasMoreElements();) {
2.188 + Header h = (Header) eh.nextElement();
2.189 + skipHeaders.add(h.getName());
2.190 + }
2.191 +
2.192 + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
2.193 + writeTo(baos, skipHeaders.toArray(new String[skipHeaders.size()]));
2.194 + return baos.toByteArray();
2.195 + }
2.196 +}
3.1 --- a/src/org/sonews/storage/impl/DrupalDatabase.java Tue Oct 11 16:34:17 2011 +0200
3.2 +++ b/src/org/sonews/storage/impl/DrupalDatabase.java Wed Oct 12 00:11:25 2011 +0200
3.3 @@ -17,29 +17,26 @@
3.4 */
3.5 package org.sonews.storage.impl;
3.6
3.7 -import java.io.UnsupportedEncodingException;
3.8 import java.sql.Connection;
3.9 import java.sql.DriverManager;
3.10 import java.sql.PreparedStatement;
3.11 import java.sql.ResultSet;
3.12 -import java.sql.SQLException;
3.13 import java.sql.Statement;
3.14 import java.text.SimpleDateFormat;
3.15 import java.util.ArrayList;
3.16 import java.util.Collections;
3.17 -import java.util.Date;
3.18 import java.util.List;
3.19 import java.util.Locale;
3.20 import java.util.logging.Level;
3.21 import java.util.logging.Logger;
3.22 -import javax.mail.internet.MailDateFormat;
3.23 -import javax.mail.internet.MimeUtility;
3.24 -import org.apache.commons.codec.net.BCodec;
3.25 +import javax.mail.Message;
3.26 import org.apache.commons.codec.net.QuotedPrintableCodec;
3.27 import org.sonews.config.Config;
3.28 import org.sonews.feed.Subscription;
3.29 import org.sonews.storage.Article;
3.30 import org.sonews.storage.ArticleHead;
3.31 +import org.sonews.storage.DrupalArticle;
3.32 +import org.sonews.storage.DrupalMessage;
3.33 import org.sonews.storage.Group;
3.34 import org.sonews.storage.Storage;
3.35 import org.sonews.storage.StorageBackendException;
3.36 @@ -54,14 +51,11 @@
3.37 private static final Logger log = Logger.getLogger(DrupalDatabase.class.getName());
3.38 public static final String CHARSET = "UTF-8";
3.39 public static final String CRLF = "\r\n";
3.40 - public static final int MAX_RESTARTS = 2;
3.41 - /** How many times the database connection was reinitialized */
3.42 - protected int restarts = 0;
3.43 protected Connection conn = null;
3.44 private QuotedPrintableCodec qpc = new QuotedPrintableCodec(CHARSET);
3.45 private SimpleDateFormat RFC822_DATE = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US);
3.46 // TODO: správná doména
3.47 - private String myDomain = "kinderporno.cz";
3.48 + private String myDomain = "nntp.i1984.cz";
3.49
3.50 public DrupalDatabase() throws StorageBackendException {
3.51 connectDatabase();
3.52 @@ -137,73 +131,12 @@
3.53 return null;
3.54 } else {
3.55 return Long.parseLong(localPart[1]);
3.56 + // If needed:
3.57 + // parseGroupName() will be same as this method, just with:
3.58 + // return localPart[2];
3.59 }
3.60 }
3.61
3.62 - private static String parseGroupName(String messageID) {
3.63 - String[] localPart = parseMessageID(messageID);
3.64 - if (localPart == null) {
3.65 - return null;
3.66 - } else {
3.67 - return localPart[2];
3.68 - }
3.69 - }
3.70 -
3.71 - private static String constructMessageId(int articleID, int groupID, String groupName, String domainName) {
3.72 - StringBuilder sb = new StringBuilder();
3.73 - sb.append("<");
3.74 - sb.append(articleID);
3.75 - sb.append("-");
3.76 - sb.append(groupID);
3.77 - sb.append("-");
3.78 - sb.append(groupName);
3.79 - sb.append("@");
3.80 - sb.append(domainName);
3.81 - sb.append(">");
3.82 - return sb.toString();
3.83 - }
3.84 -
3.85 - /**
3.86 - *
3.87 - * @param sb header list to be appended with new header. List must be terminated by line end.
3.88 - * @param key header name (without : and space)
3.89 - * @param value header value
3.90 - * @param encode true if value should be encoded/escaped before appending
3.91 - * @throws UnsupportedEncodingException
3.92 - */
3.93 - private static void addHeader(StringBuilder sb, String key, String value, boolean encode) throws UnsupportedEncodingException {
3.94 - sb.append(key);
3.95 - sb.append(": ");
3.96 - if (encode) {
3.97 - sb.append(MimeUtility.encodeWord(value));
3.98 - } else {
3.99 - sb.append(value);
3.100 - }
3.101 - sb.append(CRLF);
3.102 - }
3.103 -
3.104 - private String constructHeaders(ResultSet rs) throws SQLException, UnsupportedEncodingException {
3.105 - StringBuilder sb = new StringBuilder();
3.106 -
3.107 - addHeader(sb, "Message-id", constructMessageId(rs.getInt("id"), rs.getInt("group_id"), rs.getString("group_name"), myDomain), false);
3.108 - addHeader(sb, "From", MimeUtility.encodeWord(rs.getString("sender_name")) + " <>", false);
3.109 - addHeader(sb, "Subject", rs.getString("subject"), true);
3.110 - /** TODO: správný formát data: */
3.111 - addHeader(sb, "Date", RFC822_DATE.format(new Date(rs.getLong("created"))), false);
3.112 - addHeader(sb, "Content-Type", "text/html; charset=" + CHARSET, false);
3.113 - addHeader(sb, "Content-Transfer-Encoding", "quoted-printable", false);
3.114 - //addHeader(sb, "Content-Transfer-Encoding", "base64", false);
3.115 -
3.116 - Integer parentID = rs.getInt("parent_id");
3.117 - if (parentID != null && parentID > 0) {
3.118 - String parentMessageID = constructMessageId(parentID, rs.getInt("group_id"), rs.getString("group_name"), myDomain);
3.119 - addHeader(sb, "In-Reply-To", parentMessageID, false);
3.120 - addHeader(sb, "References", parentMessageID, false);
3.121 - }
3.122 -
3.123 - return sb.toString();
3.124 - }
3.125 -
3.126 @Override
3.127 public List<Group> getGroups() throws StorageBackendException {
3.128 PreparedStatement ps = null;
3.129 @@ -275,13 +208,8 @@
3.130 rs = ps.executeQuery();
3.131
3.132 if (rs.next()) {
3.133 - String headers = constructHeaders(rs);
3.134 - // TODO: fold?
3.135 - BCodec bc = new BCodec(CHARSET);
3.136 - byte[] body = qpc.encode(rs.getString("text")).getBytes();
3.137 - //byte[] body = bc.encode(rs.getString("text")).getBytes();
3.138 -
3.139 - return new Article(headers, body);
3.140 + DrupalMessage m = new DrupalMessage(rs, myDomain, true);
3.141 + return new DrupalArticle(m);
3.142 } else {
3.143 return null;
3.144 }
3.145 @@ -297,7 +225,6 @@
3.146 PreparedStatement ps = null;
3.147 ResultSet rs = null;
3.148 try {
3.149 - // TODO: je nutné řazení?
3.150 ps = conn.prepareStatement("SELECT * FROM nntp_article WHERE group_id = ? AND id >= ? AND id <= ? ORDER BY id");
3.151 ps.setLong(1, group.getInternalID());
3.152 ps.setLong(2, first);
3.153 @@ -307,7 +234,8 @@
3.154 List<Pair<Long, ArticleHead>> heads = new ArrayList<Pair<Long, ArticleHead>>();
3.155
3.156 while (rs.next()) {
3.157 - String headers = constructHeaders(rs);
3.158 + DrupalMessage m = new DrupalMessage(rs, myDomain, false);
3.159 + String headers = m.getHeaders();
3.160 heads.add(new Pair<Long, ArticleHead>(rs.getLong("id"), new ArticleHead(headers)));
3.161 }
3.162
3.163 @@ -320,13 +248,6 @@
3.164 }
3.165
3.166 @Override
3.167 - public List<Pair<Long, String>> getArticleHeaders(Group group, long start, long end, String header, String pattern) throws StorageBackendException {
3.168 - log.log(Level.SEVERE, "TODO: getArticleHeaders {0} / {1} / {2} / {3} / {4}", new Object[]{group, start, end, header, pattern});
3.169 - /** TODO: */
3.170 - return Collections.emptyList();
3.171 - }
3.172 -
3.173 - @Override
3.174 public long getArticleIndex(Article article, Group group) throws StorageBackendException {
3.175 Long id = parseArticleID(article.getMessageID());
3.176 if (id == null) {
3.177 @@ -416,24 +337,6 @@
3.178 }
3.179 }
3.180
3.181 - //
3.182 - // --- zatím neimplementovat ---
3.183 - //
3.184 - @Override
3.185 - public void addArticle(Article art) throws StorageBackendException {
3.186 - log.log(Level.SEVERE, "TODO: addArticle {0}", new Object[]{art});
3.187 - }
3.188 -
3.189 - @Override
3.190 - public void addEvent(long timestamp, int type, long groupID) throws StorageBackendException {
3.191 - log.log(Level.SEVERE, "TODO: addEvent {0} / {1} / {2}", new Object[]{timestamp, type, groupID});
3.192 - }
3.193 -
3.194 - @Override
3.195 - public void addGroup(String groupname, int flags) throws StorageBackendException {
3.196 - log.log(Level.SEVERE, "TODO: addGroup {0} / {1}", new Object[]{groupname, flags});
3.197 - }
3.198 -
3.199 @Override
3.200 public int countArticles() throws StorageBackendException {
3.201 PreparedStatement ps = null;
3.202 @@ -467,6 +370,45 @@
3.203 }
3.204
3.205 @Override
3.206 + public int getPostingsCount(String groupname) throws StorageBackendException {
3.207 + PreparedStatement ps = null;
3.208 + ResultSet rs = null;
3.209 + try {
3.210 + ps = conn.prepareStatement("SELECT count(*) FROM nntp_article WHERE group_name = ?");
3.211 + ps.setString(1, groupname);
3.212 + rs = ps.executeQuery();
3.213 + rs.next();
3.214 + return rs.getInt(1);
3.215 + } catch (Exception e) {
3.216 + throw new StorageBackendException(e);
3.217 + } finally {
3.218 + close(null, ps, rs);
3.219 + }
3.220 + }
3.221 +
3.222 + @Override
3.223 + public List<Pair<Long, String>> getArticleHeaders(Group group, long start, long end, String header, String pattern) throws StorageBackendException {
3.224 + log.log(Level.SEVERE, "TODO: getArticleHeaders {0} / {1} / {2} / {3} / {4}", new Object[]{group, start, end, header, pattern});
3.225 + /** TODO: */
3.226 + return Collections.emptyList();
3.227 + }
3.228 +
3.229 + @Override
3.230 + public void addArticle(Article art) throws StorageBackendException {
3.231 + log.log(Level.SEVERE, "TODO: addArticle {0}", new Object[]{art});
3.232 + }
3.233 +
3.234 + @Override
3.235 + public void addEvent(long timestamp, int type, long groupID) throws StorageBackendException {
3.236 + log.log(Level.SEVERE, "TODO: addEvent {0} / {1} / {2}", new Object[]{timestamp, type, groupID});
3.237 + }
3.238 +
3.239 + @Override
3.240 + public void addGroup(String groupname, int flags) throws StorageBackendException {
3.241 + log.log(Level.SEVERE, "TODO: addGroup {0} / {1}", new Object[]{groupname, flags});
3.242 + }
3.243 +
3.244 + @Override
3.245 public void delete(String messageID) throws StorageBackendException {
3.246 log.log(Level.SEVERE, "TODO: delete {0}", new Object[]{messageID});
3.247 }
3.248 @@ -478,6 +420,11 @@
3.249 }
3.250
3.251 @Override
3.252 + public void setConfigValue(String key, String value) throws StorageBackendException {
3.253 + log.log(Level.SEVERE, "TODO: setConfigValue {0} = {1}", new Object[]{key, value});
3.254 + }
3.255 +
3.256 + @Override
3.257 public int getEventsCount(int eventType, long startTimestamp, long endTimestamp, Group group) throws StorageBackendException {
3.258 log.log(Level.SEVERE, "TODO: getEventsCount {0} / {1} / {2} / {3}", new Object[]{eventType, startTimestamp, endTimestamp, group});
3.259 return 0;
3.260 @@ -508,23 +455,6 @@
3.261 }
3.262
3.263 @Override
3.264 - public int getPostingsCount(String groupname) throws StorageBackendException {
3.265 - PreparedStatement ps = null;
3.266 - ResultSet rs = null;
3.267 - try {
3.268 - ps = conn.prepareStatement("SELECT count(*) FROM nntp_article WHERE group_name = ?");
3.269 - ps.setString(1, groupname);
3.270 - rs = ps.executeQuery();
3.271 - rs.next();
3.272 - return rs.getInt(1);
3.273 - } catch (Exception e) {
3.274 - throw new StorageBackendException(e);
3.275 - } finally {
3.276 - close(null, ps, rs);
3.277 - }
3.278 - }
3.279 -
3.280 - @Override
3.281 public List<Subscription> getSubscriptions(int type) throws StorageBackendException {
3.282 log.log(Level.SEVERE, "TODO: getSubscriptions {0}", new Object[]{type});
3.283 return Collections.emptyList();
3.284 @@ -536,11 +466,6 @@
3.285 }
3.286
3.287 @Override
3.288 - public void setConfigValue(String key, String value) throws StorageBackendException {
3.289 - log.log(Level.SEVERE, "TODO: setConfigValue {0} = {1}", new Object[]{key, value});
3.290 - }
3.291 -
3.292 - @Override
3.293 public boolean update(Article article) throws StorageBackendException {
3.294 log.log(Level.SEVERE, "TODO: update {0}", new Object[]{article});
3.295 throw new StorageBackendException("Not implemented yet.");