To have the ability to apply for financing, it’s necessary to set up the application creation process in the system. For this, we use:
HTML pages - part of the user interface for entering information by the system’s users.
Form objects - Java objects into which data received with the application creation request are transformed. Validation of the received data also occurs at the level of these objects before an attempt is made to save them.
Mappers - Java components with which you can configure automatic mapping of data from forms to entities. Mappers allow, among other things, to avoid the problem of data loss when adding new fields for information collection. MapStruct library is used for the implementation of mappers (https://mapstruct.org/documentation/1.5/api/index.html).
Form services - Java classes that are responsible for creating and editing data.
Action controllers for creation and editing - Java objects that allow configuring the availability of creation and editing actions for the system user.
Let’s consider an example of applying for financing, where we will request the following data: Data for the application
The first step is to implement forms for the application and borrower that inherit the abstract class BaseForm<ID>.
public abstract class BaseForm<ID> implements Serializable {
private ID id;
...
ID - the type of the entity identifier.
ParticipantForm.java
public class ParticipantForm extends BaseForm<Long> {
@NotBlank
private String firstName;
@NotBlank
private String lastName;
@NotBlank
private String primaryId;
...
ApplicationForm.java
public class ApplicationForm extends BaseForm<Long> {
@NotNull
private BigDecimal principal;
@NotNull
private Integer term;
@Valid
private ParticipantForm borrower;
...
The next step is to create mappers for ParticipantForm and ApplicationForm.
For this, you need to implement the EntityToFormMapper interface.
public interface EntityToFormMapper<E, F> {
void toEntity(F form, @MappingTarget E entity);
@InheritConfiguration
E createEntity(F form);
F toForm(E entity);
}
Methods:
void toEntity(F form, @MappingTarget E entity) - transfers data from the form (F) to the entity (E).
E createEntity(F form) - creates an entity based on the form.
F toForm(E entity) - creates a form based on the entity.
E - main entity
F - entity form
Depending on the system’s requirements, it’s necessary to implement:
An abstract Java class EntityViewService responsible for preparing the model for displaying the entity.
An abstract Java class EntityFormService responsible for preparing the model for displaying and editing the entity, including the EntityViewService class.
EntityViewService.java
public abstract class EntityViewService<E extends Persistable<ID>, ID extends Serializable> {
private final Class<E> entityClass;
private final Class<ID> idClass;
@Autowired
private EntityManager entityManager;
private JpaRepositorySupport<ID, E> repositorySupport;
public EntityViewService() {
ResolvableType resolvableType = ResolvableType.forClass(EntityViewService.class, this.getClass());
this.entityClass = resolvableType.getGeneric(new int[]{0}).resolve();
this.idClass = resolvableType.getGeneric(new int[]{1}).resolve();
}
@PostConstruct
protected void initRepositorySupport() {
this.repositorySupport = new JpaRepositorySupport(this.entityClass, this.entityManager);
}
public Class<E> getEntityClass() {
return this.entityClass;
}
public Class<ID> getIdClass() {
return this.idClass;
}
protected EntityManager getEntityManager() {
return this.entityManager;
}
public JpaRepository<E, ID> getRepository() {
return this.repositorySupport.getRepository();
}
@Transactional(
readOnly = true
)
public Map<String, ?> getViewModel(ID id, Map<String, BiFunction<E, Class<E>, ?>> modelElements) {
Map<String, Object> model = new HashMap();
E entity = (Persistable)this.getRepository().getReferenceById(id);
model.put("entity", entity);
Iterator var5 = modelElements.entrySet().iterator();
while(var5.hasNext()) {
Entry<String, BiFunction<E, Class<E>, ?>> element = (Entry)var5.next();
BiFunction<E, Class<E>, ?> function = (BiFunction)element.getValue();
model.put((String)element.getKey(), Lazy.of(() -> {
return function.apply(entity, this.getEntityClass());
}));
}
this.assembleViewModel(id, entity, model);
return model;
}
protected abstract void assembleViewModel(ID id, E entity, Map<String, Object> model);
}
required methods to implement:
protected abstract void assembleViewModel(ID id, E entity, Map<String, Object> model);
optional methods:
public JpaRepository<E, ID> getRepository() - getting entity repository
public Map<String, ?> getViewModel(ID id, Map<String, BiFunction<E, Class<E>, ?>> model) - getting default data model
EntityFormService.java
public abstract class EntityFormService<E extends Persistable<ID>, F, ID extends Serializable> extends EntityViewService<E, ID> {
private final Class<F> formClass;
@Autowired
private EntityToFormMapper<E, F> mapper;
public EntityFormService() {
ResolvableType resolvableType = ResolvableType.forClass(EntityFormService.class, this.getClass());
this.formClass = resolvableType.getGeneric(new int[]{1}).resolve();
}
public Class<F> getFormClass() {
return this.formClass;
}
public EntityToFormMapper<E, F> getMapper() {
return this.mapper;
}
@Transactional(
readOnly = true
)
public Map<String, ?> getEditModel(ID id) {
Map<String, Object> model = new HashMap();
F form = this.createForm(id);
this.assembleEditModel(id, form, model);
return model;
}
protected F createForm(ID id) {
return id != null ? this.get(id) : BeanUtils.instantiateClass(this.formClass);
}
protected abstract void assembleEditModel(ID id, F form, Map<String, Object> model);
@Transactional(
readOnly = true
)
public F get(ID id) {
return this.mapper.toForm((Persistable)EntityUtils.initializeAndUnproxy((Persistable)this.getRepository().getReferenceById(id)));
}
@Transactional
public ID save(ID id, F form) throws ValidationException {
Persistable entity;
if (id != null) {
entity = (Persistable)EntityUtils.initializeAndUnproxy((Persistable)this.getRepository().getReferenceById(id));
this.mapper.toEntity(form, entity);
if (!id.equals(entity.getId())) {
this.getEntityManager().detach(entity);
entity = (Persistable)this.mapper.createEntity(form);
}
} else {
entity = (Persistable)this.mapper.createEntity(form);
}
entity = this.saveEntity(entity);
return (Serializable)entity.getId();
}
protected E saveEntity(E entity) {
return (Persistable)this.getRepository().save(entity);
}
protected void delete(ID id) {
this.getRepository().deleteById(id);
}
public boolean isEditable(E entity) {
return true;
}
}
Mandatory methods to implement:
protected abstract void assembleViewModel(ID id, E entity, Map<String, Object> model) - Method for preparing the model for displaying the entity.
protected abstract void assembleEditModel(ID id, F form, Map<String, Object> model) - Method for preparing the model for creating and editing the entity.
Auxiliary methods:
public EntityToFormMapper<E, F> getMapper() - Getting the mapper for the form and entity
public Map<String, ?> getEditModel(ID id) - Getting the default data model for creating or editing an entity.
public F get(ID id) - Getting a filled form by the entity’s identifier.
public ID save(ID id, F form) - Saving a new or updated entity based on the form.
protected E saveEntity(E entity) - Saving the entity.
protected void delete(ID id) - Marking the entity as deleted.
public boolean isEditable(E entity) - Method indicating the availability of editing the entity at any stage of work. May include multi-level conditions.
For our example, a standard implementation will be sufficient.
ApplicationFormService.java
@Service
public class ApplicationFormService extends EntityFormService<Application, ApplicationForm, Long> {
@Override
public void assembleEditModel(Long id, ApplicationForm form, Map<String, Object> model) {
// дополнительная логика по подготовке модели данных
}
@Override
public void assembleViewModel(Long id, Application entity, Map<String, Object> model) {
// дополнительная логика по подготовке модели данных
}
}
After implementing ApplicationFormService, it is necessary to implement two action controllers based on EntityCreateController and EditEntityActionController. You can read about the description of action tools and their wide configuration by <>.
For our example, a basic implementation of these two action controllers will be sufficient.
CreateApplicationAction.java
@Controller
public class CreateApplicationAction extends EntityCreateController<Long, Application, ApplicationForm> {
}
EditApplicationAction.java
@Controller
public class EditApplicationAction extends EditEntityActionController<Long, Application, ApplicationForm> {
}
The implementation of these action controllers will indicate to the system their availability for display and use by users.
The final step is to set up the user interface of the creation and editing page. For this, a certain order of HTML file placement must be observed. The system, upon finding the required file in its place, will automatically integrate its content into the main fragment of the interface display.
Our file named edit.html contains elements for entering data.
<!DOCTYPE html>
<html th:include="~{/templates/layout :: page}"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<!--Block of code embedded in the main fragment for display-->
<th:block th:fragment="content">
<form
<!--actionPath - the request address where the system will send the form to the server-->
th:action="@{${actionPath}}"
<!--form - the form prepared in ApplicationFormService-->
th:object="${form}">
<th:block th:insert="~{/form/loader :: formLoader}"/>
<div class="form__actions">
<div>
<!--cancel button for creating or editing, allowing return to the previous page-->
<a class="btn btn-grey-1"
th:href="*{id} ? @{'/application/' + *{id}} : @{/application}">Cancel</a>
<!--submit button to send the form to the server-->
<button
class="btn btn-success"
id="btnSubmit"
type="submit">Save
</button>
</div>
</div>
<input th:field="*{id}" type="hidden"/>
<!--Input fields-->
<th:block
th:insert="~{/form/components :: text('First Name', 'borrower.firstName', 'v-required')}"/>
<th:block
th:insert="~{/form/components :: text('Last Name', 'borrower.lastName', 'v-required')}"/>
<th:block
th:insert="~{/form/components :: text('Primary Id', 'borrower.primaryId', 'v-required')}"/>
<th:block
th:insert="~{/form/components :: text('Principal', 'principal', 'v-required app_decimal')}"/>
<th:block
th:insert="~{/form/components :: text('Term', 'term', 'v-required app_digits')}"/>
</form>
</th:block>
</body>
</html>