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/SpecsIo.java b/SpecsUtils/src/pt/up/fe/specs/util/SpecsIo.java index 7bb9db3e..3a9cdb8c 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; @@ -40,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; @@ -1910,6 +1913,65 @@ public static File getTempFolder(String folderName) { return mkdir(systemTemp, folderName); } + /** Creates a unique OS temporary directory. The caller deletes it when done. */ + public static File createTempDirectory(String prefix) { + try { + return Files.createTempDirectory(prefix).toFile(); + } catch (IOException e) { + throw new UncheckedIOException("Could not create temporary directory with prefix '" + prefix + "'", e); + } + } + + /** 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()) { + 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"); + 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)); + } + /** * 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 13c0c9a7..34477cad 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<>(); @@ -190,15 +191,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); @@ -254,19 +247,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); } @@ -525,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) { @@ -532,8 +513,6 @@ public static long getUsedMemory(boolean callGc) { System.gc(); } - System.gc(); - return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); } @@ -599,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 @@ -875,6 +874,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/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 diff --git a/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java b/SpecsUtils/test/pt/up/fe/specs/util/SpecsIoTest.java index 90c7f35b..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; @@ -1041,6 +1043,69 @@ void testTempFileOperations() { assertThat(randomFolder).isNotNull(); } + @Test + @DisplayName("Test OS-created temp directory") + void testCreateTempDirectory() { + 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 + @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 { 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() { 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/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/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/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/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. * 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/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 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() { 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()); + } +}