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 a246a11d0..91a9a247c 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
@@ -27,6 +27,7 @@ import org.eclipse.hawkbit.repository.builder.TargetFilterQueryUpdate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignActionTypeException;
import org.eclipse.hawkbit.repository.exception.InvalidAutoAssignDistributionSetException;
+import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
@@ -43,6 +44,8 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
@@ -58,6 +61,8 @@ import org.springframework.validation.annotation.Validated;
import com.google.common.collect.Lists;
+import cz.jirutka.rsql.parser.RSQLParserException;
+
/**
* JPA implementation of {@link TargetFilterQueryManagement}.
*
@@ -66,6 +71,8 @@ import com.google.common.collect.Lists;
@Validated
public class JpaTargetFilterQueryManagement implements TargetFilterQueryManagement {
+ private static final Logger LOGGER = LoggerFactory.getLogger(JpaTargetFilterQueryManagement.class);
+
private final TargetFilterQueryRepository targetFilterQueryRepository;
private final TargetManagement targetManagement;
@@ -102,12 +109,18 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
public TargetFilterQuery create(final TargetFilterQueryCreate c) {
final JpaTargetFilterQueryCreate create = (JpaTargetFilterQueryCreate) c;
- // enforce the 'max targets per auto assign' quota right here even if
- // the result of the filter query can vary over time
- if (create.getAutoAssignDistributionSetId().isPresent()) {
- WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement).validate(create);
- create.getQuery().ifPresent(this::assertMaxTargetsQuota);
- }
+ create.getQuery().ifPresent(query -> {
+ // validate the RSQL query syntax
+ RSQLUtility.validateRsqlFor(query, TargetFields.class);
+
+ // enforce the 'max targets per auto assign' quota right here even
+ // if the result of the filter query can vary over time
+ if (create.getAutoAssignDistributionSetId().isPresent()) {
+ WeightValidationHelper.usingContext(systemSecurityContext, tenantConfigurationManagement)
+ .validate(create);
+ assertMaxTargetsQuota(query);
+ }
+ });
return targetFilterQueryRepository.save(create.build());
}
@@ -291,8 +304,13 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
@Override
public boolean verifyTargetFilterQuerySyntax(final String query) {
- RSQLUtility.parse(query, TargetFields.class, virtualPropertyReplacer, database);
- return true;
+ try {
+ RSQLUtility.validateRsqlFor(query, TargetFields.class);
+ return true;
+ } catch (RSQLParserException | RSQLParameterUnsupportedFieldException e) {
+ LOGGER.debug("The RSQL query '" + query + "' is invalid.", e);
+ return false;
+ }
}
private void assertMaxTargetsQuota(final String query) {
diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractFieldNameRSQLVisitor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractFieldNameRSQLVisitor.java
new file mode 100644
index 000000000..ece88ff41
--- /dev/null
+++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractFieldNameRSQLVisitor.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2020 devolo AG 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 cz.jirutka.rsql.parser.ast.ComparisonNode;
+import org.eclipse.hawkbit.repository.FieldNameProvider;
+import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public abstract class AbstractFieldNameRSQLVisitor & FieldNameProvider> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractFieldNameRSQLVisitor.class);
+
+ private final Class fieldNameProvider;
+
+ public AbstractFieldNameRSQLVisitor(final Class fieldNameProvider) {
+ this.fieldNameProvider = fieldNameProvider;
+ }
+
+ protected A getFieldEnumByName(final ComparisonNode node) {
+ String enumName = node.getSelector();
+ final String[] graph = getSubAttributesFrom(enumName);
+ if (graph.length != 0) {
+ enumName = graph[0];
+ }
+ LOGGER.debug("get field identifier by name {} of enum type {}", enumName, fieldNameProvider);
+ try {
+ return Enum.valueOf(fieldNameProvider, enumName.toUpperCase());
+ } catch (final IllegalArgumentException e) {
+ throw createRSQLParameterUnsupportedException(node, e);
+ }
+ }
+
+ protected static String[] getSubAttributesFrom(final String property) {
+ return property.split("\\" + FieldNameProvider.SUB_ATTRIBUTE_SEPERATOR);
+ }
+
+ protected String getAndValidatePropertyFieldName(final A propertyEnum, final ComparisonNode node) {
+
+ final String[] graph = getSubAttributesFrom(node.getSelector());
+
+ validateMapParameter(propertyEnum, node, graph);
+
+ // sub entity need minimum 1 dot
+ if (!propertyEnum.getSubEntityAttributes().isEmpty() && graph.length < 2) {
+ throw createRSQLParameterUnsupportedException(node);
+ }
+
+ final StringBuilder fieldNameBuilder = new StringBuilder(propertyEnum.getFieldName());
+
+ for (int i = 1; i < graph.length; i++) {
+
+ final String propertyField = graph[i];
+ fieldNameBuilder.append(FieldNameProvider.SUB_ATTRIBUTE_SEPERATOR).append(propertyField);
+
+ // the key of map is not in the graph
+ if (propertyEnum.isMap() && graph.length == (i + 1)) {
+ continue;
+ }
+
+ if (!propertyEnum.containsSubEntityAttribute(propertyField)) {
+ throw createRSQLParameterUnsupportedException(node);
+ }
+ }
+
+ return fieldNameBuilder.toString();
+ }
+
+ protected 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, alternatively you could use the key/value tuple, defined by SimpleImmutableEntry class");
+ }
+
+ // enum.key
+ final int minAttributeForMap = 2;
+ if (graph.length != minAttributeForMap) {
+ throw new RSQLParameterUnsupportedFieldException("The syntax of the given map search parameter field {"
+ + node.getSelector() + "} is wrong. Syntax is: fieldname.keyname", new Exception());
+ }
+ }
+
+ protected RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(
+ final ComparisonNode node) {
+ return createRSQLParameterUnsupportedException(node, new Exception());
+ }
+
+ protected RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(final ComparisonNode node,
+ final Exception rootException) {
+ return createRSQLParameterUnsupportedException(String.format(
+ "The given search parameter field {%s} does not exist, must be one of the following fields %s",
+ node.getSelector(), getExpectedFieldList()), rootException);
+ }
+
+ protected RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(final String message) {
+ return createRSQLParameterUnsupportedException(message, null);
+ }
+
+ protected RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(final String message,
+ final Exception rootException) {
+ return new RSQLParameterUnsupportedFieldException(message, rootException);
+ }
+
+ // Exception squid:S2095 - see
+ // https://jira.sonarsource.com/browse/SONARJAVA-1478
+ @SuppressWarnings({ "squid:S2095" })
+ private List getExpectedFieldList() {
+ final List expectedFieldList = Arrays.stream(fieldNameProvider.getEnumConstants())
+ .filter(enumField -> enumField.getSubEntityAttributes().isEmpty()).map(enumField -> {
+ final String enumFieldName = enumField.name().toLowerCase();
+
+ if (enumField.isMap()) {
+ return enumFieldName + FieldNameProvider.SUB_ATTRIBUTE_SEPERATOR + "keyName";
+ }
+
+ return enumFieldName;
+ }).collect(Collectors.toList());
+
+ final List expectedSubFieldList = Arrays.stream(fieldNameProvider.getEnumConstants())
+ .filter(enumField -> !enumField.getSubEntityAttributes().isEmpty()).flatMap(enumField -> {
+ final List subEntity = enumField
+ .getSubEntityAttributes().stream().map(fieldName -> enumField.name().toLowerCase()
+ + FieldNameProvider.SUB_ATTRIBUTE_SEPERATOR + fieldName)
+ .collect(Collectors.toList());
+
+ return subEntity.stream();
+ }).collect(Collectors.toList());
+ expectedFieldList.addAll(expectedSubFieldList);
+ return expectedFieldList;
+ }
+}
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 7e0e03dff..6b2fac015 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
@@ -140,19 +140,27 @@ public final class RSQLUtility {
}
/**
- * Validate the given rsql string regarding existence and correct syntax.
- *
+ * Validates the RSQL string
+ *
* @param rsql
- * the rsql string to get validated
- *
+ * RSQL string to validate
+ * @param fieldNameProvider
+ *
+ * @throws RSQLParserException
+ * if RSQL syntax is invalid
+ * @throws RSQLParameterUnsupportedFieldException
+ * if RSQL key is not allowed
*/
- public static void isValid(final String rsql) {
- parseRsql(rsql);
+ public static & FieldNameProvider> void validateRsqlFor(final String rsql,
+ final Class fieldNameProvider) {
+ final RSQLVisitor visitor = new ValidationRSQLVisitor<>(fieldNameProvider);
+ final Node rootNode = parseRsql(rsql);
+ rootNode.accept(visitor);
}
private static Node parseRsql(final String rsql) {
try {
- LOGGER.debug("parsing rsql string {}", rsql);
+ LOGGER.debug("Parsing rsql string {}", rsql);
final Set operators = RSQLOperators.defaultOperators();
return new RSQLParser(operators).parse(rsql);
} catch (final IllegalArgumentException e) {
@@ -162,6 +170,36 @@ public final class RSQLUtility {
}
}
+ private static final class ValidationRSQLVisitor & FieldNameProvider>
+ extends AbstractFieldNameRSQLVisitor implements RSQLVisitor {
+
+ public ValidationRSQLVisitor(final Class fieldNameProvider) {
+ super(fieldNameProvider);
+ }
+
+ @Override
+ public Void visit(final AndNode node, final String param) {
+ return visitNode(node, param);
+ }
+
+ @Override
+ public Void visit(final OrNode node, final String param) {
+ return visitNode(node, param);
+ }
+
+ @Override
+ public Void visit(final ComparisonNode node, final String param) {
+ final A fieldName = getFieldEnumByName(node);
+ getAndValidatePropertyFieldName(fieldName, node);
+ return null;
+ }
+
+ private Void visitNode(final LogicalNode node, final String param) {
+ node.getChildren().forEach(child -> child.accept(this, param));
+ return null;
+ }
+ }
+
private static final class RSQLSpecification & FieldNameProvider, T> implements Specification {
private static final long serialVersionUID = 1L;
@@ -184,7 +222,7 @@ public final class RSQLUtility {
final Node rootNode = parseRsql(rsql);
query.distinct(true);
- final JpqQueryRSQLVisitor jpqQueryRSQLVisitor = new JpqQueryRSQLVisitor<>(root, cb, enumType,
+ final JpaQueryRSQLVisitor jpqQueryRSQLVisitor = new JpaQueryRSQLVisitor<>(root, cb, enumType,
virtualPropertyReplacer, database, query);
final List accept = rootNode., String> accept(jpqQueryRSQLVisitor);
@@ -198,9 +236,7 @@ public final class RSQLUtility {
/**
* An implementation of the {@link RSQLVisitor} to visit the parsed tokens
- * and build jpa where clauses.
- *
- *
+ * and build JPA where clauses.
*
* @param
* the enum for providing the field name of the entity field to
@@ -208,34 +244,35 @@ public final class RSQLUtility {
* @param
* the entity type referenced by the root
*/
- private static final class JpqQueryRSQLVisitor & FieldNameProvider, T>
- implements RSQLVisitor, String> {
+ private static final class JpaQueryRSQLVisitor & FieldNameProvider, T>
+ extends AbstractFieldNameRSQLVisitor implements RSQLVisitor, String> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JpaQueryRSQLVisitor.class);
+
+ public static final Character LIKE_WILDCARD = '*';
private static final char ESCAPE_CHAR = '\\';
private static final List NO_JOINS_OPERATOR = Lists.newArrayList("!=", "=out=");
- public static final Character LIKE_WILDCARD = '*';
+ private final Map>> joinsInLevel = new HashMap<>(3);
- private final Root root;
private final CriteriaBuilder cb;
private final CriteriaQuery> query;
- private final Class enumType;
+ private final Database database;
+ private final Root root;
+ private final SimpleTypeConverter simpleTypeConverter;
private final VirtualPropertyReplacer virtualPropertyReplacer;
+
private int level;
private boolean isOrLevel;
- private final Map>> joinsInLevel = new HashMap<>(3);
private boolean joinsNeeded;
- private final SimpleTypeConverter simpleTypeConverter;
-
- private final Database database;
-
- private JpqQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType,
+ private JpaQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType,
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database,
final CriteriaQuery> query) {
+ super(enumType);
this.root = root;
this.cb = cb;
this.query = query;
- this.enumType = enumType;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.simpleTypeConverter = new SimpleTypeConverter();
this.database = database;
@@ -297,64 +334,6 @@ public final class RSQLUtility {
return Collections.singletonList(predicate);
}
- private String getAndValidatePropertyFieldName(final A propertyEnum, final ComparisonNode node) {
-
- final String[] graph = getSubAttributesFrom(node.getSelector());
-
- validateMapParameter(propertyEnum, node, graph);
-
- // sub entity need minium 1 dot
- if (!propertyEnum.getSubEntityAttributes().isEmpty() && graph.length < 2) {
- throw createRSQLParameterUnsupportedException(node);
- }
-
- final StringBuilder fieldNameBuilder = new StringBuilder(propertyEnum.getFieldName());
-
- for (int i = 1; i < graph.length; i++) {
-
- final String propertyField = graph[i];
- fieldNameBuilder.append(FieldNameProvider.SUB_ATTRIBUTE_SEPERATOR).append(propertyField);
-
- // the key of map is not in the graph
- if (propertyEnum.isMap() && graph.length == (i + 1)) {
- continue;
- }
-
- if (!propertyEnum.containsSubEntityAttribute(propertyField)) {
- throw createRSQLParameterUnsupportedException(node);
- }
- }
-
- return fieldNameBuilder.toString();
- }
-
- 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, alternatively you could use the key/value tuple, defined by SimpleImmutableEntry class");
- }
-
- // enum.key
- final int minAttributeForMap = 2;
- if (graph.length != minAttributeForMap) {
- throw new RSQLParameterUnsupportedFieldException("The syntax of the given map search parameter field {"
- + node.getSelector() + "} is wrong. Syntax is: fieldname.keyname", new Exception());
- }
- }
-
- private RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(
- final ComparisonNode node) {
- return new RSQLParameterUnsupportedFieldException(
- "The given search parameter field {" + node.getSelector()
- + "} does not exist, must be one of the following fields {" + getExpectedFieldList() + "}",
- new Exception());
- }
-
/**
* Resolves the Path for a field in the persistence layer and joins the
* required models. This operation is part of a tree traversal through
@@ -374,12 +353,14 @@ public final class RSQLUtility {
* dot notated field path
* @return the Path for a field
*/
+ @SuppressWarnings("unchecked")
private Path