Question: This doesn't seem to work.
#Post("/register")
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#View("register")
#Error(exception = ConstraintViolationException.class)
def register(HttpRequest<?> request, ConstraintViolationException constraintViolationException) {
Optional<RegisterFormData> registerFormDataOptional = request.getBody(RegisterFormData.class)
Map<String, Object> map = new HashMap<>()
if(registerFormDataOptional.isPresent()){
RegisterRequest registerRequest = new RegisterRequest(registerFormDataOptional.get().properties)
registerRequest.returnSecureToken = true
try {
def registerResponse = firebaseClient.register(registerRequest, this.firebaseApiKey).blockingSingle()
SendEmailVerificationRequest sendEmailVerificationRequest = new SendEmailVerificationRequest()
sendEmailVerificationRequest.requestType = 'VERIFY_EMAIL'
sendEmailVerificationRequest.idToken = registerResponse.idToken
firebaseClient.sendEmailVerification(sendEmailVerificationRequest, this.firebaseApiKey)
HttpResponse.redirect(URI.create('/register-success'))
}catch(HttpClientResponseException ex){
map.put('errors', [ex.message])
return map
}
}else{
map.put('errors', violationMessageSource.violationsMessages(constraintViolationException.constraintViolations))
return map
}
}
gives me a
{"message":"Required argument [ConstraintViolationException constraintViolationException] not specified","path":"/constraintViolationException","_links":{"self":{"href":"/auth/register","templated":false}}}
Currently using Micronaut and Thymeleaf. Anyone know what else am I missing? I was following the examples from https://guides.micronaut.io/micronaut-error-handling/guide/index.html
The whole point here is to pass some error messages from the controller back to the UI when constraint violations happen. The default one that uses annotations #Body and #Valid don't work since it returns json errors without any views.
#Inject
Validator validator
#Inject
ViolationMessageSource violationMessageSource
#Post("/register")
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
def register(HttpRequest<?> request, #Body RegisterFormData registerFormData) {
//validate registerformdata object
Map<String, Object> map = new HashMap<>()
Set<ConstraintViolation<RegisterFormData>> violations = validator.validate(registerFormData)
if (violations.size() > 0) {
map.put('registerFormData', registerFormData)
map.put('errors', violationMessageSource.violationsMessages(violations))
HttpResponse.redirect(URI.create('/register')).body(map)
} else {
RegisterRequest registerRequest = new RegisterRequest(registerFormData.properties)
registerRequest.returnSecureToken = true
try {
def registerResponse = firebaseClient.register(registerRequest, this.firebaseApiKey).blockingSingle()
SendEmailVerificationRequest sendEmailVerificationRequest = new SendEmailVerificationRequest()
sendEmailVerificationRequest.requestType = 'VERIFY_EMAIL'
sendEmailVerificationRequest.idToken = registerResponse.idToken
firebaseClient.sendEmailVerification(sendEmailVerificationRequest, this.firebaseApiKey)
HttpResponse.redirect(URI.create('/register-success'))
} catch (HttpClientResponseException ex) {
map.put('errors', [ex.message])
HttpResponse.redirect(URI.create('/register')).body(map)
}
}
}
where I have injected a validator bean in Micronaut like so
#Factory
class ValidatorConfig {
Validator validator
#PostConstruct
void initialize(){
ValidatorFactory factory = Validation.buildDefaultValidatorFactory()
validator = factory.getValidator()
}
#Bean
Validator getValidator(){
return validator
}
}
and my message source like so
#Singleton
public class ViolationMessageSource {
public List<String> violationsMessages(Set<ConstraintViolation<?>> violations) {
return violations.stream()
.map(ViolationMessageSource::violationMessage)
.collect(Collectors.toList());
}
private static String violationMessage(ConstraintViolation violation) {
StringBuilder sb = new StringBuilder();
Path.Node lastNode = lastNode(violation.getPropertyPath());
if (lastNode != null) {
sb.append(lastNode.getName());
sb.append(" ");
}
sb.append(violation.getMessage());
return sb.toString();
}
private static Path.Node lastNode(Path path) {
Path.Node lastNode = null;
for (final Path.Node node : path) {
lastNode = node;
}
return lastNode;
}
}
The answers are based on the fundamentals on javax validation https://www.baeldung.com/javax-validation and error handling in micronaut https://guides.micronaut.io/micronaut-error-handling/guide/index.html
Related
I'm trying to figure out how to skip serializing empty collections using YamlDotNet. I have experimented with both a custom ChainedObjectGraphVisitor and IYamlTypeConverter. I'm new to using YamlDotNet and have some knowledge gaps here.
Below is my implementation for the visitor pattern, which results in a YamlDotNet.Core.YamlException "Expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, got MappingEnd" error. I do see some online content for MappingStart/MappingEnd, but I'm not sure how it fits into what I'm trying to do (eliminate clutter from lots of empty collections). Any pointers in the right direction are appreciated.
Instantiating the serializer:
var serializer = new YamlDotNet.Serialization.SerializerBuilder()
.WithNamingConvention(new YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention())
.WithEmissionPhaseObjectGraphVisitor(args => new YamlIEnumerableSkipEmptyObjectGraphVisitor(args.InnerVisitor))
.Build();
ChainedObjectGraphVisitor implementation:
public sealed class YamlIEnumerableSkipEmptyObjectGraphVisitor : ChainedObjectGraphVisitor
{
public YamlIEnumerableSkipEmptyObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
: base(nextVisitor)
{
}
public override bool Enter(IObjectDescriptor value, IEmitter context)
{
bool retVal;
if (typeof(System.Collections.IEnumerable).IsAssignableFrom(value.Value.GetType()))
{ // We have a collection
var enumerableObject = (System.Collections.IEnumerable)value.Value;
if (enumerableObject.GetEnumerator().MoveNext()) // Returns true if the collection is not empty.
{ // Serialize it as normal.
retVal = base.Enter(value, context);
}
else
{ // Skip this item.
retVal = false;
}
}
else
{ // Not a collection, normal serialization.
retVal = base.Enter(value, context);
}
return retVal;
}
}
I believe the answer is to also override the EnterMapping() method in the base class with logic that is similar to what was done in the Enter() method:
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
{
bool retVal = false;
if (value.Value == null)
return retVal;
if (typeof(System.Collections.IEnumerable).IsAssignableFrom(value.Value.GetType()))
{ // We have a collection
var enumerableObject = (System.Collections.IEnumerable)value.Value;
if (enumerableObject.GetEnumerator().MoveNext()) // Returns true if the collection is not empty.
{ // Don't skip this item - serialize it as normal.
retVal = base.EnterMapping(key, value, context);
}
// Else we have an empty collection and the initialized return value of false is correct.
}
else
{ // Not a collection, normal serialization.
retVal = base.EnterMapping(key, value, context);
}
return retVal;
}
I ended up with the following class:
using System.Collections;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.ObjectGraphVisitors;
sealed class YamlIEnumerableSkipEmptyObjectGraphVisitor : ChainedObjectGraphVisitor
{
public YamlIEnumerableSkipEmptyObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor): base(nextVisitor)
{
}
private bool IsEmptyCollection(IObjectDescriptor value)
{
if (value.Value == null)
return true;
if (typeof(IEnumerable).IsAssignableFrom(value.Value.GetType()))
return !((IEnumerable)value.Value).GetEnumerator().MoveNext();
return false;
}
public override bool Enter(IObjectDescriptor value, IEmitter context)
{
if (IsEmptyCollection(value))
return false;
return base.Enter(value, context);
}
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
{
if (IsEmptyCollection(value))
return false;
return base.EnterMapping(key, value, context);
}
}
you can specify DefaultValuesHandling
in the serializer:
var serializer = new SerializerBuilder()
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitEmptyCollections)
.Build();
or in an attribute YamlMember for a field/property:
public class MyDtoClass
{
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitEmptyCollections)]
public List<string> MyCollection;
}
I just wrote my own data Provider, which should read a file in chunks and provides it to my spock specification.
While debugging the next() method returns a proper batch and the hasNext() returns false if the reader can not read any more lines.
But I get this exception: SpockExecutionException: Data provider has no data
Here is my Provider and my feature
class DumpProvider implements Iterable<ArrayList<String>> {
private File fileHandle
private BufferedReader fileReader
private ArrayList<String> currentBatch = new ArrayList<String>()
private int chunksize
private boolean hasNext = true
DumpProvider(String pathToFile, int chunksize) {
this.chunksize = chunksize
this.fileHandle = new File(pathToFile)
this.fileReader = this.fileHandle.newReader()
}
#Override
Iterator iterator() {
new Iterator<ArrayList<String>>() {
#Override
boolean hasNext() {
if (hasNext) {
String nextLine = fileReader.readLine()
if (nextLine != null) {
currentBatch.push(nextLine)
} else {
hasNext = false
fileReader.close()
fileHandle = null
}
}
return hasNext
}
#Override
ArrayList<String> next() {
(chunksize - currentBatch.size()).times {
String line = fileReader.readLine()
if (line != null) {
currentBatch.push(line)
}
}
def batch = new ArrayList<String>(currentBatch)
currentBatch = new ArrayList<String>()
return batch
}
#Override
void remove() {
throw new UnsupportedOperationException();
}
}
}
}
Spock Feature
def "small import"() {
when:
println 'test'
println profileJSONStrings
connector.insertMultiple(profileJSONStrings as ArrayList<String>)
then:
println "hello"
where:
profileJSONStrings << dataProvider
}
I have a cacheBean written in Java. I am successfully pulling out Vectors using EL, but I have a HashMap and when I try to access a value I throw an error.
My cacheBean is:
package com.scoular.cache;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Vector;
import org.openntf.domino.utils.Factory;
import org.openntf.domino.Database;
import org.openntf.domino.Session;
import org.openntf.domino.View;
import org.openntf.domino.ViewEntry;
import org.openntf.domino.ViewNavigator;
public class PCConfig implements Serializable {
private static final long serialVersionUID = 1L;
private Database thisDB;
private Database compDirDB;
public Database PCDataDB;
public HashMap<Integer, String> status = new HashMap<Integer, String>();
public static Vector<Object> geoLocations = new Vector<Object>();
public static Vector<Object> models = new Vector<Object>();
// #SuppressWarnings("unchecked")
private void initConfigData() {
try {
getStatus();
getGeoLocations();
getModels();
} catch (Exception e) {
e.printStackTrace();
}
}
public PCConfig() {
// initialize application config
System.out.println("Starting CacheBean");
initConfigData();
System.out.println("Ending CacheBean");
}
public static void setModels(Vector<Object> models) {
PCConfig.models = models;
}
public void getStatus() {
status.put(1, "In Inventory");
status.put(2, "Being Built");
status.put(3, "In Production");
status.put(4, "Aquiring PC");
status.put(5, "Decommissioning");
}
public Vector<Object> getGeoLocations() {
if (PCConfig.geoLocations == null || PCConfig.geoLocations.isEmpty()) {
try {
Session session = Factory.getSession();
thisDB = session.getCurrentDatabase();
compDirDB = session.getDatabase(thisDB.getServer(), "compdir.nsf", false);
View geoView = compDirDB.getView("xpGeoLocationsByName");
for (ViewEntry ce : geoView.getAllEntries()) {
Vector<Object> rowVal = ce.getColumnValues();
geoLocations.addElement(rowVal.elementAt(0));
}
} catch (Exception e) {
e.printStackTrace();
}
}
return geoLocations;
}
public Vector<Object> getModels() {
if (PCConfig.models == null || PCConfig.models.isEmpty()) {
try {
Session session = Factory.getSession();
thisDB = session.getCurrentDatabase();
PCDataDB = session.getDatabase(thisDB.getServer(), "scoApps\\PC\\PCData.nsf", false);
ViewNavigator vn = PCDataDB.getView("dbLookupModels").createViewNav();
ViewEntry entry = vn.getFirstDocument();
while (entry != null) {
Vector<Object> thisCat = entry.getColumnValues();
if (entry.isCategory()) {
String thisCatString = thisCat.elementAt(0).toString();
models.addElement(thisCatString);
}
entry = vn.getNext(entry);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return models;
}
}
and the code to grab the a value is:
<xp:text escape="true" id="computedField2">
<xp:this.value><![CDATA[#{PCConfig.status[0]}]]></xp:this.value></xp:text>
Your method getStatus() has to return the HashMap.
public HashMap<Integer, String> getStatus() {
...
return status;
}
In addition, #{PCConfig.status[0]} tries to read the value for key 0. There is no key 0 in your HashMap status though...
As far I know you should do as follow
#{javascript:PCConfig.status.get(1)}
I am facing a problem when initializing a List with new ArrayList<E>(). This is my class:-
#ManagedBean(name = "clientBean")
#SessionScoped
public class ClientBean {
public String msg = "", input, pendingMsg = "", user = "";
public GossipClient client;
public boolean inputDisabled = true, sendBtnDisabled = true, startBtnDisabled = false;
String key = null;
public List<ClientBeanDto> dtos;
ClientBeanDto dto;
String style;
public List<OnlineList> onlineList;
OnlineList userInfo;
OnlineList newUser = null;
public void start() throws UnknownHostException, IOException {
client = new GossipClient(this);
if (user != null && user != "" && client.connect()) {
try {
BufferedReader reader = new BufferedReader(new FileReader("D://key.txt"));
key = reader.readLine();
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
client.start();
inputDisabled = sendBtnDisabled = false;
startBtnDisabled = true;
msg = pendingMsg = "";
pendingMsg = user + ", you are connected!";
client.sendNick(key + user);
onlineList = new ArrayList<OnlineList>();
dtos = new ArrayList<ClientBeanDto>();
}
.
.
.
}
The problem is with public List<OnlineList> onlineList. In start() method when the line onlineList = new ArrayList<OnlineList>(); is executed, in debugging mode I find its value null whereas a similar object dtos in the very next line gets initialized and assigned a id successfully.
I can't seem to find any reason for this behavior and I am not getting any error/exception. Any wisdom will be appreciated.
UPDATE: Found out that the list is becoming null whenever a button of xhtml (which the bean is backing) is clicked. I cannot find the reason though. The bean is SessionScoped and the other list(dtos) object is retained.
I want to add repeatable properties to the Jenkins plugin I'm developing, and created a test plugin to make make sure I was using them correctly. My plugin seems to work fine, I can add as many properties as I want when I originally edit the config, and it saves and builds. However, when I try to edit the config a second time, the config screen shows the loading overlay endlessly. If I scroll down, I can see the properties I saved earlier are still there, but I can't edit anything.
My class looks like this:
public class RepeatableTest extends Builder {
private List<Prop> property = new ArrayList<Prop>();
#DataBoundConstructor
public RepeatableTest(List<Prop> property) {
this.property = property;
}
public List<Prop> getProperty() {
return property;
}
#Override
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException {
listener.getLogger().println(property.get(0).name);
listener.getLogger().println(property.size());
return true;
}
#Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}
public static class Prop extends AbstractDescribableImpl<Prop> {
public String name;
public String getName(){
return name;
}
#DataBoundConstructor
public Prop(String name) {
this.name = name;
}
#Extension
public static class DescriptorImpl extends Descriptor<Prop> {
#Override
public String getDisplayName() {
return "";
}
}
}
#Extension // This indicates to Jenkins that this is an implementation of an extension point.
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
private String phpLoc;
public DescriptorImpl() {
load();
}
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
// Indicates that this builder can be used with all kinds of project types
return true;
}
public String getDisplayName() {
return "Repeatable Test";
}
#Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
phpLoc = formData.getString("phpLoc");
save();
return super.configure(req,formData);
}
public String getPhpLoc() {
return phpLoc;
}
}
}
My config.groovy looks like this:
package uitestplugin.uitest.RepeatableTest;
import lib.JenkinsTagLib
import lib.FormTagLib
def f = namespace(lib.FormTagLib)
t=namespace(JenkinsTagLib.class)
f.form{
f.entry(title:"Properties"){
f.repeatableProperty(field:"property")
}
}
and my prop/config.groovy looks like this:
package uitestplugin.uitest.RepeatableTest.Prop;
def f = namespace(lib.FormTagLib)
f.entry(title:"Name", field:"name") {
f.textbox()
}
The config.xml:
<?xml version='1.0' encoding='UTF-8'?>
<project>
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders>
<uitestplugin.uitest.RepeatableTest plugin="ui-test#1.0-SNAPSHOT">
<property>
<uitestplugin.uitest.RepeatableTest_-Prop>
<name>Prop1</name>
</uitestplugin.uitest.RepeatableTest_-Prop>
<uitestplugin.uitest.RepeatableTest_-Prop>
<name>Prop2</name>
</uitestplugin.uitest.RepeatableTest_-Prop>
</property>
</uitestplugin.uitest.RepeatableTest>
</builders>
<publishers/>
<buildWrappers/>
</project>
Any ideas as to what could cause this? I based a lot of the code from the ui-samples plugin (https://wiki.jenkins-ci.org/display/JENKINS/UI+Samples+Plugin).
EDIT: The current status of this is, well, I still haven't figured it out. I've done more research and tried tons of different examples, but the farthest I ever get is what I described above. It almost seems like you can't use repeatable through groovy. Anyways, I have one more piece of information to add. Using the web developer toolbar for Firefox, I can see that there is a Javascript error on the page. The error is:
Timestamp: 10/3/2014 12:58:49 PM
Error: TypeError: prototypes is undefined
Source File: http://localhost:8080/adjuncts/e58fb488/lib/form/hetero-list/hetero-list.js
Line: 16
And the code this relates to is(I've marked line 16 with a comment at the end of the line):
// #include lib.form.dragdrop.dragdrop
// do the ones that extract innerHTML so that they can get their original HTML before
// other behavior rules change them (like YUI buttons.)
Behaviour.specify("DIV.hetero-list-container", 'hetero-list', -100, function(e) {
e=$(e);
if(isInsideRemovable(e)) return;
// components for the add button
var menu = document.createElement("SELECT");
var btns = findElementsBySelector(e,"INPUT.hetero-list-add"),
btn = btns[btns.length-1]; // In case nested content also uses hetero-list
YAHOO.util.Dom.insertAfter(menu,btn);
var prototypes = $(e.lastChild);
while(!prototypes.hasClassName("prototypes")) //LINE 16, ERROR IS HERE
prototypes = prototypes.previous();
var insertionPoint = prototypes.previous(); // this is where the new item is inserted.
// extract templates
var templates = []; var i=0;
$(prototypes).childElements().each(function (n) {
var name = n.getAttribute("name");
var tooltip = n.getAttribute("tooltip");
var descriptorId = n.getAttribute("descriptorId");
menu.options[i] = new Option(n.getAttribute("title"),""+i);
templates.push({html:n.innerHTML, name:name, tooltip:tooltip,descriptorId:descriptorId});
i++;
});
Element.remove(prototypes);
var withDragDrop = initContainerDD(e);
var menuAlign = (btn.getAttribute("menualign")||"tl-bl");
var menuButton = new YAHOO.widget.Button(btn, { type: "menu", menu: menu, menualignment: menuAlign.split("-") });
$(menuButton._button).addClassName(btn.className); // copy class names
$(menuButton._button).setAttribute("suffix",btn.getAttribute("suffix"));
menuButton.getMenu().clickEvent.subscribe(function(type,args,value) {
var item = args[1];
if (item.cfg.getProperty("disabled")) return;
var t = templates[parseInt(item.value)];
var nc = document.createElement("div");
nc.className = "repeated-chunk";
nc.setAttribute("name",t.name);
nc.setAttribute("descriptorId",t.descriptorId);
nc.innerHTML = t.html;
$(nc).setOpacity(0);
var scroll = document.body.scrollTop;
renderOnDemand(findElementsBySelector(nc,"TR.config-page")[0],function() {
function findInsertionPoint() {
// given the element to be inserted 'prospect',
// and the array of existing items 'current',
// and preferred ordering function, return the position in the array
// the prospect should be inserted.
// (for example 0 if it should be the first item)
function findBestPosition(prospect,current,order) {
function desirability(pos) {
var count=0;
for (var i=0; i<current.length; i++) {
if ((i<pos) == (order(current[i])<=order(prospect)))
count++;
}
return count;
}
var bestScore = -1;
var bestPos = 0;
for (var i=0; i<=current.length; i++) {
var d = desirability(i);
if (bestScore<=d) {// prefer to insert them toward the end
bestScore = d;
bestPos = i;
}
}
return bestPos;
}
var current = e.childElements().findAll(function(e) {return e.match("DIV.repeated-chunk")});
function o(did) {
if (Object.isElement(did))
did = did.getAttribute("descriptorId");
for (var i=0; i<templates.length; i++)
if (templates[i].descriptorId==did)
return i;
return 0; // can't happen
}
var bestPos = findBestPosition(t.descriptorId, current, o);
if (bestPos<current.length)
return current[bestPos];
else
return insertionPoint;
}
(e.hasClassName("honor-order") ? findInsertionPoint() : insertionPoint).insert({before:nc});
if(withDragDrop) prepareDD(nc);
new YAHOO.util.Anim(nc, {
opacity: { to:1 }
}, 0.2, YAHOO.util.Easing.easeIn).animate();
Behaviour.applySubtree(nc,true);
ensureVisible(nc);
layoutUpdateCallback.call();
},true);
});
menuButton.getMenu().renderEvent.subscribe(function() {
// hook up tooltip for menu items
var items = menuButton.getMenu().getItems();
for(i=0; i<items.length; i++) {
var t = templates[i].tooltip;
if(t!=null)
applyTooltip(items[i].element,t);
}
});
if (e.hasClassName("one-each")) {
// does this container already has a ocnfigured instance of the specified descriptor ID?
function has(id) {
return Prototype.Selector.find(e.childElements(),"DIV.repeated-chunk[descriptorId=\""+id+"\"]")!=null;
}
menuButton.getMenu().showEvent.subscribe(function() {
var items = menuButton.getMenu().getItems();
for(i=0; i<items.length; i++) {
items[i].cfg.setProperty("disabled",has(templates[i].descriptorId));
}
});
}
});
Behaviour.specify("DIV.dd-handle", 'hetero-list', -100, function(e) {
e=$(e);
e.on("mouseover",function() {
$(this).up(".repeated-chunk").addClassName("hover");
});
e.on("mouseout",function() {
$(this).up(".repeated-chunk").removeClassName("hover");
});
});
I hope this is enough information to solve the problem. Any suggestions (even if they aren't complete answers) are really appreciated.
While not an exact answer, I did find a way to get this working. For some reason, putting the repeatableProperty in an advanced block stopped the javascript error from happening, so everything loaded fine.
So, my config.groovy for RepeatableTest looked like this:
package uitestplugin.uitest.RepeatableTest;
f = namespace(lib.FormTagLib)
f.advanced{
f.entry(title:"Properties"){
f.repeatableProperty(field:"property", minimum:"1"){
}
}
}
My config.groovy for Prop1 looked like this:
package uitestplugin.uitest.Prop1;
def f = namespace(lib.FormTagLib)
f.entry(title:"Name",field:"name") {
f.textbox()
}
f.entry {
div(align:"left") {
input(type:"button",value:"Delete",class:"repeatable-delete")
}
}
My prop 1 looked like this:
public class Prop1 extends AbstractDescribableImpl<Prop1> {
private final String name;
public String getName(){
return name;
}
#DataBoundConstructor
public Prop1( String name) {
this.name = name;
}
#Extension
public static class DescriptorImpl extends Descriptor<Prop1> {
#Override
public String getDisplayName() {
return "";
}
}
}
And my RepeatableTest.java looked like this:
public class RepeatableTest extends Builder {
private final List<Prop1> property;
// Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
#DataBoundConstructor
public RepeatableTest(List<Prop1> property) {
this.property = property;
}
public List<Prop1> getProperty() {
return property;
}
#Override
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException {
//Doesn't matter
}
#Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}
#Extension // This indicates to Jenkins that this is an implementation of an extension point.
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
private String phpLoc;
public DescriptorImpl() {
load();
}
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
// Indicates that this builder can be used with all kinds of project types
return true;
}
public String getDisplayName() {
return "Repeatable Test";
}
#Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
phpLoc = formData.getString("phpLoc");
save();
return super.configure(req,formData);
}
public String getPhpLoc() {
return phpLoc;
}
}
}