fr.ird.observe.client.form.table.ContentTableModel Maven / Gradle / Ivy
/*
* #%L
* ObServe Toolkit :: Common Client
* %%
* Copyright (C) 2008 - 2017 IRD, Ultreia.io
* %%
* 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
* .
* #L%
*/
package fr.ird.observe.client.form.table;
import fr.ird.observe.dto.AbstractObserveDto;
import fr.ird.observe.dto.data.DataDto;
import fr.ird.observe.dto.data.DataListDto;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.swing.JOptionPane;
import javax.swing.table.AbstractTableModel;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuiton.i18n.I18n;
import org.nuiton.jaxx.validator.swing.SwingValidator;
/**
* Le modele d'un tableau où les données sont une association sur une entité.
*
* Ce modèle n'est pas éditable.
*
* Les données sont stockées dans la liste {@link #data} qui sert de cache, car
* on veut pouvoir valider en temps réel l'entité principale (celle qui contient
* l'association), il faut donc toujours que les données de l'association soient
* synchronisées. L'utilisation d'un cache est cependant requise car sinon cela
* est trop couteux (notamment pour le rendu du tableau...).
*
* Le cache sera recalculé à chaque fois que l'on modifie la structure des
* données de l'association (ajout d'une entrée, suppression d'une entrée).
*
* De plus le cache permet de travailler sur une liste (alors que l'association
* n'est peut-être pas ordonnée) et cela facilite les opérations sur les données
* du tableau).
*
* Le modèle définit plusieurs propriétés :
- {@link #editable} : un
* drapeau pour savoir si le modèle est editable
- {@link #modified} : un
* drapeau pour savoir si le modèle est modifié
- {@link #create} : un
* drapeau pour savoir si l'entrée en cours d'édition est une nouvelle
* entrée
- {@link #selectedRow} : l'index de l'entrée sélectionnée
*
FIXME a finir...
*
* @param le type de l'entité qui contient la liste
* @param le type de l'entite d'une entrée de la liste
* @author Tony Chemit - [email protected]
* @since 1.0
*/
public abstract class ContentTableModel extends AbstractTableModel {
/** Le nom de la propriété de la ligne en cours d'édition */
static final String SELECTED_ROW_PROPERTY = "selectedRow";
/** Le nom de la propriété modifié du modèle */
private static final String MODIFIED_PROPERTY = "modified";
/** Le nom de la propriété pour editer le modele */
private static final String EDITABLE_PROPERTY = "editable";
/**
* Le nom de la propriété pour indiquer que l'entrée en cours d'édition est
* en mode création
*/
public static final String CREATE_PROPERTY = "create";
/** Le nom de la propriété pour savoir si le modèle est vide */
private static final String EMPTY_PROPERTY = "empty";
private static final long serialVersionUID = 1L;
/** Logger */
private static final Log log = LogFactory.getLog(ContentTableModel.class);
/** la liste des métas du modèle */
protected final List> metas;
/** pour la propagation des modifications d'états */
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
/** la liste des données du modèle */
protected List data = new ArrayList<>();
/** un drapeau pour savoir si le modèle a été modifié */
protected boolean modified;
/** un drapeau pour savoir si le modèle est éditable */
protected boolean editable;
/** un drapeau pour savoir si on edite une nouvelle entree */
protected boolean create;
/**
* un drapeau pour modifier la selection de la ligne en cours sans aucune
* verification.
*/
private boolean valueAdjusting;
/** l'entrée sélectionnée (-1 quand pas de sélection) */
protected int selectedRow = -1;
/** un message supplémentaire à afficher lors d'une suppression de ligne */
protected String deleteExtraMessage;
/** un drapeau pour savoir si le modèle a ete initialisé. */
private boolean init;
private DataTableFormUI context;
@SuppressWarnings("unchecked")
public ContentTableModel(List> metas) {
if (CollectionUtils.isEmpty(metas)) {
throw new NullPointerException("meta parameter can not be null, nor empty");
}
this.metas = Collections.unmodifiableList(metas);
}
public void setContext(DataTableFormUI context) {
this.context = context;
}
public static ContentTableMeta newTableMeta(
Class childType,
String property,
boolean unmodiableWhenExisting) {
return new ContentTableMeta<>(childType, property, unmodiableWhenExisting);
}
/**
* Positionne un bean dans le modèle.
*
* Cela va initialiser la liste à utiliser.
*/
void attachModel() {
// pas de ligne selectionne
setSelectedRow(-1);
// pas en mode creation
setCreate(false);
setInit(true);
updateEmpty();
if (log.isDebugEnabled()) {
log.debug("editable : " + isEditable());
log.debug("size : " + getRowCount());
}
// notify listeners
fireTableDataChanged();
}
void dettachModel() {
setModified(false);
int size = getRowCount();
// on indique que le modele n'est plus lie au bean
// cela permet de ne plus charger l'association dans le cache
setInit(false);
if (size > 0) {
// il y avait des données que l'on a supprimé
fireTableRowsDeleted(0, size - 1);
updateEmpty();
}
setSelectedRow(-1);
setCreate(false);
}
/** Permet l'ajout d'une nouvelle entrée à editer */
public void addNewEntry() {
ensureEditable();
if (getSelectedRow() > -1) {
// il y avait une ligne precedemment selectionnee,
// on doit verifier que l'on peut changer d'entree
if (!isCanQuitEditingRow()) {
// on ne peut pas quitter la ligne en cours d'édition
// on annule donc l'opération
return;
}
}
// on est autorise a ajouter une nouvelle entrée
int row = getRowCount();
C bean = null;
try {
bean = getModel().newTableEditBean();
} catch (Exception e) {
getHandler().getUi().getMainUI().handlingError(e);
}
data.add(bean);
updateBeanList(false);
fireTableRowsInserted(row, row);
updateEmpty();
// on est en mode creation
setCreate(true);
// la nouvelle ligne est celle en cours d'edition
changeSelectedRow(row);
}
protected DataTableFormUIModel getModel() {
return getHandler().getModel();
}
public void doRemoveRow(int rowToDelete, boolean force) {
C bean = getValueAt(rowToDelete);
ContentTableMeta meta = getColumnMeta(getColumnCount() - 1);
if (force || getHandler().confirmForEntityDelete(null, meta.klass, bean, deleteExtraMessage)) {
// delete row
removeRow(rowToDelete);
rowToDelete--;
// on veut selectionner la ligne precedente si elle existe
// ou bien la meme ligne (si on etait sur la premiere ligne)
// on force toujours le passage sur la ligne d'avant
// afin que le binding se deroule bien meme si ensuite on rechange
// la ligne selectionne (cas ou on etait sur la premier ligne et
// que le modele n'est pas vide)
changeSelectedRow(rowToDelete);
if (rowToDelete == -1 && !isEmpty()) {
// on repasse sur la premiere ligne
// car le modele n'est pas vide
changeSelectedRow(0);
}
}
}
public boolean isCanQuitEditingRow() {
if (selectedRow == -1) {
// aucune ligne selectionne
// on peut changer la ligne sans verification
return true;
}
if (!create && !isModelModified()) {
// on est sur une ligne en mode mise a jour
// et aucune changement n'a ete effectue
// on peut continuer sans rien tester
return true;
}
// une ligne etait precemment en cours d'edition et a ete modifiee
if (log.isDebugEnabled()) {
log.debug("editing row " + getSelectedRow() + " was modified, need confirmation");
}
boolean canContinue = false;
if (isModelValid()) {
// la ligne est valide, on demande a l'utilisateur s'il
// veut la sauvegarder
int reponse = getHandler().askUser(
I18n.t("observe.title.need.confirm"),
I18n.t("observe.message.table.editBean.modified"),
JOptionPane.WARNING_MESSAGE,
new Object[]{
I18n.t("observe.choice.save"),
I18n.t("observe.choice.doNotSave"),
I18n.t("observe.choice.cancel")},
0);
if (log.isDebugEnabled()) {
log.debug("response : " + reponse);
}
switch (reponse) {
case JOptionPane.CLOSED_OPTION:
case 2:
break;
case 0:
// will save ui
// sauvegarde des modifications
updateRowFromEditBean();
canContinue = true;
break;
case 1:
// edition annulé
canContinue = true;
if (create) {
// on doit supprimer la ligne de creation
removeRow(getSelectedRow());
} else {
// reset row
resetRow(getSelectedRow());
}
break;
}
} else {
// le validateur n'est pas ok, on ne peut que proposer la perte
// des donnees car elles sont ne pas enregistrables
int reponse = getHandler().askUser(
I18n.t("observe.title.need.confirm"),
I18n.t("observe.message.table.editBean.modified.but.invalid"),
JOptionPane.ERROR_MESSAGE,
new Object[]{
I18n.t("observe.choice.continue"),
I18n.t("observe.choice.cancel")},
0);
if (log.isDebugEnabled()) {
log.debug("response : " + reponse);
}
switch (reponse) {
case 0:
// wil reset ui
canContinue = true;
if (create) {
// on doit supprimer la ligne de creation
removeRow(getSelectedRow());
}
break;
}
}
return canContinue;
}
protected void resetRow(int row) {
// do nothing by default
}
/**
* Selectionne la ligne dont l'index est donné.
*
* @param row l'index de la nouvelle ligne a editer
*/
void changeSelectedRow(int row) {
if (log.isDebugEnabled()) {
log.debug("row : " + row);
log.debug("editable : " + isEditable());
log.debug("size : " + getRowCount());
}
if (editable) {
// on force la suppression de l'ancien validateur
getValidator().setBean(null);
}
if (row == -1) {
// cas special lors de la suppression de la selection, par exemple
// lors d'une suppression de colonne
setSelectedRow(row);
return;
}
ensureRowIndex(row);
if (editable) {
// on recharge le bean dans le validateur
// cela permettre de faire fonctionner les binding
// lors de la construction du nouveau editBean
getValidator().setBean(getRowBean());
}
// recherche du bean d'édition
C beanToBind;
// on recupere le bean existant
beanToBind = getValueAt(row);
// on charge le bean d'edition
load(beanToBind, getRowBean());
// on modifie la ligne d'edition
setSelectedRow(row);
if (editable) {
// pas de modification sur le validateur
getValidator().setChanged(false);
}
}
/**
* Pour mettre a jour la ligne en cours d'edition a partir du bean
* d'edition
*/
public void updateRowFromEditBean() {
ensureEditable();
int editingRow = getSelectedRow();
// mettre a jour la ligne
C bean = getValueAt(editingRow);
load(getRowBean(), bean);
fireTableRowsUpdated(editingRow, editingRow);
if (create) {
// la ligne n'est plus en mode creation
setCreate(false);
}
// plus de modification sur le bean d'edition
getValidator().setChanged(false);
// le model a ete modifie
setModified(true);
// on valide le bean principal
// pour cela on doit recharger l'association dans le bean principale
// car vu que l'on travaille sur des collections, si on ne supprime
// pas la liste avant de vouloir valider, alors aucune validation ne
// sera declanchée (car pas de propriété modifié dans le bean...)
getParentValidator().doValidate();
}
public void resetEditBean() {
C bean = getValueAt(getSelectedRow());
load(bean, getRowBean());
// plus de modification sur le bean d'edition
getValidator().setChanged(false);
}
@SuppressWarnings("unchecked")
protected DataTableFormUIHandler getHandler() {
return context.getHandler();
}
protected Collection getChilds(DataListDto bean) {
return bean.getChildren();
}
protected abstract void load(C source, C target);
protected DataListDto getBean() {
DataTableFormUIModel model = getModel();
return model == null ? null : model.getBean();
}
public C getRowBean() {
DataTableFormUIModel model = getModel();
return model == null ? null : model.getTableEditBean();
}
public boolean isNewRow() {
return getRowBean().getId() == null;
}
public boolean isCreate() {
return create;
}
public void setCreate(boolean create) {
boolean old = this.create;
this.create = create;
firePropertyChange(CREATE_PROPERTY, old, create);
}
public int getSelectedRow() {
return selectedRow;
}
public void setSelectedRow(int selectedRow) {
int old = this.selectedRow;
this.selectedRow = selectedRow;
firePropertyChange(SELECTED_ROW_PROPERTY, old, selectedRow);
}
public String getDeleteExtraMessage() {
return deleteExtraMessage;
}
public void setDeleteExtraMessage(String deleteExtraMessage) {
this.deleteExtraMessage = deleteExtraMessage;
}
public boolean isModified() {
return modified;
}
public void setModified(boolean modified) {
boolean oldModified = this.modified;
this.modified = modified;
firePropertyChange(MODIFIED_PROPERTY, oldModified, modified);
}
public boolean isEditable() {
return editable;
}
public void setEditable(boolean editable) {
boolean oldModified = this.editable;
this.editable = editable;
firePropertyChange(EDITABLE_PROPERTY, oldModified, editable);
}
public boolean isValueAdjusting() {
return valueAdjusting;
}
public boolean isEmpty() {
return getRowCount() == 0;
}
public List getData() {
if (data == null) {
if (init) {
// le modèle a ete initialise
// on recupere donc la liste a partir du bean principal
DataListDto bean = getBean();
Collection childs = getChilds(bean);
if (childs == null || childs.isEmpty()) {
data = new ArrayList<>();
} else {
data = new ArrayList<>(childs);
}
} else {
// le modèle n'est pas encore initialisé
// on retourne donc une liste vide
data = new ArrayList<>();
}
}
return data;
}
public int getColumn(String columnName) {
int i = 0;
for (ContentTableMeta m : metas) {
if (m.getName().equals(columnName)) {
return i;
}
i++;
}
return -1;
}
@Override
public int getRowCount() {
List list = getData();
return list == null ? 0 : list.size();
}
@Override
public int getColumnCount() {
return metas.size();
}
@Override
public String getColumnName(int columnIndex) {
ensureColumnIndex(columnIndex);
return metas.get(columnIndex).getName();
}
@Override
public Class> getColumnClass(int columnIndex) {
ensureColumnIndex(columnIndex);
return metas.get(columnIndex).getType();
}
private ContentTableMeta getColumnMeta(int columnIndex) {
ensureColumnIndex(columnIndex);
return metas.get(columnIndex);
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
// dans ce type de modele rien n'est editable
return false;
}
@Override
public Object getValueAt(int row, int column) {
ensureColumnIndex(column);
ContentTableMeta meta = getColumnMeta(column);
C bean = getValueAt(row);
return bean == null ? null : getValueAt(bean, row, meta);
}
public C getValueAt(int row) {
ensureRowIndex(row);
List list = getData();
return list == null ? null : list.get(row);
}
private void updateEmpty() {
firePropertyChange(EMPTY_PROPERTY, null, isEmpty());
}
/**
* @param the type of the column property
* @param column the column to scan
* @return the list of used properties for a given column
*/
@SuppressWarnings({"unchecked"})
public List getColumnValues(int column) {
List result = new ArrayList<>();
if (!isEmpty()) {
for (int i = 0; i < getRowCount(); i++) {
T value = (T) getValueAt(i, column);
if (value != null) {
result.add(value);
}
}
}
return result;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.removePropertyChangeListener(propertyName, listener);
}
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
pcs.firePropertyChange(propertyName, oldValue, newValue);
}
@SuppressWarnings("unchecked")
protected SwingValidator getValidator() {
return context == null ? null : context.getValidatorTable();
}
@SuppressWarnings("unchecked")
private SwingValidator getParentValidator() {
return context == null ? null : context.getValidator();
}
protected void removeRow(int row) {
ensureRowIndex(row);
setSelectedRow(-1);
getData().remove(row);
updateBeanList(!create);
if (log.isDebugEnabled()) {
log.debug(row);
}
// model has changed
if (!create) {
setModified(true);
}
fireTableRowsDeleted(row, row);
if (create) {
// on quitte le mode creation
setCreate(false);
}
updateEmpty();
}
protected Object getValueAt(C bean, int row, ContentTableMeta meta) {
return meta.getValue(this, bean, row);
}
protected boolean setValueAt(C bean, Object aValue, int row, ContentTableMeta meta) {
return meta.setValue(this, bean, aValue, row);
}
private void ensureColumnIndex(int columnIndex) throws ArrayIndexOutOfBoundsException {
if (columnIndex < 0 || columnIndex >= metas.size()) {
throw new ArrayIndexOutOfBoundsException("column index should be in [0," + metas.size() + "], but was " + columnIndex);
}
}
private void ensureRowIndex(int rowIndex) throws ArrayIndexOutOfBoundsException {
int size = getRowCount();
if (rowIndex < 0 || rowIndex >= size) {
throw new ArrayIndexOutOfBoundsException("row index should be in [0," + (getRowCount() - 1) + "], but was " + rowIndex);
}
}
private void ensureEditable() throws IllegalStateException {
if (!editable) {
throw new IllegalStateException("can not edit this model since it is marked as none editable " + this);
}
}
protected void setInit(boolean init) {
this.init = init;
// le changement de l'état init provoque toujours le vidage du cache
clearCache();
}
private void clearCache() {
data = null;
}
private void updateBeanList(boolean shouldChanged) {
SwingValidator parentValidator = getParentValidator();
boolean wasChanged = parentValidator.isChanged();
// on repositionne la liste sur le bean principal
// pour avoir la validation en temps reel sur le bean principal
setChilds(getBean(), data);
parentValidator.doValidate();
if (!shouldChanged && !wasChanged) {
// on repositionne le drapeau changed a faux
parentValidator.setChanged(false);
}
}
protected void setChilds(DataListDto parent, List childs) {
parent.setChildren(childs);
}
private boolean isModelModified() {
return getValidator().isChanged();
}
private boolean isModelValid() {
return getValidator().isValid();
}
}