diff --git a/src/main/java/com/epam/reportportal/extension/github/GitHubExtension.java b/src/main/java/com/epam/reportportal/extension/github/GitHubExtension.java index c64dde3..b43624b 100644 --- a/src/main/java/com/epam/reportportal/extension/github/GitHubExtension.java +++ b/src/main/java/com/epam/reportportal/extension/github/GitHubExtension.java @@ -27,6 +27,7 @@ import com.epam.reportportal.base.infrastructure.commons.ContentTypeResolver; import com.epam.reportportal.base.infrastructure.persistence.binary.UserBinaryDataService; import com.epam.reportportal.base.infrastructure.persistence.dao.IntegrationRepository; +import com.epam.reportportal.base.infrastructure.persistence.dao.IntegrationTypeRepository; import com.epam.reportportal.base.infrastructure.persistence.dao.ProjectRepository; import com.epam.reportportal.base.infrastructure.persistence.dao.UserRepository; import com.epam.reportportal.base.infrastructure.persistence.entity.enums.IntegrationAuthFlowEnum; @@ -36,20 +37,27 @@ import com.epam.reportportal.extension.IntegrationGroupEnum; import com.epam.reportportal.extension.PluginCommand; import com.epam.reportportal.extension.github.command.SynchronizeGithubUserCommand; +import com.epam.reportportal.extension.github.event.listener.PluginLoadedEventListener; import com.epam.reportportal.extension.github.oauth.GitHubOAuthProvider; import com.epam.reportportal.extension.github.service.GitHubIntegrationStrategy; import com.epam.reportportal.extension.github.service.GitHubRequiredParamNamesProvider; import com.epam.reportportal.extension.github.utils.MemoizingSupplier; import jakarta.annotation.PostConstruct; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; +import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.jasypt.util.text.BasicTextEncryptor; import org.pf4j.Extension; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ApplicationEventMulticaster; +import org.springframework.context.support.AbstractApplicationContext; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -58,11 +66,12 @@ */ @Extension @Slf4j -public class GitHubExtension implements AuthExtension { +public class GitHubExtension implements AuthExtension, DisposableBean { public static final String SSO_LOGIN_PATH = "/oauth/login"; - public static final String SCHEMA_SCRIPTS_DIR = "schema"; + public static final String SCHEMA_SCRIPTS_DIR = "resources/schema"; + private static final String PLUGIN_ID = "github"; private static final String PLUGIN_NAME = "GitHub OAuth Plugin"; private static final String DOCUMENTATION_LINK = "https://reportportal.io/docs/plugins/authorization/GitHubAuthorization"; private static final String DOCUMENTATION_LINK_FIELD = "documentationLink"; @@ -80,6 +89,12 @@ public boolean supports(Class authentication) { } }; + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private IntegrationTypeRepository integrationTypeRepository; + @Autowired private UserRepository userRepository; @@ -107,21 +122,29 @@ public boolean supports(Class authentication) { @Autowired private BasicTextEncryptor encryptor; + @Autowired + private DataSource dataSource; + private GitHubUserReplicator replicator; private GitHubOAuthProvider oauthProvider; private Map> commonCommands; private Supplier gitHubIntegrationStrategySupplier; + private Supplier pluginLoadedListenerSupplier; @PostConstruct - public void init() { + public void init() throws IOException { log.info("Initializing GitHub OAuth extension"); this.gitHubIntegrationStrategySupplier = new MemoizingSupplier<>( () -> new GitHubIntegrationStrategy(integrationRepository, new UpdateAuthRequestValidator(new GitHubRequiredParamNamesProvider()), integrationDuplicateValidator, encryptor)); + this.pluginLoadedListenerSupplier = new MemoizingSupplier<>( + () -> new PluginLoadedEventListener(PLUGIN_ID, integrationTypeRepository, integrationRepository, + integrationType -> integrationType, dataSource)); + replicator = new GitHubUserReplicator( userRepository, projectRepository, personalProjectService, userBinaryDataService, contentTypeResolver, userEventPublisher @@ -130,8 +153,24 @@ public void init() { SynchronizeGithubUserCommand syncCommand = new SynchronizeGithubUserCommand(replicator); commonCommands = Map.of(syncCommand.getName(), syncCommand); -/* initApplicationListeners(); - initSchema();*/ + initListeners(); + } + + private void initListeners() { + ApplicationEventMulticaster applicationEventMulticaster = applicationContext.getBean( + AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME, + ApplicationEventMulticaster.class + ); + applicationEventMulticaster.addApplicationListener(pluginLoadedListenerSupplier.get()); + } + + @Override + public void destroy() { + ApplicationEventMulticaster applicationEventMulticaster = applicationContext.getBean( + AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME, + ApplicationEventMulticaster.class + ); + applicationEventMulticaster.removeApplicationListener(pluginLoadedListenerSupplier.get()); } @Override diff --git a/src/main/java/com/epam/reportportal/extension/github/event/listener/PluginLoadedEventListener.java b/src/main/java/com/epam/reportportal/extension/github/event/listener/PluginLoadedEventListener.java new file mode 100644 index 0000000..b3012fe --- /dev/null +++ b/src/main/java/com/epam/reportportal/extension/github/event/listener/PluginLoadedEventListener.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.extension.github.event.listener; + +import static com.epam.reportportal.extension.github.GitHubExtension.SCHEMA_SCRIPTS_DIR; + +import com.epam.reportportal.base.core.events.domain.PluginUploadedEvent; +import com.epam.reportportal.base.infrastructure.persistence.dao.IntegrationRepository; +import com.epam.reportportal.base.infrastructure.persistence.dao.IntegrationTypeRepository; +import com.epam.reportportal.extension.github.info.PluginInfoProvider; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Objects; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationListener; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * @author Andrei Piankouski + */ +@Slf4j +public class PluginLoadedEventListener implements ApplicationListener { + + private final String pluginId; + private final IntegrationTypeRepository integrationTypeRepository; + private final IntegrationRepository integrationRepository; + private final PluginInfoProvider pluginInfoProvider; + private final DataSource dataSource; + + public PluginLoadedEventListener(String pluginId, + IntegrationTypeRepository integrationTypeRepository, + IntegrationRepository integrationRepository, PluginInfoProvider pluginInfoProvider, DataSource dataSource) { + this.pluginId = pluginId; + this.integrationTypeRepository = integrationTypeRepository; + this.integrationRepository = integrationRepository; + this.pluginInfoProvider = pluginInfoProvider; + this.dataSource = dataSource; + } + + @Override + public void onApplicationEvent(PluginUploadedEvent event) { + if (!supports(event)) { + return; + } + initSchema(); + } + + private boolean supports(PluginUploadedEvent event) { + return Objects.nonNull(event.getPluginActivityResource()) + && pluginId.equals(event.getPluginActivityResource().getName()); + } + + @SneakyThrows + public void initSchema() { + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader()); + Resource[] resources = resolver.getResources("classpath:" + SCHEMA_SCRIPTS_DIR + "/*.sql"); + log.debug("GitHub schema init: found {} script(s)", resources.length); + if (resources.length == 0) { + return; + } + Arrays.sort(resources, Comparator.comparing(Resource::getFilename)); + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + for (Resource r : resources) { + log.info("GitHub schema init: executing {}", r.getFilename()); + try (InputStream is = r.getInputStream()) { + String sql = new String(is.readAllBytes(), StandardCharsets.UTF_8); + jdbcTemplate.execute(sql); + } + } + log.info("GitHub schema init: completed"); + } + +} diff --git a/src/main/java/com/epam/reportportal/extension/github/info/PluginInfoProvider.java b/src/main/java/com/epam/reportportal/extension/github/info/PluginInfoProvider.java new file mode 100644 index 0000000..dbaf913 --- /dev/null +++ b/src/main/java/com/epam/reportportal/extension/github/info/PluginInfoProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright 2026 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.extension.github.info; + +import com.epam.reportportal.base.infrastructure.persistence.entity.integration.IntegrationType; + +/** + * @author Ivan Budayeu + */ +public interface PluginInfoProvider { + + IntegrationType provide(IntegrationType integrationType); +} diff --git a/src/main/java/com/epam/reportportal/extension/github/info/impl/PluginInfoProviderImpl.java b/src/main/java/com/epam/reportportal/extension/github/info/impl/PluginInfoProviderImpl.java new file mode 100644 index 0000000..915364d --- /dev/null +++ b/src/main/java/com/epam/reportportal/extension/github/info/impl/PluginInfoProviderImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.extension.github.info.impl; + +import static java.util.Optional.ofNullable; + +import com.epam.reportportal.base.infrastructure.persistence.entity.integration.IntegrationType; +import com.epam.reportportal.base.infrastructure.rules.exception.ErrorType; +import com.epam.reportportal.base.infrastructure.rules.exception.ReportPortalException; +import com.epam.reportportal.extension.github.info.PluginInfoProvider; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * @author Ivan Budayeu + */ +public class PluginInfoProviderImpl implements PluginInfoProvider { + + private static final String BINARY_DATA_KEY = "binaryData"; + private static final String DESCRIPTION_KEY = "description"; + private static final String METADATA_KEY = "metadata"; + + private static final String PLUGIN_DESCRIPTION = + "The integration provides an exchange of information between ReportPortal and the Jira Cloud, such as posting issues and linking issues, getting updates on their statuses."; + + static final Map PLUGIN_METADATA = Map.of( + "embedded", false, + "multiple", false + ); + + private final String resourcesDir; + private final String propertyFile; + + public PluginInfoProviderImpl(String resourcesDir, String propertyFile) { + this.resourcesDir = resourcesDir; + this.propertyFile = propertyFile; + } + + @Override + public IntegrationType provide(IntegrationType integrationType) { + loadBinaryDataInfo(integrationType); + updateDescription(integrationType); + updateMetadata(integrationType); + return integrationType; + } + + private void loadBinaryDataInfo(IntegrationType integrationType) { + Map details = integrationType.getDetails().getDetails(); + if (ofNullable(details.get(BINARY_DATA_KEY)).isEmpty()) { + try (InputStream propertiesStream = Files.newInputStream( + Paths.get(resourcesDir, propertyFile))) { + Properties binaryDataProperties = new Properties(); + binaryDataProperties.load(propertiesStream); + Map binaryDataInfo = binaryDataProperties.entrySet().stream().collect( + HashMap::new, (map, entry) -> map.put(String.valueOf(entry.getKey()), + String.valueOf(entry.getValue()) + ), HashMap::putAll); + details.put(BINARY_DATA_KEY, binaryDataInfo); + } catch (IOException ex) { + throw new ReportPortalException(ErrorType.UNABLE_TO_LOAD_BINARY_DATA, ex.getMessage()); + } + } + } + + private void updateDescription(IntegrationType integrationType) { + Map details = integrationType.getDetails().getDetails(); + details.put(DESCRIPTION_KEY, PLUGIN_DESCRIPTION); + } + + private void updateMetadata(IntegrationType integrationType) { + Map details = integrationType.getDetails().getDetails(); + details.put(METADATA_KEY, PLUGIN_METADATA); + } +} diff --git a/src/main/resources/schema/001_restore_github_integration.sql b/src/main/resources/schema/001_restore_github_integration.sql new file mode 100644 index 0000000..6855a03 --- /dev/null +++ b/src/main/resources/schema/001_restore_github_integration.sql @@ -0,0 +1,27 @@ +DO +' +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = ''integration_backup'') THEN + + -- restore saml integration from backup + INSERT INTO integration (name, type, params, creator, creation_date, enabled) + SELECT ib.name, + (SELECT it.id FROM integration_type it WHERE it.auth_flow = ''OAUTH'' AND it.group_type = ''AUTH'' AND it.plugin_type = ''EXTENSION''), + ib.params, + ''SYSTEM'', + now(), + true + FROM integration_backup ib + WHERE ib.auth_type = ''github'' + ON CONFLICT (id) DO NOTHING; + + -- delete backup record + DELETE from integration_backup ib WHERE ib.auth_type = ''github''; + + -- drop table if empty + IF NOT EXISTS (SELECT 1 FROM integration_backup) THEN + DROP TABLE integration_backup; + END IF; + END IF; +END; +';