# HG changeset patch
# User František Kučera <franta-hg@frantovo.cz>
# Date 1319097544 -7200
# Node ID d843b4fee5dc222eafb777655d122adcb45278bf
# Parent  d54786065fa34753f3d919935b95e30a8855753e
Drupal: posílání zpráv do skupiny.

diff -r d54786065fa3 -r d843b4fee5dc src/org/sonews/storage/DrupalMessage.java
--- a/src/org/sonews/storage/DrupalMessage.java	Wed Oct 19 21:40:51 2011 +0200
+++ b/src/org/sonews/storage/DrupalMessage.java	Thu Oct 20 09:59:04 2011 +0200
@@ -18,6 +18,7 @@
 package org.sonews.storage;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -45,6 +46,7 @@
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.stream.StreamResult;
 import javax.xml.transform.stream.StreamSource;
+import org.sonews.daemon.NNTPConnection;
 import org.sonews.util.io.Resource;
 
 /**
@@ -66,6 +68,8 @@
 	private static final String XHTML_CONTENT_TYPE = "text/html; charset=" + CHARSET;
 	private static final String ZNAKČKA_KONCE_ŘÁDKU = "◆";
 	private String messageID;
+	private Long parentID;
+	private Long groupID;
 
 	/**
 	 * Constructs MIME message from SQL result.
@@ -75,13 +79,14 @@
 	public DrupalMessage(ResultSet rs, String myDomain, boolean constructBody) throws SQLException, UnsupportedEncodingException, MessagingException {
 		super(Session.getDefaultInstance(System.getProperties()));
 
-		addHeader("Message-id", constructMessageId(rs.getInt("id"), rs.getInt("group_id"), rs.getString("group_name"), myDomain));
+		groupID = rs.getLong("group_id");
+		addHeader("Message-id", constructMessageId(rs.getInt("id"), groupID, rs.getString("group_name"), myDomain));
 		addHeader("Newsgroups", rs.getString("group_name"));
 		setFrom(new InternetAddress(rs.getString("sender_email"), rs.getString("sender_name")));
 		setSubject(rs.getString("subject"));
 		setSentDate(new Date(rs.getLong("created")));
 
-		int parentID = rs.getInt("parent_id");
+		parentID = rs.getLong("parent_id");
 		if (parentID > 0) {
 			String parentMessageID = constructMessageId(parentID, rs.getInt("group_id"), rs.getString("group_name"), myDomain);
 			addHeader("In-Reply-To", parentMessageID);
@@ -116,6 +121,38 @@
 		}
 	}
 
+	/**
+	 * Constructs MIME message from article posted by user.
+	 * @param article article that came through NNTP.
+	 * @throws MessagingException 
+	 */
+	public DrupalMessage(Article article) throws MessagingException {
+		super(Session.getDefaultInstance(System.getProperties()), serializeArticle(article));
+
+		String[] parentHeaders = getHeader("In-Reply-To");
+		if (parentHeaders.length == 1) {
+			String parentMessageID = parentHeaders[0];
+			parentID = parseArticleID(parentMessageID);
+			groupID = parseGroupID(parentMessageID);
+		} else {
+			throw new MessagingException("Message posted by user must have exactly one In-Reply-To header.");
+		}
+	}
+
+	private static InputStream serializeArticle(Article a) {
+		byte articleHeaders[] = a.getHeaderSource().getBytes();
+		byte delimiter[] = (NNTPConnection.NEWLINE + NNTPConnection.NEWLINE).getBytes();
+		byte body[] = a.getBody();
+
+		byte message[] = new byte[articleHeaders.length + delimiter.length + body.length];
+
+		System.arraycopy(articleHeaders, 0, message, 0, articleHeaders.length);
+		System.arraycopy(delimiter, 0, message, articleHeaders.length, delimiter.length);
+		System.arraycopy(body, 0, message, articleHeaders.length + delimiter.length, body.length);
+
+		return new ByteArrayInputStream(message);
+	}
+
 	private String readPlainText(ResultSet rs, String xhtmlText) {
 		try {
 			TransformerFactory tf = TransformerFactory.newInstance();
@@ -255,7 +292,7 @@
 		return výsledek.toString();
 	}
 
-	private static String constructMessageId(int articleID, int groupID, String groupName, String domainName) {
+	public static String constructMessageId(long articleID, long groupID, String groupName, String domainName) {
 		StringBuilder sb = new StringBuilder();
 		sb.append("<");
 		sb.append(articleID);
@@ -269,6 +306,54 @@
 		return sb.toString();
 	}
 
+	/**
+	 * @return article ID of parent of this message | or null, if this is root article and not reply to another one
+	 */
+	public Long getParentID() {
+		return parentID;
+	}
+
+	/**
+	 * @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
+	 */
+	public Long getGroupID() {
+		return groupID;
+	}
+
+	/**
+	 * 
+	 * @param messageID &lt;{0}-{1}-{2}@domain.tld&gt; where {0} is nntp_id and {1} is group_id and {2} is group_name
+	 * @return array where [0] = nntp_id and [1] = group_id and [2] = group_name or returns null if messageID is invalid
+	 */
+	private static String[] parseMessageID(String messageID) {
+		if (messageID.matches("<[0-9]+\\-[0-9]+\\-[a-z0-9\\.]+@.+>")) {
+			return messageID.substring(1).split("@")[0].split("\\-");
+		} else {
+			return null;
+		}
+	}
+
+	public static Long parseArticleID(String messageID) {
+		String[] localPart = parseMessageID(messageID);
+		if (localPart == null) {
+			return null;
+		} else {
+			return Long.parseLong(localPart[0]);
+		}
+	}
+
+	public static Long parseGroupID(String messageID) {
+		String[] localPart = parseMessageID(messageID);
+		if (localPart == null) {
+			return null;
+		} else {
+			return Long.parseLong(localPart[1]);
+			// If needed:
+			// parseGroupName() will be same as this method, just with:
+			// return localPart[2];
+		}
+	}
+
 	@Override
 	public void setHeader(String name, String value) throws MessagingException {
 		super.setHeader(name, value);
diff -r d54786065fa3 -r d843b4fee5dc src/org/sonews/storage/impl/DrupalDatabase.java
--- a/src/org/sonews/storage/impl/DrupalDatabase.java	Wed Oct 19 21:40:51 2011 +0200
+++ b/src/org/sonews/storage/impl/DrupalDatabase.java	Thu Oct 20 09:59:04 2011 +0200
@@ -28,12 +28,13 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.sonews.config.Config;
-import org.sonews.daemon.Connections;
 import org.sonews.feed.Subscription;
 import org.sonews.storage.Article;
 import org.sonews.storage.ArticleHead;
 import org.sonews.storage.DrupalArticle;
 import org.sonews.storage.DrupalMessage;
+import static org.sonews.storage.DrupalMessage.parseArticleID;
+import static org.sonews.storage.DrupalMessage.parseGroupID;
 import org.sonews.storage.Group;
 import org.sonews.storage.Storage;
 import org.sonews.storage.StorageBackendException;
@@ -68,6 +69,18 @@
 			String password = Config.inst().get(Config.LEVEL_FILE, Config.STORAGE_PASSWORD, "");
 			conn = DriverManager.getConnection(url, username, password);
 
+			/**
+			 * Kódování češtiny:
+			 * SET NAMES utf8 → dobrá čeština
+			 *		Client characterset:    utf8
+			 *		Conn.  characterset:    utf8
+			 * SET CHARACTER SET utf8; → dobrá čeština jen pro SLECT, ale při volání funkce se zmrší
+			 *		Client characterset:    utf8
+			 *		Conn.  characterset:    latin1
+			 * 
+			 * Správné řešení:
+			 *		V JDBC URL musí být: ?useUnicode=true&characterEncoding=UTF-8
+			 */
 			conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
 			if (conn.getTransactionIsolation() != Connection.TRANSACTION_SERIALIZABLE) {
 				log.warning("Database is NOT fully serializable!");
@@ -98,40 +111,6 @@
 		}
 	}
 
-	/**
-	 * 
-	 * @param messageID &lt;{0}-{1}-{2}@domain.tld&gt; where {0} is nntp_id and {1} is group_id and {2} is group_name
-	 * @return array where [0] = nntp_id and [1] = group_id and [2] = group_name or returns null if messageID is invalid
-	 */
-	private static String[] parseMessageID(String messageID) {
-		if (messageID.matches("<[0-9]+\\-[0-9]+\\-[a-z0-9\\.]+@.+>")) {
-			return messageID.substring(1).split("@")[0].split("\\-");
-		} else {
-			return null;
-		}
-	}
-
-	private static Long parseArticleID(String messageID) {
-		String[] localPart = parseMessageID(messageID);
-		if (localPart == null) {
-			return null;
-		} else {
-			return Long.parseLong(localPart[0]);
-		}
-	}
-
-	private static Long parseGroupID(String messageID) {
-		String[] localPart = parseMessageID(messageID);
-		if (localPart == null) {
-			return null;
-		} else {
-			return Long.parseLong(localPart[1]);
-			// If needed:
-			// parseGroupName() will be same as this method, just with:
-			// return localPart[2];
-		}
-	}
-
 	@Override
 	public List<Group> getGroups() throws StorageBackendException {
 		PreparedStatement ps = null;
@@ -397,7 +376,8 @@
 	 * (but should not be thrown if only bad thing is wrong username or password)
 	 */
 	@Override
-	public boolean authenticateUser(String username, char[] password) throws StorageBackendException {
+	public boolean authenticateUser(String username,
+			char[] password) throws StorageBackendException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -414,14 +394,81 @@
 		}
 	}
 
+	/**
+	 * Validates article and if OK, calls {@link #insertArticle(java.lang.String, java.lang.String, java.lang.String, java.lang.Long, java.lang.Long) }
+	 * @param article
+	 * @throws StorageBackendException 
+	 */
 	@Override
-	public void addArticle(Article art) throws StorageBackendException {
-		if (art.getAuthenticatedUser() == null) {
+	public void addArticle(Article article) throws StorageBackendException {
+		if (article.getAuthenticatedUser() == null) {
 			log.log(Level.SEVERE, "User was not authenticated, so his article was rejected.");
 			throw new StorageBackendException("User must be authenticated to post articles");
 		} else {
+			try {
+				DrupalMessage m = new DrupalMessage(article);
 
-			log.log(Level.INFO, "User ''{0}'' has posted an article", art.getAuthenticatedUser());
+				Long parentID = m.getParentID();
+				Long groupID = m.getGroupID();
+
+				if (parentID == null || groupID == null) {
+					throw new StorageBackendException("No valid In-Reply-To header was found → rejecting posted message.");
+				} else {
+					if (m.isMimeType("text/plain")) {
+						Object content = m.getContent();
+						if (content instanceof String) {
+							String subject = m.getSubject();
+							String text = (String) content;
+
+							/**
+							 * TODO: validovat a transformovat text
+							 * (v současné době se o to stará až Drupal při výstupu)
+							 */
+							if (subject == null || subject.length() < 1) {
+								subject = text.substring(0, Math.min(10, text.length()));
+							}
+
+							insertArticle(article.getAuthenticatedUser(), subject, text, parentID, groupID);
+							log.log(Level.INFO, "User ''{0}'' has posted an article", article.getAuthenticatedUser());
+						}
+					} else {
+						throw new StorageBackendException("Only text/plain messages are supported for now – post it as plain text please.");
+					}
+				}
+			} catch (Exception e) {
+				throw new StorageBackendException(e);
+			}
+		}
+	}
+
+	/**
+	 * Physically stores article in database.
+	 * @param subject
+	 * @param text
+	 * @param parentID
+	 * @param groupID 
+	 */
+	private void insertArticle(String sender, String subject, String text, Long parentID, Long groupID) throws StorageBackendException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			ps = conn.prepareStatement("SELECT nntp_post_article(?, ?, ?, ?, ?)");
+
+			ps.setString(1, sender);
+			ps.setString(2, subject);
+			ps.setString(3, text);
+			ps.setLong(4, parentID);
+			ps.setLong(5, groupID);
+
+			rs = ps.executeQuery();
+			rs.next();
+
+			Long articleID = rs.getLong(1);
+			log.log(Level.INFO, "Article was succesfully stored as {0}", articleID);
+		} catch (Exception e) {
+			throw new StorageBackendException(e);
+		} finally {
+			close(null, ps, rs);
 		}
 	}