From 4a97e2ec1c7c6f0b1ad4ed63b84ceebe78895cbb Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Fri, 4 Sep 2026 16:01:56 +0100 Subject: [PATCH 1/9] Remove explicit GC before process launch Introduced in 530a468a as an experiment to reduce fork memory. Measured at 50s of GC pauses in a 98s test run vs 0.14s with automatic GC only. Automatic GC still runs; removing this does not disable collection. --- SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index 13c0c9a7..49f767e0 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -190,15 +190,7 @@ public static ProcessOutput runProcess(ProcessBuilder builder, Process process; try { - // Experiment: Calling Garbage Collector before starting process in order to - // reduce memory required to fork VM - // http://www.bryanmarty.com/2012/01/14/forking-jvm/ - long totalMemBefore = Runtime.getRuntime().totalMemory(); - System.gc(); - long totalMemAfter = Runtime.getRuntime().totalMemory(); - SpecsLogs.msgLib("Preparing to run process, memory before -> after GC: " - + SpecsStrings.parseSize(totalMemBefore) + " -> " + SpecsStrings.parseSize(totalMemAfter)); - process = builder.start(); + process = builder.start(); } catch (IOException e) { throw new RuntimeException("Could not start process", e); From b54f698740a84c6a0df677442ccb4623e48ac78a Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Fri, 4 Sep 2026 17:07:56 +0100 Subject: [PATCH 2/9] Launch processes with direct argv on Linux Drop the bash -l -c wrapper added in 44d1d938. The wrapper added ~50ms per launch for shell parsing, broke argument fidelity (naive space escaping), and re-sourced login profiles over the inherited environment instead of carrying it faithfully. ProcessBuilder children already inherit the JVM's full environment, user, and working directory. All in-tree callers pass plain argv; shell semantics for string commands are no longer supported on Linux. --- SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index 49f767e0..f691a87e 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -246,19 +246,6 @@ private static void processCommand(ProcessBuilder builder) { newCommand.add("/c"); newCommand.addAll(builder.command()); - builder.command(newCommand); - } else if (isLinux()) { - // Update command - List newCommand = new ArrayList<>(4); - newCommand.add("bash"); - // Same user - newCommand.add("-l"); - // Command - newCommand.add("-c"); - newCommand.add(builder.command().stream() - .map(arg -> arg.replace(" ", "\\ ")) - .collect(Collectors.joining(" "))); - builder.command(newCommand); } From 44ab19744fd4902f02ed85e1079e6214ee00dfef Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sat, 5 Sep 2026 02:05:31 +0100 Subject: [PATCH 3/9] Handle failed process launches at the call sites With direct argv, a missing or non-executable command now throws from runProcess(), as its javadoc always documented ('If there is any problem with the process, throws an exception') - the removed shell wrapper used to mask launch failures as exit code 127. Handle the launch failure where graceful failure is the intent: - SpecsGraphviz: dot being absent is a normal condition, so checkDot() returns false and renderDot() logs instead of crashing. - ProcessExecution: the jobs framework is built on return codes with no exception handling, so run() reports the failed launch as exit code 127 instead of propagating. This also restores SpecsSystem.isCommandAvailable()'s intended behavior: it detects missing commands by catching the launch exception, which the wrapper's masking previously defeated. --- .../pt/up/fe/specs/util/SpecsGraphviz.java | 25 +++++++++++++++---- .../src/pt/up/fe/specs/util/SpecsSystem.java | 20 +++++++++++++++ .../util/jobs/execution/ProcessExecution.java | 16 +++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java index d0d6b3da..120242a6 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsGraphviz.java @@ -29,8 +29,13 @@ public class SpecsGraphviz { private static final Lazy IS_DOT_AVAILABLE = Lazy.newInstance(SpecsGraphviz::checkDot); private static boolean checkDot() { - var result = SpecsSystem.runProcess(Arrays.asList("dot", "-?"), false, false); - return result.getReturnValue() == 0; + try { + var result = SpecsSystem.runProcess(Arrays.asList("dot", "-?"), false, false); + return result.getReturnValue() == 0; + } catch (RuntimeException e) { + // The launch itself failed, e.g. Graphviz is not installed + return false; + } } public static boolean isDotAvailable() { @@ -47,9 +52,19 @@ public static void renderDot(File dotFile, DotRenderFormat format, File outputFi var command = Arrays.asList("dot", format.getFlag(), dotFile.getAbsolutePath(), "-o", outputFile.getAbsolutePath()); - var result = SpecsSystem.runProcess(command, false, false); - if (result.getReturnValue() == 0) { - SpecsLogs.debug(() -> "Rendered dot file '" + dotFile.getAbsolutePath() + "' as " + format); + try { + var result = SpecsSystem.runProcess(command, false, false); + if (result.getReturnValue() == 0) { + SpecsLogs.debug(() -> "Rendered dot file '" + dotFile.getAbsolutePath() + "' as " + format); + } + } catch (RuntimeException e) { + // Only a missing Graphviz installation is skipped; any other + // failure is rethrown + if (!SpecsSystem.isLaunchFailure(e)) { + throw e; + } + + SpecsLogs.msgInfo("Graphviz not available, could not render dot file '" + dotFile.getAbsolutePath() + "'"); } } diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index f691a87e..cd4400b3 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -578,6 +578,26 @@ public static boolean isCommandAvailable(List command, File workingdir) } + /** + * Checks if the throwable was caused by a failure to launch the process + * itself (e.g., command not found or not executable), as opposed to a + * failure after a successful launch. + * + * @param throwable + * @return + */ + public static boolean isLaunchFailure(Throwable throwable) { + for (Throwable currentThrowable = throwable; currentThrowable != null; currentThrowable = currentThrowable + .getCause()) { + if (currentThrowable instanceof IOException) { + return true; + } + } + + return false; + } + + /** * Adds a path to the java.library.path property, and flushes the path cache so * that subsequent System.load calls diff --git a/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java b/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java index 8c926594..5ae0ddfa 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/jobs/execution/ProcessExecution.java @@ -16,6 +16,7 @@ import java.io.File; import java.util.List; +import pt.up.fe.specs.util.SpecsLogs; import pt.up.fe.specs.util.SpecsSystem; /** @@ -45,7 +46,20 @@ public ProcessExecution(List commandArgs, String workingFoldername) { */ @Override public int run() { - return SpecsSystem.run(this.commandArgs, new File(this.workingFoldername)); + try { + return SpecsSystem.run(this.commandArgs, new File(this.workingFoldername)); + } catch (RuntimeException e) { + // Only a launch failure due to a missing command maps to a return + // code (the shell convention of 127, 'command not found'). Any + // other failure is not representable as an exit code and is + // rethrown, as the process may even have been running. + if (!SpecsSystem.isLaunchFailure(e)) { + throw e; + } + + SpecsLogs.msgInfo("Could not run command '" + getCommandString() + "': " + e.getMessage()); + return 127; + } } @Override From 8f9e7c180296475e99477b0c2e16240e9d5fb5b3 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Mon, 7 Sep 2026 15:11:48 +0100 Subject: [PATCH 4/9] refactor(jOptions): remove GuiHelper coupling (converter, notimplementedyet panels, legacy SetupData path) --- jOptions/build.gradle | 1 - .../jOptions/Utils/GuiHelperConverter.java | 196 --------- .../IntegratedSetupPanel.java | 147 ------- .../notimplementedyet/ListOfSetupsPanel.java | 400 ------------------ .../MultipleChoiceListPanel.java | 244 ----------- .../MultipleChoicePanel.java | 134 ------ .../MultipleChoiceSetup.java | 289 ------------- .../jOptions/persistence/XmlPersistence.java | 43 -- 8 files changed, 1454 deletions(-) delete mode 100644 jOptions/src/org/suikasoft/jOptions/Utils/GuiHelperConverter.java delete mode 100644 jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/IntegratedSetupPanel.java delete mode 100644 jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/ListOfSetupsPanel.java delete mode 100644 jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceListPanel.java delete mode 100644 jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoicePanel.java delete mode 100644 jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceSetup.java diff --git a/jOptions/build.gradle b/jOptions/build.gradle index 37d36fd1..4949248c 100644 --- a/jOptions/build.gradle +++ b/jOptions/build.gradle @@ -19,7 +19,6 @@ repositories { dependencies { implementation ':SpecsUtils' implementation ':XStreamPlus' - implementation ':GuiHelper' implementation 'com.google.guava:guava:33.4.0-jre' implementation 'com.google.code.gson:gson:2.12.1' diff --git a/jOptions/src/org/suikasoft/jOptions/Utils/GuiHelperConverter.java b/jOptions/src/org/suikasoft/jOptions/Utils/GuiHelperConverter.java deleted file mode 100644 index 1f3dbb47..00000000 --- a/jOptions/src/org/suikasoft/jOptions/Utils/GuiHelperConverter.java +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Copyright 2023 SPeCS. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package org.suikasoft.jOptions.Utils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Supplier; - -import org.suikasoft.jOptions.Datakey.DataKey; -import org.suikasoft.jOptions.Datakey.KeyFactory; -import org.suikasoft.jOptions.gui.panels.option.SetupListPanel; -import org.suikasoft.jOptions.storedefinition.StoreDefinition; -import org.suikasoft.jOptions.values.SetupList; - -import pt.up.fe.specs.guihelper.Base.SetupFieldEnum; -import pt.up.fe.specs.guihelper.BaseTypes.ListOfSetups; -import pt.up.fe.specs.guihelper.BaseTypes.SetupData; -import pt.up.fe.specs.guihelper.SetupFieldOptions.DefaultValue; -import pt.up.fe.specs.util.exceptions.NotImplementedException; - -/** - * Utility class for converting GUI helper objects in jOptions. - * Converts enums that implement {@link SetupFieldEnum} to StoreDefinition. - */ -public class GuiHelperConverter { - - /** - * Converts a list of setup classes to a list of StoreDefinitions. - * - * @param setups the setup classes to convert - * @return a list of StoreDefinitions - */ - public static & SetupFieldEnum> List toStoreDefinition( - @SuppressWarnings("unchecked") Class... setups) { - return toStoreDefinition(Arrays.asList(setups)); - } - - /** - * Converts a list of setup classes to a list of StoreDefinitions. - * - * @param setups the setup classes to convert - * @return a list of StoreDefinitions - */ - public static & SetupFieldEnum> List toStoreDefinition(List> setups) { - var converter = new GuiHelperConverter(); - - var definitions = new ArrayList(); - - for (var setup : setups) { - definitions.add(converter.convert(setup)); - } - - return definitions; - } - - /** - * Converts a single setup class to a StoreDefinition. - * - * @param setup the setup class to convert - * @return the StoreDefinition - */ - public & SetupFieldEnum> StoreDefinition convert(Class setup) { - var name = setup.getSimpleName(); - var keys = getDataKeys(setup.getEnumConstants()); - - return StoreDefinition.newInstance(name, keys); - } - - /** - * Converts an array of setup keys to a list of DataKeys. - * - * @param setupKeys the setup keys to convert - * @return a list of DataKeys - */ - public & SetupFieldEnum> List> getDataKeys( - @SuppressWarnings("unchecked") T... setupKeys) { - var keys = new ArrayList>(); - - for (var setupKey : setupKeys) { - keys.add(getDataKey(setupKey)); - } - return keys; - } - - /** - * Converts a single setup key to a DataKey. - * - * @param setupKey the setup key to convert - * @return the DataKey - */ - public & SetupFieldEnum> DataKey getDataKey(T setupKey) { - var key = getBaseDataKey(setupKey); - - // Set default value (must be immutable) - if (setupKey instanceof DefaultValue defaultValueProvider) { - var defaultValue = defaultValueProvider.getDefaultValue(); - - if (defaultValue != null) { - Supplier defaultSupplier = defaultValue::getRawValue; - key.setDefaultRaw(defaultSupplier); - } - } - - return key; - } - - /** - * Gets the base DataKey for a setup key. - * - * @param setupKey the setup key - * @return the base DataKey - */ - private & SetupFieldEnum> DataKey getBaseDataKey(T setupKey) { - return switch (setupKey.getType()) { - case string -> KeyFactory.string(setupKey.name()); - default -> throw new NotImplementedException(setupKey.getType()); - }; - - } - - /** - * Converts a SetupList to a ListOfSetups. - * - * @param setupList the SetupList to convert - * @param tasksList the list of task classes - * @return the ListOfSetups - */ - public static & SetupFieldEnum> ListOfSetups toListOfSetups(SetupList setupList, - List> tasksList) { - - Map> tasksKeys = new HashMap<>(); - for (var taskList : tasksList) { - var setupName = taskList.getSimpleName(); - - var taskKeys = getSetupFields(taskList); - tasksKeys.put(setupName, taskKeys); - } - - var listOfSetups = new ArrayList(); - for (var dataStore : setupList.getDataStores()) { - var setupName = SetupListPanel.toOriginalEnum(dataStore.getName()); - - var setupDataMapping = tasksKeys.get(setupName); - Objects.requireNonNull(setupDataMapping, - () -> "Could not find setup with name '" + setupName + "', available: " + tasksKeys.keySet()); - - var oldSetupName = setupDataMapping.values().stream().findFirst() - .orElseThrow(() -> new RuntimeException("Expected to find at least one key in the setup")) - .getSetupName(); - var setupData = new SetupData(oldSetupName); - listOfSetups.add(setupData); - - for (var key : dataStore.getKeysWithValues()) { - var setupField = setupDataMapping.get(key); - Objects.requireNonNull(setupField, - () -> "Could not find key with name '" + key + "', available: " + setupDataMapping.keySet()); - setupData.put(setupField, dataStore.get(key)); - } - } - - return new ListOfSetups(listOfSetups); - } - - /** - * Gets the setup fields for a task class. - * - * @param taskList the task class - * @return a map of setup field names to SetupFieldEnum objects - */ - private static & SetupFieldEnum> Map getSetupFields(Class taskList) { - - var taskKeys = new HashMap(); - - for (var key : taskList.getEnumConstants()) { - var keyName = key.name(); - taskKeys.put(keyName, key); - } - - return taskKeys; - } -} diff --git a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/IntegratedSetupPanel.java b/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/IntegratedSetupPanel.java deleted file mode 100644 index c579dfac..00000000 --- a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/IntegratedSetupPanel.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2010 SPeCS Research Group. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package org.suikasoft.jOptions.gui.panels.option.notimplementedyet; - -import java.awt.LayoutManager; -import java.io.Serial; -import java.util.Collection; - -import javax.swing.BoxLayout; -import javax.swing.JLabel; - -import pt.up.fe.specs.guihelper.FieldType; -import pt.up.fe.specs.guihelper.Base.SetupDefinition; -import pt.up.fe.specs.guihelper.BaseTypes.FieldValue; -import pt.up.fe.specs.guihelper.BaseTypes.SetupData; -import pt.up.fe.specs.guihelper.SetupFieldOptions.SingleSetup; -import pt.up.fe.specs.guihelper.gui.FieldPanel; -import pt.up.fe.specs.guihelper.gui.BasePanels.BaseSetupPanel; -import pt.up.fe.specs.util.SpecsLogs; - -/** - * Panel for integrating setup options into a single panel. - * - *

- * This panel displays setup options for a SingleSetup instance. - */ -public class IntegratedSetupPanel extends FieldPanel { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * INSTANCE VARIABLES - */ - private BaseSetupPanel setupOptionsPanel; - - /** - * Constructs an IntegratedSetupPanel for the given SingleSetup. - * - * @param setup the SingleSetup instance - */ - public IntegratedSetupPanel(SingleSetup setup) { - // Initialize objects - SetupDefinition setupDefinition = setup.getSetupOptions(); - if (setupDefinition == null) { - SpecsLogs.warn("null SetupDefinition inside '" + setup.getClass() + "'"); - } else { - initChoices(setupDefinition); - } - - LayoutManager layout = new BoxLayout(this, BoxLayout.Y_AXIS); - setLayout(layout); - - add(setupOptionsPanel); - } - - /** - * Initializes the setup options panel with the given SetupDefinition. - * - * @param setupDefinition the SetupDefinition - */ - private void initChoices(SetupDefinition setupDefinition) { - BaseSetupPanel newPanel = new BaseSetupPanel(setupDefinition); - - String labelName = setupDefinition.getSetupName(); - JLabel label = new JLabel("(" + labelName + ")"); - - newPanel.add(label, 0); - newPanel.add(new javax.swing.JSeparator(), 0); - newPanel.add(new javax.swing.JSeparator()); - this.setupOptionsPanel = newPanel; - } - - /** - * Returns the FieldType for this panel. - * - * @return the FieldType - */ - @Override - public FieldType getType() { - return FieldType.setup; - } - - /** - * Loads data from the raw Object in the FieldValue. - * - * @param value the value to load - */ - @Override - public void updatePanel(Object value) { - SetupData newSetup = (SetupData) value; - loadSetup(newSetup); - } - - /** - * Loads the given SetupData into the setup options panel. - * - * @param newSetup the SetupData to load - */ - private void loadSetup(SetupData newSetup) { - // Load values in the file - setupOptionsPanel.loadValues(newSetup); - } - - /** - * Returns the current option as a FieldValue. - * - * @return the FieldValue - */ - @Override - public FieldValue getOption() { - SetupData updatedValues = setupOptionsPanel.getMapWithValues(); - return FieldValue.create(updatedValues, getType()); - } - - /** - * Returns the label for this panel. - * - * @return the JLabel, or null if no label is set - */ - @Override - public JLabel getLabel() { - return null; - } - - /** - * Returns the collection of FieldPanels contained in this panel. - * - * @return the collection of FieldPanels - */ - @Override - public Collection getPanels() { - return setupOptionsPanel.getPanels().values(); - } - -} diff --git a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/ListOfSetupsPanel.java b/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/ListOfSetupsPanel.java deleted file mode 100644 index 0f87ac95..00000000 --- a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/ListOfSetupsPanel.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Copyright 2010 SPeCS Research Group. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package org.suikasoft.jOptions.gui.panels.option.notimplementedyet; - -import java.awt.FlowLayout; -import java.awt.LayoutManager; -import java.awt.event.ActionEvent; -import java.io.Serial; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import pt.up.fe.specs.guihelper.FieldType; -import pt.up.fe.specs.guihelper.Base.ListOfSetupDefinitions; -import pt.up.fe.specs.guihelper.Base.SetupDefinition; -import pt.up.fe.specs.guihelper.Base.SetupFieldEnum; -import pt.up.fe.specs.guihelper.BaseTypes.FieldValue; -import pt.up.fe.specs.guihelper.BaseTypes.ListOfSetups; -import pt.up.fe.specs.guihelper.BaseTypes.SetupData; -import pt.up.fe.specs.guihelper.SetupFieldOptions.MultipleSetup; -import pt.up.fe.specs.guihelper.gui.FieldPanel; -import pt.up.fe.specs.guihelper.gui.BasePanels.BaseSetupPanel; -import pt.up.fe.specs.util.SpecsCollections; -import pt.up.fe.specs.util.SpecsLogs; - -/** - * Panel for editing and managing a list of setup panels. - * - *

- * This panel provides controls for adding, removing, and selecting setup - * elements for a MultipleSetup instance. - * - * @author Joao Bispo - */ -public class ListOfSetupsPanel extends FieldPanel { - - @Serial - private static final long serialVersionUID = 1L; - - private JPanel currentOptionsPanel; - private JPanel choicePanel; - - private JLabel label; - private JComboBox elementsBox; - private JComboBox choicesBox; - private JButton removeButton; - private JButton addButton; - - private List choicesBoxShadow; - private ListOfSetupDefinitions setups; - - private List elementsBoxShadow; - private List elementsFiles; - private List elementsOptionPanels; - - // Properties - private static final String ENUM_NAME_SEPARATOR = "-"; - - /** - * Constructs a ListOfSetupsPanel for the given enum option, label, and - * MultipleSetup. - * - * @param enumOption the SetupFieldEnum - * @param labelName the label for the panel - * @param setup the MultipleSetup instance - */ - public ListOfSetupsPanel(SetupFieldEnum enumOption, String labelName, MultipleSetup setup) { - label = new JLabel(labelName + ":"); - removeButton = new JButton("X"); - addButton = new JButton("Add"); - - initChoices(setup); - initElements(); - - // Add actions - addButton.addActionListener(this::addButtonActionPerformed); - - removeButton.addActionListener(this::removeButtonActionPerformed); - - elementsBox.addActionListener(this::elementComboBoxActionPerformed); - - // Build choice panel - choicePanel = buildChoicePanel(); - - currentOptionsPanel = null; - - LayoutManager layout = new BoxLayout(this, BoxLayout.Y_AXIS); - setLayout(layout); - add(choicePanel); - - } - - /** - * Initializes the choices available in the panel. - * - * @param setupList the MultipleSetup instance containing the setup definitions - */ - private void initChoices(MultipleSetup setupList) { - setups = setupList.getSetups(); - - choicesBox = new JComboBox<>(); - choicesBoxShadow = new ArrayList<>(); - - for (SetupDefinition setup : setups.getSetupKeysList()) { - String setupName = setup.getSetupName(); - choicesBox.addItem(setupName); - choicesBoxShadow.add(setupName); - } - - } - - /** - * Initializes the elements list and related components. - */ - private void initElements() { - elementsBoxShadow = new ArrayList<>(); - elementsBox = new JComboBox<>(); - elementsFiles = new ArrayList<>(); - elementsOptionPanels = new ArrayList<>(); - } - - /** - * Builds the choice panel containing controls for managing setup elements. - * - * @return the constructed JPanel - */ - private JPanel buildChoicePanel() { - JPanel panel = new JPanel(); - - panel.add(label); - panel.add(elementsBox); - panel.add(removeButton); - panel.add(choicesBox); - panel.add(addButton); - - panel.setLayout(new FlowLayout(FlowLayout.LEFT)); - - return panel; - } - - /** - * Adds the option from the available list to the selected list. - * - * @param evt the ActionEvent triggered by the add button - */ - private void addButtonActionPerformed(ActionEvent evt) { - int choice = choicesBox.getSelectedIndex(); - if (choice == -1) { - return; - } - - addElement(choice); - } - - /** - * Removes the option from the selected list to the available list. - * - * @param evt the ActionEvent triggered by the remove button - */ - private void removeButtonActionPerformed(ActionEvent evt) { - int indexToRemove = elementsBox.getSelectedIndex(); - if (indexToRemove == -1) { - return; - } - - removeElement(indexToRemove); - } - - /** - * Updates the options panel based on the selected element. - * - * @param e the ActionEvent triggered by the elements combo box - */ - private void elementComboBoxActionPerformed(ActionEvent e) { - updateSetupOptions(); - } - - @Override - public FieldType getType() { - return FieldType.setupList; - } - - /** - * Adds an element to the elements list, from the choices list. - * - * @param choice the index of the choice to add - * @return the index of the added element - */ - public int addElement(int choice) { - elementsBoxShadow.add(choice); - SetupDefinition setupKeys = setups.getSetupKeysList().get(choice); - - elementsFiles.add(SetupData.create(setupKeys)); - - BaseSetupPanel newPanel = new BaseSetupPanel(setupKeys, identationLevel + 1); - - elementsOptionPanels.add(newPanel); - - updateElementsComboBox(); - - int elementIndex = elementsBoxShadow.size() - 1; - elementsBox.setSelectedIndex(elementIndex); - - return elementIndex; - } - - /** - * Loads several elements from a FieldValue. - * - * @param value the FieldValue containing the elements to load - */ - @Override - public void updatePanel(Object value) { - clearElements(); - - ListOfSetups maps = (ListOfSetups) value; - - for (SetupData key : maps.getMapOfSetups()) { - loadElement(key); - } - - } - - /** - * Loads a single element from a SetupData instance. - * - * @param table the SetupData instance to load - */ - private void loadElement(SetupData table) { - String enumName = table.getSetupName(); - - int setupIndex = choicesBoxShadow.indexOf(enumName); - - if (setupIndex == -1) { - SpecsLogs.warn("Could not find enum '" + enumName + "'. Available enums:" + setups); - return; - } - - int elementsIndex = addElement(setupIndex); - - elementsFiles.set(elementsIndex, table); - elementsOptionPanels.get(elementsIndex).loadValues(table); - - } - - /** - * Updates the elements combo box with the current elements. - */ - private void updateElementsComboBox() { - elementsBox.removeAllItems(); - for (int i = 0; i < elementsBoxShadow.size(); i++) { - int choice = elementsBoxShadow.get(i); - String setupName = setups.getSetupKeysList().get(choice).getSetupName(); - - String boxString = buildSetupString(setupName, i + 1); - elementsBox.addItem(boxString); - } - } - - /** - * Updates the setup options panel based on the selected element. - */ - private void updateSetupOptions() { - if (currentOptionsPanel != null) { - remove(currentOptionsPanel); - currentOptionsPanel = null; - } - - int index = elementsBox.getSelectedIndex(); - - if (index != -1) { - currentOptionsPanel = elementsOptionPanels.get(index); - add(currentOptionsPanel); - currentOptionsPanel.revalidate(); - } - - repaint(); - } - - /** - * Removes an element from the elements list. - * - * @param index the index of the element to remove - */ - public void removeElement(int index) { - if (elementsBox.getItemCount() <= index) { - SpecsLogs.warn( - "Given index ('" + index + "')is too big. Elements size: " + elementsBox.getItemCount()); - return; - } - - elementsBoxShadow.remove(index); - elementsFiles.remove(index); - elementsOptionPanels.remove(index); - - updateElementsComboBox(); - - int newIndex = calculateIndexAfterRemoval(index); - if (newIndex != -1) { - elementsBox.setSelectedIndex(newIndex); - } - } - - /** - * Calculates the new index after an element is removed. - * - * @param index the index of the removed element - * @return the new index - */ - private int calculateIndexAfterRemoval(int index) { - int numElements = elementsBox.getItemCount(); - - if (numElements == 0) { - return -1; - } - - if (numElements > index) { - return index; - } - - if (numElements == index) { - return index - 1; - } - - SpecsLogs.warn("Invalid index '" + index + "' for list with '" + numElements + "' elements."); - return -1; - } - - /** - * Retrieves the packed values of the panel. - * - * @return the ListOfSetups containing the packed values - */ - public ListOfSetups getPackedValues() { - - List listOfSetups = new ArrayList<>(); - - for (BaseSetupPanel elementsOptionPanel : elementsOptionPanels) { - listOfSetups.add(elementsOptionPanel.getMapWithValues()); - } - - return new ListOfSetups(listOfSetups); - } - - /** - * Clears all elements from the panel. - */ - private void clearElements() { - elementsBox.removeAllItems(); - - elementsBoxShadow = new ArrayList<>(); - elementsFiles = new ArrayList<>(); - elementsOptionPanels = new ArrayList<>(); - } - - @Override - public FieldValue getOption() { - return FieldValue.create(getPackedValues(), getType()); - } - - /** - * Builds a string representation of a setup element. - * - * @param enumName the name of the enum - * @param index the index of the element - * @return the string representation - */ - private static String buildSetupString(String enumName, int index) { - return index + ENUM_NAME_SEPARATOR + enumName; - } - - @Override - public JLabel getLabel() { - return label; - } - - @Override - public Collection getPanels() { - return elementsOptionPanels.stream() - .map(setupPanel -> setupPanel.getPanels().values()) - .reduce(new ArrayList<>(), SpecsCollections::add); - } -} diff --git a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceListPanel.java b/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceListPanel.java deleted file mode 100644 index 214a0cb8..00000000 --- a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceListPanel.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2010 SPeCS Research Group. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package org.suikasoft.jOptions.gui.panels.option.notimplementedyet; - -import java.awt.FlowLayout; -import java.awt.event.ActionEvent; -import java.io.Serial; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; - -import pt.up.fe.specs.guihelper.FieldType; -import pt.up.fe.specs.guihelper.BaseTypes.FieldValue; -import pt.up.fe.specs.guihelper.gui.FieldPanel; -import pt.up.fe.specs.util.SpecsLogs; -import pt.up.fe.specs.util.utilities.StringList; - -/** - * Deprecated panel for editing multiple choice lists. - * - *

- * This panel was replaced with EnumMultipleChoicePanel. - * - * @deprecated replaced with EnumMultipleChoicePanel - */ -@Deprecated -public class MultipleChoiceListPanel extends FieldPanel { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * INSTANCE VARIABLES - */ - private final JLabel label; - private final JLabel helper; - private final JComboBox selectedValues; - private final JComboBox possibleValues; - private final JButton removeButton; - private final JButton addButton; - - private List possibleValuesShadow; - private List selectedValuesShadow; - - private final Collection originalChoices; - - /** - * Constructs a MultipleChoiceListPanel for the given label and choices. - * - * @param labelName the label for the panel - * @param choices the available choices - */ - public MultipleChoiceListPanel(String labelName, Collection choices) { - label = new JLabel(labelName + ":"); - helper = new JLabel("| Options:"); - removeButton = new JButton("Remove"); - addButton = new JButton("Add"); - - originalChoices = choices; - selectedValues = new JComboBox<>(); - possibleValues = new JComboBox<>(); - resetChoiceLists(); - - addButton.addActionListener(this::addButtonActionPerformed); - - removeButton.addActionListener(this::removeButtonActionPerformed); - - add(label); - add(selectedValues); - add(removeButton); - add(helper); - add(possibleValues); - add(addButton); - - setLayout(new FlowLayout(FlowLayout.LEFT)); - } - - /** - * Resets the choice lists to their original state. - */ - private void resetChoiceLists() { - selectedValues.removeAllItems(); - possibleValues.removeAllItems(); - - selectedValuesShadow = new ArrayList<>(); - possibleValuesShadow = new ArrayList<>(); - - // Add possible values - for (String choice : originalChoices) { - possibleValues.addItem(choice); - possibleValuesShadow.add(choice); - } - } - - /** - * Moves one value from possibleValues to selectedValues. This method is not - * thread-safe. - * - * @param valueName the name of the value to add - * @return true if the value was successfully added, false otherwise - */ - private boolean addValue(String valueName) { - if (valueName == null && possibleValuesShadow.isEmpty()) { - return true; - } - - // Check if value is available - if (!possibleValuesShadow.contains(valueName)) { - SpecsLogs.msgInfo("Could not find value '" + valueName + "' in Multiple Choice " - + "list. Available choices:" + possibleValuesShadow); - return false; - } - - // Remove from possible and add to selected - possibleValues.removeItem(valueName); - possibleValuesShadow.remove(valueName); - selectedValues.addItem(valueName); - selectedValuesShadow.add(valueName); - return true; - } - - /** - * Moves one value from selectedValues to possibleValues. This method is not - * thread-safe. - * - * @param valueName the name of the value to remove - * @return true if the value was successfully removed, false otherwise - */ - private boolean removeValue(String valueName) { - if (valueName == null && selectedValuesShadow.isEmpty()) { - return true; - } - // Check if value is selected - if (!selectedValuesShadow.contains(valueName)) { - SpecsLogs.warn( - "Could not find value '" + valueName + "' in already " - + "selected choices. Currently selected choices:" + selectedValuesShadow); - return false; - } - - // Remove from possible and add to selected - selectedValues.removeItem(valueName); - selectedValuesShadow.remove(valueName); - possibleValues.addItem(valueName); - possibleValuesShadow.add(valueName); - return true; - } - - /** - * Adds the option from the available list to the selected list. - * - * @param evt the action event - */ - private void addButtonActionPerformed(ActionEvent evt) { - final String selectedValue = (String) possibleValues.getSelectedItem(); - addValue(selectedValue); - } - - /** - * Removes the option from the selected list to the available list. - * - * @param evt the action event - */ - private void removeButtonActionPerformed(ActionEvent evt) { - final String selectedValue = (String) selectedValues.getSelectedItem(); - removeValue(selectedValue); - } - - /** - * Gets the currently selected values. - * - * @return an unmodifiable list of currently selected values - */ - public List getSelectedValues() { - return Collections.unmodifiableList(selectedValuesShadow); - } - - /** - * Updates the panel with the given value. - * - * @param value the value to update the panel with - */ - @Override - public void updatePanel(Object value) { - resetChoiceLists(); - - StringList values = (StringList) value; - - for (String valueName : values.getStringList()) { - if (selectedValuesShadow.contains(valueName)) { - continue; - } - addValue(valueName); - } - } - - /** - * Gets the type of the field. - * - * @return the field type - */ - @Override - public FieldType getType() { - return FieldType.multipleChoiceStringList; - } - - /** - * Gets the current option as a FieldValue. - * - * @return the current option - */ - @Override - public FieldValue getOption() { - List values = getSelectedValues(); - return FieldValue.create(new StringList(values), getType()); - } - - /** - * Gets the label of the panel. - * - * @return the label - */ - @Override - public JLabel getLabel() { - return label; - } - -} diff --git a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoicePanel.java b/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoicePanel.java deleted file mode 100644 index 0db436df..00000000 --- a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoicePanel.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2010 SPeCS Research Group. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package org.suikasoft.jOptions.gui.panels.option.notimplementedyet; - -import java.awt.FlowLayout; -import java.io.Serial; -import java.util.Collection; - -import javax.swing.JComboBox; -import javax.swing.JLabel; - -import pt.up.fe.specs.guihelper.FieldType; -import pt.up.fe.specs.guihelper.BaseTypes.FieldValue; -import pt.up.fe.specs.guihelper.gui.FieldPanel; -import pt.up.fe.specs.util.SpecsLogs; -import pt.up.fe.specs.util.SpecsSwing; - -/** - * Panel for selecting a single value from multiple choices. - * - *

- * This panel provides a combo box for selecting one value from a set of - * choices. - */ -public class MultipleChoicePanel extends FieldPanel { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * INSTANCE VARIABLES - */ - private JLabel label; - private JComboBox comboBoxValues; - private Collection availableChoices; - - /** - * Constructs a MultipleChoicePanel for the given label and choices. - * - * @param labelName the label for the panel - * @param choices the available choices - */ - public MultipleChoicePanel(String labelName, Collection choices) { - label = new JLabel(labelName + ":"); - comboBoxValues = new JComboBox<>(); - availableChoices = choices; - - for (String choice : choices) { - comboBoxValues.addItem(choice); - } - - add(label); - add(comboBoxValues); - - setLayout(new FlowLayout(FlowLayout.LEFT)); - } - - /** - * Returns the combo box component for selecting values. - * - * @return the combo box - */ - public JComboBox getValues() { - return comboBoxValues; - } - - /** - * Returns the current option as a FieldValue. - * - * @return the FieldValue - */ - @Override - public FieldValue getOption() { - String selectedString = getValues().getItemAt(getValues().getSelectedIndex()); - return FieldValue.create(selectedString, getType()); - } - - /** - * Updates the panel to select the given value. - * - * @param value the value to select - */ - @Override - public void updatePanel(Object value) { - String stringValue = (String) value; - if (stringValue.isEmpty()) { - stringValue = availableChoices.iterator().next(); - } - - final String currentChoice = stringValue; - - boolean foundChoice = availableChoices.contains(currentChoice); - - if (!foundChoice) { - SpecsLogs.warn( - "Could not find choice '" + currentChoice + "'. Available " + "choices: " + availableChoices); - return; - } - - SpecsSwing.runOnSwing(() -> comboBoxValues.setSelectedItem(currentChoice)); - } - - /** - * Returns the type of the field. - * - * @return the FieldType - */ - @Override - public FieldType getType() { - return FieldType.multipleChoice; - } - - /** - * Returns the label component of the panel. - * - * @return the label - */ - @Override - public JLabel getLabel() { - return label; - } - -} diff --git a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceSetup.java b/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceSetup.java deleted file mode 100644 index d96794d8..00000000 --- a/jOptions/src/org/suikasoft/jOptions/gui/panels/option/notimplementedyet/MultipleChoiceSetup.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright 2010 SPeCS Research Group. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -package org.suikasoft.jOptions.gui.panels.option.notimplementedyet; - -import java.awt.FlowLayout; -import java.awt.LayoutManager; -import java.awt.event.ActionEvent; -import java.io.Serial; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import javax.swing.BoxLayout; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import pt.up.fe.specs.guihelper.FieldType; -import pt.up.fe.specs.guihelper.Base.ListOfSetupDefinitions; -import pt.up.fe.specs.guihelper.Base.SetupDefinition; -import pt.up.fe.specs.guihelper.Base.SetupFieldEnum; -import pt.up.fe.specs.guihelper.BaseTypes.FieldValue; -import pt.up.fe.specs.guihelper.BaseTypes.ListOfSetups; -import pt.up.fe.specs.guihelper.BaseTypes.SetupData; -import pt.up.fe.specs.guihelper.SetupFieldOptions.MultipleSetup; -import pt.up.fe.specs.guihelper.gui.FieldPanel; -import pt.up.fe.specs.guihelper.gui.BasePanels.BaseSetupPanel; -import pt.up.fe.specs.util.SpecsCollections; -import pt.up.fe.specs.util.SpecsLogs; - -/** - * Panel for editing and managing multiple choice setup panels. - * - *

- * This panel provides controls for adding and managing multiple setup elements - * for a MultipleSetup instance. - */ -public class MultipleChoiceSetup extends FieldPanel { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * INSTANCE VARIABLES - */ - private JPanel currentOptionsPanel; - private JPanel choicePanel; - - private JLabel label; - private JComboBox choicesBox; - - private List choicesBoxNames; - private ListOfSetupDefinitions setups; - - private List elementsBoxShadow; - private List elementsFiles; - private List elementsOptionPanels; - - /** - * Constructs a MultipleChoiceSetup for the given enum option, label, and - * MultipleSetup. - * - * @param enumOption the SetupFieldEnum - * @param labelName the label for the panel - * @param setup the MultipleSetup instance - */ - public MultipleChoiceSetup(SetupFieldEnum enumOption, String labelName, MultipleSetup setup) { - // Initialize objects - label = new JLabel(labelName + ":"); - - initChoices(setup); - initElements(); - // Add choices - for (int i = 0; i < choicesBox.getItemCount(); i++) { - addElement(i); - } - - // Add actions - choicesBox.addActionListener(this::choiceComboBoxActionPerformed); - - // Build choice panel - choicePanel = buildChoicePanel(); - - currentOptionsPanel = null; - - LayoutManager layout = new BoxLayout(this, BoxLayout.Y_AXIS); - setLayout(layout); - add(choicePanel); - - } - - /** - * Initializes the choices for the MultipleSetup. - * - * @param setupList the MultipleSetup instance - */ - private void initChoices(MultipleSetup setupList) { - setups = setupList.getSetups(); - - choicesBox = new JComboBox<>(); - choicesBoxNames = new ArrayList<>(); - - for (SetupDefinition setup : setups.getSetupKeysList()) { - String setupName = setup.getSetupName(); - choicesBox.addItem(setupName); - choicesBoxNames.add(setupName); - } - - } - - /** - * Initializes the elements for the setup panel. - */ - private void initElements() { - elementsBoxShadow = new ArrayList<>(); - elementsFiles = new ArrayList<>(); - elementsOptionPanels = new ArrayList<>(); - } - - /** - * Builds the choice panel for the setup. - * - * @return the constructed JPanel - */ - private JPanel buildChoicePanel() { - JPanel panel = new JPanel(); - - panel.add(label); - panel.add(choicesBox); - - panel.setLayout(new FlowLayout(FlowLayout.LEFT)); - - return panel; - } - - /** - * Updates the options panel based on the selected choice. - * - * @param e the ActionEvent triggered by the JComboBox - */ - private void choiceComboBoxActionPerformed(ActionEvent e) { - updateSetupOptions(); - } - - @Override - public FieldType getType() { - return FieldType.setupList; - } - - /** - * Adds an element to the elements list, from the choices list. - * - * @param choice the index of the choice to add - * @return the index of the added element - */ - private int addElement(int choice) { - // Add index to elements - elementsBoxShadow.add(choice); - - // Get setup options and create option file for element - SetupDefinition setupKeys = setups.getSetupKeysList().get(choice); - - elementsFiles.add(SetupData.create(setupKeys)); - - BaseSetupPanel newPanel = new BaseSetupPanel(setupKeys); - elementsOptionPanels.add(newPanel); - - return elementsBoxShadow.size() - 1; - } - - /** - * Updates the panel with the given value. - * - * @param value the value to update the panel with - */ - @Override - public void updatePanel(Object value) { - ListOfSetups maps = (ListOfSetups) value; - - for (SetupData key : maps.getMapOfSetups()) { - loadSetup(key); - } - - // Show preferred setup - Integer choice = maps.getPreferredIndex(); - if (choice == null) { - choice = 0; - } - - choicesBox.setSelectedIndex(choice); - updateSetupOptions(); - - } - - /** - * Loads the given setup. - * - * @param setupData the SetupData to load - */ - private void loadSetup(SetupData setupData) { - // Build name - String enumName = setupData.getSetupName(); - - int setupIndex = choicesBoxNames.indexOf(enumName); - - if (setupIndex == -1) { - SpecsLogs.warn("Could not find enum '" + enumName + "'. Available enums:" + setups); - return; - } - - elementsFiles.set(setupIndex, setupData); - elementsOptionPanels.get(setupIndex).loadValues(setupData); - - } - - /** - * Updates the setup options panel. - */ - private void updateSetupOptions() { - if (currentOptionsPanel != null) { - remove(currentOptionsPanel); - currentOptionsPanel = null; - } - - int index = choicesBox.getSelectedIndex(); - - if (index != -1) { - currentOptionsPanel = elementsOptionPanels.get(index); - add(currentOptionsPanel); - currentOptionsPanel.revalidate(); - } - - repaint(); - } - - /** - * Retrieves the setups managed by this panel. - * - * @return the ListOfSetups instance - */ - public ListOfSetups getSetups() { - List listOfSetups = new ArrayList<>(); - - for (BaseSetupPanel elementsOptionPanel : elementsOptionPanels) { - listOfSetups.add(elementsOptionPanel.getMapWithValues()); - } - - ListOfSetups currentSetups = new ListOfSetups(listOfSetups); - - int choice = choicesBox.getSelectedIndex(); - if (choice == -1) { - SpecsLogs.warn("Could not get index of selected setup."); - return null; - } - currentSetups.setPreferredIndex(choice); - - return currentSetups; - - } - - @Override - public FieldValue getOption() { - return FieldValue.create(getSetups(), getType()); - } - - @Override - public JLabel getLabel() { - return label; - } - - @Override - public Collection getPanels() { - return elementsOptionPanels.stream() - .map(setupPanel -> setupPanel.getPanels().values()) - .reduce(new ArrayList<>(), SpecsCollections::add); - } - -} diff --git a/jOptions/src/org/suikasoft/jOptions/persistence/XmlPersistence.java b/jOptions/src/org/suikasoft/jOptions/persistence/XmlPersistence.java index b1eaf3ac..8a416807 100644 --- a/jOptions/src/org/suikasoft/jOptions/persistence/XmlPersistence.java +++ b/jOptions/src/org/suikasoft/jOptions/persistence/XmlPersistence.java @@ -18,7 +18,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import org.suikasoft.XStreamPlus.ObjectXml; @@ -32,9 +31,6 @@ import com.google.common.base.Preconditions; -import pt.up.fe.specs.guihelper.SetupDataXml; -import pt.up.fe.specs.guihelper.BaseTypes.FieldValue; -import pt.up.fe.specs.guihelper.BaseTypes.SetupData; import pt.up.fe.specs.util.SpecsLogs; import pt.up.fe.specs.util.utilities.LineStream; @@ -124,11 +120,6 @@ public DataStore loadData(File file) { continue; } - // Previous format, for compatibility - if (line.equals("")) { - return loadSetupData(file); - } - // Check if if (line.startsWith("$base")) { if (definition != null) { @@ -277,40 +268,6 @@ private void parseCustomPropertiesLine(String line, DataStore baseData) { baseData.setString(key, prop.value()); } - /** - * Loads setup data from the given file. - * - * @param file the file to load - * @return the loaded DataStore object - */ - private DataStore loadSetupData(File file) { - SpecsLogs.msgInfo("!Found old version of configuration file, trying to translate it"); - SetupData parsedObject = XStreamUtils.read(file, new SetupDataXml()); - if (parsedObject == null) { - SpecsLogs.msgInfo("Could not parse file '" + file.getPath() - + "' into a SetupData object."); - return null; - } - - StoreDefinition definition = StoreDefinition.newInstance(parsedObject.getSetupName(), options); - DataStore data = DataStore.newInstance(definition); - - // Set AppPersistence - data.setPersistence(this); - - // Add values - for (Entry entry : parsedObject.getDataset().entrySet()) { - @SuppressWarnings("unchecked") - DataKey key = (DataKey) definition.getKey(entry.getKey()); - - // Try to decode raw value, higher changes of compatibility - String rawValue = entry.getValue().getRawValue().toString(); - data.set(key, key.getDecoder().get().decode(rawValue)); - } - - return data; - } - /** * Creates an ObjectXml instance for the given StoreDefinition. * From f49ca2c4a7f7729c8a3738b64ef24d5938e890f8 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Fri, 25 Sep 2026 16:33:48 +0100 Subject: [PATCH 5/9] feat(SpecsIo): add OS-created temp directory and OS cache folder helpers - SpecsIo.createTempDirectory(prefix): unique directory allocated by the OS under the user temp folder, registered for deletion on exit as a backstop, so callers no longer hand-roll UUID/timestamp names - SpecsIo.getOsCacheFolder(): user-level cache folder following OS conventions (XDG_CACHE_HOME, ~/Library/Caches, %LOCALAPPDATA%) - SpecsSystem.isMac() --- .../src/pt/up/fe/specs/util/SpecsIo.java | 53 +++++++++++++++++++ .../src/pt/up/fe/specs/util/SpecsSystem.java | 9 ++++ .../test/pt/up/fe/specs/util/SpecsIoTest.java | 23 ++++++++ 3 files changed, 85 insertions(+) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java index 7bb9db3e..cc1b3903 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java @@ -30,6 +30,7 @@ import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -1910,6 +1911,58 @@ public static File getTempFolder(String folderName) { return mkdir(systemTemp, folderName); } + /** + * An empty, uniquely-named directory in the OS temporary folder, allocated by + * the operating system. + * + *

+ * The directory is registered for deletion when the virtual machine exits, as a + * backstop. Owners should still delete it as soon as its lifecycle ends. + * + * @param prefix the directory name prefix, must be at least three characters + * long + * @return the newly created directory + */ + public static File createTempDirectory(String prefix) { + try { + File tempFolder = Files.createTempDirectory(getTempFolder().toPath(), prefix).toFile(); + + // Backstop deletion, in case the owner does not get the chance to delete it + deleteOnExit(tempFolder); + + return tempFolder; + } catch (IOException e) { + throw new UncheckedIOException("Could not create temporary directory with prefix '" + prefix + "'", e); + } + } + + /** + * The user-level cache folder, following the operating system's conventions. + * + *

+ * Linux and other Unix-likes use '$XDG_CACHE_HOME', falling back to '~/.cache'; + * macOS uses '~/Library/Caches'; Windows uses '%LOCALAPPDATA%', falling back to + * '~/AppData/Local'. + * + * @return the existing or newly created OS cache folder + */ + public static File getOsCacheFolder() { + String cacheDir; + if (SpecsSystem.isWindows()) { + String localAppData = System.getenv("LOCALAPPDATA"); + cacheDir = localAppData != null && !localAppData.isBlank() ? localAppData + : new File(System.getProperty("user.home"), "AppData/Local").getAbsolutePath(); + } else if (SpecsSystem.isMac()) { + cacheDir = new File(System.getProperty("user.home"), "Library/Caches").getAbsolutePath(); + } else { + String xdgCacheHome = System.getenv("XDG_CACHE_HOME"); + cacheDir = xdgCacheHome != null && !xdgCacheHome.isBlank() ? xdgCacheHome + : new File(System.getProperty("user.home"), ".cache").getAbsolutePath(); + } + + return SpecsIo.mkdir(new File(cacheDir)); + } + /** * List directory contents for a resource folder. Not recursive. This is * basically a brute-force implementation. diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index cd4400b3..43485977 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -61,6 +61,7 @@ public class SpecsSystem { private static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase().startsWith("linux"); private static final boolean IS_WINDOWS = System.getProperty("os.name").startsWith("Windows"); + private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac"); private static final Map CACHED_METHODS = new HashMap<>(); private static final Map> CACHED_FIELDS = new HashMap<>(); @@ -874,6 +875,14 @@ public static boolean isWindows() { return IS_WINDOWS; } + /** + * @return true if the JVM is currently executing in a macOS system, false + * otherwise + */ + public static boolean isMac() { + return IS_MAC; + } + /** * Equivalent to class.isInstance. * diff --git a/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java b/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java index 90c7f35b..b15ac236 100644 --- a/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java +++ b/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java @@ -1041,6 +1041,29 @@ void testTempFileOperations() { assertThat(randomFolder).isNotNull(); } + @Test + @DisplayName("Test OS-created temp directory") + void testCreateTempDirectory() { + File tempFolder = SpecsIo.createTempDirectory("specs-test-"); + assertThat(tempFolder).isNotNull(); + assertThat(tempFolder.isDirectory()).isTrue(); + assertThat(tempFolder.getName()).startsWith("specs-test-"); + assertThat(tempFolder.getParentFile()).isEqualTo(SpecsIo.getTempFolder()); + assertThat(SpecsIo.createTempDirectory("specs-test-")) + .isNotEqualTo(SpecsIo.createTempDirectory("specs-test-")); + } + + @Test + @DisplayName("Test OS cache folder") + void testGetOsCacheFolder() { + File cacheFolder = SpecsIo.getOsCacheFolder(); + assertThat(cacheFolder).isNotNull(); + assertThat(cacheFolder.isDirectory()).isTrue(); + + // Same call returns the same location + assertThat(SpecsIo.getOsCacheFolder()).isEqualTo(cacheFolder); + } + @Test @DisplayName("Test ZIP operations") void testZipOperations(@TempDir Path tempDir) throws IOException { From cea5be9123be8508805c03be381a26af796ae7ff Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sun, 27 Sep 2026 18:43:47 +0100 Subject: [PATCH 6/9] Honor GC flag in used-memory measurement --- .../src/pt/up/fe/specs/util/SpecsSystem.java | 3 +-- .../pt/up/fe/specs/util/SpecsSystemTest.java | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java index 43485977..34477cad 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsSystem.java @@ -505,6 +505,7 @@ interface Returnable { } /** + * @param callGc whether to request garbage collection before measuring * @return the current amount of memory, in bytes */ public static long getUsedMemory(boolean callGc) { @@ -512,8 +513,6 @@ public static long getUsedMemory(boolean callGc) { System.gc(); } - System.gc(); - return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); } diff --git a/SpecsUtils/test/pt/up/fe/specs/util/SpecsSystemTest.java b/SpecsUtils/test/pt/up/fe/specs/util/SpecsSystemTest.java index 03658277..0563f179 100644 --- a/SpecsUtils/test/pt/up/fe/specs/util/SpecsSystemTest.java +++ b/SpecsUtils/test/pt/up/fe/specs/util/SpecsSystemTest.java @@ -16,11 +16,16 @@ import static org.assertj.core.api.Assertions.*; import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ThreadFactory; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordingFile; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -443,6 +448,27 @@ void testIsAvailable() { @DisplayName("Memory and Performance") class MemoryPerformance { + @Test + @DisplayName("getUsedMemory should only request GC when asked") + void testGetUsedMemoryGcRequests(@TempDir Path tempDir) throws IOException { + assertThat(countGcRequests(false, tempDir.resolve("without-gc.jfr"))).isZero(); + assertThat(countGcRequests(true, tempDir.resolve("with-gc.jfr"))).isEqualTo(1); + } + + private long countGcRequests(boolean callGc, Path recordingPath) throws IOException { + try (var recording = new Recording()) { + recording.enable("jdk.SystemGC"); + recording.start(); + SpecsSystem.getUsedMemory(callGc); + recording.stop(); + recording.dump(recordingPath); + } + + return RecordingFile.readAllEvents(recordingPath).stream() + .filter(event -> event.getEventType().getName().equals("jdk.SystemGC")) + .count(); + } + @Test @DisplayName("getUsedMemory should return positive value") void testGetUsedMemory() { From 19c8e3e4c81dbc77a89d77cd7ab2bc8bfc3844fa Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Mon, 28 Sep 2026 14:24:05 +0100 Subject: [PATCH 7/9] Make data-store schema lookup safe during parallel parsing Build key maps before publishing definitions and atomically cache key indexes. A concurrent reader previously observed an incomplete map, allowing setRaw to silently discard AST fields. --- .../jOptions/DataStore/ListDataStore.java | 10 ++--- .../storedefinition/AStoreDefinition.java | 15 ++++--- .../StoreDefinitionConcurrencyTest.java | 43 +++++++++++++++++++ 3 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 jOptions/test/org/suikasoft/jOptions/storedefinition/StoreDefinitionConcurrencyTest.java diff --git a/jOptions/src/org/suikasoft/jOptions/DataStore/ListDataStore.java b/jOptions/src/org/suikasoft/jOptions/DataStore/ListDataStore.java index 730a313f..72e2c677 100644 --- a/jOptions/src/org/suikasoft/jOptions/DataStore/ListDataStore.java +++ b/jOptions/src/org/suikasoft/jOptions/DataStore/ListDataStore.java @@ -20,6 +20,7 @@ import org.suikasoft.jOptions.storedefinition.StoreDefinitionIndexes; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; /** * Implementation of DataStore that uses a List to store the data. @@ -32,7 +33,7 @@ */ public class ListDataStore implements DataStore { - private static final Map KEY_TO_INDEXES = new HashMap<>(); + private static final Map KEY_TO_INDEXES = new ConcurrentHashMap<>(); private final StoreDefinition keys; private final List values; @@ -334,12 +335,7 @@ private int toIndex(String key) { * @return the StoreDefinitionIndexes */ private StoreDefinitionIndexes getIndexes() { - StoreDefinitionIndexes indexes = KEY_TO_INDEXES.get(keys); - if (indexes == null) { - indexes = new StoreDefinitionIndexes(keys); - KEY_TO_INDEXES.put(keys, indexes); - } - return indexes; + return KEY_TO_INDEXES.computeIfAbsent(keys, StoreDefinitionIndexes::new); } /** diff --git a/jOptions/src/org/suikasoft/jOptions/storedefinition/AStoreDefinition.java b/jOptions/src/org/suikasoft/jOptions/storedefinition/AStoreDefinition.java index 6cf0cf8e..24808588 100644 --- a/jOptions/src/org/suikasoft/jOptions/storedefinition/AStoreDefinition.java +++ b/jOptions/src/org/suikasoft/jOptions/storedefinition/AStoreDefinition.java @@ -14,8 +14,9 @@ package org.suikasoft.jOptions.storedefinition; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -31,7 +32,7 @@ public abstract class AStoreDefinition implements StoreDefinition { private final String appName; private final List sections; private final DataStore defaultData; - private final Map> keyMap = new HashMap<>(); + private final Map> keyMap; /** * Creates a new store definition with the given name and options. @@ -56,13 +57,17 @@ protected AStoreDefinition(String appName, List sections, DataStor this.appName = appName; this.sections = new ArrayList<>(sections); this.defaultData = defaultData; + Map> keysByName = new LinkedHashMap<>(); + for (StoreSection section : this.sections) { + for (DataKey key : section.getKeys()) { + keysByName.put(key.getName(), key); + } + } + this.keyMap = Collections.unmodifiableMap(keysByName); } @Override public Map> getKeyMap() { - if (keyMap.isEmpty()) { - keyMap.putAll(StoreDefinition.super.getKeyMap()); - } return keyMap; } diff --git a/jOptions/test/org/suikasoft/jOptions/storedefinition/StoreDefinitionConcurrencyTest.java b/jOptions/test/org/suikasoft/jOptions/storedefinition/StoreDefinitionConcurrencyTest.java new file mode 100644 index 00000000..badb0dbe --- /dev/null +++ b/jOptions/test/org/suikasoft/jOptions/storedefinition/StoreDefinitionConcurrencyTest.java @@ -0,0 +1,43 @@ +package org.suikasoft.jOptions.storedefinition; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +import org.junit.jupiter.api.Test; +import org.suikasoft.jOptions.Datakey.DataKey; +import org.suikasoft.jOptions.Datakey.KeyFactory; + +class StoreDefinitionConcurrencyTest { + + @Test + void readersNeverSeePartiallyInitializedKeyMap() throws InterruptedException { + int keyCount = 50_000; + var keys = new ArrayList>(keyCount); + for (int i = 0; i < keyCount; i++) { + keys.add(KeyFactory.string("key" + i, "")); + } + var definition = StoreDefinition.newInstance("concurrent", keys); + var start = new CountDownLatch(1); + var writer = new Thread(() -> { + start.countDown(); + definition.getKeyMap(); + }); + writer.start(); + start.await(); + + boolean partial = false; + while (writer.isAlive()) { + int size = definition.getKeyMap().size(); + if (size > 0 && size < keyCount) { + partial = true; + break; + } + } + writer.join(); + assertFalse(partial, "a reader saw an incomplete key map and could silently drop AST fields"); + assertEquals(keyCount, definition.getKeyMap().size()); + } +} From 2d498527f47c8bac2732ca19d63140a0913cfd76 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Wed, 7 Oct 2026 17:47:39 +0100 Subject: [PATCH 8/9] Fix generic key defaults and temp directory lifecycle Retain copied default suppliers in generic keys, add explicit temporary-directory cleanup, and select absolute Linux cache locations. Remove the stale GuiHelper composite include and add focused default and cleanup regression coverage. --- .../src/pt/up/fe/specs/util/SpecsIo.java | 69 +++++++++++-------- .../test/pt/up/fe/specs/util/SpecsIoTest.java | 56 +++++++++++++-- jOptions/settings.gradle | 1 - .../jOptions/Datakey/KeyFactory.java | 3 +- .../jOptions/Datakey/KeyFactoryTest.java | 61 ++++++++++++++++ 5 files changed, 150 insertions(+), 40 deletions(-) diff --git a/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java index cc1b3903..3a9cdb8c 100644 --- a/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java +++ b/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java @@ -41,8 +41,10 @@ import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.DirectoryStream; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -1911,41 +1913,43 @@ public static File getTempFolder(String folderName) { return mkdir(systemTemp, folderName); } - /** - * An empty, uniquely-named directory in the OS temporary folder, allocated by - * the operating system. - * - *

- * The directory is registered for deletion when the virtual machine exits, as a - * backstop. Owners should still delete it as soon as its lifecycle ends. - * - * @param prefix the directory name prefix, must be at least three characters - * long - * @return the newly created directory - */ + /** Creates a unique OS temporary directory. The caller deletes it when done. */ public static File createTempDirectory(String prefix) { try { - File tempFolder = Files.createTempDirectory(getTempFolder().toPath(), prefix).toFile(); - - // Backstop deletion, in case the owner does not get the chance to delete it - deleteOnExit(tempFolder); - - return tempFolder; + return Files.createTempDirectory(prefix).toFile(); } catch (IOException e) { throw new UncheckedIOException("Could not create temporary directory with prefix '" + prefix + "'", e); } } - /** - * The user-level cache folder, following the operating system's conventions. - * - *

- * Linux and other Unix-likes use '$XDG_CACHE_HOME', falling back to '~/.cache'; - * macOS uses '~/Library/Caches'; Windows uses '%LOCALAPPDATA%', falling back to - * '~/AppData/Local'. - * - * @return the existing or newly created OS cache folder - */ + /** Deletes a temporary directory and its contents without traversing links. */ + public static void deleteTempDirectory(File directory) { + try { + deleteTempDirectory(directory.toPath()); + } catch (IOException e) { + throw new UncheckedIOException("Could not delete temporary directory '" + directory + "'", e); + } + } + + private static void deleteTempDirectory(Path path) throws IOException { + if (Files.notExists(path, LinkOption.NOFOLLOW_LINKS)) { + return; + } + + BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + // Windows junctions are directories and "other"; delete the link without opening it. + if (attributes.isDirectory() && !attributes.isOther()) { + try (DirectoryStream children = Files.newDirectoryStream(path)) { + for (Path child : children) { + deleteTempDirectory(child); + } + } + } + Files.delete(path); + } + + /** Returns the user cache directory, creating it if needed. */ public static File getOsCacheFolder() { String cacheDir; if (SpecsSystem.isWindows()) { @@ -1956,8 +1960,13 @@ public static File getOsCacheFolder() { cacheDir = new File(System.getProperty("user.home"), "Library/Caches").getAbsolutePath(); } else { String xdgCacheHome = System.getenv("XDG_CACHE_HOME"); - cacheDir = xdgCacheHome != null && !xdgCacheHome.isBlank() ? xdgCacheHome - : new File(System.getProperty("user.home"), ".cache").getAbsolutePath(); + String home = System.getenv("HOME"); + String fallbackHome = home != null && !home.isBlank() && new File(home).isAbsolute() + ? home + : System.getProperty("user.home"); + cacheDir = xdgCacheHome != null && !xdgCacheHome.isBlank() && new File(xdgCacheHome).isAbsolute() + ? xdgCacheHome + : new File(fallbackHome, ".cache").getAbsolutePath(); } return SpecsIo.mkdir(new File(cacheDir)); diff --git a/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java b/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java index b15ac236..4276635a 100644 --- a/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java +++ b/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java @@ -13,6 +13,7 @@ import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -25,6 +26,7 @@ import java.util.Optional; import java.util.Set; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -1044,13 +1046,53 @@ void testTempFileOperations() { @Test @DisplayName("Test OS-created temp directory") void testCreateTempDirectory() { - File tempFolder = SpecsIo.createTempDirectory("specs-test-"); - assertThat(tempFolder).isNotNull(); - assertThat(tempFolder.isDirectory()).isTrue(); - assertThat(tempFolder.getName()).startsWith("specs-test-"); - assertThat(tempFolder.getParentFile()).isEqualTo(SpecsIo.getTempFolder()); - assertThat(SpecsIo.createTempDirectory("specs-test-")) - .isNotEqualTo(SpecsIo.createTempDirectory("specs-test-")); + File first = SpecsIo.createTempDirectory("specs-test-"); + File second = SpecsIo.createTempDirectory("specs-test-"); + try { + assertThat(first).isDirectory(); + assertThat(first.getName()).startsWith("specs-test-"); + assertThat(first.getParentFile()) + .isEqualTo(new File(System.getProperty("java.io.tmpdir")).getAbsoluteFile()); + assertThat(second).isNotEqualTo(first); + } finally { + SpecsIo.deleteTempDirectory(first); + SpecsIo.deleteTempDirectory(second); + } + } + + @Test + @DisplayName("Temp cleanup deletes nested contents") + void testDeleteTempDirectory() throws IOException { + File directory = SpecsIo.createTempDirectory("specs-test-"); + try { + Path nested = Files.createDirectory(directory.toPath().resolve("nested")); + Files.writeString(nested.resolve("file.txt"), "content"); + + SpecsIo.deleteTempDirectory(directory); + assertThat(directory).doesNotExist(); + } finally { + SpecsIo.deleteTempDirectory(directory); + } + } + + @Test + @DisplayName("Temp cleanup does not follow directory symlinks") + void testDeleteTempDirectorySymlink(@TempDir Path fixture) throws IOException { + Path outsideFile = Files.writeString(fixture.resolve("keep.txt"), "keep"); + File directory = SpecsIo.createTempDirectory("specs-test-"); + try { + try { + Files.createSymbolicLink(directory.toPath().resolve("outside"), fixture); + } catch (FileSystemException | UnsupportedOperationException | SecurityException e) { + Assumptions.assumeTrue(false, "Symbolic links unavailable: " + e.getMessage()); + } + + SpecsIo.deleteTempDirectory(directory); + assertThat(directory).doesNotExist(); + assertThat(outsideFile).exists(); + } finally { + SpecsIo.deleteTempDirectory(directory); + } } @Test diff --git a/jOptions/settings.gradle b/jOptions/settings.gradle index ad694059..6a7310a3 100644 --- a/jOptions/settings.gradle +++ b/jOptions/settings.gradle @@ -2,4 +2,3 @@ rootProject.name = 'jOptions' includeBuild("../SpecsUtils") includeBuild("../XStreamPlus") -includeBuild("../GuiHelper") diff --git a/jOptions/src/org/suikasoft/jOptions/Datakey/KeyFactory.java b/jOptions/src/org/suikasoft/jOptions/Datakey/KeyFactory.java index 85029980..553ae987 100644 --- a/jOptions/src/org/suikasoft/jOptions/Datakey/KeyFactory.java +++ b/jOptions/src/org/suikasoft/jOptions/Datakey/KeyFactory.java @@ -590,8 +590,7 @@ public static DataKey generic(String id, E exampleInstance) */ public static DataKey generic(String id, Supplier defaultSupplier) { DataKey datakey = new GenericKey<>(id, defaultSupplier.get()); - datakey.setDefault(defaultSupplier); - return datakey; + return datakey.setDefault(defaultSupplier); } /** diff --git a/jOptions/test/org/suikasoft/jOptions/Datakey/KeyFactoryTest.java b/jOptions/test/org/suikasoft/jOptions/Datakey/KeyFactoryTest.java index 5d1d2af5..cbb3b9bc 100644 --- a/jOptions/test/org/suikasoft/jOptions/Datakey/KeyFactoryTest.java +++ b/jOptions/test/org/suikasoft/jOptions/Datakey/KeyFactoryTest.java @@ -5,11 +5,14 @@ import java.io.File; import java.math.BigInteger; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.DisplayName; import pt.up.fe.specs.util.utilities.StringList; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.suikasoft.jOptions.Interfaces.DataStore; +import org.suikasoft.jOptions.storedefinition.StoreDefinition; /** * Comprehensive test suite for KeyFactory static factory methods. @@ -218,6 +221,64 @@ void testStringListFactoryWithDefault_CreatesStringListDataKey() { assertThat(key.getName()).isEqualTo("default.stringlist"); assertThat(key.getValueClass()).isEqualTo(StringList.class); } + + @Test + @DisplayName("list defaults are independent across stores and validate elements") + @SuppressWarnings({ "rawtypes", "unchecked" }) + void testListFactory_DefaultsAreIndependentAcrossStores_AndSetterValidatesElements() { + DataKey> key = KeyFactory.list("typed.list", String.class); + StoreDefinition definition = StoreDefinition.newInstance("Typed Lists", key); + DataStore simpleFirst = DataStore.newInstance(definition); + DataStore simpleSecond = DataStore.newInstance(definition); + DataStore closedFirst = DataStore.newInstance(definition, true); + DataStore closedSecond = DataStore.newInstance(definition, true); + + List simpleFirstValue = simpleFirst.get(key); + List simpleSecondValue = simpleSecond.get(key); + List closedFirstValue = closedFirst.get(key); + List closedSecondValue = closedSecond.get(key); + List> defaults = List.of(simpleFirstValue, simpleSecondValue, + closedFirstValue, closedSecondValue); + + assertThat(key.hasDefaultValue()).isTrue(); + assertThat(key.getDefault()).hasValueSatisfying(value -> assertThat(value).isEmpty()); + assertThat(defaults).allSatisfy(value -> assertThat(value).isEmpty()); + + for (int i = 0; i < defaults.size(); i++) { + for (int j = i + 1; j < defaults.size(); j++) { + assertThat(defaults.get(i)).isNotSameAs(defaults.get(j)); + } + } + + simpleFirstValue.add("mutable"); + assertThat(simpleSecondValue).isEmpty(); + assertThat(closedFirstValue).isEmpty(); + assertThat(closedSecondValue).isEmpty(); + + assertThatThrownBy(() -> simpleFirst.set(key, (List) List.of(1))) + .isInstanceOf(ClassCastException.class); + } + } + + @Nested + @DisplayName("Generic Key Factory") + class GenericFactoryTests { + + @Test + @DisplayName("generic factory retains and invokes its default supplier") + void testGenericFactory_RetainsDefaultSupplier() { + AtomicInteger supplierCalls = new AtomicInteger(); + + DataKey key = KeyFactory.generic("supplier.default", () -> { + supplierCalls.incrementAndGet(); + return "default"; + }); + + assertThat(supplierCalls).hasValue(1); + assertThat(key.hasDefaultValue()).isTrue(); + assertThat(key.getDefault()).hasValue("default"); + assertThat(supplierCalls).hasValue(2); + } } @Nested From 76fe6b1586bdc6bdb7f6501f30ce238891757b1b Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Wed, 7 Oct 2026 17:50:55 +0100 Subject: [PATCH 9/9] Cover concurrent store definitions and immutable key maps Port regression coverage for parallel first readers, independent data-store creation, subclass-safe construction, immutable key maps, and section order. Keep the existing store implementation and its Protobuf optimizations. Focused AStoreDefinition and existing StoreDefinition concurrency suites passed offline; aggregate coverage was excluded for this targeted run. --- .../AStoreDefinitionConcurrencyTest.java | 85 +++++++++++++++++++ .../storedefinition/AStoreDefinitionTest.java | 36 ++++++++ 2 files changed, 121 insertions(+) create mode 100644 jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionConcurrencyTest.java diff --git a/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionConcurrencyTest.java b/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionConcurrencyTest.java new file mode 100644 index 00000000..2bd580b8 --- /dev/null +++ b/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionConcurrencyTest.java @@ -0,0 +1,85 @@ +package org.suikasoft.jOptions.storedefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; +import org.suikasoft.jOptions.DataStore.ListDataStore; +import org.suikasoft.jOptions.Datakey.DataKey; +import org.suikasoft.jOptions.Datakey.KeyFactory; + +class AStoreDefinitionConcurrencyTest { + + private static final int THREADS = 32; + private static final int SHARED_KEYS = 2_048; + private static final int STORE_KEYS = 8; + private static final int ROUNDS = 8; + + @Test + void concurrentFirstReadersAndStoreCreationSeeCompleteDefinitions() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(THREADS); + + try { + for (int round = 0; round < ROUNDS; round++) { + final int currentRound = round; + List> sharedKeys = keys("shared-" + currentRound + "-", SHARED_KEYS); + TestStoreDefinition shared = new TestStoreDefinition("shared-" + currentRound, sharedKeys); + List independent = IntStream.range(0, THREADS) + .mapToObj(thread -> new TestStoreDefinition("independent-" + currentRound + "-" + thread, + keys("independent-" + currentRound + "-" + thread + "-", STORE_KEYS))) + .toList(); + CyclicBarrier gate = new CyclicBarrier(THREADS + 1); + List> futures = new ArrayList<>(THREADS); + + for (int thread = 0; thread < THREADS; thread++) { + final int threadIndex = thread; + futures.add(executor.submit(() -> { + gate.await(15, TimeUnit.SECONDS); + + Map> map = shared.getKeyMap(); + TestStoreDefinition definition = independent.get(threadIndex); + DataKey firstKey = definition.getKeys().get(0); + DataKey secondKey = definition.getKeys().get(1); + ListDataStore eager = new ListDataStore(definition); + eager.setRaw(firstKey.getName(), "eager"); + eager.get(firstKey.getName()); + eager.setRaw(secondKey.getName(), "second"); + eager.get(secondKey.getName()); + + return map.size(); + })); + } + + gate.await(15, TimeUnit.SECONDS); + for (Future future : futures) { + assertThat(future.get(15, TimeUnit.SECONDS)).isEqualTo(SHARED_KEYS); + } + } + } finally { + executor.shutdownNow(); + } + } + + private static List> keys(String prefix, int count) { + List> keys = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + keys.add(KeyFactory.string(prefix + index)); + } + return keys; + } + + private static final class TestStoreDefinition extends AStoreDefinition { + private TestStoreDefinition(String name, List> keys) { + super(name, keys); + } + } +} diff --git a/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionTest.java b/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionTest.java index b26a28ff..871e8765 100644 --- a/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionTest.java +++ b/jOptions/test/org/suikasoft/jOptions/storedefinition/AStoreDefinitionTest.java @@ -118,6 +118,26 @@ void shouldHandleNullSectionsList() { assertThatThrownBy(() -> new TestStoreDefinition("NullSections", null, null)) .isInstanceOf(NullPointerException.class); } + + @Test + void constructionDoesNotCallSubclassKeyAccessors() { + class DefinitionWithInitializedKeys extends AStoreDefinition { + private final List> initializedKeys; + + DefinitionWithInitializedKeys(List> keys) { + super("subclass", keys); + initializedKeys = keys; + } + + @Override + public List> getKeys() { + return List.copyOf(initializedKeys); + } + } + + var definition = new DefinitionWithInitializedKeys(testKeys); + assertThat(definition.getKeyMap().values()).containsExactlyElementsOf(testKeys); + } } @Nested @@ -178,6 +198,22 @@ void shouldCacheKeyMap() { assertThat(keyMap1).isSameAs(keyMap2); } + @Test + void publishedKeyMapCannotBeMutated() { + assertThatThrownBy(() -> storeDefinition.getKeyMap().remove(testStringKey.getName())) + .isInstanceOf(UnsupportedOperationException.class); + assertThat(storeDefinition.getKeyMap()).containsKey(testStringKey.getName()); + } + + @Test + void keyMapPreservesOrderAcrossSections() { + var first = StoreSection.newInstance(List.of(testBoolKey, testStringKey)); + var second = StoreSection.newInstance(List.of(testIntKey)); + var definition = new TestStoreDefinition("ordered", List.of(first, second), null); + + assertThat(definition.getKeyMap().keySet()).containsExactly("testBool", "testString", "testInt"); + } + @Test @DisplayName("Should handle empty key list") void shouldHandleEmptyKeyList() {