Monday, May 14, 2012

Wicket - Notification Dialog Component

In previous post I've given example of Info Dialog, which is being triggered trough AJAX request, shows notification message over current HTML page, bounces few times and finally disappears.
Java Code below contains the same dialog, but this time it's being re-factored to reusable component - the Notifier.

The Notifier can be included in parent page for all other pages, this parent page will have show-dialog method, which will forward calls to our Notifier panel.


Abstract parent class for all pages - MyParentPage.java

public abstract class MyParentPage extends WebPage {

  private Notifier notifier;

  public MyParentPage() {
    notifier = new Notifier("notifierDialog", new NotifierConfig());
    add(notifier);
  }

  protected void showNotification(AjaxRequestTarget target, String infoHeader,
          String infoContent) {
    notifier.showNotification(target, infoHeader, infoContent);
  }
}


MyParentPage.html

<html>
<body>
  <div wicket:id="notifierDialog" />
  <div class="xyz"></div>
  <wicket:child />
</body>
</html>


Notifier Component - Notifier.java

/**
 * Notify Dialog component - add this panel to any wicket page in order to support ajax
 * notifications.
 * 
 * Integration example:
 * 
 * 1) Add Notifier to markup in your WebPage or Panel
 *    Notifier notifier = new Notifier("notifierDialog", new NotifierConfig());
 *    add(notifier);
 * 
 * 2) Add Notifier as div in corresponding HTML page - on the top of the page
 *    <div wicket:id="notifierDialog" />
 *  
 * 3) In order to show info message call:
 *    #createNotifierMessage(WebMarkupContainer)
 * 
 * @author mmiklas
 */
public class Notifier extends Panel {

  /** Panel configuration */
  private NotifierConfig config;

  /** Parent for all display widgets in this dialog */
  private WebMarkupContainer dialogContainer = null;

  /** HTML ID of {@link #dialogContainer} */
  private String dialogContainerId = null;

  /** Dialog header model - use it to replace dialog's header message */
  Model<String> headerModel = null;

  /** Dialog content model - use it to replace dialogs content text */
  Model<String> messageModel = null;

  /**
   * Displays notification dialog with given header and message.
   * <p>
   * Dialog display command is being rendered as AJAX response
   * {@link AjaxRequestTarget#appendJavaScript(CharSequence)}. This response contains
   * dialog body, which will replace currently empty "notifier"-div, and also jquery
   * code, which plays bounce effect and hides dialog after one second.
   */
  public void showNotification(AjaxRequestTarget target, String header, String message) {
    Validate.notNull(target, "target");
    Validate.notNull(header, "header");
    Validate.notNull(message, "message");

    // update text models
    headerModel.setObject(header);
    messageModel.setObject(message);

    // replace empty #notifier with real content
    target.add(dialogContainer);
    dialogContainer.setVisible(true);

    // after setting dialog to visible play bounce effect
    JQueryEffectBehavior effectBehavior = new JQueryEffectBehavior(dialogContainerId,
            "bounce", config.getBounceTimeMilis());

    // dialog should disappear after one second - I did not
    // find any better way to add callback java script.
    effectBehavior.setCallback(new JQueryAjaxBehavior(this) {

      @Override
      public CharSequence getCallbackScript() {
        String fadeOut = "function(){$('" + dialogContainerId + ":visible').fadeOut();}";
        String callbackScript = "setTimeout(" + fadeOut + ", "
                + Long.toString(config.getNotificationDisplayTimeMilis()) + ");";
        return callbackScript;
      }

      @Override
      protected JQueryEvent newEvent(AjaxRequestTarget target) {
        return null;
      }
    });

    target.appendJavaScript(effectBehavior.toString());
  }

  public Notifier(String id, NotifierConfig config) {
    super(id);
    Validate.notNull(config, "config");
    this.config = config;

    // div containing notify dialog
    dialogContainer = createDialogContainer();
    add(dialogContainer);

    // HTML ID for java script references
    dialogContainerId = "#" + dialogContainer.getMarkupId();

    // initialize jquery
    initEffectLib(dialogContainerId);

    // labels building info dialog
    headerModel = createNotifierHeader(dialogContainer);
    messageModel = createNotifierMessage(dialogContainer);
  }

  /**
   * @return body of the info dialog. HTML ID: "notifierMessage"
   */
  private Model<String> createNotifierMessage(WebMarkupContainer dialogContainer) {
    Model<String> model = new Model<String>();
    Label notifierMessage = new Label("notifierMessage", model);
    dialogContainer.add(notifierMessage);
    return model;
  }

  /**
   * @return header of the info dialog. HTML ID: "notifierHeader"
   */
  private Model<String> createNotifierHeader(WebMarkupContainer dialogContainer) {
    Model<String> model = new Model<String>();
    Label notifierHeader = new Label("notifierHeader", model);
    dialogContainer.add(notifierHeader);
    return model;
  }

  @Override
  protected void onBeforeRender() {
    super.onBeforeRender();

    // showNotification(....) renders #notifier (div) with info dialog
    // content. Java script will hide this dialog on client side after one second
    // (callback after bounce effect).
    // Page refresh would re-render whole HTML page, this would include in this case
    // also #notifier containing recent dialog - wicket component remembers last ajax
    // update on #notifier - and this is the whole dialog.
    //
    // Setting visibility to false on #notifier ensures, that old dialog will not
    // re-appear on page refresh
    dialogContainer.setVisible(false);

  }

  /** Initializes jquery effects library */
  private void initEffectLib(String infoDialogId) {
    add(new JQueryBehavior(infoDialogId, "effect"));
  }

  /**
   * @return hidden dialog container. It must be rendered as empty div, in order to
   *         replace it with info dialog content
   */
  private WebMarkupContainer createDialogContainer() {
    WebMarkupContainer container = new WebMarkupContainer("notifier");
    container.setOutputMarkupId(true);
    container.setOutputMarkupPlaceholderTag(true);
    container.setVisible(false);
    return container;
  }
}


Notifier Config Class

public class NotifierConfig {

  private int notificationDisplayTimeMilis = 1000;

  private int bounceTimeMilis = 500;

  public int getNotificationDisplayTimeMilis() {
    return notificationDisplayTimeMilis;
  }

  public void setNotificationDisplayTimeMilis(int notificationDisplayTimeMilis) {
    if (notificationDisplayTimeMilis < 0) {
      return;
    }
    this.notificationDisplayTimeMilis = notificationDisplayTimeMilis;
  }

  public int getBounceTimeMilis() {
    return bounceTimeMilis;
  }

  public void setBounceTimeMilis(int bounceTimeMilis) {
    if (bounceTimeMilis < 0) {
      return;
    }
    this.bounceTimeMilis = bounceTimeMilis;
  }

}


Notifier.html

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">

<wicket:head>
    <wicket:link>
        <link rel="stylesheet" type="text/css" href="notifier.css" />
    </wicket:link>
</wicket:head>

<body>
    <wicket:panel>
        <div wicket:id="notifier" class="notifier-content">
            <div wicket:id="notifierHeader" class="notifier-header">Message Header</div>
            <div wicket:id="notifierMessage" class="notifier-message">Message Body Text</div>
        </div>
    </wicket:panel>
</body>
</html>


notifier.css

.notifier-content {
    font-size: 12px;
    height: 100px;
    width: 240px;
    padding: 4px;
    position: fixed;
    background-color: #EDEDED;
    border-color: #BFBFBF;
    border-width: 1px;
    border-style: solid;
    border-radius: 4px;
}

.notifier-header {
    background-color: #F5A729;
    border-color: #E78F08;
    border-style: solid;
    border-width: 1px;
    color: white;
    font-weight: bold;
    border-radius: 4px;
    margin: 0;
    padding: 4px;
    text-align: center;
}

.notifier-message {
    padding-top: 6px;
}

Friday, May 11, 2012

Memcached Spring integration

This is very simple example showing how to integrate Spring with memcached.

MemcachedClientFactoryBean is a Spring factory, which creates instance of MemcachedClient. Client class manages connections to memcached server farm, and takes care of .... mostly everything that is required for daily usage ;)

Configured Transcoder serializes Java objects - they are available on memcached server in binary form.

Spring bean below has single injection -  "memcached.client", this is the bean name of the factory, but spring recognises, that injected bean is a factory and does not inject factory itself, but uses it to create Bean instance - in this case memcached client.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"

    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="memcached.client" class="net.spy.memcached.spring.MemcachedClientFactoryBean">
        <property name="servers" value="host1:1122,host2:1122" />
        <property name="protocol" value="BINARY" />
        <property name="transcoder">
            <bean class="net.spy.memcached.transcoders.SerializingTranscoder" />
        </property>
        <property name="locatorType" value="ARRAY_MOD" />
        <property name="opTimeout" value="2000" />
        <property name="failureMode" value="Cancel" />
        <property name="useNagleAlgorithm" value="false" />
        <property name="timeoutExceptionThreshold" value="20" />
    </bean>
</beans> 
@Named
public class MemcachedSpringBeanExample {

    private MemcachedClient memcached;

    @Inject
    protected MemcachedSpringBeanExample(@Named("memcached.client") MemcachedClient memcached) {
        this.memcached = memcached;
    }

    public void doSomething(UasAccountId uasAccountId, LastLoginHistory histiry) {
        memcached.add("my_bean_key", 2000, new Object[] { "val1", "val2" });
    }
}

Wednesday, May 9, 2012

Wicket - disappearing Notification Dialog with bounce effect (jquery)


The example below displays notification dialog, which appears over HTML content, bounces few times and finally disappears. There is also small CSS to give it some nice look.
Such dialog can be used as a central point for asynchronous application notifications.

The implementation is based on single Wicket Web Page - this should be redesigned to reusable component, but simple page is usefull to get an idea.

At the beginning the dialog is hidden - Wicket Web Markup Container is being rendered as empty div (effectDialog).
Clicking on "Show Dialog" button sends Wicket Ajax Event to the server, and as response browser receives new HTML part, which replaces empty effectDialog. This new HTML code contains our Info Dialog, and also jQuery code. This code will play bounce effect once dialog is painted, and hide it after one second.


Java Code

public class FeedbackWebPage extends WebPage {

  public FeedbackWebPage() {
    String infoHeader = "Info Header";
    String infoContent = "Variables containing text content for our dialog "
        + "should be extracted to proper Wicket Model - we keep it simple"
        + " for demonstration proposals";

    // div containing info dialog
    final WebMarkupContainer infoDialog = new WebMarkupContainer("effectDialog");

    // hide dialog, but leave empty HTML tag in order to display it later
    infoDialog.setOutputMarkupId(true);
    infoDialog.setOutputMarkupPlaceholderTag(true);
    infoDialog.setVisible(false);
    add(infoDialog);

    // HTML ID for java script references
    final String infoDialogId = "#" + infoDialog.getMarkupId();

    // initialize jquery
    add(new JQueryBehavior(infoDialogId, "effect"));

    // labels building info dialog
    Label feedbackHeader = new Label("feedbackHeader", infoHeader);
    infoDialog.add(feedbackHeader);
    Label feedbackMessage = new Label("feedbackMessage", infoContent);
    infoDialog.add(feedbackMessage);

    // submit button - it will show our dialog
    Form<Void> form = new Form<Void>("form");
    add(form);
    form.add(new AjaxButton("open-dialog", form) {

      @Override
      protected void onSubmit(AjaxRequestTarget target, Form<?> form) {

        // replace empty #effectDialog with real content
        infoDialog.setVisible(true);
        target.add(infoDialog);

        // after setting dialog to visible play bounce effect
        JQueryEffectBehavior effectBehavior = new JQueryEffectBehavior(infoDialogId,
            "bounce", 500);

        // dialog should disappear after one second 
        effectBehavior.setCallback(new JQueryAjaxBehavior(this) {

          @Override
          public CharSequence getCallbackScript() {

            // add timer, to hide dialog after one second
            // TODO move this JavaScript to notifier.js (next blog post)
            String callbackScript = "setTimeout(function(){$(\"" + infoDialogId
                + ":visible\").fadeOut();}, 1000);";
            return callbackScript;
          }

          @Override
          protected JQueryEvent newEvent(AjaxRequestTarget target) {
            return null;
          }
        });

        String jsEffect = effectBehavior.toString();
        target.appendJavaScript(jsEffect);
      }

      @Override
      protected void onError(AjaxRequestTarget target, Form<?> form) {
      }
    });

  }
}

HTML Page - FeedbackWebPage.html

<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">

<wicket:head>
    <wicket:link>
        <link rel="stylesheet" type="text/css" href="feedbackWebPage.css" />
    </wicket:link>
</wicket:head>

<body>
    <div wicket:id="effectDialog" class="ed-content">
        <div wicket:id="feedbackHeader" class="ed-header">Message Header</div>
        <div wicket:id="feedbackMessage" class="ed-message">Message Body Text</div>
    </div>
    <table border="1">
        <tr><th>COL1</th> <th>COL2</th> <th>COL3</th> </tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
        <tr><td>value 1</td> <td>test message</td> <td>some more text</td></tr>
    </table>
    <form wicket:id="form">
        <button wicket:id="open-dialog">Show Dialog</button>
    </form>
</body>
</html>

CSS - feedbackWebPage.css

.ed-content {
    font-size: 12px;
    height: 100px;
    width: 240px;
    padding: 4px;
    position: fixed;
    background-color: #EDEDED;
    border-color: #BFBFBF;
    border-width: 1px;
    border-style: solid;
    border-radius: 4px;
}

.ed-header {
    background-color: #F5A729;
    border-color: #E78F08;
    border-style: solid;
    border-width: 1px;
    color: white;
    font-weight: bold;
    border-radius: 4px;
    margin: 0;
    padding: 4px;
    text-align: center;
}

.ed-message {
    padding-top: 6px;
}

POM.XML - dependencies

<dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-core</artifactId>
    <version>1.5.5</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.6.2</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.6.2</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-servlet-api</artifactId>
    <version>7.0.22</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.1</version>
</dependency>
<dependency>
    <groupId>com.googlecode.wicket-jquery-ui</groupId>
    <artifactId>jquery-ui-core</artifactId>
    <version>1.1</version>
</dependency>