/*
* The MIT License
*
* Copyright (c) 2013-2014, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.widgets;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import hudson.model.Job;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.widgets.HistoryWidget;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* History page filter.
*
* @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
*/
public class HistoryPageFilter<T> {
private final int maxEntries;
private Long newerThan;
private Long olderThan;
private String searchString;
// Need to use different Lists for Queue.Items and Runs because
// we need access to them separately in the jelly files for rendering.
public final List<HistoryPageEntry<Queue.Item>> queueItems = new ArrayList<HistoryPageEntry<Queue.Item>>();
public final List<HistoryPageEntry<Run>> runs = new ArrayList<HistoryPageEntry<Run>>();
public boolean hasUpPage = false; // there are newer builds than on this page
public boolean hasDownPage = false; // there are older builds than on this page
public long nextBuildNumber;
public HistoryWidget widget;
public long newestOnPage = Long.MIN_VALUE; // see updateNewestOldest()
public long oldestOnPage = Long.MAX_VALUE; // see updateNewestOldest()
/**
* Create a history page filter instance.
*
* @param maxEntries The max number of entries allowed for the page.
*/
public HistoryPageFilter(int maxEntries) {
this.maxEntries = maxEntries;
}
/**
* Set the 'newerThan' queue ID.
* @param newerThan Queue IDs newer/greater than this queue ID take precedence on this page.
*/
public void setNewerThan(Long newerThan) {
if (olderThan != null) {
throw new UnsupportedOperationException("Cannot set 'newerThan'. 'olderThan' already set.");
}
this.newerThan = newerThan;
}
/**
* Set the 'olderThan' queue ID.
* @param olderThan Queue IDs older/less than this queue ID take precedence on this page.
*/
public void setOlderThan(Long olderThan) {
if (newerThan != null) {
throw new UnsupportedOperationException("Cannot set 'olderThan'. 'newerThan' already set.");
}
this.olderThan = olderThan;
}
/**
* Set the search string used to narrow the filtered set of builds.
* @param searchString The search string.
*/
public void setSearchString(@Nonnull String searchString) {
this.searchString = searchString;
}
/**
* Add build items to the History page.
*
* @param runItems The items to be added. Assumes the items are in descending queue ID order i.e. newest first.
* @deprecated Replaced by add(Iterable<T>) as of version 2.15
*/
@Deprecated
public void add(@Nonnull List<T> runItems) {
addInternal(runItems);
}
/**
* Add build items to the History page.
*
* @param runItems The items to be added. Assumes the items are in descending queue ID order i.e. newest first.
* @since TODO
*/
public void add(@Nonnull Iterable<T> runItems) {
addInternal(runItems);
}
/**
* Add run items and queued items to the History page.
*
* @param runItems The items to be added. Assumes the items are in descending queue ID order i.e. newest first.
* @param queueItems The queue items to be added. Queue items do not need to be sorted.
* @since TODO
*/
public void add(@Nonnull Iterable<T> runItems, @Nonnull List<Queue.Item> queueItems) {
sort(queueItems);
addInternal(Iterables.concat(queueItems, runItems));
}
/**
* Add items to the History page, internal implementation.
* @param items The items to be added.
* @param <ItemT> The type of items should either be T or Queue.Item.
*/
private <ItemT> void addInternal(@Nonnull Iterable<ItemT> items) {
// Note that items can be a large lazily evaluated collection,
// so this method is optimized to only iterate through it as much as needed.
if (!items.iterator().hasNext()) {
return;
}
nextBuildNumber = getNextBuildNumber(items.iterator().next());
if (newerThan == null && olderThan == null) {
// Just return the first page of entries (newest)
Iterator<ItemT> iter = items.iterator();
while (iter.hasNext()) {
add(iter.next());
if (isFull()) {
break;
}
}
hasDownPage = iter.hasNext();
} else if (newerThan != null) {
int toFillCount = getFillCount();
if (toFillCount > 0) {
// Walk through the items and keep track of the oldest
// 'toFillCount' items until we reach an item older than
// 'newerThan' or the end of the list.
LinkedList<ItemT> itemsToAdd = new LinkedList<>();
Iterator<ItemT> iter = items.iterator();
while (iter.hasNext()) {
ItemT item = iter.next();
if (HistoryPageEntry.getEntryId(item) > newerThan) {
itemsToAdd.addLast(item);
// Discard an item off the front of the list if we have
// to (which means we would be able to page up).
if (itemsToAdd.size() > toFillCount) {
itemsToAdd.removeFirst();
hasUpPage = true;
}
} else {
break;
}
}
if (itemsToAdd.size() == 0) {
// All builds are older than newerThan ?
hasDownPage = true;
} else {
// If there's less than a full page of items newer than
// 'newerThan', then it's ok to fill the page with older items.
if (itemsToAdd.size() < toFillCount) {
// We have to restart the iterator and skip the items that we added (because
// we may have popped an extra item off the iterator that did not get added).
Iterator<ItemT> skippedIter = items.iterator();
Iterators.skip(skippedIter, itemsToAdd.size());
for (int i = itemsToAdd.size(); i < toFillCount && skippedIter.hasNext(); i++) {
ItemT item = skippedIter.next();
itemsToAdd.addLast(item);
}
}
hasDownPage = iter.hasNext();
for (Object item : itemsToAdd) {
add(item);
}
}
}
} else if (olderThan != null) {
Iterator<ItemT> iter = items.iterator();
while (iter.hasNext()) {
Object item = iter.next();
if (HistoryPageEntry.getEntryId(item) >= olderThan) {
hasUpPage = true;
} else {
add(item);
if (isFull()) {
hasDownPage = iter.hasNext();
break;
}
}
}
}
}
public int size() {
return queueItems.size() + runs.size();
}
private void sort(List<? extends Object> items) {
// Queue items can start building out of order with how they got added to the queue. Sorting them
// before adding to the page. They'll still get displayed before the building items coz they end
// up in a different list in HistoryPageFilter.
Collections.sort(items, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
long o1QID = HistoryPageEntry.getEntryId(o1);
long o2QID = HistoryPageEntry.getEntryId(o2);
if (o1QID < o2QID) {
return 1;
} else if (o1QID == o2QID) {
return 0;
} else {
return -1;
}
}
});
}
private long getNextBuildNumber(@Nonnull Object entry) {
if (entry instanceof Queue.Item) {
Queue.Task task = ((Queue.Item) entry).task;
if (task instanceof Job) {
return ((Job) task).getNextBuildNumber();
}
} else if (entry instanceof Run) {
return ((Run) entry).getParent().getNextBuildNumber();
}
// TODO maybe this should be an error?
return HistoryPageEntry.getEntryId(entry) + 1;
}
private void addQueueItem(Queue.Item item) {
HistoryPageEntry<Queue.Item> entry = new HistoryPageEntry<>(item);
queueItems.add(entry);
updateNewestOldest(entry.getEntryId());
}
private void addRun(Run run) {
HistoryPageEntry<Run> entry = new HistoryPageEntry<>(run);
// Assert that runs have been added in descending order
if (runs.size() > 0) {
if (entry.getEntryId() > runs.get(runs.size() - 1).getEntryId()) {
throw new IllegalStateException("Runs were out of order");
}
}
runs.add(entry);
updateNewestOldest(entry.getEntryId());
}
private void updateNewestOldest(long entryId) {
newestOnPage = Math.max(newestOnPage, entryId);
oldestOnPage = Math.min(oldestOnPage, entryId);
}
private boolean add(Object entry) {
// Purposely not calling isFull(). May need to add a greater number of entries
// to the page initially, newerThan then cutting it back down to size using cutLeading()
if (entry instanceof Queue.Item) {
Queue.Item item = (Queue.Item) entry;
if (searchString != null && !fitsSearchParams(item)) {
return false;
}
addQueueItem(item);
return true;
} else if (entry instanceof Run) {
Run run = (Run) entry;
if (searchString != null && !fitsSearchParams(run)) {
return false;
}
addRun(run);
return true;
}
return false;
}
private boolean isFull() {
return (size() >= maxEntries);
}
/**
* Get the number of items required to fill the page.
*
* @return The number of items required to fill the page.
*/
private int getFillCount() {
return Math.max(0, (maxEntries - size()));
}
private boolean fitsSearchParams(@Nonnull Queue.Item item) {
if (fitsSearchString(item.getDisplayName())) {
return true;
} else if (fitsSearchString(item.getId())) {
return true;
}
// Non of the fuzzy matches "liked" the search term.
return false;
}
private boolean fitsSearchParams(@Nonnull Run run) {
if (searchString == null) {
return true;
}
if (fitsSearchString(run.getDisplayName())) {
return true;
} else if (fitsSearchString(run.getDescription())) {
return true;
} else if (fitsSearchString(run.getNumber())) {
return true;
} else if (fitsSearchString(run.getQueueId())) {
return true;
} else if (fitsSearchString(run.getResult())) {
return true;
}
// Non of the fuzzy matches "liked" the search term.
return false;
}
private boolean fitsSearchString(Object data) {
if (searchString == null) {
return true;
}
if (data != null) {
if (data instanceof Number) {
return data.toString().equals(searchString);
} else {
return data.toString().toLowerCase().contains(searchString);
}
}
return false;
}
}