/*
* This file is a part of GrantMaster.
* Copyright (C) 2015 Gábor Fehér <feherga@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.github.gaborfeher.grantmaster.framework.base;
import com.github.gaborfeher.grantmaster.framework.utils.DatabaseSingleton;
import com.github.gaborfeher.grantmaster.framework.utils.ValidatorFactorySingleton;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javafx.application.Platform;
import javax.persistence.EntityManager;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.slf4j.LoggerFactory;
/**
* Base class for all the entity wrappers. An entity wrapper wraps a JPA
* Entity object. It provides functions for handling computed data fields, and
* more importantly the wrapper bridges interaction between GUI controls and the
* entities.
* The JPA entities supported here have to be subclasses of EntityBase.
* The GUI controls talking to EditableTableRowItem objects are the Java FX tab
* controllers derived from ControllerBase, and the Java FX table cell
* implementations in the ui.cells subpackage.
*
* @param <T> Type of entity to wrap.
*/
public abstract class EntityWrapper<T extends EntityBase> implements EditableTableRowItem {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(EntityWrapper.class);
protected T entity;
private RowEditState state;
private boolean isSummary;
private TablePageControllerBase parent;
public EntityWrapper(T entity) {
this.entity = entity;
state = RowEditState.SAVED;
isSummary = false;
}
@Override
public boolean commitEdit(String property, Object val, Class<?> valueType) {
logger.info("commitEdit({}, {}, {})", property, val, valueType);
if (Objects.equals(val, getProperty(property))) {
return true;
}
if (state != RowEditState.EDITING_NEW && checkIsLocked()) {
return false;
}
if (!setProperty(property, val, valueType)) {
return false;
}
if (state == RowEditState.EDITING_NEW) {
// Nothing is to be done for newly created objects here. The user has to
// click the create button to commit them.
// Refresh the whole table. Currently this is only needed when the two
// amount fields of a newly created expense are tied together and one
// of them is edited, causing the other to refresh. It's simpler design
// to do this update always, not only in that case.
requestTableRefresh();
return true;
}
return DatabaseSingleton.INSTANCE.transaction(this::save);
}
protected boolean saveInternal(EntityManager em) {
entity = em.merge(entity);
setState(RowEditState.SAVED);
return true;
}
public final boolean save(EntityManager em) {
boolean success = DatabaseSingleton.INSTANCE.runOrRollback(
(EntityManager em0) -> validate() && saveInternal(em0), em);
requestTableRefresh();
return success;
}
public final boolean addRandom(EntityManager em) {
boolean success = DatabaseSingleton.INSTANCE.runOrRollback(
(EntityManager em0) -> fillRandom(em) && validate() && saveInternal(em0), em);
requestTableRefresh();
return success;
}
@Override
public RowEditState getState() {
return state;
}
@Override
public void setState(RowEditState state) {
this.state = state;
}
@Override
public boolean canEdit() {
return !isSummary;
}
public boolean setProperty(String name, Object value, Class<?> paramType) {
try {
String setterName = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
entity.getClass().getMethod(setterName, paramType).invoke(entity, value);
return true;
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
logger.error("Cannot set property: EntityWrapper.setProperty({})", name, ex);
return false;
}
}
@Override
public Object getProperty(String name) {
try {
String getterName = "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
return entity.getClass().getMethod(getterName).invoke(entity);
} catch (NoSuchMethodException ex) {
return null;
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
logger.error("Property not found: EntityWrapper.getProperty({})", name, ex);
return null;
}
}
@Override
public void requestTableRefresh() {
if (parent != null) {
parent.onRefresh();
}
}
private Set<ConstraintViolation> checkValidationConstraints() {
Validator validator = ValidatorFactorySingleton.SINGLE_INSTANCE.getValidator();
Set<ConstraintViolation> constraintViolations = new HashSet<>();
constraintViolations.addAll(validator.validate(entity));
constraintViolations.addAll(validator.validate(this));
return constraintViolations;
}
@Override
public boolean validate() {
Set<ConstraintViolation> constraintViolations = checkValidationConstraints();
if (constraintViolations.isEmpty()) {
return true;
} else {
if (parent == null) {
// Running in a test.
logger.error("Validation failure and no parent: {}", constraintViolations.toString());
} else {
// Show error message to the user. Using runLater to avoid messing
// with the table-edit GUI.
Platform.runLater(() ->
parent.showValidationFailureDialog(constraintViolations));
}
return false;
}
}
protected boolean fillRandom(EntityManager em) {
return true;
}
public boolean delete(EntityManager em) {
if (entity != null) {
entity = (T) em.find(entity.getClass(), entity.getId());
em.remove(entity);
}
return true;
}
public void discardEdits() {
if (state == RowEditState.EDITING_NEW) {
parent.discardNew();
} else {
parent.onRefresh();
}
}
protected boolean checkIsLocked() {
return false;
}
@Override
public boolean getIsSummary() {
return isSummary;
}
public void setIsSummary(boolean isSummary) {
this.isSummary = isSummary;
}
@Override
public void setParent(TablePageControllerBase parent) {
this.parent = parent;
}
public TablePageControllerBase getParent() {
return parent;
}
public Long getId() {
if (entity == null) {
return null;
} else {
return entity.getId();
}
}
@Override
public T getEntity() {
return entity;
}
public void setEntity(T entity) {
this.entity = entity;
}
}