{
+
+ /**
+ * Does the supplied {@link Queue.Item} fit the {@link BuildSearchParams} used to create this
+ *
+ *
+ * Implementations should call {@link #fitsSearchParams(Object)} after extracting the appropriate data from
+ * the {@link Queue.Item}.
+ *
+ * {@link BuildSearchParamProcessor} instance.
+ * @param item The {@link Queue.Item} to test.
+ * @return {@code true} if the {@link Queue.Item} fits, otherwise {@code false}.
+ */
+ public abstract boolean fitsSearchParams(Queue.Item item);
+
+ /**
+ * Does the supplied {@link Run} fit the {@link BuildSearchParams} used to create this
+ *
+ *
+ * Implementations should call {@link #fitsSearchParams(Object)} after extracting the appropriate data from
+ * the {@link Run}.
+ *
+ * {@link BuildSearchParamProcessor} instance.
+ * @param run The {@link Run} to test.
+ * @return {@code true} if the {@link Run} fits, otherwise {@code false}.
+ */
+ public abstract boolean fitsSearchParams(Run run);
+
+ /**
+ * Does the supplied "data" (extracted from a {@link Queue.Item} or {@link Run}) fit the {@link BuildSearchParams}
+ * used to create this {@link BuildSearchParamProcessor} instance.
+ *
+ *
+ * This method implementation should do all of the work in terms of testing the extracted {@link Queue.Item}
+ * or {@link Run} data against the search criteria used to create the instance. The
+ * {@link #fitsSearchParams(hudson.model.Queue.Item)} and {@link #fitsSearchParams(hudson.model.Run)}
+ * implementations should call this function after extracting the appropriate data from the {@link Queue.Item}
+ * or {@link Run}.
+ *
+ *
+ * This method makes unit testing of the {@link BuildSearchParamProcessor} implementation a little easier in
+ * that it allows the implementation to be hidden in the parent {@link BuildSearchParamProcessorFactory factory}
+ * class, plus allows testing without the need to create {@link Queue.Item} or {@link Run} instances, which can't
+ * be created without spinning a {@link jenkins.model.Jenkins} instance via a JenkinsRule (which in turn requires
+ * test classes to be in the test harness etc).
+ *
+ *
+ * @param data The data to test.
+ * @return {@code true} if the {@link Run} fits, otherwise {@code false}.
+ */
+ public abstract boolean fitsSearchParams(T data);
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorFactory.java
new file mode 100644
index 0000000000..6d8e2d8cd1
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorFactory.java
@@ -0,0 +1,109 @@
+/*
+ * 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 hudson.widgets.buildsearch;
+
+import hudson.ExtensionList;
+import hudson.ExtensionPoint;
+import hudson.widgets.buildsearch.processors.DateProcessorFactory;
+import hudson.widgets.buildsearch.processors.DescriptionProcessorFactory;
+import hudson.widgets.buildsearch.processors.NameProcessorFactory;
+import hudson.widgets.buildsearch.processors.ResultProcessorFactory;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementations of this class create {@link BuildSearchParamProcessor} instances from a set
+ * of {@link BuildSearchParams}. The set of {@link BuildSearchParamProcessor} instances are then used
+ * by the {@link hudson.widgets.HistoryPageFilter} class to filter the build history (via an instance of
+ * {@link BuildSearchParamProcessorList}).
+ *
+ *
+ * Each {@link BuildSearchParamProcessor} implementation processes one or more search terms e.g. the
+ * {@link BuildSearchParamProcessor} instance created by the {@link NameProcessorFactory} checks the
+ * build {@link hudson.model.Queue.Item} or {@link hudson.model.Run} name using the "name:" search token(s)
+ * provided in the search, while the {@link DateProcessorFactory} checks date using one or both of the
+ * "date-from:" and "date-to:" tokens.
+ *
+ * @author tom.fennelly@gmail.com
+ */
+public abstract class BuildSearchParamProcessorFactory implements ExtensionPoint {
+
+ private static List coreParamProcessorFactories = new ArrayList();
+
+ static {
+ // Statically adding the core BuildSearchParamProcessorFactory impls Vs adding via @Extension.
+ // Makes testing easier coz no need to create a JenkinsRule instance + put in test harness etc.
+ // Plugins etc can still contribute via @Extension.
+ coreParamProcessorFactories.add(new NameProcessorFactory());
+ coreParamProcessorFactories.add(new DescriptionProcessorFactory());
+ coreParamProcessorFactories.add(new ResultProcessorFactory());
+ coreParamProcessorFactories.add(new DateProcessorFactory());
+ }
+
+ /**
+ * Get the list of search terms that this processor is interested in.
+ *
+ * @return List of search terms.
+ */
+ public abstract String[] getSearchTerms();
+
+ /**
+ * Create the {@link BuildSearchParamProcessor} instance using the supplied set of search parameters.
+ *
+ * @param searchParams The parsed search parameters.
+ * @return The {@link BuildSearchParamProcessor} instance to be used for searching, or {@code null} if the search
+ * parameters are such that {@link BuildSearchParamProcessor} instance of this type is not needed.
+ */
+ public abstract @CheckForNull BuildSearchParamProcessor createProcessor(@Nonnull BuildSearchParams searchParams);
+
+ public static List all() {
+
+ // TODO: Not convinced we want to bother with @Extension
+
+ ExtensionList extensions = ExtensionList.lookup(BuildSearchParamProcessorFactory.class);
+ if (extensions.isEmpty()) {
+ // No BuildSearchParamProcessorFactory defined by @Extension
+ return coreParamProcessorFactories;
+ } else {
+ // One or more BuildSearchParamProcessorFactory defined by @Extension.
+ List combined = new ArrayList(coreParamProcessorFactories);
+ combined.addAll(extensions);
+ return combined;
+ }
+ }
+
+ public static Set getAllSearchTerms() {
+ Set allSearchTerms = new LinkedHashSet();
+ for (BuildSearchParamProcessorFactory paramProcessorFactory : all()) {
+ allSearchTerms.addAll(Arrays.asList(paramProcessorFactory.getSearchTerms()));
+ }
+ return allSearchTerms;
+ }
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorList.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorList.java
new file mode 100644
index 0000000000..2c0dee50cf
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParamProcessorList.java
@@ -0,0 +1,91 @@
+/*
+ * 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 hudson.widgets.buildsearch;
+
+import hudson.model.Queue;
+import hudson.model.Run;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+
+/**
+ * Wrapper class for the list of {@link hudson.widgets.buildsearch.BuildSearchParamProcessor} needed to process/apply
+ * a set of {@link hudson.widgets.buildsearch.BuildSearchParams}.
+ *
+ * @author tom.fennelly@gmail.com
+ */
+public class BuildSearchParamProcessorList {
+
+ private final ArrayList processors;
+
+ /**
+ * Create a {@link BuildSearchParamProcessorList} instance from a set of {@link hudson.widgets.buildsearch.BuildSearchParams}.
+ * @param searchParams The search parameters to use for creating the {@link BuildSearchParamProcessorList}.
+ */
+ public BuildSearchParamProcessorList(@Nonnull BuildSearchParams searchParams) {
+ processors = new ArrayList();
+ for (BuildSearchParamProcessorFactory factory : BuildSearchParamProcessorFactory.all()) {
+ BuildSearchParamProcessor processor = factory.createProcessor(searchParams);
+ if (processor != null) {
+ processors.add(processor);
+ }
+ }
+ }
+
+ public ArrayList getProcessors() {
+ return processors;
+ }
+
+ /**
+ * Does the supplied {@link hudson.model.Queue.Item} fit the {@link BuildSearchParams} used to create this
+ * {@link BuildSearchParamProcessorList} instance.
+ * @param item The {@link hudson.model.Queue.Item} to test.
+ * @return {@code true} if the {@link hudson.model.Queue.Item} fits all of the {@link BuildSearchParamProcessor}s
+ * on this {@link BuildSearchParamProcessorList}, otherwise {@code false}.
+ */
+ public boolean fitsSearchParams(@Nonnull Queue.Item item) {
+ for (BuildSearchParamProcessor processor : processors) {
+ if (!processor.fitsSearchParams(item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Does the supplied {@link hudson.model.Run} fit the {@link BuildSearchParams} used to create this
+ * {@link BuildSearchParamProcessorList} instance.
+ * @param run The {@link hudson.model.Run} to test.
+ * @return {@code true} if the {@link hudson.model.Run} fits all of the {@link BuildSearchParamProcessor}s
+ * on this {@link BuildSearchParamProcessorList}, otherwise {@code false}.
+ */
+ public boolean fitsSearchParams(@Nonnull Run run) {
+ for (BuildSearchParamProcessor processor : processors) {
+ if (!processor.fitsSearchParams(run)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParams.java b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParams.java
new file mode 100644
index 0000000000..277bd43b29
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/BuildSearchParams.java
@@ -0,0 +1,174 @@
+/*
+ * 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 hudson.widgets.buildsearch;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author tom.fennelly@gmail.com
+ */
+public class BuildSearchParams {
+
+ private final String searchString;
+ private final Map> searchParamsSets = newParamMap();
+ private boolean parsingComplete = false;
+
+ private static Map> newParamMap() {
+ Map> map = new LinkedHashMap>();
+ for (String searchTerm: BuildSearchParamProcessorFactory.getAllSearchTerms()) {
+ map.put(searchTerm, new ArrayList());
+ }
+ return Collections.unmodifiableMap(map);
+ }
+
+ public class BuildSearchParam {
+
+ public String searchTerm;
+ public String searchTermToken;
+
+ private String param;
+ // The start index (in this.searchString) of the search parameter (including the searchTermToken itself).
+ int startIndex;
+ // The end index (in this.searchString) of the search parameter, -1 being the end of the string.
+ int endIndex = -1;
+
+ BuildSearchParam(String searchTerm, String searchTermToken, int startIndex) {
+ this.searchTerm = searchTerm;
+ this.searchTermToken = searchTermToken;
+ this.startIndex = startIndex;
+ }
+
+ public String get() {
+ if (this.param != null) {
+ return this.param;
+ }
+
+ String param = searchString.substring(getEndOfTokenIndex(), getParamEndIndex());
+ param = param.trim();
+
+ if (parsingComplete) {
+ // We can cache the value if parsing is complete.
+ this.param = param;
+ }
+
+ return param;
+ }
+
+ private boolean isWhitespace() {
+ int paramStartIndex = getEndOfTokenIndex();
+ int paramEndIndex = getParamEndIndex();
+ for (int i = paramStartIndex; i < paramEndIndex; i++) {
+ if (!Character.isWhitespace(searchString.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private int getEndOfTokenIndex() {
+ return (startIndex + searchTermToken.length());
+ }
+
+ private int getParamEndIndex() {
+ return (endIndex != -1 ? endIndex : searchString.length());
+ }
+
+ @Override
+ public String toString() {
+ return searchTerm + ":'" + get() + "'";
+ }
+ }
+
+ public BuildSearchParams(@Nonnull String searchString) {
+ this.searchString = searchString;
+ if (searchString.trim().length() > 0) {
+ parseSearchString();
+ }
+ }
+
+ public List getParams(@Nonnull String searchTerm) {
+ List params = searchParamsSets.get(searchTerm);
+ if (params == null) {
+ throw new IllegalArgumentException(String.format("Unknown search term '%s'.", searchTerm));
+ }
+ return params;
+ }
+
+ @Override
+ public String toString() {
+ return searchParamsSets.toString();
+ }
+
+ private void parseSearchString() {
+ // Step 1: find all search params in the searchString, recording their startIndex
+ List allSearchParams = new ArrayList();
+ String searchStringLower = searchString.toLowerCase();
+ for (String searchTerm : BuildSearchParamProcessorFactory.getAllSearchTerms()) {
+ String searchTermToken = searchTerm + ":";
+
+ int startIndex = 0;
+ while((startIndex = searchStringLower.indexOf(searchTermToken, startIndex)) != -1) {
+ allSearchParams.add(new BuildSearchParam(searchTerm, searchTermToken, startIndex));
+ startIndex += searchTerm.length();
+ }
+ }
+
+ // Step 2: Remove all empty/whitespace params i.e. token defined but no param
+
+ // Step 2: sort all search params by their startIndex. This will then make it easy to find the endIndexes (step 3)
+ Collections.sort(allSearchParams, new Comparator() {
+ @Override
+ public int compare(BuildSearchParam param1, BuildSearchParam param2) {
+ return param1.startIndex - param2.startIndex;
+ }
+ });
+
+ // Step 3: find the endIndex for each search param.
+ // That's easy: it's the startIndex of the next search param, or the end of the searchString for the last.
+ int numParams = allSearchParams.size();
+ for (int i = 0; i < numParams; i++) {
+ BuildSearchParam searchParam = allSearchParams.get(i);
+
+ if (i == numParams - 1) {
+ // last one, so leave endIndex as -1
+ } else {
+ BuildSearchParam nextSearchParam = allSearchParams.get(i + 1);
+ searchParam.endIndex = nextSearchParam.startIndex;
+ }
+
+ // And add it to the top level searchParamsSets Map, but only if it's not whitespace.
+ if (!searchParam.isWhitespace()) {
+ searchParamsSets.get(searchParam.searchTerm).add(searchParam);
+ }
+ }
+
+ parsingComplete = true;
+ }
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/README.md b/core/src/main/java/hudson/widgets/buildsearch/README.md
new file mode 100644
index 0000000000..f539a9fda3
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/README.md
@@ -0,0 +1,34 @@
+__TODO__: Move to jenkins-ci web/wiki if changes accepted.
+
+## Search Terms
+
+* [__name:__] : Is contained in the name of the build.
+* [__desc:__] : Is contained in the description of the build.d.
+* [__result:__] : Is the "result" of the build (SUCCESS, UNSTABLE, FAILURE, NOT_BUILT, ABORTED).
+* [__date-from:__] and/or [__date-to:__] : Build date is before, after or between. Date format is *yyyy-MM-yy*. Date is start of day i.e. 00:00. Also supports some shorthands e.g. "today", "yesterday", "1 day", "2 days", "2 weeks", "2 months".
+
+Different search terms are __AND__ together to result in a matching Build, while multiples of the same search term are __OR__'d together e.g.
+
+* result: FAILURE desc: staging desc: production
+
+Is interpreted as:
+
+* (result: FAILURE) AND (desc: staging OR desc: production)
+
+## Examples
+
+Builds that contain the words "staging" or "production" in the description:
+
+* desc: staging desc: production
+
+Failed builds in last week (7 days):
+
+* result: FAILURE date-from: 1 week
+
+Failed builds since a specific date:
+
+* result: FAILURE date-from: 2015-03-31
+
+Unstable builds in March:
+
+* result: UNSTABLE date-from: 2015-03-01 date-to: 2015-04-01
diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/DateProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/DateProcessorFactory.java
new file mode 100644
index 0000000000..95a5679298
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/processors/DateProcessorFactory.java
@@ -0,0 +1,212 @@
+/*
+ * 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 hudson.widgets.buildsearch.processors;
+
+import hudson.model.Queue;
+import hudson.model.Run;
+import hudson.widgets.buildsearch.BuildSearchParamProcessor;
+import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory;
+import hudson.widgets.buildsearch.BuildSearchParams;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Search build history by {@link hudson.model.Queue.Item#getInQueueSince()}
+ * or {@link hudson.model.Run#getTimeInMillis()} using one or both of the
+ * "date-from:" and "date-to:" tokens. Also supports some shorthands such as "today"
+ *
+ * @author tom.fennelly@gmail.com
+ */
+public class DateProcessorFactory extends BuildSearchParamProcessorFactory {
+
+ public static String LONG_DATE_FORMAT = "yyyy-MM-dd";
+ public static String MEDIUM_DATE_FORMAT = "yy-MM-dd";
+ public static String SHORT_DATE_FORMAT = "MM-dd";
+
+ public static final long DAY = 1000 * 60 * 60 * 24;
+ public static final long WEEK = DAY * 7;
+ public static final long MONTH = WEEK * 4;
+
+ private static final String[] SEARCH_TERMS = new String[] {"date-from", "date-to"};
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getSearchTerms() {
+ return SEARCH_TERMS;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) {
+ final DateFromTo dateFromTo = new DateFromTo(searchParams);
+
+ if (dateFromTo.dateFrom == null && dateFromTo.dateTo == null) {
+ return null;
+ }
+
+ return new BuildSearchParamProcessor() {
+
+ @Override
+ public boolean fitsSearchParams(Long timeInMillis) {
+ if (dateFromTo.dateFrom != null && timeInMillis < dateFromTo.dateFrom) {
+ return false;
+ }
+ if (dateFromTo.dateTo != null && timeInMillis > dateFromTo.dateTo) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean fitsSearchParams(Queue.Item item) {
+ return fitsSearchParams(item.getInQueueSince());
+ }
+ @Override
+ public boolean fitsSearchParams(Run run) {
+ return fitsSearchParams(run.getTimeInMillis());
+ }
+ };
+ }
+
+ private class DateFromTo {
+
+ private Long dateFrom;
+ private Long dateTo;
+
+ private DateFromTo(BuildSearchParams searchParams) {
+ final List dateFromParams = searchParams.getParams(SEARCH_TERMS[0]);
+ final List dateToParams = searchParams.getParams(SEARCH_TERMS[1]);
+
+ if (dateFromParams.isEmpty() && dateToParams.isEmpty()) {
+ // none of the date search terms are specified
+ return;
+ }
+
+ // Only supports spec of a single pair of "date-from:" and "date-to:" search params.
+ if (!dateFromParams.isEmpty()) {
+ dateFrom = toTimeInMillis(dateFromParams.get(0).get());
+ } else {
+ dateFrom = null;
+ }
+
+ if (!dateToParams.isEmpty()) {
+ dateTo = toTimeInMillis(dateToParams.get(0).get());
+ } else {
+ dateTo = null;
+ }
+
+ if (dateFrom != null && dateTo != null && dateFrom > dateTo) {
+ dateTo = null;
+ }
+ }
+ }
+
+ // make this available to test code too
+ public static Long toTimeInMillis(String date) {
+ date = date.toLowerCase();
+
+ if (date.equals("today")) {
+ return getToday();
+ } else if (date.equals("yesterday")) {
+ return getYesterday();
+ } else if (date.endsWith("day") || date.endsWith("days")) {
+ return getToday() - (extractMultiple(date) * DAY);
+ } else if (date.endsWith("week") || date.endsWith("weeks")) {
+ return getToday() - (extractMultiple(date) * WEEK);
+ } else if (date.endsWith("month") || date.endsWith("months")) {
+ return getToday() - (extractMultiple(date) * MONTH);
+ }
+
+ try {
+ // SimpleDateFormat is not thread safe. What a PITA !!!
+ date = normalize(date);
+ if (date.length() == LONG_DATE_FORMAT.length()) {
+ return new SimpleDateFormat(LONG_DATE_FORMAT).parse(date).getTime();
+ } else if (date.length() == MEDIUM_DATE_FORMAT.length()) {
+ return new SimpleDateFormat(MEDIUM_DATE_FORMAT).parse(date).getTime();
+ } else {
+ return null;
+ }
+ } catch (ParseException e) {
+ return null;
+ }
+ }
+
+ private static long extractMultiple(String date) {
+ if (Character.isDigit(date.charAt(0))) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < date.length(); i++) {
+ char c = date.charAt(i);
+ if (!Character.isDigit(c)) {
+ break;
+ }
+ builder.append(c);
+ }
+ return new Long(builder.toString());
+ }
+ return 1;
+ }
+
+ // make this available to test code too
+ public static String normalize(String date) {
+ // In case the user forgot the format and used '/' instead of '-' as the delimiter.
+ date = date.replace('/', '-');
+ // In case there are spaces
+ date = date.replace(" ", "");
+ date = date.toLowerCase();
+
+ if (date.length() == SHORT_DATE_FORMAT.length()) {
+ date = getCurrentYearString() + "-" + date;
+ }
+
+ return date;
+ }
+
+ public static String getCurrentYearString() {
+ return new SimpleDateFormat("yyyy").format(new Date());
+ }
+
+ public static long getToday() {
+ long now = System.currentTimeMillis();
+ long partOfToday = now % DAY;
+ long startOfToday = now - partOfToday;
+
+ return startOfToday;
+ }
+
+ public static long getTomorrow() {
+ return getToday() + DAY;
+ }
+
+ public static long getYesterday() {
+ return getToday() - DAY;
+ }
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/DescriptionProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/DescriptionProcessorFactory.java
new file mode 100644
index 0000000000..60b917df16
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/processors/DescriptionProcessorFactory.java
@@ -0,0 +1,87 @@
+/*
+ * 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 hudson.widgets.buildsearch.processors;
+
+import hudson.model.Queue;
+import hudson.model.Run;
+import hudson.widgets.buildsearch.BuildSearchParamProcessor;
+import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory;
+import hudson.widgets.buildsearch.BuildSearchParams;
+
+import java.util.List;
+
+/**
+ * Search build history by {@link hudson.model.Run} description.
+ *
+ * @author tom.fennelly@gmail.com
+ */
+public class DescriptionProcessorFactory extends BuildSearchParamProcessorFactory {
+
+ private static final String[] SEARCH_TERMS = new String[] {"desc"}; // Build description
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getSearchTerms() {
+ return SEARCH_TERMS;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) {
+ final List descParams = searchParams.getParams(SEARCH_TERMS[0]);
+
+ if (descParams.isEmpty()) {
+ // "desc" search term not specified in search
+ return null;
+ }
+
+ return new BuildSearchParamProcessor() {
+
+ @Override
+ public boolean fitsSearchParams(String description) {
+ // It fits if it contains any of the specified "desc" search terms.
+ for (BuildSearchParams.BuildSearchParam nameParam : descParams) {
+ if (description.contains(nameParam.get())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean fitsSearchParams(Queue.Item item) {
+ // Queue items don't have a description.
+ return false;
+ }
+ @Override
+ public boolean fitsSearchParams(Run run) {
+ return fitsSearchParams(run.getDescription());
+ }
+ };
+ }
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/NameProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/NameProcessorFactory.java
new file mode 100644
index 0000000000..fff9f24747
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/processors/NameProcessorFactory.java
@@ -0,0 +1,86 @@
+/*
+ * 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 hudson.widgets.buildsearch.processors;
+
+import hudson.model.Queue;
+import hudson.model.Run;
+import hudson.widgets.buildsearch.BuildSearchParamProcessor;
+import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory;
+import hudson.widgets.buildsearch.BuildSearchParams;
+
+import java.util.List;
+
+/**
+ * Search build history by {@link hudson.model.Queue.Item} or {@link hudson.model.Run} name.
+ *
+ * @author tom.fennelly@gmail.com
+ */
+public class NameProcessorFactory extends BuildSearchParamProcessorFactory {
+
+ private static final String[] SEARCH_TERMS = new String[] {"name"}; // Build name
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getSearchTerms() {
+ return SEARCH_TERMS;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) {
+ final List nameParams = searchParams.getParams(SEARCH_TERMS[0]);
+
+ if (nameParams.isEmpty()) {
+ // "name" search term not specified in search
+ return null;
+ }
+
+ return new BuildSearchParamProcessor() {
+
+ @Override
+ public boolean fitsSearchParams(String name) {
+ // It fits if it contains any of the specified "name" search terms.
+ for (BuildSearchParams.BuildSearchParam nameParam : nameParams) {
+ if (name.contains(nameParam.get())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean fitsSearchParams(Queue.Item item) {
+ return fitsSearchParams(item.getDisplayName());
+ }
+ @Override
+ public boolean fitsSearchParams(Run run) {
+ return fitsSearchParams(run.getDisplayName());
+ }
+ };
+ }
+}
diff --git a/core/src/main/java/hudson/widgets/buildsearch/processors/ResultProcessorFactory.java b/core/src/main/java/hudson/widgets/buildsearch/processors/ResultProcessorFactory.java
new file mode 100644
index 0000000000..e7e69a912b
--- /dev/null
+++ b/core/src/main/java/hudson/widgets/buildsearch/processors/ResultProcessorFactory.java
@@ -0,0 +1,93 @@
+/*
+ * 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 hudson.widgets.buildsearch.processors;
+
+import hudson.model.Queue;
+import hudson.model.Result;
+import hudson.model.Run;
+import hudson.widgets.buildsearch.BuildSearchParamProcessor;
+import hudson.widgets.buildsearch.BuildSearchParamProcessorFactory;
+import hudson.widgets.buildsearch.BuildSearchParams;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Search build history by {@link hudson.model.Run} result.
+ *
+ * @author tom.fennelly@gmail.com
+ */
+public class ResultProcessorFactory extends BuildSearchParamProcessorFactory {
+
+ private static final String[] SEARCH_TERMS = new String[] {"result"}; // Build result. TODO: Could do "status" too or maybe that's different
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String[] getSearchTerms() {
+ return SEARCH_TERMS;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BuildSearchParamProcessor createProcessor(BuildSearchParams searchParams) {
+ List resultParams = searchParams.getParams(SEARCH_TERMS[0]);
+
+ if (resultParams.isEmpty()) {
+ // "result" search term not specified in search
+ return null;
+ }
+
+ final List interestingResults = new ArrayList();
+ for (BuildSearchParams.BuildSearchParam resultParam : resultParams) {
+ Result result = Result.fromString(resultParam.get());
+ // Result.fromString returns Result.FAILURE if there's no match. We don't want that.
+ if (result.toString().equalsIgnoreCase(resultParam.get())) {
+ interestingResults.add(result);
+ }
+ }
+
+ return new BuildSearchParamProcessor() {
+
+ @Override
+ public boolean fitsSearchParams(Result result) {
+ // It fits if it's any of the specified "result" search terms.
+ return interestingResults.contains(result);
+ }
+
+ @Override
+ public boolean fitsSearchParams(Queue.Item item) {
+ // Queue items don't have a Result.
+ return false;
+ }
+ @Override
+ public boolean fitsSearchParams(Run run) {
+ return fitsSearchParams(run.getResult());
+ }
+ };
+ }
+}
diff --git a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly
index 639002d537..54309319f0 100644
--- a/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly
+++ b/core/src/main/resources/hudson/widgets/BuildHistoryWidget/entries.jelly
@@ -27,52 +27,6 @@ THE SOFTWARE.
Render build histories.
-->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #${queuedItems.size()==1 ? it.owner.nextBuildNumber
- : it.owner.nextBuildNumber+queuedItems.size()-i-1}
-
-
-
-
-
-
- (${%pending}—)
-
-
- (${%pending})
-
-
-
-
-
-
-
-
- |
-
-
-
-
-
+
+
diff --git a/core/src/main/resources/hudson/widgets/HistoryPageFilter/ajaxBuildHistory.jelly b/core/src/main/resources/hudson/widgets/HistoryPageFilter/ajaxBuildHistory.jelly
new file mode 100644
index 0000000000..6fdf095c51
--- /dev/null
+++ b/core/src/main/resources/hudson/widgets/HistoryPageFilter/ajaxBuildHistory.jelly
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/resources/hudson/widgets/HistoryPageFilter/entries.jelly b/core/src/main/resources/hudson/widgets/HistoryPageFilter/entries.jelly
new file mode 100644
index 0000000000..622a838738
--- /dev/null
+++ b/core/src/main/resources/hudson/widgets/HistoryPageFilter/entries.jelly
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/resources/hudson/widgets/HistoryPageFilter/queue-items.jelly b/core/src/main/resources/hudson/widgets/HistoryPageFilter/queue-items.jelly
new file mode 100644
index 0000000000..be46285a13
--- /dev/null
+++ b/core/src/main/resources/hudson/widgets/HistoryPageFilter/queue-items.jelly
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #${queuedItems.size()==1 ? it.widget.owner.nextBuildNumber
+ : it.widget.owner.nextBuildNumber+queuedItems.size()-i-1}
+
+
+
+
+
+
+ (${%pending}—)
+
+
+ (${%pending})
+
+
+
+
+
+
+
+
+ |
+
+
+
+
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly
index 1011d9510e..6684873987 100644
--- a/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly
+++ b/core/src/main/resources/hudson/widgets/HistoryWidget/ajaxBuildHistory.jelly
@@ -25,10 +25,6 @@ THE SOFTWARE.
-
-
-
-
+
+
\ No newline at end of file
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly
index 2a49d5a698..8c07368d68 100644
--- a/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly
+++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entries.jelly
@@ -27,9 +27,9 @@ THE SOFTWARE.
-->
-
+
-
-
+
+
\ No newline at end of file
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly
index 44e3f664e3..215cb4d54a 100644
--- a/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly
+++ b/core/src/main/resources/hudson/widgets/HistoryWidget/entry.jelly
@@ -26,10 +26,12 @@ THE SOFTWARE.
Render a single build history entry indicated by ${build}
-->
-
-
-
-
+
+
+
+
+
+
@@ -42,7 +44,7 @@ THE SOFTWARE.
-
+
@@ -59,7 +61,7 @@ THE SOFTWARE.
-
+
diff --git a/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly b/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly
index f43ba94068..d42b56abb2 100644
--- a/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly
+++ b/core/src/main/resources/hudson/widgets/HistoryWidget/index.jelly
@@ -51,10 +51,33 @@ THE SOFTWARE.
-
+
+
+
+
-
-
+
+ |
+
+ |
+
+
+
+
- ${%More ...}
-
|
+
+
diff --git a/core/src/test/java/hudson/widgets/HistoryPageFilterTest.java b/core/src/test/java/hudson/widgets/HistoryPageFilterTest.java
new file mode 100644
index 0000000000..ab9eeb51c2
--- /dev/null
+++ b/core/src/test/java/hudson/widgets/HistoryPageFilterTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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 hudson.widgets;
+
+import hudson.model.Job;
+import hudson.model.ModelObject;
+import hudson.model.Queue;
+import hudson.model.Result;
+import hudson.model.Run;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author tom.fennelly@gmail.com
+ */
+public class HistoryPageFilterTest {
+
+ /**
+ * No items.
+ */
+ @Test
+ public void test_latest_empty_page() {
+ HistoryPageFilter historyPageFilter = newPage(5, null, null);
+ List itemList = new ArrayList();
+
+ historyPageFilter.add(itemList);
+ Assert.assertEquals(false, historyPageFilter.hasUpPage);
+ Assert.assertEquals(false, historyPageFilter.hasDownPage);
+ Assert.assertEquals(true, historyPageFilter.queueItems.isEmpty());
+ Assert.assertEquals(true, historyPageFilter.runs.isEmpty());
+ }
+
+ /**
+ * Latest/top page where total number of items < the max page size.
+ */
+ @Test
+ public void test_latest_partial_page() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, null, null);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 2));
+ itemList.addAll(newQueueItems(3, 4));
+
+ // want to make sure the list items are ordered by id in descending order
+ Assert.assertEquals(1, HistoryPageEntry.getEntryId(itemList.get(0)));
+ historyPageFilter.add(itemList);
+ Assert.assertEquals(4, HistoryPageEntry.getEntryId(itemList.get(0)));
+
+ Assert.assertEquals(false, historyPageFilter.hasUpPage);
+ Assert.assertEquals(false, historyPageFilter.hasDownPage);
+ Assert.assertEquals(2, historyPageFilter.queueItems.size());
+ Assert.assertEquals(2, historyPageFilter.runs.size());
+
+ Assert.assertEquals(4, historyPageFilter.queueItems.get(0).getEntryId());
+ Assert.assertEquals(4, historyPageFilter.newestOnPage);
+ Assert.assertEquals(3, historyPageFilter.queueItems.get(1).getEntryId());
+ Assert.assertEquals(2, historyPageFilter.runs.get(0).getEntryId());
+ Assert.assertEquals(1, historyPageFilter.runs.get(1).getEntryId());
+ Assert.assertEquals(1, historyPageFilter.oldestOnPage);
+ }
+
+ /**
+ * Latest/top page where total number of items > the max page size.
+ */
+ @Test
+ public void test_latest_longer_list() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, null, null);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ itemList.addAll(newQueueItems(11, 12));
+
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(false, historyPageFilter.hasUpPage);
+ Assert.assertEquals(true, historyPageFilter.hasDownPage);
+ Assert.assertEquals(2, historyPageFilter.queueItems.size());
+ Assert.assertEquals(3, historyPageFilter.runs.size());
+
+ Assert.assertEquals(12, historyPageFilter.queueItems.get(0).getEntryId());
+ Assert.assertEquals(12, historyPageFilter.newestOnPage);
+ Assert.assertEquals(10, historyPageFilter.runs.get(0).getEntryId());
+ }
+
+ /**
+ * Test olderThan (page down) when set to id greater than newest (should never happen). Should be same as not
+ * specifying newerThan/olderThan.
+ */
+ @Test
+ public void test_olderThan_gt_newest() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, null, 11L);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(false, historyPageFilter.hasUpPage);
+ Assert.assertEquals(true, historyPageFilter.hasDownPage);
+ Assert.assertEquals(5, historyPageFilter.runs.size());
+
+ Assert.assertEquals(10, historyPageFilter.newestOnPage);
+ Assert.assertEquals(6, historyPageFilter.oldestOnPage);
+ }
+
+ /**
+ * Test olderThan (page down) when set to id less than the oldest (should never happen). Should just give an
+ * empty list of builds.
+ */
+ @Test
+ public void test_olderThan_lt_oldest() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, null, 0L);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(true, historyPageFilter.hasUpPage);
+ Assert.assertEquals(false, historyPageFilter.hasDownPage);
+ Assert.assertEquals(0, historyPageFilter.runs.size());
+ }
+
+ /**
+ * Test olderThan (page down) when set to an id close to the oldest in the list (where
+ * there's less than a full page older than the supplied olderThan arg).
+ */
+ @Test
+ public void test_olderThan_leaving_part_page() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, null, 4L);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(true, historyPageFilter.hasUpPage);
+ Assert.assertEquals(false, historyPageFilter.hasDownPage);
+
+ // Should only be 3 runs on the page (oldest 3)
+ Assert.assertEquals(3, historyPageFilter.runs.size());
+
+ Assert.assertEquals(3, historyPageFilter.newestOnPage);
+ Assert.assertEquals(1, historyPageFilter.oldestOnPage);
+ }
+
+ /**
+ * Test olderThan (page down) when set to an id in the middle. Should be a page up and a page down.
+ */
+ @Test
+ public void test_olderThan_mid_page() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, null, 8L);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(true, historyPageFilter.hasUpPage);
+ Assert.assertEquals(true, historyPageFilter.hasDownPage);
+ Assert.assertEquals(5, historyPageFilter.runs.size());
+
+ Assert.assertEquals(7, historyPageFilter.newestOnPage);
+ Assert.assertEquals(3, historyPageFilter.oldestOnPage);
+ }
+
+ /**
+ * Test newerThan (page up) when set to id greater than newest (should never happen). Should be an empty list.
+ */
+ @Test
+ public void test_newerThan_gt_newest() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, 11L, null);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(false, historyPageFilter.hasUpPage);
+ Assert.assertEquals(true, historyPageFilter.hasDownPage);
+ Assert.assertEquals(0, historyPageFilter.runs.size());
+ }
+
+ /**
+ * Test newerThan (page up) when set to id less than the oldest (should never happen). Should give the oldest
+ * set of builds.
+ */
+ @Test
+ public void test_newerThan_lt_oldest() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, 0L, null);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(true, historyPageFilter.hasUpPage);
+ Assert.assertEquals(false, historyPageFilter.hasDownPage);
+ Assert.assertEquals(5, historyPageFilter.runs.size());
+
+ Assert.assertEquals(5, historyPageFilter.newestOnPage);
+ Assert.assertEquals(1, historyPageFilter.oldestOnPage);
+ }
+
+ /**
+ * Test newerThan (page up) mid range nearer the oldest build in the list.
+ */
+ @Test
+ public void test_newerThan_near_oldest() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, 3L, null);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(true, historyPageFilter.hasUpPage);
+ Assert.assertEquals(true, historyPageFilter.hasDownPage);
+ Assert.assertEquals(5, historyPageFilter.runs.size());
+
+ Assert.assertEquals(8, historyPageFilter.newestOnPage);
+ Assert.assertEquals(4, historyPageFilter.oldestOnPage);
+ }
+
+ /**
+ * Test newerThan (page up) mid range nearer the newest build in the list. This works a little different
+ * in that it will put the 2 builds newer than newerThan on the page and then fill the remaining slots on the
+ * page with builds equal to and older i.e. it return the newest/latest builds.
+ */
+ @Test
+ public void test_newerThan_near_newest() throws IOException {
+ HistoryPageFilter historyPageFilter = newPage(5, 8L, null);
+ List itemList = new ArrayList();
+
+ itemList.addAll(newRuns(1, 10));
+ historyPageFilter.add(itemList);
+
+ Assert.assertEquals(false, historyPageFilter.hasUpPage);
+ Assert.assertEquals(true, historyPageFilter.hasDownPage);
+ Assert.assertEquals(5, historyPageFilter.runs.size());
+
+ Assert.assertEquals(10, historyPageFilter.newestOnPage);
+ Assert.assertEquals(6, historyPageFilter.oldestOnPage);
+ }
+
+ private List newQueueItems(long startId, long endId) {
+ List items = new ArrayList();
+ for (long queueId = startId; queueId <= endId; queueId++) {
+ items.add(new Queue.MockItem(queueId));
+ }
+ return items;
+ }
+
+ private List newRuns(long startId, long endId) throws IOException {
+ List runs = new ArrayList();
+ for (long queueId = startId; queueId <= endId; queueId++) {
+ runs.add(new MockRun(queueId));
+ }
+ return runs;
+ }
+
+ private HistoryPageFilter newPage(int maxEntries, Long newerThan, Long olderThan) {
+ HistoryPageFilter pageFilter = new HistoryPageFilter(maxEntries);
+ if (newerThan != null) {
+ pageFilter.setNewerThan(newerThan);
+ } else if (olderThan != null) {
+ pageFilter.setOlderThan(olderThan);
+ }
+ return pageFilter;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static class MockRun extends Run {
+ private final long queueId;
+
+ public MockRun(long queueId) throws IOException {
+ super(Mockito.mock(Job.class));
+ this.queueId = queueId;
+ }
+
+ @Override
+ public int compareTo(Run o) {
+ return 0;
+ }
+
+ @Override
+ public Result getResult() {
+ return result;
+ }
+
+ @Override
+ public boolean isBuilding() {
+ return false;
+ }
+
+ @Override
+ public long getQueueId() {
+ return queueId;
+ }
+ }
+}
diff --git a/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamProcessorTest.java b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamProcessorTest.java
new file mode 100644
index 0000000000..12baebc656
--- /dev/null
+++ b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamProcessorTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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 hudson.widgets.buildsearch;
+
+import hudson.model.Result;
+import hudson.widgets.buildsearch.processors.DateProcessorFactory;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * @author tom.fennelly@gmail.com
+ */
+public class BuildSearchParamProcessorTest {
+
+ @Test
+ public void test_name() {
+ BuildSearchParams searchParams = new BuildSearchParams(" name: Build1 name: Build2 ");
+ List processors = new BuildSearchParamProcessorList(searchParams).getProcessors();
+
+ Assert.assertEquals(1, processors.size());
+
+ BuildSearchParamProcessor processor = processors.get(0);
+ Assert.assertTrue(processor.fitsSearchParams("Build1 was good"));
+ Assert.assertTrue(processor.fitsSearchParams("The Build2 not so good"));
+ Assert.assertFalse(processor.fitsSearchParams("Build3"));
+ }
+
+ @Test
+ public void test_description() {
+ BuildSearchParams searchParams = new BuildSearchParams(" desc: Build1 desc: Build2 ");
+ List processors = new BuildSearchParamProcessorList(searchParams).getProcessors();
+
+ Assert.assertEquals(1, processors.size());
+
+ BuildSearchParamProcessor processor = processors.get(0);
+ Assert.assertTrue(processor.fitsSearchParams("Build1 was good"));
+ Assert.assertTrue(processor.fitsSearchParams("The Build2 not so good"));
+ Assert.assertFalse(processor.fitsSearchParams("Build3"));
+ }
+
+ @Test
+ public void test_result() {
+ BuildSearchParams searchParams = new BuildSearchParams(" result: UNSTABLE result: aborted result: BLAH ");
+ List processors = new BuildSearchParamProcessorList(searchParams).getProcessors();
+
+ Assert.assertEquals(1, processors.size());
+
+ BuildSearchParamProcessor processor = processors.get(0);
+ Assert.assertTrue(processor.fitsSearchParams(Result.UNSTABLE));
+ Assert.assertTrue(processor.fitsSearchParams(Result.ABORTED));
+ Assert.assertFalse(processor.fitsSearchParams(Result.SUCCESS));
+ Assert.assertFalse(processor.fitsSearchParams(Result.FAILURE)); // need to make sure "BLAH" is not interpreted as FAILURE. See Result.fromString.
+ }
+
+ @Test
+ public void test_date_from_and_to_ok() {
+ BuildSearchParams searchParams = new BuildSearchParams("date-from: 2015-02-20 date-to: 2015-03-20");
+
+ List processors = new BuildSearchParamProcessorList(searchParams).getProcessors();
+
+ Assert.assertEquals(1, processors.size());
+
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-19")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-20")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-21")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-03-19")));
+ Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-03-21")));
+ }
+
+ /**
+ * date-to: set to a date before date-from: should result in the date-to: being ignored.
+ */
+ @Test
+ public void test_date_from_and_to_with_to_before_from() {
+ BuildSearchParams searchParams = new BuildSearchParams("date-from:2015-02-20 date-to:2015-02-10");
+
+ List processors = new BuildSearchParamProcessorList(searchParams).getProcessors();
+
+ Assert.assertEquals(1, processors.size());
+
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-19")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-20")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-02-21")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis("2015-03-21")));
+ }
+
+ @Test
+ public void test_date_short() {
+ List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 02-20")).getProcessors();
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ Assert.assertFalse(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis(DateProcessorFactory.getCurrentYearString() + "-02-19")));
+ Assert.assertTrue(processor.fitsSearchParams(DateProcessorFactory.toTimeInMillis(DateProcessorFactory.getCurrentYearString() + "-02-20")));
+ }
+
+ @Test
+ public void test_date_today() {
+ List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: today")).getProcessors();
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ long now = System.currentTimeMillis();
+ Assert.assertFalse(processor.fitsSearchParams(now - DateProcessorFactory.DAY));
+ Assert.assertTrue(processor.fitsSearchParams(now));
+ Assert.assertTrue(processor.fitsSearchParams(now + DateProcessorFactory.DAY));
+ }
+
+ @Test
+ public void test_date_last_week_to_today() {
+ List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: week date-to: today")).getProcessors();
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ long now = System.currentTimeMillis();
+ Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 8)));
+ Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 7)));
+ Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 1)));
+ Assert.assertFalse(processor.fitsSearchParams(now));
+ }
+
+ @Test
+ public void test_date_days() {
+ List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 5 days date-to: 1 day")).getProcessors();
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ long now = System.currentTimeMillis();
+ Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 6)));
+ Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 5)));
+ Assert.assertFalse(processor.fitsSearchParams(now));
+ }
+
+ @Test
+ public void test_date_weeks() {
+ List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 5 weeks date-to: 1 week")).getProcessors();
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ long now = System.currentTimeMillis();
+ Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.WEEK * 6)));
+ Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.WEEK * 5)));
+ Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.DAY * 6)));
+ }
+
+ @Test
+ public void test_date_months() {
+ List processors = new BuildSearchParamProcessorList(new BuildSearchParams("date-from: 5 months date-to: 1 month")).getProcessors();
+ BuildSearchParamProcessor processor = processors.get(0);
+
+ long now = System.currentTimeMillis();
+ Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.MONTH * 6)));
+ Assert.assertTrue(processor.fitsSearchParams(now - (DateProcessorFactory.MONTH * 5)));
+ Assert.assertFalse(processor.fitsSearchParams(now - (DateProcessorFactory.MONTH - DateProcessorFactory.DAY)));
+ }
+}
diff --git a/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamsTest.java b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamsTest.java
new file mode 100644
index 0000000000..a71f827ed9
--- /dev/null
+++ b/core/src/test/java/hudson/widgets/buildsearch/BuildSearchParamsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 hudson.widgets.buildsearch;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author tom.fennelly@gmail.com
+ */
+public class BuildSearchParamsTest {
+
+ @Test
+ public void test_no_params() {
+ Assert.assertEquals("{name=[], desc=[], result=[], date-from=[], date-to=[]}", new BuildSearchParams("").toString());
+ Assert.assertEquals("{name=[], desc=[], result=[], date-from=[], date-to=[]}", new BuildSearchParams("blah").toString());
+ Assert.assertEquals("{name=[], desc=[], result=[], date-from=[], date-to=[]}", new BuildSearchParams("name: desc: ").toString()); // whitespace only params should be ignored
+ }
+
+ @Test
+ public void test_unknown_search_term() {
+ try {
+ new BuildSearchParams("").getParams("blah");
+ Assert.fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("Unknown search term 'blah'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void test_with_params_one_of_each() {
+ Assert.assertEquals(
+ "{name=[name:'Build1'], desc=[desc:'something big'], result=[result:'FAILED'], date-from=[], date-to=[]}",
+ new BuildSearchParams(" result: FAILED Name: Build1 desc: something big ").toString());
+ }
+
+ @Test
+ public void test_with_params_multiples_of_each() {
+ BuildSearchParams searchParams =
+ new BuildSearchParams(" result: FAILED name: Build1 desc: something big result:SUCCESS desc: something middle ");
+
+ Assert.assertEquals("[name:'Build1']",
+ searchParams.getParams("name").toString());
+ Assert.assertEquals("[desc:'something big', desc:'something middle']",
+ searchParams.getParams("desc").toString());
+ Assert.assertEquals("[result:'FAILED', result:'SUCCESS']",
+ searchParams.getParams("result").toString());
+ Assert.assertEquals(
+ "{name=[name:'Build1'], desc=[desc:'something big', desc:'something middle'], result=[result:'FAILED', result:'SUCCESS'], date-from=[], date-to=[]}",
+ searchParams.toString());
+ }
+}
diff --git a/war/src/main/webapp/css/style.css b/war/src/main/webapp/css/style.css
index 31480a0fdc..a1867ea6c4 100644
--- a/war/src/main/webapp/css/style.css
+++ b/war/src/main/webapp/css/style.css
@@ -967,6 +967,89 @@ table.parameters > tbody:hover {
float: right;
}
+#buildHistoryPage {
+ position: relative;
+}
+
+#buildHistoryPage .build-search-row td {
+ position: relative;
+}
+#buildHistoryPage .build-search-row .clear {
+ position: absolute;
+ padding: 0px 4px;
+ cursor: pointer;
+ top: 7px;
+ right: 6px;
+ opacity: 0.5;
+}
+#buildHistoryPage .build-search-row .clear:hover {
+ background-color: #d3d3d3;
+ border: 1px solid #bbb;
+ top: 6px;
+ padding-right: 3px;
+ border-top-right-radius: 2px;
+}
+#buildHistoryPage .build-search-row input {
+ width: 100%;
+ padding-left: 5px;
+ padding-right: 20px;
+}
+#buildHistoryPage .build-search-row table {
+ width: 100%;
+}
+#buildHistoryPage .build-search-row .build-search-help {
+ width: 20px;
+}
+#buildHistoryPage .build-search-row .icon-help {
+ position: absolute;
+ cursor: pointer;
+ top: 6px;
+}
+
+#buildHistoryPageNav {
+ position: absolute;
+ right: -21px;
+ top: 41px;
+ border: 1px solid;
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ visibility: hidden;
+}
+
+#buildHistoryPageNav .nav {
+ width: 16px;
+ height: 16px;
+ background: no-repeat;
+ background-position: center;
+ background-size: 16px 16px;
+ margin: 4px 2px;
+ cursor: pointer;
+}
+
+#buildHistoryPageNav.mouseOverSidePanel {
+ visibility: visible;
+ border-color: #f0f0f0 !important;
+}
+#buildHistoryPageNav.mouseOverSidePanelBuildHistory {
+ border-color: #bbb !important;
+}
+#buildHistoryPageNav:hover {
+ border-color: #bbb !important;
+}
+
+#buildHistoryPageNav .pageOne {
+ background-image: url(../images/16x16/bullseye.png);
+}
+#buildHistoryPageNav .pageUp {
+ background-image: url(../images/16x16/go_up.png);
+}
+#buildHistoryPageNav .pageDown {
+ background-image: url(../images/16x16/go_down.png);
+}
+#buildHistoryPageNav .find {
+ background-image: url(../images/16x16/find.png);
+}
+
.build-row {
padding: 3px 4px 3px 4px;
}
diff --git a/war/src/main/webapp/images/16x16/bullseye.png b/war/src/main/webapp/images/16x16/bullseye.png
new file mode 100644
index 0000000000..442ccf26dd
Binary files /dev/null and b/war/src/main/webapp/images/16x16/bullseye.png differ
diff --git a/war/src/main/webapp/images/16x16/find.png b/war/src/main/webapp/images/16x16/find.png
new file mode 100644
index 0000000000..20d07f7050
Binary files /dev/null and b/war/src/main/webapp/images/16x16/find.png differ
diff --git a/war/src/main/webapp/images/16x16/go_down.png b/war/src/main/webapp/images/16x16/go_down.png
new file mode 100644
index 0000000000..54de8f7ec2
Binary files /dev/null and b/war/src/main/webapp/images/16x16/go_down.png differ
diff --git a/war/src/main/webapp/images/16x16/go_up.png b/war/src/main/webapp/images/16x16/go_up.png
new file mode 100644
index 0000000000..242309a631
Binary files /dev/null and b/war/src/main/webapp/images/16x16/go_up.png differ
diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js
index 270ab19cbc..270195fd53 100644
--- a/war/src/main/webapp/scripts/hudson-behavior.js
+++ b/war/src/main/webapp/scripts/hudson-behavior.js
@@ -1583,11 +1583,12 @@ function fireBuildHistoryChanged() {
function updateBuildHistory(ajaxUrl,nBuild) {
if(isRunAsTest) return;
var bh = $('buildHistory');
+ var buildHistoryPage = $('buildHistoryPage');
bh.headers = ["n",nBuild];
function getDataTable(buildHistoryDiv) {
- return $(buildHistoryDiv).getElementsBySelector('table.pane')[0];
+ return $(buildHistoryDiv).getElementsBySelector('table.pane')[0];
}
var leftRightPadding = 4;
@@ -1884,7 +1885,6 @@ function updateBuildHistory(ajaxUrl,nBuild) {
}
}
- var updateBuildsRefreshInterval = 5000;
function updateBuilds() {
if(isPageVisible()){
if (bh.headers == null) {
@@ -1898,8 +1898,12 @@ function updateBuildHistory(ajaxUrl,nBuild) {
var rows = dataTable.rows;
//delete rows with transitive data
- while (rows.length > 0 && Element.hasClassName(rows[0], "transitive")) {
- Element.remove(rows[0]);
+ var firstBuildRow = 0;
+ if (Element.hasClassName(rows[firstBuildRow], "build-search-row")) {
+ firstBuildRow++;
+ }
+ while (rows.length > 0 && Element.hasClassName(rows[firstBuildRow], "transitive")) {
+ Element.remove(rows[firstBuildRow]);
}
// insert new rows
@@ -1907,8 +1911,9 @@ function updateBuildHistory(ajaxUrl,nBuild) {
div.innerHTML = rsp.responseText;
Behaviour.applySubtree(div);
- var pivot = rows[0];
- var newRows = getDataTable(div).rows;
+ var pivot = rows[firstBuildRow];
+ var newDataTable = getDataTable(div);
+ var newRows = newDataTable.rows;
while (newRows.length > 0) {
if (pivot !== undefined) {
// The data table has rows. Insert before a "pivot" row (first row).
@@ -1917,26 +1922,201 @@ function updateBuildHistory(ajaxUrl,nBuild) {
// The data table has no rows. In this case, we just add all new rows directly to the
// table, one after the other i.e. we don't insert before a "pivot" row (first row).
dataTable.appendChild(newRows[0]);
- }
+ }
+ }
+
+ if (Element.hasClassName(newDataTable, 'hasPageData')) {
+ buildHistoryPage.setAttribute('page-entry-newest', newDataTable.getAttribute('page-entry-newest'));
}
// next update
bh.headers = ["n",rsp.getResponseHeader("n")];
- window.setTimeout(updateBuilds, updateBuildsRefreshInterval);
-
checkAllRowCellOverflows();
+ createRefreshTimeout();
}
- });
- } else {
+ });
+ } else {
// Reschedule again
- window.setTimeout(updateBuilds, updateBuildsRefreshInterval);
+ createRefreshTimeout();
}
}
- window.setTimeout(updateBuilds, updateBuildsRefreshInterval);
+
+ var updateBuildsRefreshInterval = 5000;
+ var buildRefreshTimeout;
+ function createRefreshTimeout() {
+ cancelRefreshTimeout();
+ buildRefreshTimeout = window.setTimeout(updateBuilds, updateBuildsRefreshInterval);
+ }
+ function cancelRefreshTimeout() {
+ if (buildRefreshTimeout) {
+ window.clearTimeout(buildRefreshTimeout);
+ buildRefreshTimeout = undefined;
+ }
+ }
+
+ createRefreshTimeout();
onBuildHistoryChange(function() {
checkAllRowCellOverflows();
});
+
+ function setupHistoryNav() {
+ var sidePanel = $('side-panel');
+ var buildHistoryPageNav = $('buildHistoryPageNav');
+
+ // Show/hide the nav as the mouse moves into the sidepanel and build history.
+ sidePanel.observe('mouseover', function() {
+ Element.addClassName($(buildHistoryPageNav), "mouseOverSidePanel");
+ });
+ sidePanel.observe('mouseout', function() {
+ Element.removeClassName($(buildHistoryPageNav), "mouseOverSidePanel");
+ });
+ bh.observe('mouseover', function() {
+ Element.addClassName($(buildHistoryPageNav), "mouseOverSidePanelBuildHistory");
+ });
+ bh.observe('mouseout', function() {
+ Element.removeClassName($(buildHistoryPageNav), "mouseOverSidePanelBuildHistory");
+ });
+
+ var pageSearchInput = Element.getElementsBySelector(bh, '.build-search-row input')[0];
+ var pageSearchClear = Element.getElementsBySelector(bh, '.build-search-row .clear')[0];
+ var pageOne = Element.getElementsBySelector(buildHistoryPageNav, '.pageOne')[0];
+ var pageUp = Element.getElementsBySelector(buildHistoryPageNav, '.pageUp')[0];
+ var pageDown = Element.getElementsBySelector(buildHistoryPageNav, '.pageDown')[0];
+
+ function hasPageUp() {
+ return buildHistoryPage.getAttribute('page-has-up') === 'true';
+ }
+ function hasPageDown() {
+ return buildHistoryPage.getAttribute('page-has-down') === 'true';
+ }
+ function getNewestEntryId() {
+ return parseInt(buildHistoryPage.getAttribute('page-entry-newest'));
+ }
+ function getOldestEntryId() {
+ return parseInt(buildHistoryPage.getAttribute('page-entry-oldest'));
+ }
+ function updatePageParams(dataTable) {
+ buildHistoryPage.setAttribute('page-has-up', dataTable.getAttribute('page-has-up'));
+ buildHistoryPage.setAttribute('page-has-down', dataTable.getAttribute('page-has-down'));
+ buildHistoryPage.setAttribute('page-entry-newest', dataTable.getAttribute('page-entry-newest'));
+ buildHistoryPage.setAttribute('page-entry-oldest', dataTable.getAttribute('page-entry-oldest'));
+ }
+ function togglePageUpDown() {
+ Element.removeClassName($(buildHistoryPageNav), "hasUpPage");
+ Element.removeClassName($(buildHistoryPageNav), "hasDownPage");
+ if (hasPageUp()) {
+ Element.addClassName($(buildHistoryPageNav), "hasUpPage");
+ }
+ if (hasPageDown()) {
+ Element.addClassName($(buildHistoryPageNav), "hasDownPage");
+ }
+ }
+ function logPageParams() {
+ console.log('-----');
+ console.log('Has up: ' + hasPageUp());
+ console.log('Has down: ' + hasPageDown());
+ console.log('Newest: ' + getNewestEntryId());
+ console.log('Oldest: ' + getOldestEntryId());
+ console.log('-----');
+ }
+
+ function loadPage(params, focusOnSearch) {
+ var searchString = pageSearchInput.value;
+
+ if (searchString !== '') {
+ if (params === undefined) {
+ params = {};
+ }
+ params.search = searchString;
+ }
+
+ new Ajax.Request(ajaxUrl + toQueryString(params), {
+ onSuccess: function(rsp) {
+ var dataTable = getDataTable(bh);
+ var rows = dataTable.rows;
+
+ // delete all rows
+ var searchRow;
+ if (Element.hasClassName(rows[0], "build-search-row")) {
+ searchRow = rows[0];
+ }
+ while (rows.length > 0) {
+ Element.remove(rows[0]);
+ }
+ if (searchRow) {
+ dataTable.appendChild(searchRow);
+ }
+
+ // insert new rows
+ var div = document.createElement('div');
+ div.innerHTML = rsp.responseText;
+ Behaviour.applySubtree(div);
+
+ var newDataTable = getDataTable(div);
+ var newRows = newDataTable.rows;
+ while (newRows.length > 0) {
+ dataTable.appendChild(newRows[0]);
+ }
+
+ checkAllRowCellOverflows();
+ updatePageParams(newDataTable);
+ togglePageUpDown();
+ if (!hasPageUp()) {
+ createRefreshTimeout();
+ }
+
+ if (focusOnSearch) {
+ pageSearchInput.focus();
+ }
+ //logPageParams();
+ }
+ });
+ }
+
+ pageSearchInput.observe('keypress', function(e) {
+ var key = e.which || e.keyCode;
+ // On enter
+ if (key === 13) {
+ loadPage({}, true);
+ }
+ });
+ pageSearchClear.observe('click', function() {
+ pageSearchInput.value = '';
+ loadPage({}, true);
+ });
+ pageOne.observe('click', function() {
+ loadPage();
+ });
+ pageUp.observe('click', function() {
+ loadPage({'newer-than': getNewestEntryId()});
+ });
+ pageDown.observe('click', function() {
+ cancelRefreshTimeout();
+ loadPage({'older-than': getOldestEntryId()});
+ });
+
+ togglePageUpDown();
+ //logPageParams();
+ }
+ setupHistoryNav();
+}
+
+function toQueryString(params) {
+ var query = '';
+ if (params) {
+ for (var paramName in params) {
+ if (params.hasOwnProperty(paramName)) {
+ if (query === '') {
+ query = '?';
+ } else {
+ query += '&';
+ }
+ query += paramName + '=' + encodeURIComponent(params[paramName]);
+ }
+ }
+ }
+ return query;
}
function getElementOverflowParams(element) {