Skip to content

Commit

Permalink
Add QueryCriteriaValidator to search
Browse files Browse the repository at this point in the history
Adds an extendable point to allow custom validation of search queries.

Signed-off-by: David Schwilk <david.schwilk@bosch.io>
  • Loading branch information
DerSchwilk committed Dec 2, 2020
1 parent 29d9c20 commit 059b1d2
Show file tree
Hide file tree
Showing 11 changed files with 437 additions and 22 deletions.
Expand Up @@ -44,6 +44,7 @@ public final class DittoSearchConfig implements SearchConfig {

private final DittoServiceConfig dittoServiceConfig;
@Nullable private final String mongoHintsByNamespace;
private final String queryCriteriaValidator;
private final DeleteConfig deleteConfig;
private final DeletionConfig deletionConfig;
private final UpdaterConfig updaterConfig;
Expand All @@ -62,6 +63,7 @@ private DittoSearchConfig(final ScopedConfig dittoScopedConfig) {
final ConfigWithFallback configWithFallback =
ConfigWithFallback.newInstance(dittoScopedConfig, CONFIG_PATH, SearchConfigValue.values());
mongoHintsByNamespace = configWithFallback.getStringOrNull(SearchConfigValue.MONGO_HINTS_BY_NAMESPACE);
queryCriteriaValidator = configWithFallback.getStringOrNull(SearchConfigValue.QUERY_CRITERIA_VALIDATOR);
deleteConfig = DefaultDeleteConfig.of(configWithFallback);
deletionConfig = DefaultDeletionConfig.of(configWithFallback);
updaterConfig = DefaultUpdaterConfig.of(configWithFallback);
Expand All @@ -86,6 +88,11 @@ public Optional<String> getMongoHintsByNamespace() {
return Optional.ofNullable(mongoHintsByNamespace);
}

@Override
public String getQueryValidator() {
return queryCriteriaValidator;
}

@Override
public DeleteConfig getDeleteConfig() {
return deleteConfig;
Expand Down Expand Up @@ -157,6 +164,7 @@ public boolean equals(final Object o) {
}
final DittoSearchConfig that = (DittoSearchConfig) o;
return Objects.equals(mongoHintsByNamespace, that.mongoHintsByNamespace) &&
Objects.equals(queryCriteriaValidator, that.queryCriteriaValidator) &&
Objects.equals(deleteConfig, that.deleteConfig) &&
Objects.equals(deletionConfig, that.deletionConfig) &&
Objects.equals(updaterConfig, that.updaterConfig) &&
Expand All @@ -170,14 +178,16 @@ public boolean equals(final Object o) {

@Override
public int hashCode() {
return Objects.hash(mongoHintsByNamespace, deleteConfig, deletionConfig, updaterConfig, dittoServiceConfig,
healthCheckConfig, indexInitializationConfig, persistenceOperationsConfig, mongoDbConfig, streamConfig);
return Objects.hash(mongoHintsByNamespace, queryCriteriaValidator, deleteConfig, deletionConfig, updaterConfig,
dittoServiceConfig, healthCheckConfig, indexInitializationConfig, persistenceOperationsConfig,
mongoDbConfig, streamConfig);
}

@Override
public String toString() {
return getClass().getSimpleName() + " [" +
"mongoHintsByNamespace=" + mongoHintsByNamespace +
", queryCriteriaValidator=" + queryCriteriaValidator +
", deleteConfig=" + deleteConfig +
", deletionConfig=" + deletionConfig +
", updaterConfig=" + updaterConfig +
Expand Down
Expand Up @@ -32,6 +32,16 @@ public interface SearchConfig extends ServiceSpecificConfig, WithHealthCheckConf

Optional<String> getMongoHintsByNamespace();

/**
* Returns the {@code QueryCriteriaValidator} to be used for validation and decoding
* {@link org.eclipse.ditto.model.query.criteria.Criteria} of a
* {@link org.eclipse.ditto.signals.commands.thingsearch.query.ThingSearchQueryCommand}.
*
* @return the config.
* @since 1.5.0
*/
String getQueryValidator();

/**
* Returns the configuration settings of the "delete" section.
*
Expand Down Expand Up @@ -69,7 +79,16 @@ enum SearchConfigValue implements KnownConfigValue {
/**
* Default value is {@code null}.
*/
MONGO_HINTS_BY_NAMESPACE("mongo-hints-by-namespace", null);
MONGO_HINTS_BY_NAMESPACE("mongo-hints-by-namespace", null),

/**
* The {@code QueryCriteriaValidator} used for decoding and validating {@link org.eclipse.ditto.model.query.criteria.Criteria}
* of a {@link org.eclipse.ditto.signals.commands.thingsearch.query.ThingSearchQueryCommand}.
*
* @since 1.5.0
*/
QUERY_CRITERIA_VALIDATOR("query-criteria-validator",
"org.eclipse.ditto.services.thingsearch.persistence.query.validation.DefaultQueryCriteriaValidator");

private final String path;
private final Object defaultValue;
Expand Down
Expand Up @@ -26,6 +26,7 @@
import org.eclipse.ditto.model.thingsearchparser.RqlOptionParser;
import org.eclipse.ditto.services.models.thingsearch.commands.sudo.SudoCountThings;
import org.eclipse.ditto.services.models.thingsearch.query.filter.ParameterOptionVisitor;
import org.eclipse.ditto.services.thingsearch.persistence.query.validation.QueryCriteriaValidator;
import org.eclipse.ditto.signals.commands.thingsearch.exceptions.InvalidOptionException;
import org.eclipse.ditto.signals.commands.thingsearch.query.QueryThings;
import org.eclipse.ditto.signals.commands.thingsearch.query.StreamThings;
Expand All @@ -40,14 +41,17 @@ public final class QueryParser {
private final ThingsFieldExpressionFactory fieldExpressionFactory;
private final QueryBuilderFactory queryBuilderFactory;
private final RqlOptionParser rqlOptionParser;
private final QueryCriteriaValidator queryCriteriaValidator;

private QueryParser(final CriteriaFactory criteriaFactory,
final ThingsFieldExpressionFactory fieldExpressionFactory,
final QueryBuilderFactory queryBuilderFactory) {
final QueryBuilderFactory queryBuilderFactory,
final QueryCriteriaValidator queryCriteriaValidator) {

this.queryFilterCriteriaFactory = new QueryFilterCriteriaFactory(criteriaFactory, fieldExpressionFactory);
this.fieldExpressionFactory = fieldExpressionFactory;
this.queryBuilderFactory = queryBuilderFactory;
this.queryCriteriaValidator = queryCriteriaValidator;
rqlOptionParser = new RqlOptionParser();
}

Expand All @@ -61,9 +65,10 @@ private QueryParser(final CriteriaFactory criteriaFactory,
*/
public static QueryParser of(final CriteriaFactory criteriaFactory,
final ThingsFieldExpressionFactory fieldExpressionFactory,
final QueryBuilderFactory queryBuilderFactory) {
final QueryBuilderFactory queryBuilderFactory,
final QueryCriteriaValidator queryCriteriaValidator) {

return new QueryParser(criteriaFactory, fieldExpressionFactory, queryBuilderFactory);
return new QueryParser(criteriaFactory, fieldExpressionFactory, queryBuilderFactory, queryCriteriaValidator);
}

/**
Expand All @@ -73,7 +78,7 @@ public static QueryParser of(final CriteriaFactory criteriaFactory,
* @return the query.
*/
public Query parse(final ThingSearchQueryCommand<?> command) {
final Criteria criteria = parseCriteria(command);
final Criteria criteria = queryCriteriaValidator.parseCriteria(command, queryFilterCriteriaFactory);
if (command instanceof QueryThings) {
final QueryThings queryThings = (QueryThings) command;
final QueryBuilder queryBuilder = queryBuilderFactory.newBuilder(criteria);
Expand Down Expand Up @@ -111,17 +116,6 @@ public CriteriaFactory getCriteriaFactory() {
return queryFilterCriteriaFactory.toCriteriaFactory();
}

private Criteria parseCriteria(final ThingSearchQueryCommand<?> command) {
final DittoHeaders headers = command.getDittoHeaders();
final Set<String> namespaces = command.getNamespaces().orElse(null);
final String filter = command.getFilter().orElse(null);
if (namespaces == null) {
return queryFilterCriteriaFactory.filterCriteria(filter, command.getDittoHeaders());
} else {
return queryFilterCriteriaFactory.filterCriteriaRestrictedByNamespaces(filter, headers, namespaces);
}
}

private void setOptions(final String options, final QueryBuilder queryBuilder, final DittoHeaders headers) {
try {
final ParameterOptionVisitor visitor = new ParameterOptionVisitor(fieldExpressionFactory, queryBuilder);
Expand Down
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.services.thingsearch.persistence.query.validation;

import java.util.Set;

import org.eclipse.ditto.model.base.headers.DittoHeaders;
import org.eclipse.ditto.model.query.criteria.Criteria;
import org.eclipse.ditto.model.query.filter.QueryFilterCriteriaFactory;
import org.eclipse.ditto.signals.commands.thingsearch.query.ThingSearchQueryCommand;

/**
* Default {@link org.eclipse.ditto.services.thingsearch.persistence.query.validation.QueryCriteriaValidator},
* who parses QueryCriteria without additional validation
*/
public class DefaultQueryCriteriaValidator extends QueryCriteriaValidator {

/**
* Instantiate this provider. Called by reflection.
*/
@SuppressWarnings("unused")
public DefaultQueryCriteriaValidator() {
// Nothing to initialize
}

@Override
public Criteria parseCriteria(final ThingSearchQueryCommand<?> command, final QueryFilterCriteriaFactory factory) {

final DittoHeaders headers = command.getDittoHeaders();
final Set<String> namespaces = command.getNamespaces().orElse(null);
final String filter = command.getFilter().orElse(null);
if (namespaces == null) {
return factory.filterCriteria(filter, command.getDittoHeaders());
} else {
return factory.filterCriteriaRestrictedByNamespaces(filter, headers, namespaces);
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.services.thingsearch.persistence.query.validation;

import org.eclipse.ditto.model.query.criteria.Criteria;
import org.eclipse.ditto.model.query.filter.QueryFilterCriteriaFactory;
import org.eclipse.ditto.services.thingsearch.common.config.DittoSearchConfig;
import org.eclipse.ditto.services.thingsearch.common.config.SearchConfig;
import org.eclipse.ditto.services.utils.akka.AkkaClassLoader;
import org.eclipse.ditto.services.utils.config.DefaultScopedConfig;
import org.eclipse.ditto.signals.commands.thingsearch.query.ThingSearchQueryCommand;

import akka.actor.AbstractExtensionId;
import akka.actor.ActorSystem;
import akka.actor.ExtendedActorSystem;
import akka.actor.Extension;

/**
* Search Query Validator to be loaded by reflection.
* Can be used as an extension point to use custom validation of search queries.
* Implementations MUST have a public constructor.
*/
public abstract class QueryCriteriaValidator implements Extension {

/**
* Gets the criteria of a {@link org.eclipse.ditto.signals.commands.thingsearch.query.ThingSearchQueryCommand} and
* validates it.
*
* May throw an exception depending on the implementation in the used QueryCriteriaValidator.
*
* @return the criteria of the query command.
*/
public abstract Criteria parseCriteria(final ThingSearchQueryCommand<?> command,
final QueryFilterCriteriaFactory factory);

/**
* Load a {@code QueryCriteriaValidator} dynamically according to the search configuration.
*
* @param actorSystem The actor system in which to load the validator.
* @return The validator.
*/
public static QueryCriteriaValidator get(final ActorSystem actorSystem) {
return ExtensionId.INSTANCE.get(actorSystem);
}

/**
* ID of the actor system extension to validate the {@code QueryCriteriaValidator}.
*/
private static final class ExtensionId extends AbstractExtensionId<QueryCriteriaValidator> {

private static final ExtensionId INSTANCE = new ExtensionId();

@Override
public QueryCriteriaValidator createExtension(final ExtendedActorSystem system) {
final SearchConfig searchConfig =
DittoSearchConfig.of(DefaultScopedConfig.dittoScoped(
system.settings().config()));
return AkkaClassLoader.instantiate(system, QueryCriteriaValidator.class,
searchConfig.getQueryValidator());
}
}
}
@@ -1,6 +1,7 @@
ditto.mapping-strategy.implementation = "org.eclipse.ditto.services.models.thingsearch.ThingSearchMappingStrategies"

ditto.things-search {
query-criteria-validator = "org.eclipse.ditto.services.thingsearch.persistence.query.validation.DefaultQueryCriteriaValidator"
mongodb {
connection-pool {
max-size = 100
Expand Down
Expand Up @@ -14,6 +14,7 @@

import static org.eclipse.ditto.services.thingsearch.persistence.PersistenceConstants.BACKGROUND_SYNC_COLLECTION_NAME;

import java.io.ObjectInputFilter;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -33,6 +34,7 @@
import org.eclipse.ditto.services.base.config.limits.LimitsConfig;
import org.eclipse.ditto.services.thingsearch.common.config.SearchConfig;
import org.eclipse.ditto.services.thingsearch.persistence.query.QueryParser;
import org.eclipse.ditto.services.thingsearch.persistence.query.validation.QueryCriteriaValidator;
import org.eclipse.ditto.services.thingsearch.persistence.read.MongoThingsSearchPersistence;
import org.eclipse.ditto.services.thingsearch.persistence.read.ThingsSearchPersistence;
import org.eclipse.ditto.services.thingsearch.persistence.read.query.MongoQueryBuilderFactory;
Expand Down Expand Up @@ -140,16 +142,17 @@ private ThingsSearchPersistence getThingsSearchPersistence(final SearchConfig se
private ActorRef initializeSearchActor(final LimitsConfig limitsConfig,
final ThingsSearchPersistence thingsSearchPersistence) {

final QueryParser queryParser = getQueryParser(limitsConfig);
final QueryParser queryParser = getQueryParser(limitsConfig, getContext().getSystem());

return startChildActor(SearchActor.ACTOR_NAME, SearchActor.props(queryParser, thingsSearchPersistence));
}

static QueryParser getQueryParser(final LimitsConfig limitsConfig) {
protected static QueryParser getQueryParser(final LimitsConfig limitsConfig, final ActorSystem actorSystem) {
final CriteriaFactory criteriaFactory = new CriteriaFactoryImpl();
final ThingsFieldExpressionFactory fieldExpressionFactory = getThingsFieldExpressionFactory();
final QueryBuilderFactory queryBuilderFactory = new MongoQueryBuilderFactory(limitsConfig);
return QueryParser.of(criteriaFactory, fieldExpressionFactory, queryBuilderFactory);
final QueryCriteriaValidator queryCriteriaValidator = QueryCriteriaValidator.get(actorSystem);
return QueryParser.of(criteriaFactory, fieldExpressionFactory, queryBuilderFactory, queryCriteriaValidator);
}

private ActorRef initializeHealthCheckActor(final SearchConfig searchConfig,
Expand Down
@@ -1,6 +1,8 @@
ditto {
mapping-strategy.implementation = "org.eclipse.ditto.services.models.thingsearch.ThingSearchMappingStrategies"

query-criteria-validator = "org.eclipse.ditto.services.thingsearch.persistence.query.validation.DefaultQueryCriteriaValidator"

persistence.operations.delay-after-persistence-actor-shutdown = 5s
persistence.operations.delay-after-persistence-actor-shutdown = ${?DELAY_AFTER_PERSISTENCE_ACTOR_SHUTDOWN}

Expand Down
Expand Up @@ -83,7 +83,8 @@ public final class SearchActorIT {

@BeforeClass
public static void startMongoResource() {
queryParser = SearchRootActor.getQueryParser(DefaultLimitsConfig.of(ConfigFactory.empty()));
queryParser = SearchRootActor.getQueryParser(DefaultLimitsConfig.of(ConfigFactory.empty()),
ActorSystem.create("test-system", ConfigFactory.load("actors-test.conf")));
mongoResource = new MongoDbResource("localhost");
mongoResource.start();
mongoClient = provideClientWrapper();
Expand Down

0 comments on commit 059b1d2

Please sign in to comment.