Skip to content

Instantly share code, notes, and snippets.

@justisr
Last active June 1, 2018 20:43
Show Gist options
  • Save justisr/ae6b296ff43529ade32f8ba36237d98d to your computer and use it in GitHub Desktop.
Save justisr/ae6b296ff43529ade32f8ba36237d98d to your computer and use it in GitHub Desktop.
//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 warranty 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.<br>
* @param path from the program's running folder to the destination.<br>
* 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.<br>
* @param file for which to wrap the MYMLFile object.
*/
public MYMLFile(File file) {
this.file = file;
reload();
}
/**
* Get the raw File object associated with this file wrapper instance.<br>
* @return File wrapped by this instance.
*/
public File getFile() {
return file;
}
/**
* Gets the file from disk or generates one if it doesn't exist.<br>
* Save all of the file's contents, all the paths, values, everything, to memory.<br>
* Overwrites any and all existing contents of memory<br>
* @throws IOException when the directory does not exist.
*/
public void reload() {
section = new Section(-1, null, "", "", new ArrayList<>());
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<>();
Section current = section;
while (input.hasNextLine()) {
Section returned = current.nextLine(input.nextLine(), header);
if (current != returned)
header = new ArrayList<>();
current = returned;
}
if (section.children.isEmpty())
section.header = header;
else footer = header;
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Save all the current contents of memory to disk.<br>
* Overwrites any and all existing contents of the file<br>
* @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 List<String> footer;
private static class Section {
private final Section parent;
private int tab;
private String key, value;
private List<String> header;
private final Map<String, Section> children = new LinkedHashMap<>();
private Section(int tab, Section parent, String key, String value, List<String> header) {
this.tab = tab;
this.parent = parent;
this.key = key;
this.value = value;
this.header = header;
}
private Section nextLine(String line, List<String> header) {
if (isCommented(line)) {
header.add(line);
return this;
} else {
int keyIndex = 0;
for (; keyIndex < line.length() ; keyIndex++) {
if (!Character.isWhitespace(line.charAt(keyIndex)))
break;
}
String value = line.replaceFirst(".*?" + VALUE_SEPARATOR + "\\s?", "");
String key;
if (value.length() == line.length()) {
value = line.substring(keyIndex);
key = value;
} else key = line.substring(keyIndex, line.length() - value.length() - 1);
if (key.length() > 1 && key.charAt(key.length()-1) == VALUE_SEPARATOR) key = key.substring(0, key.length()-1);
if (keyIndex > tab) return addChild(keyIndex, key, value, header);
else return nearestParent(keyIndex).addChild(keyIndex, key, value, header);
}
}
/**
* Create a child section of the current section.
* @param keyIndex The index of the first key character. Also the number of tab characters
* @param parent section of this new child
* @param key of the child section
* @param value of the child section
* @param comments for the child section
* @return a new child section of the current section with the applied settings.
*/
private Section addChild(int keyIndex, String key, String value, List<String> comments) {
Section child = new Section(keyIndex, this, key, value, comments);
children.put(key, child);
return child;
}
/**
* Get the nearest possible parent of the line with the specific tab intend
* @param indent of line whose parent is being looked for
* @return the nearest possible parent section of the line with that indent
*/
private Section nearestParent(int indent) {
return tab < indent ? this : parent.nearestParent(indent);
}
/**
* Replace the name of the specified section with a new one<br>
* <b>This will also move the section down to the bottom of whatever path branch it's on</b><br>
* Creates a new section with the specified name along the path if the section does not exist<br>
* @param key to rename the section to<br>
*/
private void setKey(String key) {
parent.children.remove(this.key);
parent.children.put(key, this);
this.key = key;
}
/**
* Replace the name of the specified section with a new one<br>
* <b>This will also move the section down to the bottom of whatever path branch it's on</b><br>
* Creates a new section with the specified name along the path if the section does not exist
* @param path to section rename
* @param key to rename the section to
*/
private void rename(List<String> path, String key) {
if (path.size() < 2) {
if (!children.containsKey(path.get(0))) addChild(tab+1, key, "", new ArrayList<>());
else children.get(path.get(0)).setKey(key);
} else {
Section child = children.get(path.get(0));
path.remove(0);
if (child != null) child.rename(path, key);
else addChild(tab+1, key, "", new ArrayList<>()).rename(path, key);
}
}
/**
* Remove a section and all inner sections
* @param path to the section to remove
*/
private void remove(List<String> path) {
if (path.size() < 2) children.remove(path.get(0));
Section child = children.get(path.get(0));
if (child == null) return;
path.remove(0);
child.remove(path);
}
/**
* Set the comments at the specified path
* @param path to set the comments for
* @param value to set as the comments
*/
private void setComments(List<String> path, List<String> value) {
Section sec = hardGet(path, "");
sec.header = value;
}
/**
* Convert an object into a string value, and set that value at the desired path<br>
* Converts arrays into a List-like string value. Otherwise uses #toString() on the object
* @param path to set the value at, [path, to.value]
* @param value to set
*/
private void setValue(List<String> path, Object value) {
StringBuilder builder = new StringBuilder(value.toString());
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).value = builder.toString();
}
/**
* 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(List<String> path, Object value) {
String key = path.get(0);
if (path.size() < 2) {
if (children.containsKey(key)) return children.get(key);
else return addChild(tab+1, key, value.toString(), new ArrayList<>());
} else {
path.remove(0);
if (children.containsKey(key)) return children.get(key).hardGet(path, value);
else return addChild(tab+1, key, value.toString(), new ArrayList<>()).hardGet(path, value);
}
}
/**
* Get the section at the specified path
* @param string list path to the section
* @return Section at the path, null if section does not exist
*/
private Section get(List<String> path) {
if (path.size() < 2) return children.get(path.get(0));
Section child = children.get(path.get(0));
if (child == null) return null;
path.remove(0);
return child.get(path);
}
/**
* Get the sections' path/value and comments, as well as all of the paths/values and comments of nested sections
* @param contents list to append the contents to
* @param indent for the section's line
* @return List of the section comments, the path/value as well as all nested sections
*/
private List<String> getContents(List<String> contents, int indent) {
for (String head : header) contents.add(head);
if (tab > -1) contents.add(new String(new char[indent++]).replace("\0", TAB) + key + VALUE_SEPARATOR + " " + value);
for (Section sec : children.values()) sec.getContents(contents, indent);
return contents;
}
}
/**
* The whitespace to be used to represent a tab when generating the file from memory
*/
public static final String TAB = " ";
public static final char PATH_CHAR = '.', VALUE_SEPARATOR = ':', COMMENT_INDICATOR = '#';
/**
* 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(splitPath(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(splitPath(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(splitPath(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.children.keySet();
Section sec = section.get(splitPath(path));
if (sec == null) return null;
return sec.children.keySet();
}
/**
* Set the comments of the section at the specified path
* @param path to the section to set the comments of
* @param comment strings to set as the comments for the section at the specified path
*/
public void setComments(String path, String... comments) {
List<String> commented = new ArrayList<>();
for (String cmt : comments)
if (isCommented(cmt)) commented.add(cmt);
else commented.add(COMMENT_INDICATOR + cmt);
section.setComments(splitPath(path), commented);
}
/**
* Get all the comments of the section at the specified path
* @param path to the section to get the comments of
* @return String list of comments for the section at the specified path
*/
public List<String> getComments(String path) {
Section sec = section.get(splitPath(path));
if (sec == null) return null;
return sec.header;
}
/**
* 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(splitPath(path), value == null ? "null" : value);
}
/**
* Get the string value at a path location, and create the path if it does not already exist.
* @param path to look for the value of or create if a value there does not already exist.
* @return the value associated with the path after the hard get.
*/
public String hardGetString(String path) {
return hardGetString(path, "");
}
/**
* Get the string value at a path location, and create the path with a specific value if it does not already exist.
* @param path to look for the value of or create if a value there does not already exist.
* @param value to apply to the path if the path did not exist.
* @return the value associated with the path after the hard get.
*/
public String hardGetString(String path, String value) {
return section.hardGet(splitPath(path), value).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(splitPath(path));
if (sec == null) return null;
return sec.value;
}
/**
* 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<br>
* If the format [s1, s2, s3] is followed, it will be used to parse, if not, the list elements will be parsed using whitespace
* @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.startsWith("[") && value.endsWith("]")) return Arrays.asList(value.substring(0, value.length()-1).split(",\\s?"));
return Arrays.asList(value.split("\\s+"));
}
/**
* Get the contents of the entire file
* @return List of every line, in order
*/
public List<String> getContents() {
List<String> contents = section.getContents(new LinkedList<>(), 0);
if (footer != null) contents.addAll(footer);
return contents;
}
/**
* Get defaults from an InputStream and copy it into the file if the file is empty, doesn't exist, or if overwrite is true
* @param InputStream to copy from
* @param overwrite contents if they exist within the file
*/
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;
}
/**
* @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("\\s*?(" + COMMENT_INDICATOR + ".*)*");
}
/**
* Split a path string using the path char, into a string list
* @param string path to split
* @return String list of path member keys
*/
public static List<String> splitPath(String string) {
int off = 0, next = 0;
ArrayList<String> list = new ArrayList<>();
while ((next = string.indexOf(PATH_CHAR, off)) != -1) {
list.add(string.substring(off, next));
off = next + 1;
}
if (off == 0)
return Arrays.asList(string);
list.add(string.substring(off, string.length()));
int resultSize = list.size();
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
return list.subList(0, resultSize);
}
/**
* @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;
}
}
@justisr
Copy link
Author

justisr commented Nov 19, 2016

It follows the same general tabbing syntax of YAML:

############
# - My File -
program:
  files:
    read: cool
  memory:
    loaded in: hi
version:  2

However, unlike YAML:
It allows for every member of a path to have a value associated with it.
It also doesn't require that you encapsulate special character strings in quotes.
It has full support for comments/empty lines, allowing you to get/set/modify/load/save them just like any value.
It has significantly increased flexibility:

  • A space is accepted but not required after the semi-colon.
  • Hierarchal structure is respected whether you're using 1, 2, or more tabs, even if tabbing amount is inconsistent.
  • If a non-comment entry is made and it cannot be split into a key and value, the entry is regarded as both its key and its value.
  • Saving the parsed file will fix any tabbing inconsistencies, value formatting, or missing spaces, for perfect MYML formatting.
  • Changing the characters used for formatting is as simple as altering the char constant under the inner class Section.

Any file that is loaded with this wrapper will have MYML compatibility.
So the following would be fully supported(loadable, changeable, saveable):

############
# - My file -
# By Justis

program: true
   files: 15.5%
    # By the way, here's some helpful info.
    # I know you needed it.
     read: &7cool
  memory:100
       loaded in: true

# Don't touch this
version:  2
   This is both a key and a value
# Footer
# - Justis

This allows for highly efficient data structuring, where you can make the most of every line.
More importantly, it allows for extreme ease of use for end users. In addition to the comment support allowing users to make sense of the file's contents and requirements. It ensures that the file parser can always make the most sense of what the user has input, no matter the input.

On top of that, I've managed to compress everything within a single class file and a hidden static inner class.
I reference only java.io, java.nio, and java.utils. No dependencies.
Making utilizing this API as simple as pasting in the class; and wrapping a file: MYMLFile data = new MYMLFile(file);
There are additional methods provided for developers as well, such as #hardGetString(), #rename(), #listSections() and of course the comment support, to name a few.
Developers and users alike should have the best possible experience when communicating through configurations.
MYML strives to facilitate that.

What can MYML not provide that YAML can?
MYML is NOT a superset of YAML.
YAML is bloated by a vast list of indicators that offer endless specific possibilities for formatting.
https://symfony.com/doc/current/components/yaml/yaml_format.html
MYML does not recognize all of YAML's endless and unnecessary indicators. MYML uses mappings and that is all.
As a result, MYML wouldn't read a multi-line value as a single value. It would read each line as its own mapping.

@crock
Copy link

crock commented Apr 24, 2017

Looks great, I'll work on a PHP port when I find some spare time! Together we can make MYML a global phenomenon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment