summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2025-06-15 18:44:18 -0500
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2025-06-15 18:44:18 -0500
commit255a0ac50b07d4fef9664767c4123ecaf4881d55 (patch)
tree526aa2945bcdb955d9043f6a07bac07298d1cdda
parent9025c4cfeb1f4e4d5d9b151a3797752f80a58bf6 (diff)
Fix potential resource leaks in Presenter
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java492
1 files changed, 250 insertions, 242 deletions
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
index ff7e23c..9913e89 100644
--- a/src/main/java/sevenUnitsGUI/Presenter.java
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -86,12 +86,11 @@ public final class Presenter {
* </ul>
*/
static final String DEFAULT_LOCALE = "en";
-
+
private static final List<String> LOCAL_LOCALES = List.of("en", "fr");
private static final Path USER_LOCALES_DIR = userConfigDir()
.resolve("SevenUnits").resolve("locales");
-
-
+
/**
* Adds default units and dimensions to a database.
*
@@ -113,14 +112,14 @@ public final class Presenter {
// nonlinear units - must be loaded manually
database.addUnit("tempCelsius", Metric.CELSIUS);
database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
-
+
// load initial dimensions
database.addDimension("Length", Metric.Dimensions.LENGTH);
database.addDimension("Mass", Metric.Dimensions.MASS);
database.addDimension("Time", Metric.Dimensions.TIME);
database.addDimension("Temperature", Metric.Dimensions.TEMPERATURE);
}
-
+
private static String displayRuleToString(
Function<UncertainDouble, String> numberDisplayRule) {
if (numberDisplayRule instanceof FixedDecimals)
@@ -134,7 +133,7 @@ public final class Presenter {
else
return numberDisplayRule.toString();
}
-
+
/**
* Determines where to wrap {@code toWrap} with a max line length of
* {@code maxLineLength}. If no good spot is found, returns -1.
@@ -149,7 +148,7 @@ public final class Presenter {
}
return -1;
}
-
+
/**
* Gets the text of a resource file as a set of strings (each one is one line
* of the text).
@@ -161,7 +160,7 @@ public final class Presenter {
*/
private static List<String> getLinesFromResource(String filename) {
final List<String> lines = new ArrayList<>();
-
+
try (var stream = inputStream(filename);
var scanner = new Scanner(stream)) {
while (scanner.hasNextLine()) {
@@ -171,10 +170,10 @@ public final class Presenter {
throw new AssertionError(
"Error occurred while loading file " + filename, e);
}
-
+
return lines;
}
-
+
/**
* Gets an input stream for a resource file.
*
@@ -186,7 +185,7 @@ public final class Presenter {
private static InputStream inputStream(String filepath) {
return Presenter.class.getResourceAsStream(filepath);
}
-
+
/**
* Convert a linear unit value to a string, where the number is rounded to
* the nearest integer.
@@ -197,26 +196,26 @@ public final class Presenter {
private static String linearUnitValueIntToString(LinearUnitValue uv) {
return Long.toString(Math.round(uv.getValueExact())) + " " + uv.getUnit();
}
-
+
private static Map.Entry<String, String> parseSettingLine(String line) {
final var equalsIndex = line.indexOf('=');
if (equalsIndex == -1)
throw new IllegalStateException(
"Settings file is malformed at line: " + line);
-
+
final var param = line.substring(0, equalsIndex);
final var value = line.substring(equalsIndex + 1);
-
+
return Map.entry(param, value);
}
-
+
/** Gets a Path from a pathname in the config file. */
private static Path pathFromConfig(String pathname) {
return CONFIG_FILE.getParent().resolve(pathname);
}
-
+
// ====== SETTINGS ======
-
+
private static String searchRuleToString(
Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
if (PrefixSearchRule.NO_PREFIXES.equals(searchRule))
@@ -228,7 +227,7 @@ public final class Presenter {
else
return searchRule.toString();
}
-
+
/**
* @return true iff a and b have any elements in common
* @since 2022-04-19
@@ -241,7 +240,7 @@ public final class Presenter {
}
return false;
}
-
+
private static Path userConfigDir() {
if (System.getProperty("os.name").startsWith("Windows")) {
final var envFolder = System.getenv("LOCALAPPDATA");
@@ -256,7 +255,7 @@ public final class Presenter {
else
return Path.of(envFolder);
}
-
+
/**
* @return {@code line} with any comments removed.
* @since 2021-03-13
@@ -266,7 +265,7 @@ public final class Presenter {
final var index = line.indexOf('#');
return index == -1 ? line : line.substring(0, index);
}
-
+
/**
* Wraps a string, ensuring no line is longer than {@code maxLineLength}.
*
@@ -291,23 +290,23 @@ public final class Presenter {
wrapped.append(remaining);
return wrapped.toString();
}
-
+
/**
* The view that this presenter communicates with
*/
private final View view;
-
+
/**
* The database that this presenter communicates with (effectively the model)
*/
final UnitDatabase database;
-
+
/**
* The rule used for parsing input numbers. Any number-string inputted into
* this program will be parsed using this method. <b>Not implemented yet.</b>
*/
private Function<String, UncertainDouble> numberParsingRule;
-
+
/**
* The rule used for displaying the results of unit conversions. The result
* of unit conversions will be put into this function, and the resulting
@@ -315,41 +314,41 @@ public final class Presenter {
*/
private Function<UncertainDouble, String> numberDisplayRule = StandardDisplayRules
.uncertaintyBased();
-
+
/**
* A predicate that determines whether or not a certain combination of
* prefixes is allowed. If it returns false, a combination of prefixes will
* not be allowed. Prefixes are put in the list from right to left.
*/
private Predicate<List<UnitPrefix>> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION;
-
+
/**
* A rule that accepts a prefixless name-unit pair and returns a map mapping
* names to prefixed versions of that unit (including the unit itself) that
* should be searchable.
*/
private Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule = PrefixSearchRule.NO_PREFIXES;
-
+
/**
* The set of units that is considered neither metric nor nonmetric for the
* purposes of the metric-imperial one-way conversion. These units are
* included in both From and To, even if One Way Conversion is enabled.
*/
private final Set<String> metricExceptions;
-
+
/** maps locale names (e.g. 'en') to key-text maps */
final Map<String, Map<String, String>> locales;
-
+
/** name of locale in locales to use */
String userLocale;
-
+
/**
* If this is true, views that show units as a list will have metric units
* removed from the From unit list and imperial/USC units removed from the To
* unit list.
*/
private boolean oneWayConversionEnabled = false;
-
+
/**
* If this is false, duplicate units and prefixes will be removed from the
* unit view in views that show units as a list to choose from.
@@ -368,7 +367,7 @@ public final class Presenter {
private final Set<Path> customDimensionFiles = new HashSet<>();
/** Custom exception datafiles that will be loaded by {@link #reloadData} */
private final Set<Path> customExceptionFiles = new HashSet<>();
-
+
/**
* Creates a Presenter
*
@@ -380,78 +379,40 @@ public final class Presenter {
this.view = view;
this.database = new UnitDatabase();
this.metricExceptions = new HashSet<>();
-
+
this.locales = this.loadLocales();
this.userLocale = DEFAULT_LOCALE;
-
+
// set default settings temporarily
if (Files.exists(CONFIG_FILE)) {
this.loadSettings(CONFIG_FILE);
}
-
+
this.reloadData();
-
+
// print out unit counts
System.out.println(this.loadStatMsg());
}
- /**
- * Clears then reloads all unit, prefix, dimension and exception data.
- */
- public void reloadData() {
- this.database.clear();
- this.metricExceptions.clear();
- addDefaults(this.database);
-
- if (this.useDefaultDatafiles) {
- this.loadDefaultData();
+ private void addLocaleFile(Map<String, Map<String, String>> locales,
+ Path file) throws IOException {
+ final Map<String, String> locale = new HashMap<>();
+ final String fileName = file.getName(file.getNameCount() - 1).toString();
+ final String localeName = fileName.substring(0, fileName.length() - 4);
+ try (Stream<String> lines = Files.lines(file)) {
+ lines.forEach(line -> this.addLocaleLine(locale, line));
}
-
- this.customUnitFiles.forEach(
- path -> this.handleLoadErrors(this.database.loadUnitsFile(path)));
- this.customDimensionFiles.forEach(
- path -> this.handleLoadErrors(this.database.loadDimensionFile(path)));
- this.customExceptionFiles.forEach(this::loadExceptionFile);
+ locales.put(localeName, locale);
}
- /**
- * Load units, prefixes and dimensions from the default files.
- */
- private void loadDefaultData() {
- // load units and prefixes
- try (final var units = inputStream(DEFAULT_UNITS_FILEPATH)) {
- this.handleLoadErrors(this.database.loadUnitsFromStream(units));
- } catch (final IOException e) {
- throw new AssertionError("Loading of unitsfile.txt failed.", e);
- }
-
- // load dimensions
- try (final var dimensions = inputStream(
- DEFAULT_DIMENSIONS_FILEPATH)) {
- this.handleLoadErrors(
- this.database.loadDimensionsFromStream(dimensions));
- } catch (final IOException e) {
- throw new AssertionError("Loading of dimensionfile.txt failed.", e);
- }
-
- // load metric exceptions
- try {
- try (var exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
- var scanner = new Scanner(exceptions)) {
- while (scanner.hasNextLine()) {
- final var line = Presenter
- .withoutComments(scanner.nextLine());
- if (!line.isBlank()) {
- this.metricExceptions.add(line);
- }
- }
- }
- } catch (final IOException e) {
- throw new AssertionError("Loading of metric_exceptions.txt failed.",
- e);
- }
+ private void addLocaleLine(Map<String, String> locale, String line) {
+ final String[] parts = line.split("=", 2);
+ if (parts.length < 2)
+ return;
+
+ locale.put(parts[0], parts[1]);
}
-
+
/**
* Applies a search rule to an entry in a name-unit map.
*
@@ -472,7 +433,7 @@ public final class Presenter {
}
return Stream.of(e);
}
-
+
/**
* Converts from the view's input expression to its output expression.
* Displays an error message if any of the required fields are invalid.
@@ -489,10 +450,10 @@ public final class Presenter {
throw new UnsupportedOperationException(
"This function can only be called when the view is an ExpressionConversionView");
final var xcview = (ExpressionConversionView) this.view;
-
+
final var fromExpression = xcview.getFromExpression();
final var toExpression = xcview.getToExpression();
-
+
// expressions must not be empty
if (fromExpression.isEmpty()) {
this.view.showErrorMessage("Parse Error",
@@ -504,7 +465,7 @@ public final class Presenter {
"Please enter a unit expression in the To: box.");
return;
}
-
+
final Optional<UnitConversionRecord> uc;
if (this.database.containsUnitSetName(toExpression)) {
uc = this.convertExpressionToNamedMultiUnit(fromExpression,
@@ -515,10 +476,10 @@ public final class Presenter {
} else {
uc = this.convertExpressionToExpression(fromExpression, toExpression);
}
-
+
uc.ifPresent(xcview::showExpressionConversionOutput);
}
-
+
/**
* Converts a unit expression to another expression.
*
@@ -547,7 +508,7 @@ public final class Presenter {
"Could not recognize text in To entry: " + e.getMessage());
return Optional.empty();
}
-
+
// convert and show output
if (!from.getUnit().canConvertTo(to)) {
this.view.showErrorMessage("Conversion Error",
@@ -556,7 +517,7 @@ public final class Presenter {
return Optional.empty();
}
final UncertainDouble uncertainValue;
-
+
// uncertainty is meaningless for non-linear units, so we will have
// to erase uncertainty information for them
if (to instanceof LinearUnit) {
@@ -566,14 +527,13 @@ public final class Presenter {
final var value = from.asUnitValue().convertTo(to).getValue();
uncertainValue = UncertainDouble.of(value, 0);
}
-
- final var uc = UnitConversionRecord.valueOf(
- fromExpression, toExpression, "",
- this.numberDisplayRule.apply(uncertainValue));
+
+ final var uc = UnitConversionRecord.valueOf(fromExpression, toExpression,
+ "", this.numberDisplayRule.apply(uncertainValue));
return Optional.of(uc);
-
+
}
-
+
/**
* Convert an expression to a MultiUnit. If an error happened, it is shown to
* the view and Optional.empty() is returned.
@@ -591,7 +551,7 @@ public final class Presenter {
"Could not recognize text in From entry: " + e.getMessage());
return Optional.empty();
}
-
+
final List<LinearUnit> toUnits = new ArrayList<>(toExpressions.length);
for (final String toExpression : toExpressions) {
try {
@@ -610,7 +570,7 @@ public final class Presenter {
return Optional.empty();
}
}
-
+
final List<LinearUnitValue> toValues;
try {
toValues = from.convertToMultiple(toUnits);
@@ -619,12 +579,12 @@ public final class Presenter {
"Invalid units separated by ';': " + e.getMessage());
return Optional.empty();
}
-
+
final var toExpression = this.linearUnitValueSumToString(toValues);
return Optional.of(
UnitConversionRecord.valueOf(fromExpression, toExpression, "", ""));
}
-
+
/**
* Convert an expression to a MultiUnit with a name from the database. If an
* error happened, it is shown to the view and Optional.empty() is returned.
@@ -642,7 +602,7 @@ public final class Presenter {
"Could not recognize text in From entry: " + e.getMessage());
return Optional.empty();
}
-
+
final var toUnits = this.database.getUnitSet(toName);
final List<LinearUnitValue> toValues;
try {
@@ -652,12 +612,12 @@ public final class Presenter {
"Invalid units separated by ';': " + e.getMessage());
return Optional.empty();
}
-
+
final var toExpression = this.linearUnitValueSumToString(toValues);
return Optional.of(
UnitConversionRecord.valueOf(fromExpression, toExpression, "", ""));
}
-
+
/**
* Converts from the view's input unit to its output unit. Displays an error
* message if any of the required fields are invalid.
@@ -674,11 +634,11 @@ public final class Presenter {
throw new UnsupportedOperationException(
"This function can only be called when the view is a UnitConversionView.");
final var ucview = (UnitConversionView) this.view;
-
+
final var fromUnitOptional = ucview.getFromSelection();
final var toUnitOptional = ucview.getToSelection();
final var inputValueString = ucview.getInputValue();
-
+
// extract values from optionals
final String fromUnitString, toUnitString;
if (fromUnitOptional.isPresent()) {
@@ -695,11 +655,11 @@ public final class Presenter {
"Please specify a To unit");
return;
}
-
+
// convert strings to data, checking if anything is invalid
final Unit fromUnit;
final UncertainDouble uncertainValue;
-
+
if (this.database.containsUnitName(fromUnitString)) {
fromUnit = this.database.getUnit(fromUnitString);
} else
@@ -717,14 +677,13 @@ public final class Presenter {
this.convertUnitToUnit(fromUnitString, toUnitString,
inputValueString, fromUnit, toUnit, uncertainValue));
} else if (this.database.containsUnitSetName(toUnitString)) {
- final var toMulti = this.database
- .getUnitSet(toUnitString);
+ final var toMulti = this.database.getUnitSet(toUnitString);
ucview.showUnitConversionOutput(this.convertUnitToMulti(fromUnitString,
inputValueString, fromUnit, toMulti, uncertainValue));
} else
throw this.viewError("Nonexistent To unit: %s", toUnitString);
}
-
+
private UnitConversionRecord convertUnitToMulti(String fromUnitString,
String inputValueString, Unit fromUnit, List<LinearUnit> toMulti,
UncertainDouble uncertainValue) {
@@ -733,7 +692,7 @@ public final class Presenter {
throw this.viewError("Could not convert between %s and %s",
fromUnit, toUnit);
}
-
+
final LinearUnitValue initValue;
if (fromUnit instanceof LinearUnit) {
final var fromLinear = (LinearUnit) fromUnit;
@@ -742,22 +701,21 @@ public final class Presenter {
initValue = UnitValue.of(fromUnit, uncertainValue.value())
.convertToBase(NameSymbol.EMPTY);
}
-
- final var converted = initValue
- .convertToMultiple(toMulti);
+
+ final var converted = initValue.convertToMultiple(toMulti);
final var toExpression = this.linearUnitValueSumToString(converted);
return UnitConversionRecord.valueOf(fromUnitString, toExpression,
inputValueString, "");
-
+
}
-
+
private UnitConversionRecord convertUnitToUnit(String fromUnitString,
String toUnitString, String inputValueString, Unit fromUnit,
Unit toUnit, UncertainDouble uncertainValue) {
if (!fromUnit.canConvertTo(toUnit))
throw this.viewError("Could not convert between %s and %s", fromUnit,
toUnit);
-
+
// convert - we will need to erase uncertainty for non-linear units, so
// we need to treat linear and non-linear units differently
final String outputValueString;
@@ -767,21 +725,21 @@ public final class Presenter {
final var initialValue = LinearUnitValue.of(fromLinear,
uncertainValue);
final var converted = initialValue.convertTo(toLinear);
-
+
outputValueString = this.numberDisplayRule.apply(converted.getValue());
} else {
final var initialValue = UnitValue.of(fromUnit,
uncertainValue.value());
final var converted = initialValue.convertTo(toUnit);
-
+
outputValueString = this.numberDisplayRule
.apply(UncertainDouble.of(converted.getValue(), 0));
}
-
+
return UnitConversionRecord.valueOf(fromUnitString, toUnitString,
inputValueString, outputValueString);
}
-
+
/**
* @return true iff duplicate units are shown in unit lists
* @since 2022-03-30
@@ -790,39 +748,44 @@ public final class Presenter {
public boolean duplicatesShown() {
return this.showDuplicates;
}
-
+
+ private String formatAboutText(Stream<String> rawLines) {
+ return rawLines.map(Presenter::withoutComments)
+ .collect(Collectors.joining("\n"))
+ .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString())
+ .replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72));
+ }
+
/**
* @return text in About file
* @since 2022-02-19
* @since v0.4.0
*/
public String getAboutText() {
- final Path customFilepath = Presenter.pathFromConfig(
- "about/" + this.userLocale + ".txt");
+ final Path customFilepath = Presenter
+ .pathFromConfig("about/" + this.userLocale + ".txt");
if (Files.exists(customFilepath)) {
- try {
- return formatAboutText(Files.lines(customFilepath));
- } catch (IOException e) {
- final String filename = String.format("/about/%s.txt", this.userLocale);
- return formatAboutText(Presenter.getLinesFromResource(filename).stream());
+ try (Stream<String> lines = Files.lines(customFilepath)) {
+ return this.formatAboutText(lines);
+ } catch (final IOException e) {
+ final String filename = String.format("/about/%s.txt",
+ this.userLocale);
+ return this.formatAboutText(
+ Presenter.getLinesFromResource(filename).stream());
}
} else if (LOCAL_LOCALES.contains(this.userLocale)) {
- final String filename = String.format("/about/%s.txt", this.userLocale);
- return formatAboutText(Presenter.getLinesFromResource(filename).stream());
+ final String filename = String.format("/about/%s.txt",
+ this.userLocale);
+ return this.formatAboutText(
+ Presenter.getLinesFromResource(filename).stream());
} else {
final String filename = String.format("/about/%s.txt", DEFAULT_LOCALE);
- return formatAboutText(Presenter.getLinesFromResource(filename).stream());
+ return this.formatAboutText(
+ Presenter.getLinesFromResource(filename).stream());
}
}
- private String formatAboutText(Stream<String> rawLines) {
- return rawLines
- .map(Presenter::withoutComments).collect(Collectors.joining("\n"))
- .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString())
- .replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72));
- }
-
/**
* @return set of all locales available to select
* @since 2025-02-21
@@ -831,7 +794,7 @@ public final class Presenter {
public Set<String> getAvailableLocales() {
return this.locales.keySet();
}
-
+
/**
* Gets a name for this dimension using the database
*
@@ -847,7 +810,7 @@ public final class Presenter {
.filter(d -> d.equals(dimension)).findAny().map(Nameable::getName)
.orElse(dimension.toString(Nameable::getName));
}
-
+
/**
* Gets the correct text for a provided ID. If this text is available in the
* user's locale, that text is provided. Otherwise, text is taken from the
@@ -861,7 +824,7 @@ public final class Presenter {
final var defaultLocale = this.locales.get(DEFAULT_LOCALE);
return userLocale.getOrDefault(textID, defaultLocale.get(textID));
}
-
+
/**
* @return the rule that is used by this presenter to convert numbers into
* strings
@@ -871,7 +834,7 @@ public final class Presenter {
public Function<UncertainDouble, String> getNumberDisplayRule() {
return this.numberDisplayRule;
}
-
+
/**
* @return the rule that is used by this presenter to convert strings into
* numbers
@@ -882,7 +845,7 @@ public final class Presenter {
private Function<String, UncertainDouble> getNumberParsingRule() {
return this.numberParsingRule;
}
-
+
/**
* @return the rule that determines whether a set of prefixes is valid
* @since 2022-04-19
@@ -891,7 +854,7 @@ public final class Presenter {
public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
return this.prefixRepetitionRule;
}
-
+
/**
* @return the rule that determines which units are prefixed
* @since 2022-07-08
@@ -900,7 +863,7 @@ public final class Presenter {
public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getSearchRule() {
return this.searchRule;
}
-
+
/**
* @return a search rule that shows all single prefixes
* @since 2022-07-08
@@ -910,16 +873,16 @@ public final class Presenter {
return PrefixSearchRule.getCoherentOnlyRule(
new HashSet<>(this.database.prefixMap(true).values()));
}
-
+
/**
* @return user's selected locale
* @since 2025-02-21
* @since v1.0.0
*/
public String getUserLocale() {
- return userLocale;
+ return this.userLocale;
}
-
+
/**
* @return the view associated with this presenter
* @since 2022-04-19
@@ -928,7 +891,7 @@ public final class Presenter {
public View getView() {
return this.view;
}
-
+
/**
* Accepts a list of errors. If that list is non-empty, prints an error
* message and alerts the user.
@@ -947,7 +910,7 @@ public final class Presenter {
errorMessage);
}
}
-
+
/**
* @return whether or not the provided unit is semi-metric (i.e. an
* exception)
@@ -964,7 +927,7 @@ public final class Presenter {
&& this.metricExceptions.contains(symbol.orElseThrow())
|| sharesAnyElements(this.metricExceptions, u.getOtherNames());
}
-
+
/**
* Convert a list of LinearUnitValues that you would get from a unit-set
* conversion to a string. All but the last have their numbers rendered as
@@ -982,7 +945,43 @@ public final class Presenter {
return integerPart + " + " + this.numberDisplayRule.apply(last.getValue())
+ " " + last.getUnit();
}
-
+
+ /**
+ * Load units, prefixes and dimensions from the default files.
+ */
+ private void loadDefaultData() {
+ // load units and prefixes
+ try (final var units = inputStream(DEFAULT_UNITS_FILEPATH)) {
+ this.handleLoadErrors(this.database.loadUnitsFromStream(units));
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of unitsfile.txt failed.", e);
+ }
+
+ // load dimensions
+ try (final var dimensions = inputStream(DEFAULT_DIMENSIONS_FILEPATH)) {
+ this.handleLoadErrors(
+ this.database.loadDimensionsFromStream(dimensions));
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of dimensionfile.txt failed.", e);
+ }
+
+ // load metric exceptions
+ try {
+ try (var exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
+ var scanner = new Scanner(exceptions)) {
+ while (scanner.hasNextLine()) {
+ final var line = Presenter.withoutComments(scanner.nextLine());
+ if (!line.isBlank()) {
+ this.metricExceptions.add(line);
+ }
+ }
+ }
+ } catch (final IOException e) {
+ throw new AssertionError("Loading of metric_exceptions.txt failed.",
+ e);
+ }
+ }
+
private void loadExceptionFile(Path exceptionFile) {
try (var lines = Files.lines(exceptionFile)) {
lines.map(Presenter::withoutComments)
@@ -993,54 +992,38 @@ public final class Presenter {
+ exceptionFile + "\": " + e.getLocalizedMessage());
}
}
-
+
/**
* Loads all available locales, including custom ones, into a map.
+ *
* @return map containing locales
*/
private Map<String, Map<String, String>> loadLocales() {
final Map<String, Map<String, String>> locales = new HashMap<>();
for (final String localeName : LOCAL_LOCALES) {
final Map<String, String> locale = new HashMap<>();
- String filename = "/locales/" + localeName + ".txt";
- getLinesFromResource(filename).forEach(line -> addLocaleLine(locale, line));
- locales.put(localeName, locale);
+ final String filename = "/locales/" + localeName + ".txt";
+ getLinesFromResource(filename)
+ .forEach(line -> this.addLocaleLine(locale, line));
+ locales.put(localeName, locale);
}
if (Files.exists(USER_LOCALES_DIR)) {
- try {
- Files.list(USER_LOCALES_DIR).forEach(
- localeFile -> {
- try {
- addLocaleFile(locales, localeFile);
- } catch (IOException e) {
- e.printStackTrace();
- }
- });
- } catch (IOException e) {
+ try (Stream<Path> files = Files.list(USER_LOCALES_DIR)) {
+ files.forEach(localeFile -> {
+ try {
+ this.addLocaleFile(locales, localeFile);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ });
+ } catch (final IOException e) {
e.printStackTrace();
}
}
return locales;
}
- private void addLocaleFile(Map<String, Map<String, String>> locales, Path file) throws IOException {
- final Map<String, String> locale = new HashMap<>();
- String fileName = file.getName(file.getNameCount()-1).toString();
- String localeName = fileName.substring(0, fileName.length()-4);
- Files.lines(file).forEach(line -> addLocaleLine(locale, line));
- locales.put(localeName, locale);
- }
-
- private void addLocaleLine(Map<String, String> locale, String line) {
- String[] parts = line.split("=", 2);
- if (parts.length < 2) {
- return;
- }
-
- locale.put(parts[0], parts[1]);
- }
-
/**
* Loads settings from the user's settings file and applies them to the
* presenter.
@@ -1053,11 +1036,11 @@ public final class Presenter {
this.customDimensionFiles.clear();
this.customExceptionFiles.clear();
this.customUnitFiles.clear();
-
+
for (final Map.Entry<String, String> setting : this
.settingsFromFile(settingsFile)) {
final var value = setting.getValue();
-
+
switch (setting.getKey()) {
// set manually to avoid the unnecessary saving of the non-manual
// methods
@@ -1094,11 +1077,11 @@ public final class Presenter {
if (this.locales.containsKey(value)) {
this.userLocale = value;
} else {
- System.err.printf(
- "Warning: unrecognized locale \"%s\".%n", value);
+ System.err.printf("Warning: unrecognized locale \"%s\".%n",
+ value);
this.view.showErrorMessage("Unrecognized Locale",
"Could not find locale \"" + value
- + "\", resetting to default.");
+ + "\", resetting to default.");
}
break;
default:
@@ -1107,12 +1090,12 @@ public final class Presenter {
break;
}
}
-
+
if (this.view.getPresenter() != null) {
this.updateView();
}
}
-
+
/**
* @return a message showing how much stuff has been loaded
* @since 2024-08-22
@@ -1120,30 +1103,36 @@ public final class Presenter {
*/
private String loadStatMsg() {
return this.getLocalizedText("load_stat_msg")
- .replace("[u]", Integer.toString(
- this.database.unitMapPrefixless(false).size()))
- .replace("[un]", Integer.toString(
- this.database.unitMapPrefixless(true).size()))
- .replace("[b]", Long.toString(this.database.unitMapPrefixless(false)
- .values().stream().filter(IS_FULL_BASE).count()))
- .replace("[p]", Integer.toString(this.database.prefixMap(false).size()))
- .replace("[pn]", Integer.toString(this.database.prefixMap(true).size()))
+ .replace("[u]",
+ Integer.toString(
+ this.database.unitMapPrefixless(false).size()))
+ .replace("[un]",
+ Integer
+ .toString(this.database.unitMapPrefixless(true).size()))
+ .replace("[b]",
+ Long.toString(this.database.unitMapPrefixless(false).values()
+ .stream().filter(IS_FULL_BASE).count()))
+ .replace("[p]",
+ Integer.toString(this.database.prefixMap(false).size()))
+ .replace("[pn]",
+ Integer.toString(this.database.prefixMap(true).size()))
.replace("[s]", Integer.toString(this.database.unitSetMap().size()))
- .replace("[d]", Integer.toString(this.database.dimensionMap().size()));
+ .replace("[d]",
+ Integer.toString(this.database.dimensionMap().size()));
}
-
+
/**
* @return true iff the One-Way Conversion feature is available (views that
* show units as a list will have metric units removed from the From
* unit list and imperial/USC units removed from the To unit list)
- *
+ *
* @since 2022-03-30
* @since v0.4.0
*/
public boolean oneWayConversionEnabled() {
return this.oneWayConversionEnabled;
}
-
+
/**
* Completes creation of the presenter. This part of the initialization
* depends on the view's functions, so it cannot be run if the components
@@ -1158,14 +1147,13 @@ public final class Presenter {
final var ucview = (UnitConversionView) this.view;
ucview.setDimensionNames(this.database.dimensionMap().keySet());
}
-
+
this.updateView();
this.view.updateText();
}
-
+
void prefixSelected() {
- final var selectedPrefixName = this.view
- .getViewedPrefixName();
+ final var selectedPrefixName = this.view.getViewedPrefixName();
final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
.map(name -> this.database.containsPrefixName(name)
? this.database.getPrefix(name)
@@ -1174,7 +1162,26 @@ public final class Presenter {
.ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(),
String.valueOf(prefix.getMultiplier())));
}
-
+
+ /**
+ * Clears then reloads all unit, prefix, dimension and exception data.
+ */
+ public void reloadData() {
+ this.database.clear();
+ this.metricExceptions.clear();
+ addDefaults(this.database);
+
+ if (this.useDefaultDatafiles) {
+ this.loadDefaultData();
+ }
+
+ this.customUnitFiles.forEach(
+ path -> this.handleLoadErrors(this.database.loadUnitsFile(path)));
+ this.customDimensionFiles.forEach(path -> this
+ .handleLoadErrors(this.database.loadDimensionFile(path)));
+ this.customExceptionFiles.forEach(this::loadExceptionFile);
+ }
+
/**
* Saves the presenter's current settings to the config file, creating it if
* it doesn't exist.
@@ -1192,10 +1199,10 @@ public final class Presenter {
return false;
}
}
-
+
return this.writeSettings(CONFIG_FILE);
}
-
+
private void setDisplayRuleFromString(String ruleString) {
final var tokens = ruleString.split(" ");
switch (tokens[0]) {
@@ -1216,7 +1223,7 @@ public final class Presenter {
break;
}
}
-
+
/**
* @param numberDisplayRule the new rule that will be used by this presenter
* to convert numbers into strings
@@ -1227,7 +1234,7 @@ public final class Presenter {
Function<UncertainDouble, String> numberDisplayRule) {
this.numberDisplayRule = numberDisplayRule;
}
-
+
/**
* @param numberParsingRule the new rule that will be used by this presenter
* to convert strings into numbers
@@ -1239,7 +1246,7 @@ public final class Presenter {
Function<String, UncertainDouble> numberParsingRule) {
this.numberParsingRule = numberParsingRule;
}
-
+
/**
* @param oneWayConversionEnabled whether not one-way conversion should be
* enabled
@@ -1251,7 +1258,7 @@ public final class Presenter {
this.oneWayConversionEnabled = oneWayConversionEnabled;
this.updateView();
}
-
+
/**
* @param prefixRepetitionRule the rule that determines whether a set of
* prefixes is valid
@@ -1263,7 +1270,7 @@ public final class Presenter {
this.prefixRepetitionRule = prefixRepetitionRule;
this.database.setPrefixRepetitionRule(prefixRepetitionRule);
}
-
+
/**
* @param searchRule A rule that accepts a prefixless name-unit pair and
* returns a map mapping names to prefixed versions of that
@@ -1276,7 +1283,7 @@ public final class Presenter {
Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
this.searchRule = searchRule;
}
-
+
private void setSearchRuleFromString(String ruleString) {
switch (ruleString) {
case "NO_PREFIXES":
@@ -1294,7 +1301,7 @@ public final class Presenter {
ruleString);
}
}
-
+
/**
* @param showDuplicateUnits whether or not duplicate units should be shown
* @since 2022-03-30
@@ -1304,7 +1311,7 @@ public final class Presenter {
this.showDuplicates = showDuplicateUnits;
this.updateView();
}
-
+
private List<Map.Entry<String, String>> settingsFromFile(Path settingsFile) {
try (var lines = Files.lines(settingsFile)) {
return lines.map(Presenter::withoutComments)
@@ -1318,17 +1325,18 @@ public final class Presenter {
}
/**
- * Sets whether or not the default datafiles will be loaded.
- * This method automatically updates the view's units.
+ * Sets whether or not the default datafiles will be loaded. This method
+ * automatically updates the view's units.
*
- * @param useDefaultDatafiles whether or not default datafiles should be loaded
+ * @param useDefaultDatafiles whether or not default datafiles should be
+ * loaded
*/
public void setUseDefaultDatafiles(boolean useDefaultDatafiles) {
this.useDefaultDatafiles = useDefaultDatafiles;
this.reloadData();
this.updateView();
}
-
+
/**
* Sets the user's locale, updating the view.
*
@@ -1338,7 +1346,7 @@ public final class Presenter {
this.userLocale = userLocale;
this.view.updateText();
}
-
+
/**
* Shows a unit in the unit viewer
*
@@ -1355,7 +1363,7 @@ public final class Presenter {
final var unitType = UnitType.getType(u, this::isSemiMetric);
this.view.showUnit(nameSymbol, definition, dimensionString, unitType);
}
-
+
/**
* Runs whenever a unit name is selected in the unit viewer. Gets the
* description of a unit and displays it.
@@ -1372,7 +1380,7 @@ public final class Presenter {
: null);
selectedUnit.ifPresent(this::showUnit);
}
-
+
/**
* Updates the view's From and To units, if it has some
*
@@ -1383,20 +1391,20 @@ public final class Presenter {
if (this.view instanceof UnitConversionView) {
final var ucview = (UnitConversionView) this.view;
final var selectedDimensionName = ucview.getSelectedDimensionName();
-
+
// load units & prefixes into viewers
this.view.setViewableUnitNames(
this.database.unitMapPrefixless(this.showDuplicates).keySet());
this.view.setViewablePrefixNames(
this.database.prefixMap(this.showDuplicates).keySet());
-
+
// get From and To units
var fromUnits = this.database.unitMapPrefixless(this.showDuplicates)
.entrySet().stream();
var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
.entrySet().stream();
var unitSets = this.database.unitSetMap().entrySet().stream();
-
+
// filter by dimension, if one is selected
if (selectedDimensionName.isPresent()) {
final var viewDimension = this.database
@@ -1408,7 +1416,7 @@ public final class Presenter {
unitSets = unitSets.filter(us -> viewDimension
.equals(us.getValue().get(0).getDimension()));
}
-
+
// filter by unit type, if desired
if (this.oneWayConversionEnabled) {
fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(),
@@ -1419,7 +1427,7 @@ public final class Presenter {
unitSets = unitSets
.filter(us -> this.metricExceptions.contains(us.getKey()));
}
-
+
// set unit names
ucview.setFromUnitNames(fromUnits.flatMap(this::applySearchRule)
.map(Map.Entry::getKey).collect(Collectors.toSet()));
@@ -1438,7 +1446,7 @@ public final class Presenter {
public boolean usingDefaultDatafiles() {
return this.useDefaultDatafiles;
}
-
+
/**
* @param message message to add
* @param args string formatting arguments for message
@@ -1451,7 +1459,7 @@ public final class Presenter {
return new AssertionError("View Programming Error (from " + this.view
+ "): " + String.format(message, args));
}
-
+
/**
* Saves the presenter's settings to the user settings file.
*
@@ -1471,8 +1479,8 @@ public final class Presenter {
String.format("include_duplicates=%s\n", this.showDuplicates));
writer.write(String.format("search_prefix_rule=%s\n",
searchRuleToString(this.searchRule)));
- writer.write(
- String.format("use_default_datafiles=%s\n", this.useDefaultDatafiles));
+ writer.write(String.format("use_default_datafiles=%s\n",
+ this.useDefaultDatafiles));
writer.write(String.format("locale=%s\n", this.userLocale));
return true;
} catch (final IOException e) {