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

Merged commit 55d1f1fe

Conflicts:
	core/src/main/resources/lib/form/optionalBlock.jelly
	core/src/main/resources/lib/hudson/newFromList/form.jelly
	test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
......@@ -26,6 +26,7 @@ package hudson.model;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import com.google.common.collect.Iterables;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import hudson.ExtensionPoint;
import hudson.PermalinkList;
......@@ -48,6 +49,7 @@ import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.CopyOnWriteList;
import hudson.util.DataSetBuilder;
import hudson.util.DescribableList;
import hudson.util.IOException2;
import hudson.util.RunList;
import hudson.util.ShiftedCategoryAxis;
......@@ -141,6 +143,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
/**
* List of {@link UserProperty}s configured for this project.
*/
// this should have been DescribableList but now it's too late
protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<JobProperty<? super JobT>>();
protected Job(ItemGroup parent, String name) {
......@@ -949,25 +952,19 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
keepDependencies = req.getParameter("keepDependencies") != null;
try {
properties.clear();
JSONObject json = req.getSubmittedForm();
if (req.getParameter("logrotate") != null)
logRotator = LogRotator.DESCRIPTOR.newInstance(req,json.getJSONObject("logrotate"));
else
logRotator = null;
int i = 0;
for (JobPropertyDescriptor d : JobPropertyDescriptor
.getPropertyDescriptors(Job.this.getClass())) {
String name = "jobProperty" + (i++);
JSONObject config = json.getJSONObject(name);
JobProperty prop = d.newInstance(req, config);
if (prop != null) {
prop.setOwner(this);
properties.add(prop);
}
DescribableList<JobProperty<?>, JobPropertyDescriptor> t = new DescribableList<JobProperty<?>, JobPropertyDescriptor>(NOOP,getAllProperties());
t.rebuild(req,json.optJSONObject("properties"),JobPropertyDescriptor.getPropertyDescriptors(Job.this.getClass()));
properties.clear();
for (JobProperty p : t) {
p.setOwner(this);
properties.add(p);
}
submit(req, rsp);
......
......@@ -26,6 +26,7 @@ package hudson.model;
import hudson.ExtensionPoint;
import hudson.Launcher;
import hudson.Plugin;
import hudson.model.Descriptor.FormException;
import hudson.model.queue.SubTask;
import hudson.tasks.BuildStep;
import hudson.tasks.Builder;
......@@ -36,6 +37,8 @@ import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.ExportedBean;
/**
......@@ -69,7 +72,7 @@ import org.kohsuke.stapler.export.ExportedBean;
* @since 1.72
*/
@ExportedBean
public abstract class JobProperty<J extends Job<?,?>> implements Describable<JobProperty<?>>, BuildStep, ExtensionPoint {
public abstract class JobProperty<J extends Job<?,?>> implements ReconfigurableDescribable<JobProperty<?>>, BuildStep, ExtensionPoint {
/**
* The {@link Job} object that owns this property.
* This value will be set by the Hudson code.
......@@ -169,6 +172,10 @@ public abstract class JobProperty<J extends Job<?,?>> implements Describable<Job
return Collections.emptyList();
}
public JobProperty<?> reconfigure(StaplerRequest req, JSONObject form) throws FormException {
return form==null ? null : getDescriptor().newInstance(req,form);
}
/**
* Contributes {@link SubTask}s to {@link AbstractProject#getSubTasks()}
*
......
/*
* The MIT License
*
* Copyright (c) 2011, 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.model;
import hudson.model.Descriptor.FormException;
import hudson.slaves.NodeProperty;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
/**
* Marks modern {@link Describable}s that allow the current instances to pass information down to the next
* instance when the configuration is submitted.
*
* <p>
* As the generics signature implies, it isn't up to individual {@link Describable} implementation to
* implement this. Rather, it is up to the designer of an extension point to mark the extension point
* as {@link ReconfigurableDescribable}, as it requires coordination at the owner of the extension point.
*
* <h1>Use Cases</h1>
* <h2>Invisible Property</h2>
* <p>
* This mechanism can be used to create an entirely invisible {@link Describable}, which is handy
* for {@link NodeProperty}, {@link JobProperty}, etc. To do so, define a descriptor with null
* {@linkplain Descriptor#getDisplayName() display name} and empty config.jelly to prevent it from
* showing up in the config UI, then implement {@link #reconfigure(StaplerRequest, JSONObject)}
* and simply return {@code this}.
*
* <h2>Passing some values without going through clients</h2>
* <p>
* Sometimes your {@link Describable} object may have some expensive objects that you might want to
* hand over to the next instance. This hook lets you do that.
*
* @author Kohsuke Kawaguchi
* @since 1.406
*/
public interface ReconfigurableDescribable<T extends ReconfigurableDescribable<T>> extends Describable<T> {
/**
* When a parent/owner object of a Describable gets a config form submission and instances are
* recreated, this method is invoked on the existing instance (meaning the 'this' reference
* points to the existing instance) to create a new instance to be added to the parent/owner object.
*
* <p>
* The default implementation of this should be the following:
* <pre>
* return form==null ? null : getDescriptor().newInstance(req, form);
* </pre>
*
* @param req
* The current HTTP request being processed.
* @param form
* JSON fragment that corresponds to this describable object.
* If the newly submitted form doesn't include a fragment for this describable
* (meaning the user has de-selected your descriptor), then this argument is null.
*
* @return
* The new instance. To not to create an instance of a describable, return null.
*/
T reconfigure(StaplerRequest req, JSONObject form) throws FormException;
}
......@@ -29,6 +29,8 @@ import hudson.ExtensionPoint;
import hudson.Util;
import hudson.model.Descriptor.FormException;
import hudson.model.Node.Mode;
import hudson.model.labels.LabelAtomProperty;
import hudson.model.labels.LabelAtomPropertyDescriptor;
import hudson.scm.ChangeLogSet.Entry;
import hudson.search.CollectionSearchIndex;
import hudson.search.SearchIndexBuilder;
......@@ -205,6 +207,19 @@ public abstract class View extends AbstractModelObject implements AccessControll
return properties;
}
/**
* Returns all the {@link LabelAtomPropertyDescriptor}s that can be potentially configured
* on this label.
*/
public List<ViewPropertyDescriptor> getApplicablePropertyDescriptors() {
List<ViewPropertyDescriptor> r = new ArrayList<ViewPropertyDescriptor>();
for (ViewPropertyDescriptor pd : ViewProperty.all()) {
if (pd.isEnabledFor(this))
r.add(pd);
}
return r;
}
public void save() throws IOException {
// persistence is a part of the owner
// due to initialization timing issue, it can be null when this method is called
......@@ -620,25 +635,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
JSONObject json = req.getSubmittedForm();
List<ViewProperty> props = new ArrayList<ViewProperty>();
int i = 0;
for (ViewPropertyDescriptor d: ViewProperty.all()) {
ViewProperty p = properties.get(d.clazz);
JSONObject o = json.optJSONObject("viewProperty" + (i++));
if (o != null) {
if (p != null) {
p = p.reconfigure(req, o);
} else {
p = d.newInstance(req, o);
}
}
if (p != null) {
props.add(p);
}
}
properties.replaceBy(props);
properties.rebuild(req, req.getSubmittedForm(), getApplicablePropertyDescriptors());
save();
......
......@@ -25,9 +25,14 @@ package hudson.model;
import hudson.DescriptorExtensionList;
import hudson.ExtensionPoint;
import hudson.console.ConsoleAnnotator;
import hudson.console.ConsoleAnnotatorFactory;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import java.util.ArrayList;
import java.util.List;
/**
* Extensible property of {@link View}.
*
......@@ -45,7 +50,7 @@ import org.kohsuke.stapler.StaplerRequest;
* @author Stephen Connolly
* @since 1.406
*/
public class ViewProperty implements Describable<ViewProperty>, ExtensionPoint {
public class ViewProperty implements ReconfigurableDescribable<ViewProperty>, ExtensionPoint {
/**
* The view object that owns this property.
* This value will be set by the core code.
......@@ -66,7 +71,6 @@ public class ViewProperty implements Describable<ViewProperty>, ExtensionPoint {
}
public ViewProperty reconfigure(StaplerRequest req, JSONObject form) throws Descriptor.FormException {
return getDescriptor().newInstance(req, form);
return form==null ? null : getDescriptor().newInstance(req, form);
}
}
......@@ -53,7 +53,9 @@ public abstract class ViewPropertyDescriptor extends Descriptor<ViewProperty> {
* @return null
* if the implementation choose not to add any property object for such view.
*/
public abstract ViewProperty newInstance(View view);
public ViewProperty newInstance(View view) {
return null;
}
/**
* Whether or not the described property is enabled in the current context.
......@@ -67,8 +69,11 @@ public abstract class ViewPropertyDescriptor extends Descriptor<ViewProperty> {
* <p>
* This mechanism is useful if the availability of the property is
* contingent of some other settings.
*
* @param view
* View for which this property is considered. Never null.
*/
public boolean isEnabled() {
public boolean isEnabledFor(View view) {
return true;
}
}
......@@ -36,8 +36,10 @@ import hudson.model.DependencyGraph;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.ReconfigurableDescribable;
import hudson.model.Saveable;
import net.sf.json.JSONObject;
import org.apache.avalon.framework.configuration.Reconfigurable;
import org.kohsuke.stapler.StaplerRequest;
import java.io.IOException;
......@@ -141,11 +143,23 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
List<T> newList = new ArrayList<T>();
for (Descriptor<T> d : descriptors) {
T existing = get((D)d);
String name = d.getJsonSafeClassName();
if (json.has(name)) {
T instance = d.newInstance(req, json.getJSONObject(name));
newList.add(instance);
JSONObject o = json.optJSONObject(name);
T instance = null;
if (o!=null) {
if (existing instanceof ReconfigurableDescribable)
instance = (T)((ReconfigurableDescribable)existing).reconfigure(req,o);
else
instance = d.newInstance(req, o);
} else {
if (existing instanceof ReconfigurableDescribable)
instance = (T)((ReconfigurableDescribable)existing).reconfigure(req,null);
}
if (instance!=null)
newList.add(instance);
}
replaceBy(newList);
......
......@@ -52,18 +52,8 @@ THE SOFTWARE.
</f:optionalBlock>
</j:if>
<!-- job property configurations -->
<j:set var="instances" value="${it.properties}" />
<j:forEach var="d" items="${h.getJobPropertyDescriptors(it.getClass())}" varStatus="loop">
<j:scope>
<j:set var="descriptor" value="${d}" />
<j:set var="instance" value="${instances[d]}" />
<f:rowSet name="jobProperty${loop.index}">
<st:include from="${d}" page="${d.configPage}" optional="true"/>
</f:rowSet>
</j:scope>
</j:forEach>
<!-- job property configurations. This should have been <f:descriptorList> -->
<f:descriptorList field="properties" descriptors="${h.getJobPropertyDescriptors(it.getClass())}" forceRowSet="true" />
<!-- additional entries from derived classes -->
<st:include page="configure-entries.jelly" />
......
......@@ -51,20 +51,7 @@ THE SOFTWARE.
<st:include page="configure-entries.jelly" optional="true" />
<!-- view property configurations -->
<j:invokeStatic var="descriptors" className="hudson.model.ViewProperty" method="all" />
<j:set var="instances" value="${it.properties}" />
<j:forEach var="d" items="${descriptors}" varStatus="loop">
<j:if test="${d.enabled}">
<f:section title="${d.displayName}">
<j:set var="descriptor" value="${d}" />
<j:set var="instance" value="${instances[d]}" />
<f:rowSet name="viewProperty${loop.index}">
<st:include from="${d}" page="${d.configPage}"/>
</f:rowSet>
</f:section>
</j:if>
</j:forEach>
<f:descriptorList descriptors="${it.getApplicablePropertyDescriptors()}" instances="${it.properties}" />
<f:block>
<f:submit value="OK" />
......
......@@ -59,9 +59,6 @@ THE SOFTWARE.
checkUrl="${attrs.checkUrl}" json="${attrs.json}"
checked="${(attrs.checked ?: instance[attrs.field] ?: attrs.default) ? 'true' : null}"/>
<j:if test="${attrs.title!=null}">
<label class="attach-previous">
<j:whitespace> </j:whitespace>
${attrs.title}
</label>
<label class="attach-previous">${attrs.title}</label>
</j:if>
</j:jelly>
\ No newline at end of file
......@@ -50,6 +50,9 @@ THE SOFTWARE.
the type for which descriptors will be configured.
default to ${it.class}
</st:attribute>
<st:attribute name="forceRowSet">
If specified, instead of a sequence of &lt;f:optionalBlock>s, draw a sequence of &lt;rowSet>s.
</st:attribute>
</st:documentation>
<j:if test="${attrs.field==null}">
......@@ -73,7 +76,7 @@ THE SOFTWARE.
<d:invokeBody />
<j:forEach var="d" items="${descriptors}">
<f:optionalBlock name="${d.jsonSafeClassName}" help="${d.helpFile}"
title="${d.displayName}" checked="${instances.get(d)!=null}">
title="${attrs.forceRowSet!=null?null:d.displayName}" checked="${instances.get(d)!=null}">
<j:set var="descriptor" value="${d}" />
<j:set var="instance" value="${instances.get(d)}" />
......
......@@ -35,6 +35,10 @@ THE SOFTWARE.
</st:attribute>
<st:attribute name="title">
Human readable text that follows the checkbox.
If this field is null, the checkbox degrades to a &lt;f:rowSet>, which provides
a grouping at JSON level but on the UI there's no checkbox (and you always see
the body of it.)
</st:attribute>
<st:attribute name="field">
Used for databinding. TBD. Either this or @name/@title combo is required.
......@@ -59,21 +63,31 @@ THE SOFTWARE.
value="${descriptor.getHelpFile(attrs.field)}" />
</j:if>
<tr class="optional-block-start ${attrs.inline?'':'row-set-start'}" hasHelp="${attrs.help!=null}"><!-- this ID marks the beginning -->
<td colspan="3">
<f:checkbox name="${attrs.name}" onclick="javascript:updateOptionalBlock(this,true)"
negative="${attrs.negative}" checked="${attrs.checked}" field="${attrs.field}" title="${title}" />
</td>
<j:if test="${attrs.help!=null}">
<td>
<a href="#" class="help-button" helpURL="${rootURL}${attrs.help}"><img src="${imagesURL}/16x16/help.gif" alt="Help for feature: ${title}" height="16" width="16" /></a>
</td>
</j:if>
</tr>
<j:if test="${attrs.help!=null}">
<f:helpArea />
</j:if>
<d:invokeBody />
<!-- end marker -->
<tr class="${attrs.inline?'':'row-set-end'} optional-block-end" />
<j:choose>
<j:when test="${attrs.title!=null}">
<tr class="optional-block-start ${attrs.inline?'':'row-set-start'}" hasHelp="${attrs.help!=null}"><!-- this ID marks the beginning -->
<td colspan="3">
<f:checkbox name="${attrs.name}" onclick="javascript:updateOptionalBlock(this,true)"
negative="${attrs.negative}" checked="${attrs.checked}" field="${attrs.field}" title="${title}" />
</td>
<j:if test="${attrs.help!=null}">
<td>
<a href="#" class="help-button" helpURL="${rootURL}${attrs.help}"><img src="${imagesURL}/16x16/help.gif" alt="Help for feature: ${title}" height="16" width="16" /></a>
</td>
</j:if>
</tr>
<j:if test="${attrs.help!=null}">
<f:helpArea />
</j:if>
<d:invokeBody />
<!-- end marker -->
<tr class="${attrs.inline?'':'row-set-end'} optional-block-end" />
</j:when>
<j:otherwise>
<f:rowSet name="${attrs.name}">
<d:invokeBody />
</f:rowSet>
</j:otherwise>
</j:choose>
</j:jelly>
......@@ -52,7 +52,6 @@ THE SOFTWARE.
<td colspan="3">
<input type="radio" name="${name}" value="${value}"
class="radio-block-control" checked="${checked?'true':null}" />
<st:nbsp/>
<label class="attach-previous">${title}</label>
</td>
<j:if test="${attrs.help!=null}">
......
......@@ -45,7 +45,6 @@ THE SOFTWARE.
<j:forEach var="descriptor" items="${descriptors}">
<s:block>
<input type="radio" name="mode" value="${descriptor.class.name}" onchange="updateOk(this.form)" onclick="updateOk(this.form)" />
<st:nbsp/>
<label class="attach-previous"><b>${descriptor.displayName}</b></label>
</s:block>
<s:entry>
......@@ -56,7 +55,6 @@ THE SOFTWARE.
<j:if test="${!empty(attrs.copyNames) or attrs.showCopyOption}">
<s:block>
<input type="radio" id="copy" name="mode" value="copy" onchange="updateOk(this.form)" onclick="updateOk(this.form)" />
<st:nbsp/>
<label class="attach-previous"><b>${attrs.copyTitle}</b></label>
</s:block>
<s:entry>
......
......@@ -773,7 +773,7 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
* This is useful during debugging a test so that one can inspect the state of Hudson through the web browser.
*/
public void interactiveBreak() throws Exception {
System.out.println("Hudson is running at http://localhost:"+localPort+"/");
System.out.println("Jenkins is running at http://localhost:"+localPort+"/");
new BufferedReader(new InputStreamReader(System.in)).readLine();
}
......@@ -854,6 +854,11 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
submit(createWebClient().goTo(u.getUrl()+"/configure").getFormByName("config"));
return u;
}
protected <V extends View> V configRoundtrip(V view) throws Exception {
submit(createWebClient().getPage(view, "configure").getFormByName("viewConfig"));
return view;
}
/**
......
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, 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
......@@ -27,13 +27,20 @@ import com.gargoylesoftware.htmlunit.WebAssert;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.matrix.MatrixProject;
import hudson.maven.MavenModuleSet;
import hudson.model.Descriptor.FormException;
import net.sf.json.JSONObject;
import org.jvnet.hudson.test.Bug;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
public class JobPropertyTest extends HudsonTestCase {
/**
* Asserts that rfe#2398 is fixed.
*/
@Bug(2398)
public void testJobPropertySummaryIsShownInMavenModuleSetIndexPage() throws Exception {
assertJobPropertySummaryIsShownInIndexPage(MavenModuleSet.DESCRIPTOR);
}
......@@ -54,7 +61,6 @@ public class JobPropertyTest extends HudsonTestCase {
}
public static class JobPropertyImpl extends JobProperty<Job<?,?>> {
public static DescriptorImpl DESCRIPTOR = new DescriptorImpl();
private final String propertyString;
public JobPropertyImpl(String propertyString) {
this.propertyString = propertyString;
......@@ -64,13 +70,8 @@ public class JobPropertyTest extends HudsonTestCase {
return propertyString;
}
@Override
public JobPropertyDescriptor getDescriptor() {
return DESCRIPTOR;
}
@SuppressWarnings("unchecked")
private static class DescriptorImpl extends JobPropertyDescriptor {
@TestExtension
public static class DescriptorImpl extends JobPropertyDescriptor {
@Override
public boolean isApplicable(Class<? extends Job> jobType) {
return false;
......@@ -82,4 +83,60 @@ public class JobPropertyTest extends HudsonTestCase {
}
}
}
/**
* Make sure that the UI rendering works as designed.
*/
public void testConfigRoundtrip() throws Exception {
FreeStyleProject p = createFreeStyleProject();
JobPropertyWithConfigImpl before = new JobPropertyWithConfigImpl("Duke");
p.addProperty(before);
configRoundtrip(p);
JobPropertyWithConfigImpl after = p.getProperty(JobPropertyWithConfigImpl.class);
assertNotSame(after,before);
assertEqualDataBoundBeans(before, after);
}
public static class JobPropertyWithConfigImpl extends JobProperty<Job<?,?>> {
public String name;
@DataBoundConstructor
public JobPropertyWithConfigImpl(String name) {
this.name = name;
}
@TestExtension("testConfigRoundtrip")
public static class DescriptorImpl extends JobPropertyDescriptor {
@Override
public String getDisplayName() { return null; }
}
}
public void testInvisibleProperty() throws Exception {
FreeStyleProject p = createFreeStyleProject();
InvisibleImpl before = new InvisibleImpl();
p.addProperty(before);
configRoundtrip(p);
InvisibleImpl after = p.getProperty(InvisibleImpl.class);
assertSame(after,before);
}
public static class InvisibleImpl extends JobProperty<Job<?,?>> {
public String name;
InvisibleImpl() {}
@Override
public JobProperty<?> reconfigure(StaplerRequest req, JSONObject form) throws FormException {
return this;
}
@TestExtension("testInvisibleProperty")
public static class DescriptorImpl extends JobPropertyDescriptor {
@Override
public String getDisplayName() { return null; }
}
}
}
/*
* The MIT License
*
* Copyright (c) 2011, 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.model;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlLabel;
import hudson.model.Descriptor.FormException;
import net.sf.json.JSONObject;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
/**
* @author Kohsuke Kawaguchi
*/
public class ViewPropertyTest extends HudsonTestCase {
public void testRoundtrip() throws Exception {
ListView foo = new ListView("foo");
hudson.addView(foo);
// make sure it renders as optionalBlock
HtmlForm f = createWebClient().getPage(foo, "configure").getFormByName("viewConfig");
((HtmlLabel)f.selectSingleNode(".//LABEL[text()='Debug Property']")).click();
submit(f);
ViewPropertyImpl vp = foo.getProperties().get(ViewPropertyImpl.class);
assertEquals("Duke",vp.name);
// make sure it roundtrips correctly
vp.name = "Kohsuke";
configRoundtrip(foo);
ViewPropertyImpl vp2 = foo.getProperties().get(ViewPropertyImpl.class);
assertNotSame(vp,vp2);
assertEqualDataBoundBeans(vp,vp2);
}
public static class ViewPropertyImpl extends ViewProperty {
public String name;
@DataBoundConstructor
public ViewPropertyImpl(String name) {
this.name = name;
}
@TestExtension
public static class DescriptorImpl extends ViewPropertyDescriptor {
@Override
public String getDisplayName() {
return "Debug Property";
}
}
}
public void testInvisibleProperty() throws Exception {
ListView foo = new ListView("foo");
hudson.addView(foo);
// test the rendering (or the lack thereof) of an invisible property
configRoundtrip(foo);
assertNull(foo.getProperties().get(InvisiblePropertyImpl.class));
// do the same but now with a configured instance
InvisiblePropertyImpl vp = new InvisiblePropertyImpl();
foo.getProperties().add(vp);
configRoundtrip(foo);
assertSame(vp,foo.getProperties().get(InvisiblePropertyImpl.class));
}
public static class InvisiblePropertyImpl extends ViewProperty {
InvisiblePropertyImpl() {
}
@Override
public ViewProperty reconfigure(StaplerRequest req, JSONObject form) throws FormException {
return this;
}
@TestExtension
public static class DescriptorImpl extends ViewPropertyDescriptor {
@Override
public String getDisplayName() {
return null;
}
}
}
}
<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">
<f:entry field="name" title="${%Name}">
<f:textbox/>
</f:entry>
</j:jelly>
<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">
<f:entry field="name" title="${%Name}">
<f:textbox default="Duke"/>
</f:entry>
</j:jelly>
......@@ -456,6 +456,10 @@ div.behavior-loading {
filter: alpha(opacity=50);
}
LABEL.attach-previous {
margin-left: 0.5em;
}
/* ======================== error/warning message (mainly in the form.) Use them on block elements ======================== */
.error {
color: #CC0000;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册