/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.search.ui.text;
import com.google.dart.tools.search.ui.ISearchResult;
import com.google.dart.tools.search.ui.ISearchResultListener;
import com.google.dart.tools.search.ui.SearchResultEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* An abstract base implementation for text-match based search results. This search result
* implementation consists of a list of {@link com.google.dart.tools.search.ui.text.Match matches}.
* No assumptions are made about the kind of elements these matches are reported against.
*/
public abstract class AbstractTextSearchResult implements ISearchResult {
protected static final Match[] EMPTY_ARRAY = new Match[0];
private static int compare(Match match1, Match match2) {
int diff = match2.getOffset() - match1.getOffset();
if (diff != 0) {
return diff;
}
return match2.getLength() - match1.getLength();
}
private static int getInsertIndex(List<Match> matches, Match match) {
int count = matches.size();
int min = 0, max = count - 1;
while (min <= max) {
int mid = (min + max) / 2;
Match data = matches.get(mid);
int compare = compare(match, data);
if (compare > 0) {
max = mid - 1;
} else {
min = mid + 1;
}
}
return min;
}
private static void insertSorted(List<Match> matches, Match match) {
int insertIndex = getInsertIndex(matches, match);
matches.add(insertIndex, match);
}
protected final Map<Object, List<Match>> fElementsToMatches;
private final List<ISearchResultListener> fListeners;
private final MatchEvent fMatchEvent;
private MatchFilter[] fMatchFilters;
/**
* Constructs a new <code>AbstractTextSearchResult</code>
*/
protected AbstractTextSearchResult() {
fElementsToMatches = new HashMap<Object, List<Match>>();
fListeners = new ArrayList<ISearchResultListener>();
fMatchEvent = new MatchEvent(this);
fMatchFilters = null; // filtering disabled by default
}
@Override
public void addListener(ISearchResultListener l) {
synchronized (fListeners) {
fListeners.add(l);
}
}
/**
* Adds a <code>Match</code> to this search result. This method does nothing if the match is
* already present.
* <p>
* Subclasses may extend this method.
* </p>
*
* @param match the match to add
*/
public void addMatch(Match match) {
boolean hasAdded = false;
synchronized (fElementsToMatches) {
hasAdded = doAddMatch(match);
}
if (hasAdded) {
fireChange(getSearchResultEvent(match, MatchEvent.ADDED));
}
}
/**
* Adds a number of Matches to this search result. This method does nothing for matches that are
* already present.
* <p>
* Subclasses may extend this method.
* </p>
*
* @param matches the matches to add
*/
public void addMatches(Match[] matches) {
Collection<Match> reallyAdded = new ArrayList<Match>();
synchronized (fElementsToMatches) {
for (int i = 0; i < matches.length; i++) {
if (doAddMatch(matches[i])) {
reallyAdded.add(matches[i]);
}
}
}
if (!reallyAdded.isEmpty()) {
fireChange(getSearchResultEvent(reallyAdded, MatchEvent.ADDED));
}
}
/**
* Returns the active match filters for this result. If not null is returned, the match filters
* will be used to update the filter state ({@link Match#isFiltered()} of matches and the
* {@link AbstractTextSearchViewPage} will only show non-filtered matches. If <code>null</code> is
* set the filter state of the match is ignored by the {@link AbstractTextSearchViewPage} and all
* matches are shown.
*
* @return the match filters to be used or <code>null</code> if the filter state of the match
* should be ignored.
*/
public MatchFilter[] getActiveMatchFilters() {
return fMatchFilters;
}
/**
* Returns all applicable filters for this result or null if match filters are not supported. If
* match filters are returned, the {@link AbstractTextSearchViewPage} will contain menu entries in
* the view menu.
*
* @return all applicable filters for this result.
*/
public MatchFilter[] getAllMatchFilters() {
return null;
}
/**
* Returns an implementation of <code>IEditorMatchAdapter</code> appropriate for this search
* result.
*
* @return an appropriate adapter or <code>null</code> if none has been implemented
* @see IEditorMatchAdapter
*/
public abstract IEditorMatchAdapter getEditorMatchAdapter();
/**
* Returns an array containing the set of all elements that matches are reported against in this
* search result. Note that all elements that contain matches are returned. The filter state of
* the matches is not relevant.
*
* @return the set of elements in this search result
*/
public Object[] getElements() {
synchronized (fElementsToMatches) {
return fElementsToMatches.keySet().toArray();
}
}
/**
* Returns an implementation of <code>IFileMatchAdapter</code> appropriate for this search result.
*
* @return an appropriate adapter or <code>null</code> if none has been implemented
* @see IFileMatchAdapter
*/
public abstract IFileMatchAdapter getFileMatchAdapter();
/**
* Returns the total number of matches contained in this search result. The filter state of the
* matches is not relevant when counting matches. All matches are counted.
*
* @return total number of matches
*/
public int getMatchCount() {
int count = 0;
synchronized (fElementsToMatches) {
for (Iterator<List<Match>> elements = fElementsToMatches.values().iterator(); elements.hasNext();) {
List<Match> element = elements.next();
if (element != null) {
count += element.size();
}
}
}
return count;
}
/**
* Returns the number of matches reported against a given element. This is equivalent to calling
* <code>getMatches(element).length</code> The filter state of the matches is not relevant when
* counting matches. All matches are counted.
*
* @param element the element to get the match count for
* @return the number of matches reported against the element
*/
public int getMatchCount(Object element) {
List<Match> matches = fElementsToMatches.get(element);
if (matches != null) {
return matches.size();
}
return 0;
}
/**
* Returns an array with all matches reported against the given element. Note that all matches of
* the given element are returned. The filter state of the matches is not relevant.
*
* @param element the element to report matches for
* @return all matches reported for this element
* @see Match#getElement()
*/
public Match[] getMatches(Object element) {
synchronized (fElementsToMatches) {
List<Match> matches = fElementsToMatches.get(element);
if (matches != null) {
return matches.toArray(new Match[matches.size()]);
}
return EMPTY_ARRAY;
}
}
/**
* Removes all matches from this search result.
* <p>
* Subclasses may extend this method.
* </p>
*/
public void removeAll() {
synchronized (fElementsToMatches) {
doRemoveAll();
}
fireChange(new RemoveAllEvent(this));
}
@Override
public void removeListener(ISearchResultListener l) {
synchronized (fListeners) {
fListeners.remove(l);
}
}
/**
* Removes the given match from this search result. This method has no effect if the match is not
* found.
* <p>
* Subclasses may extend this method.
* </p>
*
* @param match the match to remove
*/
public void removeMatch(Match match) {
boolean existed = false;
synchronized (fElementsToMatches) {
existed = doRemoveMatch(match);
}
if (existed) {
fireChange(getSearchResultEvent(match, MatchEvent.REMOVED));
}
}
/**
* Removes the given matches from this search result. This method has no effect for matches that
* are not found
* <p>
* Subclasses may extend this method.
* </p>
*
* @param matches the matches to remove
*/
public void removeMatches(Match[] matches) {
Collection<Match> existing = new ArrayList<Match>();
synchronized (fElementsToMatches) {
for (int i = 0; i < matches.length; i++) {
if (doRemoveMatch(matches[i])) {
existing.add(matches[i]); // no duplicate matches at this point
}
}
}
if (!existing.isEmpty()) {
fireChange(getSearchResultEvent(existing, MatchEvent.REMOVED));
}
}
/**
* Sets the active match filters for this result. If set to non-null, the match filters will be
* used to update the filter state ({@link Match#isFiltered()} of matches and the
* {@link AbstractTextSearchViewPage} will only show non-filtered matches. If <code>null</code> is
* set the filter state of the match is ignored by the {@link AbstractTextSearchViewPage} and all
* matches are shown. Note the model contains all matches, regardless if the filter state of a
* match.
*
* @param filters the match filters to set or <code>null</code> if the filter state of the match
* should be ignored.
*/
public void setActiveMatchFilters(MatchFilter[] filters) {
fMatchFilters = filters;
updateFilterStateForAllMatches();
}
/**
* Send the given <code>SearchResultEvent</code> to all registered search result listeners.
*
* @param e the event to be sent
* @see ISearchResultListener
*/
protected void fireChange(SearchResultEvent e) {
HashSet<ISearchResultListener> copiedListeners = new HashSet<ISearchResultListener>();
synchronized (fListeners) {
copiedListeners.addAll(fListeners);
}
Iterator<ISearchResultListener> listeners = copiedListeners.iterator();
while (listeners.hasNext()) {
listeners.next().searchResultChanged(e);
}
}
private boolean doAddMatch(Match match) {
updateFilterState(match);
List<Match> matches = fElementsToMatches.get(match.getElement());
if (matches == null) {
matches = new ArrayList<Match>();
fElementsToMatches.put(match.getElement(), matches);
matches.add(match);
return true;
}
if (!matches.contains(match)) {
insertSorted(matches, match);
return true;
}
return false;
}
private void doRemoveAll() {
fElementsToMatches.clear();
}
private boolean doRemoveMatch(Match match) {
boolean existed = false;
List<Match> matches = fElementsToMatches.get(match.getElement());
if (matches != null) {
existed = matches.remove(match);
if (matches.isEmpty()) {
fElementsToMatches.remove(match.getElement());
}
}
return existed;
}
private MatchEvent getSearchResultEvent(Collection<Match> matches, int eventKind) {
fMatchEvent.setKind(eventKind);
Match[] matchArray = matches.toArray(new Match[matches.size()]);
fMatchEvent.setMatches(matchArray);
return fMatchEvent;
}
private MatchEvent getSearchResultEvent(Match match, int eventKind) {
fMatchEvent.setKind(eventKind);
fMatchEvent.setMatch(match);
return fMatchEvent;
}
/*
* Evaluates the filter for the match and updates it. Return true if the filter changed.
*/
private boolean updateFilterState(Match match) {
MatchFilter[] matchFilters = getActiveMatchFilters();
if (matchFilters == null) {
return false; // do nothing, no change
}
boolean oldState = match.isFiltered();
for (int i = 0; i < matchFilters.length; i++) {
if (matchFilters[i].filters(match)) {
match.setFiltered(true);
return !oldState;
}
}
match.setFiltered(false);
return oldState;
}
private void updateFilterStateForAllMatches() {
boolean disableFiltering = getActiveMatchFilters() == null;
ArrayList<Match> changed = new ArrayList<Match>();
Object[] elements = getElements();
for (int i = 0; i < elements.length; i++) {
Match[] matches = getMatches(elements[i]);
for (int k = 0; k < matches.length; k++) {
if (disableFiltering || updateFilterState(matches[k])) {
changed.add(matches[k]);
}
}
}
Match[] allChanges = changed.toArray(new Match[changed.size()]);
fireChange(new FilterUpdateEvent(this, allChanges, getActiveMatchFilters()));
}
}