Friday, May 24, 2013

Implementing a progress bar with SWT and the Observer Design Pattern

In this first article, I will try to give a good solution to implement a progress bar with SWT. To achieve this, we will use the Observer Design Pattern.
   1) Goal
Assume that we have a process which takes long time to finish (more than few seconds). Generally, we can't just leave the user with an ugly spinning cursor. So, the idea here is to show him some information that our program is running and may take sometime to finish. May be, with a simple output message. But this is really old. A good way would be to show him a progress bar indicating in what level is our process.

   2) The Observer Design Pattern

This DP "defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically" (The GoF Design Patterns book).
The one object with changing state is called a subject the objects to be notified/updated are called observers.

In our case, we will have a class responsible for the execution of our process (let's say copying a list of files from one directory to another). On the other side, we have our graphical interface which contains buttons, messages and our progress bar. All of these will be grouped in one class (Window) for the simplicity of this article.
As you would have guessed, our business class (call it FileBusiness) is the subject and the window class (call it MainWindow) will contain observers for this subject.
Note that, if you have already used SWT, Swing or GWT, then you could have been using the Observer DP. In fact, observers are also called listeners and when using a Button (in SWT) you always use:

button.addSelectionListener(new SelectionListener() {
  public void widgetSelected(SelectionEvent event) {//some logic here}
  public void widgetDefaultSelected(SelectionEvent event) {//some logic here}
}
The above code registers a listener (an observer) to actions performed on the button object. If the button is clicked on the listener is notified and executes the defined logic.
For our example, we will implement almost the same concept. We will have a FileBusiness class (the subject) that has a method to register listeners to its changes, like the addSelectionListener method in the button example. Once a file is copied with success, the  FileBusiness class will fire an event to notify all of its registered listener about it. One of these listeners will be an ActionListener defined in our MainWindow class.
Now, I think it's enough talking, and it would be good to go to coding.

   3) The Subject class


package com.raissi.business;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;

public class FileBusiness implements Runnable{
 
 private volatile boolean suspendCopy = false;
 
 private List listeners = new ArrayList();
 
 private String srcDirectory;
 private String destDirectory;
 private int countCopyingErrors;
 private int countCopied;
 private int countTotalFiles;

 @Override
 public void run() {
  if(!suspendCopy){
   doBusiness();
  }
 }
 
 private void doBusiness(){
  Path source = Paths.get(srcDirectory);
  if(!source.toFile().exists()){
   fireEvent("Could not find source directory: "+srcDirectory);
   return;
  }
  Path dest = Paths.get(destDirectory);
  if(!dest.toFile().exists()){
   try {
    Files.createDirectory(dest);
   } catch (IOException e) {
    fireEvent("Could not create destination folder, please check emplacement, permissions etc...");
    suspendCopy = true;
    return;
   }
  }
  DirectoryStream stream;
  try {
   stream = Files.newDirectoryStream(source);
  } catch (IOException e) {
   fireEvent("Could not read contents of directory: " + srcDirectory + " please check permissions");
   suspendCopy = true;
   return;
  }
  countTotalFiles = source.toFile().listFiles().length;
  fireEvent("Found: "+countTotalFiles+" files to copy");
  for (Path file : stream) {
   try {
    if(!suspendCopy){
     Files.copy(file, dest.resolve(file.getFileName()), StandardCopyOption.REPLACE_EXISTING);
     countCopied++;
     fireEvent("Copied file: "+file.getFileName());
    }
   } catch (IOException e) {
    countCopyingErrors++;
    fireEvent("Could not copy file: "+file.getFileName());
   }
  }
  if(!suspendCopy){
   fireEvent("Copying process finished!!!");
  }else{
   fireEvent("Copying process interrupted!!!");
  }
 }
 
 public void stopCopy(){
  suspendCopy = true;
 }
 
 public boolean copySuspended(){
  return suspendCopy;
 }
 
 public void addActionListener(ActionListener listener) {
  //As simple as say hello world, just add the listener to our list.
  //We don't need to know more about it, than that it implements the ActionListener interface.
  listeners.add(listener);
 }
 
 public void fireEvent(String message) {
  //We build an event, that will be sent to all registered listeners notifying them about changes
  ActionEvent event = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, message);
  for(ActionListener listener: listeners){
   listener.actionPerformed(event);
  }
 }

 //Setters and getters:
 public int getCountCopyingErrors() {
  return countCopyingErrors;
 }

 public int getCountCopied() {
  return countCopied;
 }

 public int getCountTotalFiles() {
  return countTotalFiles;
 }

 public void setSrcDirectory(String srcDirectory) {
  this.srcDirectory = srcDirectory;
 }

 public void setDestDirectory(String destDirectory) {
  this.destDirectory = destDirectory;
 }
 

}


We are using thread (Runnable interface) so the the graphical interface does not crash (stop working) until the called business method returns.

   4) The Observer part
Now let's see the MainWindow class:
package com.raissi.ui;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import com.raissi.business.FileBusiness;

public class MainWindow {

 /**
  * Business components
  */
 private FileBusiness fileBusiness;
 private Thread thread;
 /**
  * UI components
  */
 protected final Shell dialog = new Shell();

 /**
  * Launch the application.
  * @param args
  */
 public static void main(String[] args) {
  try {
   MainWindow window = new MainWindow();
   window.open();
  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 /**
  * Open the window.
  */
 public void open() {
  Display display = Display.getDefault();
  createContents();
  dialog.open();
  dialog.layout();
  while (!dialog.isDisposed()) {
   if (!display.readAndDispatch()) {
    display.sleep();
   }
  }
 }

 /**
  * Create contents of the window.
  */
 protected void createContents() {
  dialog.setSize(610, 549);
  dialog.setText("Compression");
  
  final Label txtSampleText = new Label(dialog, SWT.BORDER);
  txtSampleText.setAlignment(SWT.CENTER);
  txtSampleText.setText("Compression non démarrée");
  txtSampleText.setBounds(220, 95, 331, 20);
  
  final Text srcDirectory = new Text(dialog, SWT.BORDER);
  srcDirectory.setBounds(220, 25, 196, 20);
  
  Label lblSrcDirectory = new Label(dialog, SWT.NONE);
  lblSrcDirectory.setBounds(97, 25, 120, 20);
  lblSrcDirectory.setText("Source directory*:");
  
  
  Button btnSrcDirectory = new Button(dialog, SWT.NONE);
  btnSrcDirectory.setBounds(420, 25, 91, 20);
  btnSrcDirectory.setText("Parcourir...");
  btnSrcDirectory.addSelectionListener(new SelectionAdapter() {
   public void widgetSelected(SelectionEvent event) {
    DirectoryDialog directoryDialog = new DirectoryDialog(dialog);
    directoryDialog.setMessage("Select source directory");
    String folderName = directoryDialog.open();
    if (folderName != null) {
     srcDirectory.setText(folderName);
    }
   }
  });
  
  Label lblDestDirectory = new Label(dialog, SWT.NONE);
  lblDestDirectory.setBounds(97, 55, 120, 20);
  lblDestDirectory.setText("Dest. directory*:");
  
  final Text destDirectory = new Text(dialog, SWT.BORDER);
  destDirectory.setBounds(220, 55, 196, 20);
  
  Button btnDestDirectory = new Button(dialog, SWT.NONE);
  btnDestDirectory.setBounds(420, 55, 91, 20);
  btnDestDirectory.setText("Parcourir...");
  btnDestDirectory.addSelectionListener(new SelectionAdapter() {
   public void widgetSelected(SelectionEvent event) {
    DirectoryDialog directoryDialog = new DirectoryDialog(dialog);
    directoryDialog.setMessage("Select destination directory");
    String folderName = directoryDialog.open();
    if (folderName != null) {
     destDirectory.setText(folderName);
    }
   }
  });
  
  final ProgressBar progressBar = new ProgressBar(dialog, SWT.SMOOTH);
  progressBar.setBounds(97, 125, 454, 25);
  progressBar.setMaximum(100);
  progressBar.setSelection(0);
  
  Label lblTotalCopied = new Label(dialog, SWT.NONE);
  lblTotalCopied.setBounds(97, 320, 150, 20);
  lblTotalCopied.setText("Total copied");
  
  Label lblErrors = new Label(dialog, SWT.NONE);
  lblErrors.setBounds(97, 350, 138, 20);
  lblErrors.setText("Errors");
  
  Label lblTotalFiles = new Label(dialog, SWT.NONE);
  lblTotalFiles.setBounds(97, 380, 138, 20);
  lblTotalFiles.setText("Total documents");
  
  final Label varTotalCopied = new Label(dialog, SWT.NONE);
  varTotalCopied.setBounds(319, 320, 70, 20);
  varTotalCopied.setText("0");
  
  final Label varErros = new Label(dialog, SWT.NONE);
  varErros.setBounds(319, 350, 70, 20);
  varErros.setText("0");
  
  final Label varTotalFiles = new Label(dialog, SWT.NONE);
  varTotalFiles.setBounds(319, 380, 70, 20);
  varTotalFiles.setText("0");
  
  
  final Button btnStartCopy = new Button(dialog, SWT.NONE);
  btnStartCopy.setBounds(97, 95, 91, 20);
  btnStartCopy.setToolTipText("Start copy process");
  btnStartCopy.setText("Start");
  final Button btnStopCopy = new Button(dialog, SWT.NONE);
  btnStopCopy.setBounds(97, 95, 91, 20);
  btnStopCopy.setText("Stop");
  btnStopCopy.setVisible(false);

  final Text logText = new Text(dialog, SWT.MULTI|SWT.BORDER|SWT.WRAP|SWT.READ_ONLY|SWT.V_SCROLL);
  logText.setBounds(94, 160, 454, 150);
  
  /**
   * Button events definitions
   */
  
  btnStartCopy.addSelectionListener(new SelectionListener() {

        public void widgetSelected(SelectionEvent event) {
         btnStartCopy.setVisible(false);
         btnStopCopy.setVisible(true);
         logText.clearSelection();
         logText.append("Starting\n");
         fileBusiness = new FileBusiness();
         fileBusiness.setSrcDirectory(srcDirectory.getText());
         fileBusiness.setDestDirectory(destDirectory.getText());
         fileBusiness.addActionListener(new ActionListener() {
     @Override
     public void actionPerformed(final ActionEvent event) {
      Display.getDefault().asyncExec(new Runnable() {
                      public void run() {
                      int total = fileBusiness.getCountTotalFiles();
                      if(total != 0){
                       int percent = fileBusiness.getCountCopied()*100 / total;
                 progressBar.setSelection(percent);
                 varTotalCopied.setText(""+fileBusiness.getCountCopied());
                 varErros.setText(""+fileBusiness.getCountCopyingErrors());
                 varTotalFiles.setText(""+total);
                      }
                logText.append(event.getActionCommand()+"\n");
                if(!fileBusiness.copySuspended()){
                 txtSampleText.setText("Process executing");
                }else{
                 txtSampleText.setText("Process interrupted");
                }
                      }
              });
     }
    });
         thread = new Thread(fileBusiness);
         thread.start();
        }

        public void widgetDefaultSelected(SelectionEvent event) {
         widgetSelected(event);
        }
   });
  
  
  btnStopCopy.addSelectionListener(new SelectionListener() {

        public void widgetSelected(SelectionEvent event) {
    txtSampleText.setText("Process stopped");
    btnStartCopy.setVisible(true);
    btnStopCopy.setVisible(false);
    fileBusiness.stopCopy();
    try {
     thread.join();
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
        }

        public void widgetDefaultSelected(SelectionEvent event) {
         widgetSelected(event);
        }
      });
  
  

 }
}
Please notice:

  1. When registering the listener on the subject object, I used: Display.getDefault().asyncExec(new Runnable().... If you omit it, you will get org.eclipse.swt.SWTException: Invalid thread access
  2. I used in the FileBusiness class the suspendCopy volatile property, so I can manage the thread execution through UI buttons. Especially when a user press Stop during process execution. In this case, if there is a file being copied, it will copied, and then the process execution will be interrupted.


    Conclusion
In this first article, we saw haw to use the Observer Design Pattern to implement a progress bar with SWT. We also used Java 7 new features regarding files operations.
In a next article, I will enhance the business process, with the Spring Batch framework. It's a very powerful framework to execute such batch jobs, especially when number of iterations is large and the operation itself can take longtime.
We will also, see how to execute SSH commands on distant machines through Java regardless of OS Type.
Last thing to say, please feel free to copy or comment this content.




No comments:

Post a Comment