/* * Copyright 2006-2010 The Scriptella Project Team. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package scriptella.driver.csv; import scriptella.driver.csv.opencsv.CSVReader; import scriptella.expression.PropertiesSubstitutor; import scriptella.spi.AbstractConnection; import scriptella.spi.ParametersCallback; import scriptella.spi.QueryCallback; import scriptella.util.ColumnsMap; import scriptella.util.ExceptionUtils; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Query for CSV file. * * @author Fyodor Kupolov * @version 1.0 */ public class CsvQuery implements ParametersCallback { protected static final Logger LOG = Logger.getLogger(CsvQuery.class.getName()); private ColumnsMap columnsMap; //column_name->column_number mapping private String[] row; private Pattern[][] patterns; private Matcher[][] matchers; private PropertiesSubstitutor substitutor; private CsvConnectionParameters csvParams; /** * Creates a query for CSVReader. * * @param queryReader query CSVReader. * @param substitutor properties substitutor to use. The parameters for the substitutor must be set by a caller. * @param csvParams parsed parameters of the CSV connection */ public CsvQuery(CSVReader queryReader, PropertiesSubstitutor substitutor, CsvConnectionParameters csvParams) { this.substitutor = substitutor; this.csvParams = csvParams; compileQueries(queryReader); closeSilently(queryReader); } /** * Executes a query over a specified text content. * * @param reader CSV content reader. * @param queryCallback callback to use for result set iteration. * @param counter statements counter. * @throws IOException if IO error occurs. */ public void execute(CSVReader reader, QueryCallback queryCallback, AbstractConnection.StatementCounter counter) throws IOException { try { columnsMap = parseHeader(reader); String[] r; final boolean trimLines = csvParams.isTrimLines(); //BUG-52570 Currently trimming is done after the lines is parsed as CSV. //This is wrong, because leading whitespaces confuses parser, i.e. quoted values are not recognized //The solution is to wrap reader with a trimming BufferedReader while ((r = (trimLines ? trim(reader.readNext()) : reader.readNext())) != null) { if (rowMatches(r)) { processRow(queryCallback, r); } } } finally { //clean up closeSilently(reader); } if (patterns != null) { counter.statements += patterns.length; } columnsMap = null; row = null; } protected ColumnsMap parseHeader(CSVReader reader) throws IOException { final ColumnsMap columnsMap = new ColumnsMap(); if (csvParams.isHeaders()) { String[] row = reader.readNext(); if (row != null) { for (int i = 0; i < row.length; i++) { columnsMap.registerColumn(row[i].trim(), i + 1); } } } return columnsMap; } /** * Checks if current CSV row matches any of the specified patterns. * * @param r row to check * @return true if row matches one of queries. */ protected boolean rowMatches(final String[] r) { //Checking border conditions Pattern[][] ptrs = patterns; int columnsCount = r.length; if (ptrs == null) { return true; } else if (columnsCount == 0) { return false; } for (int i = 0; i < ptrs.length; i++) { Pattern[] rowPatterns = ptrs[i]; Matcher[] rowMatchers = matchers[i]; boolean rowMatches = true; int patternsCount = rowPatterns.length; if (patternsCount > columnsCount) { //If patterns length exceeds row columns count continue; //Skip this query line } for (int j = 0; j < patternsCount; j++) { Pattern columnPtr = rowPatterns[j]; if (columnPtr != null) { Matcher m = rowMatchers[j]; String col = r[j]; //Current column value if (m == null) { //create new matcher m = columnPtr.matcher(col); rowMatchers[j] = m; } else { //reuse m.reset(col); } if (!m.find()) { rowMatches = false; break; } } } if (rowMatches) { //If this row matches current patterns return true; } //otherwise continue matching } return false; //no matches } /** * Processes the current row. * <p>This template method may be used for customizations by sublasses. * * @param queryCallback query callback to use. * @param r row to pass as current parameters callback. */ protected void processRow(QueryCallback queryCallback, String[] r) { this.row = r; queryCallback.processRow(this); } /** * Compiles queries into a list of patterns. * * @param r CSV reader for query text. */ @SuppressWarnings("unchecked") void compileQueries(final CSVReader r) { List<String[]> patternList; try { patternList = r.readAll(); } catch (IOException e) { throw new CsvProviderException("Unable to read CSV query", e); } List<Pattern[]> res = null; for (String[] columns : patternList) { Pattern[] patterns = null; trim(columns); for (int i = 0; i < columns.length; i++) { String s = columns[i]; if (s != null && s.length() > 0) { if (patterns == null) { patterns = new Pattern[columns.length]; } try { patterns[i] = Pattern.compile(substitutor.substitute(s), Pattern.CASE_INSENSITIVE | Pattern.DOTALL); } catch (Exception e) { throw new CsvProviderException("Illegal regular expression syntax for query", e, s); } } } if (patterns != null) { //if the line has at least on not empty pattern if (res == null) { res = new ArrayList<Pattern[]>(); } res.add(patterns); } } if (res != null) { int len = res.size(); Pattern[][] ptrs = res.toArray(new Pattern[len][]); //Create the matchers array to reuse for better performance Matcher[][] matchers = new Matcher[len][]; for (int i = 0; i < len; i++) { matchers[i] = new Matcher[ptrs[i].length]; } this.patterns = ptrs; this.matchers = matchers; } } /** * Trims array of strings * * @param s array of strings. * @return the same array instance. */ private String[] trim(String[] s) { if (s != null) { for (int i = 0; i < s.length; i++) { if (s[i] != null) { s[i] = s[i].trim(); } } } return s; } public Object getParameter(final String name) { if (columnsMap == null) { throw new IllegalStateException("CSV Resultset is closed"); } Integer col = columnsMap.find(name); if (col != null && col > 0 && col <= row.length) { //If col is not null and in range return getCurrentRowValueAt(col, name); } else { //otherwise call parent context. return substitutor.getParameters().getParameter(name); } } /** * Returns the value of a specified column. * <p>This method may be overriden to perform conversion etc * <br>2 parameters are passed for performance reasons, so that subclasses may decide wich one to use. * @param columnIndex column index * @param columnName column name. * @return value of the specified column of the current row. */ protected Object getCurrentRowValueAt(int columnIndex, String columnName) { final String s = row[columnIndex - 1]; return csvParams.getPropertyFormatter().parse(columnName, s); } protected ColumnsMap getColumnsMap() { return columnsMap; } /** * Helper method to close CSV reader. * * @param reader CSV reader to close. */ static void closeSilently(CSVReader reader) { try { reader.close(); } catch (Exception e) { ExceptionUtils.ignoreThrowable(e); } } }