crock
11/19/2016 - 1:43 AM

.myml | A single class file wrapper for an improved (YAML-similar) syntax

.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;
	}
}