Skip to content
This repository was archived by the owner on Jun 9, 2021. It is now read-only.

Commit 2a5c793

Browse files
author
Itay Neeman
committed
Add interactive forms to buttons
This change adds the ability to specify a JSON-based form for a given button, which will get automatically rendered when the button is pressed. The submitted data is available as serialized JSON in the ${BUTTON_FORM_DATA} variable. For the specification of what a form looks like and it's serialized result, look at README.md in the change.
1 parent 5644887 commit 2a5c793

22 files changed

+454
-88
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The Pull Request Notifier for Bitbucket can:
2828
* Can invoke CSRF protected systems, using the ${INJECTION_URL_VALUE} variable. How to to that with Jenkins is described below.
2929
* Be configured to only trigger if the pull request mathches a filter. A filter text is constructed with any combination of the variables and then a regexp is constructed to match that text.
3030
* Add buttons to pull request view in Bitbucket. And map those buttons to URL invocations. This can be done by setting the filter string to ${BUTTON_TRIGGER_TITLE} and the filter regexp to title of button.
31+
* Buttons can have forms associated with them, and then submit the form data using the ${BUTTON_FORM_DATA} variable.
3132
* Authenticate with HTTP basic authentication.
3233
* Optionally allow any SSL certificate.
3334
* Use custom SSL key store, type and password.
@@ -56,6 +57,7 @@ The filter text as well as the URL support variables. These are:
5657
* ${PULL_REQUEST_ACTION} Example: OPENED
5758
* ${PULL_REQUEST_STATE} Example: DECLINED, MERGED, OPEN
5859
* ${BUTTON_TRIGGER_TITLE} Example: Trigger Notification
60+
* ${BUTTON_FORM_DATA} The form data that was submitted
5961
* ${INJECTION_URL_VALUE} Value retrieved from any URL
6062
* ${PULL_REQUEST_URL} Example: http://localhost:7990/projects/PROJECT_1/repos/rep_1/pull-requests/1
6163
* ${PULL_REQUEST_USER_DISPLAY_NAME} Example: Some User
@@ -101,6 +103,69 @@ The ${PULL_REQUEST_USER...} contains information about the user who issued the e
101103

102104
You may want to use [Violation Comments to Stash plugin](https://wiki.jenkins-ci.org/display/JENKINS/Violation+Comments+to+Stash+Plugin) and/or [StashNotifier plugin](https://wiki.jenkins-ci.org/display/JENKINS/StashNotifier+Plugin) to report results back to Bitbucket.
103105

106+
#### Button Forms
107+
108+
For each button you can specify a form that will show up when the button is pressed. That form data will then be submitted and will be available in the ${BUTTON_FORM_DATA} variable. Additionally, the form itself can reference other variables (with the exception of the ${BUTTON_...} ones) and will have those resolved prior to rendering.
109+
110+
A form is defined as a JSON array. Here is an example that shows all possibilities:
111+
112+
```
113+
[
114+
{ "name": "var1",
115+
"label": "var1 label",
116+
"defaultValue": "you can put a variable like this: ${PULL_REQUEST_AUTHOR_NAME}",
117+
"type": "input",
118+
"required": false,
119+
"description": "var1 description"
120+
},
121+
{ "name": "var2",
122+
"label": "var2 label",
123+
"defaultValue": "any string can go here",
124+
"type": "textarea",
125+
"required": false,
126+
"description": "var2 description"
127+
},
128+
{ "name": "var3",
129+
"label": "var3 label",
130+
"defaultValue": "option2_name",
131+
"options": [
132+
{"label": "option1 label", "name": "option1_name"},
133+
{"label": "option2 label", "name": "option2_name"},
134+
{"label": "option3 label", "name": "option3_name"}
135+
],
136+
"type": "radio",
137+
"required": true,
138+
"description": "var3 description"
139+
},
140+
{ "name": "var4",
141+
"label": "var4 label",
142+
"type": "checkbox",
143+
"required": true,
144+
"options": [
145+
{"label": "option1 label", "name": "option1_name", "defaultValue": true},
146+
{"label": "option2 label", "name": "option2_name", "defaultValue": true}
147+
],
148+
"description": "var4 description"
149+
}
150+
]
151+
```
152+
153+
You can see a screenshot [here](https://raw.githubusercontent.com/tomasbjerre/pull-request-notifier-for-bitbucket/master/sandbox/rendered_form.png) when rendered.
154+
155+
When submitted with the default values, it will look like this:
156+
157+
```
158+
{
159+
"var1":"you can put a variable like this: admin",
160+
"var2":"any string can go here",
161+
"var3":"option2_name",
162+
"var4":[
163+
"option1_name",
164+
"option2_name"
165+
]
166+
}
167+
```
168+
104169
### REST
105170
Some rest resources are available. You can figure out the JSON structure by looking at the [DTO:s](https://github.com/tomasbjerre/pull-request-notifier-for-bitbucket/tree/master/src/main/java/se/bjurr/prnfb/presentation/dto).
106171

pom.xml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ Changelog of Pull Request Notifier for Bitbucket.
197197
<version>${amps.version}</version>
198198
<extensions>true</extensions>
199199
<configuration>
200+
<enableFastdev>false</enableFastdev>
201+
<enableDevToolbox>false</enableDevToolbox>
202+
<enablePde>false</enablePde>
203+
<skipRestDocGeneration>true</skipRestDocGeneration>
204+
<skipManifestValidation>true</skipManifestValidation>
205+
<extractDependencies>false</extractDependencies>
206+
<skipManifestValidation>true</skipManifestValidation>
207+
<enableQuickReload>true</enableQuickReload>
208+
<pluginArtifacts>
209+
<pluginArtifact>
210+
<groupId>com.atlassian.labs.plugins</groupId>
211+
<artifactId>quickreload</artifactId>
212+
<version>${quick.reload.version}</version>
213+
</pluginArtifact>
214+
</pluginArtifacts>
200215
<products>
201216
<product>
202217
<id>bitbucket</id>
@@ -258,8 +273,9 @@ Changelog of Pull Request Notifier for Bitbucket.
258273
</profiles>
259274

260275
<properties>
261-
<bitbucket.version>4.8.1</bitbucket.version>
276+
<bitbucket.version>4.11.1</bitbucket.version>
262277
<bitbucket.data.version>${bitbucket.version}</bitbucket.data.version>
278+
<quick.reload.version>2.0.0</quick.reload.version>
263279
<amps.version>6.1.0</amps.version>
264280
</properties>
265-
</project>
281+
</project>

sandbox/rendered_form.png

59.6 KB
Loading

src/main/java/se/bjurr/prnfb/listener/PrnfbPullRequestEventListener.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public boolean isNotificationTriggeredByAction(PrnfbNotification notification,
145145

146146
if (notification.getFilterRegexp().isPresent() && notification.getFilterString().isPresent()
147147
&& !compile(notification.getFilterRegexp().get())
148-
.matcher(renderer.render(notification.getFilterString().get(), FALSE, clientKeyStore, shouldAcceptAnyCertificate))
148+
.matcher(renderer.render(notification.getFilterString().get(), FALSE, FALSE, clientKeyStore, shouldAcceptAnyCertificate))
149149
.find()) {
150150
return FALSE;
151151
}
@@ -176,9 +176,9 @@ public NotificationResponse notify(final PrnfbNotification notification, PrnfbPu
176176
Optional<String> postContent = absent();
177177
if (notification.getPostContent().isPresent()) {
178178
postContent = of(
179-
renderer.render(notification.getPostContent().get(), FALSE, clientKeyStore, shouldAcceptAnyCertificate));
179+
renderer.render(notification.getPostContent().get(), FALSE, FALSE, clientKeyStore, shouldAcceptAnyCertificate));
180180
}
181-
String renderedUrl = renderer.render(notification.getUrl(), TRUE, clientKeyStore, shouldAcceptAnyCertificate);
181+
String renderedUrl = renderer.render(notification.getUrl(), TRUE, FALSE, clientKeyStore, shouldAcceptAnyCertificate);
182182
LOG.info(notification.getName() + " > " //
183183
+ pullRequest.getFromRef().getId() + "(" + pullRequest.getFromRef().getLatestCommit() + ") -> " //
184184
+ pullRequest.getToRef().getId() + "(" + pullRequest.getToRef().getLatestCommit() + ")" + " " //
@@ -192,7 +192,7 @@ public NotificationResponse notify(final PrnfbNotification notification, PrnfbPu
192192
for (PrnfbHeader header : notification.getHeaders()) {
193193
urlInvoker//
194194
.withHeader(header.getName(),
195-
renderer.render(header.getValue(), FALSE, clientKeyStore, shouldAcceptAnyCertificate));
195+
renderer.render(header.getValue(), FALSE, FALSE, clientKeyStore, shouldAcceptAnyCertificate));
196196
}
197197
HttpResponse httpResponse = createInvoker().invoke(urlInvoker//
198198
.withProxyServer(notification.getProxyServer()) //

src/main/java/se/bjurr/prnfb/presentation/ButtonServlet.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
import java.util.List;
1515
import java.util.UUID;
1616

17+
import javax.servlet.http.HttpServletRequest;
1718
import javax.ws.rs.Consumes;
1819
import javax.ws.rs.DELETE;
1920
import javax.ws.rs.GET;
2021
import javax.ws.rs.POST;
2122
import javax.ws.rs.Path;
2223
import javax.ws.rs.PathParam;
2324
import javax.ws.rs.Produces;
25+
import javax.ws.rs.core.Context;
2426
import javax.ws.rs.core.Response;
2527

2628
import se.bjurr.prnfb.http.NotificationResponse;
@@ -32,13 +34,15 @@
3234
import se.bjurr.prnfb.settings.PrnfbButton;
3335

3436
import com.atlassian.annotations.security.XsrfProtectionExcluded;
37+
import com.google.gson.Gson;
3538

3639
@Path("/settings/buttons")
3740
public class ButtonServlet {
3841
private final ButtonsService buttonsService;
3942
private final SettingsService settingsService;
4043
private final UserCheckService userCheckService;
41-
44+
private static final Gson gson = new Gson();
45+
4246
public ButtonServlet(ButtonsService buttonsService, SettingsService settingsService,
4347
UserCheckService userCheckService) {
4448
this.buttonsService = buttonsService;
@@ -57,6 +61,15 @@ public Response create(ButtonDTO buttonDto) {
5761
return status(UNAUTHORIZED)//
5862
.build();
5963
}
64+
65+
if (buttonDto.getButtonForm() != null && !buttonDto.getButtonForm().isEmpty()) {
66+
try {
67+
gson.fromJson(buttonDto.getButtonForm(), Object.class);
68+
} catch(com.google.gson.JsonSyntaxException ex) {
69+
throw new Error("The form specification for the button must be a valid JSON string");
70+
}
71+
}
72+
6073
PrnfbButton prnfbButton = toPrnfbButton(buttonDto);
6174
PrnfbButton created = this.settingsService.addOrUpdateButton(prnfbButton);
6275
ButtonDTO createdDto = toButtonDto(created);
@@ -103,6 +116,13 @@ public Response get(@PathParam("repositoryId") Integer repositoryId, @PathParam(
103116
Iterable<PrnfbButton> allowedButtons = this.userCheckService.filterAllowed(buttons);
104117
List<ButtonDTO> dtos = toButtonDtoList(allowedButtons);
105118
Collections.sort(dtos);
119+
120+
for(ButtonDTO dto : dtos) {
121+
if (dto.getButtonForm() != null) {
122+
dto.setButtonForm(this.buttonsService.getRenderedButtonFormData(repositoryId, pullRequestId, dto.getUuid(), dto.getButtonForm()));
123+
}
124+
}
125+
106126
return ok(dtos, APPLICATION_JSON).build();
107127
}
108128

@@ -150,16 +170,17 @@ public Response get(@PathParam("uuid") UUID uuid) {
150170
@Path("{uuid}/press/repository/{repositoryId}/pullrequest/{pullRequestId}")
151171
@XsrfProtectionExcluded
152172
@Produces(APPLICATION_JSON)
153-
public Response press(@PathParam("repositoryId") Integer repositoryId, @PathParam("pullRequestId") Long pullRequestId,
173+
public Response press(@Context HttpServletRequest request, @PathParam("repositoryId") Integer repositoryId, @PathParam("pullRequestId") Long pullRequestId,
154174
@PathParam("uuid") final UUID buttionUuid) {
175+
String formData = request.getParameter("form");
155176
PrnfbButton button = this.settingsService.getButton(buttionUuid);
156177
if (!this.userCheckService.isAllowedUseButton(button)) {
157178
return status(UNAUTHORIZED).build();
158179
}
159-
List<NotificationResponse> results = this.buttonsService.handlePressed(repositoryId, pullRequestId, buttionUuid);
180+
List<NotificationResponse> results = this.buttonsService.handlePressed(repositoryId, pullRequestId, buttionUuid, formData);
160181

161182
ButtonPressDTO dto = toTriggerResultDto(button, results);
162183
return ok(dto, APPLICATION_JSON).build();
163184
}
164185

165-
}
186+
}

src/main/java/se/bjurr/prnfb/presentation/dto/ButtonDTO.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class ButtonDTO implements Comparable<ButtonDTO> {
2121
private String repositorySlug;
2222
private USER_LEVEL userLevel;
2323
private UUID uuid;
24+
private String buttonForm;
2425

2526
@Override
2627
public int compareTo(ButtonDTO o) {
@@ -70,6 +71,13 @@ public boolean equals(Object obj) {
7071
} else if (!this.confirmation.equals(other.confirmation)) {
7172
return false;
7273
}
74+
if (this.buttonForm == null) {
75+
if (other.buttonForm != null) {
76+
return false;
77+
}
78+
} else if (!this.buttonForm.equals(other.buttonForm)) {
79+
return false;
80+
}
7381
if (this.uuid == null) {
7482
if (other.uuid != null) {
7583
return false;
@@ -88,6 +96,10 @@ public String getName() {
8896
return this.name;
8997
}
9098

99+
public String getButtonForm() {
100+
return this.buttonForm;
101+
}
102+
91103
public Optional<String> getProjectKey() {
92104
return Optional.fromNullable(this.projectKey);
93105
}
@@ -118,6 +130,7 @@ public int hashCode() {
118130
result = prime * result + ((this.userLevel == null) ? 0 : this.userLevel.hashCode());
119131
result = prime * result + ((this.uuid == null) ? 0 : this.uuid.hashCode());
120132
result = prime * result + ((this.confirmation == null) ? 0 : this.confirmation.hashCode());
133+
result = prime * result + ((this.buttonForm == null) ? 0 : this.buttonForm.hashCode());
121134
return result;
122135
}
123136

@@ -129,6 +142,10 @@ public void setName(String name) {
129142
this.name = name;
130143
}
131144

145+
public void setButtonForm(String buttonForm) {
146+
this.buttonForm = buttonForm;
147+
}
148+
132149
public void setProjectKey(String projectKey) {
133150
this.projectKey = projectKey;
134151
}
@@ -148,7 +165,7 @@ public void setUuid(UUID uuid) {
148165
@Override
149166
public String toString() {
150167
return "ButtonDTO [name=" + this.name + ", userLevel=" + this.userLevel + ", uuid=" + this.uuid + ", repositorySlug="
151-
+ this.repositorySlug + ", projectKey=" + this.projectKey + ", confirmation=" + this.confirmation + "]";
168+
+ this.repositorySlug + ", projectKey=" + this.projectKey + ", buttonForm=" + this.buttonForm + ", confirmation=" + this.confirmation + "]";
152169
}
153170

154171
}

src/main/java/se/bjurr/prnfb/service/ButtonsService.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static java.lang.Boolean.TRUE;
77
import static se.bjurr.prnfb.listener.PrnfbPullRequestAction.BUTTON_TRIGGER;
88
import static se.bjurr.prnfb.service.PrnfbVariable.BUTTON_TRIGGER_TITLE;
9+
import static se.bjurr.prnfb.service.PrnfbVariable.BUTTON_FORM_DATA;
910

1011
import java.util.HashMap;
1112
import java.util.List;
@@ -54,12 +55,25 @@ public List<PrnfbButton> getButtons(Integer repositoryId, Long pullRequestId) {
5455
return doGetButtons(notifications, clientKeyStore, pullRequest, shouldAcceptAnyCertificate);
5556
}
5657

57-
public List<NotificationResponse> handlePressed(Integer repositoryId, Long pullRequestId, UUID buttonUuid) {
58+
public String getRenderedButtonFormData(Integer repositoryId, Long pullRequestId, UUID buttonUuid, String formData) {
59+
final PrnfbSettingsData settings = this.settingsService.getPrnfbSettingsData();
60+
ClientKeyStore clientKeyStore = new ClientKeyStore(settings);
61+
final PullRequest pullRequest = this.pullRequestService.getById(repositoryId, pullRequestId);
62+
boolean shouldAcceptAnyCertificate = settings.isShouldAcceptAnyCertificate();
63+
64+
Map<PrnfbVariable, Supplier<String>> variables = getVariables(buttonUuid, formData);
65+
PrnfbPullRequestAction pullRequestAction = BUTTON_TRIGGER;
66+
67+
PrnfbRenderer renderer = this.prnfbRendererFactory.create(pullRequest, pullRequestAction, null, variables);
68+
return renderer.render(formData, false, true, clientKeyStore, shouldAcceptAnyCertificate);
69+
}
70+
71+
public List<NotificationResponse> handlePressed(Integer repositoryId, Long pullRequestId, UUID buttonUuid, String formData) {
5872
final PrnfbSettingsData prnfbSettingsData = this.settingsService.getPrnfbSettingsData();
5973
ClientKeyStore clientKeyStore = new ClientKeyStore(prnfbSettingsData);
6074
boolean shouldAcceptAnyCertificate = prnfbSettingsData.isShouldAcceptAnyCertificate();
6175
final PullRequest pullRequest = this.pullRequestService.getById(repositoryId, pullRequestId);
62-
return doHandlePressed(buttonUuid, clientKeyStore, shouldAcceptAnyCertificate, pullRequest);
76+
return doHandlePressed(buttonUuid, clientKeyStore, shouldAcceptAnyCertificate, pullRequest, formData);
6377
}
6478

6579
private boolean isTriggeredByAction(ClientKeyStore clientKeyStore, List<PrnfbNotification> notifications,
@@ -90,7 +104,7 @@ List<PrnfbButton> doGetButtons(List<PrnfbNotification> notifications, ClientKeyS
90104
final PullRequest pullRequest, boolean shouldAcceptAnyCertificate) {
91105
List<PrnfbButton> allFoundButtons = newArrayList();
92106
for (PrnfbButton candidate : this.settingsService.getButtons()) {
93-
Map<PrnfbVariable, Supplier<String>> variables = getVariables(candidate.getUuid());
107+
Map<PrnfbVariable, Supplier<String>> variables = getVariables(candidate.getUuid(), null);
94108
PrnfbPullRequestAction pullRequestAction = BUTTON_TRIGGER;
95109
if (this.userCheckService.isAllowedUseButton(candidate)//
96110
&& isTriggeredByAction(clientKeyStore, notifications, shouldAcceptAnyCertificate, pullRequestAction, pullRequest,
@@ -102,11 +116,11 @@ && isTriggeredByAction(clientKeyStore, notifications, shouldAcceptAnyCertificate
102116
allFoundButtons = usingToString().sortedCopy(allFoundButtons);
103117
return allFoundButtons;
104118
}
105-
119+
106120
@VisibleForTesting
107121
List<NotificationResponse> doHandlePressed(UUID buttonUuid, ClientKeyStore clientKeyStore,
108-
boolean shouldAcceptAnyCertificate, final PullRequest pullRequest) {
109-
Map<PrnfbVariable, Supplier<String>> variables = getVariables(buttonUuid);
122+
boolean shouldAcceptAnyCertificate, final PullRequest pullRequest, final String formData) {
123+
Map<PrnfbVariable, Supplier<String>> variables = getVariables(buttonUuid, formData);
110124
List<NotificationResponse> successes = newArrayList();
111125
for (PrnfbNotification prnfbNotification : this.settingsService.getNotifications()) {
112126
PrnfbPullRequestAction pullRequestAction = BUTTON_TRIGGER;
@@ -126,10 +140,11 @@ List<NotificationResponse> doHandlePressed(UUID buttonUuid, ClientKeyStore clien
126140
}
127141

128142
@VisibleForTesting
129-
Map<PrnfbVariable, Supplier<String>> getVariables(final UUID uuid) {
143+
Map<PrnfbVariable, Supplier<String>> getVariables(final UUID uuid, final String formData) {
130144
Map<PrnfbVariable, Supplier<String>> variables = new HashMap<PrnfbVariable, Supplier<String>>();
131145
PrnfbButton button = this.settingsService.getButton(uuid);
132146
variables.put(BUTTON_TRIGGER_TITLE, Suppliers.ofInstance(button.getName()));
147+
variables.put(BUTTON_FORM_DATA, Suppliers.ofInstance(formData));
133148
return variables;
134149
}
135150

0 commit comments

Comments
 (0)