-
Notifications
You must be signed in to change notification settings - Fork 10
/
CodeDxClient.java
424 lines (367 loc) · 13.2 KB
/
CodeDxClient.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
/*
* © 2023 Synopsys, Inc. All rights reserved worldwide.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.codedx.api.client;
import com.codedx.util.CodeDxVersion;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.Map;
/**
* A RESTful client used to access the various API end-points exposed by CodeDx.
*
* @author anthonyd
*
*/
public class CodeDxClient {
private static final String KEY_HEADER = "API-Key";
protected String key;
protected String url;
private String serverUrl;
protected HttpClientBuilder httpClientBuilder;
private Gson gson;
/**
* Creates a new client, ready to be used for communications with CodeDx.
* @param url URL of the CodeDx web application. The '/api' part of the URL is optional.
* @param key The API key. Note that permissions must be set for this key on CodeDx admin page.
*/
public CodeDxClient(String url, String key){
this(url, key, HttpClientBuilder.create());
}
/**
* Creates a new client, ready to be used for communications with CodeDx.
* @param url URL of the CodeDx web application. The '/api' part of the URL is optional.
* @param key The API key. Note that permissions must be set for this key on CodeDx admin page.
* @param clientBuilder an HttpClientBuilder that can handle the certificate used by the server
*/
public CodeDxClient(String url, String key, HttpClientBuilder clientBuilder){
this.key = key;
if(url == null)
throw new NullPointerException("Argument url is null");
if(key == null)
throw new NullPointerException("Argument key is null");
url = url.trim();
if(!url.endsWith("/")){
url = url + "/";
}
if(!url.endsWith("api/")){
url = url + "api/";
}
this.url = url;
this.serverUrl = url.replace("/api/", "/");
httpClientBuilder = clientBuilder;
gson = new Gson();
}
@Deprecated
public String buildBrowsableAnalysisRunUrl(int projectId){
return serverUrl + "projects/" + projectId;
}
public String buildLatestFindingsUrl(int projectId){
return serverUrl + "projects/" + projectId;
}
/**
* Retrieves a list of projects from CodeDx.
*
* @return Project list
*
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public List<Project> getProjects() throws CodeDxClientException, IOException{
GetProjectsResponse response = doHttpRequest(
new HttpGet(),
"projects",
false,
new TypeToken<GetProjectsResponse>(){}.getType(),
null
);
return response.getProjects();
}
/**
* Retrieves a specific project from CodeDx
*
* @param id The project ID
* @return A project
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public Project getProject(ProjectContext project) throws CodeDxClientException, IOException{
return doHttpRequest(
new HttpGet(),
"projects/" + project.getProjectId(),
true,
new TypeToken<Project>(){}.getType(),
null
);
}
/**
* Retrieves all Triage statuses for a given project.
*
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public Map<String,TriageStatus> getTriageStatuses(ProjectContext project) throws CodeDxClientException, IOException{
return doHttpRequest(
new HttpGet(),
"projects/" + project.toString() + "/statuses",
true,
new TypeToken<Map<String,TriageStatus>>(){}.getType(),
null
);
}
/**
* Retrieves a specific job from CodeDx
*
* @param id The job ID
* @return A Job
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public Job getJob(String id) throws CodeDxClientException, IOException{
return doHttpRequest(
new HttpGet(),
"jobs/" + id,
false,
new TypeToken<Job>(){}.getType(),
null
);
}
/**
* Retrieves a job status from CodeDx. This is a convenience method for polling that
* relies on getJob.
*
* @param id The job ID
* @return The Job Status
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public String getJobStatus(String id) throws CodeDxClientException, IOException{
return getJob(id).getStatus();
}
/**
* Retrieves the total findings count for a given run.
*
* @param id The run ID
* @return The count
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public int getFindingsCount(String id) throws CodeDxClientException, IOException{
CountResponse resp = doHttpRequest(
new HttpGet(),
"runs/" + id + "/findings/count",
true,
new TypeToken<CountResponse>(){}.getType(),
null
);
return resp.getCount();
}
/**
* Retrieves the total findings count for a given project using the provided Filter
*
* @param projectContext The project context string, a raw projectId optionally concatenated with branch info
* @param filter A Filter object (set to null to not filter)
* @return The count
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public int getFindingsCount(ProjectContext project, Filter filter) throws CodeDxClientException, IOException{
CountResponse resp = doHttpRequest(
new HttpPost(),
"projects/" + project.toString() + "/findings/count",
true,
new TypeToken<CountResponse>(){}.getType(),
new CountRequest(filter)
);
return resp.getCount();
}
/**
* Retrieves an array of CountGroups using the provided Filter and countBy field name.
*
* @param filter A Filter object
* @param countBy The field to group the counts by
* @return A list of CountGroups
* @throws CodeDxClientException
* @throws ClientProtocolException
* @throws IOException
*/
public List<CountGroup> getFindingsGroupedCounts(ProjectContext project, Filter filter, String countBy) throws CodeDxClientException, IOException{
return doHttpRequest(
new HttpPost(),
"projects/" + project.toString() + "/findings/grouped-counts",
true,
new TypeToken<List<CountGroup>>(){}.getType(),
new GroupedCountRequest(filter,countBy)
);
}
public void setAnalysisName(ProjectContext project, int analysisId, String name) throws IOException, CodeDxClientException {
JsonObject reqBody = new JsonObject();
reqBody.addProperty("name", name);
doHttpRequest(
new HttpPut(),
"projects/" + project.getProjectId() + "/analyses/" + analysisId,
true,
null,
reqBody
);
}
public CodeDxVersion getCodeDxVersion() throws IOException, CodeDxClientException {
CodeDxVersionResponse resp = doHttpRequest(
new HttpGet(),
"system-info",
true,
new TypeToken<CodeDxVersionResponse>(){}.getType(),
null
);
return CodeDxVersion.fromString(resp.version);
}
public static class CodeDxVersionResponse {
public String version;
public String date;
}
public List<Branch> getProjectBranches(ProjectContext project) throws IOException, CodeDxClientException {
return doHttpRequest(
new HttpGet(),
"projects/" + project.getProjectId() + "/branches",
true,
new TypeToken<List<Branch>>(){}.getType(),
null
);
}
public boolean projectPolicyShouldBreakTheBuild(ProjectContext project) throws IOException, CodeDxClientException {
return doHttpRequest(
new HttpGet(),
"projects/" + project.toString() + "/policies/any/build-broken",
true,
new TypeToken<Boolean>(){}.getType(),
null
);
}
/**
* Perform an HttpRequest to the given api path, with an optional request body, and parse the response
* @param request Generally a new `HttpGet`, `HttpPost`, or `HttpPut`
* @param path The relative API path (not including /x/ or /api/)
* @param isXApi Flag that determines whether the request will prepend /x/ or /api/ to the path (true = /x/)
* @param responseType A type instance that helps `gson` parse the response body
* @param requestBody An optional payload that will be converted to json and sent with the request
* @param <T> Type parameter that determines the parsed response type
* @return The parsed response
* @throws IOException If the underlying IO goes wrong
* @throws CodeDxClientException For non 2xx response codes
*/
protected <T> T doHttpRequest(HttpRequestBase request, String path, boolean isXApi, Type responseType, Object requestBody) throws IOException, CodeDxClientException {
// set the request path (as a URI), with a different relative path depending on the `isXApi` flag
String apiPath = (isXApi ? url.replace("/api/", "/x/") : url) + path;
request.setURI(URI.create(apiPath));
// set authentication headers
request.addHeader(KEY_HEADER, key);
// if a request body is provided, and the request is able to add entities, JSONify it and use it as the request "entity"
if(requestBody != null && request instanceof HttpEntityEnclosingRequest){
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
HttpEntity requestBodyEntity = new StringEntity(gson.toJson(requestBody));
entityRequest.setEntity(requestBodyEntity);
}
// run the request (this requires a client; both the client and the response must be closed afterwards)
try(
CloseableHttpClient client = httpClientBuilder.build();
CloseableHttpResponse response = client.execute(request);
){
// get the response code and body
int responseCode = response.getStatusLine().getStatusCode();
boolean isSuccess = (responseCode / 100) == 2;
String responseBody = response.getEntity() == null ? null : IOUtils.toString(response.getEntity().getContent());
// for 2xx responses, parse the body; for others, throw an exception
if(isSuccess){
// 200 = OK, so parse the response as JSON, then convert to a model using `gson`
if(responseType == null || responseBody == null){
return null;
} else {
return gson.<T>fromJson(responseBody, responseType);
}
} else {
// non-200 = error, so collect some helpful information as an exception
throw new CodeDxClientException(
request.getMethod(),
apiPath,
response.getStatusLine().getReasonPhrase(),
responseCode,
responseBody
);
}
}
}
/**
* Kicks off a CodeDx analysis run on a specified project
*
* (Requires projectId instead of ProjectContext since the use of ProjectContext differs for this endpoint compared to others.
* Other endpoints use the ProjectContext to indicate target project and branch, but this uses ProjectContext to determine
* parent/base branch.)
*
* @return A StartAnalysisResponse object
* @param projectId The project ID
* @param artifacts An array of streams to send over as analysis artifacts
* @throws ClientProtocolException
* @throws IOException
* @throws CodeDxClientException
*
*/
public StartAnalysisResponse startAnalysis(int projectId, String parentBranchName, String targetBranchName, Map<String, InputStream> artifacts) throws IOException, CodeDxClientException {
// // (parent branch is pulled from project context, will use default branch if not set)
ProjectContext project = new ProjectContext(projectId, parentBranchName);
String path = "projects/" + project + "/analysis";
if (targetBranchName != null && targetBranchName.length() > 0) {
path += "?branchName=" + targetBranchName;
}
HttpPost postRequest = new HttpPost(url + path);
postRequest.addHeader(KEY_HEADER, key);
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
for(Map.Entry<String, InputStream> artifact : artifacts.entrySet()){
builder.addPart("file[]", new InputStreamBody(artifact.getValue(), artifact.getKey()));
}
HttpEntity entity = builder.build();
postRequest.setEntity(entity);
HttpResponse response = httpClientBuilder.build().execute(postRequest);
int responseCode = response.getStatusLine().getStatusCode();
if(responseCode != 202){
throw new CodeDxClientException("POST", path, response.getStatusLine().getReasonPhrase(), responseCode, IOUtils.toString(response.getEntity().getContent()));
}
String data = IOUtils.toString(response.getEntity().getContent());
return gson.<StartAnalysisResponse>fromJson(data,new TypeToken<StartAnalysisResponse>(){}.getType());
}
}