Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

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 :)

2010-09-27

Example: how to start EclipseLink (or other JPA) transaction declaratively in Spring

Here is example of declarative transaction management with EclipseLink JPA provider in Spring-based application. Similar approach should work with other JPA providers as well.
I use Maven for building so these were the required dependencies:


org.eclipse.persistence
javax.persistence
2.0.0


org.aspectj
aspectjrt
1.6.9


org.springframework
spring-aspects
3.0.4.RELEASE



Here is Spring's application context file:



xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd ">





















Now inside of your.services.package you can have such class:


@Component //instantiate as Spring's bean
public class ProductServiceImpl implements ProductService {
//Trick part #1 - Spring substitutes here entity manager
//and handles it in a way that each thread thinks he's the only one working with this manager.
//For E.M. marked by this annotation declarative transaction management is possible
@PersistenceContext(type = PersistenceContextType.TRANSACTION)
private EntityManager containerEm;

//Trick part #2 - Spring wraps this method in transaction which is handled by containerEm
@Transactional(readOnly=false, propagation=Propagation.REQUIRED)
public void yourMethod() {
...
//Here you can do whatever you want with E.M.:
containerEm.createNativeQuery(...);
//or
em.persist(...);
//etc.
...
}//end of transactional method

}

2009-11-23

Maven: adding profile-dependent resources to project

Recently I needed to use different .properties files in my project depending on whether it runs in IDE at my workstation or is packed as .war file and deployed on production server. Project uses Maven to build itself. I've made some notes about solving this problem which hopefully will help somebody to do something similar.

Profiles can be configured in pom.xml file and then activated either from command line (using -P key)
or by some condition - operating system, environment variables, enabled by default etc.
Notice that few profiles can be activated at the same time (in my application this feature isn't used, but it was the reason why I didn't enable some profile by default).
Profiles can define plugins, properties, modules, resources and lot of other things which are enabled only for specific profile. But in this post we will need only resources settings. Detailed information about maven profiles available in Maven: The Definitive Guide and at Apache Maven Site.

So, suppose we have 3 different directories with main (meaning non-test) resources in our Maven-controlled project:
  • our_project/src/main/resources - common resources
  • our_project/src/main/resources_development - resources only for development version
  • our_project/src/main/resources_production - resources only for production version
Such resources may be .properties-files with data source or logger configuration or something like that. Files from resources directory (for all profiles) and files either from resources_development or resources_production (depending on selected profile) should be placed to resulting web-archive (.war), and, while making their way to archive, to our_project/target/classes (for example, from here Eclipse IDE deploys resource files to local Tomcat) and to our_project/target/our_war_file_name/WEB-INF/classes (our_war_file_name actually replaced by configured name of resulting .WAR file, from here Maven builds .WAR file).

In order to achieve this profiles may be configured this way in pom.xml file:



production



${basedir}/src/main/resources_production


${basedir}/src/main/resources





develop



${basedir}/src/main/resources_development


${basedir}/src/main/resources





This code defines 2 profiles - "production" and "develop". Notice that profile-specific directory goes first - profile-specific resources will be copied first, and if common resources directory contains same files they will NOT be owerwritten.


How these profiles can be used: type mvn clean package -Pproduction to build .WAR file for production environment or mvn clean package -Pproduction to build .WAR file for development environment. Similar options may be configured in Eclipse "Run Configurations" dialogue. Note that if you run production build in your IDE then IDE may deploy production-specific resources on your development environment, so you will need to build development version to get things back. Goal "clean" for our case is really neede only if you build for one profile, and then build for another profile in the same directory - then some resouces may remain from previous profile. If you always build for the same profile in one directory "clean" can be skiped if you want to speed up build.

If you need more advanced resource manipulation you can see options of Maven resource plugin. Its basic configuration in pom.xml looks like this:




maven-resources-plugin
2.4.1

..some configuration...



..execution-specific settings. For example, some settings may be applied to different
phases.




Hopefully, it will help somebody :)

2009-07-10

Hibernate & case sensitivity table names

To manage case sensitivity tables and key strategy in hibernate + spring you should use hibernate NamingStrategy implementation.






...




org.hibernate.dialect.MySQL5Dialect
thread
org.hibernate.transaction.JDBCTransactionFactory



<bean class="a.b.c.CustomNamingStrategy" />





a.b.c.CustomNamingStrategy should implements NamingStrategy or extends already existed implementations
- DefaultNamingStrategy
- ImprovedNamingStrategy
- ..



public class CustomNamingStrategy extends DefaultNamingStrategy {

@Override
public String classToTableName(String className) {
return super.classToTableName(className).toLowerCase();
}

....
}