提交 484d9520 编写于 作者: A Albert So 提交者: Kohsuke Kawaguchi

[JENKINS-11762] Changes to add a configurable display name to jobs

上级 05b46659
/* /*
* The MIT License * The MIT License
* *
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Daniel Dyer, Tom Huybrechts * Daniel Dyer, Tom Huybrechts, Yahoo!, Inc.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -34,6 +34,7 @@ import hudson.cli.declarative.CLIMethod; ...@@ -34,6 +34,7 @@ import hudson.cli.declarative.CLIMethod;
import hudson.cli.declarative.CLIResolver; import hudson.cli.declarative.CLIResolver;
import hudson.model.listeners.ItemListener; import hudson.model.listeners.ItemListener;
import hudson.model.listeners.SaveableListener; import hudson.model.listeners.SaveableListener;
import hudson.search.SearchIndexBuilder;
import hudson.security.AccessControlled; import hudson.security.AccessControlled;
import hudson.security.Permission; import hudson.security.Permission;
import hudson.security.ACL; import hudson.security.ACL;
...@@ -91,6 +92,8 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet ...@@ -91,6 +92,8 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
protected volatile String description; protected volatile String description;
private transient ItemGroup parent; private transient ItemGroup parent;
protected String displayName;
protected AbstractItem(ItemGroup parent, String name) { protected AbstractItem(ItemGroup parent, String name) {
this.parent = parent; this.parent = parent;
...@@ -116,9 +119,18 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet ...@@ -116,9 +119,18 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
@Exported @Exported
public String getDisplayName() { public String getDisplayName() {
if(null!=displayName) {
return displayName;
}
// if the displayName is not set, then return the name as we use to do
return getName(); return getName();
} }
public void setDisplayName(String displayName) throws IOException {
this.displayName = displayName;
save();
}
public File getRootDir() { public File getRootDir() {
return parent.getRootDirFor(this); return parent.getRootDirFor(this);
} }
...@@ -548,6 +560,18 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet ...@@ -548,6 +560,18 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
out.abort(); // don't leave anything behind out.abort(); // don't leave anything behind
} }
} }
/* (non-Javadoc)
* @see hudson.model.AbstractModelObject#getSearchName()
*/
@Override
public String getSearchName() {
// the search name of abstract items should be the name and not display name.
// this will make suggestions use the names and not the display name
// so that the links will 302 directly to the thing the user was finding
return getName();
}
public String toString() { public String toString() {
return super.toString()+'['+getFullName()+']'; return super.toString()+'['+getFullName()+']';
......
...@@ -941,6 +941,21 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R ...@@ -941,6 +941,21 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
return null; return null;
} }
/**
* Takes the displayName request parameter and sets it into the the
* displayName member variable. If the displayName request parameter is a
* 0 length string, then set the displayName member variable to null.
* @param req
* @throws IOException
*/
void setDisplayNameFromRequest(StaplerRequest req) throws IOException {
String displayName = req.getParameter("displayName");
// if displayName is an empty string, just make it null so that we
// use the project name
displayName = Util.nullify(displayName);
setDisplayName(displayName);
}
// //
// //
// actions // actions
...@@ -958,6 +973,8 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R ...@@ -958,6 +973,8 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
keepDependencies = req.getParameter("keepDependencies") != null; keepDependencies = req.getParameter("keepDependencies") != null;
setDisplayNameFromRequest(req);
try { try {
JSONObject json = req.getSubmittedForm(); JSONObject json = req.getSubmittedForm();
......
/* /*
* The MIT License * The MIT License
* *
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts,
* Yahoo!, Inc.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -66,6 +67,8 @@ import java.util.HashMap; ...@@ -66,6 +67,8 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import static jenkins.model.Jenkins.*; import static jenkins.model.Jenkins.*;
...@@ -91,6 +94,9 @@ import static jenkins.model.Jenkins.*; ...@@ -91,6 +94,9 @@ import static jenkins.model.Jenkins.*;
*/ */
@ExportedBean @ExportedBean
public abstract class View extends AbstractModelObject implements AccessControlled, Describable<View>, ExtensionPoint, Saveable { public abstract class View extends AbstractModelObject implements AccessControlled, Describable<View>, ExtensionPoint, Saveable {
private final static Logger logger = Logger.getLogger(View.class.getName());
/** /**
* Container of this view. Set right after the construction * Container of this view. Set right after the construction
* and never change thereafter. * and never change thereafter.
...@@ -672,14 +678,34 @@ public abstract class View extends AbstractModelObject implements AccessControll ...@@ -672,14 +678,34 @@ public abstract class View extends AbstractModelObject implements AccessControll
} }
} }
void addDisplayNamesToSearchIndex(SearchIndexBuilder sib, Collection<TopLevelItem> items) {
for(TopLevelItem item : items) {
if(logger.isLoggable(Level.FINE)) {
logger.fine((String.format("Adding url=%s,displayName=%s",
item.getSearchUrl(), item.getDisplayName())));
}
sib.add(item.getSearchUrl(), item.getDisplayName());
}
}
@Override @Override
public SearchIndexBuilder makeSearchIndex() { public SearchIndexBuilder makeSearchIndex() {
return super.makeSearchIndex() SearchIndexBuilder sib = super.makeSearchIndex();
.add(new CollectionSearchIndex() {// for jobs in the view sib.add(new CollectionSearchIndex<TopLevelItem>() {// for jobs in the view
protected TopLevelItem get(String key) { return getItem(key); } protected TopLevelItem get(String key) { return getItem(key); }
protected Collection<TopLevelItem> all() { return getItems(); } protected Collection<TopLevelItem> all() { return getItems(); }
@Override
protected String getName(TopLevelItem o) {
// return the name instead of the display for suggestion searching
return o.getName();
}
}); });
// add the display name for each item in the search index
addDisplayNamesToSearchIndex(sib, getItems());
return sib;
} }
/** /**
......
/* /*
* The MIT License * The MIT License
* *
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Yahoo!, Inc.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -25,6 +26,7 @@ package hudson.search; ...@@ -25,6 +26,7 @@ package hudson.search;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import hudson.Util;
import hudson.util.EditDistance; import hudson.util.EditDistance;
import java.io.IOException; import java.io.IOException;
import java.util.AbstractList; import java.util.AbstractList;
...@@ -33,6 +35,9 @@ import java.util.Collections; ...@@ -33,6 +35,9 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import org.kohsuke.stapler.Ancestor; import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.QueryParameter;
...@@ -49,12 +54,17 @@ import org.kohsuke.stapler.export.Flavor; ...@@ -49,12 +54,17 @@ import org.kohsuke.stapler.export.Flavor;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
public class Search { public class Search {
private final static Logger logger = Logger.getLogger(Search.class.getName());
public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
List<Ancestor> l = req.getAncestors(); List<Ancestor> l = req.getAncestors();
for( int i=l.size()-1; i>=0; i-- ) { for( int i=l.size()-1; i>=0; i-- ) {
Ancestor a = l.get(i); Ancestor a = l.get(i);
if (a.getObject() instanceof SearchableModelObject) { if (a.getObject() instanceof SearchableModelObject) {
SearchableModelObject smo = (SearchableModelObject) a.getObject(); SearchableModelObject smo = (SearchableModelObject) a.getObject();
if(logger.isLoggable(Level.FINE)){
logger.fine(String.format("smo.displayName=%s, searchName=%s",smo.getDisplayName(), smo.getSearchName()));
}
SearchIndex index = smo.getSearchIndex(); SearchIndex index = smo.getSearchIndex();
String query = req.getParameter("q"); String query = req.getParameter("q");
...@@ -171,12 +181,49 @@ public class Search { ...@@ -171,12 +181,49 @@ public class Search {
} }
/** /**
* Performs a search and returns the match, or null if no match was found. * When there are mutiple suggested items, this method can narrow down the resultset
* to the SuggestedItem that has a url that contains the query. This is useful is one
* job has a display name that matches another job's project name.
* @param r A list of Suggested items. It is assumed that there is at least one
* SuggestedItem in r.
* @param query A query string
* @return Returns the SuggestedItem which has a search url that contains the query.
* If no SuggestedItems have a search url which contains the query, then the first
* SuggestedItem in the List is returned.
*/
static SuggestedItem findClosestSuggestedItem(List<SuggestedItem> r, String query) {
for(SuggestedItem curItem : r) {
if(logger.isLoggable(Level.FINE)) {
logger.fine(String.format("item's searchUrl:%s;query=%s", curItem.item.getSearchUrl(), query));
}
if(curItem.item.getSearchUrl().contains(Util.rawEncode(query))) {
return curItem;
}
}
// couldn't find an item with the query in the url so just
// return the first one
return r.get(0);
}
/**
* Performs a search and returns the match, or null if no match was found
* or more than one match was found
*/ */
public static SuggestedItem find(SearchIndex index, String query) { public static SuggestedItem find(SearchIndex index, String query) {
List<SuggestedItem> r = find(Mode.FIND, index, query); List<SuggestedItem> r = find(Mode.FIND, index, query);
if(r.isEmpty()) return null; if(r.isEmpty()){
else return r.get(0); return null;
}
else if(1==r.size()){
return r.get(0);
}
else {
// we have more than one suggested item, so return the item who's url
// contains the query as this is probably the job's name
return findClosestSuggestedItem(r, query);
}
} }
public static List<SuggestedItem> suggest(SearchIndex index, final String tokenList) { public static List<SuggestedItem> suggest(SearchIndex index, final String tokenList) {
...@@ -244,6 +291,18 @@ public class Search { ...@@ -244,6 +291,18 @@ public class Search {
} }
}; };
} }
public String toString() {
StringBuilder s = new StringBuilder("TokenList{");
for(String token : tokens) {
s.append(token);
s.append(",");
}
s.append('}');
return s.toString();
}
} }
private static List<SuggestedItem> find(Mode m, SearchIndex index, String tokenList) { private static List<SuggestedItem> find(Mode m, SearchIndex index, String tokenList) {
...@@ -256,14 +315,19 @@ public class Search { ...@@ -256,14 +315,19 @@ public class Search {
List<SearchItem> items = new ArrayList<SearchItem>(); // items found in 1 step List<SearchItem> items = new ArrayList<SearchItem>(); // items found in 1 step
if(logger.isLoggable(Level.FINE)) {
logger.fine("tokens="+tokens.toString());
}
// first token // first token
int w=1; // width of token int w=1; // width of token
for (String token : tokens.subSequence(0)) { for (String token : tokens.subSequence(0)) {
items.clear(); items.clear();
m.find(index,token,items); m.find(index,token,items);
for (SearchItem si : items) for (SearchItem si : items) {
paths[w].add(new SuggestedItem(si)); paths[w].add(new SuggestedItem(si));
logger.info("found search item:"+si.getSearchName());
}
w++; w++;
} }
...@@ -285,4 +349,5 @@ public class Search { ...@@ -285,4 +349,5 @@ public class Search {
return paths[tokens.length()]; return paths[tokens.length()];
} }
} }
/* /*
* The MIT License * The MIT License
* *
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, * Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe,
* Stephen Connolly, Tom Huybrechts, Yahoo! Inc., Alan Harder, CloudBees, Inc. * Stephen Connolly, Tom Huybrechts, Yahoo! Inc., Alan Harder, CloudBees, Inc.,
* Yahoo!, Inc.
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
...@@ -3396,6 +3397,85 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup<TopLe ...@@ -3396,6 +3397,85 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup<TopLe
return getPrimaryView(); return getPrimaryView();
} }
/**
* This method checks all existing jobs to see if displayName is
* unique. It does not check the displayName against the displayName of the
* job that the user is configuring though to prevent a validation warning
* if the user sets the displayName to what it currently is.
* @param displayName
* @param currentJobName
* @return
*/
boolean isDisplayNameUnique(String displayName, String currentJobName) {
Collection<TopLevelItem> itemCollection = items.values();
// if there are a lot of projects, we'll have to store their
// display names in a HashSet or something for a quick check
for(TopLevelItem item : itemCollection) {
if(item.getName().equals(currentJobName)) {
// we won't compare the candidate displayName against the current
// item. This is to prevent an validation warning if the user
// sets the displayName to what the existing display name is
continue;
}
else if(displayName.equals(item.getDisplayName())) {
return false;
}
}
return true;
}
/**
* True if there is no item in Jenkins that has this name
* @param name The name to test
* @param currentJobName The name of the job that the user is configuring
* @return
*/
boolean isNameUnique(String name, String currentJobName) {
Item item = getItem(name);
if(null==item) {
// the candidate name didn't return any items so the name is unique
return true;
}
else if(item.getName().equals(currentJobName)) {
// the candidate name returned an item, but the item is the item
// that the user is configuring so this is ok
return true;
}
else {
// the candidate name returned an item, so it is not unique
return false;
}
}
/**
* Checks to see if the candidate displayName collides with any
* existing display names or project names
* @param displayName The display name to test
* @param jobName The name of the job the user is configuring
* @return
*/
public FormValidation doCheckDisplayName(@QueryParameter String displayName,
@QueryParameter String jobName) {
displayName = displayName.trim();
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Current job name is " + jobName);
}
if(!isNameUnique(displayName, jobName)) {
return FormValidation.warning(Messages.Jenkins_CheckDisplayName_NameNotUniqueWarning(displayName));
}
else if(!isDisplayNameUnique(displayName, jobName)){
return FormValidation.warning(Messages.Jenkins_CheckDisplayName_DisplayNameNotUniqueWarning(displayName));
}
else {
return FormValidation.ok();
}
}
public static class MasterComputer extends Computer { public static class MasterComputer extends Computer {
protected MasterComputer() { protected MasterComputer() {
super(Jenkins.getInstance()); super(Jenkins.getInstance());
......
<!-- <!--
The MIT License The MIT License
Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
Stephen Connolly, Yahoo!, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
...@@ -37,6 +38,10 @@ THE SOFTWARE. ...@@ -37,6 +38,10 @@ THE SOFTWARE.
<p:config-blockWhenUpstreamBuilding /> <p:config-blockWhenUpstreamBuilding />
<p:config-blockWhenDownstreamBuilding /> <p:config-blockWhenDownstreamBuilding />
<p:config-customWorkspace /> <p:config-customWorkspace />
<f:entry title="${%Display Name}" help="/help/project-config/displayName.html">
<f:textbox name="displayName" value="${it.displayName}"
checkUrl="'${rootURL}/checkDisplayName?displayName='+escape(this.value)+'&amp;jobName=${it.name}'"/>
</f:entry>
</f:advanced> </f:advanced>
</f:section> </f:section>
......
...@@ -27,7 +27,10 @@ THE SOFTWARE. ...@@ -27,7 +27,10 @@ THE SOFTWARE.
<l:layout title="${it.name}"> <l:layout title="${it.name}">
<st:include page="sidepanel.jelly" /> <st:include page="sidepanel.jelly" />
<l:main-panel> <l:main-panel>
<h1>${%Project} ${it.name}</h1> <h1>${%Project} ${it.displayName}</h1>
<j:if test="${it.name!=it.displayName}">
${%Project name}: ${it.name}
</j:if>
<t:editableDescription permission="${it.CONFIGURE}"/> <t:editableDescription permission="${it.CONFIGURE}"/>
<j:choose> <j:choose>
......
<!-- <!--
The MIT License The MIT License
Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
Eric Lefevre-Ardant, id:cactusman, Yahoo! Inc. Eric Lefevre-Ardant, id:cactusman, Yahoo! Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
...@@ -64,6 +64,11 @@ THE SOFTWARE. ...@@ -64,6 +64,11 @@ THE SOFTWARE.
<p:config-blockWhenUpstreamBuilding /> <p:config-blockWhenUpstreamBuilding />
<p:config-blockWhenDownstreamBuilding /> <p:config-blockWhenDownstreamBuilding />
<st:include page="configure-advanced.jelly" optional="true" /> <st:include page="configure-advanced.jelly" optional="true" />
<f:entry title="${%Display Name}" help="/help/project-config/displayName.html">
<f:textbox name="displayName" value="${it.displayName}"
checkUrl="'${rootURL}/checkDisplayName?displayName='+escape(this.value)+'&amp;jobName=${it.name}'"/>
</f:entry>
</f:advanced> </f:advanced>
</f:section> </f:section>
......
...@@ -23,3 +23,4 @@ ...@@ -23,3 +23,4 @@
default.value=(Default) default.value=(Default)
Advanced\ Project\ Options\ configure-common=Advanced Project Options Advanced\ Project\ Options\ configure-common=Advanced Project Options
title.concurrentbuilds=Execute concurrent builds if necessary title.concurrentbuilds=Execute concurrent builds if necessary
Display Name=Display Name
...@@ -28,6 +28,9 @@ THE SOFTWARE. ...@@ -28,6 +28,9 @@ THE SOFTWARE.
<st:include page="sidepanel.jelly" /> <st:include page="sidepanel.jelly" />
<l:main-panel> <l:main-panel>
<h1>${it.pronoun} ${it.displayName}</h1> <h1>${it.pronoun} ${it.displayName}</h1>
<j:if test="${(it.name!=it.displayName)&amp;&amp;(it.class.name!='hudson.matrix.MatrixConfiguration')}">
${%Project name}: ${it.name}
</j:if>
<t:editableDescription permission="${it.CONFIGURE}"/> <t:editableDescription permission="${it.CONFIGURE}"/>
<j:if test="${it.parent == app}"> <j:if test="${it.parent == app}">
......
...@@ -321,3 +321,7 @@ CLI.cancel-quiet-down.shortDescription=Cancel the effect of the "quiet-down" com ...@@ -321,3 +321,7 @@ CLI.cancel-quiet-down.shortDescription=Cancel the effect of the "quiet-down" com
CLI.reload-configuration.shortDescription=Discard all the loaded data in memory and reload everything from file system. Useful when you modified config files directly on disk. CLI.reload-configuration.shortDescription=Discard all the loaded data in memory and reload everything from file system. Useful when you modified config files directly on disk.
BuildAuthorizationToken.InvalidTokenProvided=Invalid token provided. BuildAuthorizationToken.InvalidTokenProvided=Invalid token provided.
Jenkins.CheckDisplayName.NameNotUniqueWarning=The display name, "{0}", is used as a name by a job and could cause confusing search results.
Jenkins.CheckDisplayName.DisplayNameNotUniqueWarning=The display name, "{0}", is already in use by another job and could cause confusion and delay.
rterter
\ No newline at end of file
...@@ -25,6 +25,6 @@ THE SOFTWARE. ...@@ -25,6 +25,6 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?> <?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt"> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<td style="${indenter.getCss(job)}"> <td style="${indenter.getCss(job)}">
<a href="${jobBaseUrl}${job.shortUrl}"> ${job.displayName}</a> <a href="${jobBaseUrl}${job.shortUrl}" tooltip="${job.name}"> ${job.displayName}</a>
</td> </td>
</j:jelly> </j:jelly>
\ No newline at end of file
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
# #
# Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, # Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
# Eric Lefevre-Ardant, Erik Ramfelt, Seiji Sogabe, id:cactusman, # Eric Lefevre-Ardant, Erik Ramfelt, Seiji Sogabe, id:cactusman,
# Manufacture Francaise des Pneumatiques Michelin, Romain Seguy # Manufacture Francaise des Pneumatiques Michelin, Romain Seguy,
# Yahoo!, Inc.
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal # of this software and associated documentation files (the "Software"), to deal
......
...@@ -32,5 +32,5 @@ THE SOFTWARE. ...@@ -32,5 +32,5 @@ THE SOFTWARE.
</st:documentation> </st:documentation>
<img src="${imagesURL}/16x16/${job.buildStatusUrl}" alt="${job.iconColor.description}" height="16" width="16"/> <img src="${imagesURL}/16x16/${job.buildStatusUrl}" alt="${job.iconColor.description}" height="16" width="16"/>
<a href="${h.getRelativeLinkTo(job)}">${job.displayName}</a> <a href="${h.getRelativeLinkTo(job)}" tooltip="${job.name}">${job.displayName}</a>
</j:jelly> </j:jelly>
...@@ -207,7 +207,7 @@ THE SOFTWARE. ...@@ -207,7 +207,7 @@ THE SOFTWARE.
<j:if test="${anc.prev!=null}"> <j:if test="${anc.prev!=null}">
<j:whitespace> &#187; </j:whitespace> <j:whitespace> &#187; </j:whitespace>
</j:if> </j:if>
<a href="${anc.url}/"> <a href="${anc.url}/" tooltip="${anc.object.name}">
${anc.object.displayName} ${anc.object.displayName}
</a> </a>
</j:if> </j:if>
......
...@@ -23,10 +23,14 @@ ...@@ -23,10 +23,14 @@
*/ */
package hudson.search; package hudson.search;
import hudson.Util;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.Assert;
/** /**
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
...@@ -49,4 +53,49 @@ public class SearchTest extends TestCase { ...@@ -49,4 +53,49 @@ public class SearchTest extends TestCase {
assertEquals("/abc-def-ghi",l.get(0).getUrl()); assertEquals("/abc-def-ghi",l.get(0).getUrl());
assertEquals("/abc/def-ghi",l.get(1).getUrl()); assertEquals("/abc/def-ghi",l.get(1).getUrl());
} }
/**
* This test verifies that if 2 search results are found with the same
* search name, that the one with the search query in the url is returned
*/
public void testFindClosestSuggestedItem() {
final String query = "foobar 123";
final String searchName = "sameDisplayName";
SearchItem searchItemHit = new SearchItem() {
public SearchIndex getSearchIndex() {
return null;
}
public String getSearchName() {
return searchName;
}
public String getSearchUrl() {
return "/job/"+Util.rawEncode(query) + "/";
}
};
SearchItem searchItemNoHit = new SearchItem() {
public SearchIndex getSearchIndex() {
return null;
}
public String getSearchName() {
return searchName;
}
public String getSearchUrl() {
return "/job/someotherJob/";
}
};
SuggestedItem suggestedHit = new SuggestedItem(searchItemHit);
SuggestedItem suggestedNoHit = new SuggestedItem(searchItemNoHit);
ArrayList<SuggestedItem> list = new ArrayList<SuggestedItem>();
list.add(suggestedNoHit);
list.add(suggestedHit); // make sure the hit is the second item
SuggestedItem found = Search.findClosestSuggestedItem(list, query);
Assert.assertEquals(searchItemHit, found.item);
SuggestedItem found2 = Search.findClosestSuggestedItem(list, "abcd");
Assert.assertEquals(searchItemNoHit, found2.item);
}
} }
/* /*
* The MIT License * The MIT License
* *
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,
* Yahoo! Inc., Tom Huybrechts, Olivier Lamy * Yahoo! Inc., Tom Huybrechts, Olivier Lamy
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
...@@ -354,11 +354,13 @@ public abstract class HudsonTestCase extends TestCase implements RootAction { ...@@ -354,11 +354,13 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
} }
clients.clear(); clients.clear();
for (Channel c : channels) synchronized(channels) {
c.close(); for (Channel c : channels)
for (Channel c : channels) c.close();
c.join(); for (Channel c : channels)
channels.clear(); c.join();
channels.clear();
}
} finally { } finally {
server.stop(); server.stop();
...@@ -497,11 +499,16 @@ public abstract class HudsonTestCase extends TestCase implements RootAction { ...@@ -497,11 +499,16 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException { public void onOnline(Computer c, TaskListener listener) throws IOException, InterruptedException {
VirtualChannel ch = c.getChannel(); VirtualChannel ch = c.getChannel();
if (ch instanceof Channel) if (ch instanceof Channel)
TestEnvironment.get().testCase.channels.add((Channel)ch); TestEnvironment.get().testCase.addChannel((Channel)ch);
} }
} }
private void addChannel(Channel ch) {
synchronized (channels) {
channels.add(ch);
}
}
// /** // /**
// * Sets guest credentials to access java.net Subversion repo. // * Sets guest credentials to access java.net Subversion repo.
// */ // */
......
...@@ -23,9 +23,19 @@ ...@@ -23,9 +23,19 @@
*/ */
package hudson.search; package hudson.search;
import jenkins.model.Jenkins;
import hudson.model.FreeStyleProject;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.AlertHandler; import com.gargoylesoftware.htmlunit.AlertHandler;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;
import net.sf.json.util.JSONBuilder;
import org.junit.Assert;
import org.jvnet.hudson.test.HudsonTestCase; import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.Bug; import org.jvnet.hudson.test.Bug;
...@@ -63,4 +73,134 @@ public class SearchTest extends HudsonTestCase { ...@@ -63,4 +73,134 @@ public class SearchTest extends HudsonTestCase {
assertEquals(404,e.getResponse().getStatusCode()); assertEquals(404,e.getResponse().getStatusCode());
} }
} }
public void testSearchByProjectName() throws Exception {
final String projectName = "testSearchByProjectName";
createFreeStyleProject(projectName);
Page result = search(projectName);
Assert.assertNotNull(result);
assertGoodStatus(result);
// make sure we've fetched the testSearchByDisplayName project page
String contents = result.getWebResponse().getContentAsString();
Assert.assertTrue(contents.contains(String.format("<title>%s [Jenkins]</title>", projectName)));
}
public void testSearchByDisplayName() throws Exception {
final String displayName = "displayName9999999";
FreeStyleProject project = createFreeStyleProject("testSearchByDisplayName");
project.setDisplayName(displayName);
Page result = search(displayName);
Assert.assertNotNull(result);
assertGoodStatus(result);
// make sure we've fetched the testSearchByDisplayName project page
String contents = result.getWebResponse().getContentAsString();
Assert.assertTrue(contents.contains(String.format("<title>%s [Jenkins]</title>", displayName)));
}
public void testSearch2ProjectsWithSameDisplayName() throws Exception {
// create 2 freestyle projects with the same display name
final String projectName1 = "projectName1";
final String projectName2 = "projectName2";
final String projectName3 = "projectName3";
final String displayName = "displayNameFoo";
final String otherDisplayName = "otherDisplayName";
FreeStyleProject project1 = createFreeStyleProject(projectName1);
project1.setDisplayName(displayName);
FreeStyleProject project2 = createFreeStyleProject(projectName2);
project2.setDisplayName(displayName);
FreeStyleProject project3 = createFreeStyleProject(projectName3);
project3.setDisplayName(otherDisplayName);
// make sure that on search we get back one of the projects, it doesn't
// matter which one as long as the one that is returned has displayName
// as the display name
Page result = search(displayName);
Assert.assertNotNull(result);
assertGoodStatus(result);
// make sure we've fetched the testSearchByDisplayName project page
String contents = result.getWebResponse().getContentAsString();
Assert.assertTrue(contents.contains(String.format("<title>%s [Jenkins]</title>", displayName)));
Assert.assertFalse(contents.contains(otherDisplayName));
}
public void testProjectNamePrecedesDisplayName() throws Exception {
final String project1Name = "foo";
final String project1DisplayName = "project1DisplayName";
final String project2Name = "project2Name";
final String project2DisplayName = project1Name;
final String project3Name = "project3Name";
final String project3DisplayName = "project3DisplayName";
// create 1 freestyle project with the name foo
FreeStyleProject project1 = createFreeStyleProject(project1Name);
project1.setDisplayName(project1DisplayName);
// create another with the display name foo
FreeStyleProject project2 = createFreeStyleProject(project2Name);
project2.setDisplayName(project2DisplayName);
// create a third project and make sure it's not picked up by search
FreeStyleProject project3 = createFreeStyleProject(project3Name);
project3.setDisplayName(project3DisplayName);
// search for foo
Page result = search(project1Name);
Assert.assertNotNull(result);
assertGoodStatus(result);
// make sure we get the project with the name foo
String contents = result.getWebResponse().getContentAsString();
Assert.assertTrue(contents.contains(String.format("<title>%s [Jenkins]</title>", project1DisplayName)));
// make sure projects 2 and 3 were not picked up
Assert.assertFalse(contents.contains(project2Name));
Assert.assertFalse(contents.contains(project3Name));
Assert.assertFalse(contents.contains(project3DisplayName));
}
public void testGetSuggestionsHasBothNamesAndDisplayNames() throws Exception {
final String projectName = "project name";
final String displayName = "display name";
FreeStyleProject project1 = createFreeStyleProject(projectName);
project1.setDisplayName(displayName);
WebClient wc = new WebClient();
Page result = wc.goTo("search/suggest?query=name", "application/javascript");
Assert.assertNotNull(result);
assertGoodStatus(result);
String content = result.getWebResponse().getContentAsString();
System.out.println(content);
JSONObject jsonContent = (JSONObject)JSONSerializer.toJSON(content);
Assert.assertNotNull(jsonContent);
JSONArray jsonArray = jsonContent.getJSONArray("suggestions");
Assert.assertNotNull(jsonArray);
Assert.assertEquals(2, jsonArray.size());
boolean foundProjectName = false;
boolean foundDispayName = false;
for(Object suggestion : jsonArray) {
JSONObject jsonSuggestion = (JSONObject)suggestion;
String name = (String)jsonSuggestion.get("name");
if(projectName.equals(name)) {
foundProjectName = true;
}
else if(displayName.equals(name)) {
foundDispayName = true;
}
}
Assert.assertTrue(foundProjectName);
Assert.assertTrue(foundDispayName);
}
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册