/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.common.util; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.*; import java.util.ArrayList; /** * Class for loading data tables with different delimiters and also the special * NDSC table type that is created by the config tool. The data is tokenized and * distributed to a callback class that is allowed to parse the values by the * functions offered by this class line by line. * * @author Nop * @author Martin Karing <nitram@illarion.org> */ public class TableLoader { /** * The crypto instance that is used by the table loader to decrypt the tables that are read and parsed. */ @SuppressWarnings("RedundantFieldInitialization") @Nullable private static Crypto crypto = null; /** * The error and debug logger of the client. */ @Nonnull private static final Logger LOGGER = LoggerFactory.getLogger(TableLoader.class); /** * The delimiter that is used at this table. */ @Nonnull private final String delimiter; /** * The tokes of the last line that was read encoded as strings. */ @Nonnull private final ArrayList<String> tokens; /** * Construct a table loader that loads the table from the file system. With * this constructor the table loader takes a {@code ,} as delimiter. * <p/> * <b>Important:</b> The first line is assumed as header line and thrown * away at the reading operation. * <p/> * * @param table the file that is the source for this table loader * @param callback the call back class that is allowed to parse the values * this table loader reads */ public <T extends TableLoader> TableLoader(@Nonnull File table, @Nonnull TableLoaderSink<T> callback) { this(table, callback, ","); } /** * Construct a table loader that loads the table from the file system. * <p/> * <b>Important:</b> The first line is assumed as header line and thrown away at the reading operation. * <p/> * * @param table the file that is the source for this table loader * @param callback the callback class that is allowed to parse the values this table loader reads * @param tableDelimiter the delimiter of the table, so the table columns are separated by this string */ public <T extends TableLoader> TableLoader( @Nonnull File table, @Nonnull TableLoaderSink<T> callback, @Nonnull String tableDelimiter) { this(tableDelimiter); // ignore missing tables if (!table.exists()) { return; } InputStream is = null; try { is = new FileInputStream(table); loadTable(is, false, callback); } catch (@Nonnull FileNotFoundException e) { // it's ok, just ignore it } catch (@Nonnull IOException e) { LOGGER.error("Unable to read data file {}", table.getPath(), e); } finally { try { if (is != null) { is.close(); } } catch (@Nonnull IOException e) { LOGGER.error("Unable to close data file {}", table.getPath(), e); } } } /** * Load a table from a unencrypted input stream. The table loader loads the data directly from the input stream * and closes the input stream after the reading operation. The table is read until no more item is provided. * <p/> * <b>Important:</b> The first line is assumed as header line and thrown away at the reading operation. * <p/> * * @param resource the input stream the table loader shall read * @param ndsc true in case the table that shall be loaded is a NDSC table, false if its a simple CSV file * @param callback the call back class that is allowed to parse the values this table loader reads * @param tableDelimiter the delimiter of the table, so the table columns are separated by this string */ public <T extends TableLoader> TableLoader( @Nonnull InputStream resource, boolean ndsc, @Nonnull TableLoaderSink<T> callback, @Nonnull String tableDelimiter) { this(tableDelimiter); try { loadTable(resource, ndsc, callback); } catch (@Nonnull IOException e) { LOGGER.error("Error reading the resource stream.", e); throw new NoResourceException("Error reading resource stream."); } } /** * Load a table from the jar file resources. The table needs to be in the resources and its file name ending is * {@code .dat}. The file is taken as encrypted and is decrypted using {@link Crypto}. * <p/> * <b>Important:</b> The first line is assumed as header line and thrown away at the reading operation. * <p/> * * @param table the name of the table that shall be loaded * @param ndsc true in case the table that shall be loaded is a NDSC table, false if its a simple CSV file * @param callback the call back class that is allowed to parse the values this table loader reads * @param tableDelimiter the delimiter of the table, so the table columns are separated by this string */ public <T extends TableLoader> TableLoader( @Nonnull String table, boolean ndsc, @Nonnull TableLoaderSink<T> callback, @Nonnull String tableDelimiter) { this(tableDelimiter); if (crypto == null) { throw new IllegalStateException("This constructor requires a Cryptography instance to be present."); } // read table via class loader InputStream rsc = Thread.currentThread().getContextClassLoader().getResourceAsStream(table + ".dat"); if (rsc == null) { throw new NoResourceException("Missing table " + table); } try { // load data InputStream decryptedStream = crypto.getDecryptedStream(rsc); loadTable(decryptedStream, ndsc, callback); } catch (@Nonnull IOException e) { LOGGER.error("Error reading table {}", table, e); throw new NoResourceException("Error reading table " + table, e); } catch (@Nonnull CryptoException e) { LOGGER.error("Error decrypting table {}", table, e); throw new NoResourceException("Error reading table " + table, e); } finally { try { rsc.close(); } catch (@Nonnull IOException ignored) { } } } /** * Load a table from the jar file resources. The table needs to be in the * resources and its file name ending is {@code .dat}. The * table is loaded as a NDSC table that was created by the config tool and * its delimiter is {@code ,}. Also the file is taken as encrypted and * is decrypted using {@link Crypto}. * <p/> * <b>Important:</b> The first line is assumed as header line and thrown * away at the reading operation. * <p/> * * @param table the name of the table that shall be loaded * @param callback the call back class that is allowed to parse the values * this table loader reads */ public <T extends TableLoader> TableLoader(@Nonnull String table, @Nonnull TableLoaderSink<T> callback) { this(table, true, callback, ","); } /** * Basic constructor that just instantiates the final values. This * constructor is called by all other constructors. * * @param newDelimiter the delimiter used by this table loader */ private TableLoader(@Nonnull String newDelimiter) { tokens = new ArrayList<>(); delimiter = newDelimiter; } /** * Set the Crypto instance that is used to decrypt the tables from the resources. This crypto instance needs to * be fully set up so the table loader can use it right away. All instances of the table loader will use this * crypto class. * * @param newCrypto the crypto instance that shall be used by all table * loaders */ public static void setCrypto(@Nullable Crypto newCrypto) { crypto = newCrypto; } /** * Return the string representation of a token that was read in the last * line with a given index. * * @param index the index of the token that shall be read * @return the token as string or the string {@code <missing>} */ @Nonnull @Contract(pure = true) public String get(int index) { if (index < tokens.size()) { String token = tokens.get(index); if (token != null) { return token; } } LOGGER.error("Missing element in line at {}", tokens.get(0)); return "<missing>"; } /** * Return the boolean representation of a token that was read with the last * line at a given index. The token is true for all contents but * {@code 0}. * * @param index the index of the token that shall be read * @return the boolean value of the token */ @Contract(pure = true) public boolean getBoolean(int index) { String tokenValue = get(index); return !"0".equals(tokenValue); } /** * Return the integer representation of a token that was read with the last * line at a given index. * * @param index the index of the token that shall be read * @return the integer value of the token */ @Contract(pure = true) public int getInt(int index) { String tokenValue = get(index); return Integer.parseInt(tokenValue); } /** * Return the string representation of a token that was read in the last * line with a given index. * * @param index the index of the token that shall be read * @return the token as string or the string {@code <missing>} */ @Nonnull @Contract(pure = true) public String getString(int index) { return get(index); } /** * Load a table from the stream and close the ressource stream after the * reading operations. * <p/> * <b>Important:</b> The first line is assumed as header line and thrown * away at the reading operation. * <p/> * * @param rsc the resource stream that provides the table data * @param ndsc true for NDSC table, that causes the first two tokes ignored * @param callback the callback class that is allowed to parse the values * this table loader reads * @throws IOException in case there is something wrong with the ressource * stream */ @SuppressWarnings("unchecked") private <T extends TableLoader> void loadTable( @Nonnull InputStream rsc, boolean ndsc, @Nonnull TableLoaderSink<T> callback) throws IOException { try (BufferedReader in = new BufferedReader(new InputStreamReader(rsc, "UTF-8"))) { String line; int lineCount = 0; // skip header in.readLine(); // read all lines while ((line = in.readLine()) != null) { // skip comments and empty lines if (line.isEmpty() || line.startsWith("#")) { continue; } tokens.clear(); // find tokens parseTokens(line, ndsc); if (!callback.processRecord(lineCount, (T) this)) { break; } lineCount++; } } } /** * Parse all the tokens delimited by the set delimiter ({@link #delimiter}) from one line into the tokens array * ({@link #tokens}). The tokens need to be read by the callback function after this function is done in order to * clean the tokens array again. * * @param line the string line that shall be parsed for the tokens * @param ndsc true for ndsc tables. For ndsc tables the first two tokens * are ignored */ private void parseTokens(@Nonnull String line, boolean ndsc) { int pos = 0; // skip table id and color if (ndsc) { pos = line.indexOf(delimiter); pos = line.indexOf(delimiter, pos + 1); pos++; } boolean running = true; while (running) { // find end of token int endPos = line.indexOf(delimiter, pos); if (endPos < 0) { if (line.endsWith(delimiter)) { break; } endPos = line.length(); running = false; } // it's a string if (line.charAt(pos) == '"') { tokens.add(line.substring(pos + 1, endPos - 1)); } else { // copy other data directly tokens.add(line.substring(pos, endPos)); } // move onward one entry pos = endPos + 1; } } }