Fix CustomBaseRepositoryBean - not initialized in some cases (#2241)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-01-27 14:53:28 +02:00
committed by GitHub
parent c766fd76da
commit e3c41eb0b2
9 changed files with 55 additions and 66 deletions

View File

@@ -29,7 +29,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-text</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.ben-manes.caffeine</groupId> <groupId>com.github.ben-manes.caffeine</groupId>

View File

@@ -11,8 +11,8 @@ package org.eclipse.hawkbit.repository.rsql;
import java.io.Serial; import java.io.Serial;
import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.lang3.text.StrSubstitutor; import org.apache.commons.text.lookup.StringLookupFactory;
import org.eclipse.hawkbit.repository.TimestampCalculator; import org.eclipse.hawkbit.repository.TimestampCalculator;
/** /**
@@ -41,31 +41,30 @@ import org.eclipse.hawkbit.repository.TimestampCalculator;
* configuration.</li> * configuration.</li>
* </ul> * </ul>
*/ */
public class VirtualPropertyResolver extends StrLookup<String> implements VirtualPropertyReplacer { public class VirtualPropertyResolver implements VirtualPropertyReplacer {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private transient StrSubstitutor substitutor; private transient StringSubstitutor substitutor;
@Override
public String lookup(final String rhs) {
String resolved = null;
if ("now_ts".equalsIgnoreCase(rhs)) {
resolved = String.valueOf(System.currentTimeMillis());
} else if ("overdue_ts".equalsIgnoreCase(rhs)) {
resolved = String.valueOf(TimestampCalculator.calculateOverdueTimestamp());
}
return resolved;
}
@Override @Override
public String replace(final String input) { public String replace(final String input) {
if (substitutor == null) { if (substitutor == null) {
substitutor = new StrSubstitutor(this, StrSubstitutor.DEFAULT_PREFIX, StrSubstitutor.DEFAULT_SUFFIX, substitutor = new StringSubstitutor(
StrSubstitutor.DEFAULT_ESCAPE); StringLookupFactory.builder().get().functionStringLookup(this::lookup),
StringSubstitutor.DEFAULT_PREFIX, StringSubstitutor.DEFAULT_SUFFIX, StringSubstitutor.DEFAULT_ESCAPE);
} }
return substitutor.replace(input); return substitutor.replace(input);
} }
private String lookup(final String rhs) {
if ("now_ts".equalsIgnoreCase(rhs)) {
return String.valueOf(System.currentTimeMillis());
} else if ("overdue_ts".equalsIgnoreCase(rhs)) {
return String.valueOf(TimestampCalculator.calculateOverdueTimestamp());
} else {
return null;
}
}
} }

View File

@@ -12,9 +12,11 @@ package org.eclipse.hawkbit.repository.jpa;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider; import org.eclipse.hawkbit.repository.BaseRepositoryTypeProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.lang.NonNull;
/** /**
* A {@link JpaRepositoryFactoryBean} extension that allow injection of custom repository factories by using a * A {@link JpaRepositoryFactoryBean} extension that allow injection of custom repository factories by using a
@@ -23,20 +25,24 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport
@SuppressWarnings("java:S119") // java:S119 - ID is inherited from JpaRepositoryFactoryBean @SuppressWarnings("java:S119") // java:S119 - ID is inherited from JpaRepositoryFactoryBean
public class CustomBaseRepositoryFactoryBean<T extends Repository<S, ID>, S, ID> extends JpaRepositoryFactoryBean<T, S, ID> { public class CustomBaseRepositoryFactoryBean<T extends Repository<S, ID>, S, ID> extends JpaRepositoryFactoryBean<T, S, ID> {
private final BaseRepositoryTypeProvider baseRepoProvider; private BaseRepositoryTypeProvider baseRepoProvider;
/** /**
* Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface.
* *
* @param repositoryInterface must not be {@literal null}. * @param repositoryInterface must not be {@literal null}.
*/ */
public CustomBaseRepositoryFactoryBean(final Class<? extends T> repositoryInterface, final BaseRepositoryTypeProvider baseRepoProvider) { public CustomBaseRepositoryFactoryBean(final Class<? extends T> repositoryInterface) {
super(repositoryInterface); super(repositoryInterface);
}
@Autowired // if it is a constructor injection sometimes doesn't work - base repo provider is not available at construct time
public void setBaseRepoProvider(final BaseRepositoryTypeProvider baseRepoProvider) {
this.baseRepoProvider = baseRepoProvider; this.baseRepoProvider = baseRepoProvider;
} }
@Override @Override
protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) { protected RepositoryFactorySupport createRepositoryFactory(@NonNull final EntityManager entityManager) {
final RepositoryFactorySupport rfs = super.createRepositoryFactory(entityManager); final RepositoryFactorySupport rfs = super.createRepositoryFactory(entityManager);
rfs.setRepositoryBaseClass(baseRepoProvider.getBaseRepositoryType(getObjectType())); rfs.setRepositoryBaseClass(baseRepoProvider.getBaseRepositoryType(getObjectType()));
return rfs; return rfs;

View File

@@ -187,7 +187,6 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.integration.support.locks.LockRegistry; import org.springframework.integration.support.locks.LockRegistry;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.retry.annotation.EnableRetry; import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
@@ -459,8 +458,7 @@ public class RepositoryApplicationConfiguration {
} }
/** /**
* @return the singleton instance of the * @return the singleton instance of the {@link AfterTransactionCommitExecutorHolder}
* {@link AfterTransactionCommitExecutorHolder}
*/ */
@Bean @Bean
AfterTransactionCommitExecutorHolder afterTransactionCommitExecutorHolder() { AfterTransactionCommitExecutorHolder afterTransactionCommitExecutorHolder() {
@@ -475,6 +473,17 @@ public class RepositoryApplicationConfiguration {
return new ExceptionMappingAspectHandler(); return new ExceptionMappingAspectHandler();
} }
/**
* Default {@link BaseRepositoryTypeProvider} bean always provides the NoCountBaseRepository
*
* @return a {@link BaseRepositoryTypeProvider} bean
*/
@Bean
@ConditionalOnMissingBean
BaseRepositoryTypeProvider baseRepositoryTypeProvider() {
return new HawkbitBaseRepository.RepositoryTypeProvider();
}
/** /**
* {@link JpaSystemManagement} bean. * {@link JpaSystemManagement} bean.
* *
@@ -1027,18 +1036,6 @@ public class RepositoryApplicationConfiguration {
tenantAware, lockRegistry, systemSecurityContext); tenantAware, lockRegistry, systemSecurityContext);
} }
/**
* Default {@link BaseRepositoryTypeProvider} bean always provides the
* NoCountBaseRepository
*
* @return a {@link BaseRepositoryTypeProvider} bean
*/
@Bean
@ConditionalOnMissingBean
BaseRepositoryTypeProvider baseRepositoryTypeProvider() {
return new HawkbitBaseRepository.RepositoryTypeProvider();
}
/** /**
* Default artifact encryption service bean that internally uses * Default artifact encryption service bean that internally uses
* {@link ArtifactEncryption} and {@link ArtifactEncryptionSecretsStore} beans * {@link ArtifactEncryption} and {@link ArtifactEncryptionSecretsStore} beans

View File

@@ -437,10 +437,7 @@ class RSQLUtilityTest {
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification // verification
verify(macroResolver).lookup(overdueProp); // the macro is already replaced when passed to #lessThanOrEqualTo -> the method is never invoked with the placeholder:
// the macro is already replaced when passed to #lessThanOrEqualTo ->
// the method is never invoked with the
// placeholder:
verify(criteriaBuilderMock, never()).lessThanOrEqualTo(pathOfString(baseSoftwareModuleRootMock), overduePropPlaceholder); verify(criteriaBuilderMock, never()).lessThanOrEqualTo(pathOfString(baseSoftwareModuleRootMock), overduePropPlaceholder);
} }
@@ -462,9 +459,7 @@ class RSQLUtilityTest {
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification // verification
verify(macroResolver).lookup(overdueProp); // the macro is unknown and hence never replaced -> #lessThanOrEqualTo is invoked with the placeholder:
// the macro is unknown and hence never replaced -> #lessThanOrEqualTo
// is invoked with the placeholder:
verify(criteriaBuilderMock).lessThanOrEqualTo(pathOfString(baseSoftwareModuleRootMock), overduePropPlaceholder); verify(criteriaBuilderMock).lessThanOrEqualTo(pathOfString(baseSoftwareModuleRootMock), overduePropPlaceholder);
} }

View File

@@ -17,7 +17,7 @@ import java.util.concurrent.Callable;
import io.qameta.allure.Description; import io.qameta.allure.Description;
import io.qameta.allure.Feature; import io.qameta.allure.Feature;
import io.qameta.allure.Story; import io.qameta.allure.Story;
import org.apache.commons.lang3.text.StrSubstitutor; import org.apache.commons.text.StringSubstitutor;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
@@ -31,7 +31,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.Spy;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
@@ -47,13 +46,12 @@ class VirtualPropertyResolverTest {
private static final TenantConfigurationValue<String> TEST_POLLING_OVERDUE_TIME_INTERVAL = private static final TenantConfigurationValue<String> TEST_POLLING_OVERDUE_TIME_INTERVAL =
TenantConfigurationValue.<String> builder().value("00:07:37").build(); TenantConfigurationValue.<String> builder().value("00:07:37").build();
@Spy
private final VirtualPropertyResolver resolverUnderTest = new VirtualPropertyResolver();
@MockitoBean @MockitoBean
private TenantConfigurationManagement confMgmt; private TenantConfigurationManagement confMgmt;
@MockitoBean @MockitoBean
private SystemSecurityContext securityContext; private SystemSecurityContext securityContext;
private StrSubstitutor substitutor;
private final VirtualPropertyResolver substitutor = new VirtualPropertyResolver();
@BeforeEach @BeforeEach
void before() { void before() {
@@ -61,9 +59,6 @@ class VirtualPropertyResolverTest {
.thenReturn(TEST_POLLING_TIME_INTERVAL); .thenReturn(TEST_POLLING_TIME_INTERVAL);
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class)) when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class))
.thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL); .thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL);
substitutor = new StrSubstitutor(resolverUnderTest, StrSubstitutor.DEFAULT_PREFIX,
StrSubstitutor.DEFAULT_SUFFIX, StrSubstitutor.DEFAULT_ESCAPE);
} }
@Test @Test
@@ -80,8 +75,8 @@ class VirtualPropertyResolverTest {
@Description("Tests escape mechanism for placeholders (syntax is $${SOME_PLACEHOLDER}).") @Description("Tests escape mechanism for placeholders (syntax is $${SOME_PLACEHOLDER}).")
void handleEscapedPlaceholder() { void handleEscapedPlaceholder() {
final String placeholder = "${OVERDUE_TS}"; final String placeholder = "${OVERDUE_TS}";
final String escaptedPlaceholder = StrSubstitutor.DEFAULT_ESCAPE + placeholder; final String escapedPlaceholder = StringSubstitutor.DEFAULT_ESCAPE + placeholder;
final String testString = "lhs=lt=" + escaptedPlaceholder; final String testString = "lhs=lt=" + escapedPlaceholder;
final String resolvedPlaceholders = substitutor.replace(testString); final String resolvedPlaceholders = substitutor.replace(testString);
assertThat(resolvedPlaceholders).as("Escaped OVERDUE_TS should not be resolved!").contains(placeholder); assertThat(resolvedPlaceholders).as("Escaped OVERDUE_TS should not be resolved!").contains(placeholder);
@@ -89,14 +84,13 @@ class VirtualPropertyResolverTest {
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = { "${NOW_TS}", "${OVERDUE_TS}", "${overdue_ts}" }) @ValueSource(strings = { "${NOW_TS}", "${OVERDUE_TS}", "${overdue_ts}" })
@Description("Tests resolution of NOW_TS by using a StrSubstitutor configured with the VirtualPropertyResolver.") @Description("Tests resolution of NOW_TS by using a StringSubstitutor configured with the VirtualPropertyResolver.")
void resolveNowTimestampPlaceholder(final String placeholder) { void resolveNowTimestampPlaceholder(final String placeholder) {
when(securityContext.runAsSystem(Mockito.any())).thenAnswer(a -> ((Callable<?>) a.getArgument(0)).call()); when(securityContext.runAsSystem(Mockito.any())).thenAnswer(a -> ((Callable<?>) a.getArgument(0)).call());
final String testString = "lhs=lt=" + placeholder; final String testString = "lhs=lt=" + placeholder;
final String resolvedPlaceholders = substitutor.replace(testString); final String resolvedPlaceholders = substitutor.replace(testString);
assertThat(resolvedPlaceholders).as("'%s' placeholder was not replaced", placeholder) assertThat(resolvedPlaceholders).as("'%s' placeholder was not replaced", placeholder).doesNotContain(placeholder);
.doesNotContain(placeholder);
} }
@Configuration @Configuration

View File

@@ -104,10 +104,6 @@
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId> <artifactId>spring-tx</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>

View File

@@ -65,10 +65,6 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>

View File

@@ -62,6 +62,7 @@
<rsql-parser.version>2.1.0</rsql-parser.version> <rsql-parser.version>2.1.0</rsql-parser.version>
<commons-io.version>2.16.1</commons-io.version> <commons-io.version>2.16.1</commons-io.version>
<commons-collections4.version>4.4</commons-collections4.version> <commons-collections4.version>4.4</commons-collections4.version>
<commons-text.version>1.13.0</commons-text.version>
<io-protostuff.version>1.8.0</io-protostuff.version> <io-protostuff.version>1.8.0</io-protostuff.version>
<!-- test --> <!-- test -->
<rabbitmq.http-client.version>5.3.0</rabbitmq.http-client.version> <rabbitmq.http-client.version>5.3.0</rabbitmq.http-client.version>
@@ -712,6 +713,11 @@
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version> <version>${commons-collections4.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<!-- Test --> <!-- Test -->
<dependency> <dependency>