From 9d3a727ada6bd9da2bde1ba076f733b346253be4 Mon Sep 17 00:00:00 2001 From: Sergey Grigoriev Date: Wed, 19 Jun 2024 21:13:18 +0200 Subject: [PATCH] chore: migrate from BitBucket to GitHub (#3) Refs: DEV-11795 --- README.md | 225 ++++++++++++++-- pom.xml | 11 +- .../extender/ApiExtenderAdminUiServlet.java | 16 ++ .../extender/project/CustomFieldsProject.java | 44 +++ .../api/extender/project/GenericFields.java | 93 +++++++ .../api/extender/project/GlobalRecords.java | 45 ++++ .../rest/ApiExtenderRestApplication.java | 40 +++ .../controller/GlobalRecordApiController.java | 27 ++ .../GlobalRecordInternalController.java | 109 ++++++++ .../ProjectCustomFieldApiController.java | 27 ++ .../ProjectCustomFieldInternalController.java | 111 ++++++++ .../api/extender/rest/model/Field.java | 29 ++ .../extender/rest/model/GenericFields.java | 34 +++ .../api/extender/rest/model/Project.java | 26 ++ .../api/extender/rest/model/Records.java | 14 + .../extender/settings/AuthSettingsModel.java | 46 ++++ .../settings/GlobalRecordsSettings.java | 20 ++ .../settings/GlobalRecordsSettingsModel.java | 36 +++ .../settings/ProjectCustomFieldsSettings.java | 21 ++ .../ProjectCustomFieldsSettingsModel.java | 42 +++ .../api/extender/util/CustomFieldUtils.java | 16 ++ .../api/extender/util/RolesUtils.java | 32 +++ .../velocity/VelocityCustomFieldsProject.java | 16 ++ .../velocity/VelocityGlobalRecords.java | 16 ++ .../VelocityReadOnlyCustomFieldsProject.java | 24 ++ .../VelocityReadOnlyGlobalRecords.java | 24 ++ src/main/resources/META-INF/MANIFEST.MF | 5 +- src/main/resources/META-INF/hivemodule.xml | 48 ++++ src/main/resources/plugin.xml | 19 ++ .../webapp/api-extender-admin/WEB-INF/web.xml | 64 +++++ .../css/api-extender-admin.css | 8 + .../html/help/configuration.css | 33 +++ .../html/help/configuration.html | 253 ++++++++++++++++++ .../api-extender-admin/images/app-icon.svg | 24 ++ .../webapp/api-extender-admin/js/settings.js | 103 +++++++ .../webapp/api-extender-admin/pages/about.jsp | 101 +++++++ .../api-extender-admin/pages/settings.jsp | 88 ++++++ .../webapp/api-extender/WEB-INF/web.xml | 81 ++++++ .../extender/project/GlobalRecordsTest.java | 111 ++++++++ .../GlobalRecordInternalControllerTest.java | 133 +++++++++ ...jectCustomFieldInternalControllerTest.java | 133 +++++++++ .../extender/rest/model/xml/ProjectTest.java | 73 +++++ .../extender/rest/model/xml/RecordsTest.java | 43 +++ .../GlobalRecordsSettingsModelTest.java | 24 ++ src/test/resources/fields.xml | 11 + 45 files changed, 2473 insertions(+), 26 deletions(-) create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/ApiExtenderAdminUiServlet.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/project/CustomFieldsProject.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/project/GenericFields.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecords.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/ApiExtenderRestApplication.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordApiController.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldApiController.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Records.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/settings/AuthSettingsModel.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettings.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModel.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettings.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettingsModel.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/util/CustomFieldUtils.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/util/RolesUtils.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityCustomFieldsProject.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityGlobalRecords.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyCustomFieldsProject.java create mode 100644 src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyGlobalRecords.java create mode 100644 src/main/resources/META-INF/hivemodule.xml create mode 100644 src/main/resources/plugin.xml create mode 100644 src/main/resources/webapp/api-extender-admin/WEB-INF/web.xml create mode 100644 src/main/resources/webapp/api-extender-admin/css/api-extender-admin.css create mode 100644 src/main/resources/webapp/api-extender-admin/html/help/configuration.css create mode 100644 src/main/resources/webapp/api-extender-admin/html/help/configuration.html create mode 100644 src/main/resources/webapp/api-extender-admin/images/app-icon.svg create mode 100644 src/main/resources/webapp/api-extender-admin/js/settings.js create mode 100644 src/main/resources/webapp/api-extender-admin/pages/about.jsp create mode 100644 src/main/resources/webapp/api-extender-admin/pages/settings.jsp create mode 100644 src/main/resources/webapp/api-extender/WEB-INF/web.xml create mode 100644 src/test/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecordsTest.java create mode 100644 src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalControllerTest.java create mode 100644 src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalControllerTest.java create mode 100644 src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/ProjectTest.java create mode 100644 src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/RecordsTest.java create mode 100644 src/test/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModelTest.java create mode 100644 src/test/resources/fields.xml diff --git a/README.md b/README.md index 8cc4b60..f64f65f 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,225 @@ -# Polarion ALM extension to <...> +# API extension for Polarion ALM -This Polarion extension provides possibility to <...> -## Build +This Polarion extension provides additional functionality which is not implemented in standard Polarion API for some reason. -This extension can be produced using maven: +## Custom field for project + +Polarion project does not support setting custom fields out of the box. +This API extension can be used to solve this problem. + +This API can be called using REST API and in Velocity Context. + +### REST API + +Get version: +```bash +curl --location 'https://:/polarion/api-extender/rest/api/version' \ + --header 'Authorization: Bearer ' ``` -mvn clean package +Response example: +```json +{ + "bundleName":"API Extension for Polarion ALM", + "bundleVendor":"SBB AG", + "automaticModuleName":"ch.sbb.polarion.extension.api_extender", + "bundleVersion":"1.0.0", + "bundleBuildTimestamp":"2023-06-27 12:43", + "bundleBuildTimestampDigitsOnly":"202306271243" +} ``` -## Installation to Polarion +Get custom field value: +```bash +curl --location 'https://:/polarion/api-extender/rest/api/projects//keys/' \ + --header 'Authorization: Bearer ' +``` -To install the extension to Polarion `ch.sbb.polarion.extension.-.jar` -should be copied to `/polarion/extensions/ch.sbb.polarion.extension./eclipse/plugins` -It can be done manually or automated using maven build: +Get global record value: +```bash +curl --location 'https://:/polarion/api-extender/rest/api/records/' \ + --header 'Authorization: Bearer ' ``` -mvn clean install -P polarion2304,install-to-local-polarion + +Response example: +```json +{ + "value": "custom_value" +} ``` -For automated installation with maven env variable `POLARION_HOME` should be defined and point to folder where Polarion is installed. -Changes only take effect after restart of Polarion. +Set custom field value: +```bash +curl --location 'https://:/polarion/api-extender/rest/api/projects//keys/' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer ' \ + --data '{ + "value": "" + }' +``` -## Polarion configuration +Set global record value: +```bash +curl --location 'https://:/polarion/api-extender/rest/api/records/' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer ' \ + --data '{ + "value": "" + }' +``` -<...> +### Live Report Page +Get version: +```velocity +#set ($version = $customFieldsProject.getVersion()) +#if ($version) + API Extender version = $version +#end +``` -## Extension Configuration +or -<...> +```velocity +#set ($version = $globalRecords.getVersion()) +#if ($version) + API Extender version = $version +#end +``` + +Get custom field value: +```velocity +#set ($field = $customFieldsProject.getCustomField('elibrary', 'custom_field')) +#if ($field) + $field.getValue() +
+#end +``` + +Get global record value: +```velocity +#set ($field = $globalRecords.getRecord('record_name')) +#if ($field) + $field.getValue() +
+#end +``` + +Due to Polarion limitations we are not able to save custom fields in Live Report Page using Velocity, but we can use JavaScript for this. +Set custom field value: +```html + + + + + +``` -## Usage +Set global record value: +```html + + + + +``` + +Note that internal API in URL should be used. + +### Classic Wiki Page + +Get custom field value: +```velocity +#set($projects = $projectService.searchProjects("","id")) + +#foreach($project in $projects) + #set($projectId = $project.id) + #set($field = $customFieldsProject.getCustomField($projectId, 'custom_field')) + #if ($field) + $projectId custom_field = $field.getValue() +
+ #set($field = false) + #end +#end +``` + +Get global record value: + +```velocity +$globalRecords.getRecord('record_name') +``` + +Set custom field value: + +```velocity +$customFieldsProject.setCustomField('elibrary', 'custom_field', 'new_value') +``` + +Set global record value: + +```velocity +$globalRecords.setRecord('record_name', 'record_value') +``` -<...> +## Changelog +| Version | Changes | +|---------|----------------------------------------| +| v1.3.0 | About page extended with help and icon | +| v1.2.0 | Endpoints adjusted | +| v1.1.4 | Global records added | +| v1.1.3 | Permissions validation added | +| v1.1.2 | Some bug fixed | +| v1.1.1 | Swagger page added to admin panel | +| v1.1.0 | Swagger page added | +| v1.0.0 | Initial release | diff --git a/pom.xml b/pom.xml index e318fb2..280acdb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,16 +5,16 @@ ch.sbb.polarion.extensions ch.sbb.polarion.extension.generic.parent-pom - 2.0.0 + 4.10.0 - ch.sbb.polarion.extension.extension-name - 0.0.0-SNAPSHOT + ch.sbb.polarion.extension.api-extender + 1.3.1-SNAPSHOT jar - extension-name - ch.sbb.polarion.extension.extension_name + api-extender + ch.sbb.polarion.extension.api_extender ${maven-jar-plugin.Extension-Context} @@ -50,4 +50,5 @@ + diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/ApiExtenderAdminUiServlet.java b/src/main/java/ch/sbb/polarion/extension/api/extender/ApiExtenderAdminUiServlet.java new file mode 100644 index 0000000..456dea8 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/ApiExtenderAdminUiServlet.java @@ -0,0 +1,16 @@ +package ch.sbb.polarion.extension.api.extender; + +import ch.sbb.polarion.extension.generic.GenericUiServlet; + +import java.io.Serial; + +public class ApiExtenderAdminUiServlet extends GenericUiServlet { + + @Serial + private static final long serialVersionUID = 4272543916738749821L; + + public ApiExtenderAdminUiServlet() { + super("api-extender-admin"); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/project/CustomFieldsProject.java b/src/main/java/ch/sbb/polarion/extension/api/extender/project/CustomFieldsProject.java new file mode 100644 index 0000000..e044f60 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/project/CustomFieldsProject.java @@ -0,0 +1,44 @@ +package ch.sbb.polarion.extension.api.extender.project; + +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.rest.model.Project; +import ch.sbb.polarion.extension.generic.service.PolarionService; +import com.polarion.alm.projects.model.IProject; +import com.polarion.platform.core.PlatformContext; +import com.polarion.platform.service.repository.IRepositoryService; +import com.polarion.subterra.base.location.ILocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.bind.JAXBException; +import java.io.IOException; + +public class CustomFieldsProject extends GenericFields { + private static final String POLARION_POLARION_PROJECT_XML = ".polarion/polarion-project.xml"; + + private final String projectId; + + public CustomFieldsProject(String projectId) { + super(PlatformContext.getPlatform().lookupService(IRepositoryService.class)); + this.projectId = projectId; + } + + @Nullable + public Field getCustomField(@NotNull String key) throws JAXBException { + final Project project = deserialize(); + return project.getField(key); + } + + public void setCustomField(@NotNull String key, @Nullable String value) throws JAXBException, IOException { + final Project project = deserialize(); + project.setField(key, value); + serialize(project); + } + + @Override + protected ILocation getLocation() { + IProject project = new PolarionService().getProject(projectId); + return project.getLocation().append(POLARION_POLARION_PROJECT_XML); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/project/GenericFields.java b/src/main/java/ch/sbb/polarion/extension/api/extender/project/GenericFields.java new file mode 100644 index 0000000..2e9e661 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/project/GenericFields.java @@ -0,0 +1,93 @@ +package ch.sbb.polarion.extension.api.extender.project; + +import ch.sbb.polarion.extension.generic.jaxb.JAXBUtils; +import com.polarion.alm.shared.api.transaction.TransactionalExecutor; +import com.polarion.core.util.logging.Logger; +import com.polarion.platform.service.repository.IRepositoryConnection; +import com.polarion.platform.service.repository.IRepositoryReadOnlyConnection; +import com.polarion.platform.service.repository.IRepositoryService; +import com.polarion.subterra.base.location.ILocation; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.bind.JAXBException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.ParameterizedType; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +@SuppressWarnings("unchecked") +public abstract class GenericFields { + protected static final Logger logger = Logger.getLogger(GenericFields.class); + protected final IRepositoryService repositoryService; + + protected final Class type; + + protected GenericFields(IRepositoryService repositoryService) { + this.repositoryService = repositoryService; + this.type = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; + } + + protected abstract ILocation getLocation(); + + @NotNull + protected T deserialize() throws JAXBException { + final ILocation location = getLocation(); + final String content = read(location); + if (content == null) { + return createT(); + } else { + return Objects.requireNonNull(JAXBUtils.deserialize(type, content)); + } + } + + protected void serialize(@NotNull T records) throws JAXBException, IOException { + final ILocation location = getLocation(); + final String content = JAXBUtils.serialize(records); + save(location, content); + } + + @Nullable + protected String read(@NotNull ILocation location) { + return TransactionalExecutor.executeSafelyInReadOnlyTransaction(transaction -> { + IRepositoryReadOnlyConnection readOnlyConnection = repositoryService.getReadOnlyConnection(location); + if (!readOnlyConnection.exists(location)) { + logger.warn("Location does not exist: " + location.getLocationPath()); + return null; + } + + try (InputStream inputStream = readOnlyConnection.getContent(location)) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + logger.error("Error reading content from: " + location.getLocationPath(), e); + return null; + } + }); + } + + protected void save(@NotNull ILocation location, @NotNull String content) { + TransactionalExecutor.executeInWriteTransaction(transaction -> { + IRepositoryConnection connection = repositoryService.getConnection(location); + try (InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { + if (connection.exists(location)) { + connection.setContent(location, inputStream); + } else { + connection.create(location, inputStream); + } + } catch (IOException e) { + logger.error("Can't save to location: " + location, e); + } + return null; + }); + } + + + @SneakyThrows + T createT() { + return type.getDeclaredConstructor().newInstance(); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecords.java b/src/main/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecords.java new file mode 100644 index 0000000..8f2d94a --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecords.java @@ -0,0 +1,45 @@ +package ch.sbb.polarion.extension.api.extender.project; + +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.rest.model.Records; +import ch.sbb.polarion.extension.generic.util.ScopeUtils; +import com.polarion.platform.core.PlatformContext; +import com.polarion.platform.service.repository.IRepositoryService; +import com.polarion.subterra.base.location.ILocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import javax.xml.bind.JAXBException; +import java.io.IOException; + +public class GlobalRecords extends GenericFields { + private static final String POLARION_RECORDS_XML = ".polarion/records.xml"; + + public GlobalRecords() { + super(PlatformContext.getPlatform().lookupService(IRepositoryService.class)); + } + + @VisibleForTesting + GlobalRecords(IRepositoryService repositoryService) { + super(repositoryService); + } + + @Override + protected ILocation getLocation() { + return ScopeUtils.getDefaultLocation().append(POLARION_RECORDS_XML); + } + + @Nullable + public Field getRecord(@NotNull String key) throws JAXBException { + final Records records = deserialize(); + return records.getField(key); + } + + public void setRecord(@NotNull String key, @Nullable String value) throws JAXBException, IOException { + final Records records = deserialize(); + records.setField(key, value); + serialize(records); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/ApiExtenderRestApplication.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/ApiExtenderRestApplication.java new file mode 100644 index 0000000..f89e470 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/ApiExtenderRestApplication.java @@ -0,0 +1,40 @@ +package ch.sbb.polarion.extension.api.extender.rest; + +import ch.sbb.polarion.extension.api.extender.rest.controller.GlobalRecordApiController; +import ch.sbb.polarion.extension.api.extender.rest.controller.GlobalRecordInternalController; +import ch.sbb.polarion.extension.api.extender.rest.controller.ProjectCustomFieldApiController; +import ch.sbb.polarion.extension.api.extender.rest.controller.ProjectCustomFieldInternalController; +import ch.sbb.polarion.extension.api.extender.settings.GlobalRecordsSettings; +import ch.sbb.polarion.extension.api.extender.settings.ProjectCustomFieldsSettings; +import ch.sbb.polarion.extension.generic.rest.GenericRestApplication; +import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings; +import ch.sbb.polarion.extension.generic.settings.NamedSettingsRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class ApiExtenderRestApplication extends GenericRestApplication { + + public ApiExtenderRestApplication() { + List> settingsList = new ArrayList<>(); + settingsList.add(new GlobalRecordsSettings()); + settingsList.add(new ProjectCustomFieldsSettings()); + + NamedSettingsRegistry.INSTANCE.register(settingsList); + } + + @Override + @NotNull + protected Set> getControllerClasses() { + final Set> controllerClasses = super.getControllerClasses(); + controllerClasses.addAll(Set.of( + GlobalRecordApiController.class, + GlobalRecordInternalController.class, + ProjectCustomFieldApiController.class, + ProjectCustomFieldInternalController.class + )); + return controllerClasses; + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordApiController.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordApiController.java new file mode 100644 index 0000000..ed145b8 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordApiController.java @@ -0,0 +1,27 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.generic.rest.filter.Secured; + +import javax.ws.rs.Path; + +@Secured +@Path("/api") +public class GlobalRecordApiController extends GlobalRecordInternalController { + + @Override + public Field getRecordValue(String key) { + return polarionService.callPrivileged(() -> super.getRecordValue(key)); + } + + @Override + public void setRecordValue(String key, Field field) { + polarionService.callPrivileged(() -> super.setRecordValue(key, field)); + } + + @Override + public void deleteRecordValue(String key) { + polarionService.callPrivileged(() -> super.deleteRecordValue(key)); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java new file mode 100644 index 0000000..796b0c7 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalController.java @@ -0,0 +1,109 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import ch.sbb.polarion.extension.api.extender.project.GlobalRecords; +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.settings.GlobalRecordsSettings; +import ch.sbb.polarion.extension.api.extender.settings.GlobalRecordsSettingsModel; +import ch.sbb.polarion.extension.generic.service.PolarionService; +import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings; +import ch.sbb.polarion.extension.generic.settings.NamedSettings; +import ch.sbb.polarion.extension.generic.settings.NamedSettingsRegistry; +import ch.sbb.polarion.extension.generic.settings.SettingId; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.SneakyThrows; +import org.jetbrains.annotations.VisibleForTesting; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.xml.bind.JAXBException; +import java.util.Collection; +import java.util.List; + +@Tag(name = "Global Records") +@Hidden +@Path("/internal") +public class GlobalRecordInternalController { + + protected final PolarionService polarionService; + + public GlobalRecordInternalController() { + polarionService = new PolarionService(); + } + + @VisibleForTesting + GlobalRecordInternalController(PolarionService polarionService) { + this.polarionService = polarionService; + } + + @Operation(summary = "Returns global record value") + @GET + @Path("/records/{key}") + @Produces(MediaType.APPLICATION_JSON) + public Field getRecordValue(@PathParam("key") String key) throws JAXBException { + final GlobalRecords globalRecords = new GlobalRecords(); + Field field = globalRecords.getRecord(key); + if (field == null) { + throw new NotFoundException("key '" + key + "' not found"); + } else { + return field; + } + } + + @Operation(summary = "Saves global record") + @POST + @Path("/records/{key}") + @Consumes(MediaType.APPLICATION_JSON) + @SneakyThrows + public void setRecordValue(@PathParam("key") String key, Field field) { + checkPermissions(); + + if (field == null) { + throw new IllegalArgumentException("value is not provided"); + } + + final GlobalRecords globalRecords = new GlobalRecords(); + globalRecords.setRecord(key, field.getValue()); + } + + @Operation(summary = "Removes global record") + @DELETE + @Path("/records/{key}") + @Consumes(MediaType.APPLICATION_JSON) + @SneakyThrows + public void deleteRecordValue(@PathParam("key") String key) { + checkPermissions(); + + final GlobalRecords globalRecords = new GlobalRecords(); + globalRecords.setRecord(key, null); + } + + private void checkPermissions() { + if (!isModificationAllowed()) { + throw new ForbiddenException("You are not authorized to modify records"); + } + } + + private boolean isModificationAllowed() { + String currentUser = polarionService.getSecurityService().getCurrentUser(); + Collection userRoles = polarionService.getSecurityService().getRolesForUser(currentUser); + + GlobalRecordsSettingsModel globalRecordsSettingsModel = (GlobalRecordsSettingsModel) NamedSettingsRegistry.INSTANCE.getByFeatureName(GlobalRecordsSettings.FEATURE_NAME) + .read(GenericNamedSettings.DEFAULT_SCOPE, SettingId.fromName(NamedSettings.DEFAULT_NAME), null); + + List allowedRoles = globalRecordsSettingsModel.getAllRoles(); + + return userRoles.stream() + .anyMatch(allowedRoles::contains); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldApiController.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldApiController.java new file mode 100644 index 0000000..f52e74c --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldApiController.java @@ -0,0 +1,27 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.generic.rest.filter.Secured; + +import javax.ws.rs.Path; + +@Secured +@Path("/api") +public class ProjectCustomFieldApiController extends ProjectCustomFieldInternalController { + + @Override + public Field getCustomValue(String projectId, String key) { + return polarionService.callPrivileged(() -> super.getCustomValue(projectId, key)); + } + + @Override + public void setCustomValue(String projectId, String key, Field field) { + polarionService.callPrivileged(() -> super.setCustomValue(projectId, key, field)); + } + + @Override + public void deleteCustomValue(String projectId, String key) { + polarionService.callPrivileged(() -> super.deleteCustomValue(projectId, key)); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java new file mode 100644 index 0000000..c8e0887 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalController.java @@ -0,0 +1,111 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import ch.sbb.polarion.extension.api.extender.project.CustomFieldsProject; +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.settings.ProjectCustomFieldsSettings; +import ch.sbb.polarion.extension.api.extender.settings.ProjectCustomFieldsSettingsModel; +import ch.sbb.polarion.extension.generic.service.PolarionService; +import ch.sbb.polarion.extension.generic.settings.NamedSettings; +import ch.sbb.polarion.extension.generic.settings.NamedSettingsRegistry; +import ch.sbb.polarion.extension.generic.settings.SettingId; +import ch.sbb.polarion.extension.generic.util.ScopeUtils; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.SneakyThrows; +import org.jetbrains.annotations.VisibleForTesting; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.xml.bind.JAXBException; +import java.util.Collection; +import java.util.List; + +@Tag(name = "Project custom fields") +@Hidden +@Path("/internal") +public class ProjectCustomFieldInternalController { + + protected final PolarionService polarionService; + + public ProjectCustomFieldInternalController() { + this.polarionService = new PolarionService(); + } + + @VisibleForTesting + ProjectCustomFieldInternalController(PolarionService polarionService) { + this.polarionService = polarionService; + } + + @Operation(summary = "Returns custom field value") + @GET + @Path("/projects/{projectId}/keys/{key}") + @Produces(MediaType.APPLICATION_JSON) + public Field getCustomValue(@PathParam("projectId") String projectId, @PathParam("key") String key) throws JAXBException { + final CustomFieldsProject customFieldsProject = new CustomFieldsProject(projectId); + Field field = customFieldsProject.getCustomField(key); + if (field == null) { + throw new NotFoundException("key '" + key + "' for project '" + projectId + "' not found"); + } else { + return field; + } + } + + @Operation(summary = "Saves custom field") + @POST + @Path("/projects/{projectId}/keys/{key}") + @Consumes(MediaType.APPLICATION_JSON) + @SneakyThrows + public void setCustomValue(@PathParam("projectId") String projectId, @PathParam("key") String key, Field field) { + checkPermissions(projectId); + + if (field == null) { + throw new IllegalArgumentException("value is not provided"); + } + + final CustomFieldsProject customFieldsProject = new CustomFieldsProject(projectId); + customFieldsProject.setCustomField(key, field.getValue()); + } + + @Operation(summary = "Removes custom field") + @DELETE + @Path("/projects/{projectId}/keys/{key}") + @Consumes(MediaType.APPLICATION_JSON) + @SneakyThrows + public void deleteCustomValue(@PathParam("projectId") String projectId, @PathParam("key") String key) { + checkPermissions(projectId); + + final CustomFieldsProject customFieldsProject = new CustomFieldsProject(projectId); + customFieldsProject.setCustomField(key, null); + } + + private void checkPermissions(String projectId) { + String scope = ScopeUtils.getScopeFromProject(projectId); + if (!isModificationAllowed(scope)) { + throw new ForbiddenException("You are not authorized to modify custom fields"); + } + } + + private boolean isModificationAllowed(String scope) { + String currentUser = polarionService.getSecurityService().getCurrentUser(); + Collection userRoles = polarionService.getSecurityService().getRolesForUser(currentUser); + + ProjectCustomFieldsSettingsModel projectCustomFieldsSettingsModel = (ProjectCustomFieldsSettingsModel) + NamedSettingsRegistry.INSTANCE.getByFeatureName(ProjectCustomFieldsSettings.FEATURE_NAME).read( + scope, SettingId.fromName(NamedSettings.DEFAULT_NAME), null); + + List allowedRoles = projectCustomFieldsSettingsModel.getAllRoles(); + + return userRoles.stream() + .anyMatch(allowedRoles::contains); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java new file mode 100644 index 0000000..edeb0e4 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Field.java @@ -0,0 +1,29 @@ +package ch.sbb.polarion.extension.api.extender.rest.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "field") +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@ToString +public class Field { + @XmlAttribute(name = "id") + @JsonIgnore + public String key; + @XmlValue + public String value; +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java new file mode 100644 index 0000000..8c62ae4 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/GenericFields.java @@ -0,0 +1,34 @@ +package ch.sbb.polarion.extension.api.extender.rest.model; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.bind.annotation.XmlElement; +import java.util.ArrayList; +import java.util.List; + +public abstract class GenericFields { + @XmlElement(name = "field") + protected List fields = new ArrayList<>(); + + public void setField(@NotNull String key, @Nullable String value) { + if (value == null) { + fields.removeIf(f -> f.getKey().equals(key)); + } else { + final Field field = getField(key); + if (field != null) { + field.setValue(value); + } else { + fields.add(new Field(key, value)); + } + } + } + + @Nullable + public Field getField(@NotNull String key) { + return fields.stream() + .filter(f -> f.getKey().equals(key)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java new file mode 100644 index 0000000..1e96ca9 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Project.java @@ -0,0 +1,26 @@ +package ch.sbb.polarion.extension.api.extender.rest.model; + +import ch.sbb.polarion.extension.api.extender.util.CustomFieldUtils; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "project") +@ToString +public class Project extends GenericFields { + + @Override + public void setField(@NotNull String key, @Nullable String value) { + if (CustomFieldUtils.isStandardField(key)) { + throw new IllegalArgumentException("'" + key + "' is a standard field of project"); + } + + super.setField(key, value); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Records.java b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Records.java new file mode 100644 index 0000000..b408501 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/rest/model/Records.java @@ -0,0 +1,14 @@ +package ch.sbb.polarion.extension.api.extender.rest.model; + +import lombok.ToString; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "records") +@ToString +public class Records extends GenericFields { + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/settings/AuthSettingsModel.java b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/AuthSettingsModel.java new file mode 100644 index 0000000..e824d6f --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/AuthSettingsModel.java @@ -0,0 +1,46 @@ +package ch.sbb.polarion.extension.api.extender.settings; + +import ch.sbb.polarion.extension.generic.settings.SettingsModel; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.polarion.core.util.StringUtils; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("squid:S2160") //ignore suggestion to create equals() - parent's implementation is sufficient by design +public abstract class AuthSettingsModel extends SettingsModel { + + public static final String GLOBAL_ROLES = "globalRoles"; + public static final String PROJECT_ROLES = "projectRoles"; + + @Getter + protected List globalRoles; + @Getter + protected List projectRoles; + + public void setGlobalRoles(String... roles) { + globalRoles = Arrays.asList(roles); + } + + public void setProjectRoles(String... roles) { + projectRoles = Arrays.asList(roles); + } + + @JsonIgnore + public abstract List getAllRoles(); + + @NotNull + protected String serializeRoles(@Nullable List roles) { + return roles == null ? "" : String.join(",", roles); + } + + @NotNull + protected List deserializeRoles(@NotNull String what, @NotNull String serializedString) { + final String roles = deserializeEntry(what, serializedString); + return Arrays.stream(roles.split(",")).filter(s -> !StringUtils.isEmpty(s)).map(String::trim).toList(); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettings.java b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettings.java new file mode 100644 index 0000000..87ba727 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettings.java @@ -0,0 +1,20 @@ +package ch.sbb.polarion.extension.api.extender.settings; + +import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings; +import org.jetbrains.annotations.NotNull; + +public class GlobalRecordsSettings extends GenericNamedSettings { + public static final String FEATURE_NAME = "global_records"; + + public GlobalRecordsSettings() { + super(FEATURE_NAME); + } + + @Override + public @NotNull GlobalRecordsSettingsModel defaultValues() { + final GlobalRecordsSettingsModel globalRecordsSettingsModel = new GlobalRecordsSettingsModel(); + globalRecordsSettingsModel.setGlobalRoles("admin"); + return globalRecordsSettingsModel; + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModel.java b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModel.java new file mode 100644 index 0000000..2d0298b --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModel.java @@ -0,0 +1,36 @@ +package ch.sbb.polarion.extension.api.extender.settings; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GlobalRecordsSettingsModel extends AuthSettingsModel { + + @Override + protected String serializeModelData() { + return serializeEntry(GLOBAL_ROLES, serializeRoles(globalRoles)); + } + + @Override + protected void deserializeModelData(String serializedString) { + globalRoles = deserializeRoles(GLOBAL_ROLES, serializedString); + } + + @JsonIgnore + public List getAllRoles() { + return getGlobalRoles(); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettings.java b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettings.java new file mode 100644 index 0000000..d00d523 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettings.java @@ -0,0 +1,21 @@ +package ch.sbb.polarion.extension.api.extender.settings; + +import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings; +import org.jetbrains.annotations.NotNull; + +public class ProjectCustomFieldsSettings extends GenericNamedSettings { + public static final String FEATURE_NAME = "project_custom_fields"; + + public ProjectCustomFieldsSettings() { + super(FEATURE_NAME); + } + + @Override + public @NotNull ProjectCustomFieldsSettingsModel defaultValues() { + final ProjectCustomFieldsSettingsModel projectCustomFieldsSettingsModel = new ProjectCustomFieldsSettingsModel(); + projectCustomFieldsSettingsModel.setGlobalRoles("admin"); + projectCustomFieldsSettingsModel.setProjectRoles(); + return projectCustomFieldsSettingsModel; + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettingsModel.java b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettingsModel.java new file mode 100644 index 0000000..84cc441 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/settings/ProjectCustomFieldsSettingsModel.java @@ -0,0 +1,42 @@ +package ch.sbb.polarion.extension.api.extender.settings; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@ToString +@EqualsAndHashCode(callSuper = false) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProjectCustomFieldsSettingsModel extends AuthSettingsModel { + + @Override + protected String serializeModelData() { + return serializeEntry(GLOBAL_ROLES, serializeRoles(globalRoles)) + + serializeEntry(PROJECT_ROLES, serializeRoles(projectRoles)); + } + + @Override + protected void deserializeModelData(String serializedString) { + globalRoles = deserializeRoles(GLOBAL_ROLES, serializedString); + projectRoles = deserializeRoles(PROJECT_ROLES, serializedString); + } + + @JsonIgnore + public List getAllRoles() { + List roles = new ArrayList<>(); + roles.addAll(getGlobalRoles()); + roles.addAll(getProjectRoles()); + return roles; + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/util/CustomFieldUtils.java b/src/main/java/ch/sbb/polarion/extension/api/extender/util/CustomFieldUtils.java new file mode 100644 index 0000000..2f25f81 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/util/CustomFieldUtils.java @@ -0,0 +1,16 @@ +package ch.sbb.polarion.extension.api.extender.util; + +import com.polarion.alm.projects.model.IProject; +import com.polarion.platform.core.PlatformContext; +import com.polarion.platform.persistence.IDataService; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class CustomFieldUtils { + + public static boolean isStandardField(String key) { + return PlatformContext.getPlatform().lookupService(IDataService.class) + .getPrototype(IProject.PROTO) + .isKeyDefined(key); + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/util/RolesUtils.java b/src/main/java/ch/sbb/polarion/extension/api/extender/util/RolesUtils.java new file mode 100644 index 0000000..cc989e6 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/util/RolesUtils.java @@ -0,0 +1,32 @@ +package ch.sbb.polarion.extension.api.extender.util; + +import ch.sbb.polarion.extension.generic.util.ScopeUtils; +import com.polarion.alm.projects.IProjectService; +import com.polarion.alm.projects.model.IProject; +import com.polarion.platform.core.PlatformContext; +import com.polarion.platform.security.ISecurityService; +import lombok.experimental.UtilityClass; + +import java.util.Collection; +import java.util.Set; + +@UtilityClass +public class RolesUtils { + + private static final ISecurityService securityService = PlatformContext.getPlatform().lookupService(ISecurityService.class); + private static final IProjectService projectService = PlatformContext.getPlatform().lookupService(IProjectService.class); + + public static Collection getGlobalRoles() { + return securityService.getGlobalRoles(); + } + + public static Collection getProjectRoles(String scope) { + String projectId = ScopeUtils.getProjectFromScope(scope); + if (projectId == null) { + return Set.of(); + } + IProject project = projectService.getProject(projectId); + return securityService.getContextRoles(project.getContextId()); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityCustomFieldsProject.java b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityCustomFieldsProject.java new file mode 100644 index 0000000..b318fb8 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityCustomFieldsProject.java @@ -0,0 +1,16 @@ +package ch.sbb.polarion.extension.api.extender.velocity; + +import ch.sbb.polarion.extension.api.extender.project.CustomFieldsProject; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import javax.xml.bind.JAXBException; +import java.io.IOException; + +@NoArgsConstructor +public class VelocityCustomFieldsProject extends VelocityReadOnlyCustomFieldsProject { + + public void setCustomField(@NotNull String projectId, @NotNull String key, @NotNull String value) throws JAXBException, IOException { + new CustomFieldsProject(projectId).setCustomField(key, value); + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityGlobalRecords.java b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityGlobalRecords.java new file mode 100644 index 0000000..ad7a116 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityGlobalRecords.java @@ -0,0 +1,16 @@ +package ch.sbb.polarion.extension.api.extender.velocity; + +import ch.sbb.polarion.extension.api.extender.project.GlobalRecords; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import javax.xml.bind.JAXBException; +import java.io.IOException; + +@NoArgsConstructor +public class VelocityGlobalRecords extends VelocityReadOnlyGlobalRecords { + + public void setField(@NotNull String key, @NotNull String value) throws JAXBException, IOException { + new GlobalRecords().setRecord(key, value); + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyCustomFieldsProject.java b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyCustomFieldsProject.java new file mode 100644 index 0000000..dd50120 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyCustomFieldsProject.java @@ -0,0 +1,24 @@ +package ch.sbb.polarion.extension.api.extender.velocity; + +import ch.sbb.polarion.extension.api.extender.project.CustomFieldsProject; +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.generic.util.VersionUtils; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.bind.JAXBException; + +@NoArgsConstructor +public class VelocityReadOnlyCustomFieldsProject { + + @Nullable + public String getVersion() { + return VersionUtils.getVersion().getBundleVersion(); + } + + @Nullable + public Field getCustomField(@NotNull String projectId, @NotNull String key) throws JAXBException { + return new CustomFieldsProject(projectId).getCustomField(key); + } +} diff --git a/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyGlobalRecords.java b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyGlobalRecords.java new file mode 100644 index 0000000..db77e4d --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/api/extender/velocity/VelocityReadOnlyGlobalRecords.java @@ -0,0 +1,24 @@ +package ch.sbb.polarion.extension.api.extender.velocity; + +import ch.sbb.polarion.extension.api.extender.project.GlobalRecords; +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.generic.util.VersionUtils; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.bind.JAXBException; + +@NoArgsConstructor +public class VelocityReadOnlyGlobalRecords { + + @Nullable + public String getVersion() { + return VersionUtils.getVersion().getBundleVersion(); + } + + @Nullable + public Field getRecord(@NotNull String key) throws JAXBException { + return new GlobalRecords().getRecord(key); + } +} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF index e38ad38..4eccca0 100644 --- a/src/main/resources/META-INF/MANIFEST.MF +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -1,5 +1,5 @@ Support-Email: polarion-opensource@sbb.ch -Bundle-Name: Extension for Polarion ALM +Bundle-Name: API Extension for Polarion ALM Require-Bundle: com.polarion.portal.tomcat, com.polarion.alm.ui, com.polarion.platform.guice, @@ -8,7 +8,6 @@ Require-Bundle: com.polarion.portal.tomcat, com.fasterxml.jackson, com.fasterxml.jackson.jaxrs, io.swagger, - org.apache.commons.logging, - slf4j.api, org.springframework.spring-core, org.springframework.spring-web +Export-Package: ch.sbb.polarion.extension.api.extender diff --git a/src/main/resources/META-INF/hivemodule.xml b/src/main/resources/META-INF/hivemodule.xml new file mode 100644 index 0000000..e131e07 --- /dev/null +++ b/src/main/resources/META-INF/hivemodule.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/plugin.xml b/src/main/resources/plugin.xml new file mode 100644 index 0000000..042477a --- /dev/null +++ b/src/main/resources/plugin.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/webapp/api-extender-admin/WEB-INF/web.xml b/src/main/resources/webapp/api-extender-admin/WEB-INF/web.xml new file mode 100644 index 0000000..d4c289f --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/WEB-INF/web.xml @@ -0,0 +1,64 @@ + + + + api-extender-admin + + + DoAsFilter + com.polarion.portal.tomcat.servlets.DoAsFilter + + + + DoAsFilter + /* + + + + api-extender-admin-ui + ch.sbb.polarion.extension.api.extender.ApiExtenderAdminUiServlet + + + debug + 0 + + 1 + + + + api-extender-admin-ui + /ui/* + + + + 30 + + + + log + text/plain + + + + + All + /* + + + user + + + + + + FORM + PolarionRealm + + /login/login + /login/error + + + \ No newline at end of file diff --git a/src/main/resources/webapp/api-extender-admin/css/api-extender-admin.css b/src/main/resources/webapp/api-extender-admin/css/api-extender-admin.css new file mode 100644 index 0000000..42864bd --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/css/api-extender-admin.css @@ -0,0 +1,8 @@ +.roles_table { + margin: 20px; +} + +.roles_table table td { + vertical-align: middle; + padding: 3px; +} \ No newline at end of file diff --git a/src/main/resources/webapp/api-extender-admin/html/help/configuration.css b/src/main/resources/webapp/api-extender-admin/html/help/configuration.css new file mode 100644 index 0000000..7ffb408 --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/html/help/configuration.css @@ -0,0 +1,33 @@ +.cm-builtin { + color: #09326C; +} +.cm-attribute { + color: #216E4E; +} +.cm-string { + color: #974F0C; +} +.cm-property { + color: #216E4E; +} +.cm-keyword { + color: #AE2A19; +} +.cm-operator { + color: #172B4D; +} +.cm-tag { + color: #0C66E4; +} +.cm-bracket { + color: #172B4D; +} +.cm-def { + color: #216E4E; +} +.cm-variable { + color: #44546F; +} +.cm-variable-2 { + color: #44546F; +} diff --git a/src/main/resources/webapp/api-extender-admin/html/help/configuration.html b/src/main/resources/webapp/api-extender-admin/html/help/configuration.html new file mode 100644 index 0000000..39d6b47 --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/html/help/configuration.html @@ -0,0 +1,253 @@ +

API extension for Polarion ALM

+ +

This Polarion extension provides additional functionality which is not implemented in standard Polarion API for some reason.

+ +

Custom field for project

+ +

Polarion project does not support setting custom fields out of the box. This API extension can be used to solve this problem.

+

This API can be called using REST API and in Velocity Context.

+ +

REST API

+ +

Get version:

+
+    
+curl --location 'https://<HOST>:<PORT>/polarion/api-extender/rest/api/version' \
+    --header 'Authorization: Bearer <TOKEN_GOES_HERE>'
+    
+
+ +

Response example:

+
+    
+{
+  "bundleName":"API Extension for Polarion ALM",
+  "bundleVendor":"SBB AG",
+  "automaticModuleName":"ch.sbb.polarion.extension.api_extender",
+  "bundleVersion":"1.0.0",
+  "bundleBuildTimestamp":"2023-06-27 12:43",
+  "bundleBuildTimestampDigitsOnly":"202306271243"
+}
+    
+
+ +

Get custom field value:

+
+    
+curl --location 'https://<HOST>:<PORT>/polarion/api-extender/rest/api/projects/<PROJECT_ID>/keys/<CUSTOM_FIELD>' \
+    --header 'Authorization: Bearer <TOKEN_GOES_HERE>'
+    
+
+ +

Get global record value:

+
+    
+curl --location 'https://<HOST>:<PORT>/polarion/api-extender/rest/api/records/<RECORD>' \
+    --header 'Authorization: Bearer <TOKEN_GOES_HERE>'
+    
+
+ +

Response example:

+
+    
+{
+  "value": "custom_value"
+}
+    
+
+ +

Set custom field value:

+
+    
+curl --location 'https://<HOST>:<PORT>/polarion/api-extender/rest/api/projects/<PROJECT_ID>/keys/<CUSTOM_FIELD>' \
+    --header 'Content-Type: application/json' \
+    --header 'Authorization: Bearer <TOKEN_GOES_HERE>' \
+    --data '{
+          "value": "<VALUE>"
+        }'
+    
+
+ +

Set global record value:

+
+    
+curl --location 'https://<HOST>:<PORT>/polarion/api-extender/rest/api/records/<RECORD>' \
+    --header 'Content-Type: application/json' \
+    --header 'Authorization: Bearer <TOKEN_GOES_HERE>' \
+    --data '{
+          "value": "<VALUE>"
+        }'
+    
+
+ +

Live Report Page

+ +

Get version:

+
+    
+#set ($version = $customFieldsProject.getVersion())
+#if ($version)
+    API Extender version = $version
+#end
+    
+
+

or

+
+    
+#set ($version = $globalRecords.getVersion())
+#if ($version)
+    API Extender version = $version
+#end
+    
+
+ +

Get custom field value:

+
+    
+#set ($field = $customFieldsProject.getCustomField('elibrary', 'custom_field'))
+#if ($field)
+    $field.getValue()
+    <br>
+#end
+    
+
+ +

Get global record value:

+
+    
+#set ($field = $globalRecords.getRecord('record_name'))
+#if ($field)
+    $field.getValue()
+    <br>
+#end
+    
+
+ +

Due to Polarion limitations we are not able to save custom fields in Live Report Page using Velocity, but we can use JavaScript for this.

+ +

Set custom field value:

+
+    
+<input id='cfp_project' type='text' value='elibrary' name='project'/>
+<input id='cfp_key' type='text' value='custom_field' name='key'/>
+<input id='cfp_value' type='text' value='custom_value' name='value'/>
+
+<script>
+    function save_project_custom_field() {
+        const project = document.getElementById('cfp_project').value;
+        const key = document.getElementById('cfp_key').value;
+        const value = document.getElementById('cfp_value').value;
+        const requestBody = (value === null || value === "") ? "" : JSON.stringify({'value': value});
+
+        fetch('/polarion/api-extender/rest/internal/projects/' + project + '/keys/' + key, {
+            method: 'POST',
+            headers: {
+                'Accept': 'application/json',
+                'Content-Type': 'application/json'
+            },
+            body: requestBody
+        })
+        .then(response => {
+            if (response.ok) {
+                return "Saved!"
+            } else {
+                return response.text()
+            }
+        })
+        .then(text => {
+             alert(text)
+        });
+    }
+</script>
+
+<button onclick='save_project_custom_field()'>Save</button>
+    
+
+ +

Set global record value:

+
+    
+<input id='record_key' type='text' value='record_name' name='record_name'/>
+<input id='record_value' type='text' value='record_value' name='record_value'/>
+
+<script>
+    function save_record() {
+        const key = document.getElementById('record_key').value;
+        const value = document.getElementById('record_value').value;
+        const requestBody = (value === null || value === "") ? "" : JSON.stringify({'value': value});
+
+        fetch('/polarion/api-extender/rest/internal/records/' + key, {
+            method: 'POST',
+            headers: {
+                'Accept': 'application/json',
+                'Content-Type': 'application/json'
+            },
+            body: requestBody
+        })
+            .then(response => {
+                if (response.ok) {
+                    return "Saved!"
+                } else {
+                    return response.text()
+                }
+            })
+            .then(text => {
+                alert(text)
+            });
+    }
+</script>
+
+<button onclick='save_record()'>Save</button>
+    
+
+

Note that internal API in URL should be used.

+ +

Classic Wiki Page

+ +

Get custom field value:

+
+    
+#set($projects = $projectService.searchProjects("","id"))
+
+#foreach($project in $projects)
+    #set($projectId = $project.id)
+    #set($field = $customFieldsProject.getCustomField($projectId, 'custom_field'))
+    #if ($field)
+        $projectId custom_field = $field.getValue()
+        <br>
+        #set($field = false)
+    #end
+#end
+    
+
+ +

Get global record value:

+
+    
+$globalRecords.getRecord('record_name')
+    
+
+ +

Set custom field value:

+
+    
+$customFieldsProject.setCustomField('elibrary', 'custom_field', 'new_value')
+    
+
+ +

Set global record value:

+
+    
+$globalRecords.setRecord('record_name', 'record_value')
+    
+
diff --git a/src/main/resources/webapp/api-extender-admin/images/app-icon.svg b/src/main/resources/webapp/api-extender-admin/images/app-icon.svg new file mode 100644 index 0000000..fa0b178 --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/images/app-icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/webapp/api-extender-admin/js/settings.js b/src/main/resources/webapp/api-extender-admin/js/settings.js new file mode 100644 index 0000000..cdb3583 --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/js/settings.js @@ -0,0 +1,103 @@ +const DEFAULT_SETTING_NAME = 'Default'; +SbbCommon.init({ + extension: 'api-extender', + setting: SbbCommon.getValueById("settings_name"), + scope: SbbCommon.getValueById('scope') +}); + +function getSelectedRoles(containerId) { + const result = []; + const checkboxes = document.querySelectorAll(`div#${containerId} input[type=checkbox]:checked`); + + checkboxes.forEach((checkbox) => { + result.push(checkbox.id); + }); + + return result; +} + +function setSelectedRoles(containerId, allowedRoles) { + const checkboxes = document.querySelectorAll(`div#${containerId} input[type=checkbox]`); + + checkboxes.forEach((checkbox) => { + checkbox.checked = allowedRoles.includes(checkbox.id); + }) +} + +function parseAndSetSettings(text) { + const settings = JSON.parse(text); + + setSelectedRoles('global_roles', settings.globalRoles); + setSelectedRoles('project_roles', settings.projectRoles); + + if (settings.bundleTimestamp !== SbbCommon.getValueById('bundle-timestamp')) { + SbbCommon.setNewerVersionNotificationVisible(true); + } +} + +function saveSettings() { + SbbCommon.hideActionAlerts(); + + SbbCommon.callAsync({ + method: 'PUT', + url: `/polarion/${SbbCommon.extension}/rest/internal/settings/${SbbCommon.setting}/names/${DEFAULT_SETTING_NAME}/content?scope=${SbbCommon.scope}`, + contentType: 'application/json', + body: JSON.stringify({ + 'globalRoles': getSelectedRoles('global_roles'), + 'projectRoles': getSelectedRoles('project_roles') + }), + onOk: () => { + SbbCommon.showSaveSuccessAlert(); + SbbCommon.setNewerVersionNotificationVisible(false); + readAndFillRevisions(); + }, + onError: () => SbbCommon.showSaveErrorAlert() + }); +} + +function cancelEdit() { + if (confirm("Are you sure you want to cancel editing and revert all changes made?")) { + readSettings(); + } +} + +function readSettings() { + SbbCommon.setLoadingErrorNotificationVisible(false); + + SbbCommon.callAsync({ + method: 'GET', + url: `/polarion/${SbbCommon.extension}/rest/internal/settings/${SbbCommon.setting}/names/${DEFAULT_SETTING_NAME}/content?scope=${SbbCommon.scope}`, + contentType: 'application/json', + onOk: (responseText) => { + parseAndSetSettings(responseText, true); + readAndFillRevisions(); + }, + onError: () => SbbCommon.setLoadingErrorNotificationVisible(true) + }); +} + +function readAndFillRevisions() { + SbbCommon.readAndFillRevisions({ + revertToRevisionCallback: (responseText) => parseAndSetSettings(responseText) + }); +} + +function revertToDefault() { + if (confirm("Are you sure you want to return the default values?")) { + SbbCommon.setLoadingErrorNotificationVisible(false); + SbbCommon.hideActionAlerts(); + + SbbCommon.callAsync({ + method: 'GET', + url: `/polarion/${SbbCommon.extension}/rest/internal/settings/${SbbCommon.setting}/default-content`, + contentType: 'application/json', + onOk: (responseText) => { + parseAndSetSettings(responseText); + SbbCommon.showRevertedToDefaultAlert(); + }, + onError: () => SbbCommon.setLoadingErrorNotificationVisible(true) + }); + } +} + +readSettings(); diff --git a/src/main/resources/webapp/api-extender-admin/pages/about.jsp b/src/main/resources/webapp/api-extender-admin/pages/about.jsp new file mode 100644 index 0000000..9d49713 --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/pages/about.jsp @@ -0,0 +1,101 @@ +<%@ page import="ch.sbb.polarion.extension.generic.properties.CurrentExtensionConfiguration" %> +<%@ page import="ch.sbb.polarion.extension.generic.rest.model.Version" %> +<%@ page import="ch.sbb.polarion.extension.generic.util.ExtensionInfo" %> +<%@ page import="ch.sbb.polarion.extension.generic.util.VersionUtils" %> +<%@ page import="java.util.Collections" %> +<%@ page import="java.util.Properties" %> +<%@ page import="java.io.InputStream" %> +<%@ page import="java.nio.charset.StandardCharsets" %> +<%@ page import="java.util.List" %> +<%@ page import="java.util.Set" %> +<%@ page import="java.util.ArrayList" %> +<%@ page contentType="text/html; charset=UTF-8" %> + + + +<%! + private static final String ABOUT_TABLE_ROW = "%s%s"; + private static final String CONFIGURATION_PROPERTIES_TABLE_ROW = "%s%s"; + + Version version = ExtensionInfo.getInstance().getVersion(); + Properties properties = CurrentExtensionConfiguration.getInstance().getExtensionConfiguration().getProperties(); +%> + + + + + + + + + +
+

About

+ +
+ + +

Extension info

+ + + + + + + + + + <% + out.println(ABOUT_TABLE_ROW.formatted(VersionUtils.BUNDLE_NAME, version.getBundleName())); + out.println(ABOUT_TABLE_ROW.formatted(VersionUtils.BUNDLE_VENDOR, version.getBundleVendor())); + if (version.getSupportEmail() != null) { + String mailToLink = "%s".formatted(version.getSupportEmail(), version.getSupportEmail()); + out.println(ABOUT_TABLE_ROW.formatted(VersionUtils.SUPPORT_EMAIL, mailToLink)); + } + out.println(ABOUT_TABLE_ROW.formatted(VersionUtils.AUTOMATIC_MODULE_NAME, version.getAutomaticModuleName())); + out.println(ABOUT_TABLE_ROW.formatted(VersionUtils.BUNDLE_VERSION, version.getBundleVersion())); + out.println(ABOUT_TABLE_ROW.formatted(VersionUtils.BUNDLE_BUILD_TIMESTAMP, version.getBundleBuildTimestamp())); + %> + +
Manifest entryValue
+ +

Extension configuration properties

+ + + + + + + + + + <% + Set keySet = properties.keySet(); + List propertyNames = new ArrayList<>(); + for (Object key : keySet) { + propertyNames.add((String) key); + } + Collections.sort(propertyNames); + + for (String key : propertyNames) { + String value = properties.getProperty(key); + String row = CONFIGURATION_PROPERTIES_TABLE_ROW.formatted(key, value); + out.println(row); + } + %> + +
Configuration propertyValue
+ + "/> + + <% + try (InputStream inputStream = ExtensionInfo.class.getResourceAsStream("/webapp/api-extender-admin/html/help/configuration.html")) { + assert inputStream != null; + String configurationHelp = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + out.println(configurationHelp); + } + %> +
+
+ + diff --git a/src/main/resources/webapp/api-extender-admin/pages/settings.jsp b/src/main/resources/webapp/api-extender-admin/pages/settings.jsp new file mode 100644 index 0000000..25671d9 --- /dev/null +++ b/src/main/resources/webapp/api-extender-admin/pages/settings.jsp @@ -0,0 +1,88 @@ +<%@ page import="ch.sbb.polarion.extension.api.extender.util.RolesUtils" %> +<%@ page import="ch.sbb.polarion.extension.generic.rest.model.Version" %> +<%@ page import="ch.sbb.polarion.extension.generic.util.ExtensionInfo" %> +<%@ page import="java.util.Collection" %> + + + +<%! Version version = ExtensionInfo.getInstance().getVersion();%> + + + Hooks: Settings + + + + + + + +
+

Authorization settings

+ + + +
+
+
+ + <% + Collection globalRoles = RolesUtils.getGlobalRoles(); + out.println("Global Roles"); + for (String role : globalRoles) { + out.println("" + + "" + + " " + + " " + + ""); + } + %> +
+
+ + +
+ + <% + Collection projectRoles = RolesUtils.getProjectRoles(request.getParameter("scope")); + if (!projectRoles.isEmpty()) { + out.println("Project Roles"); + } + for (String role : projectRoles) { + out.println("" + + "" + + " " + + " " + + ""); + } + %> +
+
+ +
+
+ + "/> + "/> + +
+ + + + + + + +
+

Quick Help

+ +
+

Permissions

+

Reading of project custom fields is not restricted.

+

Writing of project custom fields can be allowed with selected global and project roles.

+

By default only global admin role is allowed.

+
+
+ + + + diff --git a/src/main/resources/webapp/api-extender/WEB-INF/web.xml b/src/main/resources/webapp/api-extender/WEB-INF/web.xml new file mode 100644 index 0000000..b01a18e --- /dev/null +++ b/src/main/resources/webapp/api-extender/WEB-INF/web.xml @@ -0,0 +1,81 @@ + + + + api-extender + + + org.springframework.web.context.request.RequestContextListener + + + + DoAsFilter + com.polarion.portal.tomcat.servlets.DoAsFilter + + + + DoAsFilter + /* + + + + api-extender-rest + org.glassfish.jersey.servlet.ServletContainer + + + javax.ws.rs.Application + ch.sbb.polarion.extension.api.extender.rest.ApiExtenderRestApplication + + + + debug + 0 + + 1 + + + + api-extender-rest + /rest/* + + + + 30 + + + + log + text/plain + + + + + All + /* + + + user + + + + + + All + /rest/api/* + + + + + + + FORM + PolarionRealm + + /login/login + /login/error + + + \ No newline at end of file diff --git a/src/test/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecordsTest.java b/src/test/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecordsTest.java new file mode 100644 index 0000000..f0163e0 --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/api/extender/project/GlobalRecordsTest.java @@ -0,0 +1,111 @@ +package ch.sbb.polarion.extension.api.extender.project; + +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.rest.model.Records; +import ch.sbb.polarion.extension.generic.jaxb.JAXBUtils; +import com.polarion.alm.shared.api.transaction.ReadOnlyTransaction; +import com.polarion.alm.shared.api.transaction.RunnableInReadOnlyTransaction; +import com.polarion.alm.shared.api.transaction.RunnableInWriteTransaction; +import com.polarion.alm.shared.api.transaction.TransactionalExecutor; +import com.polarion.alm.shared.api.transaction.WriteTransaction; +import com.polarion.platform.service.repository.IRepositoryConnection; +import com.polarion.platform.service.repository.IRepositoryReadOnlyConnection; +import com.polarion.platform.service.repository.IRepositoryService; +import com.polarion.subterra.base.location.ILocation; +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.xml.bind.UnmarshalException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings({"unchecked", "rawtypes"}) +class GlobalRecordsTest { + + @Test + @SneakyThrows + void testGetRecord() { + try (MockedStatic mockedExecutor = mockStatic(TransactionalExecutor.class); + InputStream fieldsXml = this.getClass().getResourceAsStream("/fields.xml")) { + IRepositoryService repositoryService = mock(IRepositoryService.class); + + IRepositoryReadOnlyConnection mockedConnection = mock(IRepositoryReadOnlyConnection.class); + when(repositoryService.getReadOnlyConnection(any(ILocation.class))).thenReturn(mockedConnection); + when(mockedConnection.exists(any(ILocation.class))).thenReturn(true); + + mockedExecutor.when(() -> TransactionalExecutor.executeSafelyInReadOnlyTransaction(any(RunnableInReadOnlyTransaction.class))) + .thenAnswer(invocation -> { + RunnableInReadOnlyTransaction transaction = invocation.getArgument(0); + return transaction.run(mock(ReadOnlyTransaction.class)); + }); + + GlobalRecords records = new GlobalRecords(repositoryService); + + // records do not exist + when(mockedConnection.getContent(any(ILocation.class))).thenReturn(null); + assertNull(records.getRecord("name")); + + // saved value contains nonsense + when(mockedConnection.getContent(any(ILocation.class))).thenReturn(IOUtils.toInputStream("weirdString", StandardCharsets.UTF_8)); + Assertions.assertThrows(UnmarshalException.class, () -> records.getRecord("name")); + + // successful case + when(mockedConnection.getContent(any(ILocation.class))).thenReturn(fieldsXml); + Field field = records.getRecord("name"); + assertEquals("E-Library", field == null ? null : field.getValue()); + } + } + + @Test + @SneakyThrows + void testSetRecord() { + try (MockedStatic mockedExecutor = mockStatic(TransactionalExecutor.class)) { + IRepositoryService repositoryService = mock(IRepositoryService.class); + + IRepositoryReadOnlyConnection mockedConnection = mock(IRepositoryReadOnlyConnection.class); + when(repositoryService.getReadOnlyConnection(any(ILocation.class))).thenReturn(mockedConnection); + IRepositoryConnection mockedWriteConnection = mock(IRepositoryConnection.class); + when(repositoryService.getConnection(any(ILocation.class))).thenReturn(mockedWriteConnection); + when(mockedConnection.exists(any(ILocation.class))).thenReturn(true); + + mockedExecutor.when(() -> TransactionalExecutor.executeSafelyInReadOnlyTransaction(any(RunnableInReadOnlyTransaction.class))) + .thenAnswer(invocation -> { + RunnableInReadOnlyTransaction transaction = invocation.getArgument(0); + return transaction.run(mock(ReadOnlyTransaction.class)); + }); + + mockedExecutor.when(() -> TransactionalExecutor.executeInWriteTransaction(any(RunnableInWriteTransaction.class))) + .thenAnswer(invocation -> { + RunnableInWriteTransaction transaction = invocation.getArgument(0); + return transaction.run(mock(WriteTransaction.class)); + }); + + when(mockedConnection.getContent(any(ILocation.class))).thenReturn(IOUtils.toInputStream(JAXBUtils.serialize(new Records()), StandardCharsets.UTF_8)); + + GlobalRecords records = new GlobalRecords(repositoryService); + records.setRecord("name", "value"); + + // must create a new one record + verify(mockedWriteConnection, times(1)).create(any(ILocation.class), any(InputStream.class)); + verify(mockedWriteConnection, times(0)).setContent(any(ILocation.class), any(InputStream.class)); + + when(mockedConnection.getContent(any(ILocation.class))).thenReturn(IOUtils.toInputStream(JAXBUtils.serialize(new Records()), StandardCharsets.UTF_8)); + when(mockedWriteConnection.exists(any(ILocation.class))).thenReturn(true); + records.setRecord("name", "value"); + + // must reuse existing one + verify(mockedWriteConnection, times(1)).create(any(ILocation.class), any(InputStream.class)); + verify(mockedWriteConnection, times(1)).setContent(any(ILocation.class), any(InputStream.class)); + } + } +} diff --git a/src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalControllerTest.java b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalControllerTest.java new file mode 100644 index 0000000..0f53165 --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/GlobalRecordInternalControllerTest.java @@ -0,0 +1,133 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import ch.sbb.polarion.extension.api.extender.project.GlobalRecords; +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.settings.GlobalRecordsSettingsModel; +import ch.sbb.polarion.extension.generic.service.PolarionService; +import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings; +import ch.sbb.polarion.extension.generic.settings.NamedSettingsRegistry; +import com.polarion.alm.projects.IProjectService; +import com.polarion.alm.tracker.ITrackerService; +import com.polarion.platform.IPlatformService; +import com.polarion.platform.core.IPlatform; +import com.polarion.platform.core.PlatformContext; +import com.polarion.platform.security.ISecurityService; +import com.polarion.platform.service.repository.IRepositoryService; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotFoundException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@SuppressWarnings({"rawtypes", "unused", "unchecked", "ResultOfMethodCallIgnored"}) +class GlobalRecordInternalControllerTest { + + @Test + @SneakyThrows + void testGetRecordValue() { + try (MockedStatic platformContextMockedStatic = mockStatic(PlatformContext.class)) { + + mockPlatform(platformContextMockedStatic); + + try (MockedConstruction mockedGlobalRecords = Mockito.mockConstruction(GlobalRecords.class, (mock, context) -> + when(mock.getRecord(anyString())).thenReturn(new Field("someId", "someValue")))) { + GlobalRecordInternalController controller = new GlobalRecordInternalController(); + Field field = controller.getRecordValue("someId"); + assertEquals("someValue", field == null ? null : field.getValue()); + } + + try (MockedConstruction mockedGlobalRecords = Mockito.mockConstruction(GlobalRecords.class, (mock, context) -> + when(mock.getRecord(anyString())).thenReturn(null))) { + GlobalRecordInternalController controller = new GlobalRecordInternalController(); + NotFoundException notFoundException = Assertions.assertThrows(NotFoundException.class, () -> controller.getRecordValue("someId")); + assertEquals("key 'someId' not found", notFoundException.getMessage()); + } + } + } + + @Test + @SneakyThrows + void testSetRecordValue() { + try (MockedStatic platformContextMockedStatic = mockStatic(PlatformContext.class)) { + + mockPlatform(platformContextMockedStatic); + + Field field = new Field("someId", "someValue"); + try (MockedConstruction mockedGlobalRecords = Mockito.mockConstruction(GlobalRecords.class, (mock, context) -> + when(mock.getRecord(anyString())).thenReturn(field))) { + + ISecurityService securityService = mockRoles(); + + PolarionService polarionService = new PolarionService(mock(ITrackerService.class), mock(IProjectService.class), securityService, mock(IPlatformService.class), mock(IRepositoryService.class)); + GlobalRecordInternalController controller = new GlobalRecordInternalController(polarionService); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role3")); + ForbiddenException forbiddenException = Assertions.assertThrows(ForbiddenException.class, () -> controller.setRecordValue("someId", field)); + assertEquals("You are not authorized to modify records", forbiddenException.getMessage()); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role1")); + controller.setRecordValue("someId", field); + verify(mockedGlobalRecords.constructed().get(0), times(1)).setRecord("someId", "someValue"); + } + } + } + + @Test + @SneakyThrows + void testDeleteRecordValue() { + try (MockedStatic platformContextMockedStatic = mockStatic(PlatformContext.class)) { + + mockPlatform(platformContextMockedStatic); + + Field field = new Field("someId", "someValue"); + try (MockedConstruction mockedGlobalRecords = Mockito.mockConstruction(GlobalRecords.class, (mock, context) -> + when(mock.getRecord(anyString())).thenReturn(field))) { + + ISecurityService securityService = mockRoles(); + + PolarionService polarionService = new PolarionService(mock(ITrackerService.class), mock(IProjectService.class), securityService, mock(IPlatformService.class), mock(IRepositoryService.class)); + GlobalRecordInternalController controller = new GlobalRecordInternalController(polarionService); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role3")); + ForbiddenException forbiddenException = Assertions.assertThrows(ForbiddenException.class, () -> controller.deleteRecordValue("someId")); + assertEquals("You are not authorized to modify records", forbiddenException.getMessage()); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role1")); + controller.deleteRecordValue("someId"); + verify(mockedGlobalRecords.constructed().get(0), times(1)).setRecord("someId", null); + } + } + } + + private ISecurityService mockRoles() { + ISecurityService securityService = mock(ISecurityService.class); + when(securityService.getCurrentUser()).thenReturn("userId"); + + GenericNamedSettings settings = mock(GenericNamedSettings.class); + when(settings.getFeatureName()).thenReturn("global_records"); + NamedSettingsRegistry.INSTANCE.register(Collections.singletonList(settings)); + GlobalRecordsSettingsModel settingsModel = mock(GlobalRecordsSettingsModel.class); + when(settingsModel.getAllRoles()).thenReturn(Arrays.asList("role1", "role2")); + when(settings.read(any(), any(), any())).thenReturn(settingsModel); + return securityService; + } + + private void mockPlatform(MockedStatic platformContextMockedStatic) { + IPlatform platform = mock(IPlatform.class); + IRepositoryService repositoryService = mock(IRepositoryService.class); + platformContextMockedStatic.when(PlatformContext::getPlatform).thenReturn(platform); + when(platform.lookupService(IRepositoryService.class)).thenReturn(repositoryService); + } + +} diff --git a/src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalControllerTest.java b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalControllerTest.java new file mode 100644 index 0000000..71d5294 --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/controller/ProjectCustomFieldInternalControllerTest.java @@ -0,0 +1,133 @@ +package ch.sbb.polarion.extension.api.extender.rest.controller; + +import ch.sbb.polarion.extension.api.extender.project.CustomFieldsProject; +import ch.sbb.polarion.extension.api.extender.rest.model.Field; +import ch.sbb.polarion.extension.api.extender.settings.ProjectCustomFieldsSettingsModel; +import ch.sbb.polarion.extension.generic.service.PolarionService; +import ch.sbb.polarion.extension.generic.settings.GenericNamedSettings; +import ch.sbb.polarion.extension.generic.settings.NamedSettingsRegistry; +import com.polarion.alm.projects.IProjectService; +import com.polarion.alm.tracker.ITrackerService; +import com.polarion.platform.IPlatformService; +import com.polarion.platform.core.IPlatform; +import com.polarion.platform.core.PlatformContext; +import com.polarion.platform.security.ISecurityService; +import com.polarion.platform.service.repository.IRepositoryService; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotFoundException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@SuppressWarnings({"rawtypes", "unused", "unchecked", "ResultOfMethodCallIgnored"}) +class ProjectCustomFieldInternalControllerTest { + + @Test + @SneakyThrows + void testGetCustomValue() { + try (MockedStatic platformContextMockedStatic = mockStatic(PlatformContext.class)) { + + mockPlatform(platformContextMockedStatic); + + try (MockedConstruction mockedCustomFieldsProject = Mockito.mockConstruction(CustomFieldsProject.class, (mock, context) -> + when(mock.getCustomField(anyString())).thenReturn(new Field("someId", "someValue")))) { + ProjectCustomFieldInternalController controller = new ProjectCustomFieldInternalController(); + Field field = controller.getCustomValue("testProj", "someId"); + assertEquals("someValue", field == null ? null : field.getValue()); + } + + try (MockedConstruction mockedCustomFieldsProject = Mockito.mockConstruction(CustomFieldsProject.class, (mock, context) -> + when(mock.getCustomField(anyString())).thenReturn(null))) { + ProjectCustomFieldInternalController controller = new ProjectCustomFieldInternalController(); + NotFoundException notFoundException = Assertions.assertThrows(NotFoundException.class, () -> controller.getCustomValue("testProj", "someId")); + assertEquals("key 'someId' for project 'testProj' not found", notFoundException.getMessage()); + } + } + } + + @Test + @SneakyThrows + void testSetRecordValue() { + try (MockedStatic platformContextMockedStatic = mockStatic(PlatformContext.class)) { + + mockPlatform(platformContextMockedStatic); + + Field field = new Field("someId", "someValue"); + try (MockedConstruction mockedCustomFieldsProject = Mockito.mockConstruction(CustomFieldsProject.class, (mock, context) -> + when(mock.getCustomField(anyString())).thenReturn(field))) { + + ISecurityService securityService = mockRoles(); + + PolarionService polarionService = new PolarionService(mock(ITrackerService.class), mock(IProjectService.class), securityService, mock(IPlatformService.class), mock(IRepositoryService.class)); + ProjectCustomFieldInternalController controller = new ProjectCustomFieldInternalController(polarionService); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role3")); + ForbiddenException forbiddenException = Assertions.assertThrows(ForbiddenException.class, () -> controller.setCustomValue("testProj", "someId", field)); + assertEquals("You are not authorized to modify custom fields", forbiddenException.getMessage()); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role1")); + controller.setCustomValue("testProj", "someId", field); + verify(mockedCustomFieldsProject.constructed().get(0), times(1)).setCustomField("someId", "someValue"); + } + } + } + + @Test + @SneakyThrows + void testDeleteRecordValue() { + try (MockedStatic platformContextMockedStatic = mockStatic(PlatformContext.class)) { + + mockPlatform(platformContextMockedStatic); + + Field field = new Field("someId", "someValue"); + try (MockedConstruction mockedCustomFieldsProject = Mockito.mockConstruction(CustomFieldsProject.class, (mock, context) -> + when(mock.getCustomField(anyString())).thenReturn(field))) { + + ISecurityService securityService = mockRoles(); + + PolarionService polarionService = new PolarionService(mock(ITrackerService.class), mock(IProjectService.class), securityService, mock(IPlatformService.class), mock(IRepositoryService.class)); + ProjectCustomFieldInternalController controller = new ProjectCustomFieldInternalController(polarionService); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role3")); + ForbiddenException forbiddenException = Assertions.assertThrows(ForbiddenException.class, () -> controller.deleteCustomValue("testProj", "someId")); + assertEquals("You are not authorized to modify custom fields", forbiddenException.getMessage()); + + when(securityService.getRolesForUser("userId")).thenReturn(List.of("role1")); + controller.deleteCustomValue("testProj", "someId"); + verify(mockedCustomFieldsProject.constructed().get(0), times(1)).setCustomField("someId", null); + } + } + } + + private ISecurityService mockRoles() { + ISecurityService securityService = mock(ISecurityService.class); + when(securityService.getCurrentUser()).thenReturn("userId"); + + GenericNamedSettings settings = mock(GenericNamedSettings.class); + when(settings.getFeatureName()).thenReturn("project_custom_fields"); + NamedSettingsRegistry.INSTANCE.register(Collections.singletonList(settings)); + ProjectCustomFieldsSettingsModel settingsModel = mock(ProjectCustomFieldsSettingsModel.class); + when(settingsModel.getAllRoles()).thenReturn(Arrays.asList("role1", "role2")); + when(settings.read(any(), any(), any())).thenReturn(settingsModel); + return securityService; + } + + private void mockPlatform(MockedStatic platformContextMockedStatic) { + IPlatform platform = mock(IPlatform.class); + IRepositoryService repositoryService = mock(IRepositoryService.class); + platformContextMockedStatic.when(PlatformContext::getPlatform).thenReturn(platform); + when(platform.lookupService(IRepositoryService.class)).thenReturn(repositoryService); + } + +} diff --git a/src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/ProjectTest.java b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/ProjectTest.java new file mode 100644 index 0000000..aaf08b3 --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/ProjectTest.java @@ -0,0 +1,73 @@ +package ch.sbb.polarion.extension.api.extender.rest.model.xml; + +import ch.sbb.polarion.extension.api.extender.rest.model.Project; +import ch.sbb.polarion.extension.generic.jaxb.JAXBUtils; +import ch.sbb.polarion.extension.api.extender.util.CustomFieldUtils; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import javax.xml.bind.JAXBException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ProjectTest { + + private static final String XML_CONTENT = "" + + "" + System.lineSeparator() + + "" + System.lineSeparator() + + " true" + System.lineSeparator() + + " This model project demonstrates how an actual software project might be set up using the 'Agile Software Project' template." + System.lineSeparator() + + " rProject" + System.lineSeparator() + + " E-Library" + System.lineSeparator() + + " 2016-10-01" + System.lineSeparator() + + " EL" + System.lineSeparator() + + " #8E9D23" + System.lineSeparator() + + " /polarion/icons/default/topicIcons/App_985-demo-elibrary.svg" + System.lineSeparator() + + " custom_value" + System.lineSeparator() + + "" + System.lineSeparator() + + ""; + + @Test + void testGetField() throws JAXBException, IOException { + final Project project = JAXBUtils.deserialize(Project.class, XML_CONTENT); + assertEquals("custom_value", project.getField("custom_field").getValue()); + assertEquals("#8E9D23", project.getField("color").getValue()); + + final String serialized = JAXBUtils.serialize(project); + assertEquals(XML_CONTENT.replaceAll(System.lineSeparator(), "\n"), serialized); + } + + @Test + void testSetCustomField() throws JAXBException, IOException { + try (final MockedStatic customFieldUtilsMockedStatic = Mockito.mockStatic(CustomFieldUtils.class)) { + customFieldUtilsMockedStatic.when(() -> CustomFieldUtils.isStandardField("key")).thenReturn(false); + + final Project project = JAXBUtils.deserialize(Project.class, XML_CONTENT); + project.setField("key", "value"); + assertEquals("value", project.getField("key").getValue()); + + final String serialized = JAXBUtils.serialize(project); + assertTrue(serialized.contains("value")); + } + } + + @Test + @SneakyThrows + void testSetStandardField() { + try (final MockedStatic customFieldUtilsMockedStatic = Mockito.mockStatic(CustomFieldUtils.class)) { + customFieldUtilsMockedStatic.when(() -> CustomFieldUtils.isStandardField("description")).thenReturn(true); + + final Project project = JAXBUtils.deserialize(Project.class, XML_CONTENT); + IllegalArgumentException illegalArgumentException = Assertions.assertThrows(IllegalArgumentException.class, () -> { + project.setField("description", "value"); + }); + + assertEquals("'description' is a standard field of project", illegalArgumentException.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/RecordsTest.java b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/RecordsTest.java new file mode 100644 index 0000000..ed9a1d0 --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/api/extender/rest/model/xml/RecordsTest.java @@ -0,0 +1,43 @@ +package ch.sbb.polarion.extension.api.extender.rest.model.xml; + +import ch.sbb.polarion.extension.api.extender.rest.model.Records; +import ch.sbb.polarion.extension.generic.jaxb.JAXBUtils; +import org.junit.jupiter.api.Test; + +import javax.xml.bind.JAXBException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RecordsTest { + + private static final String XML_CONTENT = "" + + "" + System.lineSeparator() + + "" + System.lineSeparator() + + " recordvalue1" + System.lineSeparator() + + " recordvalue2" + System.lineSeparator() + + "" + System.lineSeparator() + + ""; + + @Test + void testGetField() throws JAXBException, IOException { + final Records records = JAXBUtils.deserialize(Records.class, XML_CONTENT); + assertEquals("recordvalue1", records.getField("key1").getValue()); + assertEquals("recordvalue2", records.getField("key2").getValue()); + + final String serialized = JAXBUtils.serialize(records); + assertEquals(XML_CONTENT.replaceAll(System.lineSeparator(), "\n"), serialized); + } + + @Test + void testSetField() throws JAXBException, IOException { + final Records records = JAXBUtils.deserialize(Records.class, XML_CONTENT); + records.setField("key", "value"); + assertEquals("value", records.getField("key").getValue()); + + final String serialized = JAXBUtils.serialize(records); + assertTrue(serialized.contains("value")); + } + +} \ No newline at end of file diff --git a/src/test/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModelTest.java b/src/test/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModelTest.java new file mode 100644 index 0000000..d21b97f --- /dev/null +++ b/src/test/java/ch/sbb/polarion/extension/api/extender/settings/GlobalRecordsSettingsModelTest.java @@ -0,0 +1,24 @@ +package ch.sbb.polarion.extension.api.extender.settings; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GlobalRecordsSettingsModelTest { + + @Test + void testDeserializeRoles() { + GlobalRecordsSettingsModel model = new GlobalRecordsSettingsModel(); + model.setGlobalRoles("rolesPlaceholderToReplace"); + String serializedString = model.serializeModelData(); + + model.deserialize(serializedString.replace("rolesPlaceholderToReplace", " role1,, \t \n role2 \n")); + List roles = model.getGlobalRoles(); + assertEquals(2, roles.size()); + assertTrue(roles.containsAll(Arrays.asList("role1", "role2"))); + } +} diff --git a/src/test/resources/fields.xml b/src/test/resources/fields.xml new file mode 100644 index 0000000..bd160cd --- /dev/null +++ b/src/test/resources/fields.xml @@ -0,0 +1,11 @@ + + + true + This model project demonstrates how an actual software project might be set up using the 'Agile Software Project' template. + rProject + E-Library + 2016-10-01 + EL + #8E9D23 + /polarion/icons/default/topicIcons/App_985-demo-elibrary.svg + \ No newline at end of file