.myml | A single class file wrapper for an improved (YAML-similar) syntax
// Created by Justis Root. Released into the public domain.
// https://gist.github.com/justisr
//
// Source is licensed for any use, provided that this copyright notice is retained.
// Modifications not expressly accepted by the author should be noted in the license of any forks.
// No warrenty for any purpose whatsoever is implied or expressed,
// and the author shall not be held liable for any losses, direct or indirect as a result of using this software.
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
/**
* A YAML-similar markup Language file wrapper
* @author Justis R
* @version 3.0.1
*/
public class MYMLFile {
private File file;
/**
* Create a MYMLFile wrapper for the file at the specified path with the specified name within the program's running folder.
* @param path from the program's running folder to the destination.
* Each parameter prior to the last being a folder.
*/
public MYMLFile(String... path) {
StringBuilder pathBuilder = new StringBuilder();
for (int i = 0; i < path.length; i++) pathBuilder.append(path[i] + File.separator);
file = new File(pathBuilder.toString());
reload();
}
/**
* Create a MYMLFile object for specified file.
* @param file for which to wrap the MYMLFile object.
*/
public MYMLFile(File file) {
this.file = file;
reload();
}
/**
* Gets the file from disk or generates one if it doesn't exist.
* Save all of the file's contents, all the paths, values, everything, to memory.
* Overwrites any and all existing contents of memory
* @throws IOException when the directory does not exist.
*/
public void reload() {
section = new Section(0);
try {
File direc = new File(file.getPath().replace(file.getName(), ""));
if (!direc.exists())
direc.mkdirs();
if (!file.exists())
file.createNewFile();
Scanner input = new Scanner(file);
List<String> header = new ArrayList<>();
while (input.hasNextLine())
if (section.nextLine(input.nextLine(), header))
header = new ArrayList<>();
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Save all the current contents of memory to disk.
* Overwrites any and all existing contents of the file
* @throws IOException when the directory does not exist.
*/
public void save() {
try {
FileWriter fw = new FileWriter(file, false);
for (String line : getContents())
fw.write(line + "\n");
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private Section section;
private static class Section {
private String[] header;
private final Map<String, Section> sections = new LinkedHashMap<>();
private int tab;
/**
* @param indent of the section being added
* @param head and comments of the section
*/
private Section(int indent, String... head) {
header = head;
tab = indent;
}
/**
* @param line to parse for addition into the header
* @param header of the section to be added
* @return true if the line was a section head, and a new section has been created, false if it was a commented line
*/
private boolean nextLine(String line, List<String> header) {
header.add(line);
if (isIndented(line, tab))
addSection(header.toArray(new String[header.size()]));
else if(!isCommented(line))
lastSection(fromTab(line)).addSection(header.toArray(new String[header.size()]));
else return false;
return true;
}
/**
* @param head of the section to be added
* @return the section that was added
*/
private Section addSection(String... head) {
Section sec = new Section(tab + 1, head);
sections.put(parseName(head[head.length - 1]), sec);
return sec;
}
/**
* @param tab the number of indents to read for the last section from
* @return the last section indented the number of times specified
*/
private Section lastSection(int tab) {
return tab <= this.tab ? this : sections.get(sections.keySet().toArray()[sections.size() - 1]).lastSection(tab);
}
/**
* @param value should the method return just the head value or the entire line?
* @return The head value, if boolean is true, the entire line if false
*/
private String getHead(boolean value) {
return value ? parseValue(header[header.length - 1]) : header[header.length - 1];
}
/**
* @param line to set the head to
* @param value is the line provided just the value, or the entire line? Sets accordingly.
*/
private void setHead(String line, boolean value) {
header[header.length - 1] = value ? header[header.length - 1].replaceFirst("(:.*)", ": ") + line : line;
}
/**
* @param string to read for comments
* @return true if the line is whitespace or commented out with a pound sign
*/
private static boolean isCommented(String string) {
return string.matches("( *| *#.*)");
}
/**
* @param string to read for indents
* @param i the number of indents the line must have
* @return true if the line is indented the number of times specified, false if it's indented more or less.
*/
private static boolean isIndented(String string, int i) {
return string.matches("(" + tab(i) + "[^ #].*)");
}
/**
* @param line to get the section name from
* @return the name of the section
*/
private static String parseName(String line) {
return line.trim().replaceFirst("(:.*)", "");
}
/**
* @param tab how many intents
* @param path the name of the section
* @param value the value of the line
* @return the combination of the provided parameters forming a section head
*/
private static String toHead(int tab, String path, Object value) {
return tab(tab) + path + ": " + value;
}
/**
* @param line to parse the value from
* @return the value of the line provided
*/
private static String parseValue(String line) {
return line.replaceFirst("(.*: )", "");
}
/**
* @param string to count the indents from
* @return how many times this string is indented
*/
private static int fromTab(String string) {
int i = 0;
while (string.startsWith(" ")) {
string = string.substring(2);
i++;
} return i;
}
/**
* @param indent how many times
* @return string indented the number of times provided
*/
private static String tab(int indent) {
StringBuilder builder = new StringBuilder();
for (int i = indent; i > 0; i--) builder.append(" ");
return builder.toString();
}
/**
* Replace the name of the specified section with a new one.
* <b>This will also move the section down to the bottom of whatever path branch it's on.</b>
* Creates a new section with the specified name along the path if the section does not exist.
* @param path to section rename
* @param name to rename the section to.
*/
private void rename(String[] path, String value) {
if (path.length < 2) {
if (!sections.containsKey(path[0])) addSection(toHead(tab, value, ""));
else {
Section sec = sections.get(path[0]);
sec.setHead(tab(tab) + sec.getHead(false).replaceFirst("(.*:)", value + ":"), false);
sections.remove(path[0]);
sections.put(value, sec);
}
} else {
if (sections.containsKey(path[0])) sections.get(path[0]).rename(split(path[1]), value);
else addSection(toHead(tab, path[0], "")).rename(split(path[1]), value);
}
}
/**
* Remove a section and all inner sections
* @param path to the section to remove
*/
private void remove(String[] path) {
if (path.length < 2) sections.remove(path[0]);
if (sections.get(path[0]) != null)
sections.get(path[0]).remove(split(path[1]));
}
/**
* Set the comments at the specified path
* @param path to set the comments for
* @param value to set as the comments
*/
private void setComments(String[] path, String[] value) {
Section sec = hardGet(path, "");
value[value.length - 1] = sec.getHead(false);
for (int i = 0; i < value.length - 1; i++)
value[i] = value[i].isEmpty() ? value[i] : tab(sec.tab - 1) + "# " + value[i];
sec.header = value;
}
/**
* Convert an object into a string value, and set that value at the desired path
*
* Converts object arrays into a List-like string value. Otherwise uses #toString() on the object.
* Add custom string generation for objects as needed.
*
* @param path to set the value at, ["path", "to.value"]
* @param value to set
*/
private void setValue(String[] path, Object value) {
StringBuilder builder = new StringBuilder(value.toString());
// Custom stringing of Object arrays.
if (value instanceof Object[]) {
builder.setLength(0);
builder.append("[");
Object[] o = (Object[]) value;
for (int i = 0; i < o.length; i++)
builder.append(o[i] + (i == o.length - 1 ? "]" : ", "));
}
hardGet(path, value).setHead(builder.toString(), true);
}
/**
* Get the comments of this section
* @return String[] of this section's comments
*/
private String[] getComments() {
String[] dest = new String[header.length - 1];
for (int i = 0; i < header.length - 1; i++) dest[i] = header[i].replaceFirst("(.*#)", "");
return dest;
}
/**
* Get the section at the specified path, or create one if it does not exist
* @param path to get the section from, or create a section at
* @param value to set as the path's value if the path does not exist.
* @return section at the specified path
*/
private Section hardGet(String[] path, Object value) {
if (path.length < 2) {
if (sections.containsKey(path[0])) return sections.get(path[0]);
else return addSection(toHead(tab, path[0], value));
} else {
if (sections.containsKey(path[0])) return sections.get(path[0]).hardGet(split(path[1]), value);
else return addSection(toHead(tab, path[0], "")).hardGet(split(path[1]), value);
}
}
/**
* Get the section at the specified path
* @param path to the section
* @return Section at the path, null if section does not exist.
*/
private Section get(String[] path) {
if (path.length < 2) return sections.get(path[0]);
if (sections.get(path[0]) == null) return null;
return sections.get(path[0]).get(split(path[1]));
}
/**
* Get the sections' path/value and comments, as well as all of the paths/values and comments of nested sections
* @param List to append the contents to.
* @return List of the section comments, the path/value as well as all nested sections.
*/
private List<String> getContents(List<String> contents) {
for (String head : header) contents.add(head);
for (Section sec : sections.values()) sec.getContents(contents);
return contents;
}
}
/**
* Replace the name of the specified section with a new one.
* <b>This will also move the section down to the bottom of whatever path branch it's on.</b>
* Creates a new section with the specified name along the path if the section does not exist.
* @param path to section rename
* @param name to rename the section to.
*/
public void renameSection(String path, String name) {
section.rename(split(path), name);
}
/**
* If the section at the specified path exists, remove it and any existing subsections
* @param path to section to remove
*/
public void removeSection(String path) {
section.remove(split(path));
}
/**
* Returns true if the path specified is one that exists
* @param path to check for existence
* @return true if the path exists, otherwise false
*/
public boolean pathExists(String path) {
return section.get(split(path)) != null;
}
/**
* List all the available sections nested after the given path
* @param path to get the nested sections from
* @return Set of all the section names after the given path, null if the path does not exist
*/
public Set<String> listSections(String path) {
if (path.isEmpty()) return section.sections.keySet();
Section sec = section.get(split(path));
if (sec == null) return null;
return sec.sections.keySet();
}
/**
* Set the comments of the section at the specified path
* @param path to the section to set the comments of
* @param comments strings to set as the comments for the section at the specified path
*/
public void setComments(String path, String... comments) {
String[] resized = new String[comments.length + 1];
System.arraycopy(comments, 0, resized, 0, comments.length);
section.setComments(split(path), resized);
}
/**
* Get all the comments of the section at the specified path
* @param path to the section to get the comments of
* @return String[] of comments for the section at the specified path
*/
public String[] getComments(String path) {
Section sec = section.get(split(path));
if (sec == null) return null;
return sec.getComments();
}
/**
* Set the value at the specified path
* @param path to set the value of
* @param value to set
*/
public void set(String path, Object value) {
section.setValue(split(path), value == null ? "null" : value);
}
/**
* Get the string value at the specified path
* @param path to get the string value at
* @return String value located at that path, or null if the path does not exist.
*/
public String getString(String path) {
Section sec = section.get(split(path));
if (sec == null) return null;
return sec.getHead(true);
}
/**
* Get the Boolean value at the specified path
* @param path to get the boolean value at
* @return True if the value at that path equalsIgnoreCase("true"), null if the path does not exist.
*/
public Boolean getBoolean(String path) {
String value = getString(path);
if (value == null) return null;
return Boolean.parseBoolean(value.trim());
}
/**
* Get the Integer value at the specified path
* @param path to get the Integer value at
* @return Integer value located at that path, or null if the path does not exist.
* @exception NumberFormatException if the value is not an integer
*/
public Integer getInt(String path) {
String value = getString(path);
if (value == null) return null;
return Integer.parseInt(value.trim());
}
/**
* Get the Byte value at the specified path
* @param path to get the Byte value at
* @return Byte value located at that path, or null if the path does not exist.
* @exception NumberFormatException if the value is not a byte
*/
public Byte getByte(String path) {
String value = getString(path);
if (value == null) return null;
return Byte.parseByte(value.trim());
}
/**
* Get the Long value at the specified path
* @param path to get the Long value at
* @return Long value located at that path, or null if the path does not exist.
* @exception NumberFormatException if the value is not a long
*/
public Long getLong(String path) {
String value = getString(path);
if (value == null) return null;
return Long.parseLong(value.trim());
}
/**
* Get the Double value at the specified path
* @param path to get the Double value at
* @return Double value located at that path, or null if the path does not exist.
* @exception NumberFormatException if the value is not a double
*/
public Double getDouble(String path) {
String value = getString(path);
if (value == null) return null;
return Double.parseDouble(value.trim());
}
/**
* Get the Float value at the specified path
* @param path to get the Float value at
* @return Float value located at that path, or null if the path does not exist.
* @exception NumberFormatException if the value is not a float
*/
public Float getFloat(String path) {
String value = getString(path);
if (value == null) return null;
return Float.parseFloat(value.trim());
}
/**
* Get a list of strings at the specified path
* Value must follow the List#toString() format. [s1, s2, s3]
* @param path to the string list
* @return list of strings parsed at the specified path, or null if the path does not exist.
*/
public List<String> getStringList(String path) {
String value = getString(path);
if (value == null) return null;
if (value.length() > 2) return Arrays.asList(value.substring(1, value.length() - 1).split(", "));
return Arrays.asList(value);
}
/**
* Split a given string at the first index of a period.
* @param path to split
* @return String[] of length 1 or 2, depending on whether or not the string contained something to split.
*/
private static String[] split(String path) {
return path.split("\\.", 2);
}
/**
* Get the contents of the entire file
* @return List of every line, in order.
*/
public List<String> getContents() {
return section.getContents(new LinkedList<>());
}
/**
* Get the contents of the file within the .jar at the path and copy it to the file
* @param InputStream source to copy from <b>getPlugin(MainClass.class).getResorce(pathToResource);</b>
* @param overwrite any existing contents?
*/
public MYMLFile copyDefaults(InputStream is, boolean overwrite) {
if (!overwrite && file.exists() && !isEmpty(file)) return this;
if (is == null) System.out.println("[Warning] " + file.getName() + "'s .jar file has been modified! Please restart!");
else
try {
Files.copy(is, file.getAbsoluteFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
e.printStackTrace();
}
reload();
return this;
}
/**
* @return true if the file is empty of characters, otherwise false
* @throws FileNotFoundException If the file is not found.
*/
public static boolean isEmpty(File file) {
Scanner input;
try {
input = new Scanner(file);
if (input.hasNextLine()) {
input.close();
return false;
}
input.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return true;
}
/**
* @param folderLoc The path from the main program folder to the destination folder
* @return Set of all files contained in the destination folder.
*/
public static Set<MYMLFile> getFolderContents(String... path) {
Set<MYMLFile> files = new HashSet<>();
StringBuilder pathBuilder = new StringBuilder();
for (int i = 0; i < path.length; i++)
pathBuilder.append(path[i] + File.separator);
File direc = new File(pathBuilder.toString());
if (!direc.exists()) direc.mkdirs();
if (direc.isDirectory())
for (File f : direc.listFiles())
files.add(new MYMLFile(f));
return files;
}
}