Skip to content

Commit c785390

Browse files
committed
Add DeclarativeDirective extension point for adding directives
New directives must provide a method for parsing from Jenkinsfile syntax, a JSON schema, a runtime model class, validation for the parsed AST model, and a method for transforming from the parsed AST model to the runtime model. Initially, will only provide support for preprocessing directives - which will provide a way to transform the full AST model after parsing and before validation and transformation. This will enable plugin authors to contribute directives that can be replaced by predetermined content before validation starts.
1 parent c85dd7b commit c785390

File tree

15 files changed

+477
-17
lines changed

15 files changed

+477
-17
lines changed

pipeline-model-api/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/ASTSchema.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,23 @@
2626

2727
import hudson.Extension;
2828
import hudson.model.RootAction;
29+
import org.jenkinsci.plugins.pipeline.modeldefinition.model.DeclarativeDirectiveDescriptor;
30+
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.fasterxml.jackson.databind.JsonNode;
31+
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.fasterxml.jackson.databind.ObjectMapper;
32+
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.fasterxml.jackson.databind.node.ObjectNode;
2933
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.github.fge.jsonschema.exceptions.ProcessingException;
34+
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.github.fge.jsonschema.load.URIManager;
3035
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.github.fge.jsonschema.main.JsonSchema;
3136
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.github.fge.jsonschema.main.JsonSchemaFactory;
37+
import org.jenkinsci.plugins.pipeline.modeldefinition.shaded.com.github.fge.jsonschema.util.JsonLoader;
3238
import org.kohsuke.stapler.StaplerRequest;
3339
import org.kohsuke.stapler.StaplerResponse;
3440

3541
import javax.servlet.ServletException;
3642
import java.io.IOException;
43+
import java.io.OutputStream;
44+
import java.net.URI;
45+
import java.net.URISyntaxException;
3746

3847
/**
3948
* Endpoint for exposing the AST JSON schema.
@@ -60,8 +69,31 @@ public String getDisplayName() {
6069
}
6170

6271
@SuppressWarnings("unused")
63-
public void doJson(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
64-
rsp.serveFile(req, getClass().getResource("/ast-schema.json"));
72+
public void doJson(StaplerRequest req, StaplerResponse rsp) throws IOException {
73+
rsp.setContentType("application/json;charset=UTF-8");
74+
try (OutputStream os = rsp.getOutputStream()) {
75+
ObjectMapper mapper = new ObjectMapper();
76+
Object json = mapper.readValue(getSchemaAsJSON().toString(), Object.class);
77+
mapper.writeValue(os, json);
78+
os.flush();
79+
}
80+
}
81+
82+
private static JsonNode getSchemaAsJSON() throws IOException {
83+
JsonNode baseSchema = JsonLoader.fromResource("/ast-schema.json");
84+
85+
if (baseSchema != null && baseSchema.get("definitions").isObject()) {
86+
JsonNode definitions = baseSchema.get("definitions");
87+
if (definitions.isObject()) {
88+
for (DeclarativeDirectiveDescriptor desc : DeclarativeDirectiveDescriptor.all()) {
89+
((ObjectNode)definitions).put(desc.getName(), desc.getSchema());
90+
}
91+
}
92+
if (!(baseSchema.get("definitions").equals(definitions))) {
93+
((ObjectNode)baseSchema).put("definitions", definitions);
94+
}
95+
}
96+
return baseSchema;
6597
}
6698

6799
/**
@@ -70,9 +102,9 @@ public void doJson(StaplerRequest req, StaplerResponse rsp) throws IOException,
70102
* @return the schema in {@link JsonSchema} form.
71103
* @throws ProcessingException if there are issues reading the schema
72104
*/
73-
public static JsonSchema getJSONSchema() throws ProcessingException {
105+
public static JsonSchema getJSONSchema() throws ProcessingException, IOException {
74106
final JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
75-
return factory.getJsonSchema("resource:/ast-schema.json");
107+
return factory.getJsonSchema(getSchemaAsJSON());
76108
}
77109

78110
}

pipeline-model-api/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/ast/ModelASTElement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public abstract class ModelASTElement {
3636
*/
3737
private Object sourceLocation;
3838

39-
ModelASTElement(Object sourceLocation) {
39+
protected ModelASTElement(Object sourceLocation) {
4040
this.sourceLocation = sourceLocation;
4141
}
4242

pipeline-model-api/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/ast/ModelASTPipelineDef.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package org.jenkinsci.plugins.pipeline.modeldefinition.ast;
22

3+
import net.sf.json.JSONArray;
34
import net.sf.json.JSONObject;
45
import org.apache.commons.lang.StringUtils;
6+
import org.jenkinsci.plugins.pipeline.modeldefinition.model.DeclarativeDirective;
57
import org.jenkinsci.plugins.pipeline.modeldefinition.validator.ModelValidator;
68

79
import javax.annotation.Nonnull;
10+
import java.util.ArrayList;
11+
import java.util.List;
812

913
/**
1014
* Represents the parsed pipeline definition for visual pipeline editor. Corresponds to {@code Root}.
@@ -22,6 +26,7 @@ public final class ModelASTPipelineDef extends ModelASTElement {
2226
private ModelASTBuildParameters parameters;
2327
private ModelASTTriggers triggers;
2428
private ModelASTLibraries libraries;
29+
private List<DeclarativeDirective> additionalDirectives = new ArrayList<>();
2530

2631
public ModelASTPipelineDef(Object sourceLocation) {
2732
super(sourceLocation);
@@ -55,6 +60,13 @@ public JSONObject toJSON() {
5560
} else {
5661
a.put("libraries", null);
5762
}
63+
if (!additionalDirectives.isEmpty()) {
64+
JSONObject directives = new JSONObject();
65+
for (DeclarativeDirective d : additionalDirectives) {
66+
directives.put(d.getDescriptor().getName(), d.toJSON());
67+
}
68+
a.put("additionalDirectives", directives);
69+
}
5870
return new JSONObject().accumulate("pipeline", a);
5971
}
6072

@@ -89,6 +101,9 @@ public void validate(@Nonnull ModelValidator validator) {
89101
if (libraries != null) {
90102
libraries.validate(validator);
91103
}
104+
for (DeclarativeDirective d : additionalDirectives) {
105+
d.validate(validator);
106+
}
92107
}
93108

94109
@Override
@@ -124,6 +139,9 @@ public String toGroovy() {
124139
if (triggers != null && !triggers.getTriggers().isEmpty()) {
125140
result.append(triggers.toGroovy());
126141
}
142+
for (DeclarativeDirective d : additionalDirectives) {
143+
result.append(d.toGroovy());
144+
}
127145

128146
result.append("}\n");
129147
return result.toString();
@@ -198,6 +216,12 @@ public void removeSourceLocation() {
198216
if (triggers != null) {
199217
triggers.removeSourceLocation();
200218
}
219+
if (agent != null) {
220+
agent.removeSourceLocation();
221+
}
222+
for (DeclarativeDirective d : additionalDirectives) {
223+
d.removeSourceLocation();
224+
}
201225
}
202226

203227
private String indent(int count) {
@@ -276,6 +300,14 @@ public void setTriggers(ModelASTTriggers triggers) {
276300
this.triggers = triggers;
277301
}
278302

303+
@Nonnull
304+
public List<DeclarativeDirective> getAdditionalDirectives() {
305+
return additionalDirectives;
306+
}
307+
308+
public void setAdditionalDirectives(@Nonnull List<DeclarativeDirective> additionalDirectives) {
309+
this.additionalDirectives.addAll(additionalDirectives);
310+
}
279311

280312
@Override
281313
public String toString() {
@@ -289,6 +321,7 @@ public String toString() {
289321
", parameters=" + parameters +
290322
", triggers=" + triggers +
291323
", libraries=" + libraries +
324+
", additionalDirectives=" + additionalDirectives +
292325
"}";
293326
}
294327

@@ -334,8 +367,11 @@ public boolean equals(Object o) {
334367
if (getLibraries() != null ? !getLibraries().equals(that.getLibraries()) : that.getLibraries() != null) {
335368
return false;
336369
}
337-
return getTriggers() != null ? getTriggers().equals(that.getTriggers()) : that.getTriggers() == null;
370+
if (getTriggers() != null ? !getTriggers().equals(that.getTriggers()) : that.getTriggers() != null) {
371+
return false;
372+
}
338373

374+
return getAdditionalDirectives().equals(that.getAdditionalDirectives());
339375
}
340376

341377
@Override
@@ -350,6 +386,7 @@ public int hashCode() {
350386
result = 31 * result + (getParameters() != null ? getParameters().hashCode() : 0);
351387
result = 31 * result + (getTriggers() != null ? getTriggers().hashCode() : 0);
352388
result = 31 * result + (getLibraries() != null ? getLibraries().hashCode() : 0);
389+
result = 31 * result + getAdditionalDirectives().hashCode();
353390
return result;
354391
}
355392
}

pipeline-model-api/src/main/java/org/jenkinsci/plugins/pipeline/modeldefinition/ast/ModelASTStage.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import net.sf.json.JSONArray;
66
import net.sf.json.JSONObject;
77
import org.apache.commons.lang.StringEscapeUtils;
8+
import org.jenkinsci.plugins.pipeline.modeldefinition.model.DeclarativeDirective;
89
import org.jenkinsci.plugins.pipeline.modeldefinition.validator.ModelValidator;
910

1011
import javax.annotation.Nonnull;
@@ -28,6 +29,7 @@ public final class ModelASTStage extends ModelASTElement {
2829
private ModelASTStages parallel;
2930
private ModelASTOptions options;
3031
private ModelASTStageInput input;
32+
private List<DeclarativeDirective> additionalDirectives = new ArrayList<>();
3133

3234
public ModelASTStage(Object sourceLocation) {
3335
super(sourceLocation);
@@ -75,6 +77,13 @@ public JSONObject toJSON() {
7577
if (input != null) {
7678
o.accumulate("input", input.toJSON());
7779
}
80+
if (!additionalDirectives.isEmpty()) {
81+
JSONObject directives = new JSONObject();
82+
for (DeclarativeDirective d : additionalDirectives) {
83+
directives.put(d.getDescriptor().getName(), d.toJSON());
84+
}
85+
o.put("additionalDirectives", directives);
86+
}
7887

7988
return o;
8089
}
@@ -113,6 +122,9 @@ public void validate(final ModelValidator validator, boolean isNested) {
113122
if (input != null) {
114123
input.validate(validator);
115124
}
125+
for (DeclarativeDirective d : additionalDirectives) {
126+
d.validate(validator, true);
127+
}
116128
}
117129

118130
@Override
@@ -173,6 +185,10 @@ public String toGroovy() {
173185
result.append("}\n");
174186
}
175187

188+
for (DeclarativeDirective d : additionalDirectives) {
189+
result.append(d.toGroovy());
190+
}
191+
176192
if (post != null) {
177193
result.append(post.toGroovy());
178194
}
@@ -212,6 +228,9 @@ public void removeSourceLocation() {
212228
if (input != null) {
213229
input.removeSourceLocation();
214230
}
231+
for (DeclarativeDirective d : additionalDirectives) {
232+
d.removeSourceLocation();
233+
}
215234
}
216235

217236
public String getName() {
@@ -302,6 +321,15 @@ public void setInput(ModelASTStageInput input) {
302321
this.input = input;
303322
}
304323

324+
@Nonnull
325+
public List<DeclarativeDirective> getAdditionalDirectives() {
326+
return additionalDirectives;
327+
}
328+
329+
public void setAdditionalDirectives(List<DeclarativeDirective> additionalDirectives) {
330+
this.additionalDirectives.addAll(additionalDirectives);
331+
}
332+
305333
@Override
306334
public String toString() {
307335
return "ModelASTStage{" +
@@ -316,6 +344,7 @@ public String toString() {
316344
", parallel=" + parallel +
317345
", options=" + options +
318346
", input=" + input +
347+
", additionalDirectives=" + additionalDirectives +
319348
"}";
320349
}
321350

@@ -363,8 +392,11 @@ public boolean equals(Object o) {
363392
if (getParallel() != null ? !getParallel().equals(that.getParallel()) : that.getParallel() != null) {
364393
return false;
365394
}
366-
return getBranches() != null ? getBranches().equals(that.getBranches()) : that.getBranches() == null;
395+
if (getBranches() != null ? !getBranches().equals(that.getBranches()) : that.getBranches() != null) {
396+
return false;
397+
}
367398

399+
return getAdditionalDirectives().equals(that.getAdditionalDirectives());
368400
}
369401

370402
@Override
@@ -381,6 +413,7 @@ public int hashCode() {
381413
result = 31 * result + (getParallel() != null ? getParallel().hashCode() : 0);
382414
result = 31 * result + (getOptions() != null ? getOptions().hashCode() : 0);
383415
result = 31 * result + (getInput() != null ? getInput().hashCode() : 0);
416+
result = 31 * result + additionalDirectives.hashCode();
384417
return result;
385418
}
386419
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2018, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package org.jenkinsci.plugins.pipeline.modeldefinition.model;
26+
27+
import hudson.model.Describable;
28+
import hudson.model.Descriptor;
29+
import jenkins.model.Jenkins;
30+
import org.codehaus.groovy.ast.expr.Expression;
31+
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTElement;
32+
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTPipelineDef;
33+
import org.jenkinsci.plugins.pipeline.modeldefinition.validator.ModelValidator;
34+
35+
import javax.annotation.Nonnull;
36+
37+
public abstract class DeclarativeDirective<D extends DeclarativeDirective<D>> extends ModelASTElement implements Describable<D> {
38+
39+
public DeclarativeDirective(Object sourceLocation) {
40+
super(sourceLocation);
41+
}
42+
43+
public abstract Expression transformDirective();
44+
45+
@Override
46+
public DeclarativeDirectiveDescriptor<D> getDescriptor() {
47+
Descriptor desc = Jenkins.getActiveInstance().getDescriptor(getClass());
48+
if (!(desc instanceof DeclarativeDirectiveDescriptor)) {
49+
throw new AssertionError(getClass()+" is missing its descriptor");
50+
} else {
51+
return (DeclarativeDirectiveDescriptor<D>)desc;
52+
}
53+
}
54+
55+
@Override
56+
public void validate(@Nonnull ModelValidator validator) {
57+
validate(validator, false);
58+
}
59+
60+
public void validate(@Nonnull ModelValidator validator, boolean inStage) {
61+
validator.validateElement(this, inStage);
62+
}
63+
64+
// TODO: Some way of requiring that implementations actually extend Preprocessor or its eventual siblings?
65+
66+
public static abstract class Preprocessor<D extends DeclarativeDirective<D>> extends DeclarativeDirective<D> {
67+
public Preprocessor(Object sourceLocation) {
68+
super(sourceLocation);
69+
}
70+
71+
public abstract ModelASTPipelineDef process(@Nonnull ModelASTPipelineDef pipelineDef);
72+
}
73+
74+
}

0 commit comments

Comments
 (0)