games.strategy.triplea.pbem.AxisAndAlliesForumPoster.java Source code

Java tutorial

Introduction

Here is the source code for games.strategy.triplea.pbem.AxisAndAlliesForumPoster.java

Source

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package games.strategy.triplea.pbem;

import games.strategy.engine.framework.GameRunner2;
import games.strategy.engine.pbem.AbstractForumPoster;
import games.strategy.engine.pbem.IForumPoster;
import games.strategy.net.DesktopUtilityBrowserLauncher;
import games.strategy.triplea.help.HelpSupport;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.params.HttpMethodParams;

/**
 * Post turn summary to www.axisandallies.org to the thread identified by the forumId
 * URL format: http://www.axisandallies.org/forums/index.php?topic=[forumId],
 * like http://www.axisandallies.org/forums/index.php?topic=25878
 * 
 * The poster logs in, and out every time it posts, this way we don't nee to manage any state between posts
 * 
 * @author Klaus Groenbaek
 */
public class AxisAndAlliesForumPoster extends AbstractForumPoster {
    // -----------------------------------------------------------------------
    // constants
    // -----------------------------------------------------------------------
    private static final long serialVersionUID = 8896923978584346664L;

    // -----------------------------------------------------------------------
    // class fields
    // -----------------------------------------------------------------------
    // the patterns used to extract values from hidden form fields posted to the server
    public static final Pattern NUM_REPLIES_PATTERN = Pattern.compile(".*name=\"num_replies\" value=\"(\\d+)\".*",
            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
    public static final Pattern SEQ_NUM_PATTERN = Pattern.compile(".*name=\"seqnum\"\\svalue=\"(\\d+)\".*",
            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
    public static final Pattern SC_PATTERN = Pattern.compile(".*name=\"sc\"\\svalue=\"(\\w+)\".*",
            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

    // Pattern that matches if the "Notify me of replies" checkbox is checked
    public static final Pattern NOTIFY_PATTERN = Pattern.compile(".*id=\"check_notify\"\\schecked=\"checked\".*",
            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

    // 3 patterns used for error handling
    public static final Pattern AN_ERROR_OCCURRED_PATTERN = Pattern.compile(".*An Error Has Occurred.*",
            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
    public static final Pattern ERROR_TEXT_PATTERN = Pattern.compile(
            ".*<tr\\s+class=\"windowbg\">\\s*<td[^>]*>([^<]*)</td>.*", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
    public static final Pattern ERROR_LIST_PATTERN = Pattern.compile(".*id=\"error_list[^>]*>\\s+([^<]*)\\s+<.*",
            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

    // -----------------------------------------------------------------------
    // instance fields
    // -----------------------------------------------------------------------
    private transient HttpState m_httpState;
    private transient HostConfiguration m_hostConfiguration;
    private transient HttpClient m_client;

    // -----------------------------------------------------------------------
    // instance methods
    // -----------------------------------------------------------------------

    /**
     * Logs into axisandallies.org
     * nb: Username and password are posted in clear text
     * 
     * @throws Exception
     *             if login fails
     */
    private void login() throws Exception {
        // creates and configures a new http client
        m_client = new HttpClient();
        m_client.getParams().setParameter("http.protocol.single-cookie-header", true);
        m_client.getParams().setParameter("http.useragent",
                "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)");
        m_httpState = new HttpState();
        m_hostConfiguration = new HostConfiguration();
        // add the proxy
        GameRunner2.addProxy(m_hostConfiguration);
        m_hostConfiguration.setHost("www.axisandallies.org");

        final PostMethod post = new PostMethod("http://www.axisandallies.org/forums/index.php?action=login2");
        try {
            post.addRequestHeader("Accept", "*/*");
            post.addRequestHeader("Accept-Language", "en-us");
            post.addRequestHeader("Cache-Control", "no-cache");
            post.addRequestHeader("Content-Type", "application/x-www-form-urlencoded");

            final List<NameValuePair> parameters = new ArrayList<NameValuePair>();
            parameters.add(new NameValuePair("user", getUsername()));
            parameters.add(new NameValuePair("passwrd", getPassword()));
            post.setRequestBody(parameters.toArray(new NameValuePair[parameters.size()]));

            int status = m_client.executeMethod(m_hostConfiguration, post, m_httpState);
            if (status == 200) {
                final String body = post.getResponseBodyAsString();
                if (body.toLowerCase().contains("password incorrect")) {
                    throw new Exception("Incorrect Password");
                }
                // site responds with 200, and a refresh header
                final Header refreshHeader = post.getResponseHeader("Refresh");
                if (refreshHeader == null) {
                    throw new Exception("Missing refresh header after login");
                }

                final String value = refreshHeader.getValue(); // refresh: 0; URL=http://...
                final Pattern p = Pattern.compile("[^;]*;\\s*url=(.*)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
                final Matcher m = p.matcher(value);
                if (m.matches()) {
                    final String url = m.group(1);
                    final GetMethod getRefreshPage = new GetMethod(url);

                    try {
                        status = m_client.executeMethod(m_hostConfiguration, getRefreshPage, m_httpState);
                        if (status != 200) {
                            // something is probably wrong, but there is not much we can do about it, we handle errors when we post
                        }
                    } finally {
                        getRefreshPage.releaseConnection();
                    }
                } else {
                    throw new Exception("The refresh header didn't contain a URL");
                }
            } else {
                throw new Exception("Failed to login to forum, server responded with status code: " + status);
            }
        } finally {
            post.releaseConnection();
        }
    }

    public boolean postTurnSummary(final String message, final String subject) {
        try {
            login();

            // Now we load the post page, and find the hidden fields needed to post
            final GetMethod get = new GetMethod(
                    "http://www.axisandallies.org/forums/index.php?action=post;topic=" + m_topicId + ".0");
            int status = m_client.executeMethod(m_hostConfiguration, get, m_httpState);
            String body = get.getResponseBodyAsString();
            if (status == 200) {
                String numReplies;
                String seqNum;
                String sc;
                Matcher m = NUM_REPLIES_PATTERN.matcher(body);
                if (m.matches()) {
                    numReplies = m.group(1);
                } else {
                    throw new Exception("Hidden field 'num_replies' not found on page");
                }

                m = SEQ_NUM_PATTERN.matcher(body);
                if (m.matches()) {
                    seqNum = m.group(1);
                } else {
                    throw new Exception("Hidden field 'seqnum' not found on page");
                }

                m = SC_PATTERN.matcher(body);
                if (m.matches()) {
                    sc = m.group(1);
                } else {
                    throw new Exception("Hidden field 'sc' not found on page");
                }

                // now we have the required hidden fields to reply to
                final PostMethod post = new PostMethod(
                        "http://www.axisandallies.org/forums/index.php?action=post2;start=0;board=40");

                try {
                    // Construct the multi part post
                    final List<Part> parts = new ArrayList<Part>();

                    parts.add(createStringPart("topic", m_topicId));
                    parts.add(createStringPart("subject", subject));
                    parts.add(createStringPart("icon", "xx"));
                    parts.add(createStringPart("message", message));

                    // If the user has chosen to receive notifications, ensure this setting is passed on
                    parts.add(createStringPart("notify", NOTIFY_PATTERN.matcher(body).matches() ? "1" : "0"));

                    if (m_includeSaveGame && m_saveGameFile != null) {
                        final FilePart part = new FilePart("attachment[]", m_saveGameFileName, m_saveGameFile);
                        part.setContentType("application/octet-stream");
                        part.setTransferEncoding(null);
                        part.setCharSet(null);
                        parts.add(part);
                    }

                    parts.add(createStringPart("post", "Post"));
                    parts.add(createStringPart("num_replies", numReplies));
                    parts.add(createStringPart("additional_options", "1"));
                    parts.add(createStringPart("sc", sc));
                    parts.add(createStringPart("seqnum", seqNum));

                    final MultipartRequestEntity entity = new MultipartRequestEntity(
                            parts.toArray(new Part[parts.size()]), new HttpMethodParams());
                    post.setRequestEntity(entity);

                    // add headers
                    post.addRequestHeader("Referer",
                            "http://www.axisandallies.org/forums/index.php?action=post;topic=" + m_topicId
                                    + ".0;num_replies=" + numReplies);
                    post.addRequestHeader("Accept", "*/*");

                    try {
                        // the site has spam prevention which means you can't post until 15 seconds after login
                        Thread.sleep(15 * 1000);
                    } catch (final InterruptedException ie) {
                        ie.printStackTrace(); // this should never happen
                    }

                    post.setFollowRedirects(false);
                    status = m_client.executeMethod(m_hostConfiguration, post, m_httpState);
                    body = post.getResponseBodyAsString();
                    if (status == 302) {
                        // site responds with a 302 redirect back to the forum index (board=40)

                        // The syntax for post is ".....topic=xx.yy" where xx is the thread id, and yy is the post number in the given thread
                        // since the site is lenient we can just give a high post_number to go to the last post in the thread
                        m_turnSummaryRef = "http://www.axisandallies.org/forums/index.php?topic=" + m_topicId
                                + ".10000";
                    } else {
                        // these two patterns find general errors, where the first pattern checks if the error text appears,
                        // the second pattern extracts the error message. This could be the "The last posting from your IP was less than 15 seconds ago.Please try again later"

                        // this patter finds errors that are marked in red (for instance "You are not allowed to post URLs", or
                        // "Some one else has posted while you vere reading"

                        Matcher matcher = ERROR_LIST_PATTERN.matcher(body);
                        if (matcher.matches()) {
                            throw new Exception("The site gave an error: '" + matcher.group(1) + "'");
                        }

                        matcher = AN_ERROR_OCCURRED_PATTERN.matcher(body);
                        if (matcher.matches()) {
                            matcher = ERROR_TEXT_PATTERN.matcher(body);
                            if (matcher.matches()) {
                                throw new Exception("The site gave an error: '" + matcher.group(1) + "'");
                            }
                        }

                        final Header refreshHeader = post.getResponseHeader("Refresh");
                        if (refreshHeader != null) {
                            // sometimes the message will be flagged as spam, and a refresh url is given
                            final String value = refreshHeader.getValue(); // refresh: 0; URL=http://...topic=26114.new%3bspam=true#new
                            final Pattern p = Pattern.compile("[^;]*;\\s*url=.*spam=true.*",
                                    Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
                            m = p.matcher(value);
                            if (m.matches()) {
                                throw new Exception("The summary was posted but was flagged as spam");
                            }
                        }
                        throw new Exception(
                                "Unknown error, please contact the forum owner and also post a bug to the tripleA development team");
                    }
                } finally {
                    post.releaseConnection();
                    // Commented out log out call since it was causing all of a user's sessions to be logged out and doesn't appear to be needed
                    // final GetMethod logout = new GetMethod("http://www.axisandallies.org/forums/index.php?action=logout;sesc=" + sc);
                    // try
                    // {
                    // status = m_client.executeMethod(m_hostConfiguration, logout, m_httpState);
                    // // site responds with a 200 + Refresh header to redirect to index.php
                    // if (status != 200)
                    // {
                    // // nothing we can do if this fails
                    // }
                    // } finally
                    // {
                    // logout.releaseConnection();
                    // }
                }
            } else {
                throw new Exception("Unable to load forum post " + m_topicId);
            }

        } catch (final Exception e) {
            m_turnSummaryRef = e.getMessage();
            return false;
        }

        return true;
    }

    /**
     * Utility method for creating string parts, since we need to remove transferEncoding and content type to behave like a browser
     * 
     * @param name
     *            the form field name
     * @param value
     *            the for field value
     * @return return the created StringPart
     */
    private StringPart createStringPart(final String name, final String value) {
        final StringPart stringPart = new StringPart(name, value);
        stringPart.setTransferEncoding(null);
        stringPart.setContentType(null);
        return stringPart;
    }

    public String getDisplayName() {
        return "AxisAndAllies.org";
    }

    /**
     * Create a clone of the poster
     * 
     * @return a copy
     */
    public IForumPoster doClone() {
        final AxisAndAlliesForumPoster clone = new AxisAndAlliesForumPoster();
        clone.setTopicId(getTopicId());
        clone.setIncludeSaveGame(getIncludeSaveGame());
        clone.setAlsoPostAfterCombatMove(getAlsoPostAfterCombatMove());
        clone.setPassword(getPassword());
        clone.setUsername(getUsername());
        return clone;
    }

    public boolean supportsSaveGame() {
        return true;
    }

    public void viewPosted() {
        final String url = "http://www.axisandallies.org/forums/index.php?topic=" + m_topicId + ".10000";
        DesktopUtilityBrowserLauncher.openURL(url);
    }

    public String getTestMessage() {
        return "Testing, this will take about 20 seconds...";
    }

    public String getHelpText() {
        return HelpSupport.loadHelp("axisAndAlliesForum.html");
    }

}