diff --git a/jooby-mongodb/pom.xml b/jooby-mongodb/pom.xml index 59c14b4177..f89839cdf4 100644 --- a/jooby-mongodb/pom.xml +++ b/jooby-mongodb/pom.xml @@ -85,6 +85,20 @@ test + + org.jooby + jooby-netty + ${project.version} + test + + + + org.jooby + jooby-jackson + ${project.version} + test + + diff --git a/jooby-mongodb/src/main/java/org/jooby/mongodb/MongoSessionStore.java b/jooby-mongodb/src/main/java/org/jooby/mongodb/MongoSessionStore.java index c1cf4be9dc..8d8a91b748 100644 --- a/jooby-mongodb/src/main/java/org/jooby/mongodb/MongoSessionStore.java +++ b/jooby-mongodb/src/main/java/org/jooby/mongodb/MongoSessionStore.java @@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -29,16 +30,19 @@ import javax.inject.Inject; import javax.inject.Named; +import org.bson.Document; +import org.bson.conversions.Bson; import org.jooby.Session; import org.jooby.Session.Builder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.mongodb.BasicDBObject; -import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.DB; -import com.mongodb.DBCollection; -import com.mongodb.MongoException; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.UpdateOptions; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; @@ -104,28 +108,31 @@ */ public class MongoSessionStore implements Session.Store { + private static final String SESSION_IDX = "_sessionIdx_"; + /** The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); - protected final DBCollection sessions; + protected final MongoCollection sessions; - protected final int timeout; + protected final long timeout; protected final String collection; private final AtomicBoolean ttlSync = new AtomicBoolean(false); - protected final DB db; + protected final MongoDatabase db; - public MongoSessionStore(final DB db, final String collection, final int timeout) { + public MongoSessionStore(final MongoDatabase db, final String collection, + final long timeoutInSeconds) { this.db = requireNonNull(db, "Mongo db is required."); this.collection = requireNonNull(collection, "Collection is required."); this.sessions = db.getCollection(collection); - this.timeout = timeout; + this.timeout = timeoutInSeconds; } @Inject - public MongoSessionStore(final DB db, + public MongoSessionStore(final MongoDatabase db, final @Named("mongodb.session.collection") String collection, final @Named("session.timeout") String timeout) { this(db, collection, seconds(timeout)); @@ -134,36 +141,40 @@ public MongoSessionStore(final DB db, @SuppressWarnings({"unchecked", "rawtypes" }) @Override public Session get(final Builder builder) { - return Optional.ofNullable(sessions.findOne(builder.sessionId())).map(dbobj -> { - Map session = dbobj.toMap(); - - Date accessedAt = (Date) session.remove("_accessedAt"); - Date createdAt = (Date) session.remove("_createdAt"); - Date savedAt = (Date) session.remove("_savedAt"); - session.remove("_id"); - - return builder - .accessedAt(accessedAt.getTime()) - .createdAt(createdAt.getTime()) - .savedAt(savedAt.getTime()) - .set(session) - .build(); - }).orElse(null); + return Optional.ofNullable(sessions.find(Filters.eq("_id", builder.sessionId())).first()) + .map(doc -> { + Map session = new LinkedHashMap<>(doc); + + Date accessedAt = (Date) session.remove("_accessedAt"); + Date createdAt = (Date) session.remove("_createdAt"); + Date savedAt = (Date) session.remove("_savedAt"); + session.remove("_id"); + + return builder + .accessedAt(accessedAt.getTime()) + .createdAt(createdAt.getTime()) + .savedAt(savedAt.getTime()) + .set(session) + .build(); + }).orElse(null); } @Override public void save(final Session session) { syncTtl(); - BasicDBObjectBuilder ob = BasicDBObjectBuilder.start() - .add("_id", session.id()) - .add("_accessedAt", new Date(session.accessedAt())) - .add("_createdAt", new Date(session.createdAt())) - .add("_savedAt", new Date(session.savedAt())); + String id = session.id(); + Bson filter = Filters.eq("_id", id); + + Document doc = new Document() + .append("_id", id) + .append("_accessedAt", new Date(session.accessedAt())) + .append("_createdAt", new Date(session.createdAt())) + .append("_savedAt", new Date(session.savedAt())); // dump attributes - session.attributes().forEach((k, v) -> ob.add(k, v)); + session.attributes().forEach((k, v) -> doc.append(k, v)); - sessions.save(ob.get()); + sessions.updateOne(filter, new Document("$set", doc), new UpdateOptions().upsert(true)); } @Override @@ -179,41 +190,49 @@ private void syncTtl() { return; } - try { - log.debug("creating session timeout index"); + log.debug("creating session timeout index"); + if (existsIdx(SESSION_IDX)) { + Document command = new Document("collMod", collection) + .append("index", + new Document("keyPattern", new Document("_accessedAt", 1)) + .append("expireAfterSeconds", timeout)); + log.debug("{}", command); + Document result = db.runCommand(command); + log.debug("{}", result); + } else { sessions.createIndex( - new BasicDBObject("_accessedAt", 1), - new BasicDBObject("expireAfterSeconds", timeout) - ); - } catch (MongoException ex) { - log.debug("Couldn't update session timeout, we are going to update session timeout", ex); - // TODO: allow to customize? ... not sure - db.command(BasicDBObjectBuilder.start() - .add("collMod", collection) - .add("index", BasicDBObjectBuilder.start() - .add("keyPattern", new BasicDBObject("_accessedAt", 1)) - .add("expireAfterSeconds", timeout) - .get() - ) - .get() - ); + new Document("_accessedAt", 1), + new IndexOptions() + .name(SESSION_IDX) + .expireAfter(timeout, TimeUnit.SECONDS)); } } } @Override public void delete(final String id) { - sessions.remove(new BasicDBObject("_id", id)); + sessions.deleteOne(new Document("_id", id)); } - private static int seconds(final String value) { + private static long seconds(final String value) { try { - return Integer.parseInt(value); + return Long.parseLong(value); } catch (NumberFormatException ex) { Config config = ConfigFactory.empty() .withValue("timeout", ConfigValueFactory.fromAnyRef(value)); - return (int) config.getDuration("timeout", TimeUnit.SECONDS); + return config.getDuration("timeout", TimeUnit.SECONDS); + } + } + + private boolean existsIdx(final String name) { + MongoCursor iterator = sessions.listIndexes().iterator(); + while (iterator.hasNext()) { + Document doc = iterator.next(); + if (doc.getString("name").equals(name)) { + return true; + } } + return false; } } diff --git a/jooby-mongodb/src/test/java/apps/MongodbApp.java b/jooby-mongodb/src/test/java/apps/MongodbApp.java new file mode 100644 index 0000000000..73316a3c93 --- /dev/null +++ b/jooby-mongodb/src/test/java/apps/MongodbApp.java @@ -0,0 +1,40 @@ +package apps; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.jooby.Jooby; +import org.jooby.Session; +import org.jooby.mongodb.MongoSessionStore; +import org.jooby.mongodb.Mongodb; + +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; + +public class MongodbApp extends Jooby { + + { + use(ConfigFactory.empty() + .withValue("db", ConfigValueFactory.fromAnyRef("mongodb://localhost/mongodbapp")) + .withValue("session.timeout", ConfigValueFactory.fromAnyRef("2m"))); + + use(new Mongodb()); + + AtomicInteger inc = new AtomicInteger(0); + session(MongoSessionStore.class); + + get("/", req -> { + Session session = req.ifSession().orElseGet(() -> { + Session newSession = req.session(); + int next = newSession.get("inc").intValue(inc.getAndIncrement()); + newSession.set("inc", next); + return newSession; + }); + return session.get("inc"); + }); + + } + + public static void main(final String[] args) { + run(MongodbApp::new, args); + } +} diff --git a/jooby-mongodb/src/test/java/org/jooby/mongodb/MongodbSessionStoreTest.java b/jooby-mongodb/src/test/java/org/jooby/mongodb/MongodbSessionStoreTest.java index fcdcbd5f13..5dd54b2055 100644 --- a/jooby-mongodb/src/test/java/org/jooby/mongodb/MongodbSessionStoreTest.java +++ b/jooby-mongodb/src/test/java/org/jooby/mongodb/MongodbSessionStoreTest.java @@ -1,31 +1,45 @@ package org.jooby.mongodb; import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.expectLastCall; import static org.junit.Assert.assertEquals; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.bson.Document; +import org.bson.conversions.Bson; import org.jooby.Session; import org.jooby.test.MockUnit; +import org.jooby.test.MockUnit.Block; import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; import com.google.common.collect.ImmutableMap; -import com.mongodb.BasicDBObject; -import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.DB; -import com.mongodb.DBCollection; import com.mongodb.DBObject; -import com.mongodb.MongoException; -import com.mongodb.WriteResult; +import com.mongodb.client.FindIterable; +import com.mongodb.client.ListIndexesIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.UpdateResult; +@RunWith(PowerMockRunner.class) +@PrepareForTest({MongoSessionStore.class, IndexOptions.class, UpdateOptions.class, Filters.class, + LinkedHashMap.class }) public class MongodbSessionStoreTest { + @SuppressWarnings({"unchecked", "rawtypes" }) MockUnit.Block boot = unit -> { - DBCollection collection = unit.get(DBCollection.class); + MongoCollection collection = unit.get(MongoCollection.class); - DB db = unit.get(DB.class); + MongoDatabase db = unit.get(MongoDatabase.class); expect(db.getCollection("sess")).andReturn(collection); }; @@ -33,9 +47,10 @@ public class MongodbSessionStoreTest { Map attrs = ImmutableMap. of("k", "v"); + @SuppressWarnings("rawtypes") MockUnit.Block saveSession = unit -> { - DBCollection collection = unit.get(DBCollection.class); + MongoCollection collection = unit.get(MongoCollection.class); Session session = unit.get(Session.class); expect(session.id()).andReturn("1234"); @@ -44,23 +59,76 @@ public class MongodbSessionStoreTest { expect(session.savedAt()).andReturn(now); expect(session.attributes()).andReturn(attrs); - WriteResult result = unit.mock(WriteResult.class); - expect(collection.save(BasicDBObjectBuilder.start() - .add("_id", "1234") - .add("_accessedAt", new Date(now)) - .add("_createdAt", new Date(now)) - .add("_savedAt", new Date(now)) - .add("k", "v") - .get() - )).andReturn(result); + UpdateResult result = unit.mock(UpdateResult.class); + Document doc = new Document() + .append("_id", "1234") + .append("_accessedAt", new Date(now)) + .append("_createdAt", new Date(now)) + .append("_savedAt", new Date(now)) + .append("k", "v"); + + UpdateOptions options = unit.constructor(UpdateOptions.class) + .build(); + expect(options.upsert(true)).andReturn(options); + + Bson eq = unit.mock(Bson.class); + unit.mockStatic(Filters.class); + expect(Filters.eq("_id", "1234")).andReturn(eq); + + expect(collection.updateOne(eq, new Document("$set", doc), options)) + .andReturn(result); + }; + + @SuppressWarnings("rawtypes") + private Block noIndexes = unit -> { + MongoCursor cursor = unit.mock(MongoCursor.class); + expect(cursor.hasNext()).andReturn(false); + + ListIndexesIterable lii = unit.mock(ListIndexesIterable.class); + expect(lii.iterator()).andReturn(cursor); + + MongoCollection coll = unit.get(MongoCollection.class); + expect(coll.listIndexes()).andReturn(lii); + }; + + @SuppressWarnings("rawtypes") + private Block indexes = unit -> { + + Document d1 = unit.mock(Document.class); + expect(d1.getString("name")).andReturn("n1"); + + Document d2 = unit.mock(Document.class); + expect(d2.getString("name")).andReturn("_sessionIdx_"); + + MongoCursor cursor = unit.mock(MongoCursor.class); + expect(cursor.hasNext()).andReturn(true); + expect(cursor.next()).andReturn(d1); + expect(cursor.hasNext()).andReturn(true); + expect(cursor.next()).andReturn(d2); + + ListIndexesIterable lii = unit.mock(ListIndexesIterable.class); + expect(lii.iterator()).andReturn(cursor); + + MongoCollection coll = unit.get(MongoCollection.class); + expect(coll.listIndexes()).andReturn(lii); + }; + + private Block runCommand = unit -> { + MongoDatabase db = unit.get(MongoDatabase.class); + Document command = new Document("collMod", "sess") + .append("index", + new Document("keyPattern", new Document("_accessedAt", 1)) + .append("expireAfterSeconds", 60L)); + + expect(db.runCommand(command)).andReturn(unit.mock(Document.class)); }; @Test public void defaults() throws Exception { - new MockUnit(DB.class, DBCollection.class) + new MockUnit(MongoDatabase.class, MongoCollection.class) .expect(boot) .run(unit -> { - new MongoSessionStore(unit.get(DB.class), "sess", "1m"); + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "1m"); }); } @@ -69,18 +137,24 @@ public void defaultsNullDB() throws Exception { new MongoSessionStore(null, "sess", "1m"); } + @SuppressWarnings("rawtypes") @Test public void create() throws Exception { - new MockUnit(Session.class, DB.class, DBCollection.class) + new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class) .expect(boot) .expect(saveSession) + .expect(noIndexes) .expect(unit -> { - DBCollection collection = unit.get(DBCollection.class); - collection.createIndex(new BasicDBObject("_accessedAt", 1), new BasicDBObject( - "expireAfterSeconds", 300)); + MongoCollection collection = unit.get(MongoCollection.class); + IndexOptions options = unit.constructor(IndexOptions.class) + .build(); + expect(options.name("_sessionIdx_")).andReturn(options); + expect(options.expireAfter(300L, TimeUnit.SECONDS)).andReturn(options); + expect(collection.createIndex(new Document("_accessedAt", 1), options)) + .andReturn("idx"); }) .run(unit -> { - new MongoSessionStore(unit.get(DB.class), "sess", "5m") + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "5m") .create(unit.get(Session.class)); ; }); @@ -88,34 +162,38 @@ public void create() throws Exception { @Test public void save() throws Exception { - new MockUnit(Session.class, DB.class, DBCollection.class) + new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class) .expect(boot) + .expect(indexes) .expect(saveSession) - .expect(unit -> { - DBCollection collection = unit.get(DBCollection.class); - collection.createIndex(new BasicDBObject("_accessedAt", 1), new BasicDBObject( - "expireAfterSeconds", 60)); - }) + .expect(runCommand) .run(unit -> { - new MongoSessionStore(unit.get(DB.class), "sess", "60") + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60") .save(unit.get(Session.class)); ; }); } + @SuppressWarnings("rawtypes") @Test public void shouldSyncTtlOnce() throws Exception { - new MockUnit(Session.class, DB.class, DBCollection.class) + new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class) .expect(boot) .expect(saveSession) + .expect(noIndexes) .expect(saveSession) .expect(unit -> { - DBCollection collection = unit.get(DBCollection.class); - collection.createIndex(new BasicDBObject("_accessedAt", 1), new BasicDBObject( - "expireAfterSeconds", 60)); + MongoCollection collection = unit.get(MongoCollection.class); + IndexOptions options = unit.constructor(IndexOptions.class) + .build(); + expect(options.name("_sessionIdx_")).andReturn(options); + expect(options.expireAfter(60L, TimeUnit.SECONDS)).andReturn(options); + expect(collection.createIndex(new Document("_accessedAt", 1), options)) + .andReturn("idx"); }) .run(unit -> { - MongoSessionStore mongodb = new MongoSessionStore(unit.get(DB.class), "sess", "60"); + MongoSessionStore mongodb = new MongoSessionStore(unit.get(MongoDatabase.class), "sess", + "60"); mongodb.save(unit.get(Session.class)); mongodb.save(unit.get(Session.class)); }); @@ -123,11 +201,11 @@ public void shouldSyncTtlOnce() throws Exception { @Test public void saveNoTimeout() throws Exception { - new MockUnit(Session.class, DB.class, DBCollection.class) + new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class) .expect(boot) .expect(saveSession) .run(unit -> { - new MongoSessionStore(unit.get(DB.class), "sess", "0") + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "0") .save(unit.get(Session.class)); ; }); @@ -135,29 +213,13 @@ public void saveNoTimeout() throws Exception { @Test public void saveSyncTtl() throws Exception { - new MockUnit(Session.class, DB.class, DBCollection.class) + new MockUnit(Session.class, MongoDatabase.class, MongoCollection.class) .expect(boot) .expect(saveSession) - .expect(unit -> { - DBCollection collection = unit.get(DBCollection.class); - collection.createIndex(new BasicDBObject("_accessedAt", 1), new BasicDBObject( - "expireAfterSeconds", 60)); - expectLastCall().andThrow(new MongoException("intentional err")); - - DB db = unit.get(DB.class); - - expect(db.command(BasicDBObjectBuilder.start() - .add("collMod", "sess") - .add("index", BasicDBObjectBuilder.start() - .add("keyPattern", new BasicDBObject("_accessedAt", 1)) - .add("expireAfterSeconds", 60) - .get() - ) - .get()) - ).andReturn(null); - }) + .expect(indexes) + .expect(runCommand) .run(unit -> { - new MongoSessionStore(unit.get(DB.class), "sess", "60") + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60") .save(unit.get(Session.class)); ; }); @@ -167,64 +229,82 @@ public void saveSyncTtl() throws Exception { @Test public void get() throws Exception { long now = System.currentTimeMillis(); - new MockUnit(Session.class, Session.Builder.class, DB.class, DBCollection.class, DBObject.class) - .expect(boot) - .expect(unit -> { - Map sessionMap = unit.mock(Map.class); - expect(sessionMap.remove("_accessedAt")).andReturn(new Date(now)); - expect(sessionMap.remove("_createdAt")).andReturn(new Date(now)); - expect(sessionMap.remove("_savedAt")).andReturn(new Date(now)); - expect(sessionMap.remove("_id")).andReturn("1234"); + new MockUnit(Session.class, Session.Builder.class, MongoDatabase.class, MongoCollection.class, + DBObject.class) + .expect(boot) + .expect(unit -> { + Document doc = unit.mock(Document.class); - DBObject dbobj = unit.get(DBObject.class); - expect(dbobj.toMap()).andReturn(sessionMap); + Map sessionMap = unit.constructor(LinkedHashMap.class) + .args(Map.class) + .build(doc); + expect(sessionMap.remove("_accessedAt")).andReturn(new Date(now)); + expect(sessionMap.remove("_createdAt")).andReturn(new Date(now)); + expect(sessionMap.remove("_savedAt")).andReturn(new Date(now)); + expect(sessionMap.remove("_id")).andReturn("1234"); - Session.Builder sb = unit.get(Session.Builder.class); - expect(sb.sessionId()).andReturn("1234"); - expect(sb.accessedAt(now)).andReturn(sb); - expect(sb.createdAt(now)).andReturn(sb); - expect(sb.savedAt(now)).andReturn(sb); - expect(sb.set(sessionMap)).andReturn(sb); - expect(sb.build()).andReturn(unit.get(Session.class)); - - DBCollection collection = unit.get(DBCollection.class); - expect(collection.findOne("1234")).andReturn(dbobj); - }) - .run(unit -> { - MongoSessionStore mss = new MongoSessionStore(unit.get(DB.class), "sess", "60"); - assertEquals(unit.get(Session.class), mss.get(unit.get(Session.Builder.class))); - }); + FindIterable result = unit.mock(FindIterable.class); + expect(result.first()).andReturn(doc); + + Session.Builder sb = unit.get(Session.Builder.class); + expect(sb.sessionId()).andReturn("1234"); + expect(sb.accessedAt(now)).andReturn(sb); + expect(sb.createdAt(now)).andReturn(sb); + expect(sb.savedAt(now)).andReturn(sb); + expect(sb.set(sessionMap)).andReturn(sb); + expect(sb.build()).andReturn(unit.get(Session.class)); + + Bson eq = unit.mock(Bson.class); + unit.mockStatic(Filters.class); + expect(Filters.eq("_id", "1234")).andReturn(eq); + + MongoCollection collection = unit.get(MongoCollection.class); + expect(collection.find(eq)).andReturn(result); + }) + .run(unit -> { + MongoSessionStore mss = new MongoSessionStore(unit.get(MongoDatabase.class), "sess", + "60"); + assertEquals(unit.get(Session.class), mss.get(unit.get(Session.Builder.class))); + }); } + @SuppressWarnings("rawtypes") @Test public void getExpired() throws Exception { - new MockUnit(Session.class, Session.Builder.class, DB.class, DBCollection.class) + new MockUnit(Session.class, Session.Builder.class, MongoDatabase.class, MongoCollection.class) .expect(boot) .expect(unit -> { - Session.Builder sb = unit.get(Session.Builder.class); expect(sb.sessionId()).andReturn("1234"); - DBCollection collection = unit.get(DBCollection.class); - expect(collection.findOne("1234")).andReturn(null); + Bson eq = unit.mock(Bson.class); + unit.mockStatic(Filters.class); + expect(Filters.eq("_id", "1234")).andReturn(eq); + + FindIterable result = unit.mock(FindIterable.class); + expect(result.first()).andReturn(null); + + MongoCollection collection = unit.get(MongoCollection.class); + expect(collection.find(eq)).andReturn(result); }) .run(unit -> { assertEquals(null, - new MongoSessionStore(unit.get(DB.class), "sess", "60") + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60") .get(unit.get(Session.Builder.class))); }); } + @SuppressWarnings("rawtypes") @Test public void delete() throws Exception { - new MockUnit(DB.class, DBCollection.class) + new MockUnit(MongoDatabase.class, MongoCollection.class) .expect(boot) .expect(unit -> { - DBCollection collection = unit.get(DBCollection.class); - expect(collection.remove(new BasicDBObject("_id", "1234"))).andReturn(null); + MongoCollection collection = unit.get(MongoCollection.class); + expect(collection.deleteOne(new Document("_id", "1234"))).andReturn(null); }) .run(unit -> { - new MongoSessionStore(unit.get(DB.class), "sess", "60") + new MongoSessionStore(unit.get(MongoDatabase.class), "sess", "60") .delete("1234"); }); }