Feature target metadata filter (#767)

* implemented RSQL query filter for target metadata

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* refactored rsql fields providers code for targets, distribution sets and software modules for consistency, fixed predicate grouping for map fields in rsql utility

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* extended tests for target management rsql queries with target metadata

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* extended target management test for RSQL not equal case of target metadata, added a suggestion of comparator operators when map key ends with = or ! symbol in Target Filter View

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Fixed peer review findings

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* small test fixes: seconds are respected while scheduling the maintenance window, redundant ds-target assignment statement removed

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>
This commit is contained in:
Bondar Bogdan
2018-11-28 14:09:52 +01:00
committed by Dominic Schabel
parent a460f61e13
commit b2dfd4a99e
16 changed files with 272 additions and 153 deletions

View File

@@ -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<String, String> 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<String, String> subEntityMapTuple) {
this(fieldName, true, subEntityMapTuple);
}
private DistributionSetFields(final String fieldName, final boolean mapField,
final Entry<String, String> subEntityMapTuple) {
this.fieldName = fieldName;
this.keyFieldName = keyFieldName;
this.valueFieldName = valueFieldName;
this.mapField = mapField;
this.subEntityMapTuple = subEntityMapTuple;
}
@Override
public String getValueFieldName() {
return valueFieldName;
public Optional<Entry<String, String>> getSubEntityMapTuple() {
return Optional.ofNullable(subEntityMapTuple);
}
@Override
public String getKeyFieldName() {
return keyFieldName;
public boolean isMap() {
return mapField;
}
@Override
public String getFieldName() {
return fieldName;
}
}

View File

@@ -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<Entry<String, String>> getSubEntityMapTuple() {
return Optional.empty();
}
/**
@@ -88,6 +79,6 @@ public interface FieldNameProvider {
* @return <true> is a map <false> is not a map
*/
default boolean isMap() {
return getKeyFieldName() != null;
return false;
}
}

View File

@@ -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<String, String> 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<String, String> subEntityMapTuple) {
this(fieldName, true, subEntityMapTuple);
}
private SoftwareModuleFields(final String fieldName, final boolean mapField,
final Entry<String, String> subEntityMapTuple) {
this.fieldName = fieldName;
this.keyFieldName = keyFieldName;
this.valueFieldName = valueFieldName;
this.mapField = mapField;
this.subEntityMapTuple = subEntityMapTuple;
}
@Override
public Optional<Entry<String, String>> 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;
}
}

View File

@@ -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<String> subEntityAttribues;
private boolean mapField;
private Entry<String, String> 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<String> subEntityAttribues) {
private TargetFields(final String fieldName, final Entry<String, String> subEntityMapTuple) {
this(fieldName, true, Collections.emptyList(), subEntityMapTuple);
}
private TargetFields(final String fieldName, final boolean mapField, final List<String> subEntityAttribues,
final Entry<String, String> 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<Entry<String, String>> getSubEntityMapTuple() {
return Optional.ofNullable(subEntityMapTuple);
}
@Override
public boolean isMap() {
return mapField;

View File

@@ -36,34 +36,27 @@ public enum TargetFilterQueryFields implements FieldNameProvider {
private final String fieldName;
private List<String> 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<String> subEntityAttribues) {
private TargetFilterQueryFields(final String fieldName, final List<String> subEntityAttribues) {
this.fieldName = fieldName;
this.mapField = mapField;
this.subEntityAttributes = subEntityAttribues;
}
@Override
public List<String> getSubEntityAttributes() {
return subEntityAttributes;
}
@Override
public boolean isMap() {
return mapField;
}
@Override
public String getFieldName() {
return fieldName;
}
@Override
public List<String> getSubEntityAttributes() {
return subEntityAttributes;
}
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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<Predicate> 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<Object> fieldPath,
private Predicate addOperatorPredicate(final ComparisonNode node, final Path<Object> fieldPath,
final List<Object> transformedValues, final Object transformedValue, final String value,
final List<Predicate> 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<Object> getMapValueFieldPath(final A enumField, final Path<Object> 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<Object> fieldPath,

View File

@@ -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<? extends SuggestToken> 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<String> 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<SuggestToken> expectedTokens) {
expectedTokens.addAll(getNextTokens(ex));
}
private static List<SuggestToken> getNextTokens(final Exception e) {
final ParseException parseException = findParseException(e);
private static void setExceptionDetails(final String rsqlQuery, final Exception ex,
final List<SuggestToken> expectedTokens) {
final ParseException parseException = findParseException(ex);
if (parseException == null) {
return Collections.emptyList();
expectedTokens.addAll(getComparatorOperatorSuggestions(rsqlQuery));
} else {
expectedTokens.addAll(getNextTokens(parseException));
}
}
private static List<SuggestToken> getNextTokens(final ParseException parseException) {
final List<SuggestToken> 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<SuggestToken> 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<String> 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<SuggestToken> 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<SuggestToken> toTopSuggestToken(final int beginToken, final int endToken,
final String tokenImageName) {
return FIELD_NAMES.stream()

View File

@@ -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<TargetMetadata> 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<Target> 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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<Target> findTargetPage = targetManagement.findByRsql(PAGE, rsqlParam);
final long countTargetsAll = findTargetPage.getTotalElements();

View File

@@ -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) {
}

View File

@@ -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<TargetMetadata> createTargetMetadata(final String controllerId, final List<MetaData> 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) {

View File

@@ -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));