/** * Copyright (c) 2009--2014 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package com.redhat.rhn.frontend.action.errata; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TimeZone; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessages; import org.apache.struts.action.DynaActionForm; import redstone.xmlrpc.XmlRpcClient; import redstone.xmlrpc.XmlRpcFault; import com.redhat.rhn.common.conf.ConfigDefaults; import com.redhat.rhn.common.db.datasource.DataResult; import com.redhat.rhn.common.util.DatePicker; import com.redhat.rhn.domain.org.Org; import com.redhat.rhn.frontend.action.BaseSearchAction; import com.redhat.rhn.frontend.action.common.DateRangePicker; import com.redhat.rhn.frontend.action.common.DateRangePicker.DatePickerResults; import com.redhat.rhn.frontend.dto.ErrataOverview; import com.redhat.rhn.frontend.struts.RequestContext; import com.redhat.rhn.frontend.struts.RhnHelper; import com.redhat.rhn.manager.errata.ErrataManager; /** * SearchAction * @version $Rev$ */ public class ErrataSearchAction extends BaseSearchAction { protected ActionForward doExecute(HttpServletRequest request, ActionMapping mapping, DynaActionForm form) throws MalformedURLException, XmlRpcFault { RequestContext ctx = new RequestContext(request); String search = form.getString(SEARCH_STR); String viewmode = form.getString(VIEW_MODE); Boolean fineGrained = (Boolean)form.get(FINE_GRAINED); List searchOptions = new ArrayList(); // setup the option list for select box (view_mode). addOption(searchOptions, OPT_ALL_FIELDS, OPT_ALL_FIELDS); addOption(searchOptions, OPT_ADVISORY, OPT_ADVISORY); addOption(searchOptions, OPT_PKG_NAME, OPT_PKG_NAME); addOption(searchOptions, OPT_CVE, OPT_CVE); request.setAttribute(SEARCH_STR, search); request.setAttribute(VIEW_MODE, viewmode); request.setAttribute(SEARCH_OPT, searchOptions); request.setAttribute(FINE_GRAINED, fineGrained); // Process the dates, default the start date to yesterday // and end date to today. Calendar today = Calendar.getInstance(); today.setTime(new Date()); Calendar yesterday = Calendar.getInstance(); yesterday.setTime(new Date()); yesterday.add(Calendar.DAY_OF_YEAR, -1); DateRangePicker picker = new DateRangePicker(form, request, yesterday.getTime(), today.getTime(), DatePicker.YEAR_RANGE_NEGATIVE, "erratasearch.jsp.start_date", "erratasearch.jsp.end_date"); DatePickerResults dates = null; Boolean dateSearch = getOptionIssueDateSearch(request); /* * If search/viewmode aren't null, we need to search and set * pageList to the resulting DataResult. * * NOTE: There is a special case when called from rhn/Search.do * (header search bar) * that we will be coming into this action and running the * performSearch on the first run through this action, i.e. * we'll never have been called with search being blank, * therefore normal setup of the form vars will not have happened. */ if (!StringUtils.isBlank(search) || dateSearch) { // If doing a dateSearch use the DatePicker values from the // request params otherwise use the defaults. dates = picker.processDatePickers(dateSearch, true); if (LOG.isDebugEnabled()) { LOG.debug("search is NOT blank"); LOG.debug("Issue Start Date = " + dates.getStart().getDate()); LOG.debug("End Start Date = " + dates.getEnd().getDate()); } List results = performSearch(request, ctx.getWebSession().getId(), search, viewmode, form); request.setAttribute(RequestContext.PAGE_LIST, results != null ? results : Collections.EMPTY_LIST); } else { // Reset info on date pickers dates = picker.processDatePickers(false, true); if (LOG.isDebugEnabled()) { LOG.debug("search is blank"); LOG.debug("Issue Start Date = " + dates.getStart().getDate()); LOG.debug("End Start Date = " + dates.getEnd().getDate()); } request.setAttribute(RequestContext.PAGE_LIST, Collections.EMPTY_LIST); } ActionMessages dateErrors = dates.getErrors(); addErrors(request, dateErrors); return mapping.findForward(RhnHelper.DEFAULT_FORWARD); } /** * Make sure we have appropriate defaults no matter how we got here * Set the defaults (where needed) back into the form so that the rest of the action * can find them * @param form where we expect values to be */ protected void insureFormDefaults(HttpServletRequest request, DynaActionForm form) { String viewmode = form.getString(VIEW_MODE); if (viewmode.equals("")) { //first time viewing page form.set(VIEW_MODE, "errata_search_by_all_fields"); } Boolean fineGrained = (Boolean)form.get(FINE_GRAINED); if (fineGrained == null) { fineGrained = false; form.set(FINE_GRAINED, fineGrained); } Boolean issueDateSrch = (Boolean)form.get(OPT_ISSUE_DATE); if (issueDateSrch == null) { form.set(OPT_ISSUE_DATE, Boolean.FALSE); } Boolean eTypeBug = (form.get(ERRATA_BUG) == null ? Boolean.FALSE : (Boolean)form.get(ERRATA_BUG)); Boolean eTypeSec = (form.get(ERRATA_SEC) == null ? Boolean.FALSE : (Boolean)form.get(ERRATA_SEC)); Boolean eTypeEnh = (form.get(ERRATA_ENH) == null ? Boolean.FALSE : (Boolean)form.get(ERRATA_ENH)); // If no errata-type is set, set them all if (!(eTypeBug || eTypeSec || eTypeEnh)) { form.set(ERRATA_BUG, Boolean.TRUE); form.set(ERRATA_SEC, Boolean.TRUE); form.set(ERRATA_ENH, Boolean.TRUE); } Map m = form.getMap(); Set<String> keys = m.keySet(); for (String key : keys) { Object vObj = m.get(key); request.setAttribute(key, vObj); } } protected List performSearch(HttpServletRequest request, Long sessionId, String searchString, String mode, DynaActionForm formIn) throws XmlRpcFault, MalformedURLException { LOG.debug("Performing errata search"); RequestContext ctx = new RequestContext(request); Org org = ctx.getCurrentUser().getOrg(); // call search server XmlRpcClient client = new XmlRpcClient( ConfigDefaults.get().getSearchServerUrl(), true); String path = null; List args = new ArrayList(); args.add(sessionId); // do a package search instead of an errata one. This uses // a different lucene index to find pkgs then reconciles // them with the errata later. if (OPT_PKG_NAME.equals(mode)) { args.add("package"); } else { args.add("errata"); } List results = new ArrayList(); // // Note: This is how "issue date" search works. // It functions in one of 2 ways, depending on the state of "searchString" // 1) It's a database lookup for all errata issued between the given range // - OR - // 2) It's a filter performed AFTER the regular search. // // The database lookup happens when no searchstring was specified, // i.e. searchString is blank. This signifies to do a full lookup to the // database....through the search-server as "db.search". // // The second responsibility is to filter results from a returned search. // This will happen when searchString is not empty AND issue date search // has been activated. Search will proceed as normal, then the final step // will be to filter the results by issue date. // Boolean dateSearch = getOptionIssueDateSearch(request); LOG.debug("Datesearch is " + dateSearch); Date startDate = getPickerDate(request, "start"); Date endDate = getPickerDate(request, "end"); if (dateSearch && StringUtils.isBlank(searchString)) { // this is a full issue date search, not just a filter args.add("listErrataByIssueDateRange:(" + getDateString(startDate) + ", " + getDateString(endDate) + ")"); } else { args.add(preprocessSearchString(searchString, mode)); } if ((dateSearch && StringUtils.isBlank(searchString)) || OPT_CVE.equals(mode)) { // Tells search server to search the database path = "db.search"; } else { Boolean fineGrained = (Boolean) formIn.get(FINE_GRAINED); args.add(fineGrained); // Tells search server to use the lucene index path = "index.search"; } if (LOG.isDebugEnabled()) { LOG.debug("Calling to search server (XMLRPC): \"index.search\", args=" + args); } results = (List)client.invoke(path, args); if (LOG.isDebugEnabled()) { LOG.debug("results = [" + results + "]"); } if (results.isEmpty()) { return Collections.emptyList(); } // need to make the search server results usable by database // so we can get the actual results we are to display to the user. // also save the items into a Map for lookup later. List<Long> ids = new ArrayList<Long>(); Map<Long, Integer> lookupmap = new HashMap<Long, Integer>(); // do it in reverse because the search server can return more than one // record for a given package name, but that means if we don't go // in reverse we risk getting the wrong rank in the lookupmap. // for example, [{id:125,name:gtk},{id:127,name:gtk}{id:200,name:kernel}] // if we go forward we end up with gtk:1 and kernel:2 but we wanted // kernel:2, gtk:0. for (int x = results.size() - 1; x >= 0; x--) { Map item = (Map) results.get(x); lookupmap.put(new Long((String)item.get("id")), x); Long id = new Long((String)item.get("id")); ids.add(id); } // The database does not maintain the order of the where clause. // In order to maintain the ranking from the search server, we // need to reorder the database results to match. This will lead // to a better user experience. List<ErrataOverview> unsorted = new ArrayList<ErrataOverview>(); if (OPT_PKG_NAME.equals(mode)) { unsorted = ErrataManager.searchByPackageIdsWithOrg(ids, ctx.getCurrentUser().getOrg()); } else { unsorted = fleshOutErrataOverview(ids, org); } if (OPT_CVE.equals(mode)) { // Flesh out all CVEs for each errata returned..generally this is a // small number of Errata to operate on. for (ErrataOverview eo : unsorted) { DataResult dr = ErrataManager.errataCVEs(eo.getId()); eo.setCves(dr); } } List<ErrataOverview> filtered = new ArrayList<ErrataOverview>(); // Filter based on errata type selected List<ErrataOverview> filteredByType = new ArrayList<ErrataOverview>(); filteredByType = filterByAdvisoryType(unsorted, formIn); List<ErrataOverview> filteredByIssueDate = new ArrayList<ErrataOverview>(); if (dateSearch && !StringUtils.isBlank(searchString)) { // search string is not blank, therefore a search was run so filter the results LOG.debug("Performing filter on issue date, we only want records between " + startDate + " - " + endDate); filteredByIssueDate = filterByIssueDate(filteredByType, startDate, endDate); filtered.addAll(filteredByIssueDate); } else { // skip issue date filter filtered.addAll(filteredByType); } if (LOG.isDebugEnabled()) { LOG.debug(filtered.size() + " records have passed being filtered " + "and will be displayed."); } // TODO: need to figure out a way to properly sort the // errata from a package search. What we get back from the // search server is pid, pkg-name in relevant order. // What we get back from searchByPackageIds, is an unsorted // list of ErrataOverviews where each one contains more than one // package-name, but no package ids. if (OPT_PKG_NAME.equals(mode)) { return filtered; } // Using a lookup map created from the results returned by search server. // The issue is that the search server returns us a list in a order which is // relevant to score the object received from the search. // When we "flesh" out the ErrataOverview by calling into the database we // lose this order, that's what we are trying to reclaim, this way when then // results are returned to the webpage they will be in a meaningfull order. List<ErrataOverview> ordered = new LinkedList<ErrataOverview>(); for (ErrataOverview eo : filtered) { if (LOG.isDebugEnabled()) { LOG.debug("Processing eo: " + eo.getAdvisory() + " id: " + eo.getId()); } int idx = lookupmap.get(eo.getId()); if (ordered.isEmpty()) { ordered.add(eo); continue; } boolean added = false; for (ListIterator itr = ordered.listIterator(); itr.hasNext();) { ErrataOverview curpo = (ErrataOverview) itr.next(); int curidx = lookupmap.get(curpo.getId()); if (idx <= curidx) { itr.previous(); itr.add(eo); added = true; break; } } if (!added) { ordered.add(eo); } } return ordered; } private List<ErrataOverview> filterByIssueDate(List<ErrataOverview> unfiltered, Date startDate, Date endDate) { if (LOG.isDebugEnabled()) { LOG.debug("Filtering " + unfiltered.size() + " records based on Issue Date"); LOG.debug("Allowed issue date range is " + startDate + " to " + endDate); } List<ErrataOverview> filteredByIssueDate = new ArrayList<ErrataOverview>(); for (ErrataOverview eo : unfiltered) { if (!startDate.after(eo.getIssueDateObj()) && !eo.getIssueDateObj().after(endDate)) { filteredByIssueDate.add(eo); } } return filteredByIssueDate; } private List<ErrataOverview> filterByAdvisoryType(List<ErrataOverview> unfiltered, DynaActionForm formIn) { if (LOG.isDebugEnabled()) { LOG.debug("Filtering " + unfiltered.size() + " records based on Advisory type"); LOG.debug("BugFixes = " + formIn.get(ERRATA_BUG)); LOG.debug("Security = " + formIn.get(ERRATA_SEC)); LOG.debug("Enhancement = " + formIn.get(ERRATA_ENH)); } List<ErrataOverview> filteredByType = new ArrayList<ErrataOverview>(); for (ErrataOverview eo : unfiltered) { Boolean type = null; if (eo.isBugFix()) { type = (Boolean)formIn.get(ERRATA_BUG); if (type != null) { if (type) { filteredByType.add(eo); } } } if (eo.isSecurityAdvisory()) { type = (Boolean)formIn.get(ERRATA_SEC); if (type != null) { if (type) { filteredByType.add(eo); } } } if (eo.isProductEnhancement()) { type = (Boolean)formIn.get(ERRATA_ENH); if (type != null) { if (type) { filteredByType.add(eo); } } } } return filteredByType; } private List<ErrataOverview> fleshOutErrataOverview(List<Long> idsIn, Org org) { // Chunk the work to avoid issue with Oracle not liking // an input parameter list to contain more than 1000 entries. // issue most commonly seen with issue date range search List<ErrataOverview> unsorted = new ArrayList<ErrataOverview>(); int chunkCount = 500; if (chunkCount > idsIn.size()) { chunkCount = idsIn.size(); } int toIndex = chunkCount; int recordsRead = 0; while (recordsRead < idsIn.size()) { List<Long> chunkIDs = idsIn.subList(recordsRead, toIndex); if (chunkIDs.size() == 0) { LOG.warn("Processing 0 size chunkIDs....something seems wrong."); break; } List<ErrataOverview> temp = ErrataManager.search(chunkIDs, org); unsorted.addAll(temp); toIndex += chunkCount; recordsRead += chunkIDs.size(); if (toIndex >= idsIn.size()) { toIndex = idsIn.size(); } } return unsorted; } private String getDateString(Date date) { Calendar cal = Calendar.getInstance(TimeZone.getDefault()); cal.setTime(date); String dateFmt = "yyyy-MM-dd HH:mm:ss"; java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(dateFmt); sdf.setTimeZone(TimeZone.getDefault()); String currentTime = sdf.format(cal.getTime()); return currentTime; } protected String preprocessSearchString(String searchstring, String mode) { StringBuilder buf = new StringBuilder(searchstring.length()); String[] tokens = searchstring.split(" "); for (String s : tokens) { if (s.trim().equalsIgnoreCase("AND") || s.trim().equalsIgnoreCase("OR") || s.trim().equalsIgnoreCase("NOT")) { s = s.toUpperCase(); } buf.append(s); buf.append(" "); } String query = buf.toString().trim(); if (OPT_ALL_FIELDS.equals(mode)) { query = escapeSpecialChars(query); return "(description:(" + query + ") topic:(" + query + ") solution:(" + query + ") notes:(" + query + ") product:(" + query + ")" + " name:(" + query + ") synopsis:(" + query + "))"; } else if (OPT_ADVISORY.equals(mode)) { query = escapeSpecialChars(query); return "(name:(" + query + "))"; } else if (OPT_PKG_NAME.equals(mode)) { // when searching the name field, we also want to include the filename // field in case the user passed in version number. return "(name:(" + query + ") filename:(" + query + "))"; } else if (OPT_CVE.equals(mode)) { query = "%" + query.toLowerCase() + "%"; return "listErrataByCVE:(" + query + ")"; } // OPT_FREE_FORM send as is. return buf.toString(); } private Date getPickerDate(HttpServletRequest request, String paramName) { Date d = null; DatePicker dPick = (DatePicker)request.getAttribute(paramName); if (dPick == null) { LOG.debug("DatePicker for request attribute '" + paramName + "' was null"); d = new Date(); } else { d = dPick.getDate(); } return d; } private String escapeSpecialChars(String queryIn) { // These are the list of possible chars to escape for Lucene: // + - && || ! ( ) { } [ ] ^ " ~ * ? : \ String query = queryIn.replace(":", "\\:"); return query; } private Boolean getOptionIssueDateSearch(HttpServletRequest request) { Object dateSrch = request.getAttribute(OPT_ISSUE_DATE); if (dateSrch == null) { return false; } if (dateSrch instanceof Boolean) { return (Boolean) dateSrch; } else if (dateSrch instanceof String) { if ("on".equals(dateSrch)) { return true; } return false; } else { return false; } } }