-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Use java.nio.file for file/path handling #750 #866
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request a review from me when ready.
c4ef491
to
ec1e15d
Compare
v1@vivekscl submitted v1 for review.
Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/866/1/head:BRANCHNAME where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor codebase to use java.nio.file for file/path handling String operations are used for the construction of file paths. Also, many APIs in the codebase take in String arguments as file paths. This method of file/path handling is error prone.
Error prone because....
Let's refactor the codebase to use java.nio.file for file/path handling.
So, how does the usage of java.nio.file
improve things?
Besides java.nio.file
providing a platform-independent way to perform operations on paths, the usage of Path
instead of String
all over our code base will allow us to properly separate the concerns of parsing/stringifying paths and manipulating paths.
This can be compared to how we model our address book using domain objects (e.g. Person
, AddressBook
) instead of using primitive data types. Conversion between the domain objects and strings/pixels on the screen is done explicitly at specific points of the program, in the controller, view and storage layers.
Using java.io.File
will also yield the same benefits, except that it has been superceded by java.nio.file
.
Since Path
is meant to replace File
, there should probably be a commit before or after this commit to convert all usages of File
in the code base to Path
, especially in FileUtil
, XmlUtil
and JsonUtil
. Yes, some conversions to File
may be needed when using legacy APIs, but that should only be done at at the boundary of the legacy API itself.
@@ -116,13 +118,13 @@ private void initLogging(Config config) { | |||
*/ | |||
protected Config initConfig(String configFilePath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be taking in a Path
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this method, there's a possibility that the MainApp#getApplicationParameter(String)
returns a null String and causes the Paths.get(String)
method to throw a NullPointerException
. So I didn't add it in this case. A workaround would be to add a null check before returning the path but that would be more complicated than just returning a String right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, putting aside the null check for the moment...
Would it be right for the application to crash with an InvalidPathException
if the user inputs an invalid path?
At the boundaries between user input and the application you need to do a proper validation check, so yes you do need the complexity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh what I meant is that, isn't there already a check where if the String is null, the default config file path will be used instead? So in this case, it's redundant to add an extra null check right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is redundant. But you'll need to balance it against the "handle errors properly" requirement that I mentioned up thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I understand now. Will make the changes accordingly. Thanks for clearing up my doubts! :)
requireNonNull(addressBook); | ||
requireNonNull(filePath); | ||
|
||
File file = new File(filePath); | ||
File file = filePath.toFile(); | ||
FileUtil.createIfMissing(file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FileUtil
methods should also be modified to take in Paths as well?
@@ -18,7 +19,7 @@ | |||
|
|||
public class ConfigUtilTest { | |||
|
|||
private static final String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/ConfigUtilTest/"); | |||
private static final Path TEST_DATA_FOLDER = Paths.get(".", "src", "test", "data", "ConfigUtilTest"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial "."
is unnecessary, can remove it.
import seedu.address.model.UserPrefs; | ||
|
||
public class JsonUserPrefsStorageTest { | ||
|
||
private static final String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/JsonUserPrefsStorageTest/"); | ||
private static final Path TEST_DATA_FOLDER = Paths.get(".", "src", "test", "data", "JsonUserPrefsStorageTest"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can remove the initial "."
import seedu.address.model.AddressBook; | ||
import seedu.address.model.ReadOnlyAddressBook; | ||
|
||
public class XmlAddressBookStorageTest { | ||
private static final String TEST_DATA_FOLDER = FileUtil.getPath("./src/test/data/XmlAddressBookStorageTest/"); | ||
private static final Path TEST_DATA_FOLDER = Paths.get(".", "src", "test", "data", "XmlAddressBookStorageTest"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can remove the initial "."
return config; | ||
} | ||
|
||
private Optional<Config> read(String configFileInTestDataFolder) throws DataConversionException { | ||
String configFilePath = addToTestDataPathIfNotNull(configFileInTestDataFolder); | ||
private Optional<Config> read(Path configFileInTestDataFolder) throws DataConversionException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a test method, it should be fine to either leave it alone taking in a String
, or add an overload that takes in a String
, so that we don't need to spam Paths.get()
in all the tests. Being a test method, it is fine to get a InvalidPathException
since it will fail the test anyway.
Same goes for all the other test methods.
273f993
to
fb665f3
Compare
v2@vivekscl submitted v2 for review.
(📚 Archive) (📈 Interdiff between v1 and v2) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/866/2/head:BRANCHNAME where |
On a side note, I tried to place the conversion of all usages of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments.
Do another sweep and see if you can use the java.nio.file.Files
API instead of the java.io.File
API. toFile()
should be used only when absolutely required.
return applicationParameters.get(parameterName); | ||
return applicationParameters.get(parameterName) == null | ||
? Optional.empty() | ||
: Optional.of(Paths.get(applicationParameters.get(parameterName))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this still cause the program to crash if the parameter is an invalid path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After trying to pass an invalid path to the master branch of this repo, the same error pops up and then the app starts, so it seems like this behaviour was already there from the beginning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a NoSuchFileException
. Paths.get
only throws an InvalidPathException
.
@@ -75,9 +77,11 @@ public void init() throws Exception { | |||
initEventsCenter(); | |||
} | |||
|
|||
private String getApplicationParameter(String parameterName) { | |||
private Optional<Path> getApplicationParameter(String parameterName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh I wouldn't expect a method called getApplicationParameter
to return a path. What happens when we want to support non-path parameters as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my bad. Should have studied the method more carefully 😅 .
@@ -114,15 +118,15 @@ private void initLogging(Config config) { | |||
* The default file path {@code Config#DEFAULT_CONFIG_FILE} will be used instead | |||
* if {@code configFilePath} is null. | |||
*/ | |||
protected Config initConfig(String configFilePath) { | |||
protected Config initConfig(Optional<Path> configFilePath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use Optional
for optional arguments anymore. Use null
instead.
public static void createDirs(File dir) throws IOException { | ||
if (!dir.exists() && !dir.mkdirs()) { | ||
throw new IOException("Failed to make directories of " + dir.getName()); | ||
public static void createDirs(Path dir) throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this whole function can be removed and replaced with Files.createDirectories()
|
||
return file.createNewFile(); | ||
return filePath.toFile().createNewFile(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it can be replaced with Files.createFile()
fb665f3
to
52b8383
Compare
v3@vivekscl submitted v3 for review.
(📚 Archive) (📈 Interdiff between v2 and v3) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/866/3/head:BRANCHNAME where |
config = initConfig(getApplicationParameter("config")); | ||
config = getApplicationParameter("config") != null | ||
? initConfig(Paths.get(getApplicationParameter("config"))) | ||
: initConfig(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, this is smushing too many things into one line.
At least, I'd expect something like:
Path configPath = getApplicationParameter("config") != null ? Path.get(getApplicationParameter("config")) : null;
config = initConfig(configPath);
(Operations on the configPath
is separated from the config
)
However, this code still does not handle the possible InvalidPathException
from Paths.get()
. You need something like:
String configPathParameter = getApplicationParameter("config");
if (configPathParameter != null && !isValidPath(configPathParameter)) {
// Handle error
// Considering the the rest of MainApp's code thinks its okay to not report errors to users,
// I'm inclined to using something like:
logger.warning("Invalid config path " + configPathParameter + ". Using default config path.");
configPathParameter = null;
}
Path configPath = configPathParameter != null ? Path.get(configPathParameter) : null;
config = initConfig(configPath);
Yeah, Java sucks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this, I tried testing if the InvalidPathException
would be thrown using invalid paths as arguments, but it always returns true the isValidPath
check on Linux. This SO post describes the same issue with Linux return true for any kind of path. Is it ok if I proceed with this check since it could work on Windows and MacOS?
private static boolean isValidPath(String path) {
try {
Paths.get(path);
} catch (InvalidPathException ipe) {
return false;
}
return true;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This SO post describes the same issue with Linux return true for any kind of path.
Seems like it has been fixed already.
https://ideone.com/4DONru
Is it ok if I proceed with this check since it could work on Windows and MacOS?
Yeah, of course. It's all about the API contract. If Paths.get
claims that it could throw an InvalidPathException
, then that needs to be handled, whether it does actually throw one or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[1/2]
@@ -62,14 +62,15 @@ public static void createParentDirsOfFile(File file) throws IOException { | |||
|
|||
if (parentDir != null) { | |||
createDirs(parentDir); | |||
Files.createFile(file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, why is this needed?
@@ -88,6 +89,8 @@ public static void writeToFile(File file, String content) throws IOException { | |||
public static String getPath(String pathWithForwardSlash) { | |||
checkArgument(pathWithForwardSlash.contains("/")); | |||
return pathWithForwardSlash.replace("/", File.separator); | |||
public static void writeToFile(Path filePath, String content) throws IOException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh? No closing }
? I'm surprised this compiles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, this compiles on the last commit because the problem is fixed in [2/2]. Do ensure that the project compiles + passes all tests on every commit.
@@ -18,53 +19,53 @@ | |||
/** | |||
* Returns the xml data in the file as an object of the specified type. | |||
* | |||
* @param file Points to a valid xml file containing data that match the {@code classToConvert}. | |||
* @param filePath Points to a valid xml file path containing data that match the {@code classToConvert}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it is "Points to a valid xml file", I guess there's no need to change the javadoc since a Path does also "Point" to a file?
} | ||
|
||
/** | ||
* Saves the data in the file in xml format. | ||
* | ||
* @param file Points to a valid xml file containing data that match the {@code classToConvert}. | ||
* Cannot be null. | ||
* @param filePath Points to a valid xml file path containing data that match the {@code classToConvert}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it is "Points to a valid xml file", I guess there's no need to change the javadoc since a Path does also "Point" to a file?
@@ -75,12 +76,12 @@ private Config getTypicalConfig() { | |||
Config config = new Config(); | |||
config.setAppTitle("Typical App Title"); | |||
config.setLogLevel(Level.INFO); | |||
config.setUserPrefsFilePath("C:\\preferences.json"); | |||
config.setUserPrefsFilePath(Paths.get("C:\\preferences.json")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed that this string is technically just C:\preferences.json
:-P
Anyway, even if it's C:\\preferences.json
, such a Path
only makes sense on Windows, and doesn't make sense for a test case which is meant to be run on different OS's.
Maybe add a commit on top to change this into a platform-independent path. Maybe just preferences.json
will do.
File manyPersonsFile = new File(TEST_DATA_FOLDER + "manyPersons.xml"); | ||
FileUtil.createFile(manyPersonsFile); | ||
Path manyPersonsFile = TEST_DATA_FOLDER.resolve("manyPersons.xml"); | ||
Files.createFile(manyPersonsFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this fail if a file called manyPersons.xml
happens to exist in src/test/data/sandbox
? (if the cleanup fails for whatever reason previously?)
Maybe just leave it as FileUtil.createFile(manyPersonFile)
?
@@ -122,7 +123,7 @@ public void saveDataToFile_missingFile_fileNotFoundException() throws Exception | |||
|
|||
@Test | |||
public void saveDataToFile_validFile_dataSaved() throws Exception { | |||
TEMP_FILE.createNewFile(); | |||
TEMP_FILE.toFile().createNewFile(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Files.createFile()
looked like it could work here, except that it throws an exception if the file already exists.
Maybe use FileUtil.createFile()
?
There are many usages of java.io.File throughout the codebase. Since Path is meant to replace File as mentioned in the Oracle Documentation on Legacy File I/O Code, let's convert all usages of File in the codebase to Path. Oracle Documentation on Legacy File I/O Code: https://docs.oracle.com/javase/tutorial/essential/io/legacy.html
dc624a1
to
f46c111
Compare
v13@vivekscl submitted v13 for review.
(📚 Archive) (📈 Interdiff between v12 and v13) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/866/13/head:BRANCHNAME where |
Merge commit message:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[v13 2/6]
The handling of application parameters is done in the MainApp. This decreases cohesiveness as handling and parsing of application parameters should be done separately and not in the MainApp as command-line parsing is at a much lower level compared to the high level setup of the application architecture. This also allows the parsing of application parameters to be tested independently. Let's extract the handling of application parameters from MainApp into the AppParameters for future parsing of application parameters. Refactor codebase to use java.nio.file for file/path handling --- String operations are used for the construction of file paths. Also, many APIs in the codebase take in String arguments as file paths. This method of file/path handling is error prone as creating paths using string concatenation can lead to the creation of invalid paths. Furthermore, checking if two paths resolve to the same file using string equality is incorrect as it only tests if the strings are equal. Let's refactor the codebase to use java.nio.file for file/path handling as it provides a safe and platform-independent way to perform operations on paths and properly separate the concerns of parsing/stringifying paths and manipulating paths. More information on why String operations are error-prone: http://twistedoakstudios.com/blog/Post4872_dont-treat-paths-like-strings
Commit message contains the messages for both [2/6] and [3/6].
Other than that, no other major issues I can find.
The handling of application parameters is done in the MainApp. This decreases cohesiveness as handling and parsing of application parameters should be done separately and not in the MainApp as command-line parsing is at a much lower level compared to the high level setup of the application architecture. This also allows the parsing of application parameters to be tested independently. Let's extract the handling of application parameters from MainApp into the AppParameters for future parsing of application parameters.
String operations are used for the construction of file paths. Also, many APIs in the codebase take in String arguments as file paths. This method of file/path handling is error prone as creating paths using string concatenation can lead to the creation of invalid paths. Furthermore, checking if two paths resolve to the same file using string equality is incorrect as it only tests if the strings are equal. Let's refactor the codebase to use java.nio.file for file/path handling as it provides a safe and platform-independent way to perform operations on paths and properly separate the concerns of parsing/stringifying paths and manipulating paths. More information on why String operations are error-prone: http://twistedoakstudios.com/blog/Post4872_dont-treat-paths-like-strings
The return value of FileUtil#createFile(Path) is unused. For code quality purposes, methods with unused return types should have void as a return type instead. Let's use void for the return type of FileUtil#createFile(Path).
After refactoring the codebase to use java.nio.file for file/path handling, several methods became unused. Following that, FileUtilTest would be testing unused methods. For code quality purposes, unused methods and tests should be removed. Let's remove unused methods in FileUtil and FileUtilTest.
ConfigUtilTest#getTypicalConfig() uses a path for the user preference file that is only meant for the Windows operating system. This should not be the case as AddressBook is meant to run on all platforms. Let's update ConfigUtilTest#getTypicalConfig() to use a platform-independent path for the user preference file. Following this, ExtraValuesConfig.json and TypicalConfig.json should be updated to reflect this change as well.
@yamgent Oh that's bad, thanks for the sharp eyes. |
f46c111
to
50771f4
Compare
@yamgent thanks for catching that! Really bad mistake on my part 😅 . |
v14@vivekscl submitted v14 for review.
(📚 Archive) (📈 Interdiff between v13 and v14) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/866/14/head:BRANCHNAME where |
@vivekscl you can last call review this. |
@yamgent I guess this is ready to merge? 😄 |
Fixes #750
Fixes #554
Things to take note of
PersonListPanelTest
is rather fickle. It fails sometimes, while passes in others. As such,TEST_TIMEOUT
will be set to 12000 as a temporary measure until PersonListPanelTest: fix flaky test #863 is merged.String
arguments for file names forClass#getResource(String)
, the file names were changed to be of typePath
and then converted to typeString
when needed. Not entirely sure if this is needed, but since usingStrings
for file names is still error prone I refactored it anyway. Do let me know if this shouldn't be the case.Issues to raise
IOException
even though it is never thrown in the first place. One example isJsonUserPrefsStorage#readUserPrefs(Path)
.FileUtil#createFile(File)
.AddressBookSystemTest
which could be made to be private. Not sure if this was intended for future use though.UserPrefs#getAddressBookName
is unused.