package prc.autodoc; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Scanner; import java.util.regex.Matcher; import java.util.regex.Pattern; import static prc.Main.*; /** * This class forms an interface for accessing 2da files in the * PRC automated manual generator. */ public class Data_2da implements Cloneable { // String matching pattern. Gets a block of non-whitespace (tab is not counted as whitespace here) that does not contain any " OR " followed by any characters until the next " private static final Pattern pattern = Pattern.compile("[[\\S&&[^\"]][\t]]+|\"[^\"]+\""); // Same as the above, but counts tab as whitespace. Bug-compatibility with BioWare's own violations of 2da spec private static final Pattern bugCompatPattern = Pattern.compile("[\\S&&[^\"]]+|\"[^\"]+\""); //private static Matcher matcher = pattern.matcher(""); private LinkedHashMap> mainData = new LinkedHashMap>(); private final String name; private String defaultValue; /** * Bugcompatibility feature for a special case where the first line of a 2da has * for line number instead of 0 */ private boolean starOnLine0 = false; /** * Used for storing the original case of the labels. The ones used in the hashmap are lowercase */ private ArrayList realLabels = new ArrayList(); private final boolean TLKEditCompatible = true; /** * Creates a new, empty Data_2da with the specified name. * * @param name The new 2da file's name. */ public Data_2da(String name) { this(name, ""); } /** * Creates a new, empty Data_2da with the specified name and default value. * * @param name the new 2da file's name * @param defaultValue the new 2da file's default value */ public Data_2da(String name, String defaultValue) { this.name = name; this.defaultValue = defaultValue; } /** * Private constructor for use in cloning. * * @param name the file name * @param defaultValue the default value * @param realLabels the labels with original case * @param mainData the contents */ public Data_2da(String name, String defaultValue, ArrayList realLabels, LinkedHashMap> mainData) { this.name = name; this.defaultValue = defaultValue; this.realLabels = realLabels; this.mainData = mainData; } /** * Saves the 2da represented by this object to file. Equivalent to calling * save2da(path, false, false). * * @param path the directory to save the file in. If this is "" or null, * the current directory is used. * @throws IOException if true and a file with the same name as this 2da * exists at path, it is overwritten. * If false and in the same situation, an IOException is * thrown. */ public void save2da(String path) throws IOException { save2da(path, false, false); } /** * Saves the 2da represented by this object to file. CRLFs are explicitly used * instead of system specific line terminator. * * @param path the directory to save the file in. If this is "" or null, * the current directory is used. * @param allowOverWrite if true and a file with the same name as this 2da * exists at path, it is overwritten. * If false and in the same situation, an IOException is * thrown. * @param evenColumns if true, every entry in a column will be padded until they * start from the same position * @throws IOException if cannot overwrite, or the underlying IO throws one */ public void save2da(String path, boolean allowOverWrite, boolean evenColumns) throws IOException { String CRLF = "\r\n"; if (path == null || path.equals("")) path = "." + File.separator; if (!path.endsWith(File.separator)) path += File.separator; File file = new File(path + name + ".2da"); if (file.exists() && !allowOverWrite) throw new IOException("File exists already: " + file.getAbsolutePath()); // Inform user if (verbose) System.out.print("Saving 2da file: " + name + " "); FileWriter fw = new FileWriter(file, false); String[] labels = this.getLabels(); String toWrite; // Get the amount of padding used, if any int[] widths = new int[labels.length + 1];// All initialised to 0 if (evenColumns) { ArrayList column; int pad; // Loop over columns for (int i = 0; i < labels.length; i++) { pad = labels[i].length(); column = mainData.get(labels[i]); // Loop over rows for (int j = 0; j < this.getEntryCount(); j++) { toWrite = column.get(j); // If the string contains spaces, it needs to be wrapped in " if (toWrite.indexOf(" ") != -1) toWrite = "\"" + toWrite + "\""; if (toWrite.length() > pad) pad = toWrite.length(); } widths[i] = pad; } // The last entry in the array is used for the numbers column widths[widths.length - 1] = new Integer(this.getEntryCount()).toString().length(); } // Write the header and default lines fw.write("2DA V2.0" + CRLF); if (!defaultValue.equals("")) fw.write("DEFAULT: " + defaultValue + CRLF); else fw.write(CRLF); // Write the labels row using the original case for (int i = 0; i < widths[widths.length - 1]; i++) fw.write(" "); for (int i = 0; i < realLabels.size(); i++) { fw.write(" " + realLabels.get(i)); for (int j = 0; j < widths[i] - realLabels.get(i).length(); j++) fw.write(" "); } fw.write((TLKEditCompatible ? " " : "") + CRLF); // Write the data for (int i = 0; i < this.getEntryCount(); i++) { // Write the number row and it's padding if (i == 0 && starOnLine0) fw.write("*"); else fw.write("" + i); for (int j = 0; j < widths[widths.length - 1] - new Integer(i).toString().length(); j++) fw.write(" "); // Loop over columns for (int j = 0; j < labels.length; j++) { toWrite = mainData.get(labels[j]).get(i); // If the string contains spaces, it needs to be wrapped in " if (toWrite.indexOf(" ") != -1) toWrite = "\"" + toWrite + "\""; fw.write(" " + toWrite); // Write padding for (int k = 0; k < widths[j] - toWrite.length(); k++) fw.write(" "); } fw.write((TLKEditCompatible ? " " : "") + CRLF); if (verbose) spinner.spin(); } fw.flush(); fw.close(); if (verbose) System.out.println("- Done"); } /** * Creates a new Data_2da on the 2da file specified. * * @param filePath path to the 2da file to load * @return a Data_2da instance containing the read 2da * @throws IllegalArgumentException filePath does not specify a 2da file * @throws TwoDAReadException reading the 2da file specified does not succeed, * or the file does not contain any data */ public static Data_2da load2da(String filePath) throws IllegalArgumentException, TwoDAReadException { return load2da(filePath, false); } /** * Creates a new Data_2da on the 2da file specified. * * @param filePath path to the 2da file to load * @param bugCompat if this is true, ignores * departures from the 2da spec present in Bioware 2das * @return a Data_2da instance containing the read 2da * @throws IllegalArgumentException filePath does not specify a 2da file * @throws TwoDAReadException reading the 2da file specified does not succeed, * or the file does not contain any data */ public static Data_2da load2da(String filePath, boolean bugCompat) throws IllegalArgumentException, TwoDAReadException { Data_2da toReturn; Matcher matcher = bugCompat ? bugCompatPattern.matcher("") : pattern.matcher(""); String name; // Some paranoia checking for bad parameters if (!filePath.toLowerCase().endsWith("2da")) throw new IllegalArgumentException("Non-2da filename passed to Data_2da: " + filePath); // Create the file handle File baseFile = new File(filePath); // More paraoia if (!baseFile.exists()) throw new IllegalArgumentException("Nonexistent file passed to Data_2da: " + filePath); if (!baseFile.isFile()) throw new IllegalArgumentException("Nonfile passed to Data_2da: " + filePath); // Drop the path from the filename name = baseFile.getName().substring(0, baseFile.getName().length() - 4); //toReturn = new Data_2da(baseFile.getName().substring(0, baseFile.getName().length() - 4)); // Tell the user what we are doing if (verbose) System.out.print("Reading 2da file: " + name + " "); // Create a Scanner for reading the 2da Scanner reader = null; try { // Fully read the file into a byte array RandomAccessFile raf = new RandomAccessFile(baseFile, "r"); byte[] bytebuf = new byte[(int) raf.length()]; raf.readFully(bytebuf); raf.close(); //reader = new Scanner(baseFile); reader = new Scanner(new String(bytebuf)); } catch (Exception e) { err_pr.println("Error: File operation failed. Aborting.\nException data:\n" + e); System.exit(1); } try { // Check the 2da header if (!reader.hasNextLine()) throw new TwoDAReadException("Empty file: " + name + "!"); String data = reader.nextLine(); if (!(bugCompat ? Pattern.matches("2DA[ \t]V2\\.0\\s*", data) : data.contains("2DA V2.0"))) throw new TwoDAReadException("2da header missing or invalid: " + name); // Initialise the return object toReturn = new Data_2da(name); // Start the actual reading try { toReturn.createData(reader, matcher, bugCompat); } catch (TwoDAReadException e) { throw new TwoDAReadException("Exception occurred when reading 2da file: " + toReturn.getName() + "\n" + e.getMessage(), e); } } finally { // Cleanup reader.close(); } if (verbose) System.out.println("- Done"); return toReturn; } /** * Reads the data rows from the 2da into the hashmap and * does validity checking on the 2da while doing so. * * @param reader Scanner that the method reads from * @param matcher Matcher being used to parse the data read * @param bugCompat if this is true, ignores * departures from the 2da spec present in Bioware 2das */ private void createData(Scanner reader, Matcher matcher, boolean bugCompat) { Scanner rowParser; String data, bugCompat_data; boolean bugCompat_MissingDefaultLine = false; int line = 0; // Get the default - though it's not used by this implementation, it should not be lost by opening and resaving a file if (!reader.hasNextLine()) throw new TwoDAReadException("No contents after header in 2da file " + name + "!"); bugCompat_data = data = reader.nextLine(); matcher.reset(data); if (matcher.find()) { // Non-blank default line data = matcher.group(); if (data.trim().equalsIgnoreCase("DEFAULT:")) { if (matcher.find()) this.defaultValue = matcher.group(); else throw new TwoDAReadException("Malformed default line in 2da file " + name + "!"); } else if (!bugCompat) throw new TwoDAReadException("Malformed default line in 2da file " + name + "!"); else bugCompat_MissingDefaultLine = true; } // Find the labels row if (bugCompat_MissingDefaultLine) // Handle cases where the labels are on the second line in the file instead of 3rd data = bugCompat_data; else { if (!reader.hasNextLine()) throw new TwoDAReadException("No labels found in 2da file!"); data = reader.nextLine(); } // Check for blank lines between the DEFAULT line and labels if (data.trim().equals("")) if (!bugCompat) throw new TwoDAReadException("Labels not present on third line of the file!"); else while (true) { if (reader.hasNextLine()) { data = reader.nextLine(); if (!data.trim().equals("")) break; } else throw new TwoDAReadException("No data in 2da file!"); } // Parse the labels String[] localrealLabels = data.trim().split("\\p{javaWhitespace}+"); String[] labels = new String[localrealLabels.length]; //System.arraycopy(realLabels, 0, labels, 0, localrealLabels.length); // Create the row containers and the main store for (int i = 0; i < labels.length; i++) { realLabels.add(localrealLabels[i]); labels[i] = localrealLabels[i].toLowerCase(); mainData.put(labels[i], new ArrayList()); } // Error if there are empty lines between the header and the data or no lines at all if (!reader.hasNextLine()) if (!bugCompat) throw new TwoDAReadException("No data in 2da file!"); else return; if ((data = reader.nextLine()).trim().equals("")) if (!bugCompat) throw new TwoDAReadException("Blank lines following labels row!"); else while (true) { if (reader.hasNextLine()) { data = reader.nextLine(); if (!data.trim().equals("")) break; } else return; } while (true) { //rowParser = new Scanner(data); matcher.reset(data); matcher.find(); // Check for the presence of the row number try { // Special case - In bugcompatibility mode, 0 can be replaced with * if (bugCompat && line == 0 && matcher.group().trim().equals("*")) { starOnLine0 = true; } else line = Integer.parseInt(matcher.group()); } catch (NumberFormatException e) { return; // throw new TwoDAReadException("Numberless 2da line: " + (line + 1)); } // Start parsing the row for (int i = 0; i < labels.length; i++) { // Find the next match and check for too short rows if (!matcher.find()) throw new TwoDAReadException("Too short 2da line: " + line); // Get the next element and add it to the data structure data = matcher.group(); // Remove the surrounding quotes if they are present if (data.startsWith("\"")) data = data.substring(1, data.length() - 1); mainData.get(labels[i]).add(data); } // Check for too long rows if (matcher.find()) return; // throw new TwoDAReadException("Too long 2da line: " + line); // Increment the entry counter //entries++; /* Get the next line if there is one, or break the loop * A bit ugly, but I couldn't figure an easy way of making the loop go right * even for 2das with only one row without biggish changes */ if (reader.hasNextLine()) { data = reader.nextLine(); if (data.trim().equals("")) break; } else break; if (verbose) spinner.spin(); } // Some validity checking on the 2da. Empty rows allowed only in the end if (getNextNonEmptyRow(reader) != null) throw new TwoDAReadException("Empty row in the middle of 2da. After row: " + line); } /** * Reads rows from a Scanner pointed at a 2da file until it finds a * row containing non-whitespace characters. * * @param reader Scanner that the method reads from * @return The row found, or null if none were found. */ private static String getNextNonEmptyRow(Scanner reader) { String toReturn = null; while (reader.hasNextLine()) { toReturn = reader.nextLine(); if (!toReturn.trim().equals("")) break; } if (toReturn == null || toReturn.trim().equals("")) return null; return toReturn; } /** * Get the list of column labels in this 2da. * * @return an array of Strings containing the column labels */ public String[] getLabels() { // For some reason, it won't let me cast the keyset directly into a String[] // return (String[])(mainData.keySet().toArray()); Object[] temp = mainData.keySet().toArray(); String[] toReturn = new String[temp.length]; for (int i = 0; i < temp.length; i++) toReturn[i] = (String) temp[i]; /*String[] toReturn = (String[])mainData.keySet().toArray();*/ return toReturn; } /** * Get the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get, as string * @return int represeting the 2da entry or null if the column does not exist * if the column is **** then 0 will be returned * @throws NumberFormatException if row cannot be converted to an integer */ public int getBiowareEntryAsInt(String label, String row) { String returnString = this.getEntry(label, Integer.parseInt(row)); if (returnString.equals("****")) return 0; else if (returnString.matches("\\D")) //check its a number return 0; return Integer.decode(returnString); } /** * Get the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get * @return String represeting the 2da entry or null if the column does not exist * if the column is **** then a zero length string will be returned */ public int getBiowareEntryAsInt(String label, int row) { ArrayList column = mainData.get(label.toLowerCase()); String returnString = column != null ? column.get(row) : null; if (returnString.equals("****")) return 0; else if (returnString.matches("\\D")) //check its a number return 0; return Integer.decode(returnString); } /** * Get the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get, as string * @return String represeting the 2da entry or null if the column does not exist * if the column is **** then a zero length string will be returned * @throws NumberFormatException if row cannot be converted to an integer */ public String getBiowareEntry(String label, String row) { String returnString = this.getEntry(label, Integer.parseInt(row)); if (returnString.equals("****")) return ""; return returnString; } /** * Get the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get * @return String represeting the 2da entry or null if the column does not exist * if the column is **** then a zero length string will be returned */ public String getBiowareEntry(String label, int row) { ArrayList column = mainData.get(label.toLowerCase()); String returnString = column != null ? column.get(row) : null; if (returnString.equals("****")) return ""; return returnString; } /** * Get the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get, as string * @return String represeting the 2da entry or null if the column does not exist * @throws NumberFormatException if row cannot be converted to an integer */ public String getEntry(String label, String row) { return this.getEntry(label, Integer.parseInt(row)); } /** * Get the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get * @return String represeting the 2da entry or null if the column does not exist */ public String getEntry(String label, int row) { ArrayList column = mainData.get(label.toLowerCase()); return column != null ? column.get(row) : null; } /** * Get number of entries in this 2da. Works by returning the size of one of the columns in the 2da. * * @return integer equal to the number of entries in this 2da */ public int getEntryCount() { if (mainData.entrySet().size() == 0) { return 0; } return mainData.entrySet().iterator().next().getValue().size(); } /** * Get the name of this 2da * * @return String representing this 2da's name */ public String getName() { return name; } /** * Sets the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get, as string * @param entry the new contents of the entry. If this is null or empty, it is replaced with **** * @throws NumberFormatException if row cannot be converted to an integer */ public void setEntry(String label, String row, String entry) { this.setEntry(label, Integer.parseInt(row), entry); } /** * Sets the 2da entry on the given row and column * * @param label the label of the column to get * @param row the number of the row to get, as string * @param entry the new contents of the entry. If this is null or empty, it is replaced with **** * or with the default value if that is set */ public void setEntry(String label, int row, String entry) { if (entry == null || entry.equals("")) if (defaultValue.equals("")) entry = "****"; else entry = defaultValue; mainData.get(label.toLowerCase()).set(row, entry); } /** * Returns the contents of the requested row as a string array. The order the columns are * taken is the same as the order of labels from getLabels(). * * @param index the index of the row to get * @return an array of strings containing the elements in the r * @throws NumberFormatException if index cannot be converted to an integer */ public String[] getRow(String index) { return getRow(Integer.parseInt(index)); } /** * Returns the contents of the requested row as a string array. The order the columns are * taken is the same as the order of labels from getLabels(). * * @param index the index of the row to get * @return an array of strings containing the elements in the row */ public String[] getRow(int index) { String[] labels = this.getLabels(); String[] toReturn = new String[labels.length]; for (int i = 0; i < labels.length; i++) { toReturn[i] = mainData.get(labels[i]).get(index); } return toReturn; } /** * Appends a new, empty row to the end of the 2da file, with entries defaulting to the * default value or if that is not set, **** */ public void appendRow() { String[] labels = this.getLabels(); for (String label : labels) { if (defaultValue.equals("")) mainData.get(label).add("****"); else mainData.get(label).add(defaultValue); } } /** * Appends a new, empty row to the end of the 2da file. The new row will be filled with the values * given as parameter. * * @param data the strings that will be used to fill the new row * @throws IllegalArgumentException if the number of elements in data array is not * same as number of columns in the 2da */ public void appendRow(String[] data) { String[] labels = this.getLabels(); // Sanity check if (labels.length != data.length) throw new IllegalArgumentException("Differing column width when attempting to insert row"); for (int i = 0; i < labels.length; i++) { mainData.get(labels[i]).add(data[i]); } } /** * Inserts a new row into the given index in the 2da file. The row currently at the index and all * subsequent rows have their index increased by one. The new row will be filled with the values * given as parameter. * * @param index the index where the new row will be located * @param data the strings that will be used to fill the new row * @throws IllegalArgumentException if the number of elements in data array is not * same as number of columns in the 2da * @throws NumberFormatException if index cannot be converted to an integer */ public void insertRow(String index, String[] data) { insertRow(Integer.parseInt(index), data); } /** * Inserts a new row into the given index in the 2da file. The row currently at the index and all * subsequent rows have their index increased by one. The new row will be filled with the values * given as parameter. * * @param index the index where the new row will be located * @param data the strings that will be used to fill the new row * @throws IllegalArgumentException if the number of elements in data array is not * same as number of columns in the 2da */ public void insertRow(int index, String[] data) { String[] labels = this.getLabels(); // Sanity check if (labels.length != data.length) throw new IllegalArgumentException("Differing column width when attempting to insert row"); for (int i = 0; i < labels.length; i++) { mainData.get(labels[i]).add(index, data[i]); } } /** * Adds a new, empty row to the given index in the 2da file. The row currently at the index and all * subsequent rows have their index increased by one. * The entries default to ****. * * @param index the index where the new row will be located * @throws NumberFormatException if index cannot be converted to an integer */ public void insertRow(String index) { insertRow(Integer.parseInt(index)); } /** * Adds a new, empty row to the given index in the 2da file. The row currently at the index and all * subsequent rows have their index increased by one. * The entries default to default value or if that is not set, ****. * * @param index the index where the new row will be located */ public void insertRow(int index) { String[] labels = this.getLabels(); for (String label : labels) { if (defaultValue.equals("")) mainData.get(label).add(index, "****"); else mainData.get(label).add(index, defaultValue); } } /** * Removes the row at the given index. All subsequent rows have their indexed shifted down by one. * * @param index the index of the row to remove * @throws NumberFormatException if index cannot be converted to an integer */ public void removeRow(String index) { removeRow(Integer.parseInt(index)); } /** * Removes the row at the given index. All subsequent rows have their indexed shifted down by one. * * @param index the index of the row to remove */ public void removeRow(int index) { String[] labels = this.getLabels(); for (String label : labels) { mainData.get(label).remove(index); } } /** * Adds a new column to the 2da file. The new column will be the last in the file. * * @param label the name of the column to add */ public void addColumn(String label) { ArrayList column = new ArrayList(); mainData.put(label.toLowerCase(), column); realLabels.add(label); if (defaultValue.equals("")) { for (int i = 0; i < this.getEntryCount(); i++) { column.add("****"); } } else { for (int i = 0; i < this.getEntryCount(); i++) { column.add(defaultValue); } } } /** * Removes the column with the given label from the 2da. * * @param label the name of the column to remove */ public void removeColumn(String label) { mainData.remove(label); realLabels.remove(label); } /** * The main method, as usual * * @param args */ public static void main(String[] args) { if (args.length == 0) readMe(); List fileNames = new ArrayList(); boolean compare = false; boolean resave = false; boolean minimal = false; boolean ignoreErrors = false; boolean readStdin = false; boolean bugCompat = false; for (String param : args) {//[-bcrmnqs] file... | - // Parameter parseage if (param.startsWith("-")) { if (param.equals("-")) readStdin = true; else if (param.equals("--help")) readMe(); else { for (char c : param.substring(1).toCharArray()) { switch (c) { case 'b': bugCompat = true; break; case 'c': compare = true; if (resave) resave = false; break; case 'r': resave = true; if (compare) compare = false; break; case 'm': minimal = true; break; case 'n': ignoreErrors = true; break; case 'q': verbose = false; break; case 's': spinner.disable(); break; default: err_pr.println("Error: Unknown parameter: " + c); readMe(); } } } } else // It's a filename fileNames.add(param); } // Read files from stdin if specified if (readStdin) { Scanner scan = new Scanner(System.in); String s; while (scan.hasNextLine()) { s = scan.nextLine(); if (s.charAt(0) == '"' && s.charAt(s.length() - 1) == '"') s = s.substring(1, s.length() - 1); fileNames.add(s); } } // Run the specified operation if (compare) { Data_2da file1, file2; file1 = load2da(args[1], bugCompat); file2 = load2da(args[2], bugCompat); doComparison(file1, file2); } else if (resave) { Data_2da temp; for (String fileName : fileNames) { try { temp = load2da(fileName, bugCompat); temp.save2da(new File(fileName).getCanonicalFile().getParent() + File.separator, true, !minimal); } catch (Exception e) { // Print the error err_pr.printException(e); // If ignoring errors, and this error is of expected type, continue if (e instanceof IllegalArgumentException || e instanceof TwoDAReadException || e instanceof IOException) if (ignoreErrors) continue; System.exit(1); } } } else { // Validify by loading for (String fileName : fileNames) { try { load2da(fileName, bugCompat); } catch (Exception e) { // Print the error err_pr.printException(e); // If ignoring errors, and this error is of expected type, continue if (e instanceof IllegalArgumentException || e instanceof TwoDAReadException) if (ignoreErrors) continue; System.exit(1); } } } } private static void readMe() { System.out.println("Usage:\n" + " [-bcrmnqs] file... | -\n" + "\n" + " -b bug-compatibility mode. Counts tabs as whitespace instead of data\n" + " -c prints the differing lines between the 2das given as first two\n" + " parameters. They must have the same label set and entrycount.\n" + " Mutually exclusive with -r\n" + " -r resaves the 2das given as parameters. Mutually exclusive with -c\n" + " -m saves the files with minimal spaces. Only relevant when resaving\n" + " -n ignores errors that occur during validity testing and resaving,\n" + " just skips to the next file\n" + " -q quiet mode\n" + " -s no spinner\n" + " - a line given as a lone parameter means that the list of files is\n" + " read from stdin in addition to the ones passed from command line.\n" + " The list passed in such manner should contain one filename per line\n" + "\n" + " --help prints this text\n" + "\n" + "\n" + "if neither -c or -r is specified, performs validity testing on the given files" ); System.exit(0); } /** * Compares the given two 2da files and prints differences it finds * Differing number of rows, or row names will cause comparison to abort. * * @param file1 Data_2da containing one of the files to be compared * @param file2 Data_2da containing the other file to be compared */ public static void doComparison(Data_2da file1, Data_2da file2) { // Check labels String[] labels1 = file1.getLabels(), labels2 = file2.getLabels(); if (labels1.length != labels2.length) { System.out.println("Differing amount of row labels\n" + file1.getName() + ": " + labels1.length + "\n" + file2.getName() + ": " + labels2.length); return; } for (int i = 0; i < labels1.length; i++) { if (!labels1[i].equals(labels2[i])) { System.out.println("Differing labels"); return; } } // Check lengths int shortCount = file1.getEntryCount(); if (file1.getEntryCount() != file2.getEntryCount()) { System.out.println("Differing line counts.\n" + file1.getName() + ": " + file1.getEntryCount() + "\n" + file2.getName() + ": " + file2.getEntryCount()); shortCount = shortCount > file2.getEntryCount() ? file2.getEntryCount() : shortCount; } // Check elements for (int i = 0; i < shortCount; i++) { for (String label : labels1) { if (!file1.getEntry(label, i).equals(file2.getEntry(label, i))) { System.out.println("Differing entries on row " + i + ", column " + label + "\n" + file1.getName() + ": " + file1.getEntry(label, i) + "\n" + file2.getName() + ": " + file2.getEntry(label, i)); } } } } public String toString() { return this.name + "(" + this.getEntryCount() + " entries)"; } /** * @see java.lang.Object#toString() */ public String toOutputString() { String CRLF = "\r\n"; StringBuffer toReturn = new StringBuffer(); boolean evenColumns = true; String[] labels = this.getLabels(); String toWrite; // Get the amount of padding used, if any int[] widths = new int[labels.length + 1];// All initialised to 0 ArrayList column; int pad; // Loop over columns for (int i = 0; i < labels.length; i++) { pad = labels[i].length(); column = mainData.get(labels[i]); // Loop over rows for (int j = 0; j < this.getEntryCount(); j++) { toWrite = column.get(j); // If the string contains spaces, it needs to be wrapped in " if (toWrite.indexOf(" ") != -1) toWrite = "\"" + toWrite + "\""; if (toWrite.length() > pad) pad = toWrite.length(); } widths[i] = pad; } // The last entry in the array is used for the numbers column widths[widths.length - 1] = new Integer(this.getEntryCount()).toString().length(); // Write the header and default lines toReturn.append("2DA V2.0" + CRLF); if (!defaultValue.equals("")) toReturn.append("DEFAULT: " + defaultValue + CRLF); else toReturn.append(CRLF); // Write the labels row using the original case for (int i = 0; i < widths[widths.length - 1]; i++) toReturn.append(" "); for (int i = 0; i < realLabels.size(); i++) { toReturn.append(" " + realLabels.get(i)); for (int j = 0; j < widths[i] - realLabels.get(i).length(); j++) toReturn.append(" "); } toReturn.append((TLKEditCompatible ? " " : "") + CRLF); // Write the data for (int i = 0; i < this.getEntryCount(); i++) { // Write the number row and it's padding toReturn.append("" + i); for (int j = 0; j < widths[widths.length - 1] - new Integer(i).toString().length(); j++) toReturn.append(" "); // Loop over columns for (int j = 0; j < labels.length; j++) { toWrite = mainData.get(labels[j]).get(i); // If the string contains spaces, it needs to be wrapped in " if (toWrite.indexOf(" ") != -1) toWrite = "\"" + toWrite + "\""; toReturn.append(" " + toWrite); // Write padding for (int k = 0; k < widths[j] - toWrite.length(); k++) toReturn.append(" "); } toReturn.append((TLKEditCompatible ? " " : "") + CRLF); } return toReturn.toString(); } /** * Makes an independent copy of this 2da. * * @see java.lang.Object#clone() */ @SuppressWarnings("unchecked") public Object clone() { // Make a sufficiently deep copy of the main data arrays LinkedHashMap> cloneData = new LinkedHashMap>(); for (String key : this.getLabels()) // Use real labels to preserve order cloneData.put(key, (ArrayList) this.mainData.get(key).clone()); // Create a new Data_2da. The Strings are immutable, so they can be used as-is and clone() // on an array produces a sufficiently deep copy right away return new Data_2da( this.name, this.defaultValue, (ArrayList) this.realLabels.clone(), cloneData ); } }