/* ********************************************************************** ** ** Copyright notice ** ** ** ** (c) 2005-2009 RSSOwl Development Team ** ** http://www.rssowl.org/ ** ** ** ** All rights reserved ** ** ** ** This program and the accompanying materials are made available under ** ** the terms of the Eclipse Public License v1.0 which accompanies this ** ** distribution, and is available at: ** ** http://www.rssowl.org/legal/epl-v10.html ** ** ** ** A copy is found in the file epl-v10.html and important notices to the ** ** license from the team is found in the textfile LICENSE.txt distributed ** ** in this package. ** ** ** ** This copyright notice MUST APPEAR in all copies of the file! ** ** ** ** Contributors: ** ** RSSOwl Development Team - initial API and implementation ** ** ** ** ********************************************************************** */ package org.rssowl.ui.internal.editors.feed; import org.eclipse.jface.viewers.AbstractTreeViewer; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.rssowl.core.Owl; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.IFolderChild; import org.rssowl.core.persist.IModelFactory; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INewsMark; import org.rssowl.core.persist.ISearchCondition; import org.rssowl.core.persist.ISearchField; import org.rssowl.core.persist.ISearchMark; import org.rssowl.core.persist.SearchSpecifier; import org.rssowl.core.persist.reference.NewsReference; import org.rssowl.core.persist.service.PersistenceException; import org.rssowl.core.util.DateUtils; import org.rssowl.core.util.SearchHit; import org.rssowl.core.util.StringUtils; import org.rssowl.ui.internal.Activator; import org.rssowl.ui.internal.FolderNewsMark; import org.rssowl.ui.internal.util.ModelUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; /** * @author bpasero */ public class NewsFilter extends ViewerFilter { /** Possible Filter Values */ public enum Type { /** Show all News */ SHOW_ALL(Messages.NewsFilter_SHOW_ALL, Messages.NewsFilter_ALL_NEWS), /** Show New News */ SHOW_NEW(Messages.NewsFilter_SHOW_NEW, Messages.NewsFilter_NEW_NEWS), /** Show Unread News */ SHOW_UNREAD(Messages.NewsFilter_SHOW_UNREAD, Messages.NewsFilter_UNREAD_NEWS), /** Show Recent News */ SHOW_RECENT(Messages.NewsFilter_SHOW_RECENT, Messages.NewsFilter_RECENT_NEWS), /** Show Sticky News */ SHOW_STICKY(Messages.NewsFilter_SHOW_STICKY, Messages.NewsFilter_STICKY_NEWS), /** Show Recent News */ SHOW_LAST_5_DAYS(Messages.NewsFilter_SHOW_LAST_DAYS, Messages.NewsFilter_LAST_DAYS), /** Show Labeled News */ SHOW_LABELED(Messages.NewsFilter_SHOW_LABELED_NEWS, Messages.NewsFilter_LABELED_NEWS); String fName; String fDisplayName; Type(String actionName, String displayName) { fName = actionName; fDisplayName = displayName; } /** * Returns a human-readable Name of this enum-value. * * @return A human-readable Name of this enum-value. */ public String getName() { return fName; } /** * @return the display name when this grouping is active. */ public String getDisplayName() { return fDisplayName; } } /** Possible Search Targets */ public enum SearchTarget { /** Search Headlines */ HEADLINE(Messages.NewsFilter_HEADLINE), /** Search Entire News */ ALL(Messages.NewsFilter_ENTIRE_NEWS), /** Search Author */ AUTHOR(Messages.NewsFilter_AUTHOR), /** Search Category */ CATEGORY(Messages.NewsFilter_CATEGORY), /** Search Source */ SOURCE(Messages.NewsFilter_SOURCE), /** Search Attachments */ ATTACHMENTS(Messages.NewsFilter_ATTACHMENTS), /** Search Labels */ LABELS(Messages.NewsFilter_LABELS); String fName; SearchTarget(String name) { fName = name; } /** * Returns a human-readable Name of this enum-value. * * @return A human-readable Name of this enum-value. */ public String getName() { return fName; } } /* Current Filter Value */ private Type fType = Type.SHOW_ALL; /* Current Search Target */ private SearchTarget fSearchTarget = SearchTarget.HEADLINE; /* Misc. */ private INewsMark fNewsMark; private Set<Long> fCachedPatternMatchingNews; private IModelFactory fModelFactory = Owl.getModelFactory(); private String fPatternString; /** * @param newsMark the {@link INewsMark} that is used as source for all news. */ public void setNewsMark(INewsMark newsMark) { fNewsMark = newsMark; } /* * @see org.eclipse.jface.viewers.ViewerFilter#select(org.eclipse.jface.viewers.Viewer, * java.lang.Object, java.lang.Object) */ @Override public final boolean select(Viewer viewer, Object parentElement, Object element) { /* Filter not Active */ if (fCachedPatternMatchingNews == null && fType == Type.SHOW_ALL) return true; return isElementVisible(viewer, element); } /** * Answers whether the given element in the given viewer matches the filter * pattern. This is a default implementation that will show a leaf element in * the tree based on whether the provided filter text matches the text of the * given element's text, or that of it's children (if the element has any). * Subclasses may override this method. * * @param viewer the tree viewer in which the element resides * @param element the element in the tree to check for a match * @return true if the element matches the filter pattern */ boolean isElementVisible(Viewer viewer, Object element) { return isParentMatch(viewer, element) || isLeafMatch(viewer, element, false); } /** * Answers whether the given element is a valid selection in the filtered * tree. For example, if a tree has items that are categorized, the category * itself may not be a valid selection since it is used merely to organize the * elements. * * @param element * @return true if this element is eligible for automatic selection */ boolean isElementSelectable(Object element) { return element != null; } /** * Check if the parent (category) is a match to the filter text. The default * behavior returns true if the element has at least one child element that is * a match with the filter text. Subclasses may override this method. * * @param viewer the viewer that contains the element * @param element the tree element to check * @return true if the given element has children that matches the filter text */ private boolean isParentMatch(Viewer viewer, Object element) { if (viewer instanceof AbstractTreeViewer) { ITreeContentProvider provider = (ITreeContentProvider) ((AbstractTreeViewer) viewer).getContentProvider(); Object[] children = provider.getChildren(element); if ((children != null) && (children.length > 0)) return filter(viewer, element, children).length > 0; } return false; } /** * @param news the {@link INews} to check if matching the filter or not. * @param ignorePattern if <code>true</code> ignore the text search pattern * and <code>false</code> otherwise. * @return <code>true</code> if the given {@link INews} should be selected by * the filter and <code>false</code> otherwise. */ boolean select(INews news, boolean ignorePattern) { return isLeafMatch(null, news, ignorePattern); } /** * @param news the identifier of the {@link INews} to check if matching the * text based filter or not. * @return <code>true</code> if the given {@link INews} should be selected by * the text based filter and <code>false</code> otherwise. */ boolean isTextPatternMatch(Long newsId) { return fCachedPatternMatchingNews == null || fCachedPatternMatchingNews.contains(newsId); } /** * Check if the current (leaf) element is a match with the filter text. The * default behavior checks that the label of the element is a match. * Subclasses should override this method. * * @param viewer the viewer that contains the element * @param element the tree element to check * @param ignorePattern if <code>true</code> ignore the text search pattern * and <code>false</code> otherwise. * @return true if the given element's label matches the filter text */ private boolean isLeafMatch(Viewer viewer, Object element, boolean ignorePattern) { /* Filter not Active */ if ((ignorePattern || fCachedPatternMatchingNews == null) && fType == Type.SHOW_ALL) return true; /* Element is a News */ if (element instanceof INews) { INews news = (INews) element; INews.State state = news.getState(); boolean isMatch = false; switch (fType) { /* Show: All */ case SHOW_ALL: isMatch = true; break; /* Show New News */ case SHOW_NEW: isMatch = (state == INews.State.NEW); break; /* Show Unread News */ case SHOW_UNREAD: isMatch = (state == INews.State.UNREAD || state == INews.State.NEW || state == INews.State.UPDATED); break; /* Show Sticky News */ case SHOW_STICKY: isMatch = news.isFlagged(); break; /* Show Labeled News */ case SHOW_LABELED: isMatch = !news.getLabels().isEmpty(); break; /* Show Recent News (max 48h old) */ case SHOW_RECENT: Date date = DateUtils.getRecentDate(news); isMatch = (date.getTime() >= (DateUtils.getToday().getTimeInMillis() - DateUtils.DAY)); break; /* Show Last 5 Days */ case SHOW_LAST_5_DAYS: date = DateUtils.getRecentDate(news); isMatch = (date.getTime() >= (DateUtils.getToday().getTimeInMillis() - 5 * DateUtils.DAY)); break; } /* Finally check the Pattern */ if (isMatch && !ignorePattern && fCachedPatternMatchingNews != null) isMatch = isTextPatternMatch(news.getId()); return isMatch; } return false; } /* * @see org.eclipse.jface.viewers.ViewerFilter#isFilterProperty(java.lang.Object, java.lang.String) */ @Override public boolean isFilterProperty(Object element, String property) { return false; // This is handled in needsRefresh() already } /** * Set the Type of this Filter. The Type is describing which elements are * filtered. * * @param type The Type of this Filter as described in the <code>Type</code> * enumeration. */ public void setType(Type type) { if (fType != type) fType = type; } /** * Get the Type of this Filter. The Type is describing which elements are * filtered. * * @return Returns the Type of this Filter as described in the * <code>Type</code> enumeration. */ Type getType() { return fType; } /** * Get the Target of the Search. The Target is describing which elements to * search when a Text-Search is performed. * * @return Returns the SearchTarget of the Search as described in the * <code>SearchTarget</code> enumeration. */ SearchTarget getSearchTarget() { return fSearchTarget; } /** * @return Returns the current set pattern string or <code>null</code> if * none. */ String getPatternString() { return fPatternString; } /** * Set the Target of the Search. The Target is describing which elements to * search when a Text-Search is performed. * * @param searchTarget The SearchTarget of the Search as described in the * <code>SearchTarget</code> enumeration. */ public void setSearchTarget(SearchTarget searchTarget) { SearchTarget oldTarget = fSearchTarget; fSearchTarget = searchTarget; /* Cause re-search if required */ if (oldTarget != fSearchTarget) setPattern(fPatternString); } /** * The pattern string for which this filter should select elements in the * viewer. * * @param patternString */ public void setPattern(String patternString) { fPatternString = patternString; /* Pattern Reset */ if (!StringUtils.isSet(patternString)) fCachedPatternMatchingNews = null; /* Pattern Set */ else { try { fCachedPatternMatchingNews = cacheMatchingNews(patternString.trim()); } /* This happens expectedly if max-clauses count reaches a certain limit */ catch (PersistenceException e) { Activator.getDefault().logError(e.getMessage(), e); } } } private Set<Long> cacheMatchingNews(String pattern) { List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(2); ISearchCondition locationCondition = null; ISearchCondition textCondition = null; /* Explicitly return on empty String */ if (!StringUtils.isSet(pattern)) return Collections.emptySet(); /* Convert to Wildcard Query */ if (StringUtils.supportsTrailingWildcards(pattern)) pattern = pattern + "*"; //$NON-NLS-1$ /* Match on Location (not supported for search marks and folder news marks) */ if (fNewsMark != null && !(fNewsMark instanceof ISearchMark) && !(fNewsMark instanceof FolderNewsMark)) { ISearchField field = fModelFactory.createSearchField(INews.LOCATION, INews.class.getName()); locationCondition = fModelFactory.createSearchCondition(field, SearchSpecifier.IS, ModelUtils.toPrimitive(Collections.singletonList((IFolderChild) fNewsMark))); conditions.add(locationCondition); } /* Match on Pattern */ ISearchField field = null; SearchSpecifier specifier = SearchSpecifier.CONTAINS_ALL; switch (fSearchTarget) { case ALL: field = fModelFactory.createSearchField(IEntity.ALL_FIELDS, INews.class.getName()); break; case ATTACHMENTS: field = fModelFactory.createSearchField(INews.ATTACHMENTS_CONTENT, INews.class.getName()); break; case AUTHOR: field = fModelFactory.createSearchField(INews.AUTHOR, INews.class.getName()); break; case CATEGORY: field = fModelFactory.createSearchField(INews.CATEGORIES, INews.class.getName()); specifier = SearchSpecifier.IS; break; case HEADLINE: field = fModelFactory.createSearchField(INews.TITLE, INews.class.getName()); break; case SOURCE: field = fModelFactory.createSearchField(INews.SOURCE, INews.class.getName()); specifier = SearchSpecifier.IS; break; case LABELS: field = fModelFactory.createSearchField(INews.LABEL, INews.class.getName()); specifier = SearchSpecifier.IS; break; } textCondition = fModelFactory.createSearchCondition(field, specifier, pattern); conditions.add(textCondition); /* Perform Search */ List<SearchHit<NewsReference>> result; if (fNewsMark != null && fNewsMark instanceof ISearchMark) { //Inject conditions into search conditions ISearchMark searchMark = (ISearchMark) fNewsMark; result = Owl.getPersistenceService().getModelSearch().searchNews(searchMark.getSearchConditions(), textCondition, searchMark.matchAllConditions()); } else { //Use the provided conditions as is result = Owl.getPersistenceService().getModelSearch().searchNews(conditions, true); } Set<Long> resultSet = new HashSet<Long>(result.size()); for (SearchHit<NewsReference> hit : result) { resultSet.add(hit.getResult().getId()); } return resultSet; } /** * @return <code>TRUE</code> in case a Pattern is set and <code>FALSE</code> * otherwise. */ boolean isPatternSet() { return fCachedPatternMatchingNews != null; } }