Search

Domino Upgrade

VersionSupport end
5.0
6.0
6.5
7.0
8.0
8.5
Upgrade to 9.x now!
(see the full Lotus lifcyle) To make your upgrade a success use the Upgrade Cheat Sheet.
Contemplating to replace Notes? You have to read this! (also available on Slideshare)

Languages

Other languages on request.

Twitter

Useful Tools

Get Firefox
Use OpenDNS
The support for Windows XP has come to an end . Time to consider an alternative to move on.

About Me

I am the "IBM Collaboration & Productivity Advisor" for IBM Asia Pacific. I'm based in Singapore.
Reach out to me via:
Follow notessensei on Twitter
(posts)
Skype
Sametime
IBM
Facebook
LinkedIn
XING
Amazon Store
Amazon Kindle
NotesSensei's Spreadshirt shop
profile for stwissel on Stack Exchange, a network of free, community-driven Q&A sites

« From Blogsphere to a Static Site (Part 3) - Generating pages | Main| Meeting a CxO »

From Blogsphere to a Static Site (Part 4) - Comment backend

The blog needed a comment function. While there are social options around (Facebook, Disqus etc), I decided I want to roll my own. Partly because I want tighter control and partly, well, because I could. My comment backend would:

  • Provide a REST API to create comments in a JSON structure. The comment body will be Markdown. Reading would provide comments in ready to use HTML (I hear howling from the API crowd). No delete or update functionality
  • Cleanup content considered harmful (code injection) and optional sport Captcha
  • Store all content in a NoSQL database, in my case CouchDB (or Cloudant with its 20G free plan)
  • Cache all queries for comment in an online cache to limit calls to the database
  • Initially run on Domino, later on liberty or the raw JVM
  • Initially also update Domino using a web service - so during transition no comments would get lost

In its initial incarnation the Comment servlet is a OSGi plugin that listens to the /comments URL implemented as Wink servlet. So the class of interest is the one defining the service. We have one method for post, one for get and a helper function

/**
 * Wink implementation of Comment service
 */
@Workspace(workspaceTitle = "Blog Comments", collectionTitle = "Create or display comments")
@Path(value = "/comments")
@Produces(MediaType.TEXT_HTML)
@Consumes(MediaType.APPLICATION_JSON)
public class CommentService extends CommentResponse {

    private final Logger logger = Logger.getLogger(this.getClass().getName());

    @POST
    public Response createComment(@Context HttpServletRequest request) {
        final Monitor mon = MonitorFactory.start("CommentService#createComment");
        String result = "Sorry I can't process your comment at this time";
        ResponseBuilder builder = Response.ok();

        try {
            InputStream in = request.getInputStream();
            BlogComment comment = BlogComment.load(in);
            in.close();

            if (comment != null) {
                this.captureSubmissionDetails(request, comment);
                result = CommentManager.INSTANCE.saveComment(comment, true);
            } else {
                builder.status(Status.NOT_ACCEPTABLE);
            }
            builder.entity(result).type(MediaType.TEXT_HTML_TYPE);

        } catch (Exception e) {
            String errorMessage = e.getMessage();
            builder.entity((((errorMessage == null) || errorMessage.equals("")) ? "Undefined error" : errorMessage)).type(
                    MediaType.TEXT_HTML_TYPE);
            Utils.logError(this.logger, e);
        }

        mon.stop();
        return builder.build();
    }

    @GET
    public Response getComments(@QueryParam("parentid") final String parentid) {
        Response response = null;
        final Monitor mon = MonitorFactory.start("CommentService#getComments");
        final ResponseBuilder builder = Response.ok();
        final Collection<BlogComment> bc = CommentManager.INSTANCE.loadComments(parentid);

        if ((bc == null) || bc.isEmpty()) {
            builder.status(Status.NO_CONTENT);
        } else {
            response = this.renderOutput(bc, "comment.mustache");
        }
        mon.stop();
        return (response == null) ? builder.build() : response;
    }

    private void captureSubmissionDetails(HttpServletRequest request, BlogComment comment) {
        final Monitor mon = MonitorFactory.start("CommentService#captureSubmissionDetails");
        try {
            @SuppressWarnings("rawtypes")
            Enumeration hn = request.getHeaderNames();
            if (hn != null) {
                while (hn.hasMoreElements()) {
                    String key = hn.nextElement().toString();
                    comment.addParameter(key, request.getHeader(key));
                }
            }
            @SuppressWarnings("rawtypes")
            Enumeration pn = request.getParameterNames();
            if (pn != null) {
                while (pn.hasMoreElements()) {
                    String key = pn.nextElement().toString();
                    String[] values = request.getParameterValues(key);
                    comment.addParameters(key, values);

                    if (key.equals("referer")) {
                        comment.setReferer(values[0]);
                    } else if (key.equals("user-agent")) {
                        comment.setUserAgent(values[0]);
                    }

                }
            }
            @SuppressWarnings("rawtypes")
            Enumeration an = request.getAttributeNames();
            if (an != null) {
                while (an.hasMoreElements()) {
                    try {
                        String key = an.nextElement().toString();
                        comment.addAttribute(key, String.valueOf(request.getAttribute(key)));
                    } catch (Exception e) {
                        // No action here
                    }
                }
            }
            comment.addParameter("REMOTE_HOST", request.getRemoteHost());
            comment.addParameter("REMOTE_ADDR", request.getRemoteAddr());
            comment.addParameter("REMOTE_USER", request.getRemoteUser());

            // Needed for Captcha
            comment.setRemoteAddress(request.getRemoteAddr());
        } catch (Exception e) {
            Utils.logError(this.logger, e);
            // But no further action here!
        }
        mon.stop();
    }
}

The two interesting lines in the class above are CommentManager.INSTANCE.saveComment(comment, true); and CommentManager.INSTANCE.loadComments(parentid);, with the former saving a new comment and the later loading the list of comments. Both use the CommentsManager Singleton to access comments. The key component is a Google Guava cache and the Ektorp CouchDB library

Setting up the cache

 class CommentLoader extends CacheLoader<String, Collection<BlogComment>> {

        private final Logger logger = Logger.getLogger(this.getClass().getName());

        @Override
        public Collection<BlogComment> load(String parentId) throws CommentException {
            Monitor mon = MonitorFactory.start("CommentManager#CommentLoader#load");
            CouchDbConnector db;
            List<BlogComment> result = null;

            try {
                db = CommentManager.this.getDB();
                ViewQuery query = new ViewQuery().designDocId("_design/comments").viewName("byParentId").key(parentId);
                result = db.queryView(query, BlogComment.class);
            } catch (MalformedURLException e) {
                Utils.logError(this.logger, e);
                throw new CommentException("Comment retrieval failed", e);
            }

            if (result != null) {
                Collections.sort(result);
            } else {
                throw new CommentException();
            }
            mon.stop();
            return result;
        }
    }

        // Get comments from the database
        CommentLoader loader = new CommentLoader();
        this.commentCache = CacheBuilder.newBuilder().maximumSize(1000).recordStats().expireAfterAccess(8, TimeUnit.HOURS)
                .build(loader);

Save a comment

public String saveComment(BlogComment bc, boolean validate) {
        String key = this.config.CHECKCAPTCHA ? this.config.RECAPTCHAKEY : null;
        boolean isValid = bc.isValid(key);
        String result = bc.getValidationResult();

        if (isValid || !validate) {
            CouchDbConnector db;
            try {
                db = this.getDB();
                db.create(bc.getId(), bc);
                // Now save into the cache
                Collection<BlogComment> cachedComments = null;
                cachedComments = this.commentCache.getIfPresent(bc.getParentId());
                if (cachedComments != null) {
                    cachedComments.add(bc);
                }
            } catch (Exception e) {
                Utils.logError(this.logger, e);
                result = e.getMessage();
            }

            // Now notify the legacyBlog
            BackgroundWebServiceCall b = new BackgroundWebServiceCall(bc);
            this.executor.execute(b);
        }
        return result;
    }

Load comments

public Collection<BlogComment> loadComments(final String parentId) {
        Collection<BlogComment> result = null;
        if (parentId != null) {
            try {
                result = this.commentCache.get(parentId);
            } catch (ExecutionException e) {
                Utils.logError(this.logger, e);
            }
        }
        return result;
    }

Getting a gravatar image

public String getGravatarURL() {

        if ((this.gravatarURL == null) || this.gravatarURL.trim().equals("")) {
            if (this.eMail != null) {
                String emailHash = DigestUtils.md5Hex(this.eMail.toLowerCase().trim());
                this.setGravatarURL(GRAVATAR_URL + emailHash + ".jpg?s=" + GRAVATAR_SIZE);
            }
        }

        return this.gravatarURL;
    }

Create HTML from Markdown

private void createHtmlBody(String markdownBody) {
        PegDownProcessor p = new PegDownProcessor();
        this.htmlBody = p.markdownToHtml(HTMLFilter.filter(markdownBody));
    }

Check for valid Captcha

public static boolean isValidCaptcha(String captchaKey, String remoteAddress, String challenge, String response) {
		boolean result = true;
		// We only test if we have a remote address and the captcha switch is on
		if (remoteAddress != null && captchaKey != null) {
			ReCaptchaImpl reCaptcha = new ReCaptchaImpl();
			reCaptcha.setPrivateKey(captchaKey);
			ReCaptchaResponse reCaptchaResponse = reCaptcha.checkAnswer(remoteAddress, challenge, response);
			result = reCaptchaResponse.isValid();
		}

		return result;
	}

For the full details of the implementation, including the background task talking to Domino, you need to wait for the source release on Github. Next stop: the comment front end

Comments

Disclaimer

This site is in no way affiliated, endorsed, sanctioned, supported, nor enlightened by Lotus Software nor IBM Corporation. I may be an employee, but the opinions, theories, facts, etc. presented here are my own and are in now way given in any official capacity. In short, these are my words and this is my site, not IBM's - and don't even begin to think otherwise. (Disclaimer shamelessly plugged from Rocky Oliver)
© 2003 - 2017 Stephan H. Wissel - some rights reserved as listed here: Creative Commons License
Unless otherwise labeled by its originating author, the content found on this site is made available under the terms of an Attribution/NonCommercial/ShareAlike Creative Commons License, with the exception that no rights are granted -- since they are not mine to grant -- in any logo, graphic design, trademarks or trade names of any type. Code samples and code downloads on this site are, unless otherwise labeled, made available under an Apache 2.0 license. Other license models are available on written request and written confirmation.