Skip to content

Commit d313974

Browse files
authored
feat: export pdf attachments in addition (#297)
Refs: #289
1 parent b1e84b9 commit d313974

File tree

23 files changed

+2356
-1502
lines changed

23 files changed

+2356
-1502
lines changed

docs/openapi.json

Lines changed: 1815 additions & 1438 deletions
Large diffs are not rendered by default.

src/main/java/ch/sbb/polarion/extension/pdf_exporter/rest/PdfExporterRestApplication.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.ConverterInternalController;
88
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.SettingsApiController;
99
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.SettingsInternalController;
10+
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.TestRunAttachmentsApiController;
11+
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.TestRunAttachmentsInternalController;
1012
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.UtilityResourcesApiController;
1113
import ch.sbb.polarion.extension.pdf_exporter.rest.controller.UtilityResourcesInternalController;
1214
import ch.sbb.polarion.extension.pdf_exporter.rest.exception.NoSuchElementExceptionMapper;
@@ -64,6 +66,8 @@ public PdfExporterRestApplication() {
6466
new ConverterInternalController(),
6567
new SettingsApiController(),
6668
new SettingsInternalController(),
69+
new TestRunAttachmentsApiController(),
70+
new TestRunAttachmentsInternalController(),
6771
new UtilityResourcesApiController(),
6872
new UtilityResourcesInternalController()
6973
);
@@ -78,4 +82,4 @@ public PdfExporterRestApplication() {
7882
new NoSuchElementExceptionMapper()
7983
);
8084
}
81-
}
85+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ch.sbb.polarion.extension.pdf_exporter.rest.controller;
2+
3+
import ch.sbb.polarion.extension.generic.rest.filter.Secured;
4+
import ch.sbb.polarion.extension.generic.service.PolarionService;
5+
import ch.sbb.polarion.extension.pdf_exporter.rest.model.attachments.TestRunAttachment;
6+
7+
import javax.ws.rs.Path;
8+
import javax.ws.rs.core.Response;
9+
import java.util.List;
10+
11+
@Secured
12+
@Path("/api")
13+
public class TestRunAttachmentsApiController extends TestRunAttachmentsInternalController {
14+
15+
private static final PolarionService polarionService = new PolarionService();
16+
17+
@Override
18+
public List<TestRunAttachment> getTestRunAttachments(String projectId, String testRunId, String revision, String filter) {
19+
return polarionService.callPrivileged(() -> super.getTestRunAttachments(projectId, testRunId, revision, filter));
20+
}
21+
22+
@Override
23+
public TestRunAttachment getTesRunAttachment(String projectId, String testRunId, String attachmentId, String revision) {
24+
return polarionService.callPrivileged(() -> super.getTesRunAttachment(projectId, testRunId, attachmentId, revision));
25+
}
26+
27+
@Override
28+
public Response getTestRunAttachmentContent(String projectId, String workItemId, String attachmentId, String revision) {
29+
return polarionService.callPrivileged(() -> super.getTestRunAttachmentContent(projectId, workItemId, attachmentId, revision));
30+
}
31+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package ch.sbb.polarion.extension.pdf_exporter.rest.controller;
2+
3+
import ch.sbb.polarion.extension.pdf_exporter.rest.model.attachments.TestRunAttachment;
4+
import ch.sbb.polarion.extension.pdf_exporter.service.PdfExporterPolarionService;
5+
import com.polarion.alm.tracker.model.ITestRunAttachment;
6+
import io.swagger.v3.oas.annotations.Hidden;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.Parameter;
9+
import io.swagger.v3.oas.annotations.media.ArraySchema;
10+
import io.swagger.v3.oas.annotations.media.Content;
11+
import io.swagger.v3.oas.annotations.media.Schema;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
15+
import javax.ws.rs.GET;
16+
import javax.ws.rs.Path;
17+
import javax.ws.rs.PathParam;
18+
import javax.ws.rs.Produces;
19+
import javax.ws.rs.QueryParam;
20+
import javax.ws.rs.core.HttpHeaders;
21+
import javax.ws.rs.core.MediaType;
22+
import javax.ws.rs.core.Response;
23+
import java.util.List;
24+
25+
@Hidden
26+
@Path("/internal")
27+
@Tag(name = "TestRuns")
28+
public class TestRunAttachmentsInternalController {
29+
30+
private static final String FILENAME_HEADER = "Filename";
31+
32+
private final PdfExporterPolarionService pdfExporterPolarionService;
33+
34+
public TestRunAttachmentsInternalController() {
35+
pdfExporterPolarionService = new PdfExporterPolarionService();
36+
}
37+
38+
public TestRunAttachmentsInternalController(PdfExporterPolarionService pdfExporterPolarionService) {
39+
this.pdfExporterPolarionService = pdfExporterPolarionService;
40+
}
41+
42+
@GET
43+
@Path("/projects/{projectId}/testruns/{testRunId}/attachments")
44+
@Produces(MediaType.APPLICATION_JSON)
45+
@Operation(summary = "Gets list of attachments of specified workitem using optional filter",
46+
responses = {
47+
@ApiResponse(responseCode = "200",
48+
description = "Successfully retrieved list of attachments",
49+
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TestRunAttachment.class)))
50+
)
51+
}
52+
)
53+
public List<TestRunAttachment> getTestRunAttachments(
54+
@Parameter(description = "Project ID", required = true) @PathParam("projectId") String projectId,
55+
@Parameter(description = "TestRun ID", required = true) @PathParam("testRunId") String testRunId,
56+
@Parameter(description = "TestRun revision") @QueryParam("revision") String revision,
57+
@Parameter(description = "Filename filter for attachment") @QueryParam("filter") String filter
58+
) {
59+
return pdfExporterPolarionService.getTestRunAttachments(projectId, testRunId, revision, filter);
60+
}
61+
62+
@GET
63+
@Path("/projects/{projectId}/testruns/{testRunId}/attachments/{attachmentId}")
64+
@Produces(MediaType.APPLICATION_JSON)
65+
@Operation(summary = "Gets attachment of specified workitem",
66+
responses = {
67+
@ApiResponse(responseCode = "200",
68+
description = "Successfully retrieved attachment",
69+
content = @Content(schema = @Schema(implementation = TestRunAttachment.class))
70+
)
71+
}
72+
)
73+
public TestRunAttachment getTesRunAttachment(
74+
@Parameter(description = "Project ID", required = true) @PathParam("projectId") String projectId,
75+
@Parameter(description = "TestRun ID", required = true) @PathParam("testRunId") String testRunId,
76+
@Parameter(description = "Attachment ID", required = true) @PathParam("attachmentId") String attachmentId,
77+
@Parameter(description = "Attachment revision") @QueryParam("revision") String revision
78+
) {
79+
ITestRunAttachment testRunAttachment = pdfExporterPolarionService.getTestRunAttachment(projectId, testRunId, attachmentId, revision);
80+
return TestRunAttachment.fromAttachment(testRunAttachment);
81+
}
82+
83+
@GET
84+
@Path("/projects/{projectId}/testruns/{testRunId}/attachments/{attachmentId}/content")
85+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
86+
@Operation(summary = "Gets content of attachment of specified workitem",
87+
responses = {
88+
@ApiResponse(responseCode = "200", description = "Successfully retrieved attachment content")
89+
}
90+
)
91+
public Response getTestRunAttachmentContent(
92+
@Parameter(description = "Project ID", required = true) @PathParam("projectId") String projectId,
93+
@Parameter(description = "TestRun ID", required = true) @PathParam("testRunId") String testRunId,
94+
@Parameter(description = "Attachment ID", required = true) @PathParam("attachmentId") String attachmentId,
95+
@Parameter(description = "Attachment revision") @QueryParam("revision") String revision
96+
) {
97+
ITestRunAttachment testRunAttachment = pdfExporterPolarionService.getTestRunAttachment(projectId, testRunId, attachmentId, revision);
98+
99+
return Response.ok(testRunAttachment.getDataStream())
100+
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + testRunAttachment.getFileName() + "\"")
101+
.header(FILENAME_HEADER, testRunAttachment.getFileName())
102+
.build();
103+
}
104+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package ch.sbb.polarion.extension.pdf_exporter.rest.model.attachments;
2+
3+
import com.polarion.alm.tracker.model.ITestRunAttachment;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
@Builder
11+
@Data
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class TestRunAttachment {
15+
private String id;
16+
private String fileName;
17+
private String revision;
18+
19+
public static @NotNull TestRunAttachment fromAttachment(@NotNull ITestRunAttachment attachment) {
20+
return TestRunAttachment.builder()
21+
.id(attachment.getId())
22+
.fileName(attachment.getFileName())
23+
.revision(attachment.getRevision())
24+
.build();
25+
}
26+
}

src/main/java/ch/sbb/polarion/extension/pdf_exporter/rest/model/conversion/ExportParams.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ public class ExportParams {
9494
@Schema(description = "Map of attributes extracted from the URL")
9595
private Map<String, String> urlQueryParameters;
9696

97+
@Schema(description = "Filter for attachments to be downloaded, example: '*.pdf'")
98+
private String attachmentsFilter;
99+
97100
@Schema(description = "Internal content")
98101
private String internalContent;
99102

src/main/java/ch/sbb/polarion/extension/pdf_exporter/rest/model/settings/stylepackage/StylePackageModel.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class StylePackageModel extends SettingsModel {
4949
private static final String LANGUAGE_ENTRY_NAME = "LANGUAGE";
5050
private static final String LINKED_WORKITEM_ROLES_ENTRY_NAME = "LINKED WORKITEM ROLES";
5151
private static final String EXPOSE_PAGE_WIDTH_VALIDATION_ENTRY_NAME = "EXPOSE PAGE WIDTH VALIDATION";
52+
private static final String ATTACHMENTS_FILTER = "ATTACHMENTS_FILTER";
5253

5354
private String matchingQuery;
5455
private Float weight;
@@ -74,6 +75,7 @@ public class StylePackageModel extends SettingsModel {
7475
private String language;
7576
private List<String> linkedWorkitemRoles;
7677
private boolean exposePageWidthValidation;
78+
private String attachmentsFilter;
7779

7880
@Override
7981
protected String serializeModelData() {
@@ -100,7 +102,8 @@ protected String serializeModelData() {
100102
serializeEntry(CUSTOM_NUMBERED_LIST_STYLES_ENTRY_NAME, customNumberedListStyles) +
101103
serializeEntry(LANGUAGE_ENTRY_NAME, language) +
102104
serializeEntry(LINKED_WORKITEM_ROLES_ENTRY_NAME, linkedWorkitemRoles) +
103-
serializeEntry(EXPOSE_PAGE_WIDTH_VALIDATION_ENTRY_NAME, exposePageWidthValidation);
105+
serializeEntry(EXPOSE_PAGE_WIDTH_VALIDATION_ENTRY_NAME, exposePageWidthValidation) +
106+
serializeEntry(ATTACHMENTS_FILTER, attachmentsFilter);
104107
}
105108

106109
@Override
@@ -130,5 +133,6 @@ protected void deserializeModelData(String serializedString) {
130133
language = deserializeEntry(LANGUAGE_ENTRY_NAME, serializedString);
131134
linkedWorkitemRoles = deserializeListEntry(LINKED_WORKITEM_ROLES_ENTRY_NAME, serializedString, String.class);
132135
exposePageWidthValidation = Boolean.parseBoolean(deserializeEntry(EXPOSE_PAGE_WIDTH_VALIDATION_ENTRY_NAME, serializedString));
136+
attachmentsFilter = deserializeEntry(ATTACHMENTS_FILTER, serializedString);
133137
}
134138
}

src/main/java/ch/sbb/polarion/extension/pdf_exporter/service/PdfExporterPolarionService.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
import ch.sbb.polarion.extension.generic.settings.SettingId;
66
import ch.sbb.polarion.extension.generic.settings.SettingName;
77
import ch.sbb.polarion.extension.generic.util.ScopeUtils;
8+
import ch.sbb.polarion.extension.pdf_exporter.rest.model.attachments.TestRunAttachment;
89
import ch.sbb.polarion.extension.pdf_exporter.rest.model.settings.stylepackage.StylePackageModel;
910
import ch.sbb.polarion.extension.pdf_exporter.rest.model.settings.stylepackage.StylePackageWeightInfo;
1011
import ch.sbb.polarion.extension.pdf_exporter.settings.StylePackageSettings;
12+
import ch.sbb.polarion.extension.pdf_exporter.util.WildcardUtils;
1113
import com.polarion.alm.projects.IProjectService;
14+
import com.polarion.alm.tracker.ITestManagementService;
1215
import com.polarion.alm.tracker.ITrackerService;
1316
import com.polarion.alm.tracker.model.IModule;
17+
import com.polarion.alm.tracker.model.ITestRun;
18+
import com.polarion.alm.tracker.model.ITestRunAttachment;
1419
import com.polarion.alm.tracker.model.ITrackerProject;
1520
import com.polarion.core.util.StringUtils;
1621
import com.polarion.platform.IPlatformService;
1722
import com.polarion.platform.persistence.IDataService;
1823
import com.polarion.platform.persistence.model.IPObjectList;
1924
import com.polarion.platform.security.ISecurityService;
2025
import com.polarion.platform.service.repository.IRepositoryService;
26+
import com.polarion.portal.internal.server.navigation.TestManagementServiceAccessor;
2127
import org.jetbrains.annotations.NotNull;
2228
import org.jetbrains.annotations.Nullable;
2329

@@ -30,13 +36,23 @@
3036

3137
public class PdfExporterPolarionService extends PolarionService {
3238

39+
protected final ITestManagementService testManagementService;
40+
3341
public PdfExporterPolarionService() {
3442
super();
43+
this.testManagementService = new TestManagementServiceAccessor().getTestingService();
3544
}
3645

37-
public PdfExporterPolarionService(@NotNull ITrackerService trackerService, @NotNull IProjectService projectService, @NotNull ISecurityService securityService,
38-
@NotNull IPlatformService platformService, @NotNull IRepositoryService repositoryService) {
46+
public PdfExporterPolarionService(
47+
@NotNull ITrackerService trackerService,
48+
@NotNull IProjectService projectService,
49+
@NotNull ISecurityService securityService,
50+
@NotNull IPlatformService platformService,
51+
@NotNull IRepositoryService repositoryService,
52+
@NotNull ITestManagementService testManagementService
53+
) {
3954
super(trackerService, projectService, securityService, platformService, repositoryService);
55+
this.testManagementService = testManagementService;
4056
}
4157

4258
public @Nullable ITrackerProject getProjectFromScope(@Nullable String scope) {
@@ -116,4 +132,37 @@ private boolean sameDocument(@Nullable String projectId, @NotNull String spaceId
116132
return projectId.equals(document.getProjectId()) && String.format("%s/%s", spaceId, documentName).equals(document.getModuleLocation().getLocationPath());
117133
}
118134
}
135+
136+
public @NotNull ITestRun getTestRun(@NotNull String projectId, @NotNull String testRunId, @Nullable String revision) {
137+
ITestRun testRun = testManagementService.getTestRun(projectId, testRunId, revision);
138+
if (testRun.isUnresolvable()) {
139+
throw new IllegalArgumentException("Test run with id '%s' not found in project '%s'".formatted(testRunId, projectId));
140+
}
141+
return testRun;
142+
}
143+
144+
public @NotNull List<TestRunAttachment> getTestRunAttachments(@NotNull String projectId, @NotNull String testRunId, @Nullable String revision, @Nullable String filter) {
145+
ITestRun testRun = getTestRun(projectId, testRunId, revision);
146+
147+
List<TestRunAttachment> result = new ArrayList<>();
148+
149+
IPObjectList<ITestRunAttachment> workItemAttachments = testRun.getAttachments();
150+
151+
for (ITestRunAttachment testRunAttachment : workItemAttachments) {
152+
if (filter == null || WildcardUtils.matches(testRunAttachment.getFileName(), filter)) {
153+
result.add(TestRunAttachment.fromAttachment(testRunAttachment));
154+
}
155+
}
156+
157+
return result;
158+
}
159+
160+
public @NotNull ITestRunAttachment getTestRunAttachment(@NotNull String projectId, @NotNull String testRunId, @NotNull String attachmentId, @Nullable String revision) {
161+
ITestRun testRun = getTestRun(projectId, testRunId, revision);
162+
ITestRunAttachment testRunAttachment = testRun.getAttachment(attachmentId);
163+
if (testRunAttachment == null) {
164+
throw new IllegalArgumentException("Attachment with id '%s' not found in test run '%s/%s'".formatted(attachmentId, projectId, testRunId));
165+
}
166+
return testRunAttachment;
167+
}
119168
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ch.sbb.polarion.extension.pdf_exporter.util;
2+
3+
import ch.sbb.polarion.extension.generic.regex.RegexMatcher;
4+
import lombok.experimental.UtilityClass;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.jetbrains.annotations.Nullable;
7+
8+
@UtilityClass
9+
public class WildcardUtils {
10+
11+
public static @NotNull String toRegex(@Nullable String wildcard) {
12+
if (wildcard == null || wildcard.isEmpty()) {
13+
return ".*";
14+
}
15+
16+
String regex = wildcard
17+
.replace(".", "\\.")
18+
.replace("?", ".")
19+
.replace("*", ".*");
20+
21+
return "^" + regex + "$";
22+
}
23+
24+
public static boolean matches(@Nullable String text, @Nullable String wildcard) {
25+
if (text == null) {
26+
return false;
27+
}
28+
String regex = toRegex(wildcard);
29+
return RegexMatcher.get(regex, RegexMatcher.CASE_INSENSITIVE).anyMatch(text);
30+
}
31+
}

src/main/java/ch/sbb/polarion/extension/pdf_exporter/util/placeholder/PlaceholderProcessor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public PlaceholderProcessor() {
2626
this.documentDataHelper = new DocumentDataHelper(pdfExporterPolarionService);
2727
}
2828

29+
public PlaceholderProcessor(PdfExporterPolarionService pdfExporterPolarionService) {
30+
this.pdfExporterPolarionService = pdfExporterPolarionService;
31+
this.documentDataHelper = new DocumentDataHelper(pdfExporterPolarionService);
32+
}
33+
2934
public PlaceholderProcessor(PdfExporterPolarionService pdfExporterPolarionService, DocumentDataHelper documentDataHelper) {
3035
this.pdfExporterPolarionService = pdfExporterPolarionService;
3136
this.documentDataHelper = documentDataHelper;

0 commit comments

Comments
 (0)