Skip to content

Commit

Permalink
Add PaginatedDbService class (#4668)
Browse files Browse the repository at this point in the history
The `PaginatedDbService` class can be used as base class for MongoJack persistence services.
It provides implementations for CRUD actions, paginated searches and stream queries.

`PaginatedList` has been moved from the rest models to the database package because
`PaginatedDbService` is using it now.

Refs #4360
  • Loading branch information
bernd authored and joschi committed Mar 20, 2018
1 parent 4ad4588 commit c463959
Show file tree
Hide file tree
Showing 8 changed files with 625 additions and 60 deletions.
@@ -0,0 +1,235 @@
/**
* This file is part of Graylog.
*
* Graylog is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog2.database;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import org.bson.types.ObjectId;
import org.graylog2.bindings.providers.MongoJackObjectMapperProvider;
import org.mongojack.DBCursor;
import org.mongojack.DBQuery;
import org.mongojack.DBSort;
import org.mongojack.JacksonDBCollection;
import org.mongojack.WriteResult;

import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* This class is a helper to implement a basic Mongojack-based database service that allows CRUD operations on a
* single DTO type and offers paginated access.
* <p>
* It makes only a few assumptions, which are common to many Graylog entities:
* <ul>
* <li>The DTO class has a name which is unique</li>
* </ul>
* </p>
* <p>
* Subclasses can add more sophisticated query methods by access the protected "db" property.<br/>
* Indices can be added in the constructor.
* </p>
* @param <DTO>
*/
public abstract class PaginatedDbService<DTO> {
protected final JacksonDBCollection<DTO, ObjectId> db;

protected PaginatedDbService(MongoConnection mongoConnection,
MongoJackObjectMapperProvider mapper,
Class<DTO> dtoClass,
String collectionName) {
this.db = JacksonDBCollection.wrap(mongoConnection.getDatabase().getCollection(collectionName),
dtoClass,
ObjectId.class,
mapper.get());
}

/**
* Get the {@link DTO} for the given ID.
*
* @param id the ID of the object
* @return an Optional containing the found object or an empty Optional if no object can be found for the given ID
*/
public Optional<DTO> get(String id) {
return Optional.ofNullable(db.findOneById(new ObjectId(id)));
}

/**
* Stores the given {@link DTO} in the database.
*
* @param dto the {@link DTO} to save
* @return the newly saved {@link DTO}
*/
public DTO save(DTO dto) {
final WriteResult<DTO, ObjectId> save = db.save(dto);
return save.getSavedObject();
}

/**
* Deletes the {@link DTO} for the given ID from the database.
*
* @param id ID of the {@link DTO} to delete
* @return the number of deleted documents
*/
public int delete(String id) {
return db.removeById(new ObjectId(id)).getN();
}

/**
* Returns a {@link PaginatedList<DTO>} for the given query and pagination parameters.
* <p>
* This method is only accessible by subclasses to avoid exposure of the {@link DBQuery} and {@link DBSort}
* interfaces to consumers.
*
* @param query the query to execute
* @param sort the sort builder for the query
* @param page the page number that should be returned
* @param perPage the number of entries per page
* @return the paginated list
*/
protected PaginatedList<DTO> findPaginatedWithQueryAndSort(DBQuery.Query query, DBSort.SortBuilder sort, int page, int perPage) {
try (final DBCursor<DTO> cursor = db.find(query)
.sort(sort)
.limit(perPage)
.skip(perPage * Math.max(0, page - 1))) {
return new PaginatedList<>(asImmutableList(cursor), cursor.count(), page, perPage);
}
}

private ImmutableList<DTO> asImmutableList(Iterator<? extends DTO> cursor) {
return ImmutableList.copyOf(cursor);
}

/**
* Returns a {@link PaginatedList<DTO>} for the given query, filter and pagination parameters.
* <p>
* Since the database cannot execute the filter function directly, this method streams over the result cursor
* and executes the filter function for each database object. <strong>This increases memory consumption and should only be
* used if necessary.</strong> Use the
* {@link PaginatedDbService#findPaginatedWithQueryFilterAndSort(DBQuery.Query, Predicate, DBSort.SortBuilder, int, int) #findPaginatedWithQueryAndSort()}
* method if possible.
* <p>
* This method is only accessible by subclasses to avoid exposure of the {@link DBQuery} and {@link DBSort}
* interfaces to consumers.
*
* @param query the query to execute
* @param filter the filter to apply to each database entry
* @param sort the sort builder for the query
* @param page the page number that should be returned
* @param perPage the number of entries per page
* @return the paginated list
*/
protected PaginatedList<DTO> findPaginatedWithQueryFilterAndSort(DBQuery.Query query,
Predicate<DTO> filter,
DBSort.SortBuilder sort,
int page,
int perPage) {
final AtomicInteger total = new AtomicInteger(0);

try (final Stream<DTO> cursor = streamQueryWithSort(query, sort)) {
// First created a filtered stream
final Stream<DTO> dtoStream = cursor
.filter(filter)
.peek(dto -> total.incrementAndGet());

// Then use that filtered stream and only collect the entries according to page and perPage
final List<DTO> list = Stream.of(dtoStream)
.flatMap(stream -> stream)
.skip(perPage * Math.max(0, page - 1))
.limit(perPage)
.collect(Collectors.toList());

return new PaginatedList<>(list, total.get(), page, perPage);
}
}

/**
* Returns an unordered stream of all entries in the database.
* <p>
* The returned stream needs to be closed to free the underlying database resources.
*
* @return stream of all database entries
*/
public Stream<DTO> streamAll() {
return streamQuery(DBQuery.empty());
}

/**
* Returns an unordered stream of all entries in the database for the given IDs.
* <p>
* The returned stream needs to be closed to free the underlying database resources.
*
* @param idSet set of IDs to query
* @return stream of database entries for the given IDs
*/
public Stream<DTO> streamByIds(Set<String> idSet) {
final List<ObjectId> objectIds = idSet.stream()
.map(ObjectId::new)
.collect(Collectors.toList());

return streamQuery(DBQuery.in("_id", objectIds));
}

/**
* Returns an unordered stream of database entries for the given {@link DBQuery.Query}.
* <p>
* The returned stream needs to be closed to free the underlying database resources.
*
* @param query the query to execute
* @return stream of database entries that match the query
*/
protected Stream<DTO> streamQuery(DBQuery.Query query) {
final DBCursor<DTO> cursor = db.find(query);
return Streams.stream((Iterable<DTO>) cursor).onClose(cursor::close);
}

/**
* Returns a stream of database entries for the given {@link DBQuery.Query} sorted by the give {@link DBSort.SortBuilder}.
* <p>
* The returned stream needs to be closed to free the underlying database resources.
*
* @param query the query to execute
* @param sort the sort order for the query
* @return stream of database entries that match the query
*/
protected Stream<DTO> streamQueryWithSort(DBQuery.Query query, DBSort.SortBuilder sort) {
final DBCursor<DTO> cursor = db.find(query).sort(sort);
return Streams.stream((Iterable<DTO>) cursor).onClose(cursor::close);
}

/**
* Returns a sort builder for the given order and field name.
*
* @param order the order. either "asc" or "desc"
* @param field the field to sort on
* @return the sort builder
*/
protected DBSort.SortBuilder getSortBuilder(String order, String field) {
DBSort.SortBuilder sortBuilder;
if ("desc".equalsIgnoreCase(order)) {
sortBuilder = DBSort.desc(field);
} else {
sortBuilder = DBSort.asc(field);
}
return sortBuilder;
}
}
Expand Up @@ -14,29 +14,29 @@
* You should have received a copy of the GNU General Public License
* along with Graylog. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog2.rest.models;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ForwardingList;
package org.graylog2.database;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ForwardingList;
import com.google.common.collect.ImmutableMap;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import javax.annotation.Nonnull;

public class PaginatedList<E> extends ForwardingList<E> {

private final List<E> delegate;

private final PaginationInfo paginationInfo;

public PaginatedList(@Nonnull List<E> delegate, int globalTotal, int page, int perPage) {
public PaginatedList(@Nonnull List<E> delegate, int total, int page, int perPage) {
this.delegate = delegate;
this.paginationInfo = new PaginationInfo(globalTotal, page, perPage);
this.paginationInfo = PaginationInfo.create(total, delegate.size(), page, perPage);
}

@Override
Expand Down Expand Up @@ -79,49 +79,31 @@ public static <T> PaginatedList<T> singleton(T entry, int page, int perPage) {
}

@JsonAutoDetect
public class PaginationInfo {
@AutoValue
public static abstract class PaginationInfo {
@JsonProperty("total")
private final int globalTotal;
public abstract int total();

@JsonProperty("count")
public abstract int count();

@JsonProperty("page")
private final int page;
public abstract int page();

@JsonProperty("per_page")
private final int perPage;

public PaginationInfo(int globalTotal, int page, int perPage) {
this.globalTotal = globalTotal;
this.page = page;
this.perPage = perPage;
}

@JsonProperty("count")
public int getCount() {
return delegate().size();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PaginatedList.PaginationInfo)) return false;
PaginationInfo that = (PaginationInfo) o;
return globalTotal == that.globalTotal &&
page == that.page &&
perPage == that.perPage;
}
public abstract int perPage();

@Override
public int hashCode() {
return Objects.hash(globalTotal, page, perPage);
public static PaginationInfo create(int total, int count, int page, int perPage) {
return new AutoValue_PaginatedList_PaginationInfo(total, count, page, perPage);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("globalTotal", globalTotal)
.add("page", page)
.add("perPage", perPage)
.toString();
public ImmutableMap<String, Object> asMap() {
return ImmutableMap.of(
"total", total(),
"page", page(),
"per_page", perPage(),
"count", count()
);
}
}
}
Expand Up @@ -17,30 +17,27 @@
package org.graylog2.lookup.db;

import com.google.common.collect.ImmutableList;

import com.google.common.collect.Streams;
import com.mongodb.BasicDBObject;

import org.bson.types.ObjectId;
import org.graylog2.bindings.providers.MongoJackObjectMapperProvider;
import org.graylog2.database.MongoConnection;
import org.graylog2.database.PaginatedList;
import org.graylog2.lookup.dto.CacheDto;
import org.graylog2.rest.models.PaginatedList;
import org.mongojack.DBCursor;
import org.mongojack.DBQuery;
import org.mongojack.DBSort;
import org.mongojack.JacksonDBCollection;
import org.mongojack.WriteResult;

import javax.inject.Inject;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;

public class DBCacheService {

private final JacksonDBCollection<CacheDto, ObjectId> db;
Expand Down

0 comments on commit c463959

Please sign in to comment.