This util is just one class with a few amount of lines, but it is extremely helpful for those wanting a database, config, language files and different types of files in their plugin. This plugin allows you to create and read in a text file with very optimized settings. Here is the class itself: Code: public class CConfig { public ArrayList<String> identifiers = new ArrayList<>(); public ArrayList<String> values = new ArrayList<>(); public static void create(JavaPlugin jp, String cconfigName, String... defaultValues) { String ccconfigName = "plugins" + File.separator + jp.getName() + File.separator + cconfigName + ".txt"; if (!new File("plugins" + File.separator + jp.getName()).exists()) { new File("plugins" + File.separator + jp.getName()).mkdirs(); } if (!new File(ccconfigName).exists()) { if (defaultValues != null && defaultValues.length > 0) { try { BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(ccconfigName))); for (int i = 0; i < defaultValues.length; i++) { if (defaultValues[i] != null && defaultValues[i].length() > 0) { writer.write(defaultValues[i]); } if (defaultValues[i] != null) { writer.newLine(); } } writer.close(); } catch (IOException e) { // e.printStackTrace(); } } } } public static String getValue(JavaPlugin jp, String cconfigName, String identifier) { String ccconfigName = "plugins" + File.separator + jp.getName() + File.separator + cconfigName + ".txt"; try { BufferedReader bufferReader = new BufferedReader(new FileReader(ccconfigName)); String line; while ((line = bufferReader.readLine()) != null) { String[] attributes = line.split(":"); if (attributes[0].equals(identifier)) { StringBuilder rest = new StringBuilder(attributes[1]); for (int i = 2; i < attributes.length; i++) { rest.append(":" + attributes[i]); } bufferReader.close(); return rest.toString(); } } bufferReader.close(); } catch (IOException e) { // e.printStackTrace(); } return "null"; } public static CConfig getValues(JavaPlugin jp, String cconfigName) { ArrayList<String> linesI = new ArrayList<String>(); ArrayList<String> linesA = new ArrayList<String>(); String ccconfigName = "plugins" + File.separator + jp.getName() + File.separator + cconfigName + ".txt"; try { BufferedReader bufferReader = new BufferedReader(new FileReader(ccconfigName)); String line; while ((line = bufferReader.readLine()) != null) { String[] attributes = line.split(":"); StringBuilder rest = new StringBuilder(attributes[1]); for (int i = 2; i < attributes.length; i++) { rest.append(":" + attributes[i]); } linesI.add(attributes[0]); linesA.add(rest.toString()); } bufferReader.close(); } catch (IOException e) { // e.printStackTrace(); } return new CConfig(linesI, linesA); } private CConfig(ArrayList<String> identifiers, ArrayList<String> values) { if (identifiers != null) { this.identifiers.addAll(identifiers); } if (values != null) { this.values.addAll(values); } } public String getValue(String identifier) { for (int i = 0; i < identifiers.size(); i++) { if (identifiers != null && identifiers.get(i) != null && identifiers.get(i).equals(identifier)) { if (values != null && values.size() > i) { return values.get(i); } } } return "null"; } } (if you want the exceptions to print remove the comment to make it code!) And here is a way to use this code: Code: CConfig.create(this, "Test", "SpawnLocal:world,0,0,0"); Bukkit.getConsoleSender().sendMessage(CConfig.getValue(this, "Test", "SpawnLocal")); // Or if you will get multiple values CConfig testFile = CConfig.getValues(this, "Test"); testFile.getValue("SpawnLocal"); testFile.getValue("SpawnLocal2"); // This will not update automatically like the other one! Which would create a file called "Test.txt" with "SpawnLocal:world,0,0,0" on its first line! Special thanks to "I Al Istannen"!
Instead of hardcoding these seperators, please use File.separator. Instead of splitting the string multiple times, why not split it once? Instead of ignoring these errors, you should be printing them.
@Zombie_Striker I personally, don't like printing exceptions (I test to see if it ever happens to me but it never really does in this case), I split it multiple times because I didn't want to waste a line with initilizing a variable (although I did this multiple times), sure I'll use File.seperator
@PhantomUnicorns You mean reading an entire file from disk every time you need some value is efficient? I wouldn't think that. Also @Zombie_Striker You can use "new File(File parent, String child)" to avoid using any seperator at all and let the File class deal with it. You check for nulls a bit excessively. That may be needed in Java due to the lack of a nicer on-null type, but if a developer passes null as default value, it may be nicer to just blow up and throw an NPE. Code: if (defaultValues[i] != null && defaultValues[i].length() > 0) { writer.write(defaultValues[i]); } if (defaultValues[i] != null && i != defaultValues.length - 1) { writer.newLine(); } The != null check here. And checking that it is > 0 is probably unneeded too, what if the default value is an empty String? And you check if the string is > 0 to write it, but you write a new line everytime? Code: BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(ccconfigName))) You should declare each variable in the-try-with-resource block on their own. See here. In some special cases it could leak the resources. Granted, that doesn't happen often, but the safeguard is easy. Code: writer.close(); The Try-with-resource statement you used is guaranteed to call close before the catch block is executed and after the try block has been left. This statement is entirely unneeded. Apart from that you would need to place that statement in a finally block to ensure it is closed correctly even if your try block throws an error. As said before, the try-with-resource removes the need to close it alltogether tough. Code: } catch (FileNotFoundException e) { //e.printStackTrace(); } catch (IOException e1) { //e1.printStackTrace(); } The two catch blocks are the same. Collapse them: "catch(FileNotFoundException | IOException e)" That variable name is too close the "cconfigName" as a parameter. Change that, it is confusing. Code: try { BufferedReader bufferReader = new BufferedReader(new FileReader(ccconfigName)); Use try-with-resource as you did in the write method. Due to what I said before the close at the end of the try block is NOT enough. You can create some nice resource leaks this way. Code: String line = bufferReader.readLine(); while (line != null) { There is a short way. Some think it is an abomination, but I like it Decide yourself: "while( (line = reader.readLine()) != null) {" Split once, as @Zombie_Striker said. Cache the value. Code: rest += ":" + line.split(":")[i]; String concatenation in a loop is bad. And slower than you can imagine. Use a StringBuilder here. There may be more I haven't pointed out. I don't quite see the need for it too. There is Files.writeAllLines(Path, Iterable<String>) and Files.lines(Path). Then just filter the stream returned by the Files.lines method for starting with the identifier and you have the exact same method in 3 lines.
To your first comment (about code), I don't check for the length twice because they might want an empty line. I should have used StringBuilder. And I'm not going to rename the variable, as it doesn't effect the code and I named it that for a reason. Other then that thank you for mentioning StringBuilder! (the double catch is throwing an error for me) And I provided both types of the try block situation for beginners to learn (or even experts) but to keep consistency, I'm going to change it (looking back it might confuse people). I don't see a Files.lines(Path), but I see Files.readLines(String, int) and looking into the source code of that it looks like it is around the same as mine. Also removing ether close throws an error for me, and could you put your comment into a spoiler? If there is any more comments, it would just be easier to scroll down. Thank you for your insight and time, it was very helpful and it actually opened some light on me! (Not sarcasm) and if you can't put it in a spoiler that is ok, I don't mind.
@PhantomUnicorns I would have proposed something like this: Code (Move your mouse to reveal the content) Code (open) Code (close) Code:java import java.io.IOException;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.StandardOpenOption;import java.util.Collection;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Map.Entry;import java.util.Objects;import java.util.StringJoiner;import java.util.function.Function;import java.util.stream.Collectors; /*** A (probably useless) custom config*/public class CustomConfig { private final static String SPLIT_SEQUENCE = "!:!"; private Map<String, String> values = new HashMap<>(); private Path saveFile; /** * @param saveFile The file to load it from and save it to */ public CustomConfig(Path saveFile) { Objects.requireNonNull(saveFile, "saveFile can not be null!"); this.saveFile = saveFile; if (Files.exists(saveFile)) { load(); } } /** * @param saveFile The file to load it from and save it to * @param defaultValues The default values to load too */ public CustomConfig(Path saveFile, Map<String, String> defaultValues) { this(saveFile); Objects.requireNonNull(defaultValues, "defaultValues can not be null!"); for (Entry<String, String> entry : defaultValues.entrySet()) { values.putIfAbsent(entry.getKey(), entry.getValue()); } } /** * Returns the value for an identifier from the in memory representation * * @param identifier The identifier to get the value for * @return The value. May be null */ public String getValue(String identifier) { return values.get(identifier); } /** * Sets a value * * @param identifier The identifier to set the value for * @param value The value to save * @throws IllegalArgumentException if the Identifier contains the split * sequence ('{@value SPLIT_SEQUENCE}') * @return The value that was associated with it before. May be null. */ public String setValue(String identifier, String value) { Objects.requireNonNull(identifier, "identifier can not be null!"); Objects.requireNonNull(value, "value can not be null!"); if (identifier.contains(SPLIT_SEQUENCE)) { throw new IllegalArgumentException("Identifier contains Split sequence: '" + SPLIT_SEQUENCE + "'"); } return values.put(identifier, value); } /** * Saves the config * * @param saveFile The file to save to * @throws IOException Thrown if an error occurs while saving */ public void save(Path saveFile) throws IOException { Objects.requireNonNull(saveFile, "saveFile can not be null!"); Function<Entry<String, String>, String> combinerFunction = entry -> entry.getKey().replace("!:!", "<ESCAPE SEQUENCE FORBIDDEN>") + "!:!" + entry.getValue(); Collection<String> lines = values.entrySet().stream().map(combinerFunction).collect(Collectors.toList()); Files.write(saveFile, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } /** * Saves the config to the default save file. * <p> * Prints errors * * @return False if an error occurred. */ public boolean save() { try { save(saveFile); return true; } catch (IOException e) { e.printStackTrace(); return false; } } /** * Loads the file */ private void load() { try { List<String> readAllLines = Files.readAllLines(saveFile, StandardCharsets.UTF_8); for (int i = 0; i < readAllLines.size(); i++) { String line = readAllLines.get(i); String[] splitted = line.split(SPLIT_SEQUENCE); if (splitted.length < 2) { throw new IllegalArgumentException("Line not valid. Split sequence was not found in line " + i); } StringJoiner joiner = new StringJoiner(SPLIT_SEQUENCE); for (int j = 1; j < splitted.length; j++) { joiner.add(splitted[j]); } values.put(splitted[0], joiner.toString()); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Map<String, String> defaultValues = new HashMap<>(); defaultValues.put("aKey", "aValue"); CustomConfig customConfig = new CustomConfig(Paths.get("S:", "Minecraft", "Test", "testConfig.txt"), defaultValues); System.out.println("TestKey: " + customConfig.getValue("testKey")); System.out.println("aKey: " + customConfig.getValue("aKey")); customConfig.setValue("testKey", "!:!testValue!:!: " + System.currentTimeMillis()); customConfig.save(); }} It does the same what your class may do, but it takes a different approach. Let me outline what it does. Outline: Code: + CustomConfig(Path) : CustomConfig + CustomConfig(Path,Map<String,String>) : CustomConfig + getValue(String) : String + setValue(String,String) : String + save(Path) : Void + save() : Boolean - load() : Void - SPLIT_SEQUENCE : String - values : Map<String, String> - saveFile : Path The constructors just set the "saveFile" variable the passed Path. One takes a Map with default values, which will be added to the Map, if they are not already in it. getValue retrieves the value from the Map. setValue puts the value in the map, if the key doesn't contain the SPLIT_SEQUENCE. You could have probably escaped that, but whatever save(Path) saves it to the given File, letting Files.write deal with the whole lower-lever writing stuff. load() is private (the '-' in front of it) and just reads the file (Files.readAllLines). Then it splits it, grabs the first as identifier and puts it back together. It would have also worked to get the substring of the line, starting at the identigier. That would have been more effifient, but I just thought of that now. SPLIT_SEQUENCE is the sequence between identifier and value values just holds the identifier and the assocciated value saveFile is the file to save to (using just save()) and the file to load from. I don't really see any need for this though, this is exactly what the ConfigurationAPI does, just less sophisticated. To your code: Code: String line = bufferReader.readLine(); while ((line = bufferReader.readLine()) != null) { Will skip the first line. You still not close the reader in a finally block AND you don't use try-with-resource. This may cause resource leaks, change it please. Code: this.identifiers = identifiers; Make a defensive copy: "this.identifiers = new ArrayList<>(identifiers)". This way the changes to the passed collection will not write through to your "identifiers" or "values" list, which could cause the two collections to get out of sync. You use two ArrayLists. Declare them as private Declare them as "List" unless you need the methods of the concrete class (ArrayList in this case.) Hint: You probably don't Use a nicer data structure. A Map<String, String> linking the identifier to the value is what you were looking for. Code: for (int i = 0; i < identifiers.size(); i++) { if (identifiers != null && identifiers.get(i).equals(identifier)) { if (values != null && values.size() > i) { return values.get(i); } } } Directly resulting out of 3. above: This runs in linear time [O(n)] and is a lot of boilerplate code. A Map reduces that whole method to "map.get(identifier)". It handles a null key (identifier) and everything for you, so you really just need that one line. Code: jp.getName() + "\\" A File.seperator (or better the Path API from Java 7 OR the new File(parent, child) constructor was what you were looking for Just slipped through there. Code: while ((line = bufferReader.readLine()) != null) { String[] attributes = line.split(":"); if (attributes[0].equals(identifier)) { StringBuilder rest = new StringBuilder(attributes[1]); for (int i = 2; i < attributes.length; i++) { rest.append(":" + attributes[i]); } bufferReader.close(); return rest.toString(); } } Use a finally block and never worry about closing it in the catch/try block. Or use the try-with-resource statement created for this exact reason. Or use Files.readAllLines to avoid dealing with it altogether. Code: for (int i = 0; i < defaultValues.length; i++) { An enhanced for loop is fine here: "for(String line : defaultValues)". Saves a lot of things to write and is not less efficient Code: new File("plugins" + File.separator + jp.getName()) Just use " newFile("plugins", jp.getName()) ". And shouldn't that be "jp.getDataFolder()"? It affects the code. As long as you know what the reason is in a few weeks (and it makes sense) all is okay though. It is best if other people can understand why you named it that way, or, more generally speaking, when they can infer what the variable means just from looking at the name. I don't understand that bit. Problem is, one is wrong. The one without the resource declared in the brackets WILL NOT properly close the reader in case of an error. A working old try block looks like this. You will want the try-with-resource, trust me. Code:java BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(new File(""))); String tmp; while((tmp = reader.readLine()) != null) { // do something } } catch (IOException e) { e.printStackTrace(); } finally { if(reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } You do not need to write it though, which is a plus Not quite sure I understand that :/ No problem Here to help, you know Have a nice day!
@I Al Istannen Would it be possible to close a buffered reader inside the finally block if there is an error? Or does the finally block not even get called if there is an error