提交 72327986 编写于 作者: K Kohsuke Kawaguchi

[FIXED JENKINS-19124]

Added another attribute 'checkDependsOn' on <input> elements (akin to
'fillDependsOn', etc) to list up the other dependency controls, and
insert appropriate onchange events.

'checkUrl' is now just the stem portion of the URL to invoke, and
the client script builds up the query parameters.
上级 176ce5f5
......@@ -57,6 +57,9 @@ Upcoming changes</a>
<ul class=image>
<li class=rfe>
Command line now supports "--sessionTimeout" option for controlling session timeout
<li class=bug>
Form validation methods weren't getting triggered when one of its dependency controls change.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-19124">issue 19124</a>)
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -73,6 +73,7 @@ import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.tasks.UserAvatarResolver;
import hudson.util.Area;
import hudson.util.FormValidation.CheckMethod;
import hudson.util.Iterators;
import hudson.util.Secret;
import hudson.views.MyViewsTabBar;
......@@ -1531,6 +1532,9 @@ public class Functions {
/**
* Determines the form validation check URL. See textbox.jelly
*
* @deprecated
* Use {@link #calcCheckUrl}
*/
public String getCheckUrl(String userDefined, Object descriptor, String field) {
if(userDefined!=null || field==null) return userDefined;
......@@ -1541,6 +1545,23 @@ public class Functions {
return null;
}
/**
* Determines the parameters that client-side needs for a form validation check. See prepareDatabinding.jelly
*
* @deprecated
* Use {@link #calcCheckUrl}
*/
public void calcCheckUrl(Map attributes, String userDefined, Object descriptor, String field) {
if(userDefined!=null || field==null) return;
if (descriptor instanceof Descriptor) {
Descriptor d = (Descriptor) descriptor;
CheckMethod m = d.getCheckMethod(field);
attributes.put("checkUrl",m.toStemUrl());
attributes.put("checkDependsOn",m.getDependsOn());
}
}
/**
* If the given href link is matching the current page, return true.
*
......
......@@ -31,6 +31,7 @@ import hudson.BulkChange;
import hudson.Util;
import hudson.model.listeners.SaveableListener;
import hudson.util.FormApply;
import hudson.util.FormValidation.CheckMethod;
import hudson.util.ReflectionUtils;
import hudson.util.ReflectionUtils.Parameter;
import hudson.views.ListViewColumn;
......@@ -44,7 +45,6 @@ import org.springframework.util.StringUtils;
import org.jvnet.tiger_types.Types;
import org.apache.commons.io.IOUtils;
import static hudson.Functions.*;
import static hudson.util.QuotedStringTokenizer.*;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import javax.servlet.ServletException;
......@@ -127,7 +127,7 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable {
*/
public transient final Class<? extends T> clazz;
private transient final Map<String,String> checkMethods = new ConcurrentHashMap<String,String>();
private transient final Map<String,CheckMethod> checkMethods = new ConcurrentHashMap<String,CheckMethod>();
/**
* Lazily computed list of properties on {@link #clazz} and on the descriptor itself.
......@@ -361,69 +361,28 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable {
return a.getUrl();
}
/**
* @deprecated since 1.528
* Use {@link #getCheckMethod(String)}
*/
public String getCheckUrl(String fieldName) {
return getCheckMethod(fieldName).toCheckUrl();
}
/**
* If the field "xyz" of a {@link Describable} has the corresponding "doCheckXyz" method,
* return the form-field validation string. Otherwise null.
* return the model of the check method.
* <p>
* This method is used to hook up the form validation method to the corresponding HTML input element.
*/
public String getCheckUrl(String fieldName) {
String method = checkMethods.get(fieldName);
public CheckMethod getCheckMethod(String fieldName) {
CheckMethod method = checkMethods.get(fieldName);
if(method==null) {
method = calcCheckUrl(fieldName);
method = new CheckMethod(this,fieldName);
checkMethods.put(fieldName,method);
}
if (method.equals(NONE)) // == would do, but it makes IDE flag a warning
return null;
// put this under the right contextual umbrella.
// a is always non-null because we already have Hudson as the sentinel
return '\'' + jsStringEscape(getCurrentDescriptorByNameUrl()) + "/'+" + method;
}
private String calcCheckUrl(String fieldName) {
String capitalizedFieldName = StringUtils.capitalize(fieldName);
Method method = ReflectionUtils.getPublicMethodNamed(getClass(),"doCheck"+ capitalizedFieldName);
if(method==null)
return NONE;
return '\'' + getDescriptorUrl() + "/check" + capitalizedFieldName + '\'' + buildParameterList(method, new StringBuilder()).append(".toString()");
}
/**
* Builds query parameter line by figuring out what should be submitted
*/
private StringBuilder buildParameterList(Method method, StringBuilder query) {
for (Parameter p : ReflectionUtils.getParameters(method)) {
QueryParameter qp = p.annotation(QueryParameter.class);
if (qp!=null) {
String name = qp.value();
if (name.length()==0) name = p.name();
if (name==null || name.length()==0)
continue; // unknown parameter name. we'll report the error when the form is submitted.
RelativePath rp = p.annotation(RelativePath.class);
if (rp!=null)
name = rp.value()+'/'+name;
if (query.length()==0) query.append("+qs(this)");
if (name.equals("value")) {
// The special 'value' parameter binds to the the current field
query.append(".addThis()");
} else {
query.append(".nearBy('"+name+"')");
}
continue;
}
Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
if (m!=null) buildParameterList(m,query);
}
return query;
return method;
}
/**
......@@ -1019,11 +978,6 @@ public abstract class Descriptor<T extends Describable<T>> implements Saveable {
private static final Logger LOGGER = Logger.getLogger(Descriptor.class.getName());
/**
* Used in {@link #checkMethods} to indicate that there's no check method.
*/
private static final String NONE = "\u0000";
/**
* Special type indicating that {@link Descriptor} describes itself.
* @see Descriptor#Descriptor(Class)
......
......@@ -27,27 +27,38 @@ import hudson.EnvVars;
import hudson.Functions;
import hudson.Launcher;
import hudson.ProxyConfiguration;
import hudson.RelativePath;
import hudson.Util;
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.tasks.Builder;
import static hudson.Util.fixEmpty;
import hudson.util.ReflectionUtils.Parameter;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.Stapler;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static hudson.Functions.jsStringEscape;
import static hudson.Util.*;
/**
* Represents the result of the form field validation.
*
......@@ -506,4 +517,111 @@ public abstract class FormValidation extends IOException implements HttpResponse
rsp.setContentType("text/html;charset=UTF-8");
rsp.getWriter().print(html);
}
/**
* Builds up the check URL for the client-side JavaScript to call back.
*/
public static class CheckMethod {
private final Descriptor descriptor;
private final Method method;
private final String capitalizedFieldName;
/**
* Names of the parameters to pass from the client.
*/
private final List<String> names;
private volatile String checkUrl; // cached once computed
private volatile String dependsOn; // cached once computed
public CheckMethod(Descriptor descriptor, String fieldName) {
this.descriptor = descriptor;
this.capitalizedFieldName = StringUtils.capitalize(fieldName);
method = ReflectionUtils.getPublicMethodNamed(descriptor.getClass(), "doCheck" + capitalizedFieldName);
if(method !=null) {
names = new ArrayList<String>();
findParameters(method);
} else {
names = null;
}
}
/**
* Builds query parameter line by figuring out what should be submitted
*/
private void findParameters(Method method) {
for (Parameter p : ReflectionUtils.getParameters(method)) {
QueryParameter qp = p.annotation(QueryParameter.class);
if (qp!=null) {
String name = qp.value();
if (name.length()==0) name = p.name();
if (name==null || name.length()==0)
continue; // unknown parameter name. we'll report the error when the form is submitted.
if (name.equals("value"))
continue; // 'value' parameter is implicit
RelativePath rp = p.annotation(RelativePath.class);
if (rp!=null)
name = rp.value()+'/'+name;
names.add(name);
continue;
}
Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
if (m!=null) findParameters(m);
}
}
/**
* Obtains the 1.526-compatible single string representation.
*
* This method computes JavaScript expression, which evaluates to the URL that the client should request
* the validation to.
* A modern version depends on {@link #toStemUrl()} and {@link #getDependsOn()}
*/
public String toCheckUrl() {
if (names==null) return null;
if (checkUrl==null) {
StringBuilder buf = new StringBuilder(singleQuote(relativePath()));
if (!names.isEmpty()) {
buf.append("+qs(this).addThis()");
for (String name : names) {
buf.append(".nearBy('"+name+"')");
}
buf.append(".toString()");
}
checkUrl = buf.toString();
}
// put this under the right contextual umbrella.
// 'a' in getCurrentDescriptorByNameUrl is always non-null because we already have Hudson as the sentinel
return '\'' + jsStringEscape(Descriptor.getCurrentDescriptorByNameUrl()) + "/'+" + checkUrl;
}
/**
* Returns the URL that the JavaScript should hit to perform form validation, except
* the query string portion (which is built on the client side.)
*/
public String toStemUrl() {
if (names==null) return null;
return jsStringEscape(Descriptor.getCurrentDescriptorByNameUrl()) + '/' + relativePath();
}
public String getDependsOn() {
if (names==null) return null;
if (dependsOn==null)
dependsOn = join(names," ");
return dependsOn;
}
private String relativePath() {
return descriptor.getDescriptorUrl() + "/check" + capitalizedFieldName;
}
}
}
......@@ -61,7 +61,7 @@ THE SOFTWARE.
value="${attrs.value}"
title="${attrs.tooltip}"
onclick="${attrs.onclick}" id="${attrs.id}" class="${attrs.negative!=null ? 'negative' : null} ${attrs.checkUrl!=null?'validated':''}"
checkUrl="${attrs.checkUrl}" json="${attrs.json}"
checkUrl="${attrs.checkUrl}" checkDependsOn="${attrs.checkDependsOn}" json="${attrs.json}"
checked="${(attrs.checked ?: instance[attrs.field] ?: attrs.default) ? 'true' : null}"/>
<j:if test="${attrs.title!=null}">
<label class="attach-previous"
......
......@@ -49,7 +49,7 @@ THE SOFTWARE.
<input id="${attrs.id}" autocomplete="off" class="combobox ${attrs.clazz}${attrs.checkUrl!=null ? ' validated' : ''}"
name="${attrs.name ?: '_.'+attrs.field}"
value="${attrs.value ?: instance[attrs.field]}"
checkUrl="${attrs.checkUrl}" />
checkUrl="${attrs.checkUrl}" checkDependsOn="${attrs.checkDependsOn}" />
<div class="combobox-values">
<j:if test="${items!=null}">
<j:forEach var="v" items="${items}">
......
......@@ -33,6 +33,5 @@ THE SOFTWARE.
<!-- this looks up the ancestor <entry> set by entry.jelly -->
<j:set target="${pattrs}" property="field" value="${entry.field}" />
</j:if>
<j:set target="${pattrs}" property="checkUrl"
value="${h.getCheckUrl(pattrs.checkUrl,descriptor,pattrs.field)}" />
<j:expr value="${h.calcCheckUrl(pattrs, pattrs.checkUrl,descriptor,pattrs.field)}" />
</j:jelly>
\ No newline at end of file
......@@ -68,7 +68,7 @@ THE SOFTWARE.
id="${attrs.id}"
type="text"
readonly="readonly"
checkUrl="${attrs.checkUrl}" checkMethod="${attrs.checkMethod}"
checkUrl="${attrs.checkUrl}" checkDependsOn="${attrs.checkDependsOn}" checkMethod="${attrs.checkMethod}"
checkMessage="${attrs.checkMessage}"
onchange="${attrs.onchange}" onkeyup="${attrs.onkeyup}"/>
</j:jelly>
......@@ -82,7 +82,7 @@ THE SOFTWARE.
<textarea id="${attrs.id}" style="${attrs.style}"
name ="${attrs.name ?: '_.'+attrs.field}"
class="setting-input ${attrs.checkUrl!=null?'validated':''} ${attrs['codemirror-mode']!=null?'codemirror':''} ${attrs.class}"
checkUrl="${attrs.checkUrl}" checkMethod="${attrs.checkMethod}"
checkUrl="${attrs.checkUrl}" checkDependsOn="${attrs.checkDependsOn}" checkMethod="${attrs.checkMethod}"
rows="${h.determineRows(value)}"
readonly="${attrs.readonly}"
codemirror-mode="${attrs['codemirror-mode']}"
......
......@@ -567,6 +567,8 @@ public class JenkinsRule implements TestRule, MethodRule, RootAction {
SocketConnector connector = new SocketConnector();
connector.setHeaderBufferSize(12*1024); // use a bigger buffer as Stapler traces can get pretty large on deeply nested URL
if (System.getProperty("port")!=null)
connector.setPort(Integer.parseInt(System.getProperty("port")));
server.setThreadPool(new ThreadPoolImpl(new ThreadPoolExecutor(10, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),new ThreadFactory() {
public Thread newThread(Runnable r) {
......
package jenkins.bugs
import hudson.Launcher
import hudson.model.AbstractBuild
import hudson.model.AbstractProject
import hudson.model.BuildListener
import hudson.tasks.BuildStepDescriptor
import hudson.tasks.Builder
import hudson.util.FormValidation
import hudson.util.ListBoxModel
import org.junit.Rule
import org.junit.Test
import org.jvnet.hudson.test.Bug
import org.jvnet.hudson.test.JenkinsRule
import org.jvnet.hudson.test.TestExtension
import org.kohsuke.stapler.QueryParameter
import javax.inject.Inject
/**
*
*
* @author Kohsuke Kawaguchi
*/
class Jenkins19124Test {
@Rule
public JenkinsRule j = new JenkinsRule();
@Inject
DescriptorImpl d;
@Bug(19124)
@Test
public void interrelatedFormValidation() {
j.jenkins.injector.injectMembers(this);
def p = j.createFreeStyleProject();
p.buildersList.add(new Foo());
def wc = j.createWebClient();
def c = wc.getPage(p, "configure");
c.getElementByName("_.alpha").valueAttribute = "hello";
assert d.alpha=="hello";
assert d.bravo=="2";
c.getElementByName("_.bravo").setSelectedAttribute("1",true);
assert d.alpha=="hello";
assert d.bravo=="1";
}
public static class Foo extends Builder {
String getAlpha() { return "alpha"; }
String getBravo() { return "2"; }
@Override
boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
return true;
}
private Object writeReplace() { return new Object(); }
}
@TestExtension
public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
String alpha,bravo;
public DescriptorImpl() {
super(Foo.class)
}
@Override
String getDisplayName() {
return "---";
}
FormValidation doCheckAlpha(@QueryParameter String value, @QueryParameter String bravo) {
this.alpha = value;
this.bravo = bravo;
return FormValidation.ok();
}
ListBoxModel doFillBravoItems() {
return new ListBoxModel().add("1").add("2").add("3");
}
@Override
boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
}
}
package jenkins.bugs.Jenkins19124Test.Foo;
def f = namespace(lib.FormTagLib)
f.entry(field:"alpha") {
f.textbox()
}
f.entry(field:"bravo") {
f.select()
}
\ No newline at end of file
......@@ -409,10 +409,21 @@ var tooltip;
function registerValidator(e) {
e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
e.targetUrl = function() {
return eval(this.getAttribute("checkUrl")); // need access to 'this'
var url = this.getAttribute("checkUrl");
var depends = this.getAttribute("checkDependsOn");
if (depends==null) {// legacy behaviour where checkUrl is a JavaScript
return eval(url); // need access to 'this', so no 'geval'
} else {
var q = qs(this).addThis();
if (depends.length>0)
depends.split(" ").each(function (n) {
q.nearBy(n);
});
return url+ q.toString();
}
};
var method = e.getAttribute("checkMethod");
if (!method) method = "get";
var method = e.getAttribute("checkMethod") || "get";
var url = e.targetUrl();
try {
......@@ -441,6 +452,19 @@ function registerValidator(e) {
e.onchange = checker;
e.onblur = checker;
var v = e.getAttribute("checkDependsOn");
if (v) {
v.split(" ").each(function (name) {
var c = findNearBy(e,name);
if (c==null) {
if (window.console!=null) console.warn("Unable to find nearby "+name);
if (window.YUI!=null) YUI.log("Unable to find a nearby control of the name "+name,"warn")
return;
}
$(c).observe("change",checker.bind(e));
});
}
e = null; // avoid memory leak
}
......@@ -895,7 +919,7 @@ var jenkinsRules = {
"TR.optional-block-start": function(e) { // see optionalBlock.jelly
// set start.ref to checkbox in preparation of row-set-end processing
var checkbox = e.firstChild.firstChild;
var checkbox = e.down().down();
e.setAttribute("ref", checkbox.id = "cb"+(iota++));
},
......@@ -1001,7 +1025,7 @@ var jenkinsRules = {
// this is suffixed by a pointless string so that two processing for optional-block-start
// can sandwitch row-set-end
// this requires "TR.row-set-end" to mark rows
var checkbox = e.firstChild.firstChild;
var checkbox = e.down().down();
updateOptionalBlock(checkbox,false);
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册