0

I have below DBImporter class which is working fine and also inserting data correctly in database table. I am trying to fetch data from .CSV file and inserting into Oracle table.

Till now i was processing only one file in my directory and which is working fine. Now i want to process more than one file. So during run the first file process correctly and inserted data, in second file it started reading data and throw an error as :

java.lang.IllegalArgumentException: SQL array must not be empty

Below is my DBimporter class. I think the error is during final commit batch somewhere in line here but not sure

jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));

@Service
public class DBImporter {

    private final static Logger log = LoggerFactory.getLogger(DBImporter.class);
    private static final List<String> NULL_VALUES = Arrays.asList("", "N.A", "N.A", "UNKNOWN");
    private static final List<String> COL_HEADERS = Arrays.asList("ID", "NM", "TYE", "SA");
    private static final int BATCH_SIZE = 50;

    private boolean eof = false;
    private String tableName;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    @Transactional(rollbackFor = IOException.class)
    public void processFile(BufferedReader reader, String tableName) {
        this.tableName = tableName;
        List<String> sqlBatch = new ArrayList<String>(BATCH_SIZE);

        log.info("Starte auslesen der Daten");
        long t1 = System.currentTimeMillis();
        log.info("Start time: " + t1);

        jdbcTemplate.execute("DELETE FROM " + tableName);

        while (!eof) {
            try {
                Map<String, ColumnData> dbColumns = getDBColumns();

                // Get a list of db column data related to the column headers.
                List<ColumnData> columnData = COL_HEADERS.stream().map(dbColumns::get).collect(toList());

                // Get the next valid data row if its starts from "FRO" or "BO".
                List<String> dataRow = findNextLineStartingWith(reader, "R", "T");

                String query = createSql(columnData, dataRow);
                sqlBatch.add(query);

                // Process batch.
                if (sqlBatch.size() >= BATCH_SIZE) {
                    jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));
                    sqlBatch.clear();
                }
            } catch (IllegalStateException e) {
                break;
            } catch (IOException e) {
                log.error(e.getLocalizedMessage());
            }
        }

        // Commit the final batch.
        jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));
        sqlBatch.clear();

        long delta = System.currentTimeMillis() - t1;
        log.info("Total runtime : " + delta / 1000 + " seconds");
    }

    /**
     * Create a SQL insert query using the data row.
     * 
     * @param tableName  Name of the table.
     * @param columnData Column data list.
     * @param dataRow    Data row to be inserted.
     * @return Generated SQL query string.
     */
    private String createSql(List<ColumnData> columnData, List<String> dataRow) {
        List<String> values = new ArrayList<>(columnData.size());

        for (int i = 0; i < columnData.size(); i++) {
            if (NULL_VALUES.contains(dataRow.get(i))) {
                values.add("NULL");
            } else if (columnData.get(i).getType() >= Types.NUMERIC && columnData.get(i).getType() <= Types.DOUBLE) {
                values.add(dataRow.get(i));
            } else {
                values.add("'" + dataRow.get(i).replace("'", "''") + "'");
            }
        }

        return "INSERT INTO " + tableName + " (" +
        columnData.stream().filter(Objects::nonNull).map(ColumnData::getName).collect(joining(", ")) +
        ", SYSTEM_INSERTED_AT) VALUES (" +
        values.stream().collect(joining(", ")) +
        ", CURRENT_TIMESTAMP)";
    }

    /**
     * Find the next line starting with the given string and split it into columns.
     * 
     * @param reader   BufferedReader object to be used.
     * @param prefixes A list of prefixes to look for in the string.
     * @return List of data objects.
     * @throws IOException
     */
    private List<String> findNextLineStartingWith(BufferedReader reader, String... prefixes) throws IOException {
        while (true) {
            String line = readLineOrThrow(reader);
            for (String prefix : prefixes)
                if (line.startsWith(prefix)) {
                    ArrayList<String> data = new ArrayList<>();
                    // Split the line using the delimiter.
                    data.addAll(Arrays.asList(line.split(";")));

                    // Build the row to be inserted.
                    List<String> row = Arrays.asList(data.get(1), data.get(2).trim(), "", "");                      

                    return row;
                }
        }
    }

    /**
     * Read a single line in the file.
     * 
     * @param reader BufferedReader object to be used.
     * @return
     * @throws IOException
     */
    private String readLineOrThrow(BufferedReader reader) throws IOException {
        String line = reader.readLine();
        if (line == null) {
            this.eof = true;
            throw new IllegalStateException("Unexpected EOF");
        }

        return line.trim();
    }

    /**
     * Read database column metadata.
     * 
     * @param tableName Name of the table to process.
     * @return A map containing column information.
     */
    private Map<String, ColumnData> getDBColumns() {
        Map<String, ColumnData> result = new HashMap<>();
        try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
            ResultSet rs = connection.getMetaData().getColumns(null, null, tableName, null);
            while (rs.next()) {
                String columnName = rs.getString(4).toUpperCase();
                int type = rs.getInt(5);
                result.put(columnName, new ColumnData(columnName, type));
            }
            return result;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}
Andrew
  • 3,632
  • 24
  • 64
  • 113

2 Answers2

1

There is the possibility that your final batch is empty.

This is possible in case you just commited BATCH_SIZE entries and have cleared the sqlBatch. In case your while loop exits at this point of time, there are no elements to commit.

You'll want to fix that by adding a size check, for example:

// Commit the final batch (only if there is something left)
if (sqlBatch.isEmpty() == false) {
    jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));
    sqlBatch.clear();
}

Edit:

As @Vasif pointed out you'll need to reset the eof between different calls of the method.

A simple solution (albeit somewhat hacky) would be

boolean eof = false
while (!eof) {
    try {

    } catch (IllegalStateException e) {
        eof = true;
        break;
    } catch (IOException e) {
        log.error(e.getLocalizedMessage());
    }
}

A proper solution would be to refactor your code so that it does not rely on these exception being thrown.

Some tips:

  • Get rid of readLineOrThrow.
  • Remove the while(true) in findNextLineStartingWith and instead return an empty list if the next line is null.
  • Adjust the outside loop to handle this return value appropriately.
  • (Note: you might also need to break the loop if you get an IOException).
second
  • 4,069
  • 2
  • 9
  • 24
  • you mean i need to change the batch_size which is 50 now ? – Andrew Nov 12 '19 at 15:56
  • can you please tell exactly where i need to make change..i am quiet new and tried this jdbc batchupdate – Andrew Nov 12 '19 at 15:58
  • now the error is gone but the problem is its processing the first file correctly and inserting but during the second file its not inserting anything – Andrew Nov 12 '19 at 16:06
  • i have updated my question and also mentione class file which is passsing file to DBImporter class – Andrew Nov 12 '19 at 16:11
  • I've updated the answer once more. I am not going to write the `proper` code for you, but I left you some tips on what to change. – second Nov 12 '19 at 16:26
  • its working now thanks just boolean eof = false this line we need to add in processfile at top – Andrew Nov 12 '19 at 16:28
  • @Andrew: Consider doing a proper solution anyway, especially if that is not a private project. – second Nov 12 '19 at 16:31
1

Please try below changes:

        // Commit the final batch.
if (sqlBatch.size() > 0){
   jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));
   sqlBatch.clear();
}

And

    @Transactional(rollbackFor = IOException.class)
    public void processFile(BufferedReader reader, String tableName) {
        eof = false;
        ...

But if you want a more clear and safe solution do changes in your code as below:

public class DBImporter {

    private final static Logger log = LoggerFactory.getLogger(DBImporter.class);
    private static final List<String> NULL_VALUES = Arrays.asList("", "N.A", "N.A", "UNKNOWN");
    private static final List<String> COL_HEADERS = Arrays.asList("USER_ID", "NAME", "TYPE", "SRC_DATA");
    private static final int BATCH_SIZE = 50;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = IOException.class)
    public void processFile(BufferedReader reader, String tableName) {
        AtomicBoolean eof = new AtomicBoolean(false);
        List<String> sqlBatch = new ArrayList<String>(BATCH_SIZE);

        log.info("Starte auslesen der Daten");
        long t1 = System.currentTimeMillis();
        log.info("Start time: " + t1);

        jdbcTemplate.execute("DELETE FROM " + tableName);

        while (!eof.get()) {
            try {
                Map<String, ColumnData> dbColumns = getDBColumns(tableName);

                // Get a list of db column data related to the column headers.
                List<ColumnData> columnData = COL_HEADERS.stream().map(dbColumns::get).collect(toList());

                // Get the next valid data row if its starts from "R" or "T".
                List<String> dataRow = findNextLineStartingWith(reader, eof, "R", "T");

                String query = createSql(tableName, columnData, dataRow);
                sqlBatch.add(query);

                // Process batch.
                if (sqlBatch.size() >= BATCH_SIZE) {
                    jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));
                    sqlBatch.clear();
                }
            } catch (IllegalStateException e) {
                break;
            } catch (IOException e) {
                log.error(e.getLocalizedMessage());
            }
        }

        // Commit the final batch.
        jdbcTemplate.batchUpdate(sqlBatch.toArray(new String[sqlBatch.size()]));
        sqlBatch.clear();

        long delta = System.currentTimeMillis() - t1;
        log.info("Total runtime : " + delta / 1000 + " seconds");
    }

    /**
     * Create a SQL insert query using the data row.
     *
     * @param tableName  Name of the table.
     * @param columnData Column data list.
     * @param dataRow    Data row to be inserted.
     * @return Generated SQL query string.
     */
    private String createSql(String tableName, List<ColumnData> columnData, List<String> dataRow) {
        List<String> values = new ArrayList<>(columnData.size());

        for (int i = 0; i < columnData.size(); i++) {
            if (NULL_VALUES.contains(dataRow.get(i))) {
                values.add("NULL");
            } else if (columnData.get(i).getType() >= Types.NUMERIC && columnData.get(i).getType() <= Types.DOUBLE) {
                values.add(dataRow.get(i));
            } else {
                values.add("'" + dataRow.get(i).replace("'", "''") + "'");
            }
        }

        return "INSERT INTO " + tableName + " (" +
                columnData.stream().filter(Objects::nonNull).map(ColumnData::getName).collect(joining(", ")) +
                ", SYSTEM_INSERTED_AT) VALUES (" +
                values.stream().collect(joining(", ")) +
                ", CURRENT_TIMESTAMP)";
    }

    /**
     * Find the next line starting with the given string and split it into columns.
     *
     * @param reader   BufferedReader object to be used.
     * @param prefixes A list of prefixes to look for in the string.
     * @return List of data objects.
     * @throws IOException
     */
    private List<String> findNextLineStartingWith(BufferedReader reader, AtomicBoolean eof, String... prefixes) throws IOException {
        while (true) {
            String line = readLineOrThrow(reader, eof);
            for (String prefix : prefixes)
                if (line.startsWith(prefix)) {
                    ArrayList<String> data = new ArrayList<>();
                    // Split the line using the delimiter.
                    data.addAll(Arrays.asList(line.split(";")));

                    // Build the row to be inserted.
                    List<String> row = Arrays.asList(data.get(1), data.get(2).trim(), "", "");

                    // Insert type depending on the prefix.
                    if (prefix.equals("R"))
                        row.set(2, "USER");
                    else if (prefix.equals("T"))
                        row.set(2, "PERM");

                    row.set(3, String.join(";", row.subList(0, 3)));

                    return row;
                }
        }
    }

    /**
     * Read a single line in the file.
     *
     * @param reader BufferedReader object to be used.
     * @return
     * @throws IOException
     */
    private String readLineOrThrow(BufferedReader reader, AtomicBoolean eof) throws IOException {
        String line = reader.readLine();
        if (line == null) {
            eof.set(true);
            throw new IllegalStateException("Unexpected EOF");
        }

        return line.trim();
    }

    /**
     * Read database column metadata.
     *
     * @param tableName Name of the table to process.
     * @return A map containing column information.
     */
    private Map<String, ColumnData> getDBColumns(String tableName) {
        Map<String, ColumnData> result = new HashMap<>();
        try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
            ResultSet rs = connection.getMetaData().getColumns(null, null, tableName, null);
            while (rs.next()) {
                String columnName = rs.getString(4).toUpperCase();
                int type = rs.getInt(5);
                result.put(columnName, new ColumnData(columnName, type));
            }
            return result;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

Vasif
  • 668
  • 4
  • 10
  • the error is gone but the problem is its processing the first file correctly and inserting but during the second file its not inserting anything – Andrew Nov 12 '19 at 16:06
  • 1
    Try to move `private boolean eof = false;` inside your method `processFile ` and pass it as a argument to other mothods where you need. – Vasif Nov 12 '19 at 16:10
  • @Vasif: Good point. If the `eof` is not reset between the passing of differents files it wont work. – second Nov 12 '19 at 16:12
  • can you please update in answer where i should pass this exactly.. i tried to remove the private boolean eof = false and trying to move in processFile but getting an error – Andrew Nov 12 '19 at 16:13
  • 2
    or you can simply on 1st line of your `processFile` reset it's value as `eof = false;`. Just be sure this is not right way of doing it. You Service should be thread safe and shouldn't contain any state – Vasif Nov 12 '19 at 16:25