/*
* Created on Dec 31, 2004
*/
package org.akaza.openclinica.web.domain;
import org.akaza.openclinica.control.form.FormProcessor;
import org.akaza.openclinica.view.Link;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* A class for displaying a table of EntityBean objects on the screen.
*
* <p>
* This class facilitates the display of multiple rows with the following UI
* features:
* <ul>
* <li> pagination - ten rows at a time are displayed on the screen
* <li> sorting - rows are sorted according to any column chosen by the user
* <li> searching - rows are filtered according to one or more keywords
* </ul>
*
* <p>
* The process for deploying these features is as follows:
*
* <ol>
* <li> Implement an EntityBeanRow class to store the rows of your table.
*
* <li> In the servlet, obtain a new EntityBeanTable from the FormProcessor:
* <br/><code>EntityBeanTable table = fp.getEntityBeanTable();</code> <br/>
* Note that this step covers both the case where the table is being generated
* for the first time, and the case where the table has already been displayed,
* and the user has requested a modification to the display (e.g., go to the
* next page or sort by a specific column) - the FormProcessor automatically
* reads in any requested modifications from the request and applies them to the
* table.
*
* <li> Populate the table with all of the rows you wish to display: <br/>
* <code>
* ArrayList allUsers = uadao.findAll();
* ArrayList allUsersRows = UserAccountRow.generateRows(allUsers);
* table.setRows(allUsersRows);
* </code>
*
* <li> Populate the table with the columns you wish to display: <br/> <code>
* String columns[] = { "username", "first name", "last name", "status", "actions"};
* table.setColumns(new ArrayList(Arrays.asList(columns)));
* </code>
*
* <li> Populate the table with the base query which invokes the screen you're
* about to display: <code>
* HashMap args = new HashMap();
* args.put("userId", userBean.getId());
* table.setBaseQuery("ViewUserAccount", args);
* </code>
*
* <li> Force the table to compute its display: <br/><code>table.computeDisplay();</code>
*
* <li> Put the table in the request: <br/><code>setTable(table);
* <br/>This method is inherited from SecureController.
*
* <li> Send the user to the JSP with forwardPage as usual.
* </ul>
*
* <p>In the JSP, the table will be displayed by include/showTable.jsp,
* which
*
* @author ssachs
* @see EntityBeanRow
* @see org.akaza.openclinica.control.admin.ListUserAccountsServlet
*
* <p>Method 'computeDisplay()' modified by ywang to remove duplicated items
* when search by keywords.
*/
public class EntityBeanTable {
/**
* The number of rows to display per page.
*/
public static final int NUM_ROWS_PER_PAGE = 10;
/**
* All of the rows which might be displyaed, before computeDisplay() is
* called. All of the rows which will be displayed (after applying the
* tabling parameters), after computeDisplay() is called. Each element is an
* <a href="{@docRoot}/org/akaza/openclinica/core/EntityBeanRow.html">EntityBeanRow</a>s
*/
protected ArrayList rows;
/**
* An array of EntityBeanColumn objects which represent column headings.
*/
protected ArrayList columns;
/**
* Always equal to columns.size(); maintained by setColumns.
*/
protected int numColumns;
/**
* Which page are we viewing now, ranges from 1 to
* ceil(rows.size()/NUM_ROWS_PER_PAGE)
*/
protected int currPageNumber;
/**
* How many page numbers are there total,always equal to ceil(rows.size() /
* NUM_ROWS_PER_PAGE); maintained by setRows.
*/
protected int totalPageNumbers;
/**
* Indicates the column we're currently sorting by. Ranges form 0 to
* columns.size() - 1; interpreted as an index into columns.
*/
protected int sortingColumnInd;
/**
* Indicates whether the sorting column was explicitly set in the GET or
* POST variables. This makes the functionality in
* setSortingIfNotExplicitlySet possible.
*/
protected boolean sortingColumnExplicitlySet = false;
/**
* <code>true</code> if we're sorting in ascending order,
* <code>false</code> otherwise
*/
protected boolean ascendingSort;
/**
* <code>true</code> if we use the keyword filter <code>false</code>
* otherwise
*/
protected boolean filtered;
/**
* String the user wants to filter the rows by.
*/
protected String keywordFilter;
/**
* <code>true</code> if we are viewing at most NUM_ROWS_PER_PAGE at a time
* <code>false</code> if we are viewing all of the pages on one screen
*/
protected boolean paginated = true;
/**
* A set of links to display in the upper-right hand corner. Each element is
* a Link object.
*/
protected ArrayList links;
protected String postAction;
protected HashMap postArgs;
protected String baseGetQuery;
protected String noRowsMessage;
protected String noColsMessage;
public EntityBeanTable() {
rows = new ArrayList();
columns = new ArrayList();
numColumns = 0;
currPageNumber = 1;
totalPageNumbers = 0;
sortingColumnInd = 0;
ascendingSort = true;
filtered = false;
keywordFilter = "";
postAction = "";
postArgs = new HashMap();
baseGetQuery = "";
noRowsMessage = "";
noColsMessage = "";
links = new ArrayList();
}
/**
* @return Returns the totalPageNumbers.
*/
public int getTotalPageNumbers() {
return totalPageNumbers;
}
/**
* @return Returns the columns.
*/
public ArrayList getColumns() {
return columns;
}
/**
* @param columns
* The columns to set. Each element is a String with the column
* name.
*/
public void setColumns(ArrayList columns) {
// this.columns = columns;
ArrayList newColumns = new ArrayList();
for (int i = 0; i < columns.size(); i++) {
String name = (String) columns.get(i);
EntityBeanColumn c = new EntityBeanColumn();
c.setName(name);
newColumns.add(c);
}
this.columns = newColumns;
numColumns = this.columns.size();
}
/**
* @return Returns the ascendingSort.
*/
public boolean isAscendingSort() {
return ascendingSort;
}
/**
* @return Returns the currPageNumber.
*/
public int getCurrPageNumber() {
return currPageNumber;
}
/**
* @return Returns the keywordFilter.
*/
public String getKeywordFilter() {
return keywordFilter;
}
/**
* @return Returns the rows.
*/
public ArrayList getRows() {
return rows;
}
/**
* Re-computes the correct value of page numbers as:
* <code>rows.size() / NUM_ROWS_PER_PAGE</code>
*/
private void updateTotalPageNumbers() {
totalPageNumbers = rows.size() / NUM_ROWS_PER_PAGE;
if (rows.size() > totalPageNumbers * NUM_ROWS_PER_PAGE) {
totalPageNumbers++;
}
}
/**
*
* @param rows
*/
public void setRows(ArrayList rows) {
this.rows = rows;
updateTotalPageNumbers();
}
// /**
// * Adds an entity to display at the bottom of the table.
// * @param e The entity to add to the table.
// */
// public void addRow(EntityBean e) {
// rows.add(e);
// updateTotalPageNumbers();
// }
/**
* @return Returns the filtered.
*/
public boolean isFiltered() {
return filtered;
}
/**
* @return Returns the sortingColumnInd.
*/
public int getSortingColumnInd() {
return sortingColumnInd;
}
/**
* @return Returns the numColumns.
*/
public int getNumColumns() {
return numColumns;
}
public void setQuery(String baseURL, HashMap args) {
postAction = baseURL;
postArgs = args;
baseGetQuery = baseURL + "?";
baseGetQuery += FormProcessor.FIELD_SUBMITTED + "=" + 1;
Iterator it = args.keySet().iterator();
while (it.hasNext()) {
String key = (String) it.next();
String value = (String) args.get(key);
// TODO: provide URL Encoding!
baseGetQuery += "&" + key + "=" + value;
}
}
/**
* @return Returns the baseGetQuery.
*/
public String getBaseGetQuery() {
return baseGetQuery;
}
/**
* @return Returns the postAction.
*/
public String getPostAction() {
return postAction;
}
/**
* @return Returns the postArgs.
*/
public HashMap getPostArgs() {
return postArgs;
}
/**
* @param ascendingSort
* The ascendingSort to set.
*/
public void setAscendingSort(boolean ascendingSort) {
this.ascendingSort = ascendingSort;
}
/**
* @param currPageNumber
* The currPageNumber to set.
*/
public void setCurrPageNumber(int currPageNumber) {
this.currPageNumber = currPageNumber;
}
/**
* @param filtered
* The filtered to set.
*/
public void setFiltered(boolean filtered) {
this.filtered = filtered;
}
/**
* @param keywordFilter
* The keywordFilter to set.
*/
public void setKeywordFilter(String keywordFilter) {
this.keywordFilter = keywordFilter;
}
/**
* @param sortingColumnInd
* The sortingColumnInd to set.
*/
public void setSortingColumnInd(int sortingColumnInd) {
this.sortingColumnInd = sortingColumnInd;
}
/**
* @return Returns the noColsMessage.
*/
public String getNoColsMessage() {
return noColsMessage;
}
/**
* @return Returns the noRowsMessage.
*/
public String getNoRowsMessage() {
return noRowsMessage;
}
/**
* @return Returns the paginated.
*/
public boolean isPaginated() {
return paginated;
}
/**
* @param paginated
* The paginated to set.
*/
public void setPaginated(boolean paginated) {
this.paginated = paginated;
}
/**
* Compute the subset of rows which should be shown on the screen. Note that
* the tabling parameters should be set properly before this method is
* called!
*/
public void computeDisplay() {
ArrayList displayRows;
Set temprows = new HashSet();
// *****************
// FILTER BY KEYWORD
// *****************
// the filter is considered to have been executed if the keyword filter
// bit is on,
// and if there is at least one keyword to search by
boolean filterExecuted = false;
displayRows = new ArrayList();
if (filtered) {
String[] keywords = null;
if(keywordFilter != null) {
if(keywordFilter.startsWith(" ")){
//the search allows the user to implement a search such as " 20 " thus
//searching only for space char-20-space char alone, not 1920, for example
keywords = new String[]{keywordFilter};
} else {
//split the keywords on a space character
keywords = keywordFilter.split("\\s");
}
}
if (keywords != null) {
for (int j = 0; j < keywords.length; j++) {
String keyword = keywords[j];
if (keyword == null || "".equals(keyword)) {
continue;
}
keyword = keyword.toLowerCase();
filterExecuted = true;
loopRows:
for (int i = 0; i < rows.size(); i++) {
EntityBeanRow row = (EntityBeanRow) rows.get(i);
String searchString = row.getSearchString().toLowerCase();
//If the keyword matches the whole search string, return a match
if (searchString.equalsIgnoreCase(keyword)){
temprows.add(row);
//continue searching the next row
continue loopRows;
}
//if the searchString contains "-" chars such as ATS-120-5
// then split the searchString on
//that character, and determine if any components of the split result match the
//keyword; SEE issue 2640
if(searchString.contains("-")){
//The searchString is the combination of the subject's primary
//and secondary identifiers
//First split the search string on a space, to accomodate
//substring searches on a search string like subject id - secondary id
String[] newSearchString = searchString.split(" ");
String[] subStrings = null;
//each component is half of a string like "subject id secondary id"
for(String component : newSearchString){
//if the entire split searchString matches the keyword...
// if (component.indexOf(keyword) >= 0){
if (component.equalsIgnoreCase(keyword)){
temprows.add(row);
//continue searching the next row
continue loopRows;
}
//if the component does contain a "-" but the entire
//component does not match the keyword
subStrings = component.split("-");
for(String innerStr : subStrings) {
//An exact match has been requested here; see 2640
//This allows the breaking up of ids like ATS-120-5 and
//and exact seaches on the separate parts like 120
if(innerStr.equalsIgnoreCase(keyword)){
temprows.add(row);
}
}
}
//continue to the next row, because the searchString contained a "-",
//and the keyword was searched for in both ways, with and without
//splitting on "-"
continue;
} //end searchString.contains("-")
//the search string doesn't contain "-"
if (searchString.indexOf(keyword) >= 0) {
temprows.add(row);
}
} // end of loop iterating over rows
} // end of loop iterating over keywords
}
Iterator it = temprows.iterator();
while (it.hasNext()) {
displayRows.add(it.next());
}
} // end of filtering by keywords
if (!filterExecuted) {
displayRows = rows;
}
// this seems redundant, since we set the rows property below before
// returning from the method
// the reason for this call is to reset the totalNumPages property,
// to reflect the number of rows that matched the search terms (if any)
setRows(displayRows);
// *************
// SORT THE ROWS
// *************
for (int i = 0; i < displayRows.size(); i++) {
EntityBeanRow row = (EntityBeanRow) displayRows.get(i);
row.setSortingColumn(sortingColumnInd);
row.setAscendingSort(ascendingSort);
displayRows.set(i, row);
}
Collections.sort(displayRows);
// ****************
// APPLY PAGINATION
// ****************
if (paginated) {
if (currPageNumber < 1) {
currPageNumber = 1;
}
if (currPageNumber > totalPageNumbers && totalPageNumbers > 0) {
currPageNumber = totalPageNumbers;
}
int firstInd = (currPageNumber - 1) * NUM_ROWS_PER_PAGE;
int lastInd = currPageNumber * NUM_ROWS_PER_PAGE;
lastInd = lastInd > displayRows.size() ? displayRows.size() : lastInd;
// JRWS>> This block added to catch issue 1223, where searching a
// large list of studies fails when search criteria result in zero
// studies in the list, but you are on the third page of the list
// when you perform the search
if (firstInd > lastInd && lastInd == 0) {
firstInd = 0;
}
ArrayList currPage = new ArrayList(displayRows.subList(firstInd, lastInd));
// it's important not to use setRows here
// calling setRows will change totalNumPages to be the number of
// pages in currPage (always 1)
// we don't want to change totalNumPages since it'll screw up the
// display of "Previous" and "Next" page links
rows = currPage;
} else {
rows = displayRows;
}
}
/**
* @return Returns the links.
*/
public ArrayList getLinks() {
return links;
}
/**
* @param links
* The links to set.
*/
public void setLinks(ArrayList links) {
this.links = links;
}
public void addLink(String caption, String url) {
Link l = new Link(caption, url);
links.add(l);
}
/**
* @return Returns the sortingColumnExplicitlySet.
*/
public boolean isSortingColumnExplicitlySet() {
return sortingColumnExplicitlySet;
}
/**
* @param sortingColumnExplicitlySet
* The sortingColumnExplicitlySet to set.
*/
public void setSortingColumnExplicitlySet(boolean sortingColumnExplicitlySet) {
this.sortingColumnExplicitlySet = sortingColumnExplicitlySet;
}
public void setSortingIfNotExplicitlySet(int sortingColumnInd, boolean ascendingSort) {
if (!sortingColumnExplicitlySet) {
this.sortingColumnInd = sortingColumnInd;
this.ascendingSort = ascendingSort;
}
}
/**
* Signal that the column at index <code>i</code> should not be displayed
* with a link in the JSP; this prevents users from sorting on the i-th
* column.
*
* @param i
* The index of the column whose link should not be displayed.
* The first column is 0.
*/
public void hideColumnLink(int i) {
if (i >= 0 && i < columns.size()) {
EntityBeanColumn c = (EntityBeanColumn) columns.get(i);
c.setShowLink(false);
columns.set(i, c);
}
}
}