Tuesday, May 14, 2013

Emptytons - Mimicking Static Classes in Java

A static class is one that has only static methods (functions) and it is meant to support procedural programming. Such classes aren't really classes in that the user of the class is only interested in the helper functions and not in creating instances of the class. While static classes are supported in C# [1], no such direct support exists in Java.

People often mimic this feature by using a private constructor for their class:
    public class StaticClass1 {
        // disallow instance creation by other classes
        private StaticClass1() {

        public static boolean isEmpty(final String s) {
            return s == null || s.isEmpty();
But, this doesn't truly disallow creation of instances. Code (static initializers, methods, inner classes, etc.) written inside the class can still create instances of the class despite the private constructor. A dedicated Java hacker can even revert to reflection to create instances of the class.

Java does however provide support for enums and that can be handily used to create these static classes or Emptytons who can truly avoid creation of any instances:
    public enum StaticClass2 {
        // Empty enum trick to avoid instance creation
        ; // this semi-colon is important

        public static boolean isEmpty(final String s) {
            return s == null || s.isEmpty();
Reflection cannot be used to create instances of such classes unlike with private constructors as reflective instantiation of enum types is prohibited [2, 3].

[1] http://msdn.microsoft.com/en-us/library/79b3xss3%28v=vs.80%29.aspx
[2] http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.9
[3] http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Constructor.html#newInstance%28java.lang.Object...%29


Monday, November 14, 2011

Cleaning unversioned files from Subversion workspace

While git has a clean command to remove untracked files, it seems subversion doesn't support such a command.

I found Patric Fornasier's Blog explaining how to remove unversioned files in subversion and I was very happy, but it seems theat xargs can';t handle spaces in the file names and so chokes.

So I went ahead and created a simple script to handle whitepsace in the file names based on Patric's piping of commands.

#! /bin/sh

# svn stat --no-ignore | grep ^\? | cut -b 8- | xargs rm -rfv

FILES=`svn stat --no-ignore | grep ^\? | cut -b 9-`

IFS=$(echo "\n\b")

for f in ${FILES}
rm -rfv "${f}"



Friday, December 19, 2008

Tri-State Checkbox using Javascript

Recently I noticed how yahoo mail select all check box only shows two
states while selecting/un-selecting mails to perform various actions.
So I decided to experiment and create a simple tri-state check box that could display my intermediate state. Below is what I ended up with:

Tri-State Checkbox Demo with dependent checkboxes

Select/Unselect All

It's really simple to use. Here's what you have to do:
  1. Include the javascript file in your html
  2. Have a place-holder node (span in my case) to hold the image for the tri-state box
  3. Place the related checkboxes inside a container, e.g. a div
  4. Invoke the initTriStateCheckBox(<place holder id>, <container id>, false) function in your html

Standalone Tri-State Checkbox Demo

I liked this

Here's what you have to do to create a standalone one:
  1. Include the javascript file in your html
  2. Have a place-holder node (span in my case) to hold the image for the tri-state box
  3. Create a hidden field to hold the state of the box
  4. Invoke the initTriStateCheckBox(<place holder id>, <hidden field id>, true) function in your html

The zip file containing the javascript, images and a sample is available here. It is also a GitHub project set up by Michal Letynski.


Sunday, July 13, 2008

Threaded Comments in Blogger

With all the new features being introduced in blogger, there was still a feature I would like which was not included - Threaded Comments. So I decided to mix the existing comment system with a little bit of javascript and come up with my own version of threaded comments. The advantage of using this is that you are still allowing blogger
to manage your comments unlike a couple of other tools I have seen. Check out the Threaded version in action
by adding your comments below ;).

The idea behind this came from the simple observation that most of us use @AuthorName to reply to comments posted by other users in 'single' threaded comments. So the javascript I wrote just parses the comment bodies for this author name (or comment ids) and then searches for appropriate comments to find parents of the reply comments.

Below is an image of how after I implemented this idea the comments section on my blog changed:

In addition to the threaded support:

  • Include multiple replies in your comment using multiple @replyTargets on separate lines in your comment
  • Blog authors can have their comments highlightd differently - use css styles for 'blog-author-comment' and 'blog-nonauthor-comment'
  • Include multiple replies in your comment using multiple @replyTargets in the comment
  • Hide/Show individual comments
  • Configurable template to display comments - use you custom template to call applyCommentTemplate();

How to install:
Note: Remember to back up your existing template before making any changes to it.
Here are the steps to follow:

  1. Include the java script file to the top of your template just before the <b:skin> tags starts

    <script type="text/javascript">
    --- Threaded Comments ---
    v 0.9.3 15th March 2009
    By Shams Mahmood
    function Author(C,A,B){this.id=C;this.name=A;this.url=B;this.toString=function(F){var E="\t";if(F){for(var D=0;D<F;D++){E+="\t"}}return"Author[\n"+E+"id="+this.id+", \n"+E+"name="+this.name+", \n"+E+"url="+this.url+"\n"+E+"]"}}function Comment(E,H,G,C,B,D,F,A){this.id=E;this.sequenceNumber=H;this.postedTime=G;this.body=F;this.deleted=A;this.deleteUrl=B;this.deleteText=D;this.parentId="";this.children=new Array();this.level=0;this.author=C;this.getChildCount=function(){return this.children.length};this.addChild=function(I){this.children[this.getChildCount()]=I.id;I.parentId=this.id;I.level=this.level+1};this.toString=function(K){var J="\t";if(K){for(var I=0;I<K;I++){J+="\t"}}return"Comment[\n"+J+"id="+this.id+", \n"+J+"sequence="+this.sequenceNumber+", \n"+J+"deleted="+this.deleted+", \n"+J+"parentId="+this.parentId+", \n"+J+"children=["+this.children+"], \n"+J+"level="+this.level+", \n"+J+"author="+this.author.toString(1)+", \n"+J+"posted time="+this.postedTime+", \n"+J+"body="+this.body+"\n"+J+"]"}}function trimBrsFromString(C){var F=trimString(C);var B=["<br>","<br >","<br/>","<br />","<BR>","<BR >","<BR/>","<BR />"];if(F){var E=true;while(E){E=false;for(var D in B){var A=B[D];if(F.indexOf(A)==0){F=F.substring(A.length);F=trimString(F);E=true}var H=F.length;var G=F.lastIndexOf(A);if(G>=0&&G==H-A.length){F=F.substring(0,G);F=trimString(F);E=true}}}}return F}function trimString(A){var E="";if(A){var D=false;for(var B=0;B<A.length;B++){var F=A.charAt(B);if(!D&&F!=" "&&F!="\n"&&F!="\t"){D=true}if(D){E+=F}}D=false;var C=-1;for(var B=E.length-1;!D&&B>0;B--){var F=E.charAt(B);if(!D&&F!=" "&&F!="\n"&&F!="\t"){D=true;C=B}}if(C>0){E=E.substring(0,C+1)}}return E}function addItem(A,B){A[B.id]=B}function getAllItems(C){var D=new Array();var B=0;for(var A in C){D[B]=C[A];B++}return D}function getItemsCount(C){var B=0;for(var A in C){B++}return B}var ALL_AUTHORS=new Object();var ALL_COMMENTS=new Object();function getNewAuthorId(){var C=1;for(var A in ALL_AUTHORS){if(ALL_AUTHORS[A]&&ALL_AUTHORS[A].id){var B=ALL_AUTHORS[A].id;if(B>=C){C=B+1}}}return C}function createAuthor(C,A,B){return new Author(C,A,B)}function addAuthor(A){addItem(ALL_AUTHORS,A)}function getAllAuthors(){return getAllItems(ALL_AUTHORS)}function getAuthorsCount(){return getItemsCount(ALL_AUTHORS)}function findAuthor(C,B){for(var A in ALL_AUTHORS){if(ALL_AUTHORS[A]){if(ALL_AUTHORS[A].name==C&&ALL_AUTHORS[A].url==B){return ALL_AUTHORS[A]}}}return null}function getNewCommentSequence(){var C=1;for(var A in ALL_COMMENTS){if(ALL_COMMENTS[A]&&ALL_COMMENTS[A].sequenceNumber){var B=ALL_COMMENTS[A].sequenceNumber;if(B>=C){C=B+1}}}return C}function createComment(E,H,G,C,B,D,F,A){return new Comment(E,H,G,C,B,D,F,A)}function addComment(A){addItem(ALL_COMMENTS,A)}function getAllComments(){return getAllItems(ALL_COMMENTS)}function getRootComments(){var D=new Array();var C=0;for(var A in ALL_COMMENTS){var B=ALL_COMMENTS[A];if(B&&B.level==0){D[C]=B;C++}}return D}function getCommentsCount(){return getItemsCount(ALL_COMMENTS)}function findComment(B){for(var A in ALL_COMMENTS){if(ALL_COMMENTS[A]){if(ALL_COMMENTS[A].id==B){return ALL_COMMENTS[A]}}}return null}function findLastCommentByAuthorName(C){var B=null;for(var A in ALL_COMMENTS){if(ALL_COMMENTS[A]){if(ALL_COMMENTS[A].author.name==C){B=ALL_COMMENTS[A]}}}return B}function findLastCommentByPartialAuthorName(C){var B=null;for(var A in ALL_COMMENTS){if(ALL_COMMENTS[A]){if(ALL_COMMENTS[A].author.name.toLowerCase().indexOf(C.toLowerCase())==0){B=ALL_COMMENTS[A]}}}return B}function addCommentHierarchy(D,C){if(D){C[C.length]=D;var A=D.children;for(var B in A){addCommentHierarchy(findComment(A[B]),C)}}}function getCommmentsInSortedOrder(){var D=new Array();var A=getRootComments();for(var B in A){var C=A[B];addCommentHierarchy(C,D)}return D}function ParsedResult(A,B){this.parentComment=A;this.body=B;this.toString=function(){return"[parentComment="+this.parentComment+", body="+this.body+", ]"}}function findParentCommentFromDescriptor(A){var B=findComment(A);if(B==null){B=findLastCommentByAuthorName(A)}if(B==null){B=findLastCommentByPartialAuthorName(A)}return B}function parseCommentBody(B,F){B=trimString(B);var A=B.indexOf("@");if(A==0){var H=B.indexOf("\n",0);var G=B.indexOf("<",0);var D=H;if(G>0&&(G<D||D<0)){D=G}if(D>2){var O=B.substring(1,D);O=trimString(O);var K=findParentCommentFromDescriptor(O);if(K==null){var J=O.indexOf(" ");if(J>0){var N=trimString(O.substring(0,J));K=findParentCommentFromDescriptor(N);if(K!=null){D=J+1}}}if(K!=null){var P=null;var Q=D;var C=B.indexOf("@",Q+1);if(C>Q){var M=trimString(B.substring(C));P=parseCommentBody(M,C)}if(P&&P.length>0&&P[0].parentComment!=null){var L=trimString(B.substring(D,C));var I=new ParsedResult(K,L);var E=[I].concat(P);return E}else{var L=trimString(B.substring(D));var I=new ParsedResult(K,L);return[I]}return E}}}var I=new ParsedResult(null,B);return[I]}function buildComment(C,K,H,L,G,I,M,A){var F=findAuthor(C,K);if(!F){F=createAuthor(getNewAuthorId(),C,K);addAuthor(F)}var D=parseCommentBody(A,0);for(var J in D){var E="";E=D[J].body;E=trimBrsFromString(E);var B=createComment(H+"."+J,getNewCommentSequence(),L,F,I,M,E,G);addComment(B);if(D[J].parentComment!=null){D[J].parentComment.addChild(B)}}}function substituteConstant(A,D,C){var B=A;while(B.indexOf(D)>=0){B=B.replace(D,C)}return B}function substituteConstantIfValueExists(D,A,I,C,H){var J=D;var F=J.indexOf(A);var E=J.indexOf(I);while(F>0&&E>F){var B=J.substring(F,E+I.length);var G=null;if(H&&H.length>0){G=substituteConstant(B,C,H);G=G.substring(A.length,G.length-I.length)}else{G=""}J=J.replace(B,G);F=J.indexOf(A);E=J.indexOf(I)}return J}function isBlogAuthor(B){var A=false;if(window.BLOG_AUTHORS){for(var C in BLOG_AUTHORS){if(BLOG_AUTHORS[C]==B){A=true;break}}}else{if(window.BLOG_AUTHOR){A=(BLOG_AUTHOR==B)}}return A}function applyCommentTemplateToComment(F,E){var A=F;A=substituteConstant(A,"${COMMENT.ID}",E.id);A=substituteConstant(A,"${COMMENT.TIMESTAMP}",E.postedTime);A=substituteConstant(A,"${COMMENT.AUTHOR.NAME}",E.author.name);var C=(E.level>3)?"gt3":E.level;A=substituteConstant(A,"${COMMENT.LEVEL}",C);A=substituteConstantIfValueExists(A,"${COMMENT.AUTHOR.URL.EXISTS.START}","${COMMENT.AUTHOR.URL.EXISTS.END}","${COMMENT.AUTHOR.URL}",E.author.url);A=substituteConstant(A,"${COMMENT.AUTHOR.URL}",E.author.url);A=substituteConstant(A,"${COMMENT.DELETE.URL}",E.deleteUrl);A=substituteConstant(A,"${COMMENT.DELETE.TEXT}",E.deleteText);A=substituteConstant(A,"${COMMENT.BODY}",E.body);var D=isBlogAuthor(E.author.url)?"blog-author-comment":"blog-nonauthor-comment";A=substituteConstant(A,"${BLOG.AUTHOR}",D);A=substituteConstant(A,"${BLOG.POST.COMMENT.LINK}",BLOG_POST_COMMENT_LINK);var B=(E.deleted)?"deleted-comment":"";A=substituteConstant(A,"${COMMENT.DELETED.STYLE}",B);document.writeln(A)}function applyCommentTemplate(C){var D=getCommmentsInSortedOrder();for(var A in D){var B=D[A];applyCommentTemplateToComment(C,B)}}function setElementDisplay(B,C){var A=document.getElementById(B);if(A){A.style.display=C}}function setElementsDisplay(B,C){for(var A in B){setElementDisplay(B[A],C)}}function showElements(A){setElementsDisplay(A,"block")}function hideElements(A){setElementsDisplay(A,"none")}function showElement(A){setElementDisplay(A,"block")}function hideElement(A){setElementDisplay(A,"none")}function toggleElementDisplays(C,B,D){if(C.innerHTML=="[hide]"){for(var A in B){if(D[A]=="both"||D[A]=="hide"){hideElement(B[A])}}C.innerHTML="[show]"}else{for(var A in B){if(D[A]=="both"||D[A]=="show"){showElement(B[A])}}C.innerHTML="[hide]"}};// ]]>

  2. Next you need add the css tyles for the comments section inside you <b:skin> section. Below is the one I am using in my blog:

    .comment-segment {
    margin-top: 10px;
    margin-right: 10px;
    .comment-level-0 {
    margin-left: 10px;
    .comment-level-1 {
    margin-left: 25px;
    .comment-level-2 {
    margin-left: 40px;
    .comment-level-3 {
    margin-left: 55px;
    .comment-level-gt3 {
    margin-left: 70px;
    .blog-author-comment {
    background-color: #F0F0BE;
    border: 1px solid #FFFF99;
    .blog-nonauthor-comment {
    background-color: #B4C8F0;
    border: 1px solid #7296E2;
    .deleted-comment {
    color: gray; font-STYLE: italic
    .delete-comment-icon {
    background: url("http://www.blogblog.com/rounders3/icon_delete13.gif")
    .comment-time {
    font-size: 80%;
    margin: inherit;
    padding-left: 10px;
    padding-bottom: 10px;
    .reply-guide {
    background-color: #FFFFFF;
    border: #076a93 1px dotted;
    display: none;
    padding-right: 10px;
    padding-left: 10px;
    padding-bottom: 0.75em;
    padding-top: 5px;
    margin-right: 10px;
    margin-bottom: 10px;
    .reply-guide-header {
    color: #076a93;
    padding-top: 10px;
    .reply-guide-list {
    list-style: none;
    padding-left: 2px;
    margin-left: 2px;
    .reply-guide-example {
    font-size: 85%;
    margin-right: 5px;
    margin-bottom: 10px;
    float: right;
    border: 1px dotted #076a93;
    padding: 5 5 5 5;

  3. Lastly you need to insert the template for to render the threaded blog comments.
    You need to find the portion in your template responsible for rendering comment. In my template it started with:

    <b:includable id='comments' var='post'>
    <div class='comments' id='comments'>

    I just replaced that <b:includable> with the following:

    <b:includable id='comments' var='post'>
    <div class='comments' id='comments'>
    <a name='comments'/>
    <b:if cond='data:post.allowComments'>
    <b:if cond='data:post.numComments == 1'> 1 <data:commentLabel/>:

    <b:if cond='data:post.numComments > 0'>
    <!-- Include a post comment link before rendering the comments -->
    <p class='comment-footer'>
    <b:if cond='data:post.embedCommentForm'>
    <b:include data='post' name='comment-form'/>
    <b:if cond='data:post.allowComments'>
    <a expr:href='data:post.addCommentUrl'

    <!-- Loop through the comments adding the comment bodies in a hidden div -->
    <b:loop values='data:post.comments' var='comment'>
    <div style="display: none;" expr:id='"comment-body-" + data:comment.id' >
    <!-- Now create the comment using our javascript -->
    <script type="text/javascript">
    // USE THIS if YOU Have multiple Authors adding them in a comma separated form after removing the '//' from the next line
    // var BLOG_AUTHORS = ['http://www.blogger.com/profile/firstauthor', 'http://www.blogger.com/profile/secondauthor', 'http://www.blogger.com/profile/thirdauthor'];
    // Use this if you have just one author like this blog :)
    var BLOG_AUTHOR = 'http://www.blogger.com/profile/10301627897367423203';
    var BLOG_POST_COMMENT_LINK = '<data:post.addCommentUrl/>';

    var eCommentDelete = false;
    var eAuthorUrl = '';
    <b:loop values='data:post.comments' var='comment'>
    eCommentDelete = false;
    eAuthorUrl = '';
    <b:if cond='data:comment.authorUrl'>
    eAuthorUrl = "<data:comment.authorUrl/>";
    <b:if cond='data:comment.isDeleted'>
    eCommentDelete = true;

    buildComment("<data:comment.author/>", eAuthorUrl,
    "<data:comment.id/>", "<data:comment.timestamp/>", eCommentDelete,
    "<data:comment.deleteUrl/>", "<data:top.deleteCommentMsg/>",
    // <![CDATA[
    var eCommentTemplate = '' +
    '<div class="comment-segment comment-level-${COMMENT.LEVEL} ${BLOG.AUTHOR} ${COMMENT.DELETED.STYLE}" >' + '\n' +
    ' <a name="comment-${COMMENT.ID}"></a>' + '\n' +
    ' <span style="float: right; margin-right: 5px; " >' + '\n' +
    ' <a href="#" ' + '\n' +
    ' onclick="toggleElementDisplays(this, ' +
    '[\'comment-${COMMENT.ID}-body\', \'comment-${COMMENT.ID}-footer\', \'reply-guide-${COMMENT.ID}\'], ' +
    '[\'both\', \'both\', \'hide\']); return false;" >[hide]</a>' + '\n' +
    ' </span>' + '\n' +
    ' <span class="comment-author" >' +
    '<a href="${COMMENT.AUTHOR.URL}" rel="nofollow">' +
    '</a>' +
    '${COMMENT.AUTHOR.URL.EXISTS.END}</span>' + '\n' +
    ' said... ' + '\n' +
    ' <div id="comment-${COMMENT.ID}-body" class="comment-body" ><p>${COMMENT.BODY}</p></div>' + '\n' +
    ' <span class="comment-time">on ${COMMENT.TIMESTAMP}</span>' + '\n' +
    ' <div id="reply-guide-${COMMENT.ID}" class="reply-guide comment-level-0 " >' + '\n' +
    ' <span style="float: right;" ><a href="#" onclick="hideElement(\'reply-guide-${COMMENT.ID}\'); return false;" >[hide]</a></span>' + '\n' +
    ' <h4 class="reply-guide-header">How to Reply to this comment</h4>' + '\n' +
    ' <span>' + '\n' +
    ' To reply to this comment please ensure that <b>one</b> of the following lines: ' + '\n' +
    ' <ul class="reply-guide-list">' + '\n' +
    '<li>@${COMMENT.ID}</li>' + '\n' +
    '<li>@${COMMENT.AUTHOR.NAME}</li>' + '\n' +
    ' </ul>' + '\n' +
    ' is the <b>first line</b> of your comment. ' + '\n' +
    ' <br />' + '\n' +
    ' <a href="${BLOG.POST.COMMENT.LINK}"' + '\n' +
    ' >Click here to enter your reply</a>' + '\n' +
    ' </span>' + '\n' +
    ' </div>' + '\n' +
    ' <div id="comment-${COMMENT.ID}-footer" class="comment-footer">' + '\n' +
    ' <span><a ' +
    'href="#" onclick="showElement(\'reply-guide-${COMMENT.ID}\'); return false;" >Reply</a></span> ' + '\n' +
    ' <span><a href="#comment-${COMMENT.ID}">Permalink</a></span> ' + '\n' +
    ' <span><a href="${COMMENT.DELETE.URL}" title="${COMMENT.DELETE.TEXT}" style="text-decoration: none;" ><span class="delete-comment-icon"> </span></a></span>' + '\n' +
    ' </div>' + '\n' +
    '</div>' + '\n';

    // ]]>
    <p class='comment-footer'>
    <a expr:href='data:post.addCommentUrl' expr:onclick='data:post.addCommentOnclick'><data:postCommentMsg/></a>
    <div id='backlinks-container'>
    <div expr:id='data:widget.instanceId + "_backlinks-container"'>
    <b:if cond='data:post.showBacklinks'>
    <b:include data='post' name='backlinks'/>

Remember to replace the
var BLOG_AUTHOR = 'http://www.blogger.com/profile/onlyauthor';
segment with your appropriate profile url(s).

A sample blogger template is also available in case you want to skip some of the work.

Future work:
Unfortunately the way blogger comments need to be posted it is the responsibilty of the user now to include a @replyTarget line to the top of her comment. Once blogger supports inline comment forms completely (currently it is available only in draft mode) it will be possible to relieve the user of this task and use javascript to auto insert the @replyTarget line into the form.

Feel free to use this in your blogs and let me know your thoughts/bugs you find while using this :)