diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java index 75d594e48..53f1f42cd 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.repository; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Map.Entry; +import java.util.Optional; + /** * Describing the fields of the DistributionSet model which can be used in the * REST API e.g. for sorting etc. @@ -59,35 +63,39 @@ public enum DistributionSetFields implements FieldNameProvider { /** * The metadata. */ - METADATA("metadata", "key", "value"); + METADATA("metadata", new SimpleImmutableEntry<>("key", "value")); private final String fieldName; - private String keyFieldName; - private String valueFieldName; + private boolean mapField; + private Entry subEntityMapTuple; private DistributionSetFields(final String fieldName) { - this(fieldName, null, null); + this(fieldName, false, null); } - private DistributionSetFields(final String fieldName, final String keyFieldName, final String valueFieldName) { + private DistributionSetFields(final String fieldName, final Entry subEntityMapTuple) { + this(fieldName, true, subEntityMapTuple); + } + + private DistributionSetFields(final String fieldName, final boolean mapField, + final Entry subEntityMapTuple) { this.fieldName = fieldName; - this.keyFieldName = keyFieldName; - this.valueFieldName = valueFieldName; + this.mapField = mapField; + this.subEntityMapTuple = subEntityMapTuple; } @Override - public String getValueFieldName() { - return valueFieldName; + public Optional> getSubEntityMapTuple() { + return Optional.ofNullable(subEntityMapTuple); } @Override - public String getKeyFieldName() { - return keyFieldName; + public boolean isMap() { + return mapField; } @Override public String getFieldName() { return fieldName; } - } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java index d8fd4281c..72b3fb452 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/FieldNameProvider.java @@ -11,6 +11,8 @@ package org.eclipse.hawkbit.repository; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; /** * An interface for declaring the name of the field described in the database @@ -65,21 +67,10 @@ public interface FieldNameProvider { } /** - * The database column for the key - * - * @return key fieldname + * @return a key/value tuple of a sub entity. */ - default String getKeyFieldName() { - return null; - } - - /** - * The database column for the value - * - * @return key fieldname - */ - default String getValueFieldName() { - return null; + default Optional> getSubEntityMapTuple() { + return Optional.empty(); } /** @@ -88,6 +79,6 @@ public interface FieldNameProvider { * @return is a map is not a map */ default boolean isMap() { - return getKeyFieldName() != null; + return false; } } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java index bff9b1f74..0587a0349 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleFields.java @@ -8,6 +8,10 @@ */ package org.eclipse.hawkbit.repository; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Map.Entry; +import java.util.Optional; + /** * Describing the fields of the SoftwareModule model which can be used in the * REST API e.g. for sorting etc. @@ -40,34 +44,39 @@ public enum SoftwareModuleFields implements FieldNameProvider { /** * The metadata. */ - METADATA("metadata", "key", "value"); + METADATA("metadata", new SimpleImmutableEntry<>("key", "value")); private final String fieldName; - private String keyFieldName; - private String valueFieldName; + private boolean mapField; + private Entry subEntityMapTuple; private SoftwareModuleFields(final String fieldName) { - this(fieldName, null, null); + this(fieldName, false, null); } - private SoftwareModuleFields(final String fieldName, final String keyFieldName, final String valueFieldName) { + private SoftwareModuleFields(final String fieldName, final Entry subEntityMapTuple) { + this(fieldName, true, subEntityMapTuple); + } + + private SoftwareModuleFields(final String fieldName, final boolean mapField, + final Entry subEntityMapTuple) { this.fieldName = fieldName; - this.keyFieldName = keyFieldName; - this.valueFieldName = valueFieldName; + this.mapField = mapField; + this.subEntityMapTuple = subEntityMapTuple; + } + + @Override + public Optional> getSubEntityMapTuple() { + return Optional.ofNullable(subEntityMapTuple); + } + + @Override + public boolean isMap() { + return mapField; } @Override public String getFieldName() { return fieldName; } - - @Override - public String getKeyFieldName() { - return keyFieldName; - } - - @Override - public String getValueFieldName() { - return valueFieldName; - } } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java index 78a2f3b95..0cb82f3be 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java @@ -8,9 +8,12 @@ */ package org.eclipse.hawkbit.repository; +import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; /** * Describing the fields of the Target model which can be used in the REST API @@ -77,28 +80,40 @@ public enum TargetFields implements FieldNameProvider { /** * Last time the DDI or DMF client polled. */ - LASTCONTROLLERREQUESTAT("lastTargetQuery"); + LASTCONTROLLERREQUESTAT("lastTargetQuery"), + + /** + * The metadata. + */ + METADATA("metadata", new SimpleImmutableEntry<>("key", "value")); private final String fieldName; private List subEntityAttribues; private boolean mapField; + private Entry subEntityMapTuple; - TargetFields(final String fieldName) { - this(fieldName, false, Collections.emptyList()); + private TargetFields(final String fieldName) { + this(fieldName, false, Collections.emptyList(), null); } - TargetFields(final String fieldName, final boolean isMapField) { - this(fieldName, isMapField, Collections.emptyList()); + private TargetFields(final String fieldName, final boolean isMapField) { + this(fieldName, isMapField, Collections.emptyList(), null); } - TargetFields(final String fieldName, final String... subEntityAttribues) { - this(fieldName, false, Arrays.asList(subEntityAttribues)); + private TargetFields(final String fieldName, final String... subEntityAttribues) { + this(fieldName, false, Arrays.asList(subEntityAttribues), null); } - TargetFields(final String fieldName, final boolean mapField, final List subEntityAttribues) { + private TargetFields(final String fieldName, final Entry subEntityMapTuple) { + this(fieldName, true, Collections.emptyList(), subEntityMapTuple); + } + + private TargetFields(final String fieldName, final boolean mapField, final List subEntityAttribues, + final Entry subEntityMapTuple) { this.fieldName = fieldName; this.mapField = mapField; this.subEntityAttribues = subEntityAttribues; + this.subEntityMapTuple = subEntityMapTuple; } @Override @@ -106,6 +121,11 @@ public enum TargetFields implements FieldNameProvider { return subEntityAttribues; } + @Override + public Optional> getSubEntityMapTuple() { + return Optional.ofNullable(subEntityMapTuple); + } + @Override public boolean isMap() { return mapField; diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryFields.java index e41cf1893..94c02fd83 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryFields.java @@ -36,34 +36,27 @@ public enum TargetFilterQueryFields implements FieldNameProvider { private final String fieldName; private List subEntityAttributes; - private boolean mapField; - TargetFilterQueryFields(final String fieldName) { - this(fieldName, false, Collections.emptyList()); + private TargetFilterQueryFields(final String fieldName) { + this(fieldName, Collections.emptyList()); } - TargetFilterQueryFields(final String fieldName, final String... subEntityAttribues) { - this(fieldName, false, Arrays.asList(subEntityAttribues)); + private TargetFilterQueryFields(final String fieldName, final String... subEntityAttribues) { + this(fieldName, Arrays.asList(subEntityAttribues)); } - TargetFilterQueryFields(final String fieldName, final boolean mapField, final List subEntityAttribues) { + private TargetFilterQueryFields(final String fieldName, final List subEntityAttribues) { this.fieldName = fieldName; - this.mapField = mapField; this.subEntityAttributes = subEntityAttribues; } - @Override - public List getSubEntityAttributes() { - return subEntityAttributes; - } - - @Override - public boolean isMap() { - return mapField; - } - @Override public String getFieldName() { return fieldName; } + + @Override + public List getSubEntityAttributes() { + return subEntityAttributes; + } } diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java index c0a51a828..d85edbeec 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetMetadataFields.java @@ -25,7 +25,7 @@ public enum TargetMetadataFields implements FieldNameProvider { private final String fieldName; - TargetMetadataFields(final String fieldName) { + private TargetMetadataFields(final String fieldName) { this.fieldName = fieldName; } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java index 756677558..226ccaecf 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java @@ -33,6 +33,17 @@ public class RSQLParameterSyntaxException extends AbstractServerRtException { super(SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX); } + /** + * Creates a new RSQLParameterSyntaxException with + * {@link SpServerError#SP_REST_RSQL_SEARCH_PARAM_SYNTAX} error. + * + * @param message + * the message of the exception + */ + public RSQLParameterSyntaxException(final String message) { + super(message, SpServerError.SP_REST_RSQL_SEARCH_PARAM_SYNTAX); + } + /** * Creates a new RSQLSyntaxException with * {@link SpServerError#SP_REST_RSQL_SEARCH_PARAM_SYNTAX} error. 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 ebef6756b..0831dc2f6 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 @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -287,7 +288,7 @@ public final class RSQLUtility { final String[] graph = node.getSelector().split("\\" + FieldNameProvider.SUB_ATTRIBUTE_SEPERATOR); - validateMapParamter(propertyEnum, node, graph); + validateMapParameter(propertyEnum, node, graph); // sub entity need minium 1 dot if (!propertyEnum.getSubEntityAttributes().isEmpty() && graph.length < 2) { @@ -314,13 +315,15 @@ public final class RSQLUtility { return fieldNameBuilder.toString(); } - private void validateMapParamter(final A propertyEnum, final ComparisonNode node, final String[] graph) { + private void validateMapParameter(final A propertyEnum, final ComparisonNode node, final String[] graph) { if (!propertyEnum.isMap()) { return; } + if (!propertyEnum.getSubEntityAttributes().isEmpty()) { - throw new UnsupportedOperationException("Currently subentity attributes for maps are not supported"); + throw new UnsupportedOperationException( + "Currently subentity attributes for maps are not supported, alternatively you could use the key/value tuple, defined by SimpleImmutableEntry class"); } // enum.key @@ -540,48 +543,37 @@ public final class RSQLUtility { value = virtualPropertyReplacer.replace(value); } - final List singleList = new ArrayList<>(); - final Predicate mapPredicate = mapToMapPredicate(node, fieldPath, enumField); - if (mapPredicate != null) { - singleList.add(mapPredicate); - } - addOperatorPredicate(node, getMapValueFieldPath(enumField, fieldPath), transformedValues, transformedValue, - value, singleList, database); - return Collections.unmodifiableList(singleList); + final Predicate valuePredicate = addOperatorPredicate(node, getMapValueFieldPath(enumField, fieldPath), + transformedValues, transformedValue, value, database); + + return toSingleList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate); } - private void addOperatorPredicate(final ComparisonNode node, final Path fieldPath, + private Predicate addOperatorPredicate(final ComparisonNode node, final Path fieldPath, final List transformedValues, final Object transformedValue, final String value, - final List singleList, final Database database) { + final Database database) { switch (node.getOperator().getSymbol()) { case "==": - singleList.add(getEqualToPredicate(transformedValue, fieldPath, database)); - break; + return getEqualToPredicate(transformedValue, fieldPath, database); case "!=": - singleList.add(getNotEqualToPredicate(transformedValue, fieldPath, database)); - break; + return getNotEqualToPredicate(transformedValue, fieldPath, database); case "=gt=": - singleList.add(cb.greaterThan(pathOfString(fieldPath), value)); - break; + return cb.greaterThan(pathOfString(fieldPath), value); case "=ge=": - singleList.add(cb.greaterThanOrEqualTo(pathOfString(fieldPath), value)); - break; + return cb.greaterThanOrEqualTo(pathOfString(fieldPath), value); case "=lt=": - singleList.add(cb.lessThan(pathOfString(fieldPath), value)); - break; + return cb.lessThan(pathOfString(fieldPath), value); case "=le=": - singleList.add(cb.lessThanOrEqualTo(pathOfString(fieldPath), value)); - break; + return cb.lessThanOrEqualTo(pathOfString(fieldPath), value); case "=in=": - singleList.add(getInPredicate(transformedValues, fieldPath)); - break; + return getInPredicate(transformedValues, fieldPath); case "=out=": - singleList.add(getOutPredicate(transformedValues, fieldPath)); - break; + return getOutPredicate(transformedValues, fieldPath); default: - LOGGER.info("operator symbol {} is either not supported or not implemented"); + throw new RSQLParameterSyntaxException("operator symbol {" + node.getOperator().getSymbol() + + "} is either not supported or not implemented"); } } @@ -616,10 +608,13 @@ public final class RSQLUtility { } private Path getMapValueFieldPath(final A enumField, final Path fieldPath) { - if (!enumField.isMap() || enumField.getValueFieldName() == null) { + final String valueFieldNameFromSubEntity = enumField.getSubEntityMapTuple().map(Entry::getValue) + .orElse(null); + + if (!enumField.isMap() || valueFieldNameFromSubEntity == null) { return fieldPath; } - return fieldPath.get(enumField.getValueFieldName()); + return fieldPath.get(valueFieldNameFromSubEntity); } @SuppressWarnings("unchecked") @@ -636,7 +631,11 @@ public final class RSQLUtility { keyValue.toUpperCase()); } - return cb.equal(cb.upper(fieldPath.get(enumField.getKeyFieldName())), keyValue.toUpperCase()); + final String keyFieldName = enumField.getSubEntityMapTuple().map(Entry::getKey) + .orElseThrow(() -> new UnsupportedOperationException( + "For the fields, defined as Map, only Map java type or tuple in the form of SimpleImmutableEntry are allowed. Neither of those could be found!")); + + return cb.equal(cb.upper(fieldPath.get(keyFieldName)), keyValue.toUpperCase()); } private Predicate getEqualToPredicate(final Object transformedValue, final Path fieldPath, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java index b270a2f3a..3642a20b5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.TargetFields; @@ -78,7 +80,7 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { context.setSyntaxError(false); suggestionContext.getSuggestions().addAll(getLogicalOperatorSuggestion(rsqlQuery)); } catch (final RSQLParameterSyntaxException | RSQLParserException ex) { - setExceptionDetails(new Exception(ex.getCause().getCause()), expectedTokens); + setExceptionDetails(rsqlQuery, new Exception(ex.getCause().getCause()), expectedTokens); errorContext.setErrorMessage(getCustomMessage(ex.getCause().getMessage(), expectedTokens)); suggestionContext.setSuggestions(expectedTokens); LOGGER.trace("Syntax exception on parsing :", ex); @@ -94,8 +96,7 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { private static Collection getLogicalOperatorSuggestion(final String rsqlQuery) { if (!rsqlQuery.endsWith(" ")) { return Collections.emptyList(); - } - if (rsqlQuery.endsWith(" ")) { + } else { final int currentQueryLength = rsqlQuery.length(); // only return and/or suggestion when there is a space at the end final Collection tokenImages = TokenDescription.getTokenImage(TokenDescription.LOGICAL_OP); @@ -106,18 +107,19 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { } return logicalOps; } - return Collections.emptyList(); } - private static void setExceptionDetails(final Exception ex, final List expectedTokens) { - expectedTokens.addAll(getNextTokens(ex)); - } - - private static List getNextTokens(final Exception e) { - final ParseException parseException = findParseException(e); + private static void setExceptionDetails(final String rsqlQuery, final Exception ex, + final List expectedTokens) { + final ParseException parseException = findParseException(ex); if (parseException == null) { - return Collections.emptyList(); + expectedTokens.addAll(getComparatorOperatorSuggestions(rsqlQuery)); + } else { + expectedTokens.addAll(getNextTokens(parseException)); } + } + + private static List getNextTokens(final ParseException parseException) { final List listTokens = new ArrayList<>(); final ParseExceptionWrapper parseExceptionWrapper = new ParseExceptionWrapper(parseException); final int[][] expectedTokenSequence = parseExceptionWrapper.getExpectedTokenSequence(); @@ -146,6 +148,22 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { return listTokens; } + private static List getComparatorOperatorSuggestions(final String rsqlQuery) { + // only return comparator operators suggestions when there is a '=' or + // '!' symbol at the end + final String mapKeyOperatorPattern = "(\\w+)\\.\\w+[=!]{1}$"; + final Matcher mapKeyOperatorMatcher = Pattern.compile(mapKeyOperatorPattern).matcher(rsqlQuery); + + if (mapKeyOperatorMatcher.find() && FieldNameDescription.isMap(mapKeyOperatorMatcher.group(1))) { + final int currentQueryLength = rsqlQuery.length() - 1; + final Collection tokenImages = TokenDescription.getTokenImage(TokenDescription.COMPARATOR); + return tokenImages.stream().map(tokenImage -> new SuggestToken(currentQueryLength, + currentQueryLength + tokenImage.length(), null, tokenImage)).collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + private static void addSuggestionOnTokenImage(final List listTokens, final int nextTokenBeginColumn, final int currentTokenEndColumn, final int[] is) { for (final int i : is) { @@ -179,7 +197,8 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { } private static boolean shouldSuggestDotToken(final String currentTokenImageName, final boolean containsDot) { - return !containsDot && FieldNameDescription.hasSubEntries(currentTokenImageName); + return !containsDot && (FieldNameDescription.hasSubEntries(currentTokenImageName) + || FieldNameDescription.isMap(currentTokenImageName)); } private static boolean shouldSuggestTopLevelFieldNames(final String currentTokenImageName, @@ -295,6 +314,12 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { .map(TargetFields::getSubEntityAttributes).flatMap(List::stream).count() > 0; } + private static boolean isMap(final String tokenImageName) { + return Arrays.stream(TargetFields.values()) + .filter(field -> field.toString().equalsIgnoreCase(tokenImageName)).findFirst() + .map(TargetFields::isMap).orElse(false); + } + private static List toTopSuggestToken(final int beginToken, final int endToken, final String tokenImageName) { return FIELD_NAMES.stream() diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java index a30f979a6..4be543826 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java @@ -149,7 +149,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { "TargetMetadata"); verifyThrownExceptionBy(() -> targetManagement.getMetaDataByControllerId(NOT_EXIST_ID, "xxx"), "Target"); verifyThrownExceptionBy(() -> targetManagement.findMetaDataByControllerId(PAGE, NOT_EXIST_ID), "Target"); - verifyThrownExceptionBy(() -> targetManagement.findMetaDataByControllerIdAndRsql(PAGE, NOT_EXIST_ID, "name==*"), + verifyThrownExceptionBy(() -> targetManagement.findMetaDataByControllerIdAndRsql(PAGE, NOT_EXIST_ID, "key==*"), "Target"); verifyThrownExceptionBy( () -> targetManagement.updateMetadata(NOT_EXIST_ID, entityFactory.generateTargetMetadata("xxx", "xxx")), @@ -921,7 +921,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { final String knownValue = "targetMetaKnownValue"; final Target target = testdataFactory.createTarget("targetIdWithMetadata"); - final JpaTargetMetadata createdMetadata = insertTargetMetadata(knownKey, target, knownValue); + final JpaTargetMetadata createdMetadata = insertTargetMetadata(knownKey, knownValue, target); assertThat(createdMetadata).isNotNull(); assertThat(createdMetadata.getId().getKey()).isEqualTo(knownKey); @@ -930,8 +930,8 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { assertThat(createdMetadata.getValue()).isEqualTo(knownValue); } - private JpaTargetMetadata insertTargetMetadata(final String knownKey, final Target target, - final String knownValue) { + private JpaTargetMetadata insertTargetMetadata(final String knownKey, final String knownValue, + final Target target) { final JpaTargetMetadata metadata = new JpaTargetMetadata(knownKey, knownValue, target); return (JpaTargetMetadata) targetManagement .createMetaData(target.getControllerId(), Collections.singletonList(metadata)).get(0); @@ -945,12 +945,12 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { final Target target1 = testdataFactory.createTarget("target1"); final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerTarget(); for (int i = 0; i < maxMetaData; ++i) { - assertThat(insertTargetMetadata("k" + i, target1, "v" + i)).isNotNull(); + assertThat(insertTargetMetadata("k" + i, "v" + i, target1)).isNotNull(); } // quota exceeded assertThatExceptionOfType(QuotaExceededException.class) - .isThrownBy(() -> insertTargetMetadata("k" + maxMetaData, target1, "v" + maxMetaData)); + .isThrownBy(() -> insertTargetMetadata("k" + maxMetaData, "v" + maxMetaData, target1)); // add multiple meta data entries at once final Target target2 = testdataFactory.createTarget("target2"); @@ -966,7 +966,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { final Target target3 = testdataFactory.createTarget("target3"); final int firstHalf = Math.round(maxMetaData / 2); for (int i = 0; i < firstHalf; ++i) { - insertTargetMetadata("k" + i, target3, "v" + i); + insertTargetMetadata("k" + i, "v" + i, target3); } // add too many data entries final int secondHalf = maxMetaData - firstHalf; @@ -994,7 +994,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { assertThat(target.getOptLockRevision()).isEqualTo(1); // create target meta data entry - insertTargetMetadata(knownKey, target, knownValue); + insertTargetMetadata(knownKey, knownValue, target); Target changedLockRevisionTarget = targetManagement.get(target.getId()).get(); assertThat(changedLockRevisionTarget.getOptLockRevision()).isEqualTo(2); @@ -1024,16 +1024,8 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { @Description("Queries and loads the metadata related to a given target.") public void findAllTargetMetadataByControllerId() { // create targets - final Target target1 = testdataFactory.createTarget("target1"); - final Target target2 = testdataFactory.createTarget("target2"); - - for (int index = 0; index < 10; index++) { - insertTargetMetadata("key" + index, target1, "value" + index); - } - - for (int index = 0; index < 8; index++) { - insertTargetMetadata("key" + index, target2, "value" + index); - } + final Target target1 = createTargetWithMetadata("target1", 10); + final Target target2 = createTargetWithMetadata("target2", 8); final Page metadataOfTarget1 = targetManagement .findMetaDataByControllerId(new PageRequest(0, 100), target1.getControllerId()); @@ -1047,4 +1039,47 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { assertThat(metadataOfTarget2.getNumberOfElements()).isEqualTo(8); assertThat(metadataOfTarget2.getTotalElements()).isEqualTo(8); } + + private Target createTargetWithMetadata(final String controllerId, final int count) { + final Target target = testdataFactory.createTarget(controllerId); + + for (int index = 1; index <= count; index++) { + insertTargetMetadata("key" + index, controllerId + "-value" + index, target); + } + + return target; + } + + @Test + @Description("Test that RSQL filter finds targets with metadata and/or controllerId.") + public void findTargetsByRsqlWithMetadata() { + final String controllerId1 = "target1"; + final String controllerId2 = "target2"; + createTargetWithMetadata(controllerId1, 2); + createTargetWithMetadata(controllerId2, 2); + + final String rsqlAndControllerIdFilter = "id==target1 and metadata.key1==target1-value1"; + final String rsqlAndControllerIdWithWrongKeyFilter = "id==* and metadata.unknown==value1"; + final String rsqlAndControllerIdNotEqualFilter = "id==* and metadata.key2!=target1-value2"; + final String rsqlOrControllerIdFilter = "id==target1 or metadata.key1==*value1"; + final String rsqlOrControllerIdWithWrongKeyFilter = "id==target2 or metadata.unknown==value1"; + final String rsqlOrControllerIdNotEqualFilter = "id==target1 or metadata.key1!=target1-value1"; + + assertThat(targetManagement.count()).as("Total targets").isEqualTo(2); + validateFoundTargetsByRsql(rsqlAndControllerIdFilter, controllerId1); + validateFoundTargetsByRsql(rsqlAndControllerIdWithWrongKeyFilter); + validateFoundTargetsByRsql(rsqlAndControllerIdNotEqualFilter, controllerId2); + validateFoundTargetsByRsql(rsqlOrControllerIdFilter, controllerId1, controllerId2); + validateFoundTargetsByRsql(rsqlOrControllerIdWithWrongKeyFilter, controllerId2); + validateFoundTargetsByRsql(rsqlOrControllerIdNotEqualFilter, controllerId1, controllerId2); + } + + private void validateFoundTargetsByRsql(final String rsqlFilter, final String... controllerIds) { + final Page foundTargetsByMetadataAndControllerId = targetManagement.findByRsql(PAGE, rsqlFilter); + + assertThat(foundTargetsByMetadataAndControllerId.getTotalElements()).as("Targets count in RSQL filter is wrong") + .isEqualTo(controllerIds.length); + assertThat(foundTargetsByMetadataAndControllerId.getContent().stream().map(Target::getControllerId)) + .as("Targets found by RSQL filter have wrong controller ids").containsExactlyInAnyOrder(controllerIds); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java index 55028413f..4018fdc63 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java @@ -112,7 +112,7 @@ public class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test filter distribution set by tag") + @Description("Test filter distribution set by tag name") public void testFilterByTag() { assertRSQLQuery(DistributionSetFields.TAG.name() + "==Tag1", 2); // does not include untagged sets @@ -124,7 +124,7 @@ public class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test filter distribution set by type") + @Description("Test filter distribution set by type key") public void testFilterByType() { assertRSQLQuery(DistributionSetFields.TYPE.name() + "==" + TestdataFactory.DS_TYPE_DEFAULT, 4); assertRSQLQuery(DistributionSetFields.TYPE.name() + "==noExist*", 0); @@ -133,7 +133,7 @@ public class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { } @Test - @Description("") + @Description("Test filter distribution set by metadata") public void testFilterByMetadata() { assertRSQLQuery(DistributionSetFields.METADATA.name() + ".metaKey==metaValue", 1); assertRSQLQuery(DistributionSetFields.METADATA.name() + ".metaKey==*v*", 2); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLSoftwareModuleFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLSoftwareModuleFieldTest.java index 59c2a680f..5994f5b6f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLSoftwareModuleFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLSoftwareModuleFieldTest.java @@ -92,7 +92,7 @@ public class RSQLSoftwareModuleFieldTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test filter software module by type") + @Description("Test filter software module by type key") public void testFilterByType() { assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "==" + TestdataFactory.SM_TYPE_APP, 2); assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "!=" + TestdataFactory.SM_TYPE_APP, 3); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java index 8ef37c24b..81fa3c66b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java @@ -49,6 +49,7 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { attributes.put("revision", "1.1"); target = controllerManagement.updateControllerAttributes(target.getControllerId(), attributes, null); target = controllerManagement.findOrRegisterTargetIfItDoesNotexist(target.getControllerId(), LOCALHOST); + createTargetMetadata(target.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "metaValue")); target2 = targetManagement .create(entityFactory.target().create().controllerId("targetId1234").description("targetId1234")); @@ -56,6 +57,7 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { Thread.sleep(1); target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), attributes, null); target2 = controllerManagement.findOrRegisterTargetIfItDoesNotexist(target2.getControllerId(), LOCALHOST); + createTargetMetadata(target2.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "value")); final Target target3 = testdataFactory.createTarget("targetId1235"); final Target target4 = testdataFactory.createTarget("targetId1236"); @@ -168,7 +170,7 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { } @Test - @Description("Test filter target by tag") + @Description("Test filter target by tag name") public void testFilterByTag() { assertRSQLQuery(TargetFields.TAG.name() + "==Tag1", 2); assertRSQLQuery(TargetFields.TAG.name() + "!=Tag1", 2); @@ -194,6 +196,18 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "=gt=${OVERDUE_TS}", 2); } + @Test + @Description("Test filter target by metadata") + public void testFilterByMetadata() { + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==metaValue", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==*v*", 2); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==noExist*", 0); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".notExist==metaValue", 0); + + } + private void assertRSQLQuery(final String rsqlParam, final long expcetedTargets) { final Page findTargetPage = targetManagement.findByRsql(PAGE, rsqlParam); final long countTargetsAll = findTargetPage.getTotalElements(); 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 690da8db1..06702c071 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 @@ -115,15 +115,23 @@ public class RSQLUtilityTest { try { RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); - fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); + fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key was not present)"); } catch (final RSQLParameterUnsupportedFieldException e) { } - wrongRSQL = TargetFields.ATTRIBUTE + ".unkwon.wrong==abc"; + wrongRSQL = TargetFields.ATTRIBUTE + ".unknown.wrong==abc"; try { RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); - fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); + fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key includes dots)"); + } catch (final RSQLParameterUnsupportedFieldException e) { + } + + wrongRSQL = TargetFields.METADATA + ".unknown.wrong==abc"; + try { + RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + criteriaQueryMock, criteriaBuilderMock); + fail("Missing expected RSQLParameterSyntaxException for target metadata map, caused by wrong RSQL syntax (key includes dots)"); } catch (final RSQLParameterUnsupportedFieldException e) { } @@ -131,7 +139,7 @@ public class RSQLUtilityTest { try { RSQLUtility.parse(wrongRSQL, DistributionSetFields.class, null, testDb) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); - fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); + fail("Missing expected RSQLParameterSyntaxException for distribution set metadata map, caused by wrong RSQL syntax (key was not present)"); } catch (final RSQLParameterUnsupportedFieldException e) { } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index dcfe57b90..b9322f7df 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -54,6 +54,7 @@ import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.repository.test.matcher.EventVerifier; @@ -297,6 +298,14 @@ public abstract class AbstractIntegrationTest { return distributionSetManagement.createMetaData(dsId, md); } + protected TargetMetadata createTargetMetadata(final String controllerId, final MetaData md) { + return createTargetMetadata(controllerId, Collections.singletonList(md)).get(0); + } + + protected List createTargetMetadata(final String controllerId, final List md) { + return targetManagement.createMetaData(controllerId, md); + } + protected Long getOsModule(final DistributionSet ds) { return ds.findFirstModuleByType(osType).get().getId(); } @@ -395,8 +404,8 @@ public abstract class AbstractIntegrationTest { protected static String getTestSchedule(final int minutesToAdd) { ZonedDateTime currentTime = ZonedDateTime.now(); currentTime = currentTime.plusMinutes(minutesToAdd); - return String.format("0 %d %d %d %d ? %d", currentTime.getMinute(), currentTime.getHour(), - currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear()); + return String.format("%d %d %d %d %d ? %d", currentTime.getSecond(), currentTime.getMinute(), + currentTime.getHour(), currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear()); } protected static String getTestDuration(final int duration) { diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index cd28d88fb..a0fb45698 100644 --- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -393,7 +393,6 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { private void assertAttributesUpdateNotRequestedAfterFailedDeployment(Target target, final DistributionSet ds) throws Exception { target = assignDistributionSet(ds.getId(), target.getControllerId()).getAssignedEntity().iterator().next(); - assignDistributionSet(ds.getId(), target.getControllerId()); final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()) .getContent().get(0); sendDeploymentActionFeedback(target, action, @@ -413,12 +412,10 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { assertThatAttributesUpdateIsRequested(target.getControllerId()); } - private void assertThatAttributesUpdateIsRequested(final String targetControllerId) - throws Exception { - mvc.perform( - get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), targetControllerId) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(jsonPath("$._links.configData.href").isNotEmpty()); + private void assertThatAttributesUpdateIsRequested(final String targetControllerId) throws Exception { + mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(), targetControllerId) + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andExpect(jsonPath("$._links.configData.href").isNotEmpty()); } private void assertThatAttributesUpdateIsNotRequested(final String targetControllerId) throws Exception { @@ -427,8 +424,8 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$._links.configData").doesNotExist()); } - private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, - final String feedback) throws Exception { + private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String feedback) + throws Exception { return mvc.perform(post("/{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId}/feedback", tenantAware.getCurrentTenant(), target.getControllerId(), action.getId()).content(feedback) .contentType(MediaType.APPLICATION_JSON));