From 1d690261843d15e6a43a1d8cbf390d38781997cd Mon Sep 17 00:00:00 2001 From: "Marcel Mager (INST-IOT/ESB)" Date: Mon, 29 Aug 2016 12:54:36 +0200 Subject: [PATCH] Overdue target filter based on makro resolution of placeholders Signed-off-by: Marcel Mager (INST-IOT/ESB) --- .../RepositoryApplicationConfiguration.java | 44 +++--- .../jpa/JpaTargetFilterQueryManagement.java | 6 +- .../repository/jpa/JpaTargetManagement.java | 26 +++- .../repository/jpa/rsql/RSQLUtility.java | 66 +++++++-- .../rsql/VirtualPropertyMakroResolver.java | 71 ++++++++++ .../repository/jpa/rsql/RSQLUtilityTest.java | 134 ++++++++++++++---- .../VirtualPropertyMakroResolverTest.java | 114 +++++++++++++++ 7 files changed, 397 insertions(+), 64 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolver.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolverTest.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java index a62de9a06..50a3d0b48 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java @@ -52,6 +52,7 @@ import org.eclipse.hawkbit.repository.jpa.model.helper.SystemManagementHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.TenantConfigurationManagementHolder; +import org.eclipse.hawkbit.repository.jpa.rsql.VirtualPropertyMakroResolver; import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -162,7 +163,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { } /** - * + * * @return the singleton instance of the * {@link AfterTransactionCommitExecutorHolder} */ @@ -225,7 +226,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaSystemManagement} bean. - * + * * @return a new {@link SystemManagement} */ @Bean @@ -236,7 +237,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaReportManagement} bean. - * + * * @return a new {@link ReportManagement} */ @Bean @@ -247,7 +248,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaDistributionSetManagement} bean. - * + * * @return a new {@link DistributionSetManagement} */ @Bean @@ -258,7 +259,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaTenantStatsManagement} bean. - * + * * @return a new {@link TenantStatsManagement} */ @Bean @@ -271,7 +272,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaTenantConfigurationManagement} bean. - * + * * @return a new {@link TenantConfigurationManagement} */ @Bean @@ -282,7 +283,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaTenantConfigurationManagement} bean. - * + * * @return a new {@link TenantConfigurationManagement} */ @Bean @@ -293,7 +294,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaTargetFilterQueryManagement} bean. - * + * * @return a new {@link TargetFilterQueryManagement} */ @Bean @@ -304,7 +305,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaTagManagement} bean. - * + * * @return a new {@link TagManagement} */ @Bean @@ -315,7 +316,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaSoftwareManagement} bean. - * + * * @return a new {@link SoftwareManagement} */ @Bean @@ -326,7 +327,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaRolloutManagement} bean. - * + * * @return a new {@link RolloutManagement} */ @Bean @@ -337,7 +338,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaRolloutGroupManagement} bean. - * + * * @return a new {@link RolloutGroupManagement} */ @Bean @@ -348,7 +349,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaDeploymentManagement} bean. - * + * * @return a new {@link DeploymentManagement} */ @Bean @@ -359,7 +360,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaControllerManagement} bean. - * + * * @return a new {@link ControllerManagement} */ @Bean @@ -370,7 +371,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaArtifactManagement} bean. - * + * * @return a new {@link ArtifactManagement} */ @@ -382,7 +383,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { /** * {@link JpaEntityFactory} bean. - * + * * @return a new {@link EntityFactory} */ @Bean @@ -390,4 +391,15 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { public EntityFactory entityFactory() { return new JpaEntityFactory(); } + + /** + * {@link VirtualPropertyMakroResolver} bean. + * + * @return a new {@link VirtualPropertyMakroResolver} + */ + @Bean + @ConditionalOnMissingBean + public VirtualPropertyMakroResolver virtualPropertyMakroResolver() { + return new VirtualPropertyMakroResolver(); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java index 6438e19f6..693f9395e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java @@ -16,6 +16,7 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.jpa.rsql.VirtualPropertyMakroResolver; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.specifications.TargetFilterQuerySpecification; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; @@ -44,6 +45,9 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme @Autowired private TargetFilterQueryRepository targetFilterQueryRepository; + @Autowired + private VirtualPropertyMakroResolver virtualPropMakroResolver; + @Override @Modifying @Transactional(isolation = Isolation.READ_UNCOMMITTED) @@ -111,7 +115,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme @Override public boolean verifyTargetFilterQuerySyntax(final String query) { - RSQLUtility.parse(query, TargetFields.class); + RSQLUtility.parse(query, TargetFields.class, virtualPropMakroResolver); return true; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index 225c0b4b2..b901ee6df 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -41,6 +41,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetInfo_; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.jpa.rsql.VirtualPropertyMakroResolver; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; import org.eclipse.hawkbit.repository.model.Target; @@ -102,6 +103,9 @@ public class JpaTargetManagement implements TargetManagement { @Autowired private AfterTransactionCommitExecutor afterCommit; + @Autowired + private VirtualPropertyMakroResolver virtualPropMakroResolver; + @Override public Target findTargetByControllerID(final String controllerId) { return targetRepository.findByControllerId(controllerId); @@ -152,12 +156,15 @@ public class JpaTargetManagement implements TargetManagement { @Override public Slice findTargetsAll(final TargetFilterQuery targetFilterQuery, final Pageable pageable) { - return findTargetsBySpec(RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class), pageable); + return findTargetsBySpec( + RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, virtualPropMakroResolver), + pageable); } @Override public Page findTargetsAll(final String targetFilterQuery, final Pageable pageable) { - return findTargetsBySpec(RSQLUtility.parse(targetFilterQuery, TargetFields.class), pageable); + return findTargetsBySpec(RSQLUtility.parse(targetFilterQuery, TargetFields.class, virtualPropMakroResolver), + pageable); } private Page findTargetsBySpec(final Specification spec, final Pageable pageable) { @@ -224,7 +231,8 @@ public class JpaTargetManagement implements TargetManagement { public Page findTargetByAssignedDistributionSet(final Long distributionSetID, final String rsqlParam, final Pageable pageReq) { - final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class); + final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class, + virtualPropMakroResolver); return convertPage( targetRepository @@ -252,7 +260,8 @@ public class JpaTargetManagement implements TargetManagement { public Page findTargetByInstalledDistributionSet(final Long distributionSetId, final String rsqlParam, final Pageable pageable) { - final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class); + final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class, + virtualPropMakroResolver); return convertPage( targetRepository @@ -544,7 +553,8 @@ public class JpaTargetManagement implements TargetManagement { final CriteriaQuery multiselect = query.multiselect(targetRoot.get(JpaTarget_.id), targetRoot.get(JpaTarget_.controllerId), targetRoot.get(JpaTarget_.name), targetRoot.get(sortProperty)); - final Specification spec = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class); + final Specification spec = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, + virtualPropMakroResolver); final List> specList = new ArrayList<>(); specList.add(spec); @@ -627,13 +637,15 @@ public class JpaTargetManagement implements TargetManagement { @Override public Long countTargetByTargetFilterQuery(final TargetFilterQuery targetFilterQuery) { - final Specification specs = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class); + final Specification specs = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, + virtualPropMakroResolver); return targetRepository.count(specs); } @Override public Long countTargetByTargetFilterQuery(final String targetFilterQuery) { - final Specification specs = RSQLUtility.parse(targetFilterQuery, TargetFields.class); + final Specification specs = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + virtualPropMakroResolver); return targetRepository.count(specs); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java index 90dd039f5..228091494 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java @@ -25,6 +25,8 @@ import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import org.apache.commons.lang3.text.StrLookup; +import org.apache.commons.lang3.text.StrSubstitutor; import org.eclipse.hawkbit.repository.FieldNameProvider; import org.eclipse.hawkbit.repository.FieldValueConverter; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; @@ -47,9 +49,8 @@ import cz.jirutka.rsql.parser.ast.RSQLOperators; import cz.jirutka.rsql.parser.ast.RSQLVisitor; /** - * A utility class which is able to parse RSQL strings into an spring data - * {@link Specification} which then can be enhanced sql queries to filter - * entities. RSQL parser library: https://github.com/jirutka/rsql-parser + * A utility class which is able to parse RSQL strings into an spring data {@link Specification} which then can be + * enhanced sql queries to filter entities. RSQL parser library: https://github.com/jirutka/rsql-parser * *
    *
  • Equal to : ==
  • @@ -59,16 +60,24 @@ import cz.jirutka.rsql.parser.ast.RSQLVisitor; *
  • Greater than operator : =gt= or >
  • *
  • Greater than or equal to : =ge= or >=
  • *
+ *

* Examples of RSQL expressions in both FIQL-like and alternative notation: *

    *
  • version==2.0.0
  • *
  • name==targetId1;description==plugAndPlay
  • *
  • name==targetId1 and description==plugAndPlay
  • - *
  • name==targetId1;description==plugAndPlay
  • - *
  • name==targetId1 and description==plugAndPlay
  • *
  • name==targetId1,description==plugAndPlay,updateStatus==UNKNOWN
  • *
  • name==targetId1 or description==plugAndPlay or updateStatus==UNKNOWN
  • *
+ *

+ * There is also a mechanism that allows to refer to known makros that can resolved by an optional {@link StrLookup} + * (cp. {@link VirtualPropertyMakroResolver}).
+ * An example that queries for all overdue targets using the ${OVERDUE_TS} placeholder introduced by + * {@link VirtualPropertyMakroResolver} looks like this:
+ * lastControllerRequestAt=le=${OVERDUE_TS}
+ * It is possible to escape a makro expression by using a second '$': $${OVERDUE_TS} would prevent the ${OVERDUE_TS} + * token from being expanded. + * */ public final class RSQLUtility { @@ -99,7 +108,28 @@ public final class RSQLUtility { */ public static & FieldNameProvider, T> Specification parse(final String rsql, final Class fieldNameProvider) { - return new RSQLSpecification<>(rsql.toLowerCase(), fieldNameProvider); + return new RSQLSpecification<>(rsql.toLowerCase(), fieldNameProvider, new VirtualPropertyMakroResolver()); + } + + /** + * parses an RSQL valid string into an JPA {@link Specification} which then can be used to filter for JPA entities + * with the given RSQL query. + * + * @param rsql + * the rsql query + * @param fieldNameProvider + * the enum class type which implements the {@link FieldNameProvider} + * @param makroLookup + * holds the logic how the known makros have to be resolved; may be null + * @return an specification which can be used with JPA + * @throws RSQLParameterUnsupportedFieldException + * if a field in the RSQL string is used but not provided by the given {@code fieldNameProvider} + * @throws RSQLParameterSyntaxException + * if the RSQL syntax is wrong + */ + public static & FieldNameProvider, T> Specification parse(final String rsql, + final Class fieldNameProvider, StrLookup makroLookup) { + return new RSQLSpecification<>(rsql.toLowerCase(), fieldNameProvider, makroLookup); } /** @@ -130,10 +160,12 @@ public final class RSQLUtility { private final String rsql; private final Class enumType; + private final StrLookup makroLookup; - private RSQLSpecification(final String rsql, final Class enumType) { + private RSQLSpecification(final String rsql, final Class enumType, StrLookup makroLookup) { this.rsql = rsql; this.enumType = enumType; + this.makroLookup = makroLookup; } @Override @@ -141,7 +173,8 @@ public final class RSQLUtility { final Node rootNode = parseRsql(rsql); - final JpqQueryRSQLVisitor jpqQueryRSQLVisitor = new JpqQueryRSQLVisitor<>(root, cb, enumType); + final JpqQueryRSQLVisitor jpqQueryRSQLVisitor = new JpqQueryRSQLVisitor<>(root, cb, enumType, + makroLookup); final List accept = rootNode., String> accept(jpqQueryRSQLVisitor); if (accept != null && !accept.isEmpty()) { @@ -171,13 +204,19 @@ public final class RSQLUtility { private final Root root; private final CriteriaBuilder cb; private final Class enumType; + private final StrLookup makroLookup; + private final StrSubstitutor substitutor; private final SimpleTypeConverter simpleTypeConverter; - private JpqQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType) { + private JpqQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType, + StrLookup makroLookup) { this.root = root; this.cb = cb; this.enumType = enumType; + this.makroLookup = makroLookup; + this.substitutor = new StrSubstitutor(makroLookup, StrSubstitutor.DEFAULT_PREFIX, + StrSubstitutor.DEFAULT_SUFFIX, StrSubstitutor.DEFAULT_ESCAPE); simpleTypeConverter = new SimpleTypeConverter(); } @@ -425,7 +464,14 @@ public final class RSQLUtility { // enums. The JPA API // cannot handle object types for greaterThan etc methods. final Object transformedValue = transformedValues.get(0); - final String value = values.get(0); + + final String value; + if (makroLookup != null) { // if substitutor is available, replace makros ... + value = substitutor.replace(values.get(0)); + } else { + value = values.get(0); + } + final List singleList = new ArrayList<>(); final Predicate mapPredicate = mapToMapPredicate(node, fieldPath, enumField); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolver.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolver.java new file mode 100644 index 000000000..8425e4a11 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolver.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2016 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.rsql; + +import java.time.Duration; +import java.time.Instant; + +import org.apache.commons.lang3.text.StrLookup; +import org.apache.commons.lang3.text.StrSubstitutor; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.jpa.model.helper.TenantConfigurationManagementHolder; +import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; + +/** + * Adds makro capabilities to RSQL expressions that are used to filter for targets. + *

+ * Some (virtual) properties do not have a representation in the database (in general these properties are time-related, + * or more explicitly, they deal with time intervals).
+ * Such a virtual property needs to be calculated on Java-side before it may be used in a target filter query that is + * passed to the database. Therefore a placeholder is used in the RSQL expression that is expanded in the + * {@link RSQLUtility} by a {@link StrSubstitutor}. This {@link StrSubstitutor} is configured with an instance of + * {@link VirtualPropertyMakroResolver} to resolve the known makros. + *

+ * A virtual property may either be a system value like the current date (aka now_ts) or a value derived from + * (tenant-specific) system configuration (e.g. overdue_ts). + *

+ * Known values are:
+ *

+ * + */ +public class VirtualPropertyMakroResolver extends StrLookup { + + @Override + public String lookup(String rhs) { + String resolved = null; + + if ("now_ts".equals(rhs.toLowerCase())) { + resolved = String.valueOf(Instant.now().toEpochMilli()); + } else if ("overdue_ts".equals(rhs.toLowerCase())) { + resolved = String.valueOf(Instant.now().toEpochMilli() // + - getDurationForKey(TenantConfigurationKey.POLLING_TIME_INTERVAL).toMillis() // + - getDurationForKey(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL).toMillis()); + } + return resolved; + } + + private Duration getDurationForKey(TenantConfigurationKey key) { + return DurationHelper.formattedStringToDuration(getRawStringForKey(key)); + } + + private String getRawStringForKey(TenantConfigurationKey key) { + return getTenantConfigurationManagement().getConfigurationValue(key, String.class).getValue(); + } + + TenantConfigurationManagement getTenantConfigurationManagement() { + return TenantConfigurationManagementHolder.getInstance().getTenantConfigurationManagement(); + } + + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java index 04901d948..e7b5ba592 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java @@ -9,14 +9,8 @@ package org.eclipse.hawkbit.repository.jpa.rsql; import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; @@ -26,18 +20,24 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.persistence.metamodel.Attribute; +import org.apache.commons.lang3.text.StrLookup; import org.eclipse.hawkbit.repository.DistributionSetFields; import org.eclipse.hawkbit.repository.FieldNameProvider; import org.eclipse.hawkbit.repository.SoftwareModuleFields; import org.eclipse.hawkbit.repository.TargetFields; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.runners.MockitoJUnitRunner; +import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Stories; @@ -48,6 +48,12 @@ import ru.yandex.qatools.allure.annotations.Stories; // method name as short text public class RSQLUtilityTest { + @Spy + VirtualPropertyMakroResolver makroResolver = new VirtualPropertyMakroResolver(); + + @Mock + TenantConfigurationManagement confMgmt; + @Mock private Root baseSoftwareModuleRootMock; @@ -59,11 +65,16 @@ public class RSQLUtilityTest { @Mock private Attribute attribute; + private static final TenantConfigurationValue TEST_POLLING_TIME_INTERVAL = TenantConfigurationValue + .builder().value("00:05:00").build(); + private static final TenantConfigurationValue TEST_POLLING_OVERDUE_TIME_INTERVAL = TenantConfigurationValue + .builder().value("00:07:37").build(); + @Test public void wrongRsqlSyntaxThrowSyntaxException() { final String wrongRSQL = "name==abc;d"; try { - RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterSyntaxException e) { @@ -75,7 +86,7 @@ public class RSQLUtilityTest { final String wrongRSQL = "unknownField==abc"; when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); try { - RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing an expected RSQLParameterUnsupportedFieldException because of unknown RSQL field"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -87,7 +98,8 @@ public class RSQLUtilityTest { public void wrongRsqlMapSyntaxThrowSyntaxException() { String wrongRSQL = TargetFields.ATTRIBUTE + "==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, + RSQLUtility.parse(wrongRSQL, TargetFields.class, null).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -95,7 +107,8 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.ATTRIBUTE + ".unkwon.wrong==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, + RSQLUtility.parse(wrongRSQL, TargetFields.class, null).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -103,7 +116,7 @@ public class RSQLUtilityTest { wrongRSQL = DistributionSetFields.METADATA + "==abc"; try { - RSQLUtility.parse(wrongRSQL, DistributionSetFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(wrongRSQL, DistributionSetFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -115,7 +128,8 @@ public class RSQLUtilityTest { public void wrongRsqlSubEntitySyntaxThrowSyntaxException() { String wrongRSQL = TargetFields.ASSIGNEDDS + "==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, + RSQLUtility.parse(wrongRSQL, TargetFields.class, null).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -123,7 +137,8 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, + RSQLUtility.parse(wrongRSQL, TargetFields.class, null).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -131,7 +146,8 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField.ToMuch==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, + RSQLUtility.parse(wrongRSQL, TargetFields.class, null).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -146,11 +162,11 @@ public class RSQLUtilityTest { when(baseSoftwareModuleRootMock.get("version")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); - when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))) + when(criteriaBuilderMock.greaterThanOrEqualTo(any(Expression.class), any(String.class))) .thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verfication @@ -164,12 +180,12 @@ public class RSQLUtilityTest { when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); - when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))) + when(criteriaBuilderMock.greaterThanOrEqualTo(any(Expression.class), any(String.class))) .thenReturn(mock(Predicate.class)); when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock)))) .thenReturn(pathOfString(baseSoftwareModuleRootMock)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verfication @@ -185,12 +201,12 @@ public class RSQLUtilityTest { when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); - when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))) + when(criteriaBuilderMock.greaterThanOrEqualTo(any(Expression.class), any(String.class))) .thenReturn(mock(Predicate.class)); when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock)))) .thenReturn(pathOfString(baseSoftwareModuleRootMock)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verfication @@ -206,10 +222,10 @@ public class RSQLUtilityTest { when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); - when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))) + when(criteriaBuilderMock.greaterThanOrEqualTo(any(Expression.class), any(String.class))) .thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verfication @@ -226,7 +242,8 @@ public class RSQLUtilityTest { when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, TestFieldEnum.class).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, + RSQLUtility.parse(correctRsql, TestFieldEnum.class, null).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); // verfication @@ -244,7 +261,7 @@ public class RSQLUtilityTest { try { // test - RSQLUtility.parse(correctRsql, TestFieldEnum.class).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.parse(correctRsql, TestFieldEnum.class, null).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("missing RSQLParameterUnsupportedFieldException for wrong enum value"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -252,6 +269,65 @@ public class RSQLUtilityTest { } } + @Test + @Description("Tests the resolution of overdue_ts placeholder in context of a RSQL expression.") + public void correctRsqlWithOverdueMakro() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String overdueProp = "overdue_ts"; + final String overduePropPlaceholder = "${" + overdueProp + "}"; + final String correctRsql = "testfield=le=" + overduePropPlaceholder; + when(baseSoftwareModuleRootMock.get("testfield")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) String.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + when(criteriaBuilderMock.lessThanOrEqualTo(any(Expression.class), eq(overduePropPlaceholder))) + .thenReturn(mock(Predicate.class)); + + // test + Predicate result = RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMakroLookup()) + .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(makroResolver, times(1)).lookup(overdueProp); + // the makro is already replaced when passed to #lessThanOrEqualTo -> the method is never invoked with the + // placeholder: + verify(criteriaBuilderMock, never()).lessThanOrEqualTo(eq(pathOfString(baseSoftwareModuleRootMock)), + eq(overduePropPlaceholder)); + } + + @Test + @Description("Tests RSQL expression with an unknown placeholder.") + public void correctRsqlWithUnknownMakro() { + reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + final String overdueProp = "unknown"; + final String overduePropPlaceholder = "${" + overdueProp + "}"; + final String correctRsql = "testfield=le=" + overduePropPlaceholder; + when(baseSoftwareModuleRootMock.get("testfield")).thenReturn(baseSoftwareModuleRootMock); + when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) String.class); + when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class)); + when(criteriaBuilderMock.lessThanOrEqualTo(any(Expression.class), eq(overduePropPlaceholder))) + .thenReturn(mock(Predicate.class)); + + // test + Predicate result = RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMakroLookup()) + .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); + + // verfication + verify(makroResolver, times(1)).lookup(overdueProp); + // the makro is unknown and hence never replaced -> #lessThanOrEqualTo is invoked with the placeholder: + verify(criteriaBuilderMock, times(1)).lessThanOrEqualTo(eq(pathOfString(baseSoftwareModuleRootMock)), + eq(overduePropPlaceholder)); + } + + public StrLookup setupMakroLookup() { + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class)) + .thenReturn(TEST_POLLING_TIME_INTERVAL); + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class)) + .thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL); + when(makroResolver.getTenantConfigurationManagement()).thenReturn(confMgmt); + + return makroResolver; + } + @SuppressWarnings("unchecked") private Path pathOfString(final Path path) { return (Path) path; @@ -262,10 +338,8 @@ public class RSQLUtilityTest { /* * (non-Javadoc) - * - * @see - * org.eclipse.hawkbit.server.rest.resource.model.FieldNameProvider# - * getFieldName() + * + * @see org.eclipse.hawkbit.server.rest.resource.model.FieldNameProvider# getFieldName() */ @Override public String getFieldName() { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolverTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolverTest.java new file mode 100644 index 000000000..f18783d5f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyMakroResolverTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2016 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.rsql; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +import java.time.Instant; + +import org.apache.commons.lang3.text.StrSubstitutor; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationKey; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@Features("Unit Tests - Repository") +@Stories("Placeholder resolution for virtual properties") +@RunWith(MockitoJUnitRunner.class) +public class VirtualPropertyMakroResolverTest { + + @Spy + VirtualPropertyMakroResolver resolverUnderTest = new VirtualPropertyMakroResolver(); + + @Mock + TenantConfigurationManagement confMgmt; + + StrSubstitutor substitutor; + + Long nowTestTime; + + private static final TenantConfigurationValue TEST_POLLING_TIME_INTERVAL = TenantConfigurationValue + . builder().value("00:05:00").build(); + private static final TenantConfigurationValue TEST_POLLING_OVERDUE_TIME_INTERVAL = TenantConfigurationValue + . builder().value("00:07:37").build(); + + @Before + public void before() { + nowTestTime = Instant.now().toEpochMilli(); + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class)) + .thenReturn(TEST_POLLING_TIME_INTERVAL); + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class)) + .thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL); + when(resolverUnderTest.getTenantConfigurationManagement()).thenReturn(confMgmt); + + this.substitutor = new StrSubstitutor(resolverUnderTest, StrSubstitutor.DEFAULT_PREFIX, + StrSubstitutor.DEFAULT_SUFFIX, StrSubstitutor.DEFAULT_ESCAPE); + } + + @Test + @Description("Tests resolution of NOW_TS by using a StrSubstitutor configured with the VirtualPropertyMakroResolver.") + public void resolveNowTimestampPlaceholder() { + String placeholder = "${NOW_TS}"; + String testString = "lhs=lt=" + placeholder; + + String resolvedPlaceholders = substitutor.replace(testString); + assertFalse(resolvedPlaceholders.contains(placeholder)); + } + + @Test + @Description("Tests resolution of OVERDUE_TS by using a StrSubstitutor configured with the VirtualPropertyMakroResolver.") + public void resolveOverdueTimestampPlaceholder() { + String placeholder = "${OVERDUE_TS}"; + String testString = "lhs=lt=" + placeholder; + + String resolvedPlaceholders = substitutor.replace(testString); + assertFalse(resolvedPlaceholders.contains(placeholder)); + } + + @Test + @Description("Tests case insensititity of VirtualPropertyMakroResolver.") + public void resolveOverdueTimestampPlaceholderLowerCase() { + String placeholder = "${overdue_ts}"; + String testString = "lhs=lt=" + placeholder; + + String resolvedPlaceholders = substitutor.replace(testString); + assertFalse(resolvedPlaceholders.contains(placeholder)); + } + + @Test + @Description("Tests VirtualPropertyMakroResolver with a placeholder unknown to VirtualPropertyMakroResolver.") + public void handleUnknownPlaceholder() { + String placeholder = "${unknown}"; + String testString = "lhs=lt=" + placeholder; + + String resolvedPlaceholders = substitutor.replace(testString); + assertTrue(resolvedPlaceholders.contains(placeholder)); + } + + @Test + @Description("Tests escape mechanism for placeholders (syntax is $${SOME_PLACEHOLDER}).") + public void handleEscapedPlaceholder() { + String placeholder = "${OVERDUE_TS}"; + String escaptedPlaceholder = StrSubstitutor.DEFAULT_ESCAPE + placeholder; + String testString = "lhs=lt=" + escaptedPlaceholder; + + String resolvedPlaceholders = substitutor.replace(testString); + assertTrue(resolvedPlaceholders.contains(placeholder)); + } +}