Skip to content

Commit

Permalink
Add ScopedEntityMongoUtils (#19396)
Browse files Browse the repository at this point in the history
* Add ScopedEntityMongoUtils
* Add idsIn unit test
* Unit tests for ScopedEntityMongoUtils
  • Loading branch information
kingzacko1 committed May 23, 2024
1 parent 3d4dc80 commit 6f3c67f
Show file tree
Hide file tree
Showing 7 changed files with 450 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.graylog2.bindings.providers.MongoJackObjectMapperProvider;
import org.graylog2.database.entities.EntityScopeService;
import org.graylog2.database.entities.ScopedEntity;
import org.graylog2.database.jackson.CustomJacksonCodecRegistry;
import org.graylog2.database.pagination.DefaultMongoPaginationHelper;
import org.graylog2.database.pagination.MongoPaginationHelper;
import org.graylog2.database.utils.MongoUtils;
import org.graylog2.database.utils.ScopedEntityMongoUtils;

@Singleton
public class MongoCollections {
Expand Down Expand Up @@ -93,4 +96,11 @@ public <T extends MongoEntity> MongoUtils<T> utils(String collectionName, Class<
public <T extends MongoEntity> MongoUtils<T> utils(MongoCollection<T> collection) {
return new MongoUtils<>(collection, objectMapper);
}

/**
* Provides utility methods for creating, updating, and deleting ScopedEntity objects
*/
public <T extends ScopedEntity> ScopedEntityMongoUtils<T> scopedEntityUtils(MongoCollection<T> collection, EntityScopeService entityScopeService) {
return new ScopedEntityMongoUtils<>(collection, entityScopeService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,18 @@
package org.graylog2.database.entities;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.graylog2.database.MongoEntity;
import org.mongojack.Id;
import org.mongojack.ObjectId;

import javax.annotation.Nullable;

/**
* Entity base class, which can be used to enforce that each entity implementation
* has the required id and _scope fields.
*/

public abstract class ScopedEntity {
public abstract class ScopedEntity implements MongoEntity {
public static final String FIELD_ID = "id";
public static final String FIELD_SCOPE = "_scope";

@Id
@ObjectId
@Nullable
@JsonProperty(FIELD_ID)
public abstract String id();

@JsonProperty(FIELD_SCOPE)
public abstract String scope();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import org.graylog2.database.jackson.CustomJacksonCodecRegistry;
import org.mongojack.InitializationRequiredForTransformation;

import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand Down Expand Up @@ -99,6 +101,26 @@ public static Bson idEq(@Nonnull ObjectId id) {
return Filters.eq("_id", id);
}

/**
* Create a query constraint to match a document's ID against the given list of Hex strings.
*
* @param ids Collection of hex string representations of an {@link ObjectId}
* @return An 'in' filter.
*/
public static Bson stringIdsIn(Collection<String> ids) {
return idsIn(ids.stream().map(ObjectId::new).collect(Collectors.toSet()));
}

/**
* Create a query constraint to match a document's ID against a list of IDs.
*
* @param ids IDs to match
* @return An 'in' filter.
*/
public static Bson idsIn(Collection<ObjectId> ids) {
return Filters.in("_id", ids);
}

/**
* Create a stream of entries from the given {@link MongoIterable}. Using this method will create a stream that
* properly closes the underlying MongoDB cursor when the stream is closed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.database.utils;

import com.mongodb.client.MongoCollection;
import org.bson.types.ObjectId;
import org.graylog2.database.entities.EntityScopeService;
import org.graylog2.database.entities.ScopedEntity;

import java.util.Objects;
import java.util.Optional;

import static org.graylog2.database.utils.MongoUtils.idEq;
import static org.graylog2.database.utils.MongoUtils.insertedIdAsString;

public class ScopedEntityMongoUtils<T extends ScopedEntity> {
private final MongoCollection<T> collection;
private final EntityScopeService entityScopeService;

public ScopedEntityMongoUtils(MongoCollection<T> delegate,
EntityScopeService entityScopeService) {
this.collection = delegate;
this.entityScopeService = entityScopeService;
}

/**
* Performs a valid scope and mutability check before updating an existing entity.
*
* @param entity ScopedEntity to be updated
* @return the newly updated entity
*/
public T update(T entity) {
Objects.requireNonNull(entity.id());
ensureValidScope(entity);
ensureMutability(entity);
collection.replaceOne(idEq(Objects.requireNonNull(entity.id())), entity);
return entity;
}

/**
* Performs a valid scope check before inserting the entity into the DB.
*
* @param entity ScopedEntity to be created
* @return the ID of the newly created ScopedEntity
*/
public String create(T entity) {
ensureValidScope(entity);
return insertedIdAsString(collection.insertOne(entity));
}

/**
* Convenience method to delete a single document identified by its ID after performing mutability checks.
*
* @param id Hex string representation of the document's {@link ObjectId}.
* @return true if a document was deleted, false otherwise.
*/
public boolean deleteById(String id) {
return deleteById(new ObjectId(id));
}

/**
* Convenience method to delete a single document identified by its ID after performing mutability checks.
*
* @param id the document's id.
* @return true if a document was deleted, false otherwise.
*/
public boolean deleteById(ObjectId id) {
final T entity = Optional.ofNullable(collection.find(idEq(id)).first())
.orElseThrow(() -> new IllegalArgumentException("Entity not found"));
ensureDeletability(entity);
ensureMutability(entity);
return collection.deleteOne(idEq(id)).getDeletedCount() > 0;
}

/**
* Deletes an entity without checking for deletability. Do not call this method for API requests for the user
* interface.
*
* @param id ID of the ScopedEntity to be deleted
*/
public final long forceDelete(String id) {
// Intentionally omit ensure mutability check.
return collection.deleteOne(idEq(id)).getDeletedCount();
}

public final boolean isMutable(T scopedEntity) {
Objects.requireNonNull(scopedEntity, "Entity must not be null");

// First, check whether this entity has been persisted, if so, the persisted entity's scope takes precedence.
// Else, the entity does not exist in the database, This could be a new entity--check it
Optional<T> current = scopedEntity.id() == null ? Optional.empty()
: Optional.ofNullable(collection.find(idEq(scopedEntity.id())).first());
return current
.map(t -> entityScopeService.isMutable(t, scopedEntity))
.orElseGet(() -> entityScopeService.isMutable(scopedEntity));
}

public final boolean isDeletable(T scopedEntity) {
Objects.requireNonNull(scopedEntity, "Entity must not be null");

// First, check whether this entity has been persisted, if so, the persisted entity's scope takes precedence.
// Else, the entity does not exist in the database, This could be a new entity--check it
Optional<T> current = scopedEntity.id() == null ? Optional.empty()
: Optional.ofNullable(collection.find(idEq(scopedEntity.id())).first());
return current
.map(entityScopeService::isDeletable)
.orElseGet(() -> entityScopeService.isDeletable(scopedEntity));
}

public final void ensureValidScope(T entity) {
if (!entityScopeService.hasValidScope(entity)) {
throw new IllegalArgumentException("Invalid Entity Scope: " + entity.scope());
}
}

public final void ensureMutability(T entity) {
if (!isMutable(entity)) {
throw new IllegalArgumentException("Immutable entity cannot be modified");
}
}

public final void ensureDeletability(T entity) {
if (!isDeletable(entity)) {
throw new IllegalArgumentException("Non-deletable entity cannot be deleted");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.List;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.graylog2.database.utils.MongoUtils.idEq;
import static org.graylog2.database.utils.MongoUtils.insertedId;
import static org.graylog2.database.utils.MongoUtils.insertedIdAsString;
import static org.graylog2.database.utils.MongoUtils.stringIdsIn;

@ExtendWith(MongoDBExtension.class)
@ExtendWith(MongoJackExtension.class)
Expand Down Expand Up @@ -92,4 +94,33 @@ void testIdEq() {
assertThat(collection.find(idEq(b.id())).first()).isEqualTo(b);
assertThat(collection.find(idEq(new ObjectId(b.id()))).first()).isEqualTo(b);
}

@Test
void testIdsIn() {
final String missingId1 = "6627add0ee216425dd6df36a";
final String missingId2 = "6627add0ee216425dd6df36b";
final String idA = "6627add0ee216425dd6df37a";
final String idB = "6627add0ee216425dd6df37b";
final String idC = "6627add0ee216425dd6df37c";
final String idD = "6627add0ee216425dd6df37d";
final String idE = "6627add0ee216425dd6df37e";
final String idF = "6627add0ee216425dd6df37f";
final var a = new DTO(idA, "a");
final var b = new DTO(idB, "b");
final var c = new DTO(idC, "c");
final var d = new DTO(idD, "d");
final var e = new DTO(idE, "e");
final var f = new DTO(idF, "f");
collection.insertMany(List.of(a, b, c, d, e, f));

assertThat(collection.find(stringIdsIn(Set.of(idA, idF)))).contains(a, f);
assertThat(collection.find(stringIdsIn(Set.of(idA, idF)))).hasSize(2);
assertThat(collection.find(stringIdsIn(Set.of(missingId1, missingId2)))).hasSize(0);
assertThat(collection.find(stringIdsIn(Set.of(idA, idB, missingId1, missingId2)))).contains(a, b);
assertThat(collection.find(stringIdsIn(Set.of(idA, idB, missingId1, missingId2)))).hasSize(2);
assertThat(collection.find(stringIdsIn(Set.of(idA, idB, idC, idD, idE, idF)))).hasSize(6);
assertThat(collection.find(stringIdsIn(Set.of(idA, idB, idC, idD, idE, idF)))).contains(a, b, c, d, e, f);
assertThat(collection.find(stringIdsIn(Set.of(idA, idB, idC, idD, idE, idF, missingId1, missingId2)))).hasSize(6);
assertThat(collection.find(stringIdsIn(Set.of(idA, idB, idC, idD, idE, idF, missingId1, missingId2)))).contains(a, b, c, d, e, f);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.database.utils;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.value.AutoValue;
import org.graylog2.database.entities.ScopedEntity;
import org.mongojack.Id;
import org.mongojack.ObjectId;

@AutoValue
@JsonAutoDetect
@JsonDeserialize(builder = ScopedDTO.Builder.class)
public abstract class ScopedDTO extends ScopedEntity {

@JsonProperty("name")
public abstract String name();

public abstract Builder toBuilder();

public static Builder builder() {
return Builder.create();
}

@AutoValue.Builder
public abstract static class Builder extends AbstractBuilder<Builder> {
@Override
@Id
@ObjectId
@JsonProperty("id")
public abstract Builder id(String id);

@Override
@JsonProperty("_scope")
public abstract Builder scope(String scope);

@JsonProperty("name")
public abstract Builder name(String name);

@JsonCreator
public static ScopedDTO.Builder create() {
return new AutoValue_ScopedDTO.Builder();
}

public abstract ScopedDTO build();
}

}
Loading

0 comments on commit 6f3c67f

Please sign in to comment.