com.redhat.rhn.frontend.action.errata.ErrataSearchAction.java Source code

Java tutorial

Introduction

Here is the source code for com.redhat.rhn.frontend.action.errata.ErrataSearchAction.java

Source

/**
 * 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;
        }
    }

}