View Javadoc
1   package fr.ifremer.dali.ui.swing.content.observation.operation.measurement.grouped.shared;
2   
3   /*
4    * #%L
5    * Dali :: UI
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2014 - 2015 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   *
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   *
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  import com.google.common.collect.ArrayListMultimap;
27  import com.google.common.collect.Lists;
28  import com.google.common.collect.Multimap;
29  import com.google.common.collect.Sets;
30  import fr.ifremer.dali.decorator.DecoratorService;
31  import fr.ifremer.dali.dto.DaliBeanFactory;
32  import fr.ifremer.dali.dto.DaliBeans;
33  import fr.ifremer.dali.dto.ErrorDTO;
34  import fr.ifremer.dali.dto.configuration.control.ControlRuleDTO;
35  import fr.ifremer.dali.dto.configuration.control.PreconditionRuleDTO;
36  import fr.ifremer.dali.dto.data.measurement.MeasurementDTO;
37  import fr.ifremer.dali.dto.data.sampling.SamplingOperationDTO;
38  import fr.ifremer.dali.dto.enums.ControlElementValues;
39  import fr.ifremer.dali.dto.enums.FilterTypeValues;
40  import fr.ifremer.dali.dto.referential.DepartmentDTO;
41  import fr.ifremer.dali.dto.referential.TaxonDTO;
42  import fr.ifremer.dali.dto.referential.TaxonGroupDTO;
43  import fr.ifremer.dali.dto.referential.pmfm.PmfmDTO;
44  import fr.ifremer.dali.dto.referential.pmfm.QualitativeValueDTO;
45  import fr.ifremer.dali.service.DaliTechnicalException;
46  import fr.ifremer.dali.ui.swing.content.observation.operation.measurement.grouped.OperationMeasurementsGroupedRowModel;
47  import fr.ifremer.dali.ui.swing.content.observation.operation.measurement.grouped.OperationMeasurementsGroupedTableModel;
48  import fr.ifremer.dali.ui.swing.content.observation.operation.measurement.grouped.SamplingOperationComparator;
49  import fr.ifremer.dali.ui.swing.util.DaliUI;
50  import fr.ifremer.dali.ui.swing.util.table.AbstractDaliTableUIHandler;
51  import fr.ifremer.dali.ui.swing.util.table.AbstractDaliTableUIModel;
52  import fr.ifremer.dali.ui.swing.util.table.DaliColumnIdentifier;
53  import fr.ifremer.dali.ui.swing.util.table.PmfmTableColumn;
54  import fr.ifremer.quadrige3.core.dao.technical.factorization.pmfm.AllowedQualitativeValuesMap;
55  import fr.ifremer.quadrige3.ui.swing.table.editor.ExtendedComboBoxCellEditor;
56  import org.apache.commons.collections4.CollectionUtils;
57  import org.apache.commons.lang3.StringUtils;
58  import org.apache.commons.logging.Log;
59  import org.apache.commons.logging.LogFactory;
60  
61  import javax.swing.SwingUtilities;
62  import java.util.*;
63  import java.util.concurrent.atomic.AtomicInteger;
64  import java.util.stream.Collectors;
65  import java.util.stream.IntStream;
66  
67  import static org.nuiton.i18n.I18n.t;
68  
69  /**
70   * Controleur pour les mesures des prelevements (ecran prelevements/mesure).
71   */
72  public abstract class AbstractOperationMeasurementsGroupedTableUIHandler<
73      R extends OperationMeasurementsGroupedRowModel,
74      M extends AbstractOperationMeasurementsGroupedTableUIModel<MeasurementDTO, R, M>,
75      UI extends DaliUI<M, ?>
76      >
77      extends AbstractDaliTableUIHandler<R, M, UI> {
78  
79      private static final Log LOG = LogFactory.getLog(AbstractOperationMeasurementsGroupedTableUIHandler.class);
80  
81      // editor for sampling operations column
82      protected ExtendedComboBoxCellEditor<SamplingOperationDTO> samplingOperationCellEditor;
83      // editor for taxon group column
84      protected ExtendedComboBoxCellEditor<TaxonGroupDTO> taxonGroupCellEditor;
85      // editor for taxon column
86      protected ExtendedComboBoxCellEditor<TaxonDTO> taxonCellEditor;
87      // editor for analyst column
88      protected ExtendedComboBoxCellEditor<DepartmentDTO> departmentCellEditor;
89  
90      /**
91       * <p>Constructor for OperationMeasurementsGroupedTableUIHandler.</p>
92       */
93      public AbstractOperationMeasurementsGroupedTableUIHandler(String... properties) {
94          super(properties);
95      }
96  
97      @Override
98      protected String[] getRowPropertiesToIgnore() {
99          return new String[]{
100             R.PROPERTY_INPUT_TAXON_ID,
101             R.PROPERTY_INPUT_TAXON_NAME
102         };
103     }
104 
105     @Override
106     public abstract AbstractOperationMeasurementsGroupedTableModel<R> getTableModel();
107 
108     /**
109      * {@inheritDoc}
110      */
111     @Override
112     public void beforeInit(final UI ui) {
113         super.beforeInit(ui);
114         ui.setContextValue(createNewModel());
115     }
116 
117     protected abstract M createNewModel();
118 
119     protected abstract R createNewRow(boolean readOnly, SamplingOperationDTO parentBean);
120 
121     /**
122      * {@inheritDoc}
123      */
124     @Override
125     public void afterInit(final UI ui) {
126 
127         // Init UI
128         initUI(ui);
129 
130         // Create editors
131         createTaxonGroupCellEditor();
132         createTaxonCellEditor();
133         createDepartmentCellEditor();
134         resetCellEditors();
135 
136         // Init table
137         initTable();
138 
139         // Init listeners
140         initListeners();
141 
142     }
143 
144     /**
145      * Initialiser les listeners
146      */
147     protected void initListeners() {
148 
149         getModel().addPropertyChangeListener(evt -> {
150 
151             switch (evt.getPropertyName()) {
152                 case AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_SURVEY:
153 
154                     // update sampling operation cell combo box
155                     if (samplingOperationCellEditor != null)
156                         samplingOperationCellEditor.getCombo().setData(getModel().getSamplingOperations());
157 
158                     // load available pmfms
159                     loadMeasurements();
160                     break;
161 
162                 case AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_MEASUREMENTS_LOADED:
163 
164                     detectPreconditionedPmfms();
165                     break;
166 
167                 case AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_MEASUREMENT_FILTER:
168 
169                     // filter if measurements are loaded
170                     if (getModel().isMeasurementsLoaded())
171                         filterMeasurements();
172                     break;
173 
174                 case AbstractDaliTableUIModel.PROPERTY_SINGLE_ROW_SELECTED:
175 
176                     if (getModel().getSingleSelectedRow() != null && !getModel().getSingleSelectedRow().isEditable()) {
177                         return;
178                     }
179 
180                     // update taxonCellEditor
181                     updateTaxonCellEditor(getModel().getSingleSelectedRow(), false);
182                     updateTaxonGroupCellEditor(getModel().getSingleSelectedRow(), false);
183                     updateDepartmentCellEditor(false);
184                     updatePmfmCellEditors(getModel().getSingleSelectedRow(), null, false);
185                     break;
186 
187             }
188         });
189 
190     }
191 
192     /**
193      * Update all editors on measurements if they have preconditions
194      *
195      * @param row               the actual selected row
196      * @param firstPmfmId       the first pmfm Id to treat (can be null)
197      * @param resetValueAllowed allow or not the target value to be reset
198      */
199     private void updatePmfmCellEditors(R row, Integer firstPmfmId, boolean resetValueAllowed) {
200 
201         if (row == null || CollectionUtils.isEmpty(row.getIndividualPmfms())) return;
202 
203         // Build list of pmfms to process (if firstPmfmId is specified, it will be threat first)
204         List<PmfmDTO> pmfms;
205         if (firstPmfmId == null) {
206             pmfms = row.getIndividualPmfms();
207         } else {
208             pmfms = new ArrayList<>(row.getIndividualPmfms());
209             PmfmDTO firstPmfm = DaliBeans.findById(pmfms, firstPmfmId);
210             if (firstPmfm != null) {
211                 pmfms.remove(firstPmfm);
212                 // add this pmfm at first position
213                 pmfms.add(0, firstPmfm);
214             }
215         }
216 
217         // clear previous allowed values map
218         row.getAllowedQualitativeValuesMap().clear();
219         List<Integer> pmfmIds = pmfms.stream().map(PmfmDTO::getId).collect(Collectors.toList());
220 
221         for (PmfmDTO pmfm : pmfms) {
222 
223             // if there is a preconditioned rule for this pmfm
224             if (getModel().isPmfmIdHasPreconditions(pmfm.getId())) {
225 
226                 // get the measurement with this pmfm
227                 MeasurementDTO measurement = DaliBeans.getIndividualMeasurementByPmfmId(row, pmfm.getId());
228                 if (measurement == null) {
229                     // create empty measurement if needed
230                     measurement = DaliBeanFactory.newMeasurementDTO();
231                     measurement.setPmfm(pmfm);
232                     row.getIndividualMeasurements().add(measurement);
233                 }
234 
235                 if (pmfm.getParameter().isQualitative()) {
236 
237                     // Process allowed qualitative values
238                     getContext().getRuleListService().buildAllowedValuesByPmfmId(
239                         pmfm.getId(),
240                         measurement.getQualitativeValue(),
241                         pmfmIds,
242                         getModel().getPreconditionRulesByPmfmId(pmfm.getId()),
243                         row.getAllowedQualitativeValuesMap()
244                     );
245 
246                     // update combos of affected pmfms
247                     for (Integer targetPmfmId : row.getAllowedQualitativeValuesMap().getTargetIds()) {
248                         updateQualitativePmfmCellEditors(row, targetPmfmId, resetValueAllowed);
249                     }
250 
251                 } else {
252 
253                     // Process allowed numerical values TODO when used
254                     // buildAllowedValuesByPmfmId(pmfm.getId(), measurement.getQualitativeValue(), pmfmIds, row.getAllowedQualitativeValuesMap());
255                 }
256             }
257         }
258 
259         // then update all pmfm editors a last time
260         for (PmfmDTO pmfm : row.getIndividualPmfms()) {
261             updateQualitativePmfmCellEditors(row, pmfm.getId(), resetValueAllowed);
262         }
263 
264     }
265 
266     @SuppressWarnings("unchecked")
267     private void updateQualitativePmfmCellEditors(R row, int targetPmfmId, boolean resetValueAllowed) {
268 
269         PmfmDTO targetPmfm = DaliBeans.findById(row.getIndividualPmfms(), targetPmfmId);
270         PmfmTableColumn targetPmfmColumn = findPmfmColumnByPmfmId(getModel().getPmfmColumns(), targetPmfmId);
271 
272         // cellEditor must be a ExtendedComboBoxCellEditor
273         if (targetPmfmColumn == null || !(targetPmfmColumn.getCellEditor() instanceof ExtendedComboBoxCellEditor)) return; // simply skip
274         ExtendedComboBoxCellEditor<QualitativeValueDTO> comboBoxCellEditor = (ExtendedComboBoxCellEditor<QualitativeValueDTO>) targetPmfmColumn.getCellEditor();
275 
276         AllowedQualitativeValuesMap.AllowedValues allowedTargetIds = new AllowedQualitativeValuesMap.AllowedValues();
277 
278         Collection<Integer> sourcePmfmIds = row.getAllowedQualitativeValuesMap().getExistingSourcePmfmIds(targetPmfmId);
279         for (Integer sourcePmfmId : sourcePmfmIds) {
280 
281             // get existing measurement for this source pmfm
282             MeasurementDTO sourceMeasurement = DaliBeans.getIndividualMeasurementByPmfmId(row, sourcePmfmId);
283 
284             Integer sourceValueId = sourceMeasurement == null ? null : sourceMeasurement.getQualitativeValue() == null ? null : sourceMeasurement.getQualitativeValue().getId();
285             AllowedQualitativeValuesMap.AllowedValues list = row.getAllowedQualitativeValuesMap().getAllowedValues(targetPmfmId, sourcePmfmId, sourceValueId);
286             allowedTargetIds.addOrRetain(list);
287         }
288 
289         // Compute allowed values
290         List<QualitativeValueDTO> allowedQualitativeValues = DaliBeans.filterCollection(targetPmfm.getQualitativeValues(),
291             qualitativeValue -> allowedTargetIds.isAllowed(qualitativeValue.getId()));
292         comboBoxCellEditor.getCombo().setData(allowedQualitativeValues);
293         // fix the target value
294         if (resetValueAllowed) {
295             QualitativeValueDTO actualValue = (QualitativeValueDTO) targetPmfmColumn.getPmfmIdentifier().getValue(row);
296             if (allowedQualitativeValues.size() == 1) {
297                 if (!allowedQualitativeValues.get(0).equals(actualValue)) {
298                     // force a 1:1 relation
299                     getModel().setAdjusting(true);
300                     targetPmfmColumn.getPmfmIdentifier().setValue(row, allowedQualitativeValues.get(0));
301                     getModel().setAdjusting(false);
302                 }
303             } else {
304                 if (actualValue != null && !allowedQualitativeValues.contains(actualValue)) {
305                     // reset a bad 1:n relation
306                     getModel().setAdjusting(true);
307                     targetPmfmColumn.getPmfmIdentifier().setValue(row, null);
308                     getModel().setAdjusting(false);
309                 }
310             }
311         }
312     }
313 
314     private void detectPreconditionedPmfms() {
315 
316         if (CollectionUtils.isEmpty(getModel().getSurvey().getPreconditionedRules())) return;
317 
318         for (ControlRuleDTO preconditionedRule : getModel().getSurvey().getPreconditionedRules()) {
319 
320             for (PreconditionRuleDTO precondition : preconditionedRule.getPreconditions()) {
321 
322                 int basePmfmId = precondition.getBaseRule().getRulePmfms(0).getPmfm().getId();
323                 int usedPmfmId = precondition.getUsedRule().getRulePmfms(0).getPmfm().getId();
324 
325                 getModel().addPreconditionRuleByPmfmId(basePmfmId, precondition);
326                 if (precondition.isBidirectional())
327                     getModel().addPreconditionRuleByPmfmId(usedPmfmId, precondition);
328             }
329         }
330 
331     }
332 
333     private void createTaxonCellEditor() {
334 
335         taxonCellEditor = newExtendedComboBoxCellEditor(null, TaxonDTO.class, DecoratorService.WITH_CITATION_AND_REFERENT, false);
336 
337         taxonCellEditor.setAction("unfilter-taxon", "dali.common.unfilter.taxon", e -> {
338             // unfilter taxon
339             updateTaxonCellEditor(getModel().getSingleSelectedRow(), true);
340         });
341 
342     }
343 
344     private void updateTaxonCellEditor(R row, boolean forceNoFilter) {
345 
346         taxonCellEditor.getCombo().setActionEnabled(!forceNoFilter);
347 
348         List<TaxonDTO> taxons = row != null
349             ? getContext().getObservationService().getAvailableTaxons(forceNoFilter ? null : row.getTaxonGroup(), false)
350             : null;
351 
352         taxonCellEditor.getCombo().setData(taxons);
353 
354     }
355 
356     private void createTaxonGroupCellEditor() {
357 
358         taxonGroupCellEditor = newExtendedComboBoxCellEditor(null, OperationMeasurementsGroupedTableModel.TAXON_GROUP, false);
359 
360         taxonGroupCellEditor.setAction("unfilter-taxon", "dali.common.unfilter.taxon", e -> {
361             // unfilter taxon groups
362             updateTaxonGroupCellEditor(getModel().getSingleSelectedRow(), true);
363         });
364 
365     }
366 
367     private void updateTaxonGroupCellEditor(R row, boolean forceNoFilter) {
368 
369         taxonGroupCellEditor.getCombo().setActionEnabled(!forceNoFilter);
370 
371         List<TaxonGroupDTO> taxonGroups = row != null
372             ? getContext().getObservationService().getAvailableTaxonGroups(forceNoFilter ? null : row.getTaxon(), false)
373             : null;
374 
375         taxonGroupCellEditor.getCombo().setData(taxonGroups);
376 
377     }
378 
379     private void createDepartmentCellEditor() {
380 
381         departmentCellEditor = newExtendedComboBoxCellEditor(null, DepartmentDTO.class, false);
382 
383         departmentCellEditor.setAction("unfilter", "dali.common.unfilter", e -> {
384             if (!askBefore(t("dali.common.unfilter"), t("dali.common.unfilter.confirmation"))) {
385                 return;
386             }
387             // unfilter location
388             updateDepartmentCellEditor(true);
389         });
390 
391     }
392 
393     private void updateDepartmentCellEditor(boolean forceNoFilter) {
394 
395         departmentCellEditor.getCombo().setActionEnabled(!forceNoFilter
396             && getContext().getDataContext().isContextFiltered(FilterTypeValues.DEPARTMENT));
397 
398         departmentCellEditor.getCombo().setData(getContext().getObservationService().getAvailableDepartments(forceNoFilter));
399     }
400 
401     /**
402      * <p>resetCellEditors.</p>
403      */
404     public void resetCellEditors() {
405 
406         updateTaxonGroupCellEditor(null, false);
407         updateTaxonCellEditor(null, false);
408         updateDepartmentCellEditor(false);
409     }
410 
411     /**
412      * Load mesurements
413      */
414     private void loadMeasurements() {
415 
416         SwingUtilities.invokeLater(() -> {
417 
418             // Uninstall save state listener
419             uninstallSaveTableStateListener();
420 
421             // Build dynamic columns
422             // TODO manage column position depending on criteria (parametrized list or specific attribute)
423             // maybe split pmfms list in 2 separated list or move afterward
424             DaliColumnIdentifier<R> insertPosition = getTableModel().getPmfmInsertPosition();
425             addPmfmColumns(
426                 getModel().getPmfms(),
427                 SamplingOperationDTO.PROPERTY_INDIVIDUAL_PMFMS,
428                 DecoratorService.NAME_WITH_UNIT,
429                 insertPosition);
430 
431             boolean notEmpty = CollectionUtils.isNotEmpty(getModel().getPmfms());
432 
433             // Build rows
434             getModel().setRows(buildRows(!getModel().getSurvey().isEditable()));
435             getTableModel().setReadOnly(!getModel().getSurvey().isEditable());
436 
437             recomputeRowsValidState();
438 
439             // restore table from swing session
440             restoreTableState();
441 
442             // Apply measurement filter
443             filterMeasurements();
444 
445             // hide analyst if no pmfm
446 //            forceColumnVisibleAtLastPosition(OperationMeasurementsGroupedTableModel.ANALYST, notEmpty);
447             // Don't force position (Mantis #49939)
448             forceColumnVisible(OperationMeasurementsGroupedTableModel.ANALYST, notEmpty);
449 
450             // set columns with errors visible (Mantis #40752)
451             ensureColumnsWithErrorAreVisible(getModel().getRows());
452 
453             // Install save state listener
454             SwingUtilities.invokeLater(this::installSaveTableStateListener);
455 
456             getModel().setMeasurementsLoaded(true);
457         });
458 
459     }
460 
461     protected abstract void filterMeasurements();
462 
463     protected List<R> buildRows(boolean readOnly) {
464         List<R> rows = Lists.newArrayList();
465 
466         List<SamplingOperationDTO> samplingOperations = Lists.newArrayList(getModel().getSamplingOperations());
467         // sort by name
468         samplingOperations.sort(SamplingOperationComparator.instance);
469 
470         // Iterate over sampling operation
471         for (SamplingOperationDTO samplingOperation : samplingOperations) {
472 
473             // build rows
474             R row = null;
475             List<MeasurementDTO> measurements = Lists.newArrayList(samplingOperation.getIndividualMeasurements());
476             // sort by individual id
477             measurements.sort(Comparator.comparingInt(MeasurementDTO::getIndividualId));
478 
479             for (final MeasurementDTO measurement : measurements) {
480                 // check previous row
481                 if (row != null) {
482 
483                     // if individual id differs = new row
484                     if (!row.getIndividualId().equals(measurement.getIndividualId())) {
485                         row = null;
486                     }
487 
488                     // if taxon group or taxon differs, should update current row (see Mantis #0026647)
489                     else if (!Objects.equals(row.getTaxonGroup(), measurement.getTaxonGroup())
490                         || !Objects.equals(row.getTaxon(), measurement.getTaxon())
491                         || !Objects.equals(row.getInputTaxonId(), measurement.getInputTaxonId())
492                         || !Objects.equals(row.getInputTaxonName(), measurement.getInputTaxonName())) {
493 
494                         // update taxon group and taxon if previously empty
495                         if (row.getTaxonGroup() == null) {
496                             row.setTaxonGroup(measurement.getTaxonGroup());
497                         } else if (measurement.getTaxonGroup() != null && !row.getTaxonGroup().equals(measurement.getTaxonGroup())) {
498                             // the taxon group in measurement differs
499                             LOG.error(String.format("taxon group in measurement (id=%s) differs with taxon group in previous measurements with same individual id (=%s) !",
500                                 measurement.getId(), measurement.getIndividualId()));
501                         }
502                         if (row.getTaxon() == null) {
503                             row.setTaxon(measurement.getTaxon());
504                         } else if (measurement.getTaxon() != null && !row.getTaxon().equals(measurement.getTaxon())) {
505                             // the taxon in measurement differs
506                             LOG.error(String.format("taxon in measurement (id=%s) differs with taxon in previous measurements with same individual id (=%s) !",
507                                 measurement.getId(), measurement.getIndividualId()));
508                         }
509                         if (row.getInputTaxonId() == null) {
510                             row.setInputTaxonId(measurement.getInputTaxonId());
511                         } else if (measurement.getInputTaxonId() != null && !row.getInputTaxonId().equals(measurement.getInputTaxonId())) {
512                             // the input taxon Id in measurement differs
513                             LOG.error(String.format("input taxon id in measurement (id=%s) differs with input taxon id in previous measurements with same individual id (=%s) !",
514                                 measurement.getId(), measurement.getIndividualId()));
515                         }
516                         if (StringUtils.isBlank(row.getInputTaxonName())) {
517                             row.setInputTaxonName(measurement.getInputTaxonName());
518                         } else if (measurement.getInputTaxonName() != null && !row.getInputTaxonName().equals(measurement.getInputTaxonName())) {
519                             // the input taxon name in measurement differs
520                             LOG.error(String.format("input taxon name in measurement (id=%s) differs with input taxon name in previous measurements with same individual id (=%s) !",
521                                 measurement.getId(), measurement.getIndividualId()));
522                         }
523                     }
524                 }
525 
526                 // build a new row
527                 if (row == null) {
528                     row = createNewRow(readOnly, samplingOperation);
529                     row.setIndividualPmfms(new ArrayList<>(getModel().getPmfms()));
530                     row.setIndividualId(measurement.getIndividualId());
531                     row.setTaxonGroup(measurement.getTaxonGroup());
532                     row.setTaxon(measurement.getTaxon());
533                     row.setInputTaxonId(measurement.getInputTaxonId());
534                     row.setInputTaxonName(measurement.getInputTaxonName());
535                     row.setAnalyst(measurement.getAnalyst());
536                     // Mantis #26724 or #29130: don't take only one comment but a concatenation of all measurements comments
537 //                    row.setComment(measurement.getComment());
538 
539                     row.setValid(true);
540 
541                     rows.add(row);
542 
543                     // add errors on new row if samplingOperations have some
544                     List<ErrorDTO> errors = DaliBeans.filterCollection(samplingOperation.getErrors(),
545                         input -> (ControlElementValues.MEASUREMENT.getCode().equals(input.getControlElementCode())
546                             || ControlElementValues.TAXON_MEASUREMENT.getCode().equals(input.getControlElementCode()))
547                             && Objects.equals(input.getIndividualId(), measurement.getIndividualId())
548                     );
549                     DaliBeans.addUniqueErrors(row, errors);
550                 }
551 
552                 // add measurement on current row
553                 row.getIndividualMeasurements().add(measurement);
554 
555                 // add errors on row if measurement have some
556                 DaliBeans.addUniqueErrors(row, measurement.getErrors());
557 
558             }
559         }
560 
561         // Mantis #26724 or #29130: don't take only one comment but a concatenation of all measurements comments
562         for (R row : rows) {
563 
564             // join and affect
565             row.setComment(DaliBeans.getUnifiedCommentFromIndividualMeasurements(row));
566         }
567 
568         return rows;
569     }
570 
571     /**
572      * {@inheritDoc}
573      */
574     @Override
575     protected void onRowModified(int rowIndex, R row, String propertyName, Integer propertyIndex, Object oldValue, Object newValue) {
576 
577         // if a value of individual pmfm changes
578         if (R.PROPERTY_INDIVIDUAL_PMFMS.equals(propertyName)) {
579 
580             // no need to tell the table is modified if no pmfms value change
581             if (oldValue == newValue) return;
582 
583             // if a value has been changed, update other editor with preconditions
584             if (!getModel().isAdjusting())
585                 updatePmfmCellEditors(row, propertyIndex, newValue != null);
586         }
587 
588         // update individual id when sampling operation changes
589         if (R.PROPERTY_SAMPLING_OPERATION.equals(propertyName)) {
590 
591             // remove measurement from old sampling
592             if (oldValue != null) {
593                 SamplingOperationDTO oldSamplingOperation = (SamplingOperationDTO) oldValue;
594                 oldSamplingOperation.removeAllIndividualMeasurements(row.getIndividualMeasurements());
595                 oldSamplingOperation.setDirty(true);
596 
597                 resetIndividualMeasurementIds(row);
598             }
599 
600             // recalculate individual id
601             if (newValue != null) {
602                 // fire event for parent listener
603                 getModel().firePropertyChanged(AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_SAMPLING_OPERATION, null, newValue);
604             }
605         }
606 
607         // update taxon when taxon group is updated
608         if (R.PROPERTY_TAXON_GROUP.equals(propertyName)) {
609             // filter taxon
610             updateTaxonCellEditor(row, false);
611 
612             // reset measurementId if no more taxon group or new taxon group
613             if ((newValue == null ^ oldValue == null) && row.getTaxon() == null) {
614 
615                 // before, remove those measurements from sampling operation
616                 if (row.getSamplingOperation() != null) {
617                     row.getSamplingOperation().removeAllIndividualMeasurements(row.getIndividualMeasurements());
618                 }
619 
620                 resetIndividualMeasurementIds(row);
621             }
622         }
623 
624         if (R.PROPERTY_TAXON.equals(propertyName)) {
625             // filter taxon
626             updateTaxonGroupCellEditor(row, false);
627 
628             // reset measurementId if no more taxon or new taxon
629             if ((newValue == null ^ oldValue == null) && row.getTaxonGroup() == null) {
630 
631                 // before, remove those measurements from sampling operation
632                 if (row.getSamplingOperation() != null) {
633                     row.getSamplingOperation().removeAllIndividualMeasurements(row.getIndividualMeasurements());
634                 }
635 
636                 resetIndividualMeasurementIds(row);
637             }
638 
639             // update input taxon
640             TaxonDTO taxon = (TaxonDTO) newValue;
641             row.setInputTaxonId(taxon != null ? taxon.getId() : null);
642             row.setInputTaxonName(taxon != null ? taxon.getName() : null);
643         }
644 
645         if (oldValue != newValue) {
646 
647             // recompute valid state on all rows
648             recomputeRowsValidState();
649 
650             row.setInitialized(false);
651         }
652 
653         // fire modify event at the end
654         super.onRowModified(rowIndex, row, propertyName, propertyIndex, oldValue, newValue);
655     }
656 
657     private PmfmTableColumn findPmfmColumnByPmfmId(List<PmfmTableColumn> pmfmTableColumns, int pmfmId) {
658 
659         if (pmfmTableColumns != null) {
660             for (PmfmTableColumn pmfmTableColumn : pmfmTableColumns) {
661                 if (pmfmTableColumn.getPmfmId() == pmfmId) return pmfmTableColumn;
662             }
663         }
664 
665         return null;
666     }
667 
668     public void duplicateSelectedRow() {
669 
670         if (getModel().getSelectedRows().size() == 1 && getModel().getSingleSelectedRow() != null) {
671             R rowToDuplicate = getModel().getSingleSelectedRow();
672             R row = createNewRow(false, rowToDuplicate.getSamplingOperation());
673             row.setTaxonGroup(rowToDuplicate.getTaxonGroup());
674             row.setTaxon(rowToDuplicate.getTaxon());
675             row.setInputTaxonId(rowToDuplicate.getInputTaxonId());
676             row.setInputTaxonName(rowToDuplicate.getInputTaxonName());
677             row.setComment(rowToDuplicate.getComment());
678             row.setIndividualPmfms(rowToDuplicate.getIndividualPmfms());
679             // add also analyst (Mantis #45565)
680             row.setAnalyst(rowToDuplicate.getAnalyst());
681             // duplicate measurements
682             row.setIndividualMeasurements(DaliBeans.duplicate(rowToDuplicate.getIndividualMeasurements()));
683 
684             // Add duplicate measurement to table
685             getModel().insertRowAfterSelected(row);
686 
687             if (row.getSamplingOperation() != null) {
688                 setDirty(row.getSamplingOperation());
689                 getModel().firePropertyChanged(AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_SAMPLING_OPERATION, null, row.getSamplingOperation());
690             }
691             recomputeRowsValidState();
692             getModel().setModify(true);
693             setFocusOnCell(row);
694         }
695     }
696 
697     /**
698      * {@inheritDoc}
699      */
700     @Override
701     protected boolean isRowValid(R row) {
702         boolean valid = super.isRowValid(row);
703 
704         if (!valid && !row.isMandatoryValid()) {
705             // check invalid mandatory errors
706             new ArrayList<>(row.getInvalidMandatoryIdentifiers()).forEach(invalidIdentifier -> {
707                 if (row.getMultipleValuesOnIdentifier().contains(invalidIdentifier)) {
708                     // if this identifier has multiple value, remove error
709                     row.getErrors().removeIf(error -> error.getPropertyName().size() == 1 && error.getPropertyName().contains(invalidIdentifier.getPropertyName()));
710                     row.getInvalidMandatoryIdentifiers().remove(invalidIdentifier);
711                 }
712             });
713             valid = row.isMandatoryValid();
714         }
715 
716         hasNoPerfectDuplicates(row);
717 
718         return valid && hasAnalyst(row) && hasNoUnicityDuplicates(row);
719     }
720 
721     private boolean hasAnalyst(R row) {
722 
723         if (!row.getMultipleValuesOnIdentifier().contains(AbstractOperationMeasurementsGroupedTableModel.ANALYST)
724             && row.getAnalyst() == null && row.getIndividualMeasurements().stream().anyMatch(measurement -> !DaliBeans.isMeasurementEmpty(measurement))) {
725             DaliBeans.addError(row,
726                 t("dali.validator.error.analyst.required"),
727                 R.PROPERTY_ANALYST);
728             return false;
729         }
730         return true;
731 
732     }
733 
734     private void hasNoPerfectDuplicates(R row) {
735 
736         // Mantis #37234 : this test must be executed even if there is no taxon
737         if (getModel().getRowCount() < 2 /*|| (row.getTaxonGroup() == null && row.getTaxon() == null)*/) {
738             // no need to check duplicates
739             return;
740         }
741 
742         for (R otherRow : getModel().getRows()) {
743             if (otherRow == row /*|| (otherRow.getTaxonGroup() == null && otherRow.getTaxon() == null)*/) continue;
744 
745             if (Objects.equals(row.getSamplingOperation(), otherRow.getSamplingOperation())
746                 && Objects.equals(row.getTaxonGroup(), otherRow.getTaxonGroup())
747                 && Objects.equals(row.getTaxon(), otherRow.getTaxon())
748                 && haveSameMeasurements(row, otherRow) // Both measurements must have same non empty measurements (Mantis #42170)
749             ) {
750                 // if sampling, taxon group and taxon equals, check all measurement values
751                 boolean allValuesEquals = true;
752                 Set<Integer> notNullMeasurementPmfmIds = Sets.newHashSet();
753                 for (MeasurementDTO measurement : row.getIndividualMeasurements()) {
754 
755                     // find the measurement with same pmfm in the other row
756                     MeasurementDTO otherMeasurement = DaliBeans.getIndividualMeasurementByPmfmId(otherRow, measurement.getPmfm().getId());
757 
758                     // fix Mantis #38335
759                     if (otherMeasurement == null) continue;
760 
761                     if (measurement.getPmfm().getParameter().isQualitative()) {
762                         if (!Objects.equals(measurement.getQualitativeValue(), otherMeasurement.getQualitativeValue())) {
763                             allValuesEquals = false;
764                             break;
765                         }
766                     } else {
767                         if (!Objects.equals(measurement.getNumericalValue(), otherMeasurement.getNumericalValue())) {
768                             allValuesEquals = false;
769                             break;
770                         }
771                     }
772 
773                     // collect pmfm ids
774                     if (!DaliBeans.isMeasurementEmpty(measurement)) {
775                         notNullMeasurementPmfmIds.add(measurement.getPmfm().getId());
776                     }
777                 }
778                 if (allValuesEquals) {
779 
780                     // The taxonGroup and taxon column are highlighted only if filled (Mantis #44713)
781                     List<String> propertiesToHighlight = new ArrayList<>();
782                     propertiesToHighlight.add(R.PROPERTY_SAMPLING_OPERATION);
783                     if (row.getTaxonGroup() != null)
784                         propertiesToHighlight.add(R.PROPERTY_TAXON_GROUP);
785                     if (row.getTaxon() != null)
786                         propertiesToHighlight.add(R.PROPERTY_TAXON);
787 
788                     // Mantis #0026890 The check for perfect duplicates is now a warning
789                     DaliBeans.addWarning(row,
790                         t("dali.samplingOperation.measurement.grouped.duplicate"),
791                         propertiesToHighlight.toArray(new String[0]));
792 
793                     for (Integer pmfmId : notNullMeasurementPmfmIds) {
794                         DaliBeans.addWarning(row,
795                             t("dali.samplingOperation.measurement.grouped.duplicate"),
796                             pmfmId,
797                             R.PROPERTY_INDIVIDUAL_PMFMS);
798                     }
799                     return;
800                 }
801             }
802         }
803     }
804 
805     private boolean haveSameMeasurements(R row, R otherRow) {
806         List<Integer> pmfmIds = row.getIndividualMeasurements().stream()
807             .filter(measurement -> !DaliBeans.isMeasurementEmpty(measurement))
808             .map(measurement -> measurement.getPmfm().getId())
809             .collect(Collectors.toList());
810         List<Integer> otherPmfmIds = otherRow.getIndividualMeasurements().stream()
811             .filter(measurement -> !DaliBeans.isMeasurementEmpty(measurement))
812             .map(measurement -> measurement.getPmfm().getId())
813             .collect(Collectors.toList());
814         return pmfmIds.containsAll(otherPmfmIds) && otherPmfmIds.containsAll(pmfmIds);
815     }
816 
817     private boolean hasNoUnicityDuplicates(R row) {
818 
819         if (getModel().getRowCount() < 2
820             || (row.getTaxonGroup() == null && row.getTaxon() == null)
821             || CollectionUtils.isEmpty(getModel().getUniquePmfms())) {
822             // no need to check duplicates
823             return true;
824         }
825 
826         for (R otherRow : getModel().getRows()) {
827             if (otherRow == row || (otherRow.getTaxonGroup() == null && otherRow.getTaxon() == null)) continue;
828 
829             if (Objects.equals(row.getSamplingOperation(), otherRow.getSamplingOperation())
830                 && Objects.equals(row.getTaxonGroup(), otherRow.getTaxonGroup())
831                 && Objects.equals(row.getTaxon(), otherRow.getTaxon())
832             ) {
833                 // if sampling, taxon group and taxon equals, check measurement values with unique pmfms
834 
835                 for (PmfmDTO uniquePmfm : getModel().getUniquePmfms()) {
836 
837                     // find the measurement with this pmfm in the row
838                     MeasurementDTO measurement = DaliBeans.getIndividualMeasurementByPmfmId(row, uniquePmfm.getId());
839 
840                     if (measurement == null) {
841                         continue;
842                     }
843 
844                     // find the measurement with this pmfm in the other row
845                     MeasurementDTO otherMeasurement = DaliBeans.getIndividualMeasurementByPmfmId(otherRow, uniquePmfm.getId());
846 
847                     if (otherMeasurement == null) {
848                         continue;
849                     }
850 
851                     if ((measurement.getPmfm().getParameter().isQualitative() && Objects.equals(measurement.getQualitativeValue(), otherMeasurement.getQualitativeValue()))
852                         || (!measurement.getPmfm().getParameter().isQualitative() && Objects.equals(measurement.getNumericalValue(), otherMeasurement.getNumericalValue()))) {
853 
854                         // duplicate value found
855                         DaliBeans.addError(row,
856                             t("dali.samplingOperation.measurement.grouped.duplicate.taxonUnique", decorate(uniquePmfm, DecoratorService.NAME_WITH_UNIT)),
857                             uniquePmfm.getId(),
858                             R.PROPERTY_SAMPLING_OPERATION,
859                             R.PROPERTY_TAXON_GROUP,
860                             R.PROPERTY_TAXON,
861                             R.PROPERTY_INDIVIDUAL_PMFMS);
862                         return false;
863 
864                     }
865                 }
866             }
867         }
868 
869         return true;
870     }
871 
872     private void resetIndividualMeasurementIds(R row) {
873         if (row != null && CollectionUtils.isNotEmpty(row.getIndividualMeasurements())) {
874             for (MeasurementDTO individualMeasurement : row.getIndividualMeasurements()) {
875                 individualMeasurement.setId(null);
876             }
877         }
878     }
879 
880     protected abstract void initTable();
881 
882     @Override
883     protected void installSortController() {
884         super.installSortController();
885 
886         // set alpha numeric comparator (Mantis #47541)
887         if (getFixedTable() != null) {
888             getSortController().setComparator(
889                 getFixedTable().getColumnModel().getColumnExt(OperationMeasurementsGroupedTableModel.SAMPLING).getModelIndex(),
890                 SamplingOperationComparator.instance
891             );
892         }
893     }
894 
895     /**
896      * {@inheritDoc}
897      */
898     @Override
899     protected void onRowsAdded(List<R> addedRows) {
900         super.onRowsAdded(addedRows);
901 
902         if (addedRows.size() == 1) {
903             R newRow = addedRows.get(0);
904 
905             // If newRow has no pmfm and no measurement (a real new row)
906             if (CollectionUtils.isEmpty(newRow.getIndividualPmfms()) && CollectionUtils.isEmpty(newRow.getIndividualMeasurements())) {
907 
908                 // Affect individual pmfms from parent model
909                 newRow.setIndividualPmfms(new ArrayList<>(getModel().getPmfms()));
910 
911                 // Create empty measurements
912                 DaliBeans.createEmptyMeasurements(newRow);
913 
914                 // Set default value if filters are set
915                 if (getModel().getSamplingFilter() != null) {
916                     newRow.setSamplingOperation(getModel().getSamplingFilter());
917                 }
918 //                if (getModel().getTaxonGroupFilter() != null) {
919 //                    newRow.setTaxonGroup(getModel().getTaxonGroupFilter());
920 //                }
921 //                if (getModel().getTaxonFilter() != null) {
922 //                    newRow.setTaxon(getModel().getTaxonFilter());
923 //                }
924 
925                 // set default analyst from pmfm strategies (Mantis #42617)
926                 newRow.setAnalyst(getContext().getProgramStrategyService().getAnalysisDepartmentOfAppliedStrategyBySurvey(getModel().getSurvey()));
927 
928             }
929 
930             // reset the cell editors
931             resetCellEditors();
932 
933             getModel().setModify(true);
934 
935             // Ajouter le focus sur la cellule de la ligne cree
936             setFocusOnCell(newRow);
937         }
938     }
939 
940     /**
941      * <p>removeIndividualMeasurements.</p>
942      */
943     public void removeIndividualMeasurements() {
944 
945         if (getModel().getSelectedRows().isEmpty()) {
946             LOG.warn("No row selected");
947             return;
948         }
949 
950         if (askBeforeDelete(t("dali.action.delete.survey.measurement.titre"), t("dali.action.delete.survey.measurement.message"))) {
951 
952             // collect sampling operation to update
953             Set<SamplingOperationDTO> samplingOperationsToUpdate = getModel().getSelectedRows().stream()
954                 // a row with measurements
955                 .filter(row -> CollectionUtils.isNotEmpty(row.getIndividualMeasurements()))
956                 // collect them
957                 .flatMap(row -> row.getIndividualMeasurements().stream())
958                 // filter with existing sapling operation
959                 .filter(measurement -> measurement.getSamplingOperation() != null)
960                 // remove each measurement from sampling operation
961                 .peek(measurement -> measurement.getSamplingOperation().removeIndividualMeasurements(measurement))
962                 // return the sampling operation
963                 .map(MeasurementDTO::getSamplingOperation)
964                 .collect(Collectors.toSet());
965 
966             getModel().setModify(true);
967 
968             // keep selected rows in a new list
969             List<R> rowsToDelete = new ArrayList<>(getModel().getSelectedRows());
970             // unselect them to prevent selection restore problem (Mantis #52738)
971             unselectAllRows();
972             // delete rows
973             getModel().deleteRows(rowsToDelete);
974 
975             // final update
976             samplingOperationsToUpdate.forEach(samplingOperation -> {
977                 samplingOperation.setDirty(true);
978                 getModel().firePropertyChanged(AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_SAMPLING_OPERATION, null, samplingOperation);
979             });
980 
981             recomputeRowsValidState();
982         }
983     }
984 
985     /**
986      * Save grouped measurements on parent model(s)
987      * This replace inline save (see Mantis #52401)
988      */
989     public void save() {
990 
991         // Get all existing measurements in beans
992         List<SamplingOperationDTO> beansToSave = getModel().getSamplingOperations().stream()
993             .sorted(SamplingOperationComparator.instance)
994             .collect(Collectors.toList());
995 
996         List<MeasurementDTO> existingMeasurements = beansToSave.stream()
997             .flatMap(bean -> bean.getIndividualMeasurements().stream())
998             .collect(Collectors.toList());
999 
1000         // get the minimum negative measurement id
1001         AtomicInteger minNegativeId = new AtomicInteger(
1002             Math.min(
1003                 0,
1004                 existingMeasurements.stream()
1005                     .filter(measurement -> measurement != null && measurement.getId() != null)
1006                     .mapToInt(MeasurementDTO::getId)
1007                     .min()
1008                     .orElse(0)
1009             )
1010         );
1011 
1012         Multimap<SamplingOperationDTO, R> rowsByBean = ArrayListMultimap.create();
1013 
1014         // Get ordered rows by bean
1015         IntStream.range(0, getTable().getRowCount())
1016             .mapToObj(i -> getTableModel().getEntry(getTable().convertRowIndexToModel(i)))
1017             // skip non dirty row (Mantis #52658)
1018             .filter(row -> !row.isInitialized())
1019             .forEach(row -> {
1020                 SamplingOperationDTO bean = row.getSamplingOperation();
1021                 if (bean == null) {
1022                     throw new DaliTechnicalException("The parent bean is null for a grouped measurements row");
1023                 }
1024                 rowsByBean.put(bean, row);
1025             });
1026 
1027         rowsByBean.keySet().forEach(bean -> {
1028 
1029             AtomicInteger individualId = new AtomicInteger();
1030             List<MeasurementDTO> beanMeasurements = new ArrayList<>();
1031 
1032             // Iterate over each row
1033             rowsByBean.get(bean).stream()
1034                 // Test is this row is to save or not
1035                 .filter(this::isRowToSave)
1036                 .forEach(row -> {
1037 
1038                     // Affect row individual ids according this order
1039                     row.setIndividualId(individualId.incrementAndGet());
1040 
1041                     // Iterate over row's measurements
1042                     row.getIndividualMeasurements().forEach(measurement -> {
1043                         if (measurement.getId() == null) {
1044                             // this measurement was not in bean, so add it with next negative id
1045                             measurement.setId(minNegativeId.decrementAndGet());
1046                         }
1047                         // update this measurement
1048                         updateMeasurementFromRow(measurement, row);
1049                         // Add to new list
1050                         beanMeasurements.add(measurement);
1051                     });
1052 
1053                 });
1054 
1055             // Affect new list of measurements
1056             bean.getIndividualMeasurements().clear();
1057             bean.getIndividualMeasurements().addAll(beanMeasurements);
1058 
1059             setDirty(bean);
1060             // Remove this bean from the list
1061             beansToSave.remove(bean);
1062         });
1063 
1064         // Clean remaining beans
1065         beansToSave.forEach(bean -> {
1066             // If a bean still in this map, it means there is no row of this bean, so remove all
1067             bean.getIndividualMeasurements().clear();
1068             setDirty(bean);
1069         });
1070     }
1071 
1072     private void updateMeasurementFromRow(MeasurementDTO measurement, R row) {
1073         measurement.setSamplingOperation(row.getSamplingOperation());
1074         measurement.setIndividualId(row.getIndividualId());
1075         measurement.setTaxonGroup(row.getTaxonGroup());
1076         measurement.setTaxon(row.getTaxon());
1077         measurement.setInputTaxonId(row.getInputTaxonId());
1078         measurement.setInputTaxonName(row.getInputTaxonName());
1079         measurement.setComment(row.getComment());
1080         measurement.setAnalyst(row.getAnalyst());
1081     }
1082 
1083     private void setDirty(SamplingOperationDTO bean) {
1084         bean.setDirty(true);
1085         getModel().firePropertyChanged(AbstractOperationMeasurementsGroupedTableUIModel.PROPERTY_SAMPLING_OPERATION, null, bean);
1086     }
1087 
1088     /**
1089      * Determine if this row is to be saved
1090      *
1091      * @param row to test
1092      * @return true if to save (default)
1093      */
1094     protected boolean isRowToSave(R row) {
1095         return
1096             // The row must exists
1097             row != null
1098                 // and contains at least 1 non empty individual measurement or a taxon group or a taxon
1099                 && (row.hasTaxonInformation() || row.hasNonEmptyMeasurements());
1100     }
1101 
1102 }