com.helion3.prism.storage.mongodb.MongoRecords.java Source code

Java tutorial

Introduction

Here is the source code for com.helion3.prism.storage.mongodb.MongoRecords.java

Source

/**
 * This file is part of Prism, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2015 Helion3 http://helion3.com/
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.helion3.prism.storage.mongodb;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import com.helion3.prism.api.records.Result;
import com.helion3.prism.util.Format;
import org.bson.Document;
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.DataQuery;
import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.MemoryDataContainer;

import com.google.common.collect.Range;
import com.helion3.prism.Prism;
import com.helion3.prism.api.query.FieldCondition;
import com.helion3.prism.api.query.Condition;
import com.helion3.prism.api.query.ConditionGroup;
import com.helion3.prism.api.query.MatchRule;
import com.helion3.prism.api.query.Query;
import com.helion3.prism.api.query.QuerySession;
import com.helion3.prism.api.query.ConditionGroup.Operator;
import com.helion3.prism.api.storage.StorageAdapterRecords;
import com.helion3.prism.api.storage.StorageDeleteResult;
import com.helion3.prism.api.storage.StorageWriteResult;
import com.helion3.prism.util.DataQueries;
import com.helion3.prism.util.DataUtil;
import com.helion3.prism.util.DateUtil;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.BulkWriteOptions;
import com.mongodb.client.model.InsertOneModel;
import com.mongodb.client.model.WriteModel;

public class MongoRecords implements StorageAdapterRecords {
    private final BulkWriteOptions bulkWriteOptions = new BulkWriteOptions().ordered(false);
    private final String expiration = Prism.getConfig().getNode("storage", "expireRecords").getString();

    /**
     * Converts a DataView to a Document, recursively if needed.
     *
     * @param view Data view/container.
     * @return Document for Mongo storage.
     */
    private Document documentFromView(DataView view) {
        Document document = new Document();

        Set<DataQuery> keys = view.getKeys(false);
        for (DataQuery query : keys) {
            Optional<Object> optional = view.get(query);
            if (optional.isPresent()) {
                String key = query.asString(".");

                if (optional.get() instanceof List) {
                    List<Object> convertedList = new ArrayList<Object>();
                    List<?> list = (List<?>) optional.get();
                    Iterator<?> iterator = list.iterator();

                    while (iterator.hasNext()) {
                        Object object = iterator.next();

                        if (object instanceof DataView) {
                            convertedList.add(documentFromView((DataView) object));
                        } else if (object.getClass().isEnum()) {
                            // Ignoring, this data should exist elsewhere in the document.
                            // this is ConnectedDirections and other vanilla manipulators
                            //convertedList.add(object.toString());
                        } else if (DataUtil.isPrimitiveType(object)) {
                            convertedList.add(optional.get());
                            break;
                        } else {
                            Prism.getLogger().error("Unsupported list data type: " + object.getClass().getName());
                        }
                    }

                    if (!convertedList.isEmpty()) {
                        document.append(key, convertedList);
                    }
                } else if (optional.get() instanceof DataView) {
                    DataView subView = (DataView) optional.get();
                    document.append(key, documentFromView(subView));
                } else {
                    if (key.equals(DataQueries.Player.toString())) {
                        document.append(DataQueries.Player.toString(), optional.get());
                    } else {
                        document.append(key, optional.get());
                    }
                }
            }
        }

        return document;
    }

    /**
     * Convert a mongo Document to a DataContainer.
     * @param document Mongo document.
     * @return Data container.
     */
    private DataContainer documentToDataContainer(Document document) {
        DataContainer result = new MemoryDataContainer();

        for (String key : document.keySet()) {
            DataQuery keyQuery = DataQuery.of(key);
            Object object = document.get(key);

            if (object instanceof Document) {
                result.set(keyQuery, documentToDataContainer((Document) object));
            } else {
                result.set(keyQuery, object);
            }
        }

        return result;
    }

    @Override
    public StorageWriteResult write(List<DataContainer> containers) throws Exception {
        MongoCollection<Document> collection = MongoStorageAdapter
                .getCollection(MongoStorageAdapter.collectionEventRecordsName);

        // Build an array of documents
        List<WriteModel<Document>> documents = new ArrayList<WriteModel<Document>>();
        for (DataContainer container : containers) {
            Document document = documentFromView(container);

            //Prism.getLogger().debug(DataUtil.jsonFromDataView(container).toString());

            // TTL
            document.append("Expires", DateUtil.parseTimeStringToDate(expiration, true));

            // Insert
            documents.add(new InsertOneModel<Document>(document));
        }

        // Write
        collection.bulkWrite(documents, bulkWriteOptions);

        // @todo implement real results, BulkWriteResult

        return new StorageWriteResult();
    }

    /**
     * Recursive method of building condition documents.
     *
     * @param fieldsOrGroups List<Condition>
     * @return Document
     */
    private Document buildConditions(List<Condition> fieldsOrGroups) {
        Document conditions = new Document();

        for (Condition fieldOrGroup : fieldsOrGroups) {
            if (fieldOrGroup instanceof ConditionGroup) {
                ConditionGroup group = (ConditionGroup) fieldOrGroup;
                Document subdoc = buildConditions(group.getConditions());

                if (group.getOperator().equals(Operator.OR)) {
                    conditions.append("$or", subdoc);
                } else {
                    conditions.putAll(subdoc);
                }
            } else {
                FieldCondition field = (FieldCondition) fieldOrGroup;

                // Match an array of items
                if (field.getValue() instanceof List) {
                    String matchRule = field.getMatchRule().equals(MatchRule.INCLUDES) ? "$in" : "$nin";
                    conditions.put(field.getFieldName().toString(), new Document(matchRule, field.getValue()));
                }

                else if (field.getMatchRule().equals(MatchRule.EQUALS)) {
                    conditions.put(field.getFieldName().toString(), field.getValue());
                }

                else if (field.getMatchRule().equals(MatchRule.GREATER_THAN_EQUAL)) {
                    conditions.put(field.getFieldName().toString(), new Document("$gte", field.getValue()));
                }

                else if (field.getMatchRule().equals(MatchRule.LESS_THAN_EQUAL)) {
                    conditions.put(field.getFieldName().toString(), new Document("$lte", field.getValue()));
                }

                else if (field.getMatchRule().equals(MatchRule.BETWEEN)) {
                    if (!(field.getValue() instanceof Range)) {
                        throw new IllegalArgumentException("\"Between\" match value must be a Range.");
                    }

                    Range<?> range = (Range<?>) field.getValue();

                    Document between = new Document("$gte", range.lowerEndpoint()).append("$lte",
                            range.upperEndpoint());
                    conditions.put(field.getFieldName().toString(), between);
                }
            }
        }

        return conditions;
    }

    @Override
    public CompletableFuture<List<Result>> query(QuerySession session, boolean translate) throws Exception {
        Query query = session.getQuery();
        checkNotNull(query);

        // Prepare results
        List<Result> results = new ArrayList<Result>();
        CompletableFuture<List<Result>> future = new CompletableFuture<List<Result>>();

        // Get collection
        MongoCollection<Document> collection = MongoStorageAdapter
                .getCollection(MongoStorageAdapter.collectionEventRecordsName);

        // Append all conditions
        Document matcher = new Document("$match", buildConditions(query.getConditions()));

        // Session configs
        int sortDir = 1; // @todo needs implementation
        boolean shouldGroup = query.isAggregate();

        // Sorting
        Document sortFields = new Document();
        sortFields.put(DataQueries.Created.toString(), sortDir);
        sortFields.put(DataQueries.Y.toString(), 1);
        sortFields.put(DataQueries.X.toString(), 1);
        sortFields.put(DataQueries.Z.toString(), 1);
        Document sorter = new Document("$sort", sortFields);

        // Offset/Limit
        Document limit = new Document("$limit", query.getLimit());

        // Build aggregators
        AggregateIterable<Document> aggregated = null;
        if (shouldGroup) {
            // Grouping fields
            Document groupFields = new Document();
            groupFields.put(DataQueries.EventName.toString(), "$" + DataQueries.EventName);
            groupFields.put(DataQueries.Player.toString(), "$" + DataQueries.Player);
            groupFields.put(DataQueries.Cause.toString(), "$" + DataQueries.Cause);
            groupFields.put(DataQueries.Target.toString(), "$" + DataQueries.Target);
            // Entity
            groupFields.put(DataQueries.Entity.toString(), "$" + DataQueries.Entity.then(DataQueries.EntityType));
            // Day
            groupFields.put("dayOfMonth", new Document("$dayOfMonth", "$" + DataQueries.Created));
            groupFields.put("month", new Document("$month", "$" + DataQueries.Created));
            groupFields.put("year", new Document("$year", "$" + DataQueries.Created));

            Document groupHolder = new Document("_id", groupFields);
            groupHolder.put(DataQueries.Count.toString(), new Document("$sum", 1));

            Document group = new Document("$group", groupHolder);

            // Aggregation pipeline
            List<Document> pipeline = new ArrayList<Document>();
            pipeline.add(matcher);
            pipeline.add(group);
            pipeline.add(sorter);
            pipeline.add(limit);

            aggregated = collection.aggregate(pipeline);
            Prism.getLogger().debug("MongoDB Query: " + pipeline);
        } else {
            // Aggregation pipeline
            List<Document> pipeline = new ArrayList<Document>();
            pipeline.add(matcher);
            pipeline.add(sorter);
            pipeline.add(limit);

            aggregated = collection.aggregate(pipeline);
            Prism.getLogger().debug("MongoDB Query: " + pipeline);
        }

        session.getCommandSource().get()
                .sendMessage(Format.subduedHeading("Query completed, building snapshots..."));

        // Iterate results and build our event record list
        MongoCursor<Document> cursor = aggregated.iterator();
        try {
            List<UUID> uuidsPendingLookup = new ArrayList<UUID>();

            while (cursor.hasNext()) {
                // Mongo document
                Document wrapper = cursor.next();
                Document document = shouldGroup ? (Document) wrapper.get("_id") : wrapper;

                DataContainer data = documentToDataContainer(document);

                if (shouldGroup) {
                    data.set(DataQueries.Count, wrapper.get(DataQueries.Count.toString()));
                }

                // Build our result object
                Result result = Result.from(wrapper.getString(DataQueries.EventName.toString()),
                        session.getQuery().isAggregate());

                // Determine the final name of the event source
                if (document.containsKey(DataQueries.Player.toString())) {
                    String uuid = document.getString(DataQueries.Player.toString());
                    data.set(DataQueries.Cause, uuid);

                    if (translate) {
                        uuidsPendingLookup.add(UUID.fromString(uuid));
                    }
                } else {
                    data.set(DataQueries.Cause, document.getString(DataQueries.Cause.toString()));
                }

                result.data = data;
                results.add(result);
            }

            if (translate && !uuidsPendingLookup.isEmpty()) {
                DataUtil.translateUuidsToNames(results, uuidsPendingLookup).thenAccept(finalResults -> {
                    future.complete(finalResults);
                });
            } else {
                future.complete(results);
            }
        } finally {
            cursor.close();
        }

        return future;
    }

    /**
     * Given a list of parameters, will remove all matching records.
     *
     * @param query Query conditions indicating what we're purging
     * @return
     */
    // @todo implement
    @Override
    public StorageDeleteResult delete(Query query) {
        return new StorageDeleteResult();
    }
}