org.onehippo.forge.blog.components.Detail.java Source code

Java tutorial

Introduction

Here is the source code for org.onehippo.forge.blog.components.Detail.java

Source

/*
 * Copyright 2010 Jasha Joachimsthal
 *
 * 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 org.onehippo.forge.blog.components;

import net.sf.akismet.Akismet;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;
import org.hippoecm.hst.content.beans.ObjectBeanManagerException;
import org.hippoecm.hst.content.beans.ObjectBeanPersistenceException;
import org.hippoecm.hst.content.beans.manager.workflow.WorkflowCallbackHandler;
import org.hippoecm.hst.content.beans.manager.workflow.WorkflowPersistenceManager;
import org.hippoecm.hst.content.beans.query.HstQuery;
import org.hippoecm.hst.content.beans.query.exceptions.QueryException;
import org.hippoecm.hst.content.beans.standard.HippoBean;
import org.hippoecm.hst.content.beans.standard.HippoDocumentBean;
import org.hippoecm.hst.core.component.HstComponentException;
import org.hippoecm.hst.core.component.HstRequest;
import org.hippoecm.hst.core.component.HstResponse;
import org.hippoecm.hst.core.linking.HstLink;
import org.hippoecm.hst.utils.BeanUtils;
import org.hippoecm.repository.api.WorkflowException;
import org.hippoecm.repository.reviewedactions.FullReviewedActionsWorkflow;
import org.onehippo.forge.blog.beans.BeanConstants;
import org.onehippo.forge.blog.beans.Blogpost;
import org.onehippo.forge.blog.beans.CommentBean;
import org.onehippo.forge.blog.hstextensions.ContentRewriterImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.io.IOException;
import java.rmi.RemoteException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;

/**
 * <p>HST Component for displaying Detail content. Handles adding user generated comments to {@link Blogpost}.</p>
 * <p>For mail notification of new {@link CommentBean} the following component parameters are expected:</p>
 * <dl>
 * <dt>mail.smtp.host</dt><dd>SMTP host, default is localhost</dd>
 * <dt>sender.email</dt><dd>Email address as sender of the notification mails. Default is noreply@example.com</dd>
 * <dt>receiver.email</dt><dd><strong>Required</strong> email address for the receiver of the notification mails.</dd>
 * </dl>
 * <p>For comment spam check service (Akismet), the following component parameters are expected:</p>
 * <dl>
 * <dt>spamfilter.apikey</dt><dd>API key for the spam check service</dd>
 * <dt>spamfilter.websiteurl</dt><dd>Full URL of the homepage, e.g. http://www.example.com/</dd>
 * </dl>
 * @author Jasha Joachimsthal
 *
 */
public class Detail extends BaseSiteComponent {

    protected static final String PARAM_SPAMFILTER_WEBSITEURL = "spamfilter.websiteurl";
    protected static final String PARAM_SPAMFILTER_APIKEY = "spamfilter.apikey";
    private static final String COMMENT_FOLDER = "/comment/";
    private static final String COMMENT_NODENAME_PREFIX = "comment-for-";
    protected static final String PARAM_WEBSITE = "website";
    protected static final String PARAM_EMAIL = "email";
    protected static final String PARAM_PERSON = "person";
    protected static final String PARAM_COMMENT = "comment";
    private static final String REFERER = "Referer";
    private static final String USER_AGENT = "User-Agent";
    private static final String DEFAULT_SENDER_MAIL = "noreply@example.com";
    private static final String LOCALHOST = "localhost";
    private static final String YYYY_MM_DD = "yyyy/MM/dd";
    private static final String HTML_BR = "<br />";
    private static final String LINE_END = "\r\n";
    private static final String REGEX_HTML_TAG = "\\<.*?\\>";
    protected static final String ENABLE_COMMENTS = "enableComments";
    private static final int MAX_TITLE_LENGTH = 30;
    public static final Logger log = LoggerFactory.getLogger(Detail.class);

    /**
     * Gets {@link HippoDocumentBean} for this request. If it's a {@link Blogpost}, also fetch comments
     */
    @Override
    public void doBeforeRender(HstRequest request, HstResponse response) throws HstComponentException {

        super.doBeforeRender(request, response);
        HippoBean documentBean = getContentBean(request);

        if (documentBean == null) {
            response.setStatus(HstResponse.SC_NOT_FOUND);
            return;
        }
        request.setAttribute("document", documentBean);
        request.setAttribute("contentrewriter", new ContentRewriterImpl());

        if (documentBean instanceof Blogpost) {
            final Blogpost blogpost = (Blogpost) documentBean;
            findComments(request, blogpost);
            checkCommentsEnabled(request);
        }

    }

    /*
     * (non-Javadoc)
     * @see org.hippoecm.hst.core.component.GenericHstComponent#doAction(org.hippoecm.hst.core.component.HstRequest, org.hippoecm.hst.core.component.HstResponse)
     */
    @Override
    public void doAction(HstRequest request, HstResponse response) throws HstComponentException {
        String type = request.getParameter("type");

        if ("add".equals(type)) {
            doAddComment(request, response);
        }
    }

    /**
     * Queries the repository for incoming beans for the current {@link Blogpost}.
     * Adds the comments to the {@link HstRequest}
     * @param request {@link HstRequest}
     * @param blogpost {@link Blogpost}
     */
    protected void findComments(HstRequest request, final Blogpost blogpost) {
        try {
            HstQuery commentQuery = BeanUtils.createIncomingBeansQuery(blogpost,
                    this.getSiteContentBaseBean(request), "blog:commentlink/@hippo:docbase", this,
                    CommentBean.class, false);
            commentQuery.addOrderByDescending(BeanConstants.PROP_DATE);

            List<CommentBean> comments = BeanUtils.getIncomingBeans(commentQuery, CommentBean.class);

            request.setAttribute("comments", comments);
        } catch (QueryException e) {
            log.warn("Error finding blog comments", e);
        }
    }

    /**
     * Checks if comments are enabled and adds its result to the request
     * @param request {@link org.hippoecm.hst.core.component.HstRequest}
     */
    protected void checkCommentsEnabled(HstRequest request) {
        boolean enableComments = Boolean.parseBoolean(getParameter(ENABLE_COMMENTS, request));
        request.setAttribute(ENABLE_COMMENTS, enableComments);
    }

    /**
     * Handles adding a user generated {@link CommentBean}
     * @param request {@link org.hippoecm.hst.core.component.HstRequest}
     * @param response {@link org.hippoecm.hst.core.component.HstResponse}
     */
    protected void doAddComment(HstRequest request, HstResponse response) {
        HippoBean commentTo = this.getContentBean(request);
        if (!(commentTo instanceof HippoDocumentBean)) {
            log.warn("Cannot comment on this type of bean");
            return;
        }

        String comment = request.getParameter(PARAM_COMMENT);
        String person = request.getParameter(PARAM_PERSON);

        if (StringUtils.isBlank(person) || StringUtils.isBlank(comment)) {
            return;
        }

        String email = request.getParameter(PARAM_EMAIL);
        String website = request.getParameter(PARAM_WEBSITE);

        if (isSpam(request, response, commentTo)) {
            handleSpam(response, comment, person, email, website);
            return;
        }

        String commentToUuidOfHandle = ((HippoDocumentBean) commentTo).getCanonicalHandleUUID();
        comment = comment.replaceAll(REGEX_HTML_TAG, StringUtils.EMPTY);

        Session persistableSession = null;
        WorkflowPersistenceManager wpm = null;

        String commentsFolderPath = getCommentFolderPath(request);
        try {
            // retrieves writable session. NOTE: this session should be logged out manually!
            persistableSession = getPersistableSession(request);
            wpm = getWorkflowPersistenceManager(persistableSession);
            wpm.setWorkflowCallbackHandler(new WorkflowCallbackHandler<FullReviewedActionsWorkflow>() {
                public void processWorkflow(FullReviewedActionsWorkflow wf)
                        throws RepositoryException, WorkflowException, RemoteException {
                    wf.requestPublication();
                }
            });

            CommentBean commentBean = createCommentBean(commentTo, wpm, commentsFolderPath);
            // update content properties
            if (commentBean == null) {
                throw new HstComponentException("WorkflowPersitenceManager returned null for a CommentBean");
            }

            String title = comment.trim().length() > MAX_TITLE_LENGTH
                    ? comment.trim().substring(0, MAX_TITLE_LENGTH)
                    : comment.trim();
            commentBean.setTitle(title);
            commentBean.setPerson(person);
            commentBean.setEmail(email);
            commentBean.setWebsite(website);
            commentBean.setSummary(comment.replaceAll(LINE_END, HTML_BR));
            commentBean.setCalendar(Calendar.getInstance());
            commentBean.setCommentTo(commentToUuidOfHandle);

            sendNotificationMail(commentBean, request);

            // update now
            wpm.update(commentBean);

        } catch (ObjectBeanManagerException e) {
            log.warn("ObjectBeanManagerException occurred while handling comment: ", e);
            refreshWorkflowPersistenceManager(wpm);
        } catch (RepositoryException e) {
            log.warn("RepositoryException occurred while handling comment: ", e);
            refreshWorkflowPersistenceManager(wpm);
        } finally {
            if (persistableSession != null) {
                persistableSession.logout();
            }
        }

    }

    /**
     * Creates {@link CommentBean} through the workflow
     *
     * @param commentTo {@link HippoBean} that gets a comment
     * @param wpm {@link WorkflowPersistenceManager}
     * @param commentsFolderPath the path under which the comment is stored
     * @return path of the comment node
     * @throws ObjectBeanManagerException in case of a workflow error
     */
    private CommentBean createCommentBean(HippoBean commentTo, WorkflowPersistenceManager wpm,
            String commentsFolderPath) throws ObjectBeanManagerException {
        // comment node name is simply a concatenation of 'comment-' and current time millis.
        StringBuffer commentNodeName = new StringBuffer(COMMENT_NODENAME_PREFIX).append(commentTo.getName());
        commentNodeName.append('-').append(System.currentTimeMillis());

        // create comment node now
        wpm.create(commentsFolderPath, BeanConstants.DOCTYPE_COMMENT, commentNodeName.toString(), true);

        // retrieve the comment content to manipulate
        return (CommentBean) wpm.getObject(commentsFolderPath + '/' + commentNodeName.toString());
    }

    /**
    * Creates folder for the comment and returns its path
    * 
    * @param request current {@link HstRequest}
    * @return String with the folder path
    */
    private String getCommentFolderPath(HstRequest request) {
        // it is not important where we store comments. WE just use some (canonical) time path below our project content
        String siteCanonicalBasePath = this.getHstSite(request).getCanonicalContentPath();
        Calendar currentDate = Calendar.getInstance();

        DateFormat folderFormat = new SimpleDateFormat(YYYY_MM_DD);
        StringBuffer commentsFolderPath = new StringBuffer(siteCanonicalBasePath).append(COMMENT_FOLDER);
        commentsFolderPath.append(folderFormat.format(currentDate.getTime()));
        return commentsFolderPath.toString();
    }

    /**
       * Refreshes {@link WorkflowPersistenceManager} (called from catch blocks)
       * @param wpm {@link WorkflowPersistenceManager}
       */
    private void refreshWorkflowPersistenceManager(WorkflowPersistenceManager wpm) {
        if (wpm == null) {
            return;
        }
        try {
            wpm.refresh();
        } catch (ObjectBeanPersistenceException obpe) {
            log.warn("Failed to refresh: ", obpe);
        }
    }

    /**
     * Checks external service (Akismet) if the comment is possible spam
     * @param request {@link org.hippoecm.hst.core.component.HstRequest}
     * @param response {@link org.hippoecm.hst.core.component.HstResponse}
     * @param bean of the current page to which the comment is added (needed for link rewriting)
     * @return {@literal true} if the external service thinks the comment is spam, {@literal false} if the external
     * service responses the comment is safe OR if no external spam check service is configured
     */
    protected boolean isSpam(final HstRequest request, final HstResponse response, final HippoBean bean) {
        String apikey = getParameter(PARAM_SPAMFILTER_APIKEY, request);
        String websiteurl = getParameter(PARAM_SPAMFILTER_WEBSITEURL, request);
        if (StringUtils.isBlank(apikey) || StringUtils.isBlank(websiteurl)) {
            log.debug("No spamfilter is configured. All comments will be allowed.");
            return false;
        }
        Akismet akismet = new Akismet(apikey, websiteurl);

        String userAgent = request.getHeader(USER_AGENT);
        String referrer = request.getHeader(REFERER);
        String remoteAddr = request.getRemoteAddr();
        String comment = request.getParameter(PARAM_COMMENT);
        String person = request.getParameter(PARAM_PERSON);
        String email = request.getParameter(PARAM_EMAIL);
        String website = request.getParameter(PARAM_WEBSITE);
        final HstLink link = request.getRequestContext().getHstLinkCreator().create(bean,
                request.getRequestContext());
        String permalink = link.toUrlForm(request, response, true);
        return akismet.commentCheck(remoteAddr, userAgent, referrer, permalink, Akismet.COMMENT_TYPE_COMMENT,
                person, email, website, comment, null);
    }

    /**
     * Called in case the spam checker marks the new comment as spam
     *
     * @param response {@link HstResponse}
     * @param comment Text of the comment
     * @param person name of the commenter
     * @param email email address of the commenter
     * @param website url of the commenter's website
     */
    protected void handleSpam(HstResponse response, String comment, String person, String email, String website) {
        if (log.isInfoEnabled()) {
            StringBuffer sb = new StringBuffer("Received comment spam:");
            sb.append("\nFrom: ").append(person);
            sb.append("\nMail: ").append(email);
            sb.append("\nWebsite ").append(website);
            sb.append("\nComment\n").append(comment);
            log.info(sb.toString());
        }
        try {
            response.sendError(HstResponse.SC_FORBIDDEN);
        } catch (IOException e) {
            log.error("Could not send http response error to spammer", e);
        }
    }

    /**
     * <p>Sends mail notification for newly added comment.</p>
     * <p>If component configuration for <code>receiver.email</code> is missing, no mails will be sent.</p>
     * @param commentBean {@link CommentBean}
     * @param request {@link HstRequest}
     */
    protected void sendNotificationMail(final CommentBean commentBean, final HstRequest request) {
        String mailhost = getParameter(Email.MAIL_HOST, request);
        String senderEmail = getParameter(Email.SENDER_EMAIL, request);
        String receiverEmail = getParameter(Email.RECEIVER_EMAIL, request);

        if (StringUtils.isBlank(mailhost)) {
            log.info("No value for mail.smtp.host in component configuration, trying localhost");
            mailhost = LOCALHOST;
        }
        if (StringUtils.isBlank(senderEmail)) {
            log.info("No value for sender.email in component configuration, trying noreply@example.com");
            senderEmail = DEFAULT_SENDER_MAIL;
        }
        if (StringUtils.isBlank(receiverEmail)) {
            log.warn(
                    "No value for receiver.email in component configuration. Will not try to send mail notification.");
            return;
        }

        SimpleEmail email = new SimpleEmail();
        StringBuffer subject = new StringBuffer("New comment: ");
        subject.append(commentBean.getTitle());

        StringBuffer msg = new StringBuffer("The following comment has been added:\n");
        msg.append(commentBean.getSummary());

        email.setHostName(mailhost);
        try {
            email.addTo(receiverEmail);
            email.setFrom(senderEmail);
            email.setSubject(subject.toString());
            email.setMsg(msg.toString());
            email.send();
        } catch (EmailException e) {
            log.error("Error sending notification for added comment", e);
        }
    }

}