Not so long ago I was asked to write test application for working with Loans. Conditions :
1. XML-less spring
2. SpringMVC for rests
3. Two entity in application:Loan and User. Loan Can be extended with rate 1.5*previous rate. In case of more than 3 operation from one IP - reject operation.
4. Code have to be covered by unit and integration tests with ability for being executed from command line.
May be it will be useful for somebody, so I decided to publish it. Created application is very big so in post I will show only main classes, but entire application can be downloaded by link provided at the end of the post.
First, simple interface for entities which have to be stored in DB:
Loan entity:
mvn test - run unit tests
mvn integration-test - run integration tests
Full source code can be downloaded from here.
1. XML-less spring
2. SpringMVC for rests
3. Two entity in application:Loan and User. Loan Can be extended with rate 1.5*previous rate. In case of more than 3 operation from one IP - reject operation.
4. Code have to be covered by unit and integration tests with ability for being executed from command line.
May be it will be useful for somebody, so I decided to publish it. Created application is very big so in post I will show only main classes, but entire application can be downloaded by link provided at the end of the post.
1. pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demien</groupId> <artifactId>springmvcrest</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>Spring MVC Rest example</name> <properties> <spring.version>3.2.0.RELEASE</spring.version> <jetty.version>8.1.15.v20140411</jetty.version> <slf4j.version>1.7.5</slf4j.version> <hibernate.version>4.2.5.Final</hibernate.version> <h2.version>1.3.173</h2.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <integration.test.package>**/com/demien/springmvcrest/integration/test/**</integration.test.package> </properties> <dependencies> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0</version> </dependency> <!-- HIBERNATE --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <!-- H2 DB --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${h2.version}</version> </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> <exclusions> <exclusion> <artifactId>commons-logging</artifactId> <groupId>commons-logging</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <!-- NOXML Sping library --> <dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>2.2</version> </dependency> <!-- Jetty embedded --> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>${jetty.version}</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> <!-- JSON marshaller --> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-jaxrs</artifactId> <version>1.9.11</version> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.11</version> </dependency> <!-- Testing --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.3.3</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> </dependency> </dependencies> <build> <finalName>springmvcrest</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.5</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.13</version> <configuration> <includes> <include>**/*IT.java</include> </includes> </configuration> <executions> <execution> <id>failsafe-integration-tests</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <profiles> <profile> <id>integration</id> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.5</version> <configuration> <excludes> <exclude>**/*$*</exclude> </excludes> </configuration> </plugin> </plugins> </build> </profile> </profiles> </project>
2. domains
Domains objects are very simple, so only one(of two) will be shown.First, simple interface for entities which have to be stored in DB:
public interface IPersistable { Integer getId(); void setId(Integer id); }
Loan entity:
package com.demien.springmvcrest.domain; import java.io.Serializable; import java.util.Date; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Entity; @Entity(name = "LOAN") public class Loan implements Serializable, IPersistable { public static enum STATE { OPEN, CLOSED, EXTENDED } private static final long serialVersionUID = 7566184847780781908L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "ID") private Integer id; @Column(name = "IP_ADDR") private String ipAddr; @Column(name = "AMOUNT") private Float amount; @Column(name = "RATE") private Float rate; @Column(name = "STATE") private String state; @Column(name = "DEADLINE") private Date deadLine; @Column(name = "PARENT_ID") private Integer parentId; @Column(name = "CREATE_DATE") private Date createDate; @Column(name = "CHANGE_DATE") private Date changeDate; @ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name= "USER_ID") private User user; public Loan() { } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getIpAddr() { return ipAddr; } public void setIpAddr(String ipAddr) { this.ipAddr = ipAddr; } public Float getAmount() { return amount; } public void setAmount(Float amount) { this.amount = amount; } public Float getRate() { return rate; } public void setRate(Float rate) { this.rate = rate; } public String getState() { return state; } public void setState(String state) { this.state = state; } public Date getDeadLine() { return deadLine; } public void setDeadLine(Date deadLine) { this.deadLine = deadLine; } public Integer getParentId() { return parentId; } public void setParentId(Integer parentId) { this.parentId = parentId; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public Date getChangeDate() { return changeDate; } public void setChangeDate(Date changeDate) { this.changeDate = changeDate; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Loan clone() { Loan loan = new Loan(); loan.setAmount(new Float(this.getAmount())); loan.setDeadLine(new Date(this.getDeadLine().getTime())); loan.setIpAddr(new String(this.getIpAddr())); if (this.getParentId() != null) { loan.setParentId(new Integer(this.getParentId())); } loan.setRate(new Float(this.getRate())); loan.setState(new String(this.getState())); loan.setCreateDate(new Date()); return loan; } @Override public boolean equals(Object object) { if (!(object instanceof Loan)) { return false; } else { Loan loan = (Loan) object; if (loan.getId().equals(this.getId()) && loan.getIpAddr().equals(this.getIpAddr()) && loan.getCreateDate().equals(this.getCreateDate())) { return true; } return false; } } @Override public String toString() { return "Loan [id=" + id + ", ipAddr=" + ipAddr + ", amount=" + amount + ", rate=" + rate + ", state=" + state + ", deadLine=" + deadLine + ", parentId=" + parentId + ", createDate=" + createDate + ", changeDate=" + changeDate + "]"; } @Override public int hashCode() { return getId(); } }
3. DAO
- Used generic dao, like in my previous posts.3.1 Interface
package com.demien.springmvcrest.dao; import java.util.List; import java.util.Map; public interface IBaseDAO<T> { public T get(Integer id); public T save(T object); public void update(T object); public void delete(T object); public List<T> query(String hsql, Map<String, Object> params); }
3.2 Implementation
package com.demien.springmvcrest.dao; import java.util.List; import java.util.Map; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; public class BaseDAOImpl<T> implements IBaseDAO<T> { private SessionFactory sessionFactory; private Class<T> cl; public BaseDAOImpl(Class<T> cl, SessionFactory sessionFactory) { this.cl=cl; this.sessionFactory = sessionFactory; } @Override public T get(Integer id) { Session session = sessionFactory.getCurrentSession(); @SuppressWarnings("unchecked") T element = (T) session.get(cl, id); return element; } @SuppressWarnings("unchecked") @Override public T save(T object) { Session session = sessionFactory.getCurrentSession(); T result=(T)session.save(object); return result; } @Override public void update(T object) { Session session = sessionFactory.getCurrentSession(); session.update(object); } @Override public void delete(T object) { Session session = sessionFactory.getCurrentSession(); session.delete(object); } @SuppressWarnings("unchecked") @Override public List<T> query(String hsql, Map<String, Object> params) { Session session = sessionFactory.getCurrentSession(); Query query = session.createQuery(hsql); if (params != null) { for (String i : params.keySet()) { query.setParameter(i, params.get(i)); } } List<T> result = null; if ((hsql.toUpperCase().indexOf("DELETE") == -1) && (hsql.toUpperCase().indexOf("UPDATE") == -1) && (hsql.toUpperCase().indexOf("INSERT") == -1)) { result = query.list(); } else { query.executeUpdate(); } return result; } }
4. Service level
The same as in y previous posts: BaseService(interface and implementation) for common actions and "specific" services for "specific" actions related with entities - only one of two will be shown.4.1 BaseService interface
package com.demien.springmvcrest.service; import java.util.List; import com.demien.springmvcrest.dao.IBaseDAO; public interface IBaseService<T> extends IBaseDAO<T> { List<T> getAll(); void deleteAll(); }
4.2 BaseService implementation
package com.demien.springmvcrest.service; import java.util.List; import java.util.Map; import org.springframework.transaction.annotation.Transactional; import com.demien.springmvcrest.dao.IBaseDAO; public abstract class BaseServiceImpl<T> implements IBaseService<T> { private IBaseDAO<T> dao; private Class<T> cl; public BaseServiceImpl(Class<T> cl) { this.cl=cl; } public void setDao(IBaseDAO<T> dao) { this.dao=dao; } @Transactional @Override public T get(Integer id) { return (T) dao.get(id); } @Transactional @Override public T save(T object) { T result=(T)dao.save(object); return result; } @Transactional @Override public void update(T object) { dao.update(object); } @Transactional @Override public void delete(T object) { dao.delete(object); } @Transactional @Override public List<T> query(String hsql, Map<String, Object> params) { return (List<T>)dao.query(hsql, params); } @Transactional @Override public List<T> getAll() { return query("from "+cl.getName(), null); } @Transactional @Override public void deleteAll() { query("delete from "+cl.getName(),null); } }
4.2 Loan service interface - added specific to Loan entity methods:
package com.demien.springmvcrest.service; import com.demien.springmvcrest.domain.Loan; public interface ILoanService extends IBaseService<Loan> { void extendLoan(Integer id); String checkLoan(final String ipAddr, final Float amount); void setCheckHourFrom(final int checkHourFrom); void setCheckHourTo(final int checkHourTo); }
4.3 Loan Service implementation
package com.demien.springmvcrest.service; import static com.demien.springmvcrest.AppConst.CHECK_MAX_AMOUNT; import static com.demien.springmvcrest.AppConst.CHECK_MAX_IP_DAY_REQUESTS; import static com.demien.springmvcrest.AppConst.REJECT_REASON_BAD_TIME; import static com.demien.springmvcrest.AppConst.REJECT_REASON_IP_DAY_ATTEMPTS; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.demien.springmvcrest.AppConst; import com.demien.springmvcrest.dao.IBaseDAO; import com.demien.springmvcrest.domain.Loan; @Service public class LoanServiceImpl extends BaseServiceImpl<Loan> implements ILoanService { public final static String QUERY_IP_COUNT="from "+Loan.class.getName()+" where ipAddr=:ipAddr and createDate>=:dateFrom and createDate<=:dateTo"; private static int checkHourFrom; private static int checkHourTo; public LoanServiceImpl() { super(Loan.class); } @Autowired @Qualifier("loanDAOImpl") public void setDao(final IBaseDAO<Loan> dao) { super.setDao(dao); } public void setCheckHourFrom(final int checkHourFrom) { LoanServiceImpl.checkHourFrom = checkHourFrom; } public void setCheckHourTo(final int checkHourTo) { LoanServiceImpl.checkHourTo = checkHourTo; } @SuppressWarnings("deprecation") public boolean isTimeInHourInterval(final int from, final int to) { Date d = new Date(); int hour = d.getHours(); if (hour >= from && hour <= to) { return true; } else { return false; } } @SuppressWarnings("deprecation") public boolean isDateToday(final Date loanDate) { Date d = new Date(); if (loanDate.getYear() == d.getYear() && loanDate.getMonth() == d.getMonth() && loanDate.getDay() == d.getDay()) { return true; } else { return false; } } public int getIpRequestTodayCount(final String ipAddr) { Map<String,Object> params=new HashMap<String,Object>(); params.put("ipAddr", ipAddr); Date dateTo=new Date(); Date dateFrom=new Date(dateTo.getTime()-AppConst.DAY_SECONDS); params.put("dateFrom", dateFrom); params.put("dateTo", dateTo); List<Loan> loans=query(QUERY_IP_COUNT, params); return loans.size(); } @Transactional @Override public String checkLoan(final String ipAddr, final Float amount) { StringBuilder result = new StringBuilder(); // 1 check by max amount and hours if (amount >= CHECK_MAX_AMOUNT && isTimeInHourInterval(checkHourFrom, checkHourTo)) { result.append(REJECT_REASON_BAD_TIME); } // 2 check by ip addr if (getIpRequestTodayCount(ipAddr) >= CHECK_MAX_IP_DAY_REQUESTS) { result.append(REJECT_REASON_IP_DAY_ATTEMPTS); } return result.toString(); } @Transactional @Override public void extendLoan(final Integer id) { Loan oldLoan=get(id); Loan newLoan=oldLoan.clone(); oldLoan.setState(Loan.STATE.EXTENDED.toString()); oldLoan.setChangeDate(new Date()); newLoan.setRate(newLoan.getRate()*AppConst.EXTEND_RATE_MUL); Date d=new Date(); d.setTime(d.getTime()+AppConst.EXTEND_INTERVAL); newLoan.setDeadLine(d); newLoan.setParentId(id); update(oldLoan); save(newLoan); } }
5. Controllers
5.1 BaseController - for common CRUD operations on entities.
package com.demien.springmvcrest.controller; import java.util.List; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.demien.springmvcrest.domain.IPersistable; import com.demien.springmvcrest.service.IBaseService; import com.demien.springmvcrest.utils.JsonHelper; import com.demien.springmvcrest.utils.JsonHelper.JsonHelperException; import static com.demien.springmvcrest.AppConst.*; public class BaseController<T extends IPersistable> { private IBaseService<T> service; public static final String SERVICE_HELLO="sayhello"; public static final String SERVICE_GET_ALL="getall"; public static final String SERVICE_GETBYID="getbyid"; public static final String SERVICE_ADD="add"; public static final String SERVICE_UPDATE="update"; public static final String SERVICE_DELETE="delete"; private Class<T> cl; public BaseController(Class<T> cl) { this.cl=cl; } public void setService(IBaseService<T> service) { this.service = service; } @RequestMapping(method=RequestMethod.GET, value="/sayhello") @ResponseBody public String sayHello() { System.out.println("Executing sayHello"); return HELLO; } @RequestMapping(method=RequestMethod.GET, value="/getall") public @ResponseBody String getAll() throws JsonHelperException { List<T> list = service.getAll(); String json=JsonHelper.object2json(list); return json; } private T getEntityById(Integer id) { return service.get(id); } @RequestMapping(method=RequestMethod.GET, value="/getbyid") public @ResponseBody String getEntity(@RequestParam("id") Integer id) throws JsonHelperException { T result =service.get(id); String resultJson=JsonHelper.object2json(result); return resultJson; } protected T addEntity(T entity) { Object o=service.save(entity); if (o instanceof Integer) { Integer id=(Integer)o; entity.setId(id); } return entity; } @SuppressWarnings("unchecked") @RequestMapping(method=RequestMethod.POST, value="/add") public @ResponseBody String addEntity(@RequestBody String json) throws JsonHelperException { T entity=(T)JsonHelper.Json2Object(json, cl); if (entity==null) { throw new RuntimeException("addEnity Error: entity is null"); } T searchResult = getEntityById(entity.getId()); if (searchResult == null) { entity=addEntity(entity); } String resultJson=JsonHelper.object2json(entity); return resultJson; } protected void updateEntity(T entity) { service.update(entity); } @SuppressWarnings("unchecked") @RequestMapping(method=RequestMethod.POST, value="/update") public @ResponseBody String updateEntity(@RequestBody String json) throws JsonHelperException { T entity=(T)JsonHelper.Json2Object(json, cl); updateEntity(entity); String resultJson=JsonHelper.object2json(entity); return resultJson; } protected void deleteEntity(T entity) { service.delete(entity); } @SuppressWarnings("unchecked") @RequestMapping(method=RequestMethod.POST, value="/delete") public @ResponseBody void deleteEntity(@RequestBody String json) throws JsonHelperException { T entity=(T)JsonHelper.Json2Object(json, cl); deleteEntity(entity); } }
5.2. "specific" controller example - Loan Controller.
package com.demien.springmvcrest.controller; import static com.demien.springmvcrest.AppConst.*; import java.util.Date; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import com.demien.springmvcrest.domain.Loan; import com.demien.springmvcrest.domain.User; import com.demien.springmvcrest.service.ILoanService; import com.demien.springmvcrest.service.IUserService; import com.demien.springmvcrest.utils.JsonHelper; import com.demien.springmvcrest.utils.JsonHelper.JsonHelperException; @Controller @RequestMapping(value = "/loan") public class LoanController extends BaseController<Loan> { private ILoanService loanService; private IUserService userService; // services public static final String SERVICE_MAIN = "loan"; public static final String SERVICE_CREATE_LOAN = "createLoan"; public static final String SERVICE_EXTEND_LOAN = "extendLoan"; public LoanController() { super(Loan.class); } @Autowired public void setLoanService(ILoanService loanService) { this.loanService = loanService; super.setService(loanService); } @Autowired public void setUserService(IUserService userService) { this.userService = userService; } @RequestMapping(method=RequestMethod.GET, value="/createLoan") public @ResponseBody String createLoan(@RequestParam final String amount, @RequestParam final String date, @RequestParam final Integer userId, HttpServletRequest request) { String ipAddr = ""; ipAddr=request.getRemoteAddr(); Loan loan = new Loan(); // checks for input params StringBuilder error = new StringBuilder(); if (amount == null || amount.length() < 1) { error.append(ERROR_AMOUNT_IS_NULL); } if (date == null || amount.length() < 1) { error.append(ERROR_DATE_IS_NULL); } try { loan.setAmount(Float.parseFloat(amount)); } catch (Exception e) { error.append(ERROR_WRONG_AMOUNT_FORMAT); } try { loan.setDeadLine(FORMATTER.parse(date)); } catch (Exception e) { error.append(ERROR_WRONG_DATE_FORMAT); } User user = userService.get(userId); if (user == null) { error.append(ERROR_WRONG_USER_ID); } // main check procedure error.append(loanService.checkLoan(ipAddr, loan.getAmount())); if (error.length() == 0) { loan.setRate(DEFAULT_RATE); loan.setIpAddr(ipAddr); loan.setState(Loan.STATE.OPEN.toString()); loan.setCreateDate(new Date()); loan.setUser(user); addEntity(loan); return RESULT_OK; } else { return RESULT_REJECTED + error.toString(); } } @RequestMapping(method=RequestMethod.GET, value="/extendLoan") public @ResponseBody String extendloan(@RequestParam("id") final Integer id) throws JsonHelperException { String json=getEntity(id); Loan loan=(Loan) JsonHelper.Json2Object(json, Loan.class); Date now=new Date(); if (loan.getDeadLine().getTime()<now.getTime()) { return ERROR_EXTEND_DEADLINE_EXCEED; } loanService.extendLoan(id); return RESULT_EXTENDED; } }
6. Configuration
6.1. Main config
package com.demien.springmvcrest.config; import java.util.Properties; import org.hibernate.SessionFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.orm.hibernate4.HibernateTransactionManager; import org.springframework.orm.hibernate4.LocalSessionFactoryBean; import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.interceptor.TransactionAttributeSource; import com.demien.springmvcrest.dao.BaseDAOImpl; import com.demien.springmvcrest.dao.IBaseDAO; import com.demien.springmvcrest.domain.Loan; import com.demien.springmvcrest.domain.User; @Configuration @EnableTransactionManagement @ComponentScan(basePackages = "com.demien.springmvcrest") public class AppConfig { // ---------- DAO ------------------ @Bean public DriverManagerDataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); Properties p = new Properties(); dataSource.setConnectionProperties(p); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setUrl("jdbc:h2:~/test"); dataSource.setUsername("sa"); dataSource.setPassword("sa"); return dataSource; } @SuppressWarnings("rawtypes") @Bean public LocalSessionFactoryBean sessionFactoryBean() { LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource()); Class[] annotatedClasses = new Class[] { User.class, Loan.class }; sessionFactoryBean.setAnnotatedClasses(annotatedClasses); Properties p = new Properties(); p.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); p.put("hibernate.show_sql", "true"); p.put("hibernate.hbm2ddl.auto", "create"); sessionFactoryBean.setHibernateProperties(p); return sessionFactoryBean; } @Bean public SessionFactory sessionFactory() { return sessionFactoryBean().getObject(); } @Bean public HibernateTransactionManager txManager() { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(sessionFactory()); return txManager; } @Bean public TransactionAttributeSource annotationTransactionAttributeSource() { return new AnnotationTransactionAttributeSource(); } @Bean public IBaseDAO<Loan> loanDAOImpl() { IBaseDAO<Loan> loanDAOImpl = new BaseDAOImpl<Loan>(Loan.class, sessionFactory()); return loanDAOImpl; } @Bean public IBaseDAO<User> userDAOImpl() { IBaseDAO<User> userDAOImpl = new BaseDAOImpl<User>(User.class, sessionFactory()); return userDAOImpl; } }
6.2. Configuration for enabling MVC
package com.demien.springmvcrest.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration @EnableWebMvc @Profile("container") public class WebMvcConfig extends WebMvcConfigurerAdapter { }
7. Utilities
7.1 JsonJelper - for converting Object->Json->Object
package com.demien.springmvcrest.utils; import java.util.List; import org.codehaus.jackson.map.ObjectMapper; public class JsonHelper { private static ObjectMapper mapper = new ObjectMapper(); @SuppressWarnings("rawtypes") public static Object Json2Object(String json, Class cl) throws JsonHelperException { return Json2ObjectMain(json, cl, false); } @SuppressWarnings("rawtypes") public static Object Json2ObjectList(String json, Class cl) throws JsonHelperException{ return Json2ObjectMain(json, cl, true); } @SuppressWarnings({ "rawtypes", "unchecked" }) private static Object Json2ObjectMain(String json, Class cl, boolean asList) throws JsonHelperException { if (json==null || json.length()==0) { return null; } Object result=null; try { if (asList) { result=mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, cl)); } else { result=mapper.readValue(json, cl); } } catch (Exception e) { throw new JsonHelper.JsonHelperException("Exception in Json2Object. Exception:"+e.getMessage()+". JSON="+json); } return result; } public static String object2json(Object object) throws JsonHelperException { String result=""; try { result=mapper.writeValueAsString(object); } catch (Exception e){ throw new JsonHelper.JsonHelperException("Exception in object2json. Exception:"+e.getMessage()+"."); } return result; } public static class JsonHelperException extends Exception { private static final long serialVersionUID = -7568626836534766197L; public JsonHelperException(String message) { super(message); } } }
7.2. Object data populator - for tests - fill object fields with random values
package com.demien.springmvcrest.utils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import com.demien.springmvcrest.domain.IPersistable; public class ObjectDataPopulator { public static Object populate(final IPersistable instance) throws Exception { instance.setId(getRandomInteger()); List<Field> fields = getAllFields(instance); for (Field eachField : fields) { eachField.setAccessible(true); if (eachField.getType().equals(Integer.class)) { eachField.set(instance, getRandomInteger()); } else if (eachField.getType().equals(String.class)) { eachField.set(instance, getRandomString()); } else if (eachField.getType().equals(Float.class)) { eachField.set(instance, getRandomFloat()); } else if (eachField.getType().equals(Date.class)) { eachField.set(instance, getRandomDate()); } else if (eachField.getType().equals(Set.class)) { // here should be generators for other standard types like Float, Long.... } else { // processing aggregated objects if (eachField.getType().newInstance() instanceof IPersistable) { //eachField.set(instance, populate((IPersistable) eachField.getType().newInstance())); } } } return instance; } private static Integer getRandomInteger() { return new Integer((int) (Math.random() * 1000)); } private static Float getRandomFloat() { return new Float( Math.random() * 1000+Math.random()); } private static Date getRandomDate() { return new Date( new Date().getTime() - (int)(Math.random() * 1000*60*60*24*100) ); } private static String getRandomString() { StringBuffer result = new StringBuffer(); String[] letters = new String[] { "A", "B", "C", "D", "E", "F", "G" }; int length = (int) (Math.random() * 15) + 5; for (int i = 0; i < length; i++) { int pos = (int) (Math.random() * letters.length); result.append(letters[pos]); } return result.toString(); } private static List<Field> getAllFields(final Object instance) { Field[] fields = instance.getClass().getDeclaredFields(); List<Field> result = new ArrayList<Field>(); for (int i = 0; i < fields.length; i++) { if (!java.lang.reflect.Modifier.isFinal(fields[i].getModifiers()) && !java.lang.reflect.Modifier.isStatic(fields[i] .getModifiers())) { result.add(fields[i]); } } return result; } }
7.3. RestTestClient - client for calling rest methods and transform results into proper objects
package com.demien.springmvcrest.utils; import java.io.BufferedReader; import org.apache.http.NameValuePair; import java.io.IOException; import java.io.InputStreamReader; import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import com.demien.springmvcrest.utils.JsonHelper.JsonHelperException; public class RestTestClient { public static final int RESPONSE_OK = 200; public String getResponseData(HttpResponse response) throws Exception { StringBuilder result = new StringBuilder(); try { BufferedReader rd = new BufferedReader(new InputStreamReader( response.getEntity().getContent())); String line = ""; while ((line = rd.readLine()) != null) { result.append(line); } } catch (Exception e) { throw new Exception("Exception in getResponseData:" + e.getMessage() + "."); } return result.toString(); } @SuppressWarnings("rawtypes") public Object Json2Object(String src, Class cl, boolean asList) throws JsonHelperException { Object result = null; if (asList) { result = JsonHelper.Json2ObjectList(src, cl); } else { result = JsonHelper.Json2Object(src, cl); } return result; } public HttpResponse sendGet(String url) throws ClientProtocolException, IOException { HttpClient client = HttpClientBuilder.create().build(); HttpGet get = new HttpGet(url); get.setHeader( "Accept", "text/html,application/xhtml+xml,application/xml,application/json;q=0.9,*/*;q=0.8"); get.setHeader("Content-Type", "application/json"); HttpResponse response = client.execute(get); return response; } public String sendGetStringResult(String url) throws Exception { return getResponseData(sendGet(url)); } @SuppressWarnings("rawtypes") public Object sendGetObjectResult(String url, Class cl) throws Exception { return sendGetObjectResult(url, cl, false); } @SuppressWarnings("rawtypes") public Object sendGetObjectResult(String url, Class cl, boolean asList) throws Exception { String src = sendGetStringResult(url); Object result = null; if (asList) { result = JsonHelper.Json2ObjectList(src, cl); } else { result = JsonHelper.Json2Object(src, cl); } return result; } public HttpResponse sendPost(String url, List<NameValuePair> postParams, Object object) throws Exception { HttpClient client = HttpClientBuilder.create().build(); HttpPost post = new HttpPost(url); post.setHeader("Accept", "text/html,application/xhtml+xml,application/xml, application/json;q=0.9,*/*;q=0.8"); if (postParams != null) { post.setEntity(new UrlEncodedFormEntity(postParams)); post.setHeader("Content-Type", "application/x-www-form-urlencoded"); } if (object!=null) { String json = JsonHelper.object2json(object); StringEntity input = new StringEntity(json); input.setContentType("application/json"); post.setEntity(input); post.setHeader("Content-Type", "application/json"); } HttpResponse response = client.execute(post); return response; } public HttpResponse sendPost(String url, List<NameValuePair> postParams) throws Exception { return sendPost(url, postParams, null); } public HttpResponse sendPost(String url, Object object) throws Exception { return sendPost(url, null, object); } public String sendPostStringResult(String url, List<NameValuePair> postParams) throws IllegalStateException, IOException, Exception { return getResponseData(sendPost(url, postParams)); } @SuppressWarnings("rawtypes") public Object sendPostObjectResult(String url, List<NameValuePair> postParams, Class cl) throws IllegalStateException, IOException, Exception { return sendPostObjectResult(url, postParams, cl, false); } @SuppressWarnings("rawtypes") public Object sendPostObjectResult(String url, List<NameValuePair> postParams, Class cl, boolean asArray) throws IllegalStateException, IOException, Exception { String src = sendPostStringResult(url, postParams); return Json2Object(src, cl, asArray); } }
7.4. RestTestServer - starting application on embedded jetty server
package com.demien.springmvcrest.utils; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.springframework.core.io.ClassPathResource; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import java.io.IOException; public class RestTestServer { public static final int TEST_PORT = 8080; public static final String TEST_CONTEXT = "/"; private static final String CONFIG_LOCATION = "com.demien.springmvcrest.config"; private static final String MAPPING_URL = "/*"; private String mode; private Server server; public RestTestServer() { } public RestTestServer(final String mode) { this.mode = mode; } public void start() throws Exception { //LOGGER.debug("Starting server at port :"+ TEST_PORT); server = new Server(TEST_PORT); server.setHandler(getServletContextHandler(getContext())); server.start(); System.out.println("Server started at http://localhost:" + TEST_PORT + TEST_CONTEXT); //LOGGER.info("Server started at http://localhost:" + TEST_PORT + TEST_CONTEXT); if (mode == null || !mode.equals("JUNIT")) { server.join(); } } public void stop() throws Exception { if (server != null) { server.stop(); server.join(); server.destroy(); server = null; } } private ServletContextHandler getServletContextHandler(WebApplicationContext context) throws IOException { ServletContextHandler contextHandler = new ServletContextHandler(); contextHandler.setErrorHandler(null); contextHandler.setContextPath(TEST_CONTEXT); contextHandler.addServlet(new ServletHolder(new DispatcherServlet(context)), MAPPING_URL); contextHandler.addEventListener(new ContextLoaderListener(context)); contextHandler.setResourceBase(new ClassPathResource("webapp").getURI().toString()); return contextHandler; } private WebApplicationContext getContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); //context.register(AppConfig.class); context.setConfigLocation(CONFIG_LOCATION); return context; } }
8. Unit tests - there are a lot of tests in application - only few of them will be shown.
8.1. BaseDAOTest
package com.demien.springmvcrest.dao; import java.io.Serializable; import junit.framework.Assert; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.junit.Before; import org.junit.Test; import com.demien.springmvcrest.dao.BaseDAOImpl; import static org.mockito.Mockito.*; public class BaseDAOTest { BaseDAOImpl<TestClass> dao; SessionFactory sessionFactory; Session session; TestClass testObject; private class TestClass implements Serializable { private static final long serialVersionUID = -8877252618686760948L; } @Before public void init() { testObject=new TestClass(); sessionFactory=mock(SessionFactory.class); session=mock(Session.class); when(sessionFactory.getCurrentSession()).thenReturn(session); dao=new BaseDAOImpl<TestClass>(TestClass.class, sessionFactory); } @Test public void getTest() { Integer id=1; when(session.get(TestClass.class, id)).thenReturn(testObject); Object result=dao.get(id); Assert.assertEquals(testObject, result); } @Test public void saveTest() { TestClass newObject=new TestClass(); when(session.save(testObject)).thenReturn(newObject); Object result=dao.save(testObject); Assert.assertEquals(newObject, result); } @Test public void updateTest() { dao.update(testObject); verify(session).update(testObject); } @Test public void deleteTest() { dao.delete(testObject); verify(session).delete(testObject); } @Test public void queryTest() { Query query=mock(Query.class); when(session.createQuery(anyString())).thenReturn(query); //1 dao.query("select * from dual", null); verify(query).list(); //2 dao.query("delete from dual", null); verify(query).executeUpdate(); } }
8.2. LoanController test
package com.demien.springmvcrest.rest; import static junit.framework.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Date; import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import com.demien.springmvcrest.service.ILoanService; import com.demien.springmvcrest.service.IUserService; import com.demien.springmvcrest.service.LoanServiceImplTest; import com.demien.springmvcrest.utils.JsonHelper.JsonHelperException; import com.demien.springmvcrest.utils.ObjectDataPopulator; import com.demien.springmvcrest.AppConst; import com.demien.springmvcrest.controller.LoanController; import com.demien.springmvcrest.domain.Loan; import com.demien.springmvcrest.domain.User; public class LoanControllerTest extends BaseControllerTest<Loan> { private static ILoanService loanService; private static IUserService userService; private LoanController controller; public LoanControllerTest() { super(Loan.class); } @Before public void init() { loanService=mock(ILoanService.class); userService=mock(IUserService.class); controller=new LoanController(); controller.setLoanService(loanService); controller.setUserService(userService); } @Test public void createLoanTest () throws Exception { String ipAddr="127.0.0.1"; HttpServletRequest request=mock(HttpServletRequest.class); when(request.getRemoteAddr()).thenReturn(ipAddr); User user=new User(); ObjectDataPopulator.populate(user); Loan loan=new Loan(); ObjectDataPopulator.populate(loan); when(userService.get(user.getId())).thenReturn(user); ArgumentCaptor<Loan> captor = ArgumentCaptor.forClass(Loan.class); String dateStr=AppConst.FORMATTER.format(loan.getDeadLine()); when(loanService.checkLoan(ipAddr, loan.getAmount())).thenReturn(""); controller.createLoan(loan.getAmount().toString(), dateStr, user.getId(), request); verify(userService).get(user.getId()); verify(loanService).save(captor.capture()); assertEquals(loan.getAmount(), captor.getValue().getAmount()); assertEquals(dateStr, AppConst.FORMATTER.format(captor.getValue().getDeadLine()) ); assertEquals(user.getId(), captor.getValue().getUser().getId()); } @Test public void extendLoanTest() throws JsonHelperException { Integer id=666; Date d=new Date(); Loan loan=LoanServiceImplTest.getTestLoan("", new Date()); loan.setDeadLine(new Date(d.getTime()+AppConst.DAY_SECONDS)); when(loanService.get(id)).thenReturn(loan); controller.extendloan(id); verify(loanService).extendLoan(id); } }9. Integration tests
9.1. Base Integration test - test for BaseController
package com.demien.springmvcrest.integration.test; import static org.junit.Assert.*; import java.io.IOException; import java.util.List; import org.apache.http.HttpResponse; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import com.demien.springmvcrest.controller.BaseController; import com.demien.springmvcrest.utils.ObjectDataPopulator; import com.demien.springmvcrest.utils.RestTestClient; import com.demien.springmvcrest.utils.RestTestServer; import com.demien.springmvcrest.AppConst; import com.demien.springmvcrest.domain.IPersistable; public abstract class BaseIT<T extends IPersistable> { protected static RestTestServer server = new RestTestServer("JUNIT"); protected static RestTestClient client = new RestTestClient(); protected final String BASE_URL = "http://localhost:" + RestTestServer.TEST_PORT + RestTestServer.TEST_CONTEXT //+ "/services/rest/" ; protected final String SERVICE_URL; private Class<T> cl; public BaseIT(String sevicePrexif, Class<T> cl) { SERVICE_URL = BASE_URL + sevicePrexif + "/"; this.cl=cl; } @BeforeClass public static void init() throws Exception { server.start(); } @AfterClass public static void finish() throws Exception { server.stop(); } private T getTestEntity() throws Exception { T entity = cl.newInstance(); ObjectDataPopulator.populate(entity); return entity; } @Test public void sayHello() throws Exception { String value = client.sendGetStringResult(SERVICE_URL + BaseController.SERVICE_HELLO); System.out.println("value=" + value); assertEquals(AppConst.HELLO, value); } @SuppressWarnings("unchecked") private T getById(Integer id) throws Exception { T result = (T) client.sendGetObjectResult(SERVICE_URL + BaseController.SERVICE_GETBYID + "?id=" + id, cl); return result; } private void addEntity(T entity) throws Exception { HttpResponse response = client.sendPost(SERVICE_URL + BaseController.SERVICE_ADD, entity); int responseCode = response.getStatusLine().getStatusCode(); assertEquals(RestTestClient.RESPONSE_OK, responseCode); } private void updateEntity(T entity) throws Exception { HttpResponse response = client.sendPost(SERVICE_URL + BaseController.SERVICE_UPDATE, entity); int responseCode = response.getStatusLine().getStatusCode(); assertEquals(RestTestClient.RESPONSE_OK, responseCode); } private void deleteEntity(T entity) throws Exception { HttpResponse response = client.sendPost(SERVICE_URL + BaseController.SERVICE_DELETE, entity); int responseCode = response.getStatusLine().getStatusCode(); assertEquals(RestTestClient.RESPONSE_OK, responseCode); } @SuppressWarnings({ "unchecked" }) private List<T> getEntityList() throws IllegalStateException, IOException, Exception { List<T> result = (List<T>) client.sendGetObjectResult(SERVICE_URL + BaseController.SERVICE_GET_ALL, cl, true); return result; } @Test public void addEntityTest() throws Exception { int cnt=getEntityList().size(); T testEntity = getTestEntity(); addEntity(testEntity); assertEquals(cnt+1, getEntityList().size()); } @Test public void updateEntityTest() throws Exception { T testEntity = getTestEntity(); addEntity(testEntity); testEntity=getEntityList().get(0); Integer id = testEntity.getId(); T forUpdate = getTestEntity(); forUpdate.setId(id); updateEntity(forUpdate); T searchEntity = getById(id); assertEquals(forUpdate, searchEntity); } @Test public void deleteEntityTest() throws Exception { T testEntity = getTestEntity(); addEntity(testEntity); Integer id = getEntityList().get(0).getId(); testEntity.setId(id); deleteEntity(testEntity); T searchEntity = getById(id); assertTrue(searchEntity==null); } }
9.2. Loan integration test- for Loan Controller
package com.demien.springmvcrest.integration.test; import static com.demien.springmvcrest.AppConst.*; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import java.util.Date; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.AnnotationConfigContextLoader; import org.springframework.test.context.web.WebAppConfiguration; import com.demien.springmvcrest.controller.BaseController; import com.demien.springmvcrest.controller.LoanController; import com.demien.springmvcrest.service.ILoanService; import com.demien.springmvcrest.service.IUserService; import com.demien.springmvcrest.utils.ObjectDataPopulator; import com.demien.springmvcrest.config.AppConfig; import com.demien.springmvcrest.config.WebMvcConfig; import com.demien.springmvcrest.domain.Loan; import com.demien.springmvcrest.domain.User; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {AppConfig.class,WebMvcConfig.class}, loader = AnnotationConfigContextLoader.class) @WebAppConfiguration public class LoanIT extends BaseIT<Loan> { @Autowired private ILoanService loanService; @Autowired private IUserService userService; @Autowired private LoanController loanController; private final int DAY_SECONDS=1000*60*60*24; public LoanIT() { super(LoanController.SERVICE_MAIN, Loan.class); } @Before public void clear() throws Exception { loanService.deleteAll(); userService.deleteAll(); // create test user User user=new User(); ObjectDataPopulator.populate(user); userService.save(user); } private Integer getTestUserId() { User user=userService.getAll().get(0); return user.getId(); } @SuppressWarnings("unchecked") private List<Loan> getAllLoans() throws Exception { List<Loan> loans = (List<Loan>) client.sendGetObjectResult(SERVICE_URL + BaseController.SERVICE_GET_ALL, Loan.class, true); assertNotNull(loans); return loans; } private String createLoan(final Float amount, final Date date, final Integer userId) throws Exception { String result=client.sendGetStringResult(SERVICE_URL+LoanController.SERVICE_CREATE_LOAN+"?amount="+amount+"&date="+FORMATTER.format(date)+"&userId="+userId); assertNotNull(result); return result; } @Test public void loanCreationSuccessfullTest() throws Exception { List<Loan> loans =getAllLoans(); int cnt=loans.size(); assertEquals(0, cnt); // create String result=createLoan(1f, new Date(), getTestUserId()); assertEquals(result, RESULT_OK); loans =getAllLoans(); assertEquals(1, loans.size()); //check for user Loan loan=loans.get(0); assertEquals(getTestUserId(), loan.getUser().getId()); } @Test public void loanCreationRejectedByMaxIpRequestTest() throws Exception { // create 3 requests from one ip for (int i=0; i<CHECK_MAX_IP_DAY_REQUESTS; i++) { String result=createLoan(1f, new Date(), getTestUserId()); assertEquals(result, RESULT_OK); } // next should fail String result=createLoan(1f, new Date(), -666); assertTrue(result.contains(RESULT_REJECTED)); assertTrue(result.contains(REJECT_REASON_IP_DAY_ATTEMPTS)); assertTrue(result.contains(ERROR_WRONG_USER_ID)); } @SuppressWarnings("deprecation") @Test public void loanCreationRejectedByBadTimeAndMaxAmountTest() throws Exception { // set "wrong time" Date d=new Date(); loanService.setCheckHourFrom(d.getHours()-1); loanService.setCheckHourTo(d.getHours()+1); String result=createLoan(CHECK_MAX_AMOUNT+1, new Date(), getTestUserId()); assertTrue(result.contains(RESULT_REJECTED)); assertTrue(result.contains(REJECT_REASON_BAD_TIME)); } @Test public void testLoanExtendSuccess() throws Exception { Date d=new Date(); String result=createLoan(1f, new Date(d.getTime()+DAY_SECONDS ), getTestUserId()); assertEquals(result, RESULT_OK); List<Loan> loans =getAllLoans(); int cnt=loans.size(); Loan loan=loans.get(0); result=client.sendGetStringResult(SERVICE_URL+LoanController.SERVICE_EXTEND_LOAN+"?id="+loan.getId()); assertEquals(RESULT_EXTENDED, result); loans =getAllLoans(); assertEquals(cnt+1, loans.size()); // check for correct ID/PARENT ID int cnt1=0; int cnt2=0; for (Loan eachLoan:loans) { if (eachLoan.getId().equals(loan.getId())) { cnt1++; assertEquals(Loan.STATE.EXTENDED.toString(), eachLoan.getState()); } if (eachLoan.getParentId()!=null && eachLoan.getParentId().equals(loan.getId())) { cnt2++; assertEquals(Loan.STATE.OPEN.toString(), eachLoan.getState()); assertTrue(eachLoan.getRate()>loan.getRate()); assertTrue(eachLoan.getDeadLine().getTime()>loan.getDeadLine().getTime()); } } assertEquals(1,cnt1); assertEquals(1,cnt2); } @Test public void testLoanExtendFail() throws Exception { int DAY_SECONDS=1000*60*60*24; Date d=new Date(); String result=createLoan(1f, new Date(d.getTime()-DAY_SECONDS), getTestUserId()); assertEquals(result, RESULT_OK); Loan loan=getAllLoans().get(0); result=client.sendGetStringResult(SERVICE_URL+LoanController.SERVICE_EXTEND_LOAN+"?id="+loan.getId()); assertEquals(ERROR_EXTEND_DEADLINE_EXCEED, result); // check for correct ID/PARENT ID } }
10. resume
Commands for lunching application from command line:mvn test - run unit tests
mvn integration-test - run integration tests
Full source code can be downloaded from here.