Print the data in ResultSet along with column names - java

I am retrieving columns names from a SQL database through Java. I know I can retrieve columns names from ResultSet too. So I have this sql query
select column_name from information_schema.columns where table_name='suppliers'
The problem is I don't know how can I get columns names from ResultSet and my code is
public void getAllColumnNames() throws Exception{
String sql = "SELECT column_name from information_schema.columns where table_name='suppliers'";
PreparedStatement ps = connection.prepareStatement(sql);
ResultSet rs = ps.executeQuery(sql);
// extract values from rs
}

ResultSet resultSet = statement.executeQuery("SELECT * from foo");
ResultSetMetaData rsmd = resultSet.getMetaData();
int columnsNumber = rsmd.getColumnCount();
while (resultSet.next()) {
for (int i = 1; i <= columnsNumber; i++) {
if (i > 1) System.out.print(", ");
String columnValue = resultSet.getString(i);
System.out.print(columnValue + " " + rsmd.getColumnName(i));
}
System.out.println("");
}
Reference : Printing the result of ResultSet

1) Instead of PreparedStatement use Statement
2) After executing query in ResultSet, extract values with the help of rs.getString() as :
Statement st=cn.createStatement();
ResultSet rs=st.executeQuery(sql);
while(rs.next())
{
rs.getString(1); //or rs.getString("column name");
}

use further as
rs.getString(1);
rs.getInt(2);
1, 2 is the column number of table and set int or string as per data-type of coloumn

For those who wanted more better version of the resultset printing as util class
This was really helpful for printing resultset and does many things from a single util... thanks to Hami Torun!
In this class printResultSet uses ResultSetMetaData in a generic way have a look at it..
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
public final class DBTablePrinter {
/**
* Column type category for CHAR, VARCHAR
* and similar text columns.
*/
public static final int CATEGORY_STRING = 1;
/**
* Column type category for TINYINT, SMALLINT,
* INT and BIGINT columns.
*/
public static final int CATEGORY_INTEGER = 2;
/**
* Column type category for REAL, DOUBLE,
* and DECIMAL columns.
*/
public static final int CATEGORY_DOUBLE = 3;
/**
* Column type category for date and time related columns like
* DATE, TIME, TIMESTAMP etc.
*/
public static final int CATEGORY_DATETIME = 4;
/**
* Column type category for BOOLEAN columns.
*/
public static final int CATEGORY_BOOLEAN = 5;
/**
* Column type category for types for which the type name
* will be printed instead of the content, like BLOB,
* BINARY, ARRAY etc.
*/
public static final int CATEGORY_OTHER = 0;
/**
* Default maximum number of rows to query and print.
*/
private static final int DEFAULT_MAX_ROWS = 10;
/**
* Default maximum width for text columns
* (like a VARCHAR) column.
*/
private static final int DEFAULT_MAX_TEXT_COL_WIDTH = 150;
/**
* Overloaded method that prints rows from table tableName
* to standard out using the given database connection
* conn. Total number of rows will be limited to
* {#link #DEFAULT_MAX_ROWS} and
* {#link #DEFAULT_MAX_TEXT_COL_WIDTH} will be used to limit
* the width of text columns (like a VARCHAR column).
*
* #param conn Database connection object (java.sql.Connection)
* #param tableName Name of the database table
*/
public static void printTable(Connection conn, String tableName) {
printTable(conn, tableName, DEFAULT_MAX_ROWS, DEFAULT_MAX_TEXT_COL_WIDTH);
}
/**
* Overloaded method that prints rows from table tableName
* to standard out using the given database connection
* conn. Total number of rows will be limited to
* maxRows and
* {#link #DEFAULT_MAX_TEXT_COL_WIDTH} will be used to limit
* the width of text columns (like a VARCHAR column).
*
* #param conn Database connection object (java.sql.Connection)
* #param tableName Name of the database table
* #param maxRows Number of max. rows to query and print
*/
public static void printTable(Connection conn, String tableName, int maxRows) {
printTable(conn, tableName, maxRows, DEFAULT_MAX_TEXT_COL_WIDTH);
}
/**
* Overloaded method that prints rows from table tableName
* to standard out using the given database connection
* conn. Total number of rows will be limited to
* maxRows and
* maxStringColWidth will be used to limit
* the width of text columns (like a VARCHAR column).
*
* #param conn Database connection object (java.sql.Connection)
* #param tableName Name of the database table
* #param maxRows Number of max. rows to query and print
* #param maxStringColWidth Max. width of text columns
*/
public static void printTable(Connection conn, String tableName, int maxRows, int maxStringColWidth) {
if (conn == null) {
System.err.println("DBTablePrinter Error: No connection to database (Connection is null)!");
return;
}
if (tableName == null) {
System.err.println("DBTablePrinter Error: No table name (tableName is null)!");
return;
}
if (tableName.length() == 0) {
System.err.println("DBTablePrinter Error: Empty table name!");
return;
}
if (maxRows
* ResultSet to standard out using {#link #DEFAULT_MAX_TEXT_COL_WIDTH}
* to limit the width of text columns.
*
* #param rs The ResultSet to print
*/
public static void printResultSet(ResultSet rs) {
printResultSet(rs, DEFAULT_MAX_TEXT_COL_WIDTH);
}
/**
* Overloaded method to print rows of a
* ResultSet to standard out using maxStringColWidth
* to limit the width of text columns.
*
* #param rs The ResultSet to print
* #param maxStringColWidth Max. width of text columns
*/
public static void printResultSet(ResultSet rs, int maxStringColWidth) {
try {
if (rs == null) {
System.err.println("DBTablePrinter Error: Result set is null!");
return;
}
if (rs.isClosed()) {
System.err.println("DBTablePrinter Error: Result Set is closed!");
return;
}
if (maxStringColWidth columns = new ArrayList(columnCount);
// List of table names. Can be more than one if it is a joined
// table query
List tableNames = new ArrayList(columnCount);
// Get the columns and their meta data.
// NOTE: columnIndex for rsmd.getXXX methods STARTS AT 1 NOT 0
for (int i = 1; i maxStringColWidth) {
value = value.substring(0, maxStringColWidth - 3) + "...";
}
break;
}
// Adjust the column width
c.setWidth(value.length() > c.getWidth() ? value.length() : c.getWidth());
c.addValue(value);
} // END of for loop columnCount
rowCount++;
} // END of while (rs.next)
/*
At this point we have gone through meta data, get the
columns and created all Column objects, iterated over the
ResultSet rows, populated the column values and adjusted
the column widths.
We cannot start printing just yet because we have to prepare
a row separator String.
*/
// For the fun of it, I will use StringBuilder
StringBuilder strToPrint = new StringBuilder();
StringBuilder rowSeparator = new StringBuilder();
/*
Prepare column labels to print as well as the row separator.
It should look something like this:
+--------+------------+------------+-----------+ (row separator)
| EMP_NO | BIRTH_DATE | FIRST_NAME | LAST_NAME | (labels row)
+--------+------------+------------+-----------+ (row separator)
*/
// Iterate over columns
for (Column c : columns) {
int width = c.getWidth();
// Center the column label
String toPrint;
String name = c.getLabel();
int diff = width - name.length();
if ((diff % 2) == 1) {
// diff is not divisible by 2, add 1 to width (and diff)
// so that we can have equal padding to the left and right
// of the column label.
width++;
diff++;
c.setWidth(width);
}
int paddingSize = diff / 2; // InteliJ says casting to int is redundant.
// Cool String repeater code thanks to user102008 at stackoverflow.com
String padding = new String(new char[paddingSize]).replace("\0", " ");
toPrint = "| " + padding + name + padding + " ";
// END centering the column label
strToPrint.append(toPrint);
rowSeparator.append("+");
rowSeparator.append(new String(new char[width + 2]).replace("\0", "-"));
}
String lineSeparator = System.getProperty("line.separator");
// Is this really necessary ??
lineSeparator = lineSeparator == null ? "\n" : lineSeparator;
rowSeparator.append("+").append(lineSeparator);
strToPrint.append("|").append(lineSeparator);
strToPrint.insert(0, rowSeparator);
strToPrint.append(rowSeparator);
StringJoiner sj = new StringJoiner(", ");
for (String name : tableNames) {
sj.add(name);
}
String info = "Printing " + rowCount;
info += rowCount > 1 ? " rows from " : " row from ";
info += tableNames.size() > 1 ? "tables " : "table ";
info += sj.toString();
System.out.println(info);
// Print out the formatted column labels
System.out.print(strToPrint.toString());
String format;
// Print out the rows
for (int i = 0; i
* Integers should not be truncated so column widths should
* be adjusted without a column width limit. Text columns should be
* left justified and can be truncated to a max. column width etc...
*
* See also:
* java.sql.Types
*
* #param type Generic SQL type
* #return The category this type belongs to
*/
private static int whichCategory(int type) {
switch (type) {
case Types.BIGINT:
case Types.TINYINT:
case Types.SMALLINT:
case Types.INTEGER:
return CATEGORY_INTEGER;
case Types.REAL:
case Types.DOUBLE:
case Types.DECIMAL:
return CATEGORY_DOUBLE;
case Types.DATE:
case Types.TIME:
case Types.TIME_WITH_TIMEZONE:
case Types.TIMESTAMP:
case Types.TIMESTAMP_WITH_TIMEZONE:
return CATEGORY_DATETIME;
case Types.BOOLEAN:
return CATEGORY_BOOLEAN;
case Types.VARCHAR:
case Types.NVARCHAR:
case Types.LONGVARCHAR:
case Types.LONGNVARCHAR:
case Types.CHAR:
case Types.NCHAR:
return CATEGORY_STRING;
default:
return CATEGORY_OTHER;
}
}
/**
* Represents a database table column.
*/
private static class Column {
/**
* Column label.
*/
private String label;
/**
* Generic SQL type of the column as defined in
*
* java.sql.Types
* .
*/
private int type;
/**
* Generic SQL type name of the column as defined in
*
* java.sql.Types
* .
*/
private String typeName;
/**
* Width of the column that will be adjusted according to column label
* and values to be printed.
*/
private int width = 0;
/**
* Column values from each row of a ResultSet.
*/
private List values = new ArrayList();
/**
* Flag for text justification using String.format.
* Empty string ("") to justify right,
* dash (-) to justify left.
*
* #see #justifyLeft()
*/
private String justifyFlag = "";
/**
* Column type category. The columns will be categorised according
* to their column types and specific needs to print them correctly.
*/
private int typeCategory = 0;
/**
* Constructs a new Column with a column label,
* generic SQL type and type name (as defined in
*
* java.sql.Types
* )
*
* #param label Column label or name
* #param type Generic SQL type
* #param typeName Generic SQL type name
*/
public Column(String label, int type, String typeName) {
this.label = label;
this.type = type;
this.typeName = typeName;
}
/**
* Returns the column label
*
* #return Column label
*/
public String getLabel() {
return label;
}
/**
* Returns the generic SQL type of the column
*
* #return Generic SQL type
*/
public int getType() {
return type;
}
/**
* Returns the generic SQL type name of the column
*
* #return Generic SQL type name
*/
public String getTypeName() {
return typeName;
}
/**
* Returns the width of the column
*
* #return Column width
*/
public int getWidth() {
return width;
}
/**
* Sets the width of the column to width
*
* #param width Width of the column
*/
public void setWidth(int width) {
this.width = width;
}
/**
* Adds a String representation (value)
* of a value to this column object's {#link #values} list.
* These values will come from each row of a
*
* ResultSet
* of a database query.
*
* #param value The column value to add to {#link #values}
*/
public void addValue(String value) {
values.add(value);
}
/**
* Returns the column value at row index i.
* Note that the index starts at 0 so that getValue(0)
* will get the value for this column from the first row
* of a
* ResultSet.
*
* #param i The index of the column value to get
* #return The String representation of the value
*/
public String getValue(int i) {
return values.get(i);
}
/**
* Returns the value of the {#link #justifyFlag}. The column
* values will be printed using String.format and
* this flag will be used to right or left justify the text.
*
* #return The {#link #justifyFlag} of this column
* #see #justifyLeft()
*/
public String getJustifyFlag() {
return justifyFlag;
}
/**
* Sets {#link #justifyFlag} to "-" so that
* the column value will be left justified when printed with
* String.format. Typically numbers will be right
* justified and text will be left justified.
*/
public void justifyLeft() {
this.justifyFlag = "-";
}
/**
* Returns the generic SQL type category of the column
*
* #return The {#link #typeCategory} of the column
*/
public int getTypeCategory() {
return typeCategory;
}
/**
* Sets the {#link #typeCategory} of the column
*
* #param typeCategory The type category
*/
public void setTypeCategory(int typeCategory) {
this.typeCategory = typeCategory;
}
}
}
This is the scala version of doing this... which will print column names and data as well in a generic way...
def printQuery(res: ResultSet): Unit = {
val rsmd = res.getMetaData
val columnCount = rsmd.getColumnCount
var rowCnt = 0
val s = StringBuilder.newBuilder
while (res.next()) {
s.clear()
if (rowCnt == 0) {
s.append("| ")
for (i <- 1 to columnCount) {
val name = rsmd.getColumnName(i)
s.append(name)
s.append("| ")
}
s.append("\n")
}
rowCnt += 1
s.append("| ")
for (i <- 1 to columnCount) {
if (i > 1)
s.append(" | ")
s.append(res.getString(i))
}
s.append(" |")
System.out.println(s)
}
System.out.println(s"TOTAL: $rowCnt rows")
}

Have a look at the documentation. You made the following mistakes.
Firstly, ps.executeQuery() doesn't have any parameters. Instead you passed the SQL query into it.
Secondly, regarding the prepared statement, you have to use the ? symbol if want to pass any parameters. And later bind it using
setXXX(index, value)
Here xxx stands for the data type.

You can use the TableSaw library, which is a great Java data science / data frame library. The code is simple then:
System.out.println(Table.read().db(resultSet).print());

For what you are trying to do, instead of PreparedStatement you can use Statement. Your code may be modified as-
String sql = "SELECT column_name from information_schema.columns where table_name='suppliers';";
Statement s = connection.createStatement();
ResultSet rs = s.executeQuery(sql);
Hope this helps.

Solution with rowMapper impl.
Repository class:
jdbcTemplate.query(sqlRequest, new ResponseQuikRowMapper())
Row mapper class:
public class ResponseQuikRowMapper implements RowMapper<Map<String, String>> {
#Override
#SneakyThrows
public Map<String, String> mapRow(ResultSet rs, int rowNum) {
return IntStream.range(1, rs.getMetaData().getColumnCount())
.mapToObj(columnNumber -> getColumnName(rs, columnNumber))
.collect(Collectors.toMap(column -> column, value -> getColumnValue(rs, value)));
}
#SneakyThrows
private String getColumnName(ResultSet rs, Integer columnNumber) {
return rs.getMetaData().getColumnName(columnNumber);
}
#SneakyThrows
private String getColumnValue(ResultSet rs, String column) {
return Optional.ofNullable(rs.getString(column)).orElse("");
}
HashMap it`s simple matrix where key is column name and value is value :)

If you are using Spring data, you could try this:
var mapped = new ColumnMapRowMapper().mapRow(resultSet, rowNum);
log.debug("Result={}", mapped);

Related

Java Hash table linear probing

Hi I am having issues printing and/or adding entries to my hash table I am looking to handle collisions with linear probing. Could someone help me? This is NOT a school project or assignment, just for fun.
I am new to the Java programming language.
I cannot get all of the entries to output to the console and I am wondering where I am going wrong.
/**
* File : ContactTable.java
* Project : None
* Programmer : Braiden Gole
* First version : 2020-05-20
* Description : This is the implementation of a hash table algorithm. We will be
* storing a record with three pieces of information for contacting
* a customer that is: First name, last name, email.
*/
import java.util.ArrayList;
import java.util.Scanner;
import java.util.InputMismatchException;
class ContactTable {
/** -- Class header comment
* Name : HashNode
* Purpose : This represents an entry in our hash table.
*/
static class HashNode {
String firstName;
String lastName;
String emailAddress;
int position;
HashNode next;
}
/** -- Class header comment
* Name : TableMapper
* Purpose : This class represents the operations that our hash table will be
* able to carry out.
*/
static class TableMapper {
private ArrayList<HashNode> hashTable = new ArrayList<>();
private final int KTABLELIMIT = 5;
/** -- Method header comment
* Method : initializeContactTable
* Description : This will add (null) markers so we can detect when a position
* has been filled with an entry or not.
* Parameters : None
* Returns : None
*/
private void initializeContactTable() {
for (int positions = 0; positions < KTABLELIMIT; positions++) {
hashTable.add(null);
}
}
/** -- Method header comment
* Method : hashMethod
* Description : This is the hash method to calculate the position at which the
* record of contact will sit in the contact table. We will be
* using the name field to store the record.
* Parameters : key
* Returns : hashValue % KTABLELIMIT
*/
private int hashMethod(String key) {
int hashValue = 0;
for (int letters = 0; letters < key.length(); letters++) {
hashValue += key.charAt(letters);
}
return hashValue % KTABLELIMIT;
}
/** -- Method header commment
* Method : insertRecord
* Description : This method will insert the entire object record into the
* has table. We will handle collisions with linear probing and
* make use of the "wrap around method."
* Parameters : key, position lastName, email
* Returns : currentSize
*/
private int insertRecord(String key, int position, String lastName, String email) {
HashNode checkPosition = hashTable.get(position);
HashNode newRecord = new HashNode();
// Check to make sure that the name does not already exist.
while (checkPosition != null) {
if ((checkPosition.firstName.equals(key)) == true) { return 1; }
checkPosition = checkPosition.next;
}
// The calculated position has been moved, recalculate.
checkPosition = hashTable.get(position);
if (checkPosition == null) {
newRecord.firstName = key;
newRecord.position = position;
newRecord.lastName = lastName;
newRecord.emailAddress = email;
newRecord.next = checkPosition;
hashTable.add(position, newRecord);
return 0;
} else {
while ((hashTable.get(position)) != null) {
++position;
position %= KTABLELIMIT;
}
newRecord.firstName = key;
newRecord.position = position;
newRecord.lastName = lastName;
newRecord.emailAddress = email;
newRecord.next = hashTable.get(position);
hashTable.add(position, newRecord);
return 0;
}
}
/** -- Method header comment
* Method : showContacts
* Description : This will display all contact records to the console.
* Parameters : None
* Returns : None
*/
private void showContacts() {
for (int entries = 0; entries < KTABLELIMIT; entries++) {
if ((hashTable.get(entries)) != null) {
System.out.println();
System.out.println(hashTable.get(entries).firstName);
System.out.println(hashTable.get(entries).lastName);
System.out.println(hashTable.get(entries).emailAddress);
}
}
}
}
public static void main(String[] args) {
TableMapper mapper = new TableMapper();
Scanner reader = new Scanner(System.in);
// The primaryKey is the users first name.
String primaryKey = "";
String lastName = "";
String email = "";
int index = 0;
int currentSize = 0;
int returnInsertVal = 0;
// Initialize the contact table with (null) markers.
mapper.initializeContactTable();
boolean contactEntryCondition = true;
while (contactEntryCondition) {
System.out.println();
System.out.print("Enter in your first name: ");
primaryKey = reader.next();
System.out.print("Enter in your last name: ");
lastName = reader.next();
System.out.print("Enter in your email address: ");
email = reader.next();
// Calculate the index at which the record will sit.
index = mapper.hashMethod(primaryKey);
// Use the calculated index to insert the record at the proper position.
returnInsertVal = mapper.insertRecord(primaryKey, index, lastName, email);
// When the table is full exit the loop and print the table.
if (currentSize == mapper.KTABLELIMIT) { contactEntryCondition = false; }
if (returnInsertVal == 0) { ++currentSize; }
else { primaryKey = ""; }
System.out.println(currentSize);
}
reader.close();
// Output the contact records.
mapper.showContacts();
}
}
after each hashTable.add(position, newRecord) your underlying list (hashTable) grows in size by one
void add(int index, E element)
Inserts the specified element at the specified position in this list (optional > operation). Shifts the element currently at that position (if any) and any subsequent elements to the right (adds one to their indices).
this messes up the "bookkeeping" that you do on the table.
in showContacts(), change the for line as follows:
for (int entries = 0; entries < hashTable.size(); entries++) {
and it will print all the elements inside the table.
you should rethink your implementation and use a "fixed size underlying list" (possibly an array) enlarging it only when it's necessary and rehashing when you do so.

Checking With Assertion

The methods setDates and setTimes have as preconditions that none of their arguments are null. This is to be checked by means of an assertion. (This means that if the precondition is not met, the program will fail at the assertion, and an AssertionError will be thrown.)
here is my code:
public class Section
{
/**
* Default section number.
*/
private static final String DEFAULT_SECTION_NUMBER = "";
/**
* Constant for building Strings with newline characters within them.
*/
private static final String LINE_SEPARATOR = System.
getProperty("line.separator");
/**
* The maximum number of students permitted into a section.
*/
private static final int MAXIMUM_STUDENTS_PER_SECTION = 30;
/**
* Valid length for a sectionNumber string.
*/
private static final int SECTION_NUMBER_LENGTH = 3;
/**
* Shared variable for keeping count of the number of section objects in
* existence.
*/
private static int count = 0;
/**
* The date at which the section is finished.
*/
private Date endDate;
/**
* The end time for the meeting of the section.
*/
private Time2 endTime;
/**
* The list of students in the class. This declaration uses the Java 7 facility
* of not repeating the generic type if that type can be inferred by the
* compiler.
*/
private final List<Student> roster = new ArrayList<>();
/**
* The three-character designation of the section (called a
* “number”).
*/
private String sectionNumber = DEFAULT_SECTION_NUMBER;
/**
* The date on which the section starts to meet.
*/
private Date startDate;
/**
* The time of day at which the section meets.
*/
private Time2 startTime;
/**
* The course of which this is a section.
*/
private final Course thisCourse;
/**
* Constructor.
*
* #param course the course of which this is a section
* #param sectionNumber the section number (within the course) of this section
* #throws SectionException
*/
public Section(Course course, String sectionNumber) throws SectionException
{
/* Increment the collective count of all Section objects that have been
created. Do this first as the object already exists. */
++count;
this.thisCourse = course;
try
{
if( isValidSectionNumber(sectionNumber) )
this.sectionNumber = sectionNumber;
}
catch (Exception ex)
{
throw new SectionException("Error in constructor", ex);
}
}
/**
* Add a student to the course.
*
* #param student the student object to be added. If the course is full, the
* student is not added
*/
public void addStudent(Student student)
{
if( roster.size() != MAXIMUM_STUDENTS_PER_SECTION )
roster.add(student);
}
/**
* Get details about the current state of this section, including the course of
* which it is part, the dates it starts and ends, times, etc., and the current
* count of the enrollment.
*
* #return the section details
*/
public String getDetails()
{
return String.join(LINE_SEPARATOR,
"Section: " + this.toString(),
"Course: " + thisCourse.getDetails(),
"Dates: " + startDate + " to " + endDate,
"Times: " + startTime + " to " + endTime,
"Enrollment: " + roster.size());
}
/**
* Create a string that represents the information about the students in the
* course.
*
* #return a string that represents the information about the students in the
* course
*/
public String getRoster()
{
/* The following commented-out code is the obvious way to do this, using
String concatenation (and this is acceptable). However, the recommended
Java approach to this kind of operation is to use a StringJoiner (new
class in Java 8), as this uses less garbage collection resources. */
// String result = "";
// for( Student student : roster )
// {
// result += ( result.isEmpty() ? "" : LINE_SEPARATOR) + student;
// }
// return result;
StringJoiner stringJoiner = new StringJoiner(LINE_SEPARATOR);
for( Student student : roster )
stringJoiner.add(student.toString());
return stringJoiner.toString();
}
/**
* Get a count of the number of students registered (on the roster) for this course.
*
* #return a count of the number of students registered for this course
*/
public int getRosterCount()
{
return roster.size();
}
/**
* Get the section number for this course.
*
* #return the section number for this course
*/
public String getSectionNumber()
{
return sectionNumber;
}
/**
* Set the start and end dates for the section.
*
* #param startDate the start date
* #param endDate the end date
*/
public void setDates(Date startDate, Date endDate)
{
/* There is no requirement to validate these. */
this.startDate = startDate;
this.endDate = endDate;
}
/**
* Set the start time and the end time for the meetings of the section.
*
* #param startTime the start time for meetings of the section
* #param endTime the end time for the meetings of the section
*/
public void setTimes(Time2 startTime, Time2 endTime)
{
/* There is no requirement to validate these. */
this.startTime = startTime;
this.endTime = endTime;
}
/**
* Section number (prefixed)
*
* #return Section number (prefixed)
*/
#Override
public String toString()
{
return thisCourse.toString() + "-" + sectionNumber;
}
/**
* Finalization. Reduce the instance count by 1.
*
* #throws Throwable standard interface.
*/
#SuppressWarnings("FinalizeDeclaration")
#Override
protected void finalize() throws Throwable
{
/* Decrement the count of the collective total of all Section objects. */
--count;
super.finalize();
}
/**
* Get a count of how many total Section objects are currently in existence.
*
* #return a count of how many Section objects are currently in existence
*/
public static int getSectionCount()
{
return count;
}
/**
* Validate the sectionNumber string. It must be of the correct length.
*
* #param sectionNumber the sectionNumber string
* #return true if the string if valid, otherwise false
*/
private static boolean isValidSectionNumber(String sectionNumber)
{
return sectionNumber != null &&
sectionNumber.length() == SECTION_NUMBER_LENGTH;
}
}
would i simply place 'assert' before this.startDate = startDate; and so forth??? my book only has one example and it is for ensuring a value is between 0 and 10.
this is the example my book uses:
public class AssertTest
{
public static void main(string[] args)
{
Scanner input = new Scanner(System.in);
System.out.print("Enter a number between 0 and 10: ");
int number = input.nextInt();
//assert that the value is >= 0 and <= 10
assert (number >= 0 && number <= 10) : "bad number: " + number;
System.out.printf("You entered %d%n", number);
}
}
so could i say
assert this.startDate = startDate
assert this.endDate = endDate
and so on?
First of all the methods setTime and setDates are public what suggests that they may be used outside of the package. Given that you have no control over parameters - using assert would not be considered as the best practice. You should rather use Runtime Exceptions such as IllegalArgumentException when value can be supplied externally (and you have no control over it):
if (startDate == null || endDate == null)
throw new IllegalArgumentException("Non-null arguments are required");
The syntax for the Assert would be as follows:
assert startDate != null;
assert endDate != null;
You can also use the following syntax in order to output additional information when assertion fails:
assert startDate != null : "startDate was set to null"
assert endDate != null : "endDate was set to null"

accent indiferent regex with jTable

I've a Java regex for a RowFilter that works as a filter for information shown in a table.
The idea is that I've a table with information in it, and below the table is a text field where I write something and filters the rows only if the row has at least on cell with a regex match based on the text entered on the text field.
I've a class that extends AbstractTableModel that's the model that I use for the table. Let's suppose that the class is called ClientesTableModel.
Then I put a event in the text field, the KeyReleased one, which does the following:
private void EventFiredInTextField() {
RowFilter<ClientesTableModel, Object> rf;
try {
rf = RowFilter.regexFilter("(?i)" + jTextFieldFiltro.getText());
} catch (PatternSyntaxException ex) {
return;
}
sorter.setRowFilter(rf);
}
Already the filter is case insensitive. Any way to make it accent insensitive?
Tried what follows already, but it doesn't works if a cell has a string with accents.
private void Filtrar() {
RowFilter<ClientesTableModel, Object> rf;
try {
rf = RowFilter.regexFilter("(?i)" + Normalizer.normalize(jTextFieldFiltro.getText(), Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""));
} catch (PatternSyntaxException ex) {
return;
}
sorter.setRowFilter(rf);
}
And also tried at least 20+ solutions that appears here on SO and another sites, with no luck. None of them seem to be working with a table...
Edit 1:
Tried something more:
private void Filtrar() {
RowFilter<ClientesTableModel, Object> rf;
try {
Map<String, String> replacements = new HashMap();
replacements.put("a", "[aá]");
replacements.put("e", "[eé]");
replacements.put("i", "[ií]");
replacements.put("o", "[oó]");
replacements.put("u", "[uú]");
String regex = "";
for (char c : jTextFieldFiltro.getText().toCharArray()) {
String replacement = replacements.get(Normalizer.normalize(Character.toString(Character.toLowerCase(c)), Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""));
if (replacement == null) {
regex += c;
} else {
regex += replacement;
}
}
rf = RowFilter.regexFilter("(?i)" + regex);
} catch (PatternSyntaxException ex) {
return;
}
sorter.setRowFilter(rf);
}
It just makes some changes in the regex text before it's applied, with the following idea:
If the filter text is edit it will be transformed into [eé]d[ií]t, and it will be a regex that's accent indiferent for Central American letters.
However, I'm experiencing one big problem. I've the flag (?i) in the regex so it's supposed to be case insensitive. But if a cell has for example the text edÍt, the í and Í doesn't catch the (?i) flag, they aren't treated as they should, case insensitive...
A simple solution is to change replacements.put("i", "[ií]"); by replacements.put("i", "[iíÍ]");, but hey... what's the point on putting the (?i) flag then?
Anyway, this solution isn't so elegant, and it will fail for other accent types (like ¨). Ideas?
And sorter variable is of type TableRowSorter<ClientesTableModel>.
SO doesn't allows more than 30000 characters by response, so, I'm going to split this in three answers.
Part 1/3:
After try and error for 4+ hours, found a way. Not so sexy, but it's efficient and it's a real accent indiferent solution for jTables filtering with regexes.
I'd to modify some source files of JDK version 7 update 7. They're DefaultRowSorter and TableRowSorter.
I added a extra class called RowFilterSpecialFilter for coding simplification.
The modified DefaultRowSorter and TableRowSorter classes are called DefaultRowSorterSpecialFilter and TableRowSorterSpecialFilter respectively.
DefaultRowSorterSpecialFilter and RowFilterSpecialFilter are in a package called javax.swing.
TableRowSorterSpecialFilter is in a package called javax.swing.table.
TableRowSorterSpecialFilter is basically the same as TableRowSorter. The only change is that all the ocurrences of TableRowSorter where replaced by TableRowSorterSpecialFilter, and now it inherits from DefaultRowSorterSpecialFilter. Modified file source (TableRowSorterSpecialFilter.java):
/*
* Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javax.swing.table;
import java.text.Collator;
import java.util.*;
import javax.swing.DefaultRowSorterSpecialFilter;
import javax.swing.RowFilter;
/**
* An implementation of
* <code>RowSorter</code> that provides sorting and filtering using a
* <code>TableModel</code>. The following example shows adding sorting to a
* <code>JTable</code>:
* <pre>
* TableModel myModel = createMyTableModel();
* JTable table = new JTable(myModel);
* table.setRowSorter(new TableRowSorterSpecialFilter(myModel));
* </pre> This will do all the wiring such that when the user does the
* appropriate gesture, such as clicking on the column header, the table will
* visually sort. <p>
* <code>JTable</code>'s row-based methods and
* <code>JTable</code>'s selection model refer to the view and not the
* underlying model. Therefore, it is necessary to convert between the two. For
* example, to get the selection in terms of
* <code>myModel</code> you need to convert the indices:
* <pre>
* int[] selection = table.getSelectedRows();
* for (int i = 0; i < selection.length; i++) {
* selection[i] = table.convertRowIndexToModel(selection[i]);
* }
* </pre> Similarly to select a row in
* <code>JTable</code> based on a coordinate from the underlying model do the
* inverse:
* <pre>
* table.setRowSelectionInterval(table.convertRowIndexToView(row),
* table.convertRowIndexToView(row));
* </pre> <p> The previous example assumes you have not enabled filtering. If
* you have enabled filtering
* <code>convertRowIndexToView</code> will return -1 for locations that are not
* visible in the view. <p>
* <code>TableRowSorterSpecialFilter</code> uses
* <code>Comparator</code>s for doing comparisons. The following defines how a
* <code>Comparator</code> is chosen for a column: <ol> <li>If a
* <code>Comparator</code> has been specified for the column by the
* <code>setComparator</code> method, use it. <li>If the column class as
* returned by
* <code>getColumnClass</code> is
* <code>String</code>, use the
* <code>Comparator</code> returned by
* <code>Collator.getInstance()</code>. <li>If the column class implements
* <code>Comparable</code>, use a
* <code>Comparator</code> that invokes the
* <code>compareTo</code> method. <li>If a
* <code>TableStringConverter</code> has been specified, use it to convert the
* values to
* <code>String</code>s and then use the
* <code>Comparator</code> returned by
* <code>Collator.getInstance()</code>. <li>Otherwise use the
* <code>Comparator</code> returned by
* <code>Collator.getInstance()</code> on the results from calling
* <code>toString</code> on the objects. </ol> <p> In addition to sorting
* <code>TableRowSorterSpecialFilter</code> provides the ability to filter. A
* filter is specified using the
* <code>setFilter</code> method. The following example will only show rows
* containing the string "foo":
* <pre>
* TableModel myModel = createMyTableModel();
* TableRowSorterSpecialFilter sorter = new TableRowSorterSpecialFilter(myModel);
* sorter.setRowFilter(RowFilter.regexFilter(".*foo.*"));
* JTable table = new JTable(myModel);
* table.setRowSorter(sorter);
* </pre> <p> If the underlying model structure changes (the
* <code>modelStructureChanged</code> method is invoked) the following are reset
* to their default values:
* <code>Comparator</code>s by column, current sort order, and whether each
* column is sortable. The default sort order is natural (the same as the
* model), and columns are sortable by default. <p>
* <code>TableRowSorterSpecialFilter</code> has one formal type parameter: the
* type of the model. Passing in a type that corresponds exactly to your model
* allows you to filter based on your model without casting. Refer to the
* documentation of
* <code>RowFilter</code> for an example of this. <p> <b>WARNING:</b>
* <code>DefaultTableModel</code> returns a column class of
* <code>Object</code>. As such all comparisons will be done using
* <code>toString</code>. This may be unnecessarily expensive. If the column
* only contains one type of value, such as an
* <code>Integer</code>, you should override
* <code>getColumnClass</code> and return the appropriate
* <code>Class</code>. This will dramatically increase the performance of this
* class.
*
* #param <M> the type of the model, which must be an implementation of
* <code>TableModel</code>
* #see javax.swing.JTable
* #see javax.swing.RowFilter
* #see javax.swing.table.DefaultTableModel
* #see java.text.Collator
* #see java.util.Comparator
* #since 1.6
*/
public final class TableRowSorterSpecialFilter<M extends TableModel> extends DefaultRowSorterSpecialFilter<M, Integer> {
/**
* Comparator that uses compareTo on the contents.
*/
private static final Comparator COMPARABLE_COMPARATOR =
new ComparableComparator();
/**
* Underlying model.
*/
private M tableModel;
/**
* For toString conversions.
*/
private TableStringConverter stringConverter;
/**
* Creates a
* <code>TableRowSorterSpecialFilter</code> with an empty model.
*/
public TableRowSorterSpecialFilter() {
this(null);
}
/**
* Creates a
* <code>TableRowSorterSpecialFilter</code> using
* <code>model</code> as the underlying
* <code>TableModel</code>.
*
* #param model the underlying <code>TableModel</code> to use,
* <code>null</code> is treated as an empty model
*/
public TableRowSorterSpecialFilter(M model) {
setModel(model);
}
/**
* Sets the
* <code>TableModel</code> to use as the underlying model for this
* <code>TableRowSorterSpecialFilter</code>. A value of
* <code>null</code> can be used to set an empty model.
*
* #param model the underlying model to use, or <code>null</code>
*/
public void setModel(M model) {
tableModel = model;
setModelWrapper(new TableRowSorterModelWrapper());
}
/**
* Sets the object responsible for converting values from the model to
* strings. If non-
* <code>null</code> this is used to convert any object values, that do not
* have a registered
* <code>Comparator</code>, to strings.
*
* #param stringConverter the object responsible for converting values from
* the model to strings
*/
public void setStringConverter(TableStringConverter stringConverter) {
this.stringConverter = stringConverter;
}
/**
* Returns the object responsible for converting values from the model to
* strings.
*
* #return object responsible for converting values to strings.
*/
public TableStringConverter getStringConverter() {
return stringConverter;
}
/**
* Returns the
* <code>Comparator</code> for the specified column. If a
* <code>Comparator</code> has not been specified using the
* <code>setComparator</code> method a
* <code>Comparator</code> will be returned based on the column class
* (
* <code>TableModel.getColumnClass</code>) of the specified column. If the
* column class is
* <code>String</code>,
* <code>Collator.getInstance</code> is returned. If the column class
* implements
* <code>Comparable</code> a private
* <code>Comparator</code> is returned that invokes the
* <code>compareTo</code> method. Otherwise
* <code>Collator.getInstance</code> is returned.
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public Comparator<?> getComparator(int column) {
Comparator comparator = super.getComparator(column);
if (comparator != null) {
return comparator;
}
Class columnClass = getModel().getColumnClass(column);
if (columnClass == String.class) {
return Collator.getInstance();
}
if (Comparable.class.isAssignableFrom(columnClass)) {
return COMPARABLE_COMPARATOR;
}
return Collator.getInstance();
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
protected boolean useToString(int column) {
Comparator comparator = super.getComparator(column);
if (comparator != null) {
return false;
}
Class columnClass = getModel().getColumnClass(column);
if (columnClass == String.class) {
return false;
}
if (Comparable.class.isAssignableFrom(columnClass)) {
return false;
}
return true;
}
/**
* Implementation of DefaultRowSorterSpecialFilter.ModelWrapper that
* delegates to a TableModel.
*/
private class TableRowSorterModelWrapper extends ModelWrapper<M, Integer> {
#Override
public M getModel() {
return tableModel;
}
#Override
public int getColumnCount() {
return (tableModel == null) ? 0 : tableModel.getColumnCount();
}
#Override
public int getRowCount() {
return (tableModel == null) ? 0 : tableModel.getRowCount();
}
#Override
public Object getValueAt(int row, int column) {
return tableModel.getValueAt(row, column);
}
#Override
public String getStringValueAt(int row, int column) {
TableStringConverter converter = getStringConverter();
if (converter != null) {
// Use the converter
String value = converter.toString(
tableModel, row, column);
if (value != null) {
return value;
}
return "";
}
// No converter, use getValueAt followed by toString
Object o = getValueAt(row, column);
if (o == null) {
return "";
}
String string = o.toString();
if (string == null) {
return "";
}
return string;
}
#Override
public Integer getIdentifier(int index) {
return index;
}
}
private static class ComparableComparator implements Comparator {
#SuppressWarnings("unchecked")
#Override
public int compare(Object o1, Object o2) {
return ((Comparable) o1).compareTo(o2);
}
}
}
In the case of DefaultRowSorterSpecialFilter, it has more changes than TableRowSorterSpecialFilter. Basically it has a extra property, public boolean accentIndiferent which starts in false and the overrided method public String getStringValue(int index) of the nested class private class FilterEntry extends RowFilter.Entry<M, I> has been modified to return a string without accents based on the accentIndiferent value. Modified source (DefaultRowSorterSpecialFilter.java):
SO doesn't allows more than 30000 characters by response, so, I'm going to split this in three answers.
Part 2/3:
/*
* Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javax.swing;
import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* An implementation of
* <code>RowSorter</code> that provides sorting and filtering around a
* grid-based data model. Beyond creating and installing a
* <code>RowSorter</code>, you very rarely need to interact with one directly.
* Refer to {#link javax.swing.table.TableRowSorter TableRowSorter} for a
* concrete implementation of
* <code>RowSorter</code> for
* <code>JTable</code>. <p> Sorting is done based on the current
* <code>SortKey</code>s, in order. If two objects are equal (the
* <code>Comparator</code> for the column returns 0) the next
* <code>SortKey</code> is used. If no
* <code>SortKey</code>s remain or the order is
* <code>UNSORTED</code>, then the order of the rows in the model is used. <p>
* Sorting of each column is done by way of a
* <code>Comparator</code> that you can specify using the
* <code>setComparator</code> method. If a
* <code>Comparator</code> has not been specified, the
* <code>Comparator</code> returned by
* <code>Collator.getInstance()</code> is used on the results of calling
* <code>toString</code> on the underlying objects. The
* <code>Comparator</code> is never passed
* <code>null</code>. A
* <code>null</code> value is treated as occuring before a non-
* <code>null</code> value, and two
* <code>null</code> values are considered equal. <p> If you specify a
* <code>Comparator</code> that casts its argument to a type other than that
* provided by the model, a
* <code>ClassCastException</code> will be thrown when the data is sorted. <p>
* In addition to sorting,
* <code>DefaultRowSorterSpecialFilter</code> provides the ability to
* filterInclude rows. Filtering is done by way of a
* <code>RowFilter</code> that is specified using the
* <code>setRowFilter</code> method. If no filterInclude has been specified all
* rows are included. <p> By default, rows are in unsorted order (the same as
* the model) and every column is sortable. The default
* <code>Comparator</code>s are documented in the subclasses (for example, {#link
* javax.swing.table.TableRowSorter TableRowSorter}). <p> If the underlying
* model structure changes (the
* <code>modelStructureChanged</code> method is invoked) the following are reset
* to their default values:
* <code>Comparator</code>s by column, current sort order, and whether each
* column is sortable. To find the default
* <code>Comparator</code>s, see the concrete implementation (for example, {#link
* javax.swing.table.TableRowSorter TableRowSorter}). The default sort order is
* unsorted (the same as the model), and columns are sortable by default. <p> If
* the underlying model structure changes (the
* <code>modelStructureChanged</code> method is invoked) the following are reset
* to their default values:
* <code>Comparator</code>s by column, current sort order and whether a column
* is sortable. <p>
* <code>DefaultRowSorterSpecialFilter</code> is an abstract class. Concrete
* subclasses must provide access to the underlying data by invoking
* {#code setModelWrapper}. The {#code setModelWrapper} method <b>must</b> be
* invoked soon after the constructor is called, ideally from within the
* subclass's constructor. Undefined behavior will result if you use a {#code
* DefaultRowSorterSpecialFilter} without specifying a {#code ModelWrapper}. <p>
* <code>DefaultRowSorterSpecialFilter</code> has two formal type parameters.
* The first type parameter corresponds to the class of the model, for example
* <code>DefaultTableModel</code>. The second type parameter corresponds to the
* class of the identifier passed to the
* <code>RowFilter</code>. Refer to
* <code>TableRowSorter</code> and
* <code>RowFilter</code> for more details on the type parameters.
*
* #param <M> the type of the model
* #param <I> the type of the identifier passed to the <code>RowFilter</code>
* #see javax.swing.table.TableRowSorter
* #see javax.swing.table.DefaultTableModel
* #see java.text.Collator
* #since 1.6
*/
public abstract class DefaultRowSorterSpecialFilter<M, I> extends RowSorter<M> {
public boolean accentIndiferent = false;
/**
* Whether or not we resort on TableModelEvent.UPDATEs.
*/
private boolean sortsOnUpdates;
/**
* View (JTable) -> model.
*/
private Row[] viewToModel;
/**
* model -> view (JTable)
*/
private int[] modelToView;
/**
* Comparators specified by column.
*/
private Comparator[] comparators;
/**
* Whether or not the specified column is sortable, by column.
*/
private boolean[] isSortable;
/**
* Cached SortKeys for the current sort.
*/
private SortKey[] cachedSortKeys;
/**
* Cached comparators for the current sort
*/
private Comparator[] sortComparators;
/**
* Developer supplied Filter.
*/
private RowFilter<? super M, ? super I> filter;
/**
* Value passed to the filterInclude. The same instance is passed to the
* filterInclude for different rows.
*/
private FilterEntry filterEntry;
/**
* The sort keys.
*/
private List<SortKey> sortKeys;
/**
* Whether or not to use getStringValueAt. This is indexed by column.
*/
private boolean[] useToString;
/**
* Indicates the contents are sorted. This is used if getSortsOnUpdates is
* false and an update event is received.
*/
private boolean sorted;
/**
* Maximum number of sort keys.
*/
private int maxSortKeys;
/**
* Provides access to the data we're sorting/filtering.
*/
private ModelWrapper<M, I> modelWrapper;
/**
* Size of the model. This is used to enforce error checking within the
* table changed notification methods (such as rowsInserted).
*/
private int modelRowCount;
/**
* Creates an empty
* <code>DefaultRowSorterSpecialFilter</code>.
*/
public DefaultRowSorterSpecialFilter() {
sortKeys = Collections.emptyList();
maxSortKeys = 3;
}
/**
* Sets the model wrapper providing the data that is being sorted and
* filtered.
*
* #param modelWrapper the model wrapper responsible for providing the data
* that gets sorted and filtered
* #throws IllegalArgumentException if {#code modelWrapper} is {#code null}
*/
protected final void setModelWrapper(ModelWrapper<M, I> modelWrapper) {
if (modelWrapper == null) {
throw new IllegalArgumentException(
"modelWrapper most be non-null");
}
ModelWrapper<M, I> last = this.modelWrapper;
this.modelWrapper = modelWrapper;
if (last != null) {
modelStructureChanged();
} else {
// If last is null, we're in the constructor. If we're in
// the constructor we don't want to call to overridable methods.
modelRowCount = getModelWrapper().getRowCount();
}
}
/**
* Returns the model wrapper providing the data that is being sorted and
* filtered.
*
* #return the model wrapper responsible for providing the data that gets
* sorted and filtered
*/
protected final ModelWrapper<M, I> getModelWrapper() {
return modelWrapper;
}
/**
* Returns the underlying model.
*
* #return the underlying model
*/
#Override
public final M getModel() {
return getModelWrapper().getModel();
}
/**
* Sets whether or not the specified column is sortable. The specified value
* is only checked when
* <code>toggleSortOrder</code> is invoked. It is still possible to sort on
* a column that has been marked as unsortable by directly setting the sort
* keys. The default is true.
*
* #param column the column to enable or disable sorting on, in terms of the
* underlying model
* #param sortable whether or not the specified column is sortable
* #throws IndexOutOfBoundsException if <code>column</code> is outside the
* range of the model
* #see #toggleSortOrder
* #see #setSortKeys
*/
public void setSortable(int column, boolean sortable) {
checkColumn(column);
if (isSortable == null) {
isSortable = new boolean[getModelWrapper().getColumnCount()];
for (int i = isSortable.length - 1; i >= 0; i--) {
isSortable[i] = true;
}
}
isSortable[column] = sortable;
}
/**
* Returns true if the specified column is sortable; otherwise, false.
*
* #param column the column to check sorting for, in terms of the underlying
* model
* #return true if the column is sortable
* #throws IndexOutOfBoundsException if column is outside the range of the
* underlying model
*/
public boolean isSortable(int column) {
checkColumn(column);
return (isSortable == null) ? true : isSortable[column];
}
/**
* Sets the sort keys. This creates a copy of the supplied {#code List};
* subsequent changes to the supplied {#code List} do not effect this
* {#code DefaultRowSorterSpecialFilter}. If the sort keys have changed this
* triggers a sort.
*
* #param sortKeys the new <code>SortKeys</code>; <code>null</code> is a
* shorthand for specifying an empty list, indicating that the view should
* be unsorted
* #throws IllegalArgumentException if any of the values in
* <code>sortKeys</code> are null or have a column index outside the range
* of the model
*/
#Override
public void setSortKeys(List<? extends SortKey> sortKeys) {
List<SortKey> old = this.sortKeys;
if (sortKeys != null && sortKeys.size() > 0) {
int max = getModelWrapper().getColumnCount();
for (SortKey key : sortKeys) {
if (key == null || key.getColumn() < 0
|| key.getColumn() >= max) {
throw new IllegalArgumentException("Invalid SortKey");
}
}
this.sortKeys = Collections.unmodifiableList(
new ArrayList<>(sortKeys));
} else {
this.sortKeys = Collections.emptyList();
}
if (!this.sortKeys.equals(old)) {
fireSortOrderChanged();
if (viewToModel == null) {
// Currently unsorted, use sort so that internal fields
// are correctly set.
sort();
} else {
sortExistingData();
}
}
}
/**
* Returns the current sort keys. This returns an unmodifiable
* {#code non-null List}. If you need to change the sort keys, make a copy
* of the returned {#code List}, mutate the copy and invoke
* {#code setSortKeys} with the new list.
*
* #return the current sort order
*/
#Override
public List<? extends SortKey> getSortKeys() {
return sortKeys;
}
/**
* Sets the maximum number of sort keys. The number of sort keys determines
* how equal values are resolved when sorting. For example, assume a table
* row sorter is created and
* <code>setMaxSortKeys(2)</code> is invoked on it. The user clicks the
* header for column 1, causing the table rows to be sorted based on the
* items in column 1. Next, the user clicks the header for column 2, causing
* the table to be sorted based on the items in column 2; if any items in
* column 2 are equal, then those particular rows are ordered based on the
* items in column 1. In this case, we say that the rows are primarily
* sorted on column 2, and secondarily on column 1. If the user then clicks
* the header for column 3, then the items are primarily sorted on column 3
* and secondarily sorted on column 2. Because the maximum number of sort
* keys has been set to 2 with
* <code>setMaxSortKeys</code>, column 1 no longer has an effect on the
* order. <p> The maximum number of sort keys is enforced by
* <code>toggleSortOrder</code>. You can specify more sort keys by invoking
* <code>setSortKeys</code> directly and they will all be honored. However
* if
* <code>toggleSortOrder</code> is subsequently invoked the maximum number
* of sort keys will be enforced. The default value is 3.
*
* #param max the maximum number of sort keys
* #throws IllegalArgumentException if <code>max</code> < 1
*/
public void setMaxSortKeys(int max) {
if (max < 1) {
throw new IllegalArgumentException("Invalid max");
}
maxSortKeys = max;
}
/**
* Returns the maximum number of sort keys.
*
* #return the maximum number of sort keys
*/
public int getMaxSortKeys() {
return maxSortKeys;
}
/**
* If true, specifies that a sort should happen when the underlying model is
* updated (
* <code>rowsUpdated</code> is invoked). For example, if this is true and
* the user edits an entry the location of that item in the view may change.
* The default is false.
*
* #param sortsOnUpdates whether or not to sort on update events
*/
public void setSortsOnUpdates(boolean sortsOnUpdates) {
this.sortsOnUpdates = sortsOnUpdates;
}
/**
* Returns true if a sort should happen when the underlying model is
* updated; otherwise, returns false.
*
* #return whether or not to sort when the model is updated
*/
public boolean getSortsOnUpdates() {
return sortsOnUpdates;
}
/**
* Sets the filterInclude that determines which rows, if any, should be
* hidden from the view. The filterInclude is applied before sorting. A
* value of
* <code>null</code> indicates all values from the model should be included.
* <p>
* <code>RowFilter</code>'s
* <code>include</code> method is passed an
* <code>Entry</code> that wraps the underlying model. The number of columns
* in the
* <code>Entry</code> corresponds to the number of columns in the
* <code>ModelWrapper</code>. The identifier comes from the
* <code>ModelWrapper</code> as well. <p> This method triggers a sort.
*
* #param filterInclude the filterInclude used to determine what entries
* should be included
*/
public void setRowFilter(RowFilter<? super M, ? super I> filter) {
this.filter = filter;
sort();
}
/**
* Returns the filterInclude that determines which rows, if any, should be
* hidden from view.
*
* #return the filterInclude
*/
public RowFilter<? super M, ? super I> getRowFilter() {
return filter;
}
/**
* Reverses the sort order from ascending to descending (or descending to
* ascending) if the specified column is already the primary sorted column;
* otherwise, makes the specified column the primary sorted column, with an
* ascending sort order. If the specified column is not sortable, this
* method has no effect.
*
* #param column index of the column to make the primary sorted column, in
* terms of the underlying model
* #throws IndexOutOfBoundsException {#inheritDoc}
* #see #setSortable(int,boolean)
* #see #setMaxSortKeys(int)
*/
#Override
public void toggleSortOrder(int column) {
checkColumn(column);
if (isSortable(column)) {
List<SortKey> keys = new ArrayList<>(getSortKeys());
SortKey sortKey;
int sortIndex;
for (sortIndex = keys.size() - 1; sortIndex >= 0; sortIndex--) {
if (keys.get(sortIndex).getColumn() == column) {
break;
}
}
if (sortIndex == -1) {
// Key doesn't exist
sortKey = new SortKey(column, SortOrder.ASCENDING);
keys.add(0, sortKey);
} else if (sortIndex == 0) {
// It's the primary sorting key, toggle it
keys.set(0, toggle(keys.get(0)));
} else {
// It's not the first, but was sorted on, remove old
// entry, insert as first with ascending.
keys.remove(sortIndex);
keys.add(0, new SortKey(column, SortOrder.ASCENDING));
}
if (keys.size() > getMaxSortKeys()) {
keys = keys.subList(0, getMaxSortKeys());
}
setSortKeys(keys);
}
}
private SortKey toggle(SortKey key) {
if (key.getSortOrder() == SortOrder.ASCENDING) {
return new SortKey(key.getColumn(), SortOrder.DESCENDING);
}
return new SortKey(key.getColumn(), SortOrder.ASCENDING);
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public int convertRowIndexToView(int index) {
if (modelToView == null) {
if (index < 0 || index >= getModelWrapper().getRowCount()) {
throw new IndexOutOfBoundsException("Invalid index");
}
return index;
}
return modelToView[index];
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public int convertRowIndexToModel(int index) {
if (viewToModel == null) {
if (index < 0 || index >= getModelWrapper().getRowCount()) {
throw new IndexOutOfBoundsException("Invalid index");
}
return index;
}
return viewToModel[index].modelIndex;
}
private boolean isUnsorted() {
List<? extends SortKey> keys = getSortKeys();
int keySize = keys.size();
return (keySize == 0 || keys.get(0).getSortOrder()
== SortOrder.UNSORTED);
}
/**
* Sorts the existing filtered data. This should only be used if the
* filterInclude hasn't changed.
*/
private void sortExistingData() {
int[] lastViewToModel = getViewToModelAsInts(viewToModel);
updateUseToString();
cacheSortKeys(getSortKeys());
if (isUnsorted()) {
if (getRowFilter() == null) {
viewToModel = null;
modelToView = null;
} else {
int included = 0;
for (int i = 0; i < modelToView.length; i++) {
if (modelToView[i] != -1) {
viewToModel[included].modelIndex = i;
modelToView[i] = included++;
}
}
}
} else {
// sort the data
Arrays.sort(viewToModel);
// Update the modelToView array
setModelToViewFromViewToModel(false);
}
fireRowSorterChanged(lastViewToModel);
}
/**
* Sorts and filters the rows in the view based on the sort keys of the
* columns currently being sorted and the filterInclude, if any, associated
* with this sorter. An empty
* <code>sortKeys</code> list indicates that the view should unsorted, the
* same as the model.
*
* #see #setRowFilter
* #see #setSortKeys
*/
public void sort() {
sorted = true;
int[] lastViewToModel = getViewToModelAsInts(viewToModel);
updateUseToString();
if (isUnsorted()) {
// Unsorted
cachedSortKeys = new SortKey[0];
if (getRowFilter() == null) {
// No filterInclude & unsorted
if (viewToModel != null) {
// sorted -> unsorted
viewToModel = null;
modelToView = null;
} else {
// unsorted -> unsorted
// No need to do anything.
return;
}
} else {
// There is filterInclude, reset mappings
initializeFilteredMapping();
}
} else {
cacheSortKeys(getSortKeys());
if (getRowFilter() != null) {
initializeFilteredMapping();
} else {
createModelToView(getModelWrapper().getRowCount());
createViewToModel(getModelWrapper().getRowCount());
}
// sort them
Arrays.sort(viewToModel);
// Update the modelToView array
setModelToViewFromViewToModel(false);
}
fireRowSorterChanged(lastViewToModel);
}
/**
* Updates the useToString mapping before a sort.
*/
private void updateUseToString() {
int i = getModelWrapper().getColumnCount();
if (useToString == null || useToString.length != i) {
useToString = new boolean[i];
}
for (--i; i >= 0; i--) {
useToString[i] = useToString(i);
}
}
/**
* Resets the viewToModel and modelToView mappings based on the current
* Filter.
*/
private void initializeFilteredMapping() {
int rowCount = getModelWrapper().getRowCount();
int i, j;
int excludedCount = 0;
// Update model -> view
createModelToView(rowCount);
for (i = 0; i < rowCount; i++) {
if (include(i)) {
modelToView[i] = i - excludedCount;
} else {
modelToView[i] = -1;
excludedCount++;
}
}
// Update view -> model
createViewToModel(rowCount - excludedCount);
for (i = 0, j = 0; i < rowCount; i++) {
if (modelToView[i] != -1) {
viewToModel[j++].modelIndex = i;
}
}
}
/**
* Makes sure the modelToView array is of size rowCount.
*/
private void createModelToView(int rowCount) {
if (modelToView == null || modelToView.length != rowCount) {
modelToView = new int[rowCount];
}
}
/**
* Resets the viewToModel array to be of size rowCount.
*/
private void createViewToModel(int rowCount) {
int recreateFrom = 0;
if (viewToModel != null) {
recreateFrom = Math.min(rowCount, viewToModel.length);
if (viewToModel.length != rowCount) {
Row[] oldViewToModel = viewToModel;
viewToModel = new Row[rowCount];
System.arraycopy(oldViewToModel, 0, viewToModel,
0, recreateFrom);
}
} else {
viewToModel = new Row[rowCount];
}
int i;
for (i = 0; i < recreateFrom; i++) {
viewToModel[i].modelIndex = i;
}
for (i = recreateFrom; i < rowCount; i++) {
viewToModel[i] = new Row(this, i);
}
}
/**
* Caches the sort keys before a sort.
*/
private void cacheSortKeys(List<? extends SortKey> keys) {
int keySize = keys.size();
sortComparators = new Comparator[keySize];
for (int i = 0; i < keySize; i++) {
sortComparators[i] = getComparator0(keys.get(i).getColumn());
}
cachedSortKeys = keys.toArray(new SortKey[keySize]);
}
/**
* Returns whether or not to convert the value to a string before doing
* comparisons when sorting. If true
* <code>ModelWrapper.getStringValueAt</code> will be used, otherwise
* <code>ModelWrapper.getValueAt</code> will be used. It is up to
* subclasses, such as
* <code>TableRowSorter</code>, to honor this value in their
* <code>ModelWrapper</code> implementation.
*
* #param column the index of the column to test, in terms of the underlying
SO doesn't allows more than 30000 characters by response, so, I'm going to split this in three answers.
Part 3/3:
* model
* #throws IndexOutOfBoundsException if <code>column</code> is not valid
*/
protected boolean useToString(int column) {
return (getComparator(column) == null);
}
/**
* Refreshes the modelToView mapping from that of viewToModel. If
* <code>unsetFirst</code> is true, all indices in modelToView are first set
* to -1.
*/
private void setModelToViewFromViewToModel(boolean unsetFirst) {
int i;
if (unsetFirst) {
for (i = modelToView.length - 1; i >= 0; i--) {
modelToView[i] = -1;
}
}
for (i = viewToModel.length - 1; i >= 0; i--) {
modelToView[viewToModel[i].modelIndex] = i;
}
}
private int[] getViewToModelAsInts(Row[] viewToModel) {
if (viewToModel != null) {
int[] viewToModelI = new int[viewToModel.length];
for (int i = viewToModel.length - 1; i >= 0; i--) {
viewToModelI[i] = viewToModel[i].modelIndex;
}
return viewToModelI;
}
return new int[0];
}
/**
* Sets the
* <code>Comparator</code> to use when sorting the specified column. This
* does not trigger a sort. If you want to sort after setting the comparator
* you need to explicitly invoke
* <code>sort</code>.
*
* #param column the index of the column the <code>Comparator</code> is to
* be used for, in terms of the underlying model
* #param comparator the <code>Comparator</code> to use
* #throws IndexOutOfBoundsException if <code>column</code> is outside the
* range of the underlying model
*/
public void setComparator(int column, Comparator<?> comparator) {
checkColumn(column);
if (comparators == null) {
comparators = new Comparator[getModelWrapper().getColumnCount()];
}
comparators[column] = comparator;
}
/**
* Returns the
* <code>Comparator</code> for the specified column. This will return
* <code>null</code> if a
* <code>Comparator</code> has not been specified for the column.
*
* #param column the column to fetch the <code>Comparator</code> for, in
* terms of the underlying model
* #return the <code>Comparator</code> for the specified column
* #throws IndexOutOfBoundsException if column is outside the range of the
* underlying model
*/
public Comparator<?> getComparator(int column) {
checkColumn(column);
if (comparators != null) {
return comparators[column];
}
return null;
}
// Returns the Comparator to use during sorting. Where as
// getComparator() may return null, this will never return null.
private Comparator getComparator0(int column) {
Comparator comparator = getComparator(column);
if (comparator != null) {
return comparator;
}
// This should be ok as useToString(column) should have returned
// true in this case.
return Collator.getInstance();
}
private RowFilter.Entry<M, I> getFilterEntry(int modelIndex) {
if (filterEntry == null) {
filterEntry = new FilterEntry();
}
filterEntry.modelIndex = modelIndex;
return filterEntry;
}
/**
* {#inheritDoc}
*/
#Override
public int getViewRowCount() {
if (viewToModel != null) {
// When filtering this may differ from getModelWrapper().getRowCount()
return viewToModel.length;
}
return getModelWrapper().getRowCount();
}
/**
* {#inheritDoc}
*/
#Override
public int getModelRowCount() {
return getModelWrapper().getRowCount();
}
private void allChanged() {
modelToView = null;
viewToModel = null;
comparators = null;
isSortable = null;
if (isUnsorted()) {
// Keys are already empty, to force a resort we have to
// call sort
sort();
} else {
setSortKeys(null);
}
}
/**
* {#inheritDoc}
*/
#Override
public void modelStructureChanged() {
allChanged();
modelRowCount = getModelWrapper().getRowCount();
}
/**
* {#inheritDoc}
*/
#Override
public void allRowsChanged() {
modelRowCount = getModelWrapper().getRowCount();
sort();
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public void rowsInserted(int firstRow, int endRow) {
checkAgainstModel(firstRow, endRow);
int newModelRowCount = getModelWrapper().getRowCount();
if (endRow >= newModelRowCount) {
throw new IndexOutOfBoundsException("Invalid range");
}
modelRowCount = newModelRowCount;
if (shouldOptimizeChange(firstRow, endRow)) {
rowsInserted0(firstRow, endRow);
}
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public void rowsDeleted(int firstRow, int endRow) {
checkAgainstModel(firstRow, endRow);
if (firstRow >= modelRowCount || endRow >= modelRowCount) {
throw new IndexOutOfBoundsException("Invalid range");
}
modelRowCount = getModelWrapper().getRowCount();
if (shouldOptimizeChange(firstRow, endRow)) {
rowsDeleted0(firstRow, endRow);
}
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public void rowsUpdated(int firstRow, int endRow) {
checkAgainstModel(firstRow, endRow);
if (firstRow >= modelRowCount || endRow >= modelRowCount) {
throw new IndexOutOfBoundsException("Invalid range");
}
if (getSortsOnUpdates()) {
if (shouldOptimizeChange(firstRow, endRow)) {
rowsUpdated0(firstRow, endRow);
}
} else {
sorted = false;
}
}
/**
* {#inheritDoc}
*
* #throws IndexOutOfBoundsException {#inheritDoc}
*/
#Override
public void rowsUpdated(int firstRow, int endRow, int column) {
checkColumn(column);
rowsUpdated(firstRow, endRow);
}
private void checkAgainstModel(int firstRow, int endRow) {
if (firstRow > endRow || firstRow < 0 || endRow < 0
|| firstRow > modelRowCount) {
throw new IndexOutOfBoundsException("Invalid range");
}
}
/**
* Returns true if the specified row should be included.
*/
private boolean include(int row) {
RowFilter<? super M, ? super I> filterInclude = getRowFilter();
if (filterInclude != null) {
return filterInclude.include(getFilterEntry(row));
}
// null filterInclude, always include the row.
return true;
}
#SuppressWarnings("unchecked")
private int compare(int model1, int model2) {
int column;
SortOrder sortOrder;
Object v1, v2;
int result;
for (int counter = 0; counter < cachedSortKeys.length; counter++) {
column = cachedSortKeys[counter].getColumn();
sortOrder = cachedSortKeys[counter].getSortOrder();
if (sortOrder == SortOrder.UNSORTED) {
result = model1 - model2;
} else {
// v1 != null && v2 != null
if (useToString[column]) {
v1 = getModelWrapper().getStringValueAt(model1, column);
v2 = getModelWrapper().getStringValueAt(model2, column);
} else {
v1 = getModelWrapper().getValueAt(model1, column);
v2 = getModelWrapper().getValueAt(model2, column);
}
// Treat nulls as < then non-null
if (v1 == null) {
if (v2 == null) {
result = 0;
} else {
result = -1;
}
} else if (v2 == null) {
result = 1;
} else {
result = sortComparators[counter].compare(v1, v2);
}
if (sortOrder == SortOrder.DESCENDING) {
result *= -1;
}
}
if (result != 0) {
return result;
}
}
// If we get here, they're equal. Fallback to model order.
return model1 - model2;
}
/**
* Whether not we are filtering/sorting.
*/
private boolean isTransformed() {
return (viewToModel != null);
}
/**
* Insets new set of entries.
*
* #param toAdd the Rows to add, sorted
* #param current the array to insert the items into
*/
private void insertInOrder(List<Row> toAdd, Row[] current) {
int last = 0;
int index;
int max = toAdd.size();
for (int i = 0; i < max; i++) {
index = Arrays.binarySearch(current, toAdd.get(i));
if (index < 0) {
index = -1 - index;
}
System.arraycopy(current, last,
viewToModel, last + i, index - last);
viewToModel[index + i] = toAdd.get(i);
last = index;
}
System.arraycopy(current, last, viewToModel, last + max,
current.length - last);
}
/**
* Returns true if we should try and optimize the processing of the
* <code>TableModelEvent</code>. If this returns false, assume the event was
* dealt with and no further processing needs to happen.
*/
private boolean shouldOptimizeChange(int firstRow, int lastRow) {
if (!isTransformed()) {
// Not transformed, nothing to do.
return false;
}
if (!sorted || (lastRow - firstRow) > viewToModel.length / 10) {
// We either weren't sorted, or to much changed, sort it all
sort();
return false;
}
return true;
}
private void rowsInserted0(int firstRow, int lastRow) {
int[] oldViewToModel = getViewToModelAsInts(viewToModel);
int i;
int delta = (lastRow - firstRow) + 1;
List<Row> added = new ArrayList<>(delta);
// Build the list of Rows to add into added
for (i = firstRow; i <= lastRow; i++) {
if (include(i)) {
added.add(new Row(this, i));
}
}
// Adjust the model index of rows after the effected region
int viewIndex;
for (i = modelToView.length - 1; i >= firstRow; i--) {
viewIndex = modelToView[i];
if (viewIndex != -1) {
viewToModel[viewIndex].modelIndex += delta;
}
}
// Insert newly added rows into viewToModel
if (added.size() > 0) {
Collections.sort(added);
Row[] lastViewToModel = viewToModel;
viewToModel = new Row[viewToModel.length + added.size()];
insertInOrder(added, lastViewToModel);
}
// Update modelToView
createModelToView(getModelWrapper().getRowCount());
setModelToViewFromViewToModel(true);
// Notify of change
fireRowSorterChanged(oldViewToModel);
}
private void rowsDeleted0(int firstRow, int lastRow) {
int[] oldViewToModel = getViewToModelAsInts(viewToModel);
int removedFromView = 0;
int i;
int viewIndex;
// Figure out how many visible rows are going to be effected.
for (i = firstRow; i <= lastRow; i++) {
viewIndex = modelToView[i];
if (viewIndex != -1) {
removedFromView++;
viewToModel[viewIndex] = null;
}
}
// Update the model index of rows after the effected region
int delta = lastRow - firstRow + 1;
for (i = modelToView.length - 1; i > lastRow; i--) {
viewIndex = modelToView[i];
if (viewIndex != -1) {
viewToModel[viewIndex].modelIndex -= delta;
}
}
// Then patch up the viewToModel array
if (removedFromView > 0) {
Row[] newViewToModel = new Row[viewToModel.length
- removedFromView];
int newIndex = 0;
int last = 0;
for (i = 0; i < viewToModel.length; i++) {
if (viewToModel[i] == null) {
System.arraycopy(viewToModel, last,
newViewToModel, newIndex, i - last);
newIndex += (i - last);
last = i + 1;
}
}
System.arraycopy(viewToModel, last,
newViewToModel, newIndex, viewToModel.length - last);
viewToModel = newViewToModel;
}
// Update the modelToView mapping
createModelToView(getModelWrapper().getRowCount());
setModelToViewFromViewToModel(true);
// And notify of change
fireRowSorterChanged(oldViewToModel);
}
private void rowsUpdated0(int firstRow, int lastRow) {
int[] oldViewToModel = getViewToModelAsInts(viewToModel);
int i, j;
int delta = lastRow - firstRow + 1;
int modelIndex;
int last;
int index;
if (getRowFilter() == null) {
// Sorting only:
// Remove the effected rows
Row[] updated = new Row[delta];
for (j = 0, i = firstRow; i <= lastRow; i++, j++) {
updated[j] = viewToModel[modelToView[i]];
}
// Sort the update rows
Arrays.sort(updated);
// Build the intermediary array: the array of
// viewToModel without the effected rows.
Row[] intermediary = new Row[viewToModel.length - delta];
for (i = 0, j = 0; i < viewToModel.length; i++) {
modelIndex = viewToModel[i].modelIndex;
if (modelIndex < firstRow || modelIndex > lastRow) {
intermediary[j++] = viewToModel[i];
}
}
// Build the new viewToModel
insertInOrder(Arrays.asList(updated), intermediary);
// Update modelToView
setModelToViewFromViewToModel(false);
} else {
// Sorting & filtering.
// Remove the effected rows, adding them to updated and setting
// modelToView to -2 for any rows that were not filtered out
List<Row> updated = new ArrayList<>(delta);
int newlyVisible = 0;
int newlyHidden = 0;
int effected = 0;
for (i = firstRow; i <= lastRow; i++) {
if (modelToView[i] == -1) {
// This row was filtered out
if (include(i)) {
// No longer filtered
updated.add(new Row(this, i));
newlyVisible++;
}
} else {
// This row was visible, make sure it should still be
// visible.
if (!include(i)) {
newlyHidden++;
} else {
updated.add(viewToModel[modelToView[i]]);
}
modelToView[i] = -2;
effected++;
}
}
// Sort the updated rows
Collections.sort(updated);
// Build the intermediary array: the array of
// viewToModel without the updated rows.
Row[] intermediary = new Row[viewToModel.length - effected];
for (i = 0, j = 0; i < viewToModel.length; i++) {
modelIndex = viewToModel[i].modelIndex;
if (modelToView[modelIndex] != -2) {
intermediary[j++] = viewToModel[i];
}
}
// Recreate viewToModel, if necessary
if (newlyVisible != newlyHidden) {
viewToModel = new Row[viewToModel.length + newlyVisible
- newlyHidden];
}
// Rebuild the new viewToModel array
insertInOrder(updated, intermediary);
// Update modelToView
setModelToViewFromViewToModel(true);
}
// And finally fire a sort event.
fireRowSorterChanged(oldViewToModel);
}
private void checkColumn(int column) {
if (column < 0 || column >= getModelWrapper().getColumnCount()) {
throw new IndexOutOfBoundsException(
"column beyond range of TableModel");
}
}
/**
* <code>DefaultRowSorterSpecialFilter.ModelWrapper</code> is responsible
* for providing the data that gets sorted by
* <code>DefaultRowSorterSpecialFilter</code>. You normally do not interact
* directly with
* <code>ModelWrapper</code>. Subclasses of
* <code>DefaultRowSorterSpecialFilter</code> provide an implementation of
* <code>ModelWrapper</code> wrapping another model. For example,
* <code>TableRowSorter</code> provides a
* <code>ModelWrapper</code> that wraps a
* <code>TableModel</code>. <p>
* <code>ModelWrapper</code> makes a distinction between values as
* <code>Object</code>s and
* <code>String</code>s. This allows implementations to provide a custom
* string converter to be used instead of invoking
* <code>toString</code> on the object.
*
* #param <M> the type of the underlying model
* #param <I> the identifier supplied to the filterInclude
* #since 1.6
* #see RowFilter
* #see RowFilter.Entry
*/
protected abstract static class ModelWrapper<M, I> {
/**
* Creates a new
* <code>ModelWrapper</code>.
*/
protected ModelWrapper() {
}
/**
* Returns the underlying model that this
* <code>Model</code> is wrapping.
*
* #return the underlying model
*/
public abstract M getModel();
/**
* Returns the number of columns in the model.
*
* #return the number of columns in the model
*/
public abstract int getColumnCount();
/**
* Returns the number of rows in the model.
*
* #return the number of rows in the model
*/
public abstract int getRowCount();
/**
* Returns the value at the specified index.
*
* #param row the row index
* #param column the column index
* #return the value at the specified index
* #throws IndexOutOfBoundsException if the indices are outside the
* range of the model
*/
public abstract Object getValueAt(int row, int column);
/**
* Returns the value as a
* <code>String</code> at the specified index. This implementation uses
* <code>toString</code> on the result from
* <code>getValueAt</code> (making sure to return an empty string for
* null values). Subclasses that override this method should never
* return null.
*
* #param row the row index
* #param column the column index
* #return the value at the specified index as a <code>String</code>
* #throws IndexOutOfBoundsException if the indices are outside the
* range of the model
*/
public String getStringValueAt(int row, int column) {
Object o = getValueAt(row, column);
if (o == null) {
return "";
}
String string = o.toString();
if (string == null) {
return "";
}
return string;
}
/**
* Returns the identifier for the specified row. The return value of
* this is used as the identifier for the
* <code>RowFilter.Entry</code> that is passed to the
* <code>RowFilter</code>.
*
* #param row the row to return the identifier for, in terms of the
* underlying model
* #return the identifier
* #see RowFilter.Entry#getIdentifier
*/
public abstract I getIdentifier(int row);
}
/**
* RowFilter.Entry implementation that delegates to the ModelWrapper.
* getFilterEntry(int) creates the single instance of this that is passed to
* the Filter. Only call getFilterEntry(int) to get the instance.
*/
private class FilterEntry extends RowFilter.Entry<M, I> {
/**
* The index into the model, set in getFilterEntry
*/
int modelIndex;
#Override
public M getModel() {
return getModelWrapper().getModel();
}
#Override
public int getValueCount() {
return getModelWrapper().getColumnCount();
}
#Override
public Object getValue(int index) {
return getModelWrapper().getValueAt(modelIndex, index);
}
#Override
public String getStringValue(int index) {
/* Original code here was:
*
* return getModelWrapper().getStringValueAt(modelIndex, index);
*/
if (accentIndiferent) {
return Normalizer.normalize((String) getModelWrapper().getStringValueAt(modelIndex, index), Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
} else {
return getModelWrapper().getStringValueAt(modelIndex, index);
}
}
#Override
public I getIdentifier() {
return getModelWrapper().getIdentifier(modelIndex);
}
}
/**
* Row is used to handle the actual sorting by way of Comparable. It will
* use the sortKeys to do the actual comparison.
*/
// NOTE: this class is static so that it can be placed in an array
private static class Row implements Comparable<Row> {
private DefaultRowSorterSpecialFilter sorter;
int modelIndex;
public Row(DefaultRowSorterSpecialFilter sorter, int index) {
this.sorter = sorter;
modelIndex = index;
}
#Override
public int compareTo(Row o) {
return sorter.compare(modelIndex, o.modelIndex);
}
}
}
And finally, a helper class, RowFilterSpecialFilter. Source (RowFilterSpecialFilter.java):
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package javax.swing;
import java.text.Normalizer;
/**
*
* #author Miroslav
*/
public abstract class RowFilterSpecialFilter {
public static <M, I> RowFilter<M, I> regexFilterAccentIndiferent(String regex) {
return RowFilter.regexFilter("(?i)" + Normalizer.normalize(regex, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""));
}
public static <M, I> RowFilter<M, I> regexFilterAccentIndiferent(String regex, int... indices) {
return RowFilter.regexFilter("(?i)" + Normalizer.normalize(regex, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", ""), indices);
}
}
Now, how to use the filter with the accent indiferent function?
Instead of attaching a TableRowSorter<MyClassThatInheritsFromAbstractModel> to the jTable via setRowSorter method, attach a TableRowSorterSpecialFilter<MyClassThatInheritsFromAbstractModel> to it via the same method.
Don't forget to set accentIndiferent property to true. A simple myTableRowSorterSpecialFilterInstance.AccentIndiferent = true; is enough;
The code that should be fired up to apply a filter is what follows, where FilterText is the string that's going to be the filter:
RowFilter<ClientesTableModel, Object> rf;
try {
rf = RowFilterSpecialFilter.regexFilterAccentIndiferent("(?i)" + FilterText);
} catch (PatternSyntaxException ex) {
return;
}
sorter.setRowFilter(rf);
I've put so many sources to help to anyone that gets the same trouble that I had. I'm pretty sure that I've modified something else but I don't remember it, so, better to be sure and make a good contribution.
And... done!!!

Java Oracle exception - "maximum number of expressions in a list is 1000"

I am passing a list of Strings to my query(SQL query written) to fetch the required data.
But I am getting this exception:
ora-01795 maximum number of expressions in a list is 1000
I checked that I have more than 1000 entries in the list passed to the query IN parameter.
this is a oracle limitation in the number of list pass in the query.
you will have to chop your query or
provide a subquery/join in the IN clause instead.
You cant have a list with more than 1000 elements in a single "where" condition if you are working with Oracle DB. So you can chop down your "where" condition in multiple "where" conditions and join them with "or" clause.
If you are using hibernate Criteria, you can use below Java method to do this.
Just replace your code where ever you used
criteria.add(Restrictions.in(propertyName, mainList));
with
addCriteriaIn(propertyName, mainList, criteria);
which the method is :
private void addCriteriaIn (String propertyName, List<?> list,Criteria criteria)
{
Disjunction or = Restrictions.disjunction();
if(list.size()>1000)
{
while(list.size()>1000)
{
List<?> subList = list.subList(0, 1000);
or.add(Restrictions.in(propertyName, subList));
list.subList(0, 1000).clear();
}
}
or.add(Restrictions.in(propertyName, list));
criteria.add(or);
}
I solved this by breaking the list into batch of size 1000 and joining it using OR.
e.g.
eid[] array of ids.
If I want to execute this query,
String sql = select * from employee where some conditions and empid in(eid)
I have re-written this query by writing a small piece of code:
String sql = select * from employee where some conditions and (
empid in(empid[0...999]) OR
empid in(empid[1000...1999]) OR
empid in(empid[2000...2999]) OR .... );
Dealing with this error while using hibernate, you have to tackle this issue by chopping the list into batch of 100 and then join the individual results (as shown in the query above).
I don't think that it's a limitation of hibernate for not handling this issue, because it may be the case that this issue is not a case of another DB like MySQL or DB2. Hibernate is a cross-DB ORM framework.
you can create a temporary table, and insert the values you want use in your IN statement, and join the temporary table with your real table. more information about temporary tables.
From dba-oracle.com:
ORA-01795: maximum number of expressions in a list is 1000 tips
Oracle Error Tips by Burleson Consulting (S. Karam)
The Oracle docs note this on the ora-01795 error*: ORA-01795 maximum
number of expressions in a list is 1000 Cause: More than 254 columns
or expressions were specified in a list. Action: Remove some of the
expressions from the list. In the Oracle MOSC Forums, an Oracle user
was attempting to find a way around error code ORA-01795. His
question was answered by Reem Munakash of Oracle:
The limit in Oracle8 is 1000 expressions. There is a bug 495555, filed
against the error text giving the wrong number (254). However, there
may be a further restriction depending on the tool you are using. The
1000 expressions is within sqlplus.
The workaround would be to use a sub-query.
The bug regarding the error message is fixed in 8.1.5.
If you are able to convert your db-side logic from a query into a stored procedure, then you can pass longer arrays (collections) to it.
Here you can find a brief example how to do it. The link to the docs is outdated, so here's a link to the 9i docs http://docs.oracle.com/cd/B10500_01/java.920/a96654/oraarr.htm#1040124
import java.io.*;
import java.sql.*;
import oracle.sql.*;
import oracle.jdbc.driver.*;
public class ArrayDemo
{
public static void passArray() throws SQLException
{
Connection conn =
new OracleDriver().defaultConnection();
int intArray[] = { 1,2,3,4,5,6 };
ArrayDescriptor descriptor =
ArrayDescriptor.createDescriptor( "NUM_ARRAY", conn );
ARRAY array_to_pass =
new ARRAY( descriptor, conn, intArray );
OraclePreparedStatement ps =
(OraclePreparedStatement)conn.prepareStatement
( "begin give_me_an_array(:x); end;" );
ps.setARRAY( 1, array_to_pass );
ps.execute();
}
}
and the SQL part
create or replace type NUM_ARRAY as table of number;
create or replace
procedure give_me_an_array( p_array in num_array )
as
begin
for i in 1 .. p_array.count
loop
dbms_output.put_line( p_array(i) );
end loop;
end;
Using Java Hibernate, to solve this problem I decided to change the Hibernate-core JAR. I madea a helper class to split an expression in more joins like: ... t.column IN (: list_1) OR t.column IN (: list_2) ... , Then I changed AbstractQueryImpl.expandParameterList method from hibernate to call my method if the collection exceeds the limit.
My hibernate-core version is 3.6.10.Final, but it work fine and for 4.x versions - I tested it.
My code is tested for next cases:
where t.id in (:idList)
where (t.id in (:idList))
where ((t.id) in (:idList))
where 1=1 and t.id in (:idList)
where 1=1 and (t.id in (:idList))
where 1=1 and(t.id) in (:idList)
where 1=1 and((t.id) in (:idList))
where 1=1 and(t.id in (:idList))
where t.id not in (:idList)
where (t.id not in (:idList))
where ((t.id) not in (:idList))
AbstractQueryImpl.expandParameterList :
private String expandParameterList(String query, String name, TypedValue typedList, Map namedParamsCopy) {
Collection vals = (Collection) typedList.getValue();
Type type = typedList.getType();
boolean isJpaPositionalParam = parameterMetadata.getNamedParameterDescriptor( name ).isJpaStyle();
String paramPrefix = isJpaPositionalParam ? "?" : ParserHelper.HQL_VARIABLE_PREFIX;
String placeholder =
new StringBuffer( paramPrefix.length() + name.length() )
.append( paramPrefix ).append( name )
.toString();
if ( query == null ) {
return query;
}
int loc = query.indexOf( placeholder );
if ( loc < 0 ) {
return query;
}
String beforePlaceholder = query.substring( 0, loc );
String afterPlaceholder = query.substring( loc + placeholder.length() );
// check if placeholder is already immediately enclosed in parentheses
// (ignoring whitespace)
boolean isEnclosedInParens =
StringHelper.getLastNonWhitespaceCharacter( beforePlaceholder ) == '(' &&
StringHelper.getFirstNonWhitespaceCharacter( afterPlaceholder ) == ')';
if ( vals.size() == 1 && isEnclosedInParens ) {
// short-circuit for performance when only 1 value and the
// placeholder is already enclosed in parentheses...
namedParamsCopy.put( name, new TypedValue( type, vals.iterator().next(), session.getEntityMode() ) );
return query;
}
// *** changes by Vasile Bors for HHH-1123 ***
// case vals.size() > 1000
if ((vals.size() >= InExpressionExpander.MAX_ALLOWED_PER_INEXPR) && isEnclosedInParens) {
InExpressionExpander inExpressionExpander = new InExpressionExpander(beforePlaceholder, afterPlaceholder);
if(inExpressionExpander.isValidInOrNotInExpression()){
List<String> list = new ArrayList<String>( vals.size() );
Iterator iter = vals.iterator();
int i = 0;
String alias;
while ( iter.hasNext() ) {
alias = ( isJpaPositionalParam ? 'x' + name : name ) + i++ + '_';
namedParamsCopy.put( alias, new TypedValue( type, iter.next(), session.getEntityMode() ) );
list.add(ParserHelper.HQL_VARIABLE_PREFIX + alias );
}
String expandedExpression = inExpressionExpander.expandExpression(list);
if(expandedExpression != null){
return expandedExpression;
}
}
}
// *** end changes by Vasile Bors for HHH-1123 ***
StringBuffer list = new StringBuffer(16);
Iterator iter = vals.iterator();
int i = 0;
while (iter.hasNext()) {
String alias = (isJpaPositionalParam ? 'x' + name : name) + i++ + '_';
namedParamsCopy.put(alias, new TypedValue(type, iter.next(), session.getEntityMode()));
list.append(ParserHelper.HQL_VARIABLE_PREFIX).append(alias);
if (iter.hasNext()) {
list.append(", ");
}
}
return StringHelper.replace(
beforePlaceholder,
afterPlaceholder,
placeholder.toString(),
list.toString(),
true,
true
);
}
My helper class InExpressionExpander:
package org.hibernate.util;
import org.hibernate.QueryException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
/**
* Utility class for expand Hql and Sql IN expressions with a parameter with more than IN expression limit size (HHH-1123).
* <br/>
* It work for expression with formats:
* <pre>
*
* where t.id in (:idList)
* where (t.id in (:idList))
* where ((t.id) in (:idList))
* where 1=1 and t.id in (:idList)
* where 1=1 and (t.id in (:idList))
* where 1=1 and(t.id) in (:idList)
* where 1=1 and((t.id) in (:idList))
* where 1=1 and(t.id in (:idList))
*
* where t.id not in (:idList)
* where (t.id not in (:idList))
* where ((t.id) not in (:idList))
* </pre>
* <p/>
* Example:
* <pre>
* select t.id from tableOrEntity t where t.id IN (:idList)
* </pre
*
* #author Vasile Bors
* #since 13/12/2015.
*/
public class InExpressionExpander {
private static final Logger log = LoggerFactory.getLogger(InExpressionExpander.class);
public static final int MAX_ALLOWED_PER_INEXPR = 1000;
private static final int MAX_PARAMS_PER_INEXPR = 500;
private Stack<String> stackExpr = new Stack<String>();
private StringBuilder toWalkQuery;
private final String beforePlaceholder;
private final String afterPlaceholder;
private boolean wasChecked = false;
private boolean isEnclosedInParens = false;
private boolean isInExpr = false;
private boolean isNotInExpr = false;
public InExpressionExpander(String beforePlaceholder, String afterPlaceholder) {
this.toWalkQuery = new StringBuilder(beforePlaceholder);
this.beforePlaceholder = beforePlaceholder;
this.afterPlaceholder = afterPlaceholder;
}
public boolean isValidInOrNotInExpression() {
if (!wasChecked) {
String lastExpr = extractLastExpression();
if ("(".equals(lastExpr)) {
isEnclosedInParens = true;
lastExpr = extractLastExpression();
}
isInExpr = "in".equalsIgnoreCase(lastExpr);
}
wasChecked = true;
return isInExpr;
}
public String expandExpression(List paramList) {
if (isValidInOrNotInExpression()) {
final String lastExpr = extractLastExpression(false);
if ("not".equalsIgnoreCase(lastExpr)) {
isNotInExpr = true;
extractLastExpression(); //extract "not" and consume it
}
extractColumnForInExpression();
StringBuilder exprPrefixBuilder = new StringBuilder();
for (int i = stackExpr.size() - 1; i > -1; i--) {
exprPrefixBuilder.append(stackExpr.get(i)).append(' ');
}
if (!isEnclosedInParens) {
exprPrefixBuilder.append('(');
}
String expandedExpression = expandInExpression(exprPrefixBuilder, paramList);
String beforeExpression = getBeforeExpression();
String afterExpression = getAfterExpression();
String expandedQuery = new StringBuilder(beforeExpression).append(expandedExpression)
.append(afterExpression)
.toString();
if (log.isDebugEnabled()) {
log.debug(
"Query was changed to prevent exception for maximum number of expression in a list. Expanded IN expression query:\n {}",
expandedExpression);
log.debug("Expanded query:\n {}", expandedQuery);
}
return expandedQuery;
}
log.error("Illegal call of InExpressionExpander.expandExpression() without IN expression.");
return null;
}
private String expandInExpression(StringBuilder exprPrefixBuilder, List values) {
String joinExpr = isNotInExpr ? ") and " : ") or ";
StringBuilder expr = new StringBuilder(16);
Iterator iter = values.iterator();
int i = 0;
boolean firstExpr = true;
while (iter.hasNext()) {
if (firstExpr || i % MAX_PARAMS_PER_INEXPR == 0) {
//close previous expression and start new expression
if (!firstExpr) {
expr.append(joinExpr);
} else {
firstExpr = false;
}
expr.append(exprPrefixBuilder);
} else {
expr.append(", ");
}
expr.append(iter.next());
i++;
}
expr.append(')');// close for last in expression
return expr.toString();
}
/**
* Method extract last expression parsed by space from toWalkQuery and remove it from toWalkQuery;<br/>
* If expression has brackets it will return al content between brackets and it will add additional space to adjust splitting by space.
*
* #return last expression from toWalkQuery
*/
private String extractLastExpression() {
return extractLastExpression(true);
}
/**
* Method extract last expression parsed by space from toWalkQuery, remove it from toWalkQuery if is consume = true;<br/>
* If expression has brackets it will return al content between brackets and it will add additional space to adjust splitting by space.
*
* #param consum if true the method will extract and remove last expression from toWalkQuery
* #return last expression from toWalkQuery
*/
private String extractLastExpression(final boolean consum) {
int lastIndex = this.toWalkQuery.length() - 1;
String lastExpr;
int exprSeparatorIndex = this.toWalkQuery.lastIndexOf(" ");
if (lastIndex == exprSeparatorIndex) { //remove last space from the end
this.toWalkQuery.delete(exprSeparatorIndex, this.toWalkQuery.length());
return extractLastExpression(consum);
} else {
lastExpr = this.toWalkQuery.substring(exprSeparatorIndex + 1, this.toWalkQuery.length());
if (lastExpr.length() > 1) {
if (lastExpr.endsWith(")")) {
//if parens are closed at the end we need to find where it is open
int opensParens = 0;
int closedParens = 0;
int startExprIndex = -1;
char c;
for (int i = lastExpr.length() - 1; i > -1; i--) {
c = lastExpr.charAt(i);
if (c == ')') {
closedParens++;
} else if (c == '(') {
opensParens++;
}
if (closedParens == opensParens) {
startExprIndex = i;
break;
}
}
if (startExprIndex > -1) {
lastExpr = lastExpr.substring(startExprIndex, lastExpr.length());
exprSeparatorIndex = exprSeparatorIndex + startExprIndex
+ 1; // +1 because separator is not space and don't must be deleted
}
} else if (lastExpr.contains("(")) {
int parentsIndex = exprSeparatorIndex + lastExpr.indexOf('(') + 1;
this.toWalkQuery.replace(parentsIndex, parentsIndex + 1, " ( ");
return extractLastExpression(consum);
}
}
if (consum) {
this.toWalkQuery.delete(exprSeparatorIndex, this.toWalkQuery.length());
}
}
if (consum) {
stackExpr.push(lastExpr);
}
return lastExpr;
}
private String extractColumnForInExpression() {
String column = extractLastExpression();
String beforeColumn = extractLastExpression(false);
long pointIndx = beforeColumn.lastIndexOf('.');
if (pointIndx > -1) {
if (pointIndx == (beforeColumn.length() - 1)) {
throw new QueryException(
"Invalid column format: " + beforeColumn + ' ' + column
+ " . Remove space from column!");
}
}
return column;
}
private String getBeforeExpression() {
return this.toWalkQuery + " (";
}
private String getAfterExpression() {
if (StringHelper.getFirstNonWhitespaceCharacter(afterPlaceholder) == ')') {
return afterPlaceholder;
}
return afterPlaceholder + ") ";
}
}
I am happy to receive any suggestions for improving this solution.

Using JDBC, how can I substitute multiple IDs into "DELETE FROM T WHERE id IN (?)"

I have some code that produces a set of primary key values that I want to delete from a database table.
long[] keysToDelete = { 0, 1, 2, 3 };
and I'd like to use a PreparedStatement to execute the equivalent of
DELETE FROM MyTable WHERE myPrimaryKey IN (0, 1, 2, 3);
Any idea how?
Two steps:
Build up the PreparedStatement SQL String with the appropriate # of parameters.
Loop over the array of values and bind each one to its parameter.
Unfortunately, there's no good way to bind an array all at once.
I've written a class to dynamically generate such a multi-parameter query. It currently has some limitations (for quickness of writing) and has not been thoroughly tested, but may be a good way to get you started. Limitations:
Only handles one multi-argument parameter (??)
Falsely recognizes question marks in quotes as parameters
API is not pretty but the alternative was writing a full-on PreparedStatement decorator with lots of state management and that was more work than I was willing to put into it.
Source:
/**
* A PreparedStatement decorator that can bind a set of arguments
*
* A specialized ?? placeholder in a string can be bound to a set of
* values instead of just single values. Currently, only one such
* specialized placeholder is supported, and you must bind it before
* obtaining the prepared statement.
*
* If you want to bind additional values to the PreparedStatement after
* producing it, you must run the parameter index through the param()
* method.
*
* Example use:
*
*
* MultiValueBinder binder = new MultiValueBinder(
* "UPDATE table SET value = ? WHERE id IN (??)", conn);
* binder.setInts(myIds);
*
* PreparedStatement stmt = binder.statement();
* stmt.setString(binder.param(1), "myValue");
*
* ResultSet rs = stmt.executeQuery();
*
* Note: this class is not robust against using question marks in literal
* strings. Don't use them :).
*/
public class MultiValueBinder {
private Connection connection;
private PreparedStatement statement;
private String sql;
private int argumentsBefore = 0;
private int setSize = 0;
public MultiValueBinder(String sql, Connection connection) {
this.sql = sql;
this.connection = connection;
}
/**
* Bind a collection of integers to the multi-valued argument
*/
public void setInts(Collection<Integer> ints) throws SQLException {
explodeQuery(ints.size());
buildStatement();
try {
int i = 0;
for (Integer integer: ints)
statement.setInt(1 + argumentsBefore + i++, integer);
} catch (Exception ex) {
cleanStatement();
throw (ex instanceof SQLException) ? (SQLException) ex : new SQLException(ex);
}
}
/**
* Bind a collection of strings to the multi-valued argument
*/
public void setStrings(Collection<String> strings) throws SQLException {
explodeQuery(strings.size());
buildStatement();
try {
int i = 0;
for (String str: strings)
statement.setString(1 + argumentsBefore + i++, str);
} catch (Exception ex) {
cleanStatement();
throw (ex instanceof SQLException) ? (SQLException) ex : new SQLException(ex);
}
}
/**
* Explode the multi-value parameter into a sequence of comma-separated
* question marks.
*/
private void explodeQuery(int size) throws SQLException {
int mix = sql.indexOf("??");
if (mix == -1) throw new SQLException("Query does not contain a multi-valued argument.");
if (size == 0) throw new SQLException("Can't bind an empty collection; generated SQL won't parse.");
// Count the number of arguments before the multi-marker
argumentsBefore = 0;
for (int i = 0; i < mix; i++) {
if (sql.charAt(i) == '?') argumentsBefore++;
}
setSize = size;
// Generate the exploded SQL query
StringBuilder sb = new StringBuilder(sql.substring(0, mix)); // Start
for (int i = 0; i < setSize; i++) { // ?, ?, ...
if (i > 0) sb.append(", ");
sb.append('?');
}
sb.append(sql.substring(mix + 2)); // Remainder
sql = sb.toString();
}
/**
* Create the statement if it hasn't been created yet
*/
private void buildStatement() throws SQLException {
if (statement != null) return;
if (sql.contains("??"))
throw new SQLException("Multi-valued argument not bound yet.");
statement = connection.prepareStatement(sql);
}
private void cleanStatement() {
if (statement != null) {
try {
statement.close();
} catch (Exception ex) {
/* Ignore */
}
statement = null;
}
}
public PreparedStatement statement() throws SQLException {
buildStatement();
return statement;
}
/**
* Transform the 1-based-index of the given argument before query expansion
* into the index after expansion.
*
* The ?? placeholder takes up one index slot.
*/
public int param(int ix) {
if (ix <= argumentsBefore) return ix;
if (ix == argumentsBefore + 1)
throw new RuntimeException(ix + " is the index of the multi-valued parameter.");
return argumentsBefore + 1 + setSize;
}
}
Not totally sure but this might help:
PreparedStatement pstmt = Connection.prepareStatement("DELETE FROM MyTable WHERE myPrimaryKey IN (?)");
pstmt.setArray(1, idArray);

Categories

Resources