Tuesday, 7 August 2012

ZK Hibernate one to Many annotation mapping bidirectional CRUD example using MVVM

In this post, we will see how we can implement hibernate one to Many mapping (master & detail) bidirectional using ZK Components

If you new to hibernate, please look the following URL to understand better on one to many mapping in hibernate.
Example1
Example2

Now let us start with ZK.

Step : 1

Let us set up the environment. Follow this post to set up Hibernate with ZK in eclipse IDE.

 

Step : 2

Let us set up the mysql tables as follows.

image_thumb6_thumb[4]


Now let us start creating individual files

Department.java

package domain;


import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.OneToMany;
@Entity
@Table(name = "department")
public class Department implements Cloneable {

@Id
@GeneratedValue
@Column(name = "ID")
private int ID;

@Column(name = "DepartName")
private String departName;


/**
mappedBy – means “I am not the owner side”, I am mapped by Employee from the other side of the relationship.
It will also not create the database column which makes sense, I would expect a foreign key on the Employee table instead.
Here we have used department as mapped by, so there must be property on the Employee Class.

Department has a bidirectional one to many relationship with Employee through the department property.
You don't have to (must not) define any physical mapping in the mappedBy side.

orphanRemoval=true is used because, even though we specified cascadetype all, but if only child records are deleted,
then when you save the master, the child will not delete. for this, we have used orphanRemoval=true.

**/

@OneToMany(fetch=FetchType.EAGER, cascade = CascadeType.ALL, mappedBy="department", orphanRemoval=true)
List<Employees> employees;

public int getID() {
return ID;
}

public void setID(int iD) {
ID = iD;
}

public String getDepartName() {
return departName;
}

public void setDepartName(String departName) {
this.departName = departName;
}

public List<Employees> getEmployees() {
return employees;
}

public void setEmployees(List<Employees> employees) {
this.employees = employees;
}

public Object clone() throws CloneNotSupportedException {
return super.clone();
}


}


Employees.java


package domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.ManyToOne;
import javax.persistence.JoinColumn;


@Entity
@Table(name = "employees")
public class Employees implements Cloneable {

@Id
@GeneratedValue
private int ID;
@Column(name = "firstName")
private String firstName;

@Column(name = "lastName")
private String lastName;

@ManyToOne
@JoinColumn(name="DepartmentID")
private Department department;

public int getID() {
return ID;
}

public void setID(int iD) {
ID = iD;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public Department getDepartment() {
return department;
}

public void setDepartment(Department department) {
this.department = department;
}

public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}


DepartmentDAO.java


package domainDAO;

import java.util.List;

import domain.Department;

import org.hibernate.Query;
import org.hibernate.Session;

import HibernateUtilities.HibernateUtil;

public class DepartmentDAO {

@SuppressWarnings("unchecked")
public List<Department> getAllDepartment() {
List<Department> allrecords = null;
try {
Session session = HibernateUtil.beginTransaction();
Query q1 = session.createQuery("from Department");
allrecords = q1.list();
HibernateUtil.CommitTransaction();
} catch (RuntimeException e) {
e.printStackTrace();
}
return allrecords;
}

public void saveOrUpdate(Department p1) {

try {
Session session = HibernateUtil.beginTransaction();
session.saveOrUpdate(p1);
HibernateUtil.CommitTransaction();
} catch (RuntimeException e) {
e.printStackTrace();
}

}

public Integer delete(Department p1) {
try {
Session session = HibernateUtil.beginTransaction();
session.delete(p1);
HibernateUtil.CommitTransaction();
return 1;
} catch (RuntimeException e) {
e.printStackTrace();
return 0;
}

}
}


DepartmentCRUDVM.java


package domainVMS;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.zkoss.bind.BindUtils;
import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.bind.annotation.ExecutionArgParam;
import org.zkoss.bind.annotation.GlobalCommand;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.select.Selectors;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Window;

import bsh.This;

import domain.Department;
import domain.Employees;
import domainDAO.DepartmentDAO;

public class DepartmentCRUDVM {

@Wire("#DepartmentCRUD")
private Window win;

private boolean makeAsReadOnly;
private Department selectedDepartment;
private List<Employees> employees = new ArrayList<Employees>();
private Employees curSelectedEmployee;
private Integer curSelectedEmployeeIndex;
private String recordMode;

public void setEmployees(List<Employees> employees) {
this.employees = employees;
}

public String getRecordMode() {
return recordMode;
}

public void setRecordMode(String recordMode) {
this.recordMode = recordMode;
}

public Integer getCurSelectedEmployeeIndex() {
return curSelectedEmployeeIndex;
}

public void setCurSelectedEmployeeIndex(Integer curSelectedEmployeeIndex) {
this.curSelectedEmployeeIndex = curSelectedEmployeeIndex;
}

public boolean isMakeAsReadOnly() {
return makeAsReadOnly;
}

public void setMakeAsReadOnly(boolean makeAsReadOnly) {
this.makeAsReadOnly = makeAsReadOnly;
}

public Department getSelectedDepartment() {
return selectedDepartment;
}

public void setSelectedDepartment(Department selectedDepartment) {
this.selectedDepartment = selectedDepartment;
}

public List<Employees> getallEmployees() {
return employees;
}

public Employees getCurSelectedEmployee() {
return curSelectedEmployee;
}

public void setCurSelectedEmployee(Employees curSelectedEmployee) {
this.curSelectedEmployee = curSelectedEmployee;
}

@Init
public void initSetup(@ContextParam(ContextType.VIEW) Component view,
@ExecutionArgParam("sDepartment") Department d1,
@ExecutionArgParam("recordMode") String recordMode)
throws CloneNotSupportedException {
Selectors.wireComponents(view, this, false);

setRecordMode(recordMode);
if (recordMode.equals("NEW")) {
this.selectedDepartment = new Department();
}

if (recordMode.equals("EDIT")) {
this.selectedDepartment = (Department) d1.clone();
setEmployees(d1.getEmployees());
}

if (recordMode == "READ") {
setMakeAsReadOnly(true);
win.setTitle(win.getTitle() + " (Readonly)");
}

}

@Command
public void addNewEmployee() {

final HashMap<String, Object> map = new HashMap<String, Object>();
map.put("sEmployee", null);
map.put("recordMode", "NEW");
Executions.createComponents("EmployeeCRUD.zul", null, map);
}

@Command
public void editThisEmployee() {
final HashMap<String, Object> map = new HashMap<String, Object>();
map.put("sEmployee", this.curSelectedEmployee);
setCurSelectedEmployeeIndex(employees.indexOf(curSelectedEmployee));
map.put("recordMode", "EDIT");
Executions.createComponents("EmployeeCRUD.zul", null, map);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Command
public void deleteThisEmployee() {

String str = "The \""
+ this.curSelectedEmployee.getLastName()
+ "\" will be permanently deleted and the action cannot be undone.";

Messagebox.show(str, "Confirm", Messagebox.OK | Messagebox.CANCEL,
Messagebox.QUESTION, new EventListener() {
@Override
public void onEvent(Event event) throws Exception {
if (((Integer) event.getData()).intValue() == Messagebox.OK) {
employees.remove(curSelectedEmployee);
BindUtils.postNotifyChange(null, null,
DepartmentCRUDVM.this, "allEmployees");
}
}
});
}

@Command
public void save() {
selectedDepartment.setEmployees(employees);
new DepartmentDAO().saveOrUpdate(selectedDepartment);
Map args = new HashMap();
args.put("pDepartment", this.selectedDepartment);
args.put("recordMode", this.recordMode);
BindUtils.postGlobalCommand(null, null, "updateDepartmentInfo", args);
win.detach();
}

@Command
public void closeThis() {
win.detach();
}

@GlobalCommand
@NotifyChange("allEmployees")
public void updateEmployeeInfo(@BindingParam("pEmployee") Employees e1,
@BindingParam("recordMode") String recordMode) {

if (recordMode.equals("EDIT")) {
e1.setDepartment(selectedDepartment);
employees.set(this.curSelectedEmployeeIndex, e1);
}

if (recordMode.equals("NEW")) {
e1.setDepartment(selectedDepartment);
employees.add(e1);
}
}

}


DepartmentListVM.java


package domainVMS;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.zkoss.bind.BindUtils;
import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.GlobalCommand;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zul.Messagebox;
import domain.Department;
import domainDAO.DepartmentDAO;

public class DepartmentListVM {

private List<Department> departments= new ArrayList<Department>();
private Department curSelectedDepartment;
private Integer curSelectedDepartmentIndex;


public Integer getCurSelectedDepartmentIndex() {
return curSelectedDepartmentIndex;
}
public void setCurSelectedDepartmentIndex(Integer curSelectedDepartmentIndex) {
this.curSelectedDepartmentIndex = curSelectedDepartmentIndex;
}
public Department getCurSelectedDepartment() {
return curSelectedDepartment;
}
public void setCurSelectedDepartment(Department curSelectedDepartment) {
this.curSelectedDepartment = curSelectedDepartment;
}

public List<Department> getallDepartments() {
return departments;
}

@Init
public void initSetup() {
departments= new DepartmentDAO().getAllDepartment();
}

@Command
public void addNewDepartment() {
final HashMap<String, Object> map = new HashMap<String, Object>();
map.put("sDepartment", null);
map.put("recordMode", "NEW");
Executions.createComponents("DepartmentCRUD.ZUL", null, map);
}

@Command
public void editThisDepartment()
{
final HashMap<String, Object> map = new HashMap<String, Object>();
map.put("sDepartment", this.curSelectedDepartment);
map.put("recordMode", "EDIT");
setCurSelectedDepartmentIndex(departments.indexOf(curSelectedDepartment));
Executions.createComponents("DepartmentCRUD.ZUL", null, map);
}

//The following method will be called from DepartmentCRUDVM.java after the save
@GlobalCommand
@NotifyChange("allDepartments")
public void updateDepartmentInfo(@BindingParam("pDepartment") Department d1,
@BindingParam("recordMode") String recordMode) {

if (recordMode.equals("EDIT")) {
departments.set(this.curSelectedDepartmentIndex, d1);
}

if (recordMode.equals("NEW")) {
departments.add(d1);
}
}


@SuppressWarnings({ "unchecked", "rawtypes" })
@Command
public void deleteThisDepartment()
{
int OkCancel;

String str = "The Selected \"" + curSelectedDepartment.getDepartName()
+ "\" will be deleted.";
OkCancel = Messagebox.show(str, "Confirm", Messagebox.OK
| Messagebox.CANCEL, Messagebox.QUESTION);
if (OkCancel == Messagebox.CANCEL) {
return;
}

str = "The \""
+ curSelectedDepartment.getDepartName()
+ "\" will be permanently deleted and the action cannot be undone.";

Messagebox.show(str, "Confirm", Messagebox.OK | Messagebox.CANCEL,
Messagebox.QUESTION, new EventListener() {
@Override
public void onEvent(Event event) throws Exception {
if (((Integer) event.getData()).intValue() == Messagebox.OK) {
new DepartmentDAO().delete(curSelectedDepartment);
departments.remove(departments.indexOf(curSelectedDepartment));
BindUtils.postNotifyChange(null, null,
DepartmentListVM.this, "allDepartments");
}
}
});
}

}


EmployeeCRUDVM.java


package domainVMS;

import java.util.HashMap;
import java.util.Map;

import org.zkoss.bind.BindUtils;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.bind.annotation.ExecutionArgParam;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.select.Selectors;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Window;

import domain.Employees;

public class EmployeeCRUDVM {

@Wire("#win")
private Window win;

private Employees selectedEmployee;
private Employees selectedEmployeeOrg;

private boolean makeAsReadOnly;
private String recordMode;


public Employees getSelectedEmployeeOrg() {
return selectedEmployeeOrg;
}

public void setSelectedEmployeeOrg(Employees selectedEmployeeOrg) {
this.selectedEmployeeOrg = selectedEmployeeOrg;
}

public String getRecordMode() {
return recordMode;
}

public void setRecordMode(String recordMode) {
this.recordMode = recordMode;
}

public Employees getSelectedEmployee() {
return selectedEmployee;
}

public void setSelectedEmployee(Employees selectedEmployee) {
this.selectedEmployee = selectedEmployee;
}

public boolean isMakeAsReadOnly() {
return makeAsReadOnly;
}
public void setMakeAsReadOnly(boolean makeAsReadOnly) {
this.makeAsReadOnly = makeAsReadOnly;
}

@Init
@NotifyChange("selectedEmployee")
public void initSetup(@ContextParam(ContextType.VIEW) Component view,
@ExecutionArgParam("sEmployee") Employees selectedEmployee,
@ExecutionArgParam("recordMode") String recordMode) throws CloneNotSupportedException {
Selectors.wireComponents(view, this, false);
setRecordMode(recordMode);


if (recordMode.equals("NEW")) {
this.selectedEmployee = new Employees();
}

if (recordMode.equals("EDIT")) {
System.out.println("Value is " + selectedEmployee.getFirstName());
this.selectedEmployeeOrg = selectedEmployee;
this.selectedEmployee= (Employees) selectedEmployee.clone();
}

if (recordMode == "READ") {
setMakeAsReadOnly(true);
win.setTitle(win.getTitle() + " (Readonly)");
}

}

@Command
public void save() {
Map args = new HashMap();
args.put("pEmployee", this.selectedEmployee);
args.put("recordMode", this.recordMode);
BindUtils.postGlobalCommand(null, null, "updateEmployeeInfo", args);
win.detach();
}

@Command
public void closeThis() {
win.detach();
}
}
HibernateUtil.java


package HibernateUtilities;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class HibernateUtil {

private static SessionFactory factory;
private static ServiceRegistry serviceRegistry;

public static Configuration getInitConfiguration() {
Configuration config = new Configuration();
config.configure();
return config;
}

public static Session getSession() {
if (factory == null) {
Configuration config = HibernateUtil.getInitConfiguration();
serviceRegistry = new ServiceRegistryBuilder().applySettings(
config.getProperties()).buildServiceRegistry();
factory = config.buildSessionFactory(serviceRegistry);
}
Session hibernateSession = factory.getCurrentSession();
return hibernateSession;
}

public static Session beginTransaction() {
Session hibernateSession;
hibernateSession = HibernateUtil.getSession();
hibernateSession.beginTransaction();
return hibernateSession;
}

public static void CommitTransaction() {
HibernateUtil.getSession().getTransaction().commit();
}

public static void closeSession() {
HibernateUtil.getSession().close();
}

public static void rollbackTransaction() {
HibernateUtil.getSession().getTransaction().rollback();
}

}



HibernateUtilities.cfg.xml


<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost/sampledb</property>
<property name="connection.username">root</property>
<property name="connection.password">123</property>

<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>

<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>

<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>

<!-- Disable the second-level cache -->
<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>

<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>



<!-- Mapping Classes -->
<mapping class="domain.Department" />
<mapping class="domain.Employees" />

</session-factory>
</hibernate-configuration>


DepartmentCRUD.ZUL


<?page title="new page title" contentType="text/html;charset=UTF-8"?>
<zk>
<window title="One to Many Hibernate using MVVM" border="normal"
id="DepartmentCRUD" width="430px" height="520px"
apply="org.zkoss.bind.BindComposer" minimizable="false" mode="modal"
maximizable="false" closable="true"
viewModel="@id('vm') @init('domainVMS.DepartmentCRUDVM')">
<separator />
<label value="Department information" />
<separator />
<panel width="100%">
<panelchildren>
<separator />
<grid width="99.5%">
<columns>
<column label="" width="150px" />
<column label="" />
</columns>
<rows>
<row>
<hbox>
<label value="Department Name" />
<label value="*" />
</hbox>
<textbox name="name"
readonly="@load(vm.makeAsReadOnly)"
value="@bind(vm.selectedDepartment.departName)" cols="20" />
</row>
</rows>
</grid>

<separator />
<separator />
<separator />
<separator />

<label value="Employee information" />
<separator />
<div>
<button label="Add New Employee"
onClick="@command('addNewEmployee')" />
</div>
<separator />
<listbox id="test" model="@load(vm.allEmployees)"
selectedItem="@bind(vm.curSelectedEmployee)">
<listhead sizable="true">
<listheader label="First Name" />
<listheader label="Last Name" />
<listheader label="Action" />
</listhead>
<template name="model" var="p1">
<listitem>
<listcell label="@load(p1.firstName)" />
<listcell label="@load(p1.lastName)" />
<listcell>
<hbox spacing="20px">
<image sclass="fimageDelete"
onClick="@command('deleteThisEmployee')" />
<image sclass="fimageedit"
onClick="@command('editThisEmployee')" />
</hbox>
</listcell>
</listitem>
</template>
</listbox>
</panelchildren>
</panel>
<separator />
<div align="center">
<button id="submit" label="Submit"
onClick="@command('save')" visible="@load(not vm.makeAsReadOnly)" />
<button id="cancel"
label="@load(vm.makeAsReadOnly ?'Ok':'Cancel')"
onClick="@command('closeThis')" />
</div>
</window>
</zk>


DepartmentList.zul


<?page title="DepartmentList" contentType="text/html;charset=UTF-8"?>
<zk>
<style>
.z-listcell.red .z-listcell-cnt, .z-label.red{ color:red; }

/* Start: Action Images- Edit
---------------------------------------------- */

.fimageedit { width: 25px; background-image:
url('./images/icon-edit.png'); background-repeat: no-repeat;
border: 0 none; cursor: pointer; }

/* End: Action Images - Edit
---------------------------------------------- */


/* Start: Action Images- Delete
---------------------------------------------- */

.fimageDelete { width: 25px; background-image:
url('./images/icon-trash-red.png'); background-repeat:
no-repeat; border: 0 none; cursor: pointer; }

/* End: Action Images - Delete
---------------------------------------------- */


/* Start: Action Images- Active
---------------------------------------------- */

.fimageActive { width: 25px; background-image:
url('./images/icon-enable.png'); background-repeat: no-repeat;
border: 0 none; cursor: pointer; }

/* End: Action Images - Active
---------------------------------------------- */

/* Start: Action Images- Inactive' ]

---------------------------------------------- */

.fimageInactive { width: 25px; background-image:
url('./images/icon-disable.png'); background-repeat: no-repeat;
border: 0 none; cursor: pointer; }

/* End: Action Images - InActive
---------------------------------------------- */

.z-listcell.highlightcell .z-listcell-cnt,
.z-label.highlightcell { color:blue; cursor: pointer; }


</style>
<window title="One to Many Hibernate using MVVM" border="normal"
apply="org.zkoss.bind.BindComposer"
viewModel="@id('myvm') @init('domainVMS.DepartmentListVM')">

<div>
<button label="Add New Department"
onClick="@command('addNewDepartment')" />
</div>
<separator />

<listbox id="test" model="@load(myvm.allDepartments)"
selectedItem="@bind(myvm.curSelectedDepartment)">
<listhead sizable="true">
<listheader label="Department Name" width="400px"
sort="auto(departName)" />
<listheader label="Action" />
</listhead>
<template name="model" var="p1">
<listitem>
<listcell label="@load(p1.departName)"
onClick="@command('openAsReadOnly')" sclass="highlightcell" />
<listcell>
<hbox spacing="20px">
<image sclass="fimageDelete" onClick="@command('deleteThisDepartment')" />
<image sclass="fimageedit"
onClick="@command('editThisDepartment')" />
</hbox>
</listcell>
</listitem>
</template>
</listbox>
</window>
</zk>


EmployeeCRUD.zul


<zk>
<window id="win" title="Employee" width="520px" height="220px"
border="normal" minimizable="false" mode="modal" maximizable="false"
closable="true" action="show: slideDown;hide: slideUp"
apply="org.zkoss.bind.BindComposer"
viewModel="@id('vm') @init('domainVMS.EmployeeCRUDVM')">
<separator />
<label value="Employee information" />
<separator />
<panel width="100%">
<panelchildren>
<separator />
<grid width="99.5%">
<columns>
<column label="" width="150px" />
<column label="" />
</columns>
<rows>
<row>
<hbox>
<label value="First Name" />
<label value="*" />
</hbox>
<textbox name="name"
readonly="@load(vm.makeAsReadOnly)"
value="@bind(vm.selectedEmployee.firstName)" cols="50" />
</row>
<row>
<hbox>
<label value="Last Name" />
<label value="*" />
</hbox>
<textbox id="txtlastName"
readonly="@load(vm.makeAsReadOnly)"
value="@bind(vm.selectedEmployee.lastName)" />
</row>
</rows>
</grid>
</panelchildren>
</panel>
<separator />
<div align="center">
<button id="submit" label="Submit"
onClick="@command('save')" visible="@load(not vm.makeAsReadOnly)" />
<button id="cancel"
label="@load(vm.makeAsReadOnly ?'Ok':'Cancel')"
onClick="@command('closeThis')" />
</div>
</window>
</zk>




Project Structure

image


 


You can run the departmentlist.zul file and check the output.


 


image


 


You can download the source code here.


You can also download as individual files here.



  1. Department.java
  2. DepartmentCRUD.ZUL
  3. DepartmentCRUDVM.java
  4. DepartmentDAO.java
  5. DepartmentList.zul
  6. DepartmentListVM.java
  7. EmployeeCRUD.zul
  8. EmployeeCRUDVM.java
  9. Employees.java
  10. HibernateUtil.java
  11. icon-edit.png
  12. icon-enable.png
  13. icon-trash-red.png

3 comments:

  1. Many thanks Nathan for the detailed codes.

    best
    TerryTornado

    ReplyDelete
  2. greate tutorial,thanks!

    Ricardo Johannsen

    ReplyDelete
  3. thank you for sharing this informative blog.. this blog really helpful for everyone.. explanation are clear so easy to understand...

    core java training institute in chennai | core java training topics | core java training in chennai | core java training online

    ReplyDelete