All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy