Util Custom Config

Discussion in 'Resources' started by PhantomUnicorns, Nov 3, 2016.

Thread Status:
Not open for further replies.
  1. Offline

    PhantomUnicorns

    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"!
     
    Last edited: Nov 5, 2016
    ChipDev likes this.
  2. Offline

    Zombie_Striker

    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.
     
  3. Offline

    PhantomUnicorns

    @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
     
  4. Offline

    I Al Istannen

    @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 :p 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.
     
  5. Offline

    PhantomUnicorns

    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.
     
  6. Offline

    I Al Istannen

    @PhantomUnicorns
    I would have proposed something like this:
    Code (open)

    Code:java
    1. import java.io.IOException;
    2. import java.nio.charset.StandardCharsets;
    3. import java.nio.file.Files;
    4. import java.nio.file.Path;
    5. import java.nio.file.Paths;
    6. import java.nio.file.StandardOpenOption;
    7. import java.util.Collection;
    8. import java.util.HashMap;
    9. import java.util.List;
    10. import java.util.Map;
    11. import java.util.Map.Entry;
    12. import java.util.Objects;
    13. import java.util.StringJoiner;
    14. import java.util.function.Function;
    15. import java.util.stream.Collectors;
    16.  
    17. /**
    18. * A (probably useless) custom config
    19. */
    20. public class CustomConfig {
    21.  
    22. private final static String SPLIT_SEQUENCE = "!:!";
    23.  
    24. private Map<String, String> values = new HashMap<>();
    25. private Path saveFile;
    26.  
    27. /**
    28.   * @param saveFile The file to load it from and save it to
    29.   */
    30. public CustomConfig(Path saveFile) {
    31. Objects.requireNonNull(saveFile, "saveFile can not be null!");
    32.  
    33. this.saveFile = saveFile;
    34.  
    35. if (Files.exists(saveFile)) {
    36. load();
    37. }
    38. }
    39.  
    40. /**
    41.   * @param saveFile The file to load it from and save it to
    42.   * @param defaultValues The default values to load too
    43.   */
    44. public CustomConfig(Path saveFile, Map<String, String> defaultValues) {
    45. this(saveFile);
    46.  
    47. Objects.requireNonNull(defaultValues, "defaultValues can not be null!");
    48.  
    49. for (Entry<String, String> entry : defaultValues.entrySet()) {
    50. values.putIfAbsent(entry.getKey(), entry.getValue());
    51. }
    52. }
    53.  
    54. /**
    55.   * Returns the value for an identifier from the in memory representation
    56.   *
    57.   * @param identifier The identifier to get the value for
    58.   * @return The value. May be null
    59.   */
    60. public String getValue(String identifier) {
    61. return values.get(identifier);
    62. }
    63.  
    64. /**
    65.   * Sets a value
    66.   *
    67.   * @param identifier The identifier to set the value for
    68.   * @param value The value to save
    69.   * @throws IllegalArgumentException if the Identifier contains the split
    70.   * sequence ('{@value SPLIT_SEQUENCE}')
    71.   * @return The value that was associated with it before. May be null.
    72.   */
    73. public String setValue(String identifier, String value) {
    74. Objects.requireNonNull(identifier, "identifier can not be null!");
    75. Objects.requireNonNull(value, "value can not be null!");
    76.  
    77. if (identifier.contains(SPLIT_SEQUENCE)) {
    78. throw new IllegalArgumentException("Identifier contains Split sequence: '" + SPLIT_SEQUENCE + "'");
    79. }
    80.  
    81. return values.put(identifier, value);
    82. }
    83.  
    84. /**
    85.   * Saves the config
    86.   *
    87.   * @param saveFile The file to save to
    88.   * @throws IOException Thrown if an error occurs while saving
    89.   */
    90. public void save(Path saveFile) throws IOException {
    91. Objects.requireNonNull(saveFile, "saveFile can not be null!");
    92.  
    93. Function<Entry<String, String>, String> combinerFunction = entry -> entry.getKey().replace("!:!", "<ESCAPE SEQUENCE FORBIDDEN>")
    94. + "!:!" + entry.getValue();
    95.  
    96. Collection<String> lines = values.entrySet().stream().map(combinerFunction).collect(Collectors.toList());
    97.  
    98. Files.write(saveFile, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
    99. }
    100.  
    101. /**
    102.   * Saves the config to the default save file.
    103.   * <p>
    104.   * Prints errors
    105.   *
    106.   * @return False if an error occurred.
    107.   */
    108. public boolean save() {
    109. try {
    110. save(saveFile);
    111. return true;
    112. } catch (IOException e) {
    113. e.printStackTrace();
    114. return false;
    115. }
    116. }
    117.  
    118. /**
    119.   * Loads the file
    120.   */
    121. private void load() {
    122. try {
    123. List<String> readAllLines = Files.readAllLines(saveFile, StandardCharsets.UTF_8);
    124.  
    125. for (int i = 0; i < readAllLines.size(); i++) {
    126. String line = readAllLines.get(i);
    127. String[] splitted = line.split(SPLIT_SEQUENCE);
    128. if (splitted.length < 2) {
    129. throw new IllegalArgumentException("Line not valid. Split sequence was not found in line " + i);
    130. }
    131.  
    132. StringJoiner joiner = new StringJoiner(SPLIT_SEQUENCE);
    133. for (int j = 1; j < splitted.length; j++) {
    134. joiner.add(splitted[j]);
    135. }
    136.  
    137. values.put(splitted[0], joiner.toString());
    138. }
    139. } catch (IOException e) {
    140. e.printStackTrace();
    141. }
    142. }
    143.  
    144. public static void main(String[] args) {
    145. Map<String, String> defaultValues = new HashMap<>();
    146. defaultValues.put("aKey", "aValue");
    147.  
    148. CustomConfig customConfig = new CustomConfig(Paths.get("S:", "Minecraft", "Test", "testConfig.txt"), defaultValues);
    149.  
    150. System.out.println("TestKey: " + customConfig.getValue("testKey"));
    151. System.out.println("aKey: " + customConfig.getValue("aKey"));
    152.  
    153. customConfig.setValue("testKey", "!:!testValue!:!: " + System.currentTimeMillis());
    154. customConfig.save();
    155. }
    156. }



    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.
    1. Declare them as private
    2. Declare them as "List" unless you need the methods of the concrete class (ArrayList in this case.)
      Hint: You probably don't :)
    3. 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
    1. BufferedReader reader = null;
    2. try {
    3. reader = new BufferedReader(new FileReader(new File("")));
    4.  
    5. String tmp;
    6. while((tmp = reader.readLine()) != null) {
    7. // do something
    8. }
    9.  
    10. } catch (IOException e) {
    11. e.printStackTrace();
    12. } finally {
    13. if(reader != null) {
    14. try {
    15. reader.close();
    16. } catch (IOException e) {
    17. e.printStackTrace();
    18. }
    19. }
    20. }



    You do not need to write it though, which is a plus :p


    Not quite sure I understand that :/


    No problem :) Here to help, you know ;)

    Have a nice day!
     
  7. Offline

    PhantomUnicorns

    @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
     
    Last edited: Feb 27, 2017
  8. Offline

    PhantomUnicorns

    DELETE THIS
     
Thread Status:
Not open for further replies.

Share This Page