2010-11-24

Generic CRUD (Spring 3)

Time to time I have situation where generic CRUD (java, Spring) required
So - let's do it



Generic DAO here.

Main components:
- generic DAO (above)
- generic service
- generic controller
- view & generic models
and smart developer ;)

The good approach - usage jQuery & jqgrid on frontend.

jqgrid allows us to make:
- dynamic grid
- filtering
- create, update, delete, view & search by entities

URLs:
/${appName}/${entity}/management/
/${appName}/${entity}/management/index
/${appName}/${entity}/management/insert
/${appName}/${entity}/management/update
/${appName}/${entity}/management/delete


Controllers:

public abstract class DomainController<T extends AbstractEntity> {

private final Log LOG = LogFactory.getLog(getClass());

public abstract DomainService<T> getDomainService();

/**
* Generic main page controller
*
* @return ModelAndView
*/
@RequestMapping(value = "/management", method = RequestMethod.GET)
public ModelAndView main() {
LOG.debug("main page");
return new ModelAndView(getMainView());
}

protected abstract String getMainView();

/**
* Generic entity list controller (refreshing, sorting, filtering)
*
* @param gridViewModel
* @param response
* @return ResponseGridViewModel - model in JSON for grid filling
*/
@RequestMapping(value = "/management/index", method = RequestMethod.POST)
@ResponseBody
public ResponseGridViewModel<T> index(@RequestBody RequestGridViewModel gridViewModel,
HttpServletResponse response) {
prepareGridViewModel(gridViewModel);
return getDomainService().getFilteredEntitiy(gridViewModel);
}

/**
* Process filtering criteria
*
* @param gridViewModel
*/
protected void prepareGridViewModel(RequestGridViewModel gridViewModel) {
if (!gridViewModel.getCriteria().isEmpty()) {
Map<String, Object> criteriaMap = new HashMap<String, Object>();

for (String key : gridViewModel.getCriteria().keySet()) {
Object value = gridViewModel.getCriteria().get(key);
try {
Method m = null;

try {
m = getDomainService().getPersistentClass().getMethod(constructGetter(key));
} catch (NoSuchMethodException e) {
// it's not GET
// let's try IS
m = getDomainService().getPersistentClass().getMethod(constructIsGetter(key));
}


if (m.getReturnType().equals(value.getClass())) {
criteriaMap.put(key, value);
} else if (Integer.class.equals(m.getReturnType())) {
criteriaMap.put(key, Integer.valueOf((String) value));
} else if (Long.class.equals(m.getReturnType())) {
criteriaMap.put(key, Long.valueOf((String) value));
} else if (Boolean.class.equals(m.getReturnType())) {
criteriaMap.put(key, Boolean.valueOf((String) value));
}

} catch (Exception e) {
// skip method - out of the scope
}
}

gridViewModel.setCriteria(criteriaMap);
}
}

private String constructGetter(String strKey) {
return "get" + WordUtils.capitalize(strKey);
}

private String constructIsGetter(String strKey) {
return "is" + WordUtils.capitalize(strKey);
}

/**
* Insert generic controller
*
* @param entity
* @param response
* @throws IOException
*/
@RequestMapping(value = "/management/insert", method = RequestMethod.POST)
protected void doInsert(@RequestBody T entity, HttpServletResponse response) throws IOException {

entity.setId(null);
LOG.debug("add entity : " + entity);

// validation entity
Map<String, String> failures = validateOnInsert(entity);
if (!failures.isEmpty()) {
response.getWriter().write(validationMessages(failures));
} else {
getDomainService().insert(entity);
}
}

/**
* Update generic controller
*
* @param entity
* @param response
* @throws IOException
*/
@RequestMapping(value = "/management/update", method = RequestMethod.POST)
protected void doUpdate(@RequestBody T entity, HttpServletResponse response) throws IOException {
LOG.debug("update entity : " + entity);

// validation entity
Map<String, String> failures = validateOnUpdate(entity);
if (!failures.isEmpty()) {
response.getWriter().write(validationMessages(failures));
} else {
getDomainService().update(entity);
}
}

/**
* Delete generic controller
*
* @param entityId
* @param response
*/
@RequestMapping(value = "/management/delete", method = RequestMethod.POST)
public void doDelete(@RequestParam("id") long entityId, HttpServletResponse response) {
LOG.debug("remove item ID : " + entityId);
getDomainService().removeEntity(entityId);
}

protected String validationMessages(Map<String, String> failures) {
StringBuffer sb = new StringBuffer();

for (String failureMsg : failures.values()) {
if (sb.length() > 0) {
sb.append("\",");
}
sb.append("\"").append(failureMsg).append("\"");
}

if (failures.size() > 0) {
sb.insert(0, "{\"error\":[");
sb.append("]}");
}

return sb.toString();
}

/**
* Generic entity validation on insert - could be overridden
*
* @param entity
* @return
*/
protected Map<String, String> validateOnInsert(T entity) {
return new HashMap<String, String>();
}

/**
* Generic entity validation on update - could be overridden
*
* @param entity
* @return
*/
protected Map<String, String> validateOnUpdate(T entity) {
return new HashMap<String, String>();
}

}



@Controller(value = "concreteDomainController")
@RequestMapping("/entity")
public class ConcreteDomainController extends DomainController<Entity> {

private String view = "entityIndex";

private ConcreteDomainService domainService;

public void setConcreteDomainService(ConcreteDomainService domainService) {
this.domainService = domainService;
}

@Override
public DomainService<Entity> getDomainService() {
return domainService;
}

@Override
protected String getMainView() {
return view;
}

public void setView(String view) {
this.view = view;
}

}


Models:

public class RequestGridViewModel implements Serializable {

private static final long serialVersionUID = 997675961840479859L;

private Map<String, Object> criteria;

private Boolean search = Boolean.FALSE;

private Integer page = 0;

private Integer rows = 0;

private String order;

private String sort;

public RequestGridViewModel() {
criteria = new HashMap<String, Object>();
}

public Integer getPage() {
return page;
}

public void setPage(Integer page) {
this.page = page;
}

public Integer getRows() {
return rows;
}

public void setRows(Integer rows) {
this.rows = rows;
}

public Boolean getSearch() {
return search;
}

public void setSearch(Boolean search) {
this.search = search;
}

public String getOrder() {
return order;
}

public void setOrder(String order) {
this.order = order;
}

public String getSort() {
return sort;
}

public void setSort(String sort) {
this.sort = sort;
}

public Map<String, Object> getCriteria() {
return criteria;
}

public void setCriteria(Map<String, Object> criteria) {
this.criteria = criteria;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}

@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}

@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}

}



public class ResponseGridViewModel<T> implements Serializable {

private static final long serialVersionUID = -2277041672814148333L;

private List<T> gridData;

private int currPage;

private int totalPages;

private int totalRecords;

public ResponseGridViewModel() {
}

public ResponseGridViewModel(List<T> gridData, int currPage, int totalPages, int totalRecords) {
this.gridData = gridData;
this.currPage = currPage;
this.totalPages = totalPages;
this.totalRecords = totalRecords;
}

public List<T> getGridData() {
return gridData;
}

public void setGridData(List<T> gridData) {
this.gridData = gridData;
}

public int getCurrPage() {
return currPage;
}

public void setCurrPage(int currPage) {
this.currPage = currPage;
}

public int getTotalPages() {
return totalPages;
}

public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}

public int getTotalRecords() {
return totalRecords;
}

public void setTotalRecords(int totalRecords) {
this.totalRecords = totalRecords;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}

@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}

@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}

}


View:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<html>
<head>
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/redmond/jquery-ui-1.8.6.custom.css" />
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/ui.jqgrid.css" />
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/main.css" />

<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-1.4.2.min.js" ></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery.json-2.2.min.js" ></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-ui-1.8.6.custom.min.js" ></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/i18n/grid.locale-en.js" ></script>

<script type="text/javascript">
jQuery.jgrid.no_legacy_api = true;
</script>

<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery.jqGrid.min.js" ></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/js/entityProcessor.js" ></script>
</head>
<body>
<table id="gridContainer"></table>
<div id="pagerGridContainer"></div>
</body>
</html>


entityProcessor.js :

if (typeof (DomainProcessor) == 'undefined') {
var DomainProcessor = {
columns : [],
processInsertResponse : function(response, postdata) {
var success = true;
var msg = "";
if(response.responseText != "") {
var obj = jQuery.parseJSON(response.responseText);
if(obj != null && obj.error !== 'undefined') {
success = false;

msg = "<ul>";
$.each(obj.error, function(index, value) {
msg += "<li>" + value + "</li>";
});
msg += "</ul>";
}
}
return [success, msg, null];
},
processUpdateResponse : function(response, postdata) {
DomainProcessor.processInsertResponse(response, postdata);
},
init : function() {
jQuery("#gridContainer").jqGrid({
ajaxGridOptions : {
type : 'POST',
contentType : 'application/json',
},
serializeGridData : function(postdata) {
if(postdata.search == true) {
if(DomainProcessor.columns.length == 0) {
var colModel = jQuery("#gridContainer").jqGrid('getGridParam','colModel');
$.each(colModel, function(i, item) {
DomainProcessor.columns.push(item.name);
});
}

var transformedPostData = {};
var objectJson = new Object;

$.each(postdata, function(i, item) {
if(-1 != jQuery.inArray(i, DomainProcessor.columns)) {
if(objectJson['criteria'] == undefined) {
objectJson['criteria'] = {};
}
objectJson['criteria'][i] = item;
} else {
objectJson[i] = item;
}
});

postdata = objectJson;
}
return $.toJSON(postdata);
},
url : 'index',
editurl : 'manipulation',
jsonReader: {
root : 'gridData',
page : 'currPage',
total : 'totalPages',
records : 'totalRecords',
repeatitems : false,
},
datatype : 'json',
colNames : [ '#', 'Name', 'Active' ],
colModel : [ {
name : 'id',
index : 'id',
width : 20,
key : true,
sortable: true,
editable: false,
search: true
}, {
name : 'name',
index : 'name',
sortable : true,
editable : true,
edittype : 'text',
editoptions : {size:10, maxlength: 10},
editrules : {required:true},
search: true
}, {
name : 'active',
index : 'active',
width : 80,
editable : true,
edittype : 'checkbox',
editoptions : {value:"true:false"},
search:true,
sortable : false,
} ],
rowNum : 10,
width : 700,
height : 300,
rowList : [ 2, 5, 10 ],
pager : '#pagerGridContainer',
viewrecords : true,
caption : "Entity domain edit",
prmNames : {
search : "search",
order : "order",
oper : "action",
sort : "sort"
}
}).jqGrid('filterToolbar', { searchOnEnter: true });

jQuery("#gridContainer").jqGrid('navGrid',
'#pagerGridContainer', {
edit : true,
add : true,
view : true,
del : true,
search : false
},
// prmEdit, prmAdd, prmDel, prmSearch, prmView
{
closeAfterAdd: true,
closeAfterEdit: true,
viewPagerButtons: false,
top: 100,
left: 100,
url:'update',
afterSubmit : DomainProcessor.processInsertResponse,
ajaxEditOptions : {
type : 'POST',
contentType : 'application/json',
},
serializeEditData: function(data) {
return $.toJSON($.extend({}, data));
}
},
{
closeAfterAdd: true,
closeAfterEdit: true,
viewPagerButtons: false,
afterSubmit : DomainProcessor.processInsertResponse,
top: 100,
left: 100,
url:'insert',
ajaxEditOptions : {
type : 'POST',
contentType : 'application/json',
},
serializeEditData: function(data) {
return $.toJSON($.extend({}, data, {id:0}));
}
},
{url:'delete'},
{},
{viewPagerButtons: false} // view
);
}
};
}

jQuery(document).ready(function() {
DomainProcessor.init();
});


Service:

public interface DomainService<T extends AbstractEntity> {

ResponseGridViewModel<T> getFilteredEntitiy(RequestGridViewModel gridViewModel);

void removeEntity(Long entityId);

void insert(T entity);

void update(T entity);

Class<T> getPersistentClass();

}



public class DomainServiceImpl<T extends AbstractEntity> implements DomainService<T> {

private final Log LOG = LogFactory.getLog(getClass());

private Class<T> persistentClass;

private GenericRepository<T> rep;

public void setGenericRepository(GenericRepository<T> rep) {
this.rep = rep;
}

@Override
public Class<T> getPersistentClass() {
return persistentClass;
}

@SuppressWarnings("unchecked")
public DomainServiceImpl() {
this.persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
}

@Override
public ResponseGridViewModel<T> getFilteredEntitiy(RequestGridViewModel gridViewModel) {
ResponseGridViewModel<T> gridResponse = new ResponseGridViewModel<T>();

Map<String, Object> crits = new HashMap<String, Object>();

int maxResults = gridViewModel.getRows();
int firstResult = ((gridViewModel.getPage() - 1) * maxResults);

// criterias
if (gridViewModel.getSearch() && !gridViewModel.getCriteria().isEmpty()) {
crits.putAll(gridViewModel.getCriteria());
}

if(!gridViewModel.getCriteria().containsKey("active")) {
crits.put("active", Boolean.TRUE);
}

// ordering
String orderByField = (StringUtils.isEmpty(gridViewModel.getSort())) ? "id" : gridViewModel.getSort();
OrderBy orderBy = ("desc".equals(gridViewModel.getOrder())) ? OrderBy.desc(orderByField) : OrderBy.asc(orderByField);

List<T> entities = rep.findByCriteria(getPersistentClass(), crits, firstResult, maxResults, orderBy);
gridResponse.setGridData(entities);

// page counting
Integer totalRecords = rep.findByCriteria(getPersistentClass(), crits);

int totalPages = (totalRecords / maxResults) + (((totalRecords % maxResults) == 0) ? 0 : 1);
gridResponse.setTotalPages(totalPages);
gridResponse.setTotalRecords(totalRecords);

int currentPage = gridViewModel.getPage();

if (currentPage > totalPages) {
currentPage = 1;
}

if (totalPages == 1) {
currentPage = 1;
}

if (totalPages == 0) {
currentPage = 0;
}

gridResponse.setCurrPage(currentPage);

return gridResponse;
}

@Override
public void removeEntity(Long entityId) {
LOG.debug("remove entity ID : " + entityId);

T entity = rep.findById(getPersistentClass(), entityId);

if (entity != null) {
rep.remove(entity);
}
}

@Override
public void insert(T entity) {
rep.add(entity);
}

@Override
public void update(T entity) {
rep.update(entity);
}

}



public interface EntityDomainService extends DomainService<Entity> {

}



@Service
public class EntityDomainServiceImpl extends DomainServiceImpl<Entity> implements EntityDomainService {

}


I hope - this story helps you :)

1 comment:

Gustavo said...

Thanks for share your code with us.

Could you send or post your code (this app example)?