package emu.nebula.database; import java.util.List; import java.util.stream.Stream; import emu.nebula.Config.DatabaseInfo; import emu.nebula.Config.InternalMongoInfo; import emu.nebula.Nebula; import emu.nebula.Nebula.ServerType; import emu.nebula.database.codecs.*; import emu.nebula.util.Utils; import org.bson.codecs.configuration.CodecRegistries; import org.reflections.Reflections; import com.mongodb.MongoCommandException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; import com.mongodb.client.result.DeleteResult; import de.bwaldvogel.mongo.MongoBackend; import de.bwaldvogel.mongo.MongoServer; import de.bwaldvogel.mongo.backend.h2.H2Backend; import de.bwaldvogel.mongo.backend.memory.MemoryBackend; import dev.morphia.*; import dev.morphia.annotations.Entity; import dev.morphia.mapping.MapperOptions; import dev.morphia.query.FindOptions; import dev.morphia.query.Sort; import dev.morphia.query.filters.Filters; import dev.morphia.query.updates.UpdateOperators; import lombok.Getter; @Getter public final class DatabaseManager { @Getter private static MongoServer server; private Datastore datastore; private static final InsertOneOptions INSERT_OPTIONS = new InsertOneOptions(); private static final DeleteOptions DELETE_OPTIONS = new DeleteOptions(); private static final DeleteOptions DELETE_MANY = new DeleteOptions().multi(true); public DatabaseManager(DatabaseInfo info, ServerType type) { // Variables var internalConfig = Nebula.getConfig().getInternalMongoServer(); String connectionString = info.getUri(); // Start local mongo server if (info.isUseInternal()) { if (Utils.isPortOpen(internalConfig.getAddress(), internalConfig.getPort())) { connectionString = startInternalMongoServer(internalConfig); Nebula.getLogger().info("Started local MongoDB server at " + server.getConnectionString()); } else { Nebula.getLogger().warn("Local MongoDB server could not be created because the port is in use."); } } // Initialize MongoClient mongoClient = MongoClients.create(connectionString); // Add our custom fastutil codecs var codecProvider = CodecRegistries.fromCodecs( new IntSetCodec(), new IntListCodec(), new Int2IntMapCodec(), new ItemParamMapCodec(), new String2IntMapCodec(), new BitsetCodec() ); // Set mapper options MapperOptions mapperOptions = MapperOptions.builder() .storeEmpties(true) .storeNulls(false) .codecProvider(codecProvider) .build(); // Create data store. datastore = Morphia.createDatastore(mongoClient, info.getCollection(), mapperOptions); // Map classes var entities = new Reflections(Nebula.class.getPackageName()) .getTypesAnnotatedWith(Entity.class) .stream() .filter(cls -> { Entity e = cls.getAnnotation(Entity.class); return e != null; }) .toList(); if (type.runLogin()) { // Only map account related entities var map = entities.stream().filter(cls -> { return cls.getAnnotation(AccountDatabaseOnly.class) != null; }).toArray(Class[]::new); datastore.getMapper().map(map); } if (type.runGame()) { // Only map game related entities var map = entities.stream().filter(cls -> { return cls.getAnnotation(AccountDatabaseOnly.class) == null; }).toArray(Class[]::new); datastore.getMapper().map(map); } // Ensure indexes ensureIndexes(); // Done Nebula.getLogger().info("Connected to the MongoDB database at " + connectionString); } public MongoDatabase getDatabase() { return getDatastore().getDatabase(); } private void ensureIndexes() { try { datastore.ensureIndexes(); } catch (MongoCommandException exception) { Nebula.getLogger().warn("Mongo index error: ", exception); // Duplicate index error if (exception.getCode() == 85) { // Drop all indexes and re add them MongoIterable collections = datastore.getDatabase().listCollectionNames(); for (String name : collections) { datastore.getDatabase().getCollection(name).dropIndexes(); } // Add back indexes datastore.ensureIndexes(); } } } // Database Functions public boolean checkIfObjectExists(Class cls, String filter, String value) { return getDatastore().find(cls).filter(Filters.eq(filter, value)).count() > 0; } public T getObjectByUid(Class cls, long uid) { return getDatastore().find(cls).filter(Filters.eq("_id", uid)).first(); } public T getObjectByField(Class cls, String filter, Object value) { return getDatastore().find(cls).filter(Filters.eq(filter, value)).first(); } public T getObjectByField(Class cls, String filter, long value) { return getDatastore().find(cls).filter(Filters.eq(filter, value)).first(); } public Stream getObjects(Class cls, String filter, Object value) { return getDatastore().find(cls).filter(Filters.eq(filter, value)).stream(); } public Stream getObjects(Class cls, String filter, long value) { return getDatastore().find(cls).filter(Filters.eq(filter, value)).stream(); } public Stream getObjects(Class cls) { return getDatastore().find(cls).stream(); } public List getSortedObjects(Class cls, String filter, int value, String sortBy, int limit) { var options = new FindOptions() .sort(Sort.descending(sortBy)) .limit(limit); return getDatastore() .find(cls) .filter(Filters.eq(filter, value)) .iterator(options) .toList(); } public void save(T obj) { getDatastore().save(obj, INSERT_OPTIONS); } public boolean delete(T obj) { DeleteResult result = getDatastore().delete(obj, DELETE_OPTIONS); return result.getDeletedCount() > 0; } public boolean delete(Class cls, String filter, long uid) { DeleteResult result = getDatastore().find(cls).filter(Filters.eq(filter, uid)).delete(DELETE_MANY); return result.getDeletedCount() > 0; } public void update(Object obj, int uid, String field, Object item) { update(obj, uid, field, item, false); } public void update(Object obj, int uid, String field, Object value, boolean upsert) { var opt = new UpdateOptions().upsert(upsert); getDatastore().find(obj.getClass()) .filter(Filters.eq("_id", uid)) .update(opt, UpdateOperators.set(field, value)); } // TODO optimize to not require 2 db calls public void update(Object obj, int uid, String field, Object value, String field2, Object value2) { /* getDatastore().find(obj.getClass()) .filter(Filters.eq("_id", uid)) .update(UpdateOperators.set(field, value), UpdateOperators.set(field2, value2)); */ update(obj, uid, field, value); update(obj, uid, field2, value2); } public void updateNested(Object obj, int uid, String filter, int filterId, String field, Object item) { var opt = new UpdateOptions().upsert(false); getDatastore().find(obj.getClass()) .filter(Filters.eq("_id", uid)) .filter(Filters.eq(filter, filterId)) .update(opt, UpdateOperators.set(field, item)); } public void addToSet(Object obj, int uid, String field, Object item) { var opt = new UpdateOptions().upsert(false); getDatastore().find(obj.getClass()) .filter(Filters.eq("_id", uid)) .update(opt, UpdateOperators.addToSet(field, item)); } // Database counter public synchronized int getNextObjectId(Class c) { DatabaseCounter counter = getDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first(); if (counter == null) { counter = new DatabaseCounter(c.getSimpleName()); } try { return counter.getNextId(); } finally { getDatastore().save(counter); } } // Internal MongoDB server public static String startInternalMongoServer(InternalMongoInfo internalMongo) { // Get backend MongoBackend backend = null; if (internalMongo.filePath != null && internalMongo.filePath.length() > 0) { backend = new H2Backend(internalMongo.filePath); } else { backend = new MemoryBackend(); } // Create the local mongo server and replace the connection string server = new MongoServer(backend); // Bind to address of it exists if (internalMongo.getAddress() != null && internalMongo.getPort() != 0) { server.bind(internalMongo.getAddress(), internalMongo.getPort()); } else { server.bind(); // Binds to random port } return server.getConnectionString(); } }