ru.org.linux.site.Message.java Source code

Java tutorial

Introduction

Here is the source code for ru.org.linux.site.Message.java

Source

/*
 * Copyright 1998-2010 Linux.org.ru
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package ru.org.linux.site;

import java.io.IOException;
import java.io.Serializable;
import java.sql.*;
import java.util.List;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import ru.org.linux.spring.AddMessageForm;
import ru.org.linux.spring.SectionStore;
import ru.org.linux.util.*;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.javabb.bbcode.BBCodeProcessor;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;

public class Message implements Serializable {
    private static final Log logger = LogFactory.getLog(Message.class);

    private final int msgid;
    private final int postscore;
    private final boolean votepoll;
    private final boolean sticky;
    private String linktext;
    private String url;
    private final String title;
    private final int userid;
    private final int guid;
    private final boolean deleted;
    private final boolean expired;
    private final int commitby;
    private final boolean havelink;
    private final Timestamp postdate;
    private final Timestamp commitDate;
    private final String groupTitle;
    private final String groupUrl;
    private final Timestamp lastModified;
    private final int sectionid;
    private final int commentCount;
    private final boolean moderate;
    private final String message;
    private final boolean notop;
    private final int userAgent;
    private final String postIP;
    private final boolean lorcode;
    private final boolean resolved;
    private final int groupCommentsRestriction;
    private final boolean minor;

    private final Section section;

    private static final long serialVersionUID = 807240555706110851L;
    public static final int POSTSCORE_MOD_AUTHOR = 9999;
    public static final int POSTSCORE_UNRESTRICTED = -9999;
    public static final int POSTSCORE_MODERATORS_ONLY = 10000;
    public static final int POSTSCORE_REGISTERED_ONLY = -50;

    public Message(Connection db, int msgid) throws SQLException, MessageNotFoundException {
        this(db, null, msgid);
    }

    public Message(Connection db, SectionStore sectionStore, int msgid)
            throws SQLException, MessageNotFoundException {
        Statement st = db.createStatement();

        ResultSet rs = st.executeQuery("SELECT " + "postdate, topics.id as msgid, userid, topics.title, "
                + "topics.groupid as guid, topics.url, topics.linktext, ua_id, "
                + "groups.title as gtitle, urlname, vote, havelink, section, topics.sticky, topics.postip, "
                + "postdate<(CURRENT_TIMESTAMP-sections.expire) as expired, deleted, lastmod, commitby, "
                + "commitdate, topics.stat1, postscore, topics.moderate, message, notop,bbcode, "
                + "topics.resolved, restrict_comments, minor " + "FROM topics "
                + "INNER JOIN groups ON (groups.id=topics.groupid) "
                + "INNER JOIN sections ON (sections.id=groups.section) "
                + "INNER JOIN msgbase ON (msgbase.id=topics.id) " + "WHERE topics.id=" + msgid);
        if (!rs.next()) {
            throw new MessageNotFoundException(msgid);
        }

        this.msgid = rs.getInt("msgid");
        int ps = rs.getInt("postscore");

        if (rs.wasNull()) {
            postscore = POSTSCORE_UNRESTRICTED;
        } else {
            postscore = ps;
        }

        votepoll = rs.getBoolean("vote");
        sticky = rs.getBoolean("sticky");
        linktext = rs.getString("linktext");
        url = rs.getString("url");
        userid = rs.getInt("userid");
        title = StringUtil.makeTitle(rs.getString("title"));
        guid = rs.getInt("guid");
        deleted = rs.getBoolean("deleted");
        expired = !sticky && rs.getBoolean("expired");
        havelink = rs.getBoolean("havelink");
        postdate = rs.getTimestamp("postdate");
        commitDate = rs.getTimestamp("commitdate");
        commitby = rs.getInt("commitby");
        groupTitle = rs.getString("gtitle");
        groupUrl = rs.getString("urlname");
        lastModified = rs.getTimestamp("lastmod");
        sectionid = rs.getInt("section");
        commentCount = rs.getInt("stat1");
        moderate = rs.getBoolean("moderate");
        message = rs.getString("message");
        notop = rs.getBoolean("notop");
        userAgent = rs.getInt("ua_id");
        postIP = rs.getString("postip");
        lorcode = rs.getBoolean("bbcode");
        resolved = rs.getBoolean("resolved");
        groupCommentsRestriction = rs.getInt("restrict_comments");
        minor = rs.getBoolean("minor");

        rs.close();
        st.close();

        try {
            if (sectionStore == null) {
                section = new Section(db, sectionid);
            } else {
                section = sectionStore.getSection(sectionid);
            }
        } catch (SectionNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Message(SectionStore sectionStore, ResultSet rs) throws SQLException {
        msgid = rs.getInt("msgid");
        postscore = rs.getInt("postscore");
        votepoll = rs.getBoolean("vote");
        sticky = rs.getBoolean("sticky");
        linktext = rs.getString("linktext");
        url = rs.getString("url");
        userid = rs.getInt("userid");
        title = StringUtil.makeTitle(rs.getString("title"));
        guid = rs.getInt("guid");
        deleted = rs.getBoolean("deleted");
        expired = !sticky && rs.getBoolean("expired");
        havelink = rs.getBoolean("havelink");
        postdate = rs.getTimestamp("postdate");
        commitDate = rs.getTimestamp("commitdate");
        commitby = rs.getInt("commitby");
        groupTitle = rs.getString("gtitle");
        groupUrl = rs.getString("urlname");
        lastModified = rs.getTimestamp("lastmod");
        sectionid = rs.getInt("section");
        commentCount = rs.getInt("stat1");
        moderate = rs.getBoolean("moderate");
        message = rs.getString("message");
        notop = rs.getBoolean("notop");
        userAgent = rs.getInt("ua_id");
        postIP = rs.getString("postip");
        lorcode = rs.getBoolean("bbcode");
        resolved = rs.getBoolean("resolved");
        groupCommentsRestriction = rs.getInt("restrict_comments");
        minor = rs.getBoolean("minor");

        try {
            section = sectionStore.getSection(sectionid);
        } catch (SectionNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Message(Connection db, AddMessageForm form, User user)
            throws SQLException, UtilException, ScriptErrorException, UserErrorException {
        // Init fields

        userAgent = 0;
        postIP = form.getPostIP();

        guid = form.getGuid();

        Group group = new Group(db, guid);

        groupCommentsRestriction = group.getCommentsRestriction();

        linktext = form.getLinktextHTML();
        url = form.getUrl();

        // url check
        if (!group.isImagePostAllowed()) {
            if (url != null && !"".equals(url)) {
                if (linktext == null) {
                    throw new BadInputException(" URL  ?");
                }
                url = URLUtil.fixURL(url);
            }
        }
        // Setting Message fields
        //    tags = new Tags(Tags.parseTags(form.getTagsHTML()));
        title = form.getTitleHTML();
        havelink = form.getUrl() != null && form.getLinktext() != null && form.getUrl().length() > 0
                && form.getLinktext().length() > 0 && !group.isImagePostAllowed();
        sectionid = group.getSectionId();
        // Defaults
        msgid = 0;
        postscore = 0;
        votepoll = false;
        sticky = false;
        deleted = false;
        expired = false;
        commitby = 0;
        postdate = new Timestamp(System.currentTimeMillis());
        commitDate = null;
        groupTitle = "";
        groupUrl = "";
        lastModified = new Timestamp(System.currentTimeMillis());
        commentCount = 0;
        moderate = false;
        notop = false;
        userid = user.getId();
        lorcode = true;
        resolved = false;
        minor = false;

        message = form.processMessage(group);

        try {
            section = new Section(db, sectionid);
        } catch (SectionNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Message(Connection db, Message original, ServletRequest request)
            throws BadGroupException, SQLException, UtilException, UserErrorException {
        userAgent = original.userAgent;
        postIP = original.postIP;
        guid = original.guid;

        Group group = new Group(db, guid);
        groupCommentsRestriction = group.getCommentsRestriction();

        if (request.getParameter("linktext") != null) {
            linktext = request.getParameter("linktext");
        } else {
            linktext = original.linktext;
        }

        if (request.getParameter("url") != null) {
            url = request.getParameter("url");
        } else {
            url = original.url;
        }

        //    if (request.getParameter("tags")!=null) {
        //      List<String> newTags = Tags.parseTags(request.getParameter("tags"));
        //
        //      tags = new Tags(newTags);
        //    } else {
        //      tags = original.tags;
        //    }

        // url check
        if (!group.isImagePostAllowed()) {
            if (url != null && !"".equals(url)) {
                if (linktext == null) {
                    throw new BadInputException(" URL  ?");
                }
                url = URLUtil.fixURL(url);
            }
        }

        if (request.getParameter("title") != null) {
            title = HTMLFormatter.htmlSpecialChars(request.getParameter("title"));
        } else {
            title = original.title;
        }

        if (request.getParameter("resolve") != null) {
            resolved = "yes".equals(request.getParameter("resolve"));
        } else {
            resolved = original.resolved;
        }

        havelink = original.havelink;

        sectionid = group.getSectionId();

        msgid = original.msgid;
        postscore = original.getPostScore();
        votepoll = original.votepoll;
        sticky = original.sticky;
        deleted = original.deleted;
        expired = original.expired;
        commitby = original.commitby;
        postdate = original.postdate;
        commitDate = original.commitDate;
        groupTitle = original.groupTitle;
        groupUrl = original.groupUrl;
        lastModified = new Timestamp(System.currentTimeMillis());
        commentCount = original.commentCount;
        moderate = original.moderate;
        notop = original.notop;
        userid = original.userid;
        lorcode = original.lorcode;
        minor = original.minor;

        if (request.getParameter("newmsg") != null) {
            message = request.getParameter("newmsg");
        } else {
            message = original.message;
        }

        try {
            section = new Section(db, sectionid);
        } catch (SectionNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    public boolean isExpired() {
        return expired;
    }

    public boolean isDeleted() {
        return deleted;
    }

    public String getTitle() {
        return title;
    }

    public String getSectionTitle() {
        return section.getName();
    }

    public String getGroupTitle() {
        return groupTitle;
    }

    public Timestamp getLastModified() {
        if (lastModified == null) {
            return new Timestamp(0);
        }

        return lastModified;
    }

    public int getGroupId() {
        return guid;
    }

    public int getSectionId() {
        return sectionid;
    }

    public int getCommentCount() {
        return commentCount;
    }

    private int getCommentCountRestriction() {
        int commentCountPS = POSTSCORE_UNRESTRICTED;

        if (!sticky) {
            if (commentCount > 3000) {
                commentCountPS = 200;
            } else if (commentCount > 2000) {
                commentCountPS = 100;
            } else if (commentCount > 1000) {
                commentCountPS = 50;
            }
        }

        return commentCountPS;
    }

    public int getPostScore() {
        int effective = Math.max(postscore, groupCommentsRestriction);

        effective = Math.max(effective, section.getCommentPostscore());

        effective = Math.max(effective, getCommentCountRestriction());

        return effective;
    }

    public String getPostScoreInfo() {
        return getPostScoreInfo(getPostScore());
    }

    public static String getPostScoreInfo(int postscore) {
        switch (postscore) {
        case POSTSCORE_UNRESTRICTED:
            return "";
        case 100:
            return "<b>   </b>: "
                    + User.getStars(100, 100);
        case 200:
            return "<b>   </b>: "
                    + User.getStars(200, 200);
        case 300:
            return "<b>   </b>: "
                    + User.getStars(300, 300);
        case 400:
            return "<b>   </b>: "
                    + User.getStars(400, 400);
        case 500:
            return "<b>   </b>: "
                    + User.getStars(500, 500);
        case POSTSCORE_MOD_AUTHOR:
            return "<b>   </b>:  ?   ";
        case POSTSCORE_MODERATORS_ONLY:
            return "<b>   </b>:  ? ";
        case POSTSCORE_REGISTERED_ONLY:
            return "<b>   </b>:  ? ? ";
        default:
            return "<b>   </b>:  ? ? , score>="
                    + postscore;
        }
    }

    public Message getNextMessage(Connection db, SectionStore sectionStore) throws SQLException {
        PreparedStatement pst;

        int scrollMode = Section.getScrollMode(sectionid);

        switch (scrollMode) {
        case Section.SCROLL_SECTION:
            pst = db.prepareStatement(
                    "SELECT topics.id as msgid FROM topics WHERE topics.commitdate=(SELECT min(commitdate) FROM topics, groups, sections WHERE sections.id=groups.section AND topics.commitdate>? AND topics.groupid=groups.id AND groups.section=? AND (topics.moderate OR NOT sections.moderate) AND NOT deleted)");
            pst.setTimestamp(1, commitDate);
            pst.setInt(2, sectionid);
            break;

        case Section.SCROLL_GROUP:
            pst = db.prepareStatement(
                    "SELECT min(topics.id) as msgid FROM topics, groups, sections WHERE sections.id=groups.section AND topics.id>? AND topics.groupid=? AND topics.groupid=groups.id AND (topics.moderate OR NOT sections.moderate) AND NOT deleted");
            pst.setInt(1, msgid);
            pst.setInt(2, guid);
            break;

        case Section.SCROLL_NOSCROLL:
        default:
            return null;
        }

        try {
            ResultSet rs = pst.executeQuery();

            if (!rs.next()) {
                return null;
            }

            int nextMsgid = rs.getInt("msgid");

            if (rs.wasNull()) {
                return null;
            }

            return new Message(db, sectionStore, nextMsgid);
        } catch (MessageNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            pst.close();
        }
    }

    public Message getPreviousMessage(Connection db, SectionStore sectionStore) throws SQLException {
        PreparedStatement pst;

        int scrollMode = Section.getScrollMode(sectionid);

        switch (scrollMode) {
        case Section.SCROLL_SECTION:
            pst = db.prepareStatement(
                    "SELECT topics.id as msgid FROM topics WHERE topics.commitdate=(SELECT max(commitdate) FROM topics, groups, sections WHERE sections.id=groups.section AND topics.commitdate<? AND topics.groupid=groups.id AND groups.section=? AND (topics.moderate OR NOT sections.moderate) AND NOT deleted)");
            pst.setTimestamp(1, commitDate);
            pst.setInt(2, sectionid);
            break;

        case Section.SCROLL_GROUP:
            pst = db.prepareStatement(
                    "SELECT max(topics.id) as msgid FROM topics, groups, sections WHERE sections.id=groups.section AND topics.id<? AND topics.groupid=? AND topics.groupid=groups.id AND (topics.moderate OR NOT sections.moderate) AND NOT deleted");
            pst.setInt(1, msgid);
            pst.setInt(2, guid);
            break;

        case Section.SCROLL_NOSCROLL:
        default:
            return null;
        }

        try {
            ResultSet rs = pst.executeQuery();

            if (!rs.next()) {
                return null;
            }

            int prevMsgid = rs.getInt("msgid");

            if (rs.wasNull()) {
                return null;
            }

            return new Message(db, sectionStore, prevMsgid);
        } catch (MessageNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            pst.close();
        }
    }

    public int getMessageId() {
        return msgid;
    }

    public boolean isCommited() {
        return moderate;
    }

    public int getPageCount(int messages) {
        return (int) Math.ceil(commentCount / ((double) messages));
    }

    public static int getPageCount(int commentCount, int messages) {
        return (int) Math.ceil(commentCount / ((double) messages));
    }

    public boolean isVotePoll() {
        return votepoll;
    }

    public boolean isSticky() {
        return sticky;
    }

    public boolean updateMessageText(Connection db, User editor, List<String> newTags) throws SQLException {
        SingleConnectionDataSource scds = new SingleConnectionDataSource(db, true);

        PreparedStatement pstGet = db.prepareStatement(
                "SELECT message,title FROM msgbase JOIN topics ON msgbase.id=topics.id WHERE topics.id=? FOR UPDATE");

        pstGet.setInt(1, msgid);
        ResultSet rs = pstGet.executeQuery();
        if (!rs.next()) {
            throw new RuntimeException("Can't fetch previous message text");
        }

        String oldMessage = rs.getString("message");
        String oldTitle = rs.getString("title");

        rs.close();
        pstGet.close();

        List<String> oldTags = Tags.getMessageTags(db, msgid);

        EditInfoDTO editInfo = new EditInfoDTO();

        editInfo.setMsgid(msgid);
        editInfo.setEditor(editor.getId());

        boolean modified = false;

        SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(scds);

        if (!oldMessage.equals(message)) {
            editInfo.setOldmessage(oldMessage);
            modified = true;

            jdbcTemplate.update("UPDATE msgbase SET message=:message WHERE id=:msgid",
                    ImmutableMap.of("message", message, "msgid", msgid));
        }

        if (!oldTitle.equals(title)) {
            modified = true;
            editInfo.setOldtitle(oldTitle);

            jdbcTemplate.update("UPDATE topics SET title=:title WHERE id=:id",
                    ImmutableMap.of("title", title, "id", msgid));
        }

        if (newTags != null) {
            boolean modifiedTags = Tags.updateTags(db, msgid, newTags);

            if (modifiedTags) {
                editInfo.setOldtags(Tags.toString(oldTags));
                Tags.updateCounters(db, oldTags, newTags);
                modified = true;
            }
        }

        if (modified) {
            SimpleJdbcInsert insert = new SimpleJdbcInsert(scds).withTableName("edit_info").usingColumns("msgid",
                    "editor", "oldmessage", "oldtitle", "oldtags");

            insert.execute(new BeanPropertySqlParameterSource(editInfo));
        }

        return modified;
    }

    public String getUrl() {
        return url;
    }

    public String getLinktext() {
        return linktext;
    }

    public int addTopicFromPreview(Connection db, Template tmpl, HttpServletRequest request,
            String previewImagePath, User user) throws SQLException, UtilException, IOException, BadImageException,
            InterruptedException, ScriptErrorException {

        Group group = new Group(db, guid);

        int msgid = allocateMsgid(db);

        if (group.isImagePostAllowed()) {
            if (previewImagePath == null) {
                throw new ScriptErrorException("previewImagePath==null!?");
            }

            ScreenshotProcessor screenshot = new ScreenshotProcessor(previewImagePath);
            screenshot.copyScreenshotFromPreview(tmpl, msgid);

            url = "gallery/" + screenshot.getMainFile().getName();
            linktext = "gallery/" + screenshot.getIconFile().getName();
        }

        PreparedStatement pst = db.prepareStatement(
                "INSERT INTO topics (postip, groupid, userid, title, url, moderate, postdate, id, linktext, deleted, ua_id) VALUES ('"
                        + request.getRemoteAddr()
                        + "',?, ?, ?, ?, 'f', CURRENT_TIMESTAMP, ?, ?, 'f', create_user_agent(?))");
        //                pst.setString(1, request.getRemoteAddr());
        pst.setInt(1, group.getId());
        pst.setInt(2, user.getId());
        pst.setString(3, title);
        pst.setString(4, url);
        pst.setInt(5, msgid);
        pst.setString(6, linktext);
        pst.setString(7, request.getHeader("User-Agent"));
        pst.executeUpdate();
        pst.close();

        // insert message text
        PreparedStatement pstMsgbase = db
                .prepareStatement("INSERT INTO msgbase (id, message, bbcode) values (?,?, ?)");
        pstMsgbase.setLong(1, msgid);
        pstMsgbase.setString(2, message);
        pstMsgbase.setBoolean(3, lorcode);
        pstMsgbase.executeUpdate();
        pstMsgbase.close();

        String logmessage = "??  " + msgid + ' ' + LorHttpUtils.getRequestIP(request);
        logger.info(logmessage);

        return msgid;
    }

    private static int allocateMsgid(Connection db) throws SQLException {
        Statement st = null;
        ResultSet rs = null;

        try {
            st = db.createStatement();
            rs = st.executeQuery("select nextval('s_msgid') as msgid");
            rs.next();
            return rs.getInt("msgid");
        } finally {
            if (rs != null) {
                rs.close();
            }

            if (st != null) {
                st.close();
            }
        }
    }

    public boolean isCommentsAllowed(User user) {
        if (user != null && user.isBlocked()) {
            return false;
        }

        if (deleted || expired) {
            return false;
        }

        int score = getPostScore();

        if (score == POSTSCORE_UNRESTRICTED) {
            return true;
        }

        if (user == null || user.isAnonymous()) {
            return false;
        }

        if (user.canModerate()) {
            return true;
        }

        if (score == POSTSCORE_REGISTERED_ONLY) {
            return true;
        }

        if (score == POSTSCORE_MODERATORS_ONLY) {
            return false;
        }

        boolean isAuthor = user.getId() == userid;

        if (score == POSTSCORE_MOD_AUTHOR) {
            return isAuthor;
        }

        if (isAuthor) {
            return true;
        } else {
            return user.getScore() >= score;
        }
    }

    public void checkCommentsAllowed(User user) throws AccessViolationException {
        user.checkBlocked();

        if (deleted) {
            throw new AccessViolationException(
                    "?? ?    ?");
        }

        if (expired) {
            throw new AccessViolationException("  ?");
        }

        if (!isCommentsAllowed(user)) {
            throw new AccessViolationException(
                    "   ?   ? ");
        }
    }

    public int getUid() {
        return userid;
    }

    public boolean isNotop() {
        return notop;
    }

    public String getLinkLastmod() {
        if (expired) {
            return getLink();
        } else {
            return getLink() + "?lastmod=" + getLastModified().getTime();
        }
    }

    public boolean isHaveLink() {
        return havelink;
    }

    public int getId() {
        return msgid;
    }

    public int getUserAgent() {
        return userAgent;
    }

    public Section getSection() {
        return section;
    }

    public String getMessage() {
        return message;
    }

    public String getProcessedMessage(Connection db) throws SQLException {
        return getProcessedMessage(db, false);
    }

    public String getProcessedMessage(Connection db, boolean includeCut) throws SQLException {
        if (lorcode) {
            BBCodeProcessor proc = new BBCodeProcessor();
            proc.setIncludeCut(includeCut);
            return proc.preparePostText(db, message);
        } else {
            return "<p>" + message;
        }
    }

    public Timestamp getPostdate() {
        return postdate;
    }

    public String getPostIP() {
        return postIP;
    }

    public int getCommitby() {
        return commitby;
    }

    public Timestamp getCommitDate() {
        return commitDate;
    }

    public boolean isLorcode() {
        return lorcode;
    }

    public List<EditInfoDTO> loadEditInfo(Connection db) {
        SingleConnectionDataSource scds = new SingleConnectionDataSource(db, true);

        SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(scds);

        List<EditInfoDTO> list = jdbcTemplate.query("SELECT * FROM edit_info WHERE msgid=? ORDER BY id DESC",
                BeanPropertyRowMapper.newInstance(EditInfoDTO.class), msgid);

        return ImmutableList.copyOf(list);
    }

    public boolean isResolved() {
        return resolved;
    }

    public void resolveMessage(Connection db, boolean b) throws SQLException {
        PreparedStatement pstMsgbase = db
                .prepareStatement("UPDATE topics SET resolved=?,lastmod=CURRENT_TIMESTAMP WHERE id=?");
        pstMsgbase.setBoolean(1, b);
        pstMsgbase.setInt(2, msgid);
        pstMsgbase.executeUpdate();
    }

    public String getLink() {
        return Section.getSectionLink(sectionid) + groupUrl + '/' + msgid;
    }

    public String getLinkPage(int page) {
        if (page == 0) {
            return getLink();
        }

        return Section.getSectionLink(sectionid) + groupUrl + '/' + msgid + "/page" + page;
    }

    public void commit(Connection db, User commiter, int bonus) throws SQLException, UserErrorException {
        if (bonus < 0 || bonus > 20) {
            throw new UserErrorException("?  bonus");
        }

        PreparedStatement pst = null;
        try {
            pst = db.prepareStatement("UPDATE topics SET moderate='t', commitby=?, commitdate='now' WHERE id=?");
            pst.setInt(2, msgid);
            pst.setInt(1, commiter.getId());
            pst.executeUpdate();

            User author;
            try {
                author = User.getUser(db, userid);
            } catch (UserNotFoundException e) {
                throw new RuntimeException(e);
            }

            if (author.getScore() < 300) {
                author.changeScore(db, bonus);
            }
        } finally {
            if (pst != null) {
                pst.close();
            }
        }
    }

    public void changeGroup(Connection db, int changeGroupId) throws SQLException {
        Statement st = db.createStatement();
        st.executeUpdate("UPDATE topics SET groupid=" + changeGroupId + " WHERE id=" + msgid);
        /* to recalc counters */
        st.executeUpdate("UPDATE groups SET stat4=stat4+1 WHERE id=" + guid + " or id=" + changeGroupId);
        st.close();
    }

    public boolean isMinor() {
        return minor;
    }
}