Tony CHEMIT pushed to branch develop at ultreiaio / ird-observe Commits: 23507f24 by Tony Chemit at 2024-03-21T14:54:48+01:00 update pom - - - - - 4e2aa441 by Tony Chemit at 2024-03-21T14:54:48+01:00 fix Observations - Types d'activités par zones FPA report (missing GroupBy for postgresql) - - - - - edd00f7a by Tony Chemit at 2024-03-21T14:54:48+01:00 Fix save step when simple referential synchronize (was using a not open data source) - - - - - 1cf5f9f1 by Tony Chemit at 2024-03-21T14:54:48+01:00 Checks parameters in generated services local - - - - - f64c2710 by Tony Chemit at 2024-03-21T14:54:48+01:00 Improve webmotion injectors, jsonAware one can now return null value - - - - - cf32dd6f by Tony Chemit at 2024-03-21T14:54:48+01:00 Synchro avancée données - Mise à jour de l'arbre de sélection après avoir enregistrer des actions - - - - - 22 changed files: - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/save/actions/Start.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/DataSynchroModel.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/DataSynchroUI.jaxx - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/DataSynchroUIHandler.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/actions/Apply.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/actions/Start.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/DataSelectionTreePane.jaxx - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/DataSelectionTreePaneHandler.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/DataSelectionTreePaneModel.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/actions/DataSelectionTreePaneActionSupport.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/actions/RegisterCopy.java - client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/actions/RegisterDelete.java - core/persistence/report/src/main/resources/META-INF/report/default/ps/psObservationActivitiesByZone.report - pom.xml - server/core/src/main/java/fr/ird/observe/server/injector/JsonAwareDtoInjector.java - toolkit/api/src/main/java/fr/ird/observe/navigation/tree/ToolkitTreeModelSupport.java - toolkit/api/src/main/java/fr/ird/observe/navigation/tree/io/ToolkitTreeFlatModel.java - toolkit/api/src/main/java/fr/ird/observe/navigation/tree/selection/SelectionTreeModelSupport.java - toolkit/plugin/src/main/java/fr/ird/observe/toolkit/maven/plugin/service/ServiceGenerateLocalRunner.java - toolkit/plugin/src/main/java/fr/ird/observe/toolkit/maven/plugin/service/ServiceLocalMethodDescriptionImpl.java - toolkit/server/src/main/java/org/debux/webmotion/server/handler/injector/CollectionInjector.java - toolkit/server/src/main/java/org/debux/webmotion/server/handler/injector/JsonInjector.java Changes: ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/save/actions/Start.java ===================================== @@ -93,7 +93,7 @@ public class Start extends SaveLocalUIActionSupport { stepModel.getClientConfig().updateBackupDirectory(backupFile); } if (stepModel.containsStepForSave(AdminStep.SYNCHRONIZE)) { - saveUnidirectionalSynchronizeReferential(); + saveUnidirectionalSynchronizeReferential(source); } sendMessage(t("observe.ui.datasource.editor.actions.operation.message.done", new Date(), Strings.convertTime(TimeLog.getTime() - t00))); } @@ -102,7 +102,7 @@ public class Start extends SaveLocalUIActionSupport { } - private void saveUnidirectionalSynchronizeReferential() { + private void saveUnidirectionalSynchronizeReferential(ObserveSwingDataSource source) { SynchronizeModel stepModel = ui.getModel().getSynchronizeReferentielModel(); @@ -113,8 +113,10 @@ public class Start extends SaveLocalUIActionSupport { sendMessage(t("observe.ui.datasource.editor.actions.synchro.referential.message.script.path", referentialSynchronizeContext.getSqlScriptPath())); } sendMessage("Mise à jour de la date de dernière de synchronisation de référentiel simple."); - referentialSynchronizeContext.finish(stepModel.getSource().getSynchronizeService()); - stepModel.getSource().setModified(true); + referentialSynchronizeContext.finish(source.getSynchronizeService()); + if (source.isLocal()) { + source.setModified(true); + } sendMessage(t("observe.ui.datasource.editor.actions.synchro.referential.message.apply.done", new Date())); } ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/DataSynchroModel.java ===================================== @@ -27,8 +27,10 @@ import fr.ird.observe.client.datasource.actions.AdminStep; import fr.ird.observe.client.datasource.actions.AdminUIModel; import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePaneModel; import fr.ird.observe.client.datasource.api.data.DataTaskSupport; +import fr.ird.observe.client.datasource.api.data.TaskSide; import fr.ird.observe.client.datasource.editor.api.wizard.connexion.DataSourceSelectorModel; import fr.ird.observe.datasource.configuration.ObserveDataSourceInformation; +import io.ultreia.java4all.util.TwoSideContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.nuiton.jaxx.runtime.swing.wizard.ext.WizardState; @@ -41,11 +43,10 @@ import javax.swing.DefaultListModel; * @author Tony Chemit - dev@tchemit.fr * @since 5.0 */ -public class DataSynchroModel extends AdminActionModel { +public class DataSynchroModel extends AdminActionModel implements TwoSideContext<TaskSide, DataSelectionTreePaneModel> { public static final String LEFT_MODEL = "leftModel"; public static final String RIGHT_MODEL = "rightModel"; - private static final String TASKS_EMPTY_PROPERTY_NAME = "tasksEmpty"; private static final Logger log = LogManager.getLogger(DataSynchroModel.class); /** @@ -56,6 +57,7 @@ public class DataSynchroModel extends AdminActionModel { * Right model. */ private final DataSelectionTreePaneModel rightModel = new DataSelectionTreePaneModel(); + /** * Registered tasks to apply. */ @@ -65,15 +67,13 @@ public class DataSynchroModel extends AdminActionModel { super(AdminStep.DATA_SYNCHRONIZE); } - public DataSelectionTreePaneModel getModel(boolean left) { - return left ? getLeftModel() : getRightModel(); - } - - public DataSelectionTreePaneModel getLeftModel() { + @Override + public DataSelectionTreePaneModel left() { return leftModel; } - public DataSelectionTreePaneModel getRightModel() { + @Override + public DataSelectionTreePaneModel right() { return rightModel; } @@ -120,8 +120,10 @@ public class DataSynchroModel extends AdminActionModel { @Override public void destroy() { super.destroy(); - leftModel.dispose(); - rightModel.dispose(); + left().dispose(); + right().dispose(); +// leftModel.dispose(); +// rightModel.dispose(); tasks.clear(); } @@ -132,9 +134,9 @@ public class DataSynchroModel extends AdminActionModel { rightModel.finalizeSelectionModel(rightModel.getSelectionDataModel().getRoot(), leftModel.getDataIds()); } - public void rebuildSelectionModel(boolean left, boolean rebuildFlatModel) { - DataSelectionTreePaneModel otherSideModel = getModel(!left); - getModel(left).rebuildSelectionModel(rebuildFlatModel, otherSideModel.getDataIds()); + public void rebuildSelectionModel(TaskSide side, boolean rebuildFlatModel) { + DataSelectionTreePaneModel otherSideModel = onOppositeSide(side); + onSameSide(side).rebuildSelectionModel(rebuildFlatModel, otherSideModel.getDataIds()); } public DefaultListModel<DataTaskSupport> getTasks() { ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/DataSynchroUI.jaxx ===================================== @@ -18,7 +18,7 @@ #L% --> -<fr.ird.observe.client.datasource.actions.AdminTabUI> +<fr.ird.observe.client.datasource.actions.AdminTabUI implements="TwoSideContext<TaskSide, DataSelectionTreePane>"> <import> fr.ird.observe.client.util.UIHelper @@ -26,6 +26,8 @@ fr.ird.observe.client.datasource.actions.AdminStep fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePane fr.ird.observe.client.datasource.api.data.DataTaskSupport + fr.ird.observe.client.datasource.api.data.TaskSide + io.ultreia.java4all.util.TwoSideContext static io.ultreia.java4all.i18n.I18n.t </import> @@ -33,6 +35,16 @@ public static DataSynchroUI get(AdminUI ui) { return get(ui, AdminStep.DATA_SYNCHRONIZE); } + +@Override +public DataSelectionTreePane left() { + return leftTreePane; +} + +@Override +public DataSelectionTreePane right() { + return rightTreePane; +} ]]> </script> @@ -53,8 +65,8 @@ public static DataSynchroUI get(AdminUI ui) { <row> <cell weightx="1"> <JPanel layout="{new GridLayout(1, 0)}"> - <DataSelectionTreePane id="leftTreePane" constructorParams="UIHelper.initialContext(this, false)"/> - <DataSelectionTreePane id="rightTreePane" constructorParams="UIHelper.initialContext(this, true)"/> + <DataSelectionTreePane id="leftTreePane" constructorParams="UIHelper.initialContext(this, TaskSide.FROM_LEFT)"/> + <DataSelectionTreePane id="rightTreePane" constructorParams="UIHelper.initialContext(this, TaskSide.FROM_RIGHT)"/> </JPanel> </cell> </row> ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/DataSynchroUIHandler.java ===================================== @@ -26,6 +26,7 @@ import fr.ird.observe.client.datasource.actions.AdminTabUIHandler; import fr.ird.observe.client.datasource.actions.config.ConfigModel; import fr.ird.observe.client.datasource.actions.config.ConfigUI; import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePaneHandler; +import fr.ird.observe.client.datasource.api.data.TaskSide; import fr.ird.observe.client.datasource.editor.api.selection.actions.SelectUnselectWithOpposite; import fr.ird.observe.client.datasource.editor.api.wizard.StorageUIModel; import fr.ird.observe.client.util.init.UIInitHelper; @@ -48,8 +49,8 @@ class DataSynchroUIHandler extends AdminTabUIHandler<DataSynchroUI> implements U public void beforeInit(DataSynchroUI ui) { super.beforeInit(ui); DataSynchroModel dataSynchroModel = parentUI.getModel().getDataSynchroModel(); - ui.setContextValue(dataSynchroModel.getLeftModel(), DataSynchroModel.LEFT_MODEL); - ui.setContextValue(dataSynchroModel.getRightModel(), DataSynchroModel.RIGHT_MODEL); + ui.setContextValue(dataSynchroModel.onSameSide(TaskSide.FROM_LEFT), DataSelectionTreePaneHandler.MODEL_NAMES.onSameSide(TaskSide.FROM_LEFT)); + ui.setContextValue(dataSynchroModel.onOppositeSide(TaskSide.FROM_LEFT), DataSelectionTreePaneHandler.MODEL_NAMES.onOppositeSide(TaskSide.FROM_LEFT)); } @Override ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/actions/Apply.java ===================================== @@ -77,7 +77,7 @@ public class Apply extends DataSynchroUIActionSupport { private WizardState doApply() throws BabModelVersionException, DataSourceCreateWithNoReferentialImportException, DatabaseNotFoundException, IncompatibleDataSourceCreateConfigurationException, DatabaseConnexionNotAuthorizedException { DataSynchroModel stepModel = ui.getStepModel(); - DataSelectionTreePaneModel leftModel = stepModel.getModel(true); + DataSelectionTreePaneModel leftModel = stepModel.onSameSide(TaskSide.FROM_LEFT); String moduleName = leftModel.getSelectionDataModel().getConfig().getModuleName(); Class<? extends RootOpenableDto> dataType = "ps".equals(moduleName) ? fr.ird.observe.dto.data.ps.common.TripDto.class : fr.ird.observe.dto.data.ll.common.TripDto.class; DefaultListModel<DataTaskSupport> tasks = stepModel.getTasks(); @@ -112,7 +112,7 @@ public class Apply extends DataSynchroUIActionSupport { progressModel.setMaximum(stepCount); try (ObserveSwingDataSource leftSource = openSource(leftModel.getSource())) { - try (ObserveSwingDataSource rightSource = openSource(stepModel.getModel(false).getSource())) { + try (ObserveSwingDataSource rightSource = openSource(stepModel.onOppositeSide(TaskSide.FROM_LEFT).getSource())) { long t00 = TimeLog.getTime(); progressModel.increments(); try { ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/actions/Start.java ===================================== @@ -29,6 +29,7 @@ import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelect import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePaneHandler; import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePaneModel; import fr.ird.observe.client.datasource.api.ObserveSwingDataSource; +import fr.ird.observe.client.datasource.api.data.TaskSide; import org.nuiton.jaxx.runtime.swing.wizard.ext.WizardState; import java.awt.event.ActionEvent; @@ -57,11 +58,11 @@ public class Start extends DataSynchroUIActionSupport { ConfigModel configModel = ui.getModel().getConfigModel(); try (ObserveSwingDataSource leftSource = configModel.getLeftSourceModel().getSafeSource(true)) { - DataSelectionTreePaneModel leftModel = stepModel.getModel(true); + DataSelectionTreePaneModel leftModel = stepModel.onSameSide(TaskSide.FROM_LEFT); leftModel.setSource(leftSource); try (ObserveSwingDataSource rightSource = configModel.getRightSourceModel().getSafeSource(true)) { - DataSelectionTreePaneModel rightModel = stepModel.getModel(false); + DataSelectionTreePaneModel rightModel = stepModel.onOppositeSide(TaskSide.FROM_LEFT); rightModel.setSource(rightSource); DataSelectionTreePane leftTreePane = tabUI.getLeftTreePane(); ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/DataSelectionTreePane.jaxx ===================================== @@ -20,17 +20,9 @@ <JPanel id='topPanel' layout="{new BorderLayout()}"> <import> fr.ird.observe.client.datasource.editor.api.selection.SelectionTreePane + fr.ird.observe.client.datasource.api.data.TaskSide </import> - <script><![CDATA[ -public boolean isLeft() { - return !isRight(); -} -public boolean isRight() { - return Boolean.TRUE.equals(getOpposite()); -} -]]> - </script> - <Boolean id='opposite' initializer='getContextValue(Boolean.class)'/> + <TaskSide id="side" initializer='getContextValue(TaskSide.class)'/> <DataSelectionTreePaneModel id="model" initializer='DataSelectionTreePaneHandler.getModel(this)'/> <JToolBar id="toolbar"> <JSeparator orientation='{JSeparator.VERTICAL}'/> @@ -42,5 +34,5 @@ public boolean isRight() { <JButton id="copy" enabled="false"/> <JButton id="delete" enabled="false"/> </JToolBar> - <SelectionTreePane id='tree' constructorParams="isRight()" constraints='BorderLayout.CENTER'/> + <SelectionTreePane id='tree' constructorParams="!side.onLeft()" constraints='BorderLayout.CENTER'/> </JPanel> ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/DataSelectionTreePaneHandler.java ===================================== @@ -39,6 +39,7 @@ import fr.ird.observe.navigation.tree.selection.IdState; import fr.ird.observe.navigation.tree.selection.SelectionTree; import fr.ird.observe.navigation.tree.selection.SelectionTreeModel; import fr.ird.observe.navigation.tree.selection.SelectionTreeNode; +import io.ultreia.java4all.util.TwoSideContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.nuiton.jaxx.runtime.spi.UIHandler; @@ -64,19 +65,30 @@ public class DataSelectionTreePaneHandler implements UIHandler<DataSelectionTree private static final Logger log = LogManager.getLogger(DataSelectionTreePaneHandler.class); + public static final TwoSideContext<TaskSide, String> MODEL_NAMES = new TwoSideContext<>() { + @Override + public String left() { + return DataSynchroModel.LEFT_MODEL; + } + + @Override + public String right() { + return DataSynchroModel.RIGHT_MODEL; + } + }; + static DataSelectionTreePaneModel getModel(DataSelectionTreePane ui) { - return ui.getContextValue(DataSelectionTreePaneModel.class, ui.isLeft() ? DataSynchroModel.LEFT_MODEL : DataSynchroModel.RIGHT_MODEL); + return ui.getContextValue(DataSelectionTreePaneModel.class, MODEL_NAMES.onSameSide(ui.getSide())); } //FIXME Maybe we can place this in afterInit method? check this out! public static void init(DataSelectionTreePane ui) { - boolean isLeft = ui.isLeft(); - TaskSide taskSide = isLeft ? TaskSide.FROM_LEFT : TaskSide.FROM_RIGHT; + TaskSide taskSide = ui.getSide(); SelectionTreePane treePane = ui.getTree(); SelectionTree tree = treePane.getTree(); UIInitHelper.init(tree); - RegisterCopy.init(ui, ui.getCopy(), new RegisterCopy(taskSide)); - RegisterDelete.init(ui, ui.getDelete(), new RegisterDelete(taskSide)); + RegisterCopy.install(ui, ui.getCopy(), taskSide); + RegisterDelete.install(ui, ui.getDelete(), taskSide); ToggleIdState.install(ui, ui.getToggleAdded(), IdState.ADDED); ToggleIdState.install(ui, ui.getToggleNewer(), IdState.UPDATED); ToggleIdState.install(ui, ui.getToggleOlder(), IdState.OBSOLETE); @@ -103,7 +115,7 @@ public class DataSelectionTreePaneHandler implements UIHandler<DataSelectionTree model.setSelectionDataModel(treeModel); // When model idStates has changed, rebuild the tree (but not the flat model) model.addPropertyChangeListener(DataSelectionTreePaneModel.ID_STATES_PROPERTY_NAME, - evt-> DataSelectionTreePaneHandler.rebuildTree(parent.getStepModel(), ui, false)); + evt -> DataSelectionTreePaneHandler.rebuildTree(parent.getStepModel(), ui, false)); // When tree model has changed, rebuild accessibility to copy and delete action treeModel.addPropertyChangeListener(evt -> { @@ -154,7 +166,7 @@ public class DataSelectionTreePaneHandler implements UIHandler<DataSelectionTree public static void rebuildTree(DataSynchroModel stepModel, DataSelectionTreePane ui, boolean rebuildFlatModel) { ui.getTree().getTree().clearSelection(); - stepModel.rebuildSelectionModel(ui.isLeft(),rebuildFlatModel); + stepModel.rebuildSelectionModel(ui.getSide(), rebuildFlatModel); finalizeTree(ui); } ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/DataSelectionTreePaneModel.java ===================================== @@ -23,6 +23,8 @@ package fr.ird.observe.client.datasource.actions.synchronize.data.tree; */ import fr.ird.observe.client.datasource.api.ObserveSwingDataSource; +import fr.ird.observe.client.datasource.editor.api.selection.SelectionTreePane; +import fr.ird.observe.client.datasource.editor.api.selection.SelectionTreePaneHandler; import fr.ird.observe.navigation.tree.io.ToolkitTreeFlatModel; import fr.ird.observe.navigation.tree.selection.IdAndLastUpdateDate; import fr.ird.observe.navigation.tree.selection.IdState; @@ -44,6 +46,7 @@ import java.util.Enumeration; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -54,9 +57,17 @@ import java.util.stream.Collectors; * @since 9.2.0 */ public class DataSelectionTreePaneModel extends AbstractJavaBean { + public static final String ID_STATES_PROPERTY_NAME = "idStates"; private static final Logger log = LogManager.getLogger(DataSelectionTreePaneModel.class); private static final String SOURCE_PROPERTY_NAME = "source"; - public static final String ID_STATES_PROPERTY_NAME = "idStates"; + /** + * Which id states we use in tree model. + */ + private final EnumSet<IdState> idStates = EnumSet.allOf(IdState.class); + /** + * Id states count in tree model. + */ + private final EnumMap<IdState, Integer> idStateCount = new EnumMap<>(IdState.class); /** * Data source. */ @@ -76,14 +87,6 @@ public class DataSelectionTreePaneModel extends AbstractJavaBean { * All existing ids (computed only once at init). */ private List<IdAndLastUpdateDate> dataIds = List.of(); - /** - * Which id states we use in tree model. - */ - private final EnumSet<IdState> idStates = EnumSet.allOf(IdState.class); - /** - * Id states count in tree model. - */ - private final EnumMap<IdState, Integer> idStateCount = new EnumMap<>(IdState.class); public void dispose() { source = null; @@ -96,11 +99,19 @@ public class DataSelectionTreePaneModel extends AbstractJavaBean { return source; } + public void setSource(ObserveSwingDataSource source) { + this.source = source; + firePropertyChange(SOURCE_PROPERTY_NAME, source); + } public SelectionTreeModel getSelectionDataModel() { return selectionDataModel; } + public void setSelectionDataModel(SelectionTreeModel selectionTreeModel) { + this.selectionDataModel = selectionTreeModel; + } + public List<IdAndLastUpdateDate> getDataIds() { return dataIds; } @@ -113,15 +124,6 @@ public class DataSelectionTreePaneModel extends AbstractJavaBean { return idStateCount; } - public void setSource(ObserveSwingDataSource source) { - this.source = source; - firePropertyChange(SOURCE_PROPERTY_NAME, source); - } - - public void setSelectionDataModel(SelectionTreeModel selectionTreeModel) { - this.selectionDataModel = selectionTreeModel; - } - public void buildFirstSelectionModel() { SelectionTreeConfig config = selectionDataModel.getConfig(); SelectionTreeConfig newConfig = source.newSelectionTreeConfig(); @@ -258,4 +260,36 @@ public class DataSelectionTreePaneModel extends AbstractJavaBean { }); } } + + public void removeData(List<SelectionTreeNode> data, SelectionTreePane tree) { + SelectionTreeModel selectDataModel = getSelectionDataModel(); + int removedCount = 0; + for (SelectionTreeNode datum : data) { + String dataPath = datum.getNodePath().toString(); + log.debug("Remove {} from flat model.", dataPath); + Set<String> paths = treeFlatModel.getMapping().keySet().stream().filter(e -> e.startsWith(dataPath)).collect(Collectors.toSet()); + for (String path : paths) { + removedCount++; + log.debug("Removed {} - {} from flat model.", removedCount, path); + treeFlatModel.removeMapping(path); + } + SelectionTreeNode parent = datum.getParent(); + if (parent != null) { + String parentPath = parent.getNodePath().toString(); + long count = treeFlatModel.getMapping().keySet().stream().filter(e -> e.startsWith(parentPath)).count(); + if (count == 1) { + removedCount++; + log.debug("Removed {} - {} from flat model.", removedCount, parentPath); + treeFlatModel.removeMapping(parentPath); + } + } + } + selectDataModel.removeData(data); + if (removedCount > 0) { + log.info("Removed {} mapping(s) from flat model.", removedCount); + tree.getTree().getModel().updateDataCount(treeFlatModel); + SelectionTreePaneHandler.updateStatistics(tree); + firePropertyChange(ID_STATES_PROPERTY_NAME, idStates); + } + } } ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/actions/DataSelectionTreePaneActionSupport.java ===================================== @@ -26,7 +26,6 @@ import fr.ird.observe.client.datasource.actions.AdminTabUIHandler; import fr.ird.observe.client.datasource.actions.synchronize.data.DataSynchroModel; import fr.ird.observe.client.datasource.actions.synchronize.data.DataSynchroUI; import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePane; -import fr.ird.observe.navigation.tree.selection.SelectionTreeModel; import org.nuiton.jaxx.runtime.swing.action.JComponentActionSupport; import javax.swing.KeyStroke; @@ -37,41 +36,29 @@ import javax.swing.KeyStroke; */ public abstract class DataSelectionTreePaneActionSupport extends JComponentActionSupport<DataSelectionTreePane> { - private final KeyStroke oppositeAcceleratorKey; + private final KeyStroke rightAcceleratorKey; - DataSelectionTreePaneActionSupport(String label, String shortDescription, String actionIcon, KeyStroke acceleratorKey, KeyStroke oppositeAcceleratorKey) { + DataSelectionTreePaneActionSupport(String label, String shortDescription, String actionIcon, KeyStroke acceleratorKey, KeyStroke rightAcceleratorKey) { super(label, shortDescription, actionIcon, acceleratorKey); - this.oppositeAcceleratorKey = oppositeAcceleratorKey; + this.rightAcceleratorKey = rightAcceleratorKey; } - DataSelectionTreePaneActionSupport(String actionCommandKey, String label, String shortDescription, String actionIcon, KeyStroke acceleratorKey, KeyStroke oppositeAcceleratorKey) { + DataSelectionTreePaneActionSupport(String actionCommandKey, String label, String shortDescription, String actionIcon, KeyStroke acceleratorKey, KeyStroke rightAcceleratorKey) { super(actionCommandKey, label, shortDescription, actionIcon, acceleratorKey); - this.oppositeAcceleratorKey = oppositeAcceleratorKey; + this.rightAcceleratorKey = rightAcceleratorKey; } @Override public void init() { - if (ui.isRight()) { - setKeyStroke(oppositeAcceleratorKey); + if (!ui.getSide().onLeft()) { + setKeyStroke(rightAcceleratorKey); } super.init(); } - - protected DataSynchroModel getDataSynchroModel() { - DataSynchroUI parent = ui.getContextValue(DataSynchroUI.class, AdminTabUIHandler.ADMIN_TAB_UI); - return parent.getStepModel(); - } - protected DataSynchroUI getDataSynchroUI() { return ui.getContextValue(DataSynchroUI.class, AdminTabUIHandler.ADMIN_TAB_UI); } - - protected SelectionTreeModel getSelectionTreeModel() { - return ui.getModel().getSelectionDataModel(); - } - - protected SelectionTreeModel getOppositeSelectionTreeModel() { - DataSynchroModel dataSynchroModel = getDataSynchroModel(); - return dataSynchroModel.getModel(!ui.isLeft()).getSelectionDataModel(); + protected DataSynchroModel getDataSynchroModel() { + return getDataSynchroUI().getStepModel(); } } ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/actions/RegisterCopy.java ===================================== @@ -25,46 +25,53 @@ package fr.ird.observe.client.datasource.actions.synchronize.data.tree.actions; import fr.ird.observe.client.datasource.actions.ObserveKeyStrokesActions; import fr.ird.observe.client.datasource.actions.synchronize.data.DataSynchroModel; import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePane; +import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePaneModel; import fr.ird.observe.client.datasource.api.data.CopyDataTask; import fr.ird.observe.client.datasource.api.data.TaskSide; import fr.ird.observe.dto.ToolkitIdLabel; import fr.ird.observe.navigation.tree.selection.SelectionTreeModel; +import fr.ird.observe.navigation.tree.selection.SelectionTreeNode; +import javax.swing.JButton; import java.awt.event.ActionEvent; +import java.util.LinkedList; +import java.util.List; /** * To register a copy task. * <p> - * It will copy it {@link #taskSide} side to the other side. * * @author Tony Chemit - dev@tchemit.fr * @since 8.0 */ public class RegisterCopy extends DataSelectionTreePaneActionSupport { - /** - * Indicates the side used to copy data. - */ - private final TaskSide taskSide; + public static void install(DataSelectionTreePane ui, JButton editor, TaskSide taskSide) { + RegisterCopy.init(ui, editor, new RegisterCopy(taskSide)); + } - public RegisterCopy(TaskSide taskSide) { + private RegisterCopy(TaskSide taskSide) { super("", taskSide.getCopyTipKey(), taskSide.getCopyIconName(), ObserveKeyStrokesActions.KEY_STROKE_DATA_SYNCHRO_COPY_LEFT, ObserveKeyStrokesActions.KEY_STROKE_DATA_SYNCHRO_COPY_RIGHT); - this.taskSide = taskSide; } @Override protected void doActionPerformed(ActionEvent e, DataSelectionTreePane ui) { + TaskSide taskSide = ui.getSide(); DataSynchroModel stepModel = getDataSynchroModel(); - SelectionTreeModel treeModel = getSelectionTreeModel(); - SelectionTreeModel oppositeTreeModel = getOppositeSelectionTreeModel(); - CopyDataTask.of(taskSide, treeModel).forEach(t -> { + DataSelectionTreePaneModel thisSideModel = stepModel.onSameSide(taskSide); + DataSelectionTreePaneModel oppositeSideModel = stepModel.onOppositeSide(taskSide); + List<SelectionTreeNode> oppositeNodes = new LinkedList<>(); + SelectionTreeModel thisSideTreeModel = thisSideModel.getSelectionDataModel(); + SelectionTreeModel oppositeSideTreeModel = oppositeSideModel.getSelectionDataModel(); + CopyDataTask.of(taskSide, thisSideTreeModel).forEach(t -> { stepModel.addTask(t); + ToolkitIdLabel data = t.getData(); if (t.isDataExistOnOpposite()) { - ToolkitIdLabel data = t.getData(); - oppositeTreeModel.getDataParentNode(data).ifPresent(p -> oppositeTreeModel.removeDataFromParent(p, data)); + oppositeSideTreeModel.getDataNode(data).ifPresent(oppositeNodes::add); } }); - treeModel.removeAllSelectedData(); + thisSideModel.removeData(thisSideTreeModel.selectedDataNodes(), getDataSynchroUI().onSameSide(taskSide).getTree()); + oppositeSideModel.removeData(oppositeNodes, getDataSynchroUI().onOppositeSide(taskSide).getTree()); } } ===================================== client/datasource/actions/src/main/java/fr/ird/observe/client/datasource/actions/synchronize/data/tree/actions/RegisterDelete.java ===================================== @@ -25,37 +25,37 @@ package fr.ird.observe.client.datasource.actions.synchronize.data.tree.actions; import fr.ird.observe.client.datasource.actions.ObserveKeyStrokesActions; import fr.ird.observe.client.datasource.actions.synchronize.data.DataSynchroModel; import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePane; +import fr.ird.observe.client.datasource.actions.synchronize.data.tree.DataSelectionTreePaneModel; import fr.ird.observe.client.datasource.api.data.DeleteDataTask; import fr.ird.observe.client.datasource.api.data.TaskSide; import fr.ird.observe.navigation.tree.selection.SelectionTreeModel; +import javax.swing.JButton; import java.awt.event.ActionEvent; /** * To register a delete task. * <p> - * It will delete it from the {@link #taskSide} side. * * @author Tony Chemit - dev@tchemit.fr * @since 8.0 */ public class RegisterDelete extends DataSelectionTreePaneActionSupport { + public static void install(DataSelectionTreePane ui, JButton editor, TaskSide taskSide) { + RegisterDelete.init(ui, editor, new RegisterDelete(taskSide)); + } - /** - * Indicates the side used to delete data. - */ - private final TaskSide taskSide; - - public RegisterDelete(TaskSide taskSide) { + private RegisterDelete(TaskSide taskSide) { super("", taskSide.getDeleteTipKey(), taskSide.getDeleteIconName(), ObserveKeyStrokesActions.KEY_STROKE_DATA_SYNCHRO_DELETE_LEFT, ObserveKeyStrokesActions.KEY_STROKE_DATA_SYNCHRO_DELETE_RIGHT); - this.taskSide = taskSide; } @Override protected void doActionPerformed(ActionEvent e, DataSelectionTreePane ui) { + TaskSide taskSide = ui.getSide(); DataSynchroModel stepModel = getDataSynchroModel(); - SelectionTreeModel selectionDataModel = getSelectionTreeModel(); + DataSelectionTreePaneModel model = stepModel.onSameSide(taskSide); + SelectionTreeModel selectionDataModel = model.getSelectionDataModel(); DeleteDataTask.of(taskSide, selectionDataModel).forEach(stepModel::addTask); - selectionDataModel.removeAllSelectedData(); + model.removeData(selectionDataModel.selectedDataNodes(), getDataSynchroUI().onOppositeSide(taskSide).getTree()); } } ===================================== core/persistence/report/src/main/resources/META-INF/report/default/ps/psObservationActivitiesByZone.report ===================================== @@ -42,7 +42,8 @@ From fr.ird.observe.entities.data.ps.common.TripImpl m \ Join m.routeObs r \ Join r.activity a With a.currentFpaZone.id = :zoneFpaId \ Join a.floatingObject dcp \ -Where m.id In :tripId +Where m.id In :tripId \ +Group By a.currentFpaZone.code, a.currentFpaZone.@i18nColumnName@ request.1.repeat.name=zoneFpaId request.1.repeat.layout=column request.1.comment=visite + peche / visite - peche / Deploiement + peche / Deploiement - peche / Modifie + peche / Modifie - peche / Retire + peche / Retire - peche / Abandonne + peche / Abandonne - peche / Coule + peche / Coule - peche / Remplace + peche / Remplace - peche / Autre ou ancien peche + peche / Autre ou ancien peche - peche / Nombre de tortues ===================================== pom.xml ===================================== @@ -23,7 +23,7 @@ <parent> <groupId>io.ultreia.maven</groupId> <artifactId>pom</artifactId> - <version>2024.13</version> + <version>2024.16</version> </parent> <groupId>fr.ird.observe</groupId> <artifactId>ird-observe</artifactId> ===================================== server/core/src/main/java/fr/ird/observe/server/injector/JsonAwareDtoInjector.java ===================================== @@ -78,6 +78,6 @@ public class JsonAwareDtoInjector extends JsonInjector<JsonAware> { if (ToolkitRequestFilter.class.isAssignableFrom(type)) { return gson.fromJson("{}", generic); } - return super.buildNullValue(call, name, type, generic); + return null; } } ===================================== toolkit/api/src/main/java/fr/ird/observe/navigation/tree/ToolkitTreeModelSupport.java ===================================== @@ -114,6 +114,10 @@ public abstract class ToolkitTreeModelSupport<R extends ToolkitTreeNode> extends populate(flatModel, rootConsumer); } + public final void updateDataCount(ToolkitTreeFlatModel flatModel) { + dataCount = flatModel.getDataCount(); + } + public final ToolkitTreeFlatModelRootRequest getRequest() { return request; } ===================================== toolkit/api/src/main/java/fr/ird/observe/navigation/tree/io/ToolkitTreeFlatModel.java ===================================== @@ -24,6 +24,7 @@ package fr.ird.observe.navigation.tree.io; import fr.ird.observe.dto.ObserveDto; import fr.ird.observe.navigation.tree.ToolkitTreeNode; +import fr.ird.observe.navigation.tree.ToolkitTreeNodeBean; import fr.ird.observe.navigation.tree.ToolkitTreeNodeBeanState; import fr.ird.observe.navigation.tree.states.BooleanState; @@ -45,7 +46,8 @@ public class ToolkitTreeFlatModel implements ObserveDto { private final String path; private final Date lastUpdateDate; private final Map<String, ToolkitTreeNodeStates> mapping; - private final long dataCount; + private long dataCount; + public static ToolkitTreeFlatModel of(ToolkitTreeNode node, Date now, long dataCount) { return ToolkitTreeFlatModelWriter.of(node.getNodePath().toString(), now, node, dataCount); } @@ -85,4 +87,11 @@ public class ToolkitTreeFlatModel implements ObserveDto { mapping.entrySet().stream().filter(e -> pathFilter.test(e.getKey())).forEach(e -> state.setValue(e.getValue(), value)); } + public void removeMapping(String path) { + ToolkitTreeNodeStates removed = mapping.remove(path); + Integer count = removed.getState(ToolkitTreeNodeBean.STATE_COUNT.name()); + if (count == null) { + dataCount--; + } + } } ===================================== toolkit/api/src/main/java/fr/ird/observe/navigation/tree/selection/SelectionTreeModelSupport.java ===================================== @@ -38,7 +38,6 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -139,40 +138,33 @@ public class SelectionTreeModelSupport<R extends SelectionTreeNode> extends Tool return builder; } - public Optional<SelectionTreeNode> getDataParentNode(ToolkitIdLabel data) { + public Optional<SelectionTreeNode> getDataNode(ToolkitIdLabel data) { Enumeration<?> children = getRoot().children(); while (children.hasMoreElements()) { SelectionTreeNode topNode = (SelectionTreeNode) children.nextElement(); SelectionTreeNode tripNode = topNode.findPath(data.getId()); - if (tripNode != null) { - return Optional.of(topNode); + return Optional.of(tripNode); } } return Optional.empty(); } - public void removeDataFromParent(SelectionTreeNode topNode, ToolkitIdLabel data) { - SelectionTreeNode tripNode = topNode.findPath(data.getId()); - Objects.requireNonNull(tripNode, "Could not find program node with id: " + data); - removeNodeFromParent(tripNode); - if (topNode.isLeaf()) { - removeNodeFromParent(topNode); - } - } - - public void removeAllSelectedData() { - for (SelectionTreeNode node : selectedDataNodes()) { + public void removeData(List<SelectionTreeNode> data) { + for (SelectionTreeNode node : data) { SelectionTreeNode parent = node.getParent(); if (parent.getParent() == null) { // This means that the parent was already removed continue; } - if (parent.isSelected()) { + if (parent.isSelected() || data.contains(parent)) { parent.removeFromParent(); } else { node.removeFromParent(); } + if (parent.isLeaf()) { + parent.removeFromParent(); + } } recomputeSelectedCount(); } ===================================== toolkit/plugin/src/main/java/fr/ird/observe/toolkit/maven/plugin/service/ServiceGenerateLocalRunner.java ===================================== @@ -25,6 +25,7 @@ package fr.ird.observe.toolkit.maven.plugin.service; import fr.ird.observe.datasource.security.Permission; import fr.ird.observe.services.service.ObserveService; import fr.ird.observe.toolkit.maven.plugin.MojoRunnable; +import io.ultreia.java4all.http.spi.Nullable; import io.ultreia.java4all.http.spi.model.ImportManager; import io.ultreia.java4all.http.spi.model.MethodDescription; import io.ultreia.java4all.http.spi.model.ServiceMapping; @@ -61,35 +62,35 @@ public class ServiceGenerateLocalRunner extends MojoRunnable { " private static final TimeLog TIME_LOG = new TimeLog(%5$s.class, 500, 1000);\n\n" + "%6$s\n" + "}"; - public static final String CALL_BODY_NO_PERSISTENCE = "" + + public static final String CALL_BODY_NO_PERSISTENCE = "%1$s" + " long t0 = TimeLog.getTime();\n" + - " String methodName = %2$s;\n" + + " String methodName = %3$s;\n" + " try {\n" + - " %1$s" + + " %2$s" + " } catch (Exception e) {\n" + " recordError(e, methodName);\n" + " throw e;\n" + " } finally {\n" + " TIME_LOG.log(t0, String.format(\"invoke method %%s\", methodName));\n" + " }"; - public static final String CALL_BODY_NO_TRANSACTION = "" + + public static final String CALL_BODY_NO_TRANSACTION = "%1$s" + " long t0 = TimeLog.getTime();\n" + - " String methodName = %2$s;\n" + + " String methodName = %3$s;\n" + " initPersistence(methodName);\n" + " try {\n" + - " %1$s" + + " %2$s" + " } catch (Exception e) {\n" + " recordError(e, methodName);\n" + " throw e;\n" + " } finally {\n" + " TIME_LOG.log(t0, String.format(\"invoke method %%s\", methodName));\n" + " }"; - public static final String CALL_BODY_WITH_TRANSACTION = "" + + public static final String CALL_BODY_WITH_TRANSACTION = "%1$s" + " long t0 = TimeLog.getTime();\n" + - " String methodName = %2$s;\n" + - " boolean newTransaction = %3$s(methodName, Permission.%4$s);\n" + + " String methodName = %3$s;\n" + + " boolean newTransaction = %4$s(methodName, Permission.%5$s);\n" + " try {\n" + - " %1$s" + + " %2$s" + " } catch (Exception e) {\n" + " recordError(e, methodName);\n" + " throw e;\n" + @@ -212,11 +213,20 @@ public class ServiceGenerateLocalRunner extends MojoRunnable { } String exceptions = exceptionsBuilder.length() == 0 ? "" : "throws " + exceptionsBuilder.substring(2); + StringBuilder checkParametersBuilder = new StringBuilder(); StringBuilder parametersBuilder = new StringBuilder(); StringBuilder parametersDeclarationBuilder = new StringBuilder(); int index = 0; String methodNameClassifier = null; for (String parameterName : methodDescription.getParameterNames()) { + parametersDeclarationBuilder.append(", "); + if (methodDescription.isNullableParameterName(parameterName)) { + importManager.addImport(Nullable.class); + parametersDeclarationBuilder.append("@").append(Nullable.class.getSimpleName()).append(" "); + } else if (!methodDescription.getMethod().getParameterTypes()[index].isPrimitive()) { + importManager.addImport(Objects.class); + checkParametersBuilder.append(String.format(" Objects.requireNonNull(%1$s, \"Parameter '%1$s' (in method %2$s#%3$s) can not be null.\");\n", parameterName, className, methodName)); + } String parameterType = methodDescription.getParameterTypes().get(index++); if (!parameterType.endsWith("[]")) { parameterType = importAndSimplify(importManager, parameterType); @@ -233,11 +243,12 @@ public class ServiceGenerateLocalRunner extends MojoRunnable { // } } parametersBuilder.append(", ").append(parameterName); - parametersDeclarationBuilder.append(", ").append(parameterType).append(" ").append(parameterName); + parametersDeclarationBuilder.append(parameterType).append(" ").append(parameterName); } String parameters = index == 0 ? "" : parametersBuilder.substring(2); String parametersDeclaration = index == 0 ? "" : parametersDeclarationBuilder.substring(2); + String checkParametersDeclaration = checkParametersBuilder.toString(); String returnInvocation = methodDescription.getReturnInvocation(); @SuppressWarnings("SpellCheckingInspection") String superCall = String.format("%ssuper.%s(%s);\n", returnInvocation, methodName, parameters); @@ -246,7 +257,7 @@ public class ServiceGenerateLocalRunner extends MojoRunnable { if (noTransaction) { getLog().debug(String.format("No transaction required on %s for method: %s", className, methodName)); // no transaction in this call - bodyContent = String.format(CALL_BODY_NO_PERSISTENCE, superCall, generatedMethodName); + bodyContent = String.format(CALL_BODY_NO_PERSISTENCE, checkParametersDeclaration, superCall, generatedMethodName); } else { boolean noCredential = methodeCredentials == null; if (noCredential) { @@ -254,13 +265,13 @@ public class ServiceGenerateLocalRunner extends MojoRunnable { } if (noCredential) { // init persistence, but do not open transaction - bodyContent = String.format(CALL_BODY_NO_TRANSACTION, superCall, generatedMethodName); + bodyContent = String.format(CALL_BODY_NO_TRANSACTION, checkParametersDeclaration, superCall, generatedMethodName); } else { // init persistence and create a new transaction if required String initTransactionMethodName = write ? "initWriteTransaction" : "initReadTransaction"; importManager.importAndSimplify(Permission.class.getName()); - bodyContent = String.format(CALL_BODY_WITH_TRANSACTION, superCall, generatedMethodName, initTransactionMethodName, methodeCredentials); + bodyContent = String.format(CALL_BODY_WITH_TRANSACTION, checkParametersDeclaration, superCall, generatedMethodName, initTransactionMethodName, methodeCredentials); } } return String.format(METHOD, realReturnType, methodName, parametersDeclaration, exceptions, bodyContent); ===================================== toolkit/plugin/src/main/java/fr/ird/observe/toolkit/maven/plugin/service/ServiceLocalMethodDescriptionImpl.java ===================================== @@ -24,6 +24,7 @@ package fr.ird.observe.toolkit.maven.plugin.service; import fr.ird.observe.datasource.security.Permission; import fr.ird.observe.services.service.MethodCredential; +import io.ultreia.java4all.http.spi.Nullable; import io.ultreia.java4all.http.spi.SpiHelper; import io.ultreia.java4all.http.spi.model.ImportManager; import io.ultreia.java4all.http.spi.model.MethodDescription; @@ -50,6 +51,7 @@ public class ServiceLocalMethodDescriptionImpl implements MethodDescription { private final List<Class<?>> exceptions; private final List<String> parameterNames; private final List<String> parameterTypes; + private final List<String> nullableParameterNames; private final Permission methodeCredentials; private final boolean write; private final boolean noTransaction; @@ -64,15 +66,17 @@ public class ServiceLocalMethodDescriptionImpl implements MethodDescription { parameterNames = new LinkedList<>(); parameterTypes = new LinkedList<>(); + nullableParameterNames = new LinkedList<>(); for (java.lang.reflect.Parameter parameter : method.getParameters()) { String name = parameter.getName(); parameterNames.add(name); parameterTypes.add(importManager.importParameterType(parameter, currentMapping)); + if (parameter.isAnnotationPresent(Nullable.class)) { + nullableParameterNames.add(name); + } } - this.returnInvocation = returnType.contains("void") ? "" : "return "; this.exceptions = MethodDescription.getExceptions(method, importManager); - this.methodeCredentials = Optional.ofNullable(method.getAnnotation(MethodCredential.class)).map(MethodCredential::value).orElse(null); this.write = SpiHelper.write(method); this.noTransaction = !SpiHelper.getRequestAnnotation(method).isAddAuthenticationToken(); @@ -127,6 +131,10 @@ public class ServiceLocalMethodDescriptionImpl implements MethodDescription { return parameterTypes; } + public boolean isNullableParameterName(String parameterName) { + return nullableParameterNames.contains(parameterName); + } + public boolean isNoTransaction() { return noTransaction; } ===================================== toolkit/server/src/main/java/org/debux/webmotion/server/handler/injector/CollectionInjector.java ===================================== @@ -54,7 +54,7 @@ public class CollectionInjector extends JsonInjector<Collection> { } String[] values = (String[]) parameterTree.getValue(); String value = values[0]; - if (value.length() > 0) { + if (!value.isEmpty()) { if (onString) { if (!value.contains("[")) { // use values ===================================== toolkit/server/src/main/java/org/debux/webmotion/server/handler/injector/JsonInjector.java ===================================== @@ -80,7 +80,7 @@ public abstract class JsonInjector<O> implements ExecutorParametersInjectorHandl } protected O buildNullValue(Call call, String name, Class<?> type, Type generic) { - throw new NullPointerException(String.format("By default do not accept null parameterTrees for parameter: %s with type: %s", name, type.getName())); + throw new NullPointerException(String.format("By default do not accept null parameterTree for parameter: %s with type: %s", name, type.getName())); } } View it on GitLab: https://gitlab.com/ultreiaio/ird-observe/-/compare/84da9a9a59508179532215626... -- View it on GitLab: https://gitlab.com/ultreiaio/ird-observe/-/compare/84da9a9a59508179532215626... You're receiving this email because of your account on gitlab.com.
participants (1)
-
Tony CHEMIT (@tchemit)