/****************************************************************************** * Copyright (C) 2014 Low Heng Sin * * Copyright (C) 2014 Trek Global * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. This program is distributed in the hope * * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * * with this program; if not, write to the Free Software Foundation, Inc., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * *****************************************************************************/ package org.adempiere.webui.apps; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.adempiere.webui.LayoutUtils; import org.adempiere.webui.component.Label; import org.adempiere.webui.util.ZKUpdateUtil; import org.compiere.model.I_AD_SearchDefinition; import org.compiere.model.MColumn; import org.compiere.model.MLookup; import org.compiere.model.MLookupFactory; import org.compiere.model.MLookupInfo; import org.compiere.model.MPayment; import org.compiere.model.MQuery; import org.compiere.model.MRole; import org.compiere.model.MSearchDefinition; import org.compiere.model.MSysConfig; import org.compiere.model.MTable; import org.compiere.model.MWindow; import org.compiere.model.Query; import org.compiere.util.DB; import org.compiere.util.DisplayType; import org.compiere.util.Env; import org.compiere.util.Msg; import org.compiere.util.Util; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.event.Events; import org.zkoss.zul.A; import org.zkoss.zul.Textbox; import org.zkoss.zul.Vlayout; /** * @author hengsin * */ public class DocumentSearchController implements EventListener{ /** Style for transaction code guide or execution error */ private static final String MESSAGE_LABEL_STYLE = "color: rgba(0,0,0,0.34)"; /** {@link A} component attribute to hold reference to corresponding {@link #SEARCH_RESULT} **/ private static final String SEARCH_RESULT = "search.result"; /** onSearchDocuments event **/ private static final String ON_SEARCH_DOCUMENTS_EVENT = "onSearchDocuments"; private int MAX_RESULTS_PER_SEARCH_IN_DOCUMENT_CONTROLLER = 3; /** layout to show links ({@link A}) for each {@link #SEARCH_RESULT} in {@link #list} **/ private Vlayout layout; /** results from execution of search **/ private ArrayList list; /** Current selected index of {@link #list} **/ private int selected = -1; /** True when showing transaction code available */ private boolean showingGuide = false; /** * default constructor */ public DocumentSearchController() { MAX_RESULTS_PER_SEARCH_IN_DOCUMENT_CONTROLLER = MSysConfig.getIntValue(MSysConfig.MAX_RESULTS_PER_SEARCH_IN_DOCUMENT_CONTROLLER, 3, Env.getAD_Client_ID(Env.getCtx())); } /** * Create {@link #layout} for search result * @param parent */ public void create(Component parent) { layout = new Vlayout(); layout.setStyle("padding: 3px; overflow:auto;"); ZKUpdateUtil.setWidth(layout, "100%"); ZKUpdateUtil.setVflex(layout, "true"); parent.appendChild(layout); layout.addEventListener(ON_SEARCH_DOCUMENTS_EVENT, this); } /** * Echo {@link #ON_SEARCH_DOCUMENTS_EVENT} with value as event data. * @param value */ public void search(String value) { if (Util.isEmpty(value) || (value.startsWith("/") && value.indexOf(" ") < 0)) { if (!showingGuide) layout.getChildren().clear(); } else { layout.getChildren().clear(); } Events.echoEvent(ON_SEARCH_DOCUMENTS_EVENT, layout, value); } /** * Handle {@link #ON_SEARCH_DOCUMENTS_EVENT} event. * Delegate execution of search to {@link #doSearch(String)}. * @param searchString */ private void onSearchDocuments(String searchString) { list = new ArrayList(); if (Util.isEmpty(searchString) || (searchString.startsWith("/") && searchString.indexOf(" ") < 0)) { // No search string, show available transaction code if (!showingGuide) { Query query = new Query(Env.getCtx(), I_AD_SearchDefinition.Table_Name, "TransactionCode IS NOT NULL", null); List definitions = query.setOnlyActiveRecords(true).setOrderBy("TransactionCode").list(); for(MSearchDefinition definition : definitions) { Label label = new Label("/"+definition.getTransactionCode() + " " + definition.getName()); label.setStyle(MESSAGE_LABEL_STYLE); layout.appendChild(label); } showingGuide = true; } return; } showingGuide = false; // Search and show results List list = doSearch(searchString); if (list.size() == 1 && list.get(0).getRecordId() == -1) { // DB error or query timeout Label label = new Label(list.get(0).getLabel()); label.setStyle(MESSAGE_LABEL_STYLE); layout.appendChild(label); } else if (list.size() > 0) { Collections.sort(list, new Comparator() { @Override public int compare(SearchResult o1, SearchResult o2) { int r = o1.getWindowName().compareTo(o2.getWindowName()); if (r == 0) r = o1.getLabel().compareTo(o2.getLabel()); return r; } }); String matchString = searchString.toLowerCase(); if (searchString != null && searchString.startsWith("/") && searchString.indexOf(" ") > 1) { // "/TransactionCode Search Text" matchString = searchString.substring(searchString.indexOf(" ")+1).toLowerCase(); } String windowName = null; for(SearchResult result : list) { if (windowName == null || !windowName.equals(result.getWindowName())) { windowName = result.getWindowName(); Label label = new Label(windowName); LayoutUtils.addSclass("window-name", label); layout.appendChild(label); } A a = new A(); a.setAttribute(SEARCH_RESULT, result); layout.appendChild(a); LayoutUtils.addSclass("search-result", a); a.addEventListener(Events.ON_CLICK, this); String label = result.getLabel(); if (!Util.isEmpty(matchString, true)) { int match = label.toLowerCase().indexOf(matchString); while (match >= 0) { if (match > 0) { a.appendChild(new Label(label.substring(0, match))); Label l = new Label(label.substring(match, match+matchString.length())); LayoutUtils.addSclass("highlight", l); a.appendChild(l); label = label.substring(match+matchString.length()); } else { Label l = new Label(label.substring(0, matchString.length())); LayoutUtils.addSclass("highlight", l); a.appendChild(l); label = label.substring(matchString.length()); } match = label.toLowerCase().indexOf(matchString); } } if (label.length() > 0) a.appendChild(new Label(label)); } layout.invalidate(); } } /** * Perform search with searchString using definition from AD_SearchDefinition. * @param searchString * @return List of {@link SearchResult} */ private List doSearch(String searchString) { final MRole role = MRole.get(Env.getCtx(), Env.getAD_Role_ID(Env.getCtx()), Env.getAD_User_ID(Env.getCtx()), true); selected = -1; // Search with or without transaction code StringBuilder whereClause = new StringBuilder(); String transactionCode = null; if (searchString != null && searchString.startsWith("/") && searchString.indexOf(" ") > 1) { // "/TransactionCode Search Text" transactionCode = searchString.substring(1, searchString.indexOf(" ")); searchString = searchString.substring(searchString.indexOf(" ")+1); whereClause.append("Upper(TransactionCode) = ?"); } else { // Search with definition that doesn't use transaction code whereClause.append("TransactionCode IS NULL"); } Query query = new Query(Env.getCtx(), I_AD_SearchDefinition.Table_Name, whereClause.toString(), null); if (transactionCode != null) query.setParameters(transactionCode.toUpperCase()); List definitions = query.setOnlyActiveRecords(true).list(); for(MSearchDefinition msd : definitions) { MTable table = new MTable(Env.getCtx(), msd.getAD_Table_ID(), null); StringBuilder sql = null; MWindow window = msd.getAD_Window_ID() > 0 && role.getWindowAccess(msd.getAD_Window_ID()) != null ? MWindow.get(Env.getCtx(), msd.getAD_Window_ID()) : null; MWindow powindow = msd.getPO_Window_ID() > 0 && role.getWindowAccess(msd.getPO_Window_ID()) != null ? MWindow.get(Env.getCtx(), msd.getPO_Window_ID()) : null; if (window == null && powindow == null) continue; List params = new ArrayList(); // SearchDefinition with a given table and column if (msd.getSearchType().equals(MSearchDefinition.SEARCHTYPE_TABLE)) { MColumn column = MColumn.get(Env.getCtx(), msd.getAD_Column_ID()); sql = new StringBuilder("SELECT ").append(table.getTableName()).append("_ID, ") .append(column.getColumnName()); sql.append(" FROM ") .append(table.getTableName()) .append(" "); // search for an Integer if (msd.getDataType().equals(MSearchDefinition.DATATYPE_INTEGER)) { sql.append("WHERE ").append(column.getColumnName()).append("=?"); // search for a String } else { sql.append("WHERE UPPER(").append(column.getColumnName()).append(") LIKE UPPER(?)"); } sql.append(" AND AD_Client_ID=@#AD_Client_ID@ "); // search for a Integer if (msd.getDataType().equals(MSearchDefinition.DATATYPE_INTEGER)) { params.add(Integer.valueOf(searchString.replaceAll("\\D", ""))); // search for a String } else if (msd.getDataType().equals(MSearchDefinition.DATATYPE_STRING)) { if (searchString.endsWith("%")) params.add(searchString); else params.add(searchString+"%"); } // SearchDefinition with a special query } else if (msd.getSearchType().equals(MSearchDefinition.SEARCHTYPE_QUERY)) { sql = new StringBuilder().append(msd.getQuery()); // count '?' in statement int count = 1; for (char c : sql.toString().toCharArray()) { if (c == '?') { count++; } } for (int i = 1; i < count; i++) { if (msd.getDataType().equals(MSearchDefinition.DATATYPE_INTEGER)) { params.add(Integer.valueOf(searchString.replaceAll("\\D", ""))); } else if (msd.getDataType().equals(MSearchDefinition.DATATYPE_STRING)) { if (searchString.endsWith("%")) params.add(searchString); else params.add(searchString+"%"); } } } MLookupInfo lookupInfo = MLookupFactory.getLookupInfo(Env.getCtx(), -1, -1, DisplayType.Search, Env.getLanguage(Env.getCtx()), table.getTableName() + "_ID", 0, false, null); MLookup lookup = new MLookup(lookupInfo, -1); if (sql != null) { if (powindow != null) { String whereCol = null; if (table.columnExistsInDictionary("IsSOTrx")) { whereCol = " AND IsSOTrx="; } else { if (MPayment.Table_Name.equals(table.getTableName())) { whereCol = " AND IsReceipt="; } } if (whereCol == null) { doRetrieval(msd, sql, params, lookup, powindow, table.getTableName(), null, list); } else { if (window != null) { String soWhereTrx = whereCol + "'Y' "; doRetrieval(msd, sql, params, lookup, window, table.getTableName(), soWhereTrx, list); } String poWhereTrx = whereCol + "'N' "; doRetrieval(msd, sql, params, lookup, powindow, table.getTableName(), poWhereTrx, list); } } else if (window != null) { doRetrieval(msd, sql, params, lookup, window, table.getTableName(), null, list); } } } return list; } /** * Execute query and output result to list. * @param msd * @param builder * @param params * @param lookup * @param window * @param tableName * @param extraWhereClase * @param list */ private void doRetrieval(MSearchDefinition msd, StringBuilder builder, List params, MLookup lookup, MWindow window, String tableName, String extraWhereClase, List list) { PreparedStatement pstmt = null; ResultSet rs = null; try { String sql = builder.toString(); if (!Util.isEmpty(extraWhereClase)) sql = sql + extraWhereClase; //@@ is full text search operator for postgresql boolean hasFullTextOperator = sql.indexOf("@@") >= 0; if (hasFullTextOperator) sql = sql.replace("@@", "~!#$*"); sql = Env.parseContext(Env.getCtx(), -1, sql, false, true); if (hasFullTextOperator) sql = sql.replace("~!#$*", "@@"); pstmt = DB.prepareStatement(sql, (String)null); if (params.size() > 0) DB.setParameters(pstmt, params); pstmt.setQueryTimeout(1); rs = pstmt.executeQuery(); int count = 0; while (rs.next() && count < MAX_RESULTS_PER_SEARCH_IN_DOCUMENT_CONTROLLER) { count++; int id = rs.getInt(1); SearchResult result = new SearchResult(); result.setLabel(lookup.getDisplay(id)); result.setRecordId(id); result.setWindowName(window.get_Translation("Name")); result.setWindowId(window.getAD_Window_ID()); result.setTableName(tableName); if (rs.getMetaData().getColumnCount() > 1) { result.setName(rs.getString(2)); } list.add(result); } } catch (SQLException e) { SearchResult result = new SearchResult(); result.setRecordId(-1); if (DB.getDatabase().isQueryTimeout(e)) { result.setLabel(Msg.getMsg(Env.getCtx(), "Timeout")); } else { result.setLabel(Msg.getMsg(Env.getCtx(), "DBExecuteError")); e.printStackTrace(); } list.add(result); } finally { DB.close(rs, pstmt); } } @Override public void onEvent(Event event) throws Exception { if (Events.ON_CLICK.equals(event.getName())) { if (event.getTarget() instanceof A) { SearchResult result = (SearchResult) event.getTarget().getAttribute(SEARCH_RESULT); doZoom(result); } } else if (event.getName().equals(ON_SEARCH_DOCUMENTS_EVENT)) { onSearchDocuments((String)event.getData()); } } /** * Zoom to AD Window * @param result */ private void doZoom(SearchResult result) { MQuery query = new MQuery(); query.addRestriction(result.getTableName()+"_ID", "=", result.getRecordId()); AEnv.zoom(result.getWindowId(), query); } /** * Value class to hold search result */ public static class SearchResult { private String windowName; private int windowId; private String tableName; private int recordId; private String label; private String name; /** * @return the windowId */ public int getWindowId() { return windowId; } /** * @param windowId the windowId to set */ public void setWindowId(int windowId) { this.windowId = windowId; } /** * @return the tableName */ public String getTableName() { return tableName; } /** * @param tableName the tableName to set */ public void setTableName(String tableName) { this.tableName = tableName; } /** * @return the windowName */ public String getWindowName() { return windowName; } /** * @param windowName the windowName to set */ public void setWindowName(String windowName) { this.windowName = windowName; } /** * @return the recordId */ public int getRecordId() { return recordId; } /** * @param recordId the recordId to set */ public void setRecordId(int recordId) { this.recordId = recordId; } /** * @return the label */ public String getLabel() { return label; } /** * @param label the label to set */ public void setLabel(String label) { this.label = label; } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } } /** * Find {@link SearchResult} link from {@link #layout} that matches text from textbox. *
* Call {@link #doZoom(SearchResult)} if a match is found. * @param textbox * @return true if a match is found */ public boolean onOk(Textbox textbox) { String text = textbox.getText(); if (Util.isEmpty(text)) return false; text = text.toLowerCase(); int size = layout.getChildren().size(); A firstStart = null; A exact = null; for(int i = 0; i < size; i++) { if (!(layout.getChildren().get(i) instanceof A)) continue; A a = (A) layout.getChildren().get(i); SearchResult result = (SearchResult) a.getAttribute(SEARCH_RESULT); if (result.getLabel().equalsIgnoreCase(text)) { exact = a; break; } else if (text.equalsIgnoreCase(result.getName())) { exact = a; break; } else if (firstStart == null && result.getLabel().toLowerCase().startsWith(text) && text.length() >=3 ) { firstStart = a; } } SearchResult result = null; if (exact != null) result = (SearchResult) exact.getAttribute(SEARCH_RESULT); else if (firstStart != null) result = (SearchResult) firstStart.getAttribute(SEARCH_RESULT); if (result != null) { doZoom(result); return true; } return false; } /** * Select and return {@link SearchResult} that comes before the current selected {@link SearchResult} link in {@link #layout}. * @return {@link SearchResult} */ public SearchResult selectPrior() { if (selected > 0) { selected--; SearchResult result = list.get(selected); List links = layout.getChildren(); for(Component link : links) { if (link instanceof A) { A a = (A) link; if (result.getLabel().equals(a.getLabel())) { a.setSclass("document-search-current-link"); } else if ("document-search-current-link".equals(a.getSclass())) { a.setSclass(null); } } } return result; } return null; } /** * Select and return {@link SearchResult} that comes after the current selected {@link SearchResult} link in {@link #layout}. * @return {@link SearchResult} */ public SearchResult selectNext() { if (selected < (list.size()-1)) { selected++; SearchResult result = list.get(selected); List links = layout.getChildren(); for(Component link : links) { if (link instanceof A) { A a = (A) link; if (result.getLabel().equals(a.getLabel())) { a.setSclass("document-search-current-link"); } else if ("document-search-current-link".equals(a.getSclass())) { a.setSclass(null); } } } return result; } return null; } }