From 1857052d3f7c92ed0969edf95e916122aff97a32 Mon Sep 17 00:00:00 2001 From: e551763 Date: Tue, 4 Jun 2024 12:50:34 +0200 Subject: [PATCH] feat!: save/delete methods proxy calls improvement, introduced separate pre&post hook methods --- hooks/module-save-time-logger/.gitignore | 31 ++++ hooks/module-save-time-logger/README.md | 19 +++ hooks/module-save-time-logger/pom.xml | 159 ++++++++++++++++++ .../ModuleSaveTimeHook.java | 52 ++++++ .../src/main/resources/META-INF/MANIFEST.MF | 2 + hooks/only-assignee-can-delete/pom.xml | 2 +- .../OnlyAssigneeCanDeleteHook.java | 13 +- hooks/plan-save/pom.xml | 2 +- .../plan_save/PlanSaveHook.java | 14 +- hooks/single-assignee/pom.xml | 2 +- .../single_assignee/SingleAssigneeHook.java | 16 +- hooks/testrun/pom.xml | 2 +- .../testrun/TestRunHook.java | 14 +- hooks/title-length-check/pom.xml | 2 +- .../title_length_check/TitleLengthHook.java | 14 +- .../interceptor/ActionInterceptorFactory.java | 58 ------- .../interceptor/ActionInvocationHandler.java | 61 ------- .../interceptor/DataServiceInterceptor.java | 45 +++++ .../DataServiceInterceptorFactory.java | 115 ++++++++++++- .../interceptor/model/ActionHook.java | 12 +- .../interceptor/model/HookExecutor.java | 28 +++ .../interceptor/util/HookJarUtils.java | 31 +++- src/main/resources/META-INF/hivemodule.xml | 40 ----- 23 files changed, 529 insertions(+), 205 deletions(-) create mode 100644 hooks/module-save-time-logger/.gitignore create mode 100644 hooks/module-save-time-logger/README.md create mode 100644 hooks/module-save-time-logger/pom.xml create mode 100644 hooks/module-save-time-logger/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/module_save_time_logger/ModuleSaveTimeHook.java create mode 100644 hooks/module-save-time-logger/src/main/resources/META-INF/MANIFEST.MF delete mode 100644 src/main/java/ch/sbb/polarion/extension/interceptor/ActionInterceptorFactory.java delete mode 100644 src/main/java/ch/sbb/polarion/extension/interceptor/ActionInvocationHandler.java create mode 100644 src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptor.java create mode 100644 src/main/java/ch/sbb/polarion/extension/interceptor/model/HookExecutor.java diff --git a/hooks/module-save-time-logger/.gitignore b/hooks/module-save-time-logger/.gitignore new file mode 100644 index 0000000..824ec60 --- /dev/null +++ b/hooks/module-save-time-logger/.gitignore @@ -0,0 +1,31 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* +.idea/* + +# code style config +!.idea/codeStyles +.idea/codeStyles/* +!.idea/codeStyles/Project.xml +!.idea/codeStyles/codeStyleConfig.xml + +target/ +*.iml diff --git a/hooks/module-save-time-logger/README.md b/hooks/module-save-time-logger/README.md new file mode 100644 index 0000000..9266d0d --- /dev/null +++ b/hooks/module-save-time-logger/README.md @@ -0,0 +1,19 @@ +# Hook example for Polarion Interceptor extension + +This hook writes to log module save time. + +## Build + +This hook can be produced using maven: +``` +mvn clean package +``` + +## Installation to Polarion + +To install this hook to Polarion `ch.sbb.polarion.extension.interceptor-hooks.-.jar` should be copied to `/polarion/extensions/ch.sbb.polarion.extension.interceptor/eclipse/plugins/hooks`. +It can be done manually or automated using maven build: +``` +mvn clean install -P install-to-local-polarion +``` +For automated installation with maven env variable `POLARION_HOME` should be defined and point to folder where Polarion is installed. diff --git a/hooks/module-save-time-logger/pom.xml b/hooks/module-save-time-logger/pom.xml new file mode 100644 index 0000000..970bbc9 --- /dev/null +++ b/hooks/module-save-time-logger/pom.xml @@ -0,0 +1,159 @@ + + + 4.0.0 + + ch.sbb.polarion.extensions.interceptor-hooks + ch.sbb.polarion.extension.interceptor-hooks.module-save-time-logger + 1.1.2-SNAPSHOT + jar + + + 2.0.0 + + ch.sbb.polarion.extension.interceptor_hooks.module_save_time_logger + module-save-time-logger + + ch.sbb.polarion.extension.interceptor + hooks + + 17 + 17 + 2404 + + UTF-8 + yyyy-MM-dd HH:mm + + + 3.3.2 + 3.4.1 + 3.6.1 + + + 24.0.1 + + + ${project.artifact.selectedVersion.majorVersion}.${project.artifact.selectedVersion.minorVersion}.${project.artifact.selectedVersion.incrementalVersion} + + + + + install-to-local-polarion + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + + ${env.POLARION_HOME}/polarion/extensions/${interceptor.artifactId}/eclipse/plugins/${hooks.folder.name} + + *${maven-jar-plugin.Extension-Context}*.jar + + false + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + copy-to-local-polarion + install + + copy + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + ${env.POLARION_HOME}/polarion/extensions/${interceptor.artifactId}/eclipse/plugins/${hooks.folder.name} + + + + + + + + + + + + + ch.sbb.polarion.extensions + ch.sbb.polarion.extension.interceptor + ${ch.sbb.polarion.extension.interceptor.version} + provided + + + + + com.polarion.alm.projects + projects + ${polarion.version} + provided + + + com.polarion.alm.tracker + tracker + ${polarion.version} + provided + + + com.polarion.platform.persistence + platform-persistence + ${polarion.version} + provided + + + com.polarion.core.util + util + ${polarion.version} + provided + + + + + org.jetbrains + annotations + ${jetbrains.api.version} + provided + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + src/main/resources/META-INF/MANIFEST.MF + + + + + + + + diff --git a/hooks/module-save-time-logger/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/module_save_time_logger/ModuleSaveTimeHook.java b/hooks/module-save-time-logger/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/module_save_time_logger/ModuleSaveTimeHook.java new file mode 100644 index 0000000..3e77eac --- /dev/null +++ b/hooks/module-save-time-logger/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/module_save_time_logger/ModuleSaveTimeHook.java @@ -0,0 +1,52 @@ +package ch.sbb.polarion.extension.interceptor_hooks.module_save_time_logger; + +import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; +import ch.sbb.polarion.extension.interceptor.util.PropertiesUtils; +import com.polarion.core.util.logging.Logger; +import com.polarion.platform.persistence.model.IPObject; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public class ModuleSaveTimeHook extends ActionHook implements HookExecutor { + + public static final String SETTINGS_LOG_MESSAGE = "logMessage"; + public static final String SETTINGS_TIME = "time"; + public static final String TIME_VARIABLE = "{%s}".formatted(SETTINGS_TIME); + public static final String DEFAULT_LOG_MESSAGE = "Module saved in " + TIME_VARIABLE + "ms"; + public static final String VERSION = "1.0.0"; + public static final Logger logger = Logger.getLogger(ModuleSaveTimeHook.class); + + public ModuleSaveTimeHook() { + super(ItemType.MODULE, ActionType.SAVE, VERSION, "Logs module save time"); + } + + @Override + public @NotNull HookExecutor getExecutor() { + return new SaveTimeLoggerExecutor(); + } + + @Override + public String getDefaultSettings() { + return PropertiesUtils.build( + SETTINGS_LOG_MESSAGE, DEFAULT_LOG_MESSAGE + ); + } + + public class SaveTimeLoggerExecutor implements HookExecutor { + + private long startTime; + + @Override + public String preAction(@NotNull IPObject polarionObject) { + startTime = System.currentTimeMillis(); + return null; + } + + @Override + public void postAction(@NotNull IPObject polarionObject) { + logger.info(getSettingsValue(SETTINGS_LOG_MESSAGE) + .replace(TIME_VARIABLE, String.valueOf(System.currentTimeMillis() - startTime))); + } + } +} diff --git a/hooks/module-save-time-logger/src/main/resources/META-INF/MANIFEST.MF b/hooks/module-save-time-logger/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..2cb9b6a --- /dev/null +++ b/hooks/module-save-time-logger/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Name: Title length hook for Polarion ALM Interceptor plugin +Main-Class: ch.sbb.polarion.extension.interceptor_hooks.module_save_time_logger.ModuleSaveTimeHook diff --git a/hooks/only-assignee-can-delete/pom.xml b/hooks/only-assignee-can-delete/pom.xml index 4493d98..2c1f048 100644 --- a/hooks/only-assignee-can-delete/pom.xml +++ b/hooks/only-assignee-can-delete/pom.xml @@ -8,7 +8,7 @@ jar - 1.1.1 + 2.0.0 ch.sbb.polarion.extension.interceptor_hooks.only_assignee_can_delete only-assignee-can-delete diff --git a/hooks/only-assignee-can-delete/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/only_assignee_can_delete/OnlyAssigneeCanDeleteHook.java b/hooks/only-assignee-can-delete/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/only_assignee_can_delete/OnlyAssigneeCanDeleteHook.java index b2bb5c2..27607c8 100644 --- a/hooks/only-assignee-can-delete/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/only_assignee_can_delete/OnlyAssigneeCanDeleteHook.java +++ b/hooks/only-assignee-can-delete/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/only_assignee_can_delete/OnlyAssigneeCanDeleteHook.java @@ -1,8 +1,8 @@ package ch.sbb.polarion.extension.interceptor_hooks.only_assignee_can_delete; import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; import ch.sbb.polarion.extension.interceptor.util.PropertiesUtils; -import com.polarion.alm.projects.model.IUniqueObject; import com.polarion.alm.projects.model.IUser; import com.polarion.alm.tracker.ITrackerService; import com.polarion.alm.tracker.model.IWorkItem; @@ -15,14 +15,14 @@ import java.util.Iterator; @SuppressWarnings({"unused", "unchecked", "rawtypes"}) -public class OnlyAssigneeCanDeleteHook extends ActionHook { +public class OnlyAssigneeCanDeleteHook extends ActionHook implements HookExecutor { private static final String SETTINGS_PROJECTS = "projects"; private static final String SETTINGS_ERROR_MESSAGE = "errorMessage"; private static final String SETTINGS_DELETE_UNASSIGNED = "deleteUnassigned"; private static final boolean DEFAULT_DELETE_UNASSIGNED = true; private static final String DEFAULT_ERROR_MESSAGE = "Only assignee user can delete WI!"; - private static final String VERSION = "1.0.0"; + private static final String VERSION = "2.0.0"; private static final ITrackerService trackerService = PlatformContext.getPlatform().lookupService(ITrackerService.class); private static final Logger logger = Logger.getLogger(OnlyAssigneeCanDeleteHook.class); @@ -32,7 +32,7 @@ public OnlyAssigneeCanDeleteHook() { } @Override - public String processAction(@NotNull IUniqueObject object) { + public String preAction(@NotNull IPObject object) { boolean deleteUnassigned = DEFAULT_DELETE_UNASSIGNED; String deleteUnassignedStringValue = getSettingsValue(SETTINGS_DELETE_UNASSIGNED); try { @@ -63,6 +63,11 @@ public String processAction(@NotNull IUniqueObject object) { return returnMessage; } + @Override + public @NotNull HookExecutor getExecutor() { + return this; //there is no need to create a separate executor instance coz only 'pre' action used + } + @Override public String getDefaultSettings() { return PropertiesUtils.build( diff --git a/hooks/plan-save/pom.xml b/hooks/plan-save/pom.xml index bd8a6e8..4dc0d51 100644 --- a/hooks/plan-save/pom.xml +++ b/hooks/plan-save/pom.xml @@ -8,7 +8,7 @@ jar - 1.1.1 + 2.0.0 ch.sbb.polarion.extension.interceptor_hooks.plan_save plan-save diff --git a/hooks/plan-save/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/plan_save/PlanSaveHook.java b/hooks/plan-save/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/plan_save/PlanSaveHook.java index a87e418..d34d0db 100644 --- a/hooks/plan-save/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/plan_save/PlanSaveHook.java +++ b/hooks/plan-save/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/plan_save/PlanSaveHook.java @@ -1,21 +1,22 @@ package ch.sbb.polarion.extension.interceptor_hooks.plan_save; import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; import ch.sbb.polarion.extension.interceptor.util.PropertiesUtils; -import com.polarion.alm.projects.model.IUniqueObject; import com.polarion.alm.tracker.model.IPlan; import com.polarion.alm.tracker.model.IWorkItem; import com.polarion.core.util.logging.Logger; +import com.polarion.platform.persistence.model.IPObject; import org.jetbrains.annotations.NotNull; import java.util.LinkedHashSet; @SuppressWarnings("unused") -public class PlanSaveHook extends ActionHook { +public class PlanSaveHook extends ActionHook implements HookExecutor { private static final String SETTINGS_PROJECTS = "projects"; private static final String SETTINGS_TEMPLATES = "templates"; - private static final String VERSION = "1.0.0"; + private static final String VERSION = "2.0.0"; private static final Logger logger = Logger.getLogger(PlanSaveHook.class); public PlanSaveHook() { @@ -23,7 +24,7 @@ public PlanSaveHook() { } @Override - public String processAction(@NotNull IUniqueObject object) { + public void postAction(@NotNull IPObject object) { IPlan plan = (IPlan) object; logger.debug("Processing Plan: " + plan.getId()); @@ -50,8 +51,11 @@ public String processAction(@NotNull IUniqueObject object) { } else { logger.debug("Unsupported project: " + plan.getProjectId() + " Supporting: " + getSettingsValue(SETTINGS_PROJECTS)); } + } - return null; + @Override + public @NotNull HookExecutor getExecutor() { + return this; //there is no need to create a separate executor instance coz only 'post' action used } @Override diff --git a/hooks/single-assignee/pom.xml b/hooks/single-assignee/pom.xml index 10643d9..e4f14c3 100644 --- a/hooks/single-assignee/pom.xml +++ b/hooks/single-assignee/pom.xml @@ -8,7 +8,7 @@ jar - 1.1.1 + 2.0.0 ch.sbb.polarion.extension.interceptor_hooks.single_assignee single-assignee diff --git a/hooks/single-assignee/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/single_assignee/SingleAssigneeHook.java b/hooks/single-assignee/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/single_assignee/SingleAssigneeHook.java index 9b880d1..296ae1b 100644 --- a/hooks/single-assignee/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/single_assignee/SingleAssigneeHook.java +++ b/hooks/single-assignee/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/single_assignee/SingleAssigneeHook.java @@ -1,17 +1,18 @@ package ch.sbb.polarion.extension.interceptor_hooks.single_assignee; import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; import ch.sbb.polarion.extension.interceptor.util.PropertiesUtils; -import com.polarion.alm.projects.model.IUniqueObject; import com.polarion.alm.tracker.model.IWorkItem; import com.polarion.core.util.logging.Logger; +import com.polarion.platform.persistence.model.IPObject; import org.jetbrains.annotations.NotNull; /** * Save hook for control that user can add only single assignee to WI. PS-1695 */ @SuppressWarnings("unused") -public class SingleAssigneeHook extends ActionHook { +public class SingleAssigneeHook extends ActionHook implements HookExecutor { private static final String SETTINGS_PROJECTS_DESCRIPTION = "Comma-separated list of projects. Use * to process all."; private static final String SETTINGS_PROJECTS = "projects"; @@ -20,7 +21,7 @@ public class SingleAssigneeHook extends ActionHook { private static final String SETTINGS_ERROR_MESSAGE = "errorMessage"; private static final String SETTINGS_ERROR_MESSAGE_DESCRIPTION = "Message which will be displayed in the negative case."; private static final String DEFAULT_ERROR_MESSAGE = "Only single assignee can be added to Work Item according project settings!"; - private static final String VERSION = "1.0.0"; + private static final String VERSION = "2.0.0"; private static final Logger logger = Logger.getLogger(SingleAssigneeHook.class); public SingleAssigneeHook() { @@ -28,10 +29,10 @@ public SingleAssigneeHook() { } @Override - public String processAction(@NotNull IUniqueObject object) { + public String preAction(@NotNull IPObject object) { String returnMessage = null; IWorkItem workItem = (IWorkItem) object; - if (workItem.getType() != null && workItem.getId() != null && !workItem.isUnresolvable() && workItem.isModified()) { + if (workItem.getType() != null && workItem.getId() != null && !workItem.isUnresolvable() && workItem.isModified() && workItem.getAssignees().size() > 1) { try { if (isCommaSeparatedSettingsHasItem(workItem.getProjectId(), SETTINGS_PROJECTS) && isCommaSeparatedSettingsHasItem(workItem.getType().getId(), SETTINGS_TYPES, workItem.getProjectId())) { returnMessage = getSettingsValue(SETTINGS_ERROR_MESSAGE); @@ -45,6 +46,11 @@ public String processAction(@NotNull IUniqueObject object) { return returnMessage; } + @Override + public @NotNull HookExecutor getExecutor() { + return this; //there is no need to create a separate executor instance coz only 'pre' action used + } + @Override public String getDefaultSettings() { return PropertiesUtils.buildWithDescription( diff --git a/hooks/testrun/pom.xml b/hooks/testrun/pom.xml index b23b687..510e6b3 100644 --- a/hooks/testrun/pom.xml +++ b/hooks/testrun/pom.xml @@ -8,7 +8,7 @@ jar - 1.1.1 + 2.0.0 ch.sbb.polarion.extension.interceptor_hooks.testrun testrun diff --git a/hooks/testrun/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/testrun/TestRunHook.java b/hooks/testrun/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/testrun/TestRunHook.java index 28eef15..d244415 100644 --- a/hooks/testrun/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/testrun/TestRunHook.java +++ b/hooks/testrun/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/testrun/TestRunHook.java @@ -1,8 +1,8 @@ package ch.sbb.polarion.extension.interceptor_hooks.testrun; import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; import ch.sbb.polarion.extension.interceptor.util.PropertiesUtils; -import com.polarion.alm.projects.model.IUniqueObject; import com.polarion.alm.tracker.ITrackerService; import com.polarion.alm.tracker.model.ITestRecord; import com.polarion.alm.tracker.model.ITestRun; @@ -10,6 +10,7 @@ import com.polarion.core.util.logging.Logger; import com.polarion.platform.core.PlatformContext; import com.polarion.platform.persistence.IEnumOption; +import com.polarion.platform.persistence.model.IPObject; import org.jetbrains.annotations.NotNull; import java.util.Iterator; @@ -20,14 +21,14 @@ * - Does not allow mark test case as passed if any of step not passed */ @SuppressWarnings("unused") -public class TestRunHook extends ActionHook { +public class TestRunHook extends ActionHook implements HookExecutor { private static final String SETTINGS_PROJECTS = "projects"; private static final String SETTINGS_ERROR_MESSAGE = "errorMessage"; private static final String TEST_CASE_ID_VARIABLE = "{testCaseId}"; private static final String STEP_NUMBER_VARIABLE = "{stepNumber}"; private static final String DEFAULT_ERROR_MESSAGE = "Cannot save execution results for TC " + TEST_CASE_ID_VARIABLE + " as Passed because it does not match to result in step N" + STEP_NUMBER_VARIABLE; - private static final String VERSION = "1.0.0"; + private static final String VERSION = "2.0.0"; private static final ITrackerService trackerService = PlatformContext.getPlatform().lookupService(ITrackerService.class); private static final Logger logger = Logger.getLogger(TestRunHook.class); @@ -36,7 +37,7 @@ public TestRunHook() { } @Override - public String processAction(@NotNull IUniqueObject object) { + public String preAction(@NotNull IPObject object) { String returnMessage = null; ITestRun testRun = (ITestRun) object; try { @@ -71,6 +72,11 @@ public String processAction(@NotNull IUniqueObject object) { return returnMessage; } + @Override + public @NotNull HookExecutor getExecutor() { + return this; //there is no need to create a separate executor instance coz only 'pre' action used + } + @Override public String getDefaultSettings() { return PropertiesUtils.build( diff --git a/hooks/title-length-check/pom.xml b/hooks/title-length-check/pom.xml index 88db481..4f8891f 100644 --- a/hooks/title-length-check/pom.xml +++ b/hooks/title-length-check/pom.xml @@ -8,7 +8,7 @@ jar - 1.1.1 + 2.0.0 ch.sbb.polarion.extension.interceptor_hooks.title_length_check title-length-check diff --git a/hooks/title-length-check/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/title_length_check/TitleLengthHook.java b/hooks/title-length-check/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/title_length_check/TitleLengthHook.java index 842401a..a21e01e 100644 --- a/hooks/title-length-check/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/title_length_check/TitleLengthHook.java +++ b/hooks/title-length-check/src/main/java/ch/sbb/polarion/extension/interceptor_hooks/title_length_check/TitleLengthHook.java @@ -1,23 +1,24 @@ package ch.sbb.polarion.extension.interceptor_hooks.title_length_check; import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; import ch.sbb.polarion.extension.interceptor.util.PropertiesUtils; -import com.polarion.alm.projects.model.IUniqueObject; import com.polarion.alm.tracker.model.IWorkItem; import com.polarion.core.util.logging.Logger; +import com.polarion.platform.persistence.model.IPObject; import org.jetbrains.annotations.NotNull; import java.util.List; @SuppressWarnings("unused") -public class TitleLengthHook extends ActionHook { +public class TitleLengthHook extends ActionHook implements HookExecutor { private static final String SETTINGS_ERROR_MESSAGE = "errorMessage"; private static final String SETTINGS_MAX_LENGTH = "titleMaxLength"; private static final String MAX_LENGTH_VARIABLE = "{%s}".formatted(SETTINGS_MAX_LENGTH); private static final String DEFAULT_ERROR_MESSAGE = "Title length is over the limit (" + MAX_LENGTH_VARIABLE + " symbols). Please correct it before saving"; private static final int DEFAULT_MAX_LENGTH = 256; - private static final String VERSION = "1.0.0"; + private static final String VERSION = "2.0.0"; private static final Logger logger = Logger.getLogger(TitleLengthHook.class); public TitleLengthHook() { @@ -25,7 +26,7 @@ public TitleLengthHook() { } @Override - public String processAction(@NotNull IUniqueObject object) { + public String preAction(@NotNull IPObject object) { int maxLength = DEFAULT_MAX_LENGTH; String maxLengthStringValue = getSettingsValue(SETTINGS_MAX_LENGTH); try { @@ -36,6 +37,11 @@ public String processAction(@NotNull IUniqueObject object) { return ((IWorkItem) object).getTitle().length() > maxLength ? getSettingsValue(SETTINGS_ERROR_MESSAGE).replace(MAX_LENGTH_VARIABLE, String.valueOf(maxLength)) : null; } + @Override + public @NotNull HookExecutor getExecutor() { + return this; //there is no need to create a separate executor instance coz only 'pre' action used + } + @Override public String getDefaultSettings() { return PropertiesUtils.build( diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/ActionInterceptorFactory.java b/src/main/java/ch/sbb/polarion/extension/interceptor/ActionInterceptorFactory.java deleted file mode 100644 index cc4fe5d..0000000 --- a/src/main/java/ch/sbb/polarion/extension/interceptor/ActionInterceptorFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -package ch.sbb.polarion.extension.interceptor; - -import com.polarion.core.util.logging.Logger; -import com.polarion.platform.persistence.IDataService; -import com.polarion.platform.persistence.model.IPObject; -import com.polarion.platform.persistence.model.IPObjectFactory; -import com.polarion.platform.persistence.model.IPrototype; -import com.polarion.subterra.base.SubterraURI; -import com.polarion.subterra.base.data.object.IDataObject; -import org.apache.hivemind.InterceptorStack; -import org.apache.hivemind.ServiceInterceptorFactory; -import org.apache.hivemind.internal.Module; - -import java.util.List; - -/* - * Intercept creation of polarion objects to make sure that the proxy IDataService instance is used. - * Default implementation - Nothing to change - */ -public class ActionInterceptorFactory implements ServiceInterceptorFactory { - - private static final Logger logger = Logger.getLogger(ActionInterceptorFactory.class); - - private final IDataService dataService; - - public ActionInterceptorFactory(IDataService dataService) { - this.dataService = dataService; - } - - @Override - public void createInterceptor(InterceptorStack stack, Module arg1, List arg2) { - final IPObjectFactory delegate = (IPObjectFactory) stack.peek(); - stack.push(new IPObjectFactory() { - - @SuppressWarnings("rawtypes") - @Override - public Class getCommonSuperclass() { - return delegate.getCommonSuperclass(); - } - - @Override - public IPObject createObjectForURI(SubterraURI uri, IDataService dataService) { - return delegate.createObjectForURI(uri, ActionInterceptorFactory.this.dataService); - } - - @Override - public IPObject createObjectForDAO(IDataObject dao, IDataService dataService) { - return delegate.createObjectForDAO(dao, ActionInterceptorFactory.this.dataService); - } - - @Override - public IPObject createNewObject(IPrototype prototype, IDataService dataService) { - return delegate.createNewObject(prototype, ActionInterceptorFactory.this.dataService); - } - }); - logger.info("Factory created..."); - } -} diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/ActionInvocationHandler.java b/src/main/java/ch/sbb/polarion/extension/interceptor/ActionInvocationHandler.java deleted file mode 100644 index 3b8100d..0000000 --- a/src/main/java/ch/sbb/polarion/extension/interceptor/ActionInvocationHandler.java +++ /dev/null @@ -1,61 +0,0 @@ -package ch.sbb.polarion.extension.interceptor; - -import ch.sbb.polarion.extension.interceptor.model.ActionHook; -import ch.sbb.polarion.extension.interceptor.model.HooksRegistry; -import com.polarion.alm.projects.model.IUniqueObject; -import com.polarion.core.util.exceptions.UserFriendlyRuntimeException; -import com.polarion.core.util.logging.Logger; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Iterator; -import java.util.Set; -import java.util.stream.Collectors; - -public class ActionInvocationHandler implements InvocationHandler { - - private static final Logger logger = Logger.getLogger(ActionInvocationHandler.class); - private static final Set SUPPORTED_ACTIONS = Arrays.stream(ActionHook.ActionType.values()).map(ActionHook.ActionType::getMethodName).collect(Collectors.toSet()); - - private final Object delegate; - - public ActionInvocationHandler(Object delegate) { - this.delegate = delegate; - HooksRegistry.HOOKS.refresh(); - } - - @Override - @SuppressWarnings("squid:S112") //no need for dedicated exception instead of basic RuntimeException in this method - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String interruptReasonMessage = null; - if (SUPPORTED_ACTIONS.contains(method.getName()) && args != null && args.length > 0 && args[0] instanceof IUniqueObject polarionObject) { - Iterator hooksIterator = HooksRegistry.HOOKS.list().stream() - .filter(h -> h.isEnabled() && - h.getActionType().canProcess(method) && - h.getItemTypes().stream().anyMatch(type -> type.canProcess(args[0])) - ).iterator(); - while (hooksIterator.hasNext() && interruptReasonMessage == null) { - try { - interruptReasonMessage = hooksIterator.next().processAction(polarionObject); - } catch (Exception e) { - logger.error("Error running interceptor for method: " + method, e); - } - } - } - - // Delegate all calls to the base IDataService if no error messages. - if (interruptReasonMessage == null) { - try { - return method.invoke(delegate, args); - } catch (IllegalArgumentException e) { - throw new RuntimeException("Internal error in ITrackerService proxy.", e); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - } else { - throw new UserFriendlyRuntimeException(interruptReasonMessage); - } - } -} diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptor.java b/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptor.java new file mode 100644 index 0000000..d4bd1a0 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptor.java @@ -0,0 +1,45 @@ +package ch.sbb.polarion.extension.interceptor; + +import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HookExecutor; +import ch.sbb.polarion.extension.interceptor.model.HooksRegistry; +import com.polarion.core.util.StringUtils; +import com.polarion.core.util.exceptions.UserFriendlyRuntimeException; +import com.polarion.core.util.logging.Logger; +import com.polarion.platform.persistence.model.IPObject; + +import java.util.List; + +@SuppressWarnings({"unused", "unchecked"}) +public class DataServiceInterceptor { + + private static final Logger logger = Logger.getLogger(DataServiceInterceptor.class); + + public List getHookExecutors(boolean saveAction, IPObject polarionObject) { + return HooksRegistry.HOOKS.list().stream() + .filter(h -> h.isEnabled() && + h.getActionType().equals(saveAction ? ActionHook.ActionType.SAVE : ActionHook.ActionType.DELETE) && + h.getItemTypes().stream().anyMatch(type -> type.canProcess(polarionObject)) + ).map(ActionHook::getExecutor).toList(); + } + + public void callExecutors(Object executorsListObject, boolean pre, IPObject polarionObject) { + List executors = (List) executorsListObject; + executors.forEach(executor -> { + String interruptReasonMessage = null; + try { + if (pre) { + interruptReasonMessage = executor.preAction(polarionObject); + } else { + executor.postAction(polarionObject); + } + } catch (Exception e) { + logger.error("Error running hook executor", e); + } + if (!StringUtils.isEmpty(interruptReasonMessage)) { + throw new UserFriendlyRuntimeException(interruptReasonMessage); + } + }); + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptorFactory.java b/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptorFactory.java index ca35ef6..8178be1 100644 --- a/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptorFactory.java +++ b/src/main/java/ch/sbb/polarion/extension/interceptor/DataServiceInterceptorFactory.java @@ -1,22 +1,119 @@ package ch.sbb.polarion.extension.interceptor; -import com.polarion.core.util.logging.Logger; -import com.polarion.platform.persistence.IDataService; +import ch.sbb.polarion.extension.interceptor.model.ActionHook; +import ch.sbb.polarion.extension.interceptor.model.HooksRegistry; +import com.polarion.platform.persistence.model.IPObject; import org.apache.hivemind.InterceptorStack; import org.apache.hivemind.ServiceInterceptorFactory; import org.apache.hivemind.internal.Module; +import org.apache.hivemind.service.BodyBuilder; +import org.apache.hivemind.service.ClassFab; +import org.apache.hivemind.service.ClassFabUtils; +import org.apache.hivemind.service.ClassFactory; +import org.apache.hivemind.service.MethodIterator; +import org.apache.hivemind.service.MethodSignature; -import java.lang.reflect.Proxy; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +@SuppressWarnings({"unused", "rawtypes"}) public class DataServiceInterceptorFactory implements ServiceInterceptorFactory { - private static final Logger logger = Logger.getLogger(DataServiceInterceptorFactory.class); + private final DataServiceInterceptor interceptor; + private ClassFactory classFactory; + + public DataServiceInterceptorFactory() { + HooksRegistry.HOOKS.refresh(); + interceptor = new DataServiceInterceptor(); + } + + private static boolean checkMethodSignature(MethodSignature sig, String name, List params) { + Set parameterTypes = Arrays.stream(sig.getParameterTypes()).collect(Collectors.toSet()); + return sig.getName().equals(name) && parameterTypes.size() == params.size() && parameterTypes.containsAll(params); + } + + public void setFactory(ClassFactory factory) { + classFactory = factory; + } @Override - public void createInterceptor(InterceptorStack stack, Module module, List parameters) { - final Object delegate = stack.peek(); - stack.push(Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{IDataService.class}, new ActionInvocationHandler(delegate))); - logger.info("Interceptor proxy created..."); + public void createInterceptor(InterceptorStack stack, Module invokingModule, List parameters) { + try { + Class interceptorClass = constructInterceptorClass(stack); + Object stackTop = stack.peek(); + Object proxy = interceptorClass.getConstructors()[0].newInstance(stackTop, interceptor); + stack.push(proxy); + } catch ( + InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | SecurityException e) { + throw new IllegalStateException("The DataService Interceptor could not be instantiated.", e); + } + } + + private Class constructInterceptorClass(InterceptorStack stack) { + Class serviceInterfaceClass = stack.getServiceInterface(); + String name = ClassFabUtils.generateClassName(serviceInterfaceClass); + ClassFab classFab = classFactory.newClass(name, Object.class); + classFab.addInterface(serviceInterfaceClass); + createInfrastructure(stack, classFab); + addServiceMethods(stack, classFab); + return classFab.createClass(); + } + + private void createInfrastructure(InterceptorStack stack, ClassFab classFab) { + Class topClass = ClassFabUtils.getInstanceClass(stack.peek(), stack.getServiceInterface()); + classFab.addField("_delegate", topClass); + classFab.addField("_interceptor", DataServiceInterceptor.class); + classFab.addConstructor( + new Class[]{topClass, DataServiceInterceptor.class}, null, "{_delegate = $1; _interceptor = $2;}" + ); } -} + + private void addServiceMethods(InterceptorStack stack, ClassFab fab) { + MethodIterator mi = new MethodIterator(stack.getServiceInterface()); + while (mi.hasNext()) { + MethodSignature sig = mi.next(); + if (checkMethodSignature(sig, ActionHook.ActionType.SAVE.getMethodName(), List.of(IPObject.class))) { + addSaveMethodImplementation(fab, sig); + } else if (checkMethodSignature(sig, ActionHook.ActionType.DELETE.getMethodName(), List.of(IPObject.class))) { + addDeleteMethodImplementation(fab, sig); + } else { + addPassThroughMethodImplementation(fab, sig); + } + } + } + + private void addSaveMethodImplementation(ClassFab classFab, MethodSignature sig) { + BodyBuilder builder = new BodyBuilder(); + builder.begin(); + builder.add("Object _hooksExecutors = _interceptor.getHookExecutors(true, $1); "); + builder.add("_interceptor.callExecutors(_hooksExecutors, true, $1); "); + builder.add("_delegate.{0}($$); ", sig.getName()); + builder.add("_interceptor.callExecutors(_hooksExecutors, false, $1); "); + builder.end(); + classFab.addMethod(1, sig, builder.toString()); + } + + private void addDeleteMethodImplementation(ClassFab classFab, MethodSignature sig) { + BodyBuilder builder = new BodyBuilder(); + builder.begin(); + builder.add("Object _hooksExecutors = _interceptor.getHookExecutors(false, $1); "); + builder.add("_interceptor.callExecutors(_hooksExecutors, true, $1); "); + builder.add("_delegate.{0}($$); ", sig.getName()); + builder.add("_interceptor.callExecutors(_hooksExecutors, false, $1); "); + builder.end(); + classFab.addMethod(1, sig, builder.toString()); + } + + private void addPassThroughMethodImplementation(ClassFab classFab, MethodSignature sig) { + BodyBuilder builder = new BodyBuilder(); + builder.begin(); + builder.add("return ($r) _delegate." + sig.getName() + "($$);"); + builder.end(); + classFab.addMethod(1, sig, builder.toString()); + } + +} \ No newline at end of file diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/model/ActionHook.java b/src/main/java/ch/sbb/polarion/extension/interceptor/model/ActionHook.java index 0cfdf60..a8f94ae 100644 --- a/src/main/java/ch/sbb/polarion/extension/interceptor/model/ActionHook.java +++ b/src/main/java/ch/sbb/polarion/extension/interceptor/model/ActionHook.java @@ -21,7 +21,11 @@ import java.util.List; import java.util.Objects; +/** + * Base class for a hook. + */ @Data +@SuppressWarnings("java:S5993") //leave public constructors public abstract class ActionHook { public static final String ALL_WILDCARD = "*"; @@ -52,7 +56,13 @@ public HookModel loadSettings(boolean forceUpdate) { return settings; } - public abstract String processAction(@NotNull IUniqueObject polarionObject); + /** + * Basic usage - instantiate executor only once and reuse its instance. But if both methods ('pre' & 'post') used + * in the same executor and 'post' wants to process some data which was produced by 'pre' - in this case it's a good idea to + * return a new instance here in order to prevent data rewrite by the concurrent executions. + */ + @JsonIgnore + public abstract @NotNull HookExecutor getExecutor(); @JsonProperty("name") public String getName() { diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/model/HookExecutor.java b/src/main/java/ch/sbb/polarion/extension/interceptor/model/HookExecutor.java new file mode 100644 index 0000000..d4d1172 --- /dev/null +++ b/src/main/java/ch/sbb/polarion/extension/interceptor/model/HookExecutor.java @@ -0,0 +1,28 @@ +package ch.sbb.polarion.extension.interceptor.model; + +import com.polarion.platform.persistence.model.IPObject; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public interface HookExecutor { + + /** + * Executed before action. It is possible to prevent action execution by returning non-null string from this method. + * This string will be displayed as a popup message when running from UI or as a 'detail' field in the + * Internal Server Error response (500) when calling REST API. + * + * @return non-null interrupt reason string in case if action must be prevented + */ + default String preAction(@NotNull IPObject polarionObject) { + //do nothing by default + return null; + } + + /** + * Executed after action + */ + default void postAction(@NotNull IPObject polarionObject) { + //do nothing by default + } + +} diff --git a/src/main/java/ch/sbb/polarion/extension/interceptor/util/HookJarUtils.java b/src/main/java/ch/sbb/polarion/extension/interceptor/util/HookJarUtils.java index 381d8c9..a9e8f5d 100644 --- a/src/main/java/ch/sbb/polarion/extension/interceptor/util/HookJarUtils.java +++ b/src/main/java/ch/sbb/polarion/extension/interceptor/util/HookJarUtils.java @@ -11,6 +11,7 @@ import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; +import java.util.jar.JarEntry; import java.util.jar.JarInputStream; @UtilityClass @@ -49,8 +50,8 @@ private List loadClasses(String jarsDirPath) { if (jarFiles != null) { for (File jarFile : jarFiles) { try { - classList.add(loadClass(jarFile)); - } catch (Throwable e) { + classList.add(loadJarReturnHookClass(jarFile)); + } catch (Exception e) { logger.error("Cannot load jar file " + jarFile.getPath(), e); } } @@ -58,16 +59,28 @@ private List loadClasses(String jarsDirPath) { return classList; } - private Class loadClass(File jarFile) throws IOException, ClassNotFoundException { - try (JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jarFile))) { + private Class loadJarReturnHookClass(File jarFile) throws IOException, ClassNotFoundException { + try (JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jarFile)); + URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:" + jarFile.getAbsolutePath() + "!/")}, HookJarUtils.class.getClassLoader())) { String mainClassName = jarInputStream.getManifest().getMainAttributes().getValue(MAIN_CLASS_MANIFEST_ATTRIBUTE); - if (mainClassName != null) { - //closing urlClassLoader allows us to remove initial jar file after its load - try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarFile.toURI().toURL()}, HookJarUtils.class.getClassLoader())) { - return Class.forName(mainClassName, true, urlClassLoader); + + //load all classes from jar and return hook class which name must be declared in the manifest + Class hookClass = null; + JarEntry jarEntry; + while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { + if (jarEntry.getName().endsWith(".class")) { + String className = jarEntry.getName().replace("/", ".").replace(".class", ""); + Class clazz = urlClassLoader.loadClass(className); + if (className.equals(mainClassName)) { + hookClass = clazz; + } } - } else { + } + + if (hookClass == null) { throw new ClassNotFoundException("Jar file " + jarFile.getPath() + " does not contain " + MAIN_CLASS_MANIFEST_ATTRIBUTE + " attribute in the MANIFEST"); + } else { + return hookClass; } } } diff --git a/src/main/resources/META-INF/hivemodule.xml b/src/main/resources/META-INF/hivemodule.xml index 41b17ca..0f751a8 100644 --- a/src/main/resources/META-INF/hivemodule.xml +++ b/src/main/resources/META-INF/hivemodule.xml @@ -12,46 +12,6 @@ - - - - com.polarion.platform.persistence.dataservice.dataService - - - - - - - - - - - - - - com.polarion.platform.persistence.dataservice.dataService - - - - - - - - - - - - - - com.polarion.platform.persistence.dataservice.dataService - - - - - - - -