Drupal: posílání zpráv do skupiny.
authorFrantišek Kučera <franta-hg@frantovo.cz>
Thu Oct 20 09:59:04 2011 +0200 (2011-10-20)
changeset 102d843b4fee5dc
parent 101 d54786065fa3
child 103 a788bf0e1080
Drupal: posílání zpráv do skupiny.
src/org/sonews/storage/DrupalMessage.java
src/org/sonews/storage/impl/DrupalDatabase.java
     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 &lt;{0}-{1}-{2}@domain.tld&gt; 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 &lt;{0}-{1}-{2}@domain.tld&gt; 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