/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.mongodb; 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; 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 javax.inject.Inject; import javax.inject.Named; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Objects.requireNonNull; /** * A {@link Session.Store} powered by * <a href="http://mongodb.github.io/mongo-java-driver/">Mongodb</a>. * * <h2>usage</h2> * * <pre> * { * use(new Mongodb()); * * session(MongoSessionStore.class); * * get("/", req {@literal ->} { * req.session().set("name", "jooby"); * }); * } * </pre> * * The <code>name</code> attribute and value will be stored in a * <a href="http://mongodb.github.io/mongo-java-driver/">Mongodb</a>. * * <h2>options</h2> * * <h3>timeout</h3> * <p> * By default, a mongodb session will expire after <code>30 minutes</code>. Changing the default * timeout is as simple as: * </p> * * <pre> * # 8 hours * session.timeout = 8h * * # 15 seconds * session.timeout = 15 * * # 120 minutes * session.timeout = 120m * </pre> * * <p> * It uses MongoDB's TTL collection feature (2.2+) to have <code>mongod</code> automatically remove * expired sessions. * </p> * * If no timeout is required, use <code>-1</code>. * * <h3>session collection</h3> * <p> * Default mongodb collection is <code>sessions</code>. * * <p> * It's possible to change the default key setting the <code>mongodb.sesssion.collection</code> * properties. * </p> * * @author edgar * @since 0.5.0 */ public class MongoSessionStore implements Session.Store { private static final char DOT = '.'; private static final char UDOT = '\uFF0E'; private static final char DOLLAR = '$'; private static final char UDOLLAR = '\uFF04'; private static final String SESSION_IDX = "_sessionIdx_"; /** The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); protected final MongoCollection<Document> sessions; protected final long timeout; protected final String collection; private final AtomicBoolean ttlSync = new AtomicBoolean(false); protected final MongoDatabase db; 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 = timeoutInSeconds; } @Inject public MongoSessionStore(final MongoDatabase db, final @Named("mongodb.session.collection") String collection, final @Named("session.timeout") String timeout) { this(db, collection, seconds(timeout)); } @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Session get(final Builder builder) { 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"); builder .accessedAt(accessedAt.getTime()) .createdAt(createdAt.getTime()) .savedAt(savedAt.getTime()); session.forEach((k, v) -> builder.set(decode(k.toString()), v.toString())); return builder.build(); }).orElse(null); } @Override public void save(final Session session) { syncTtl(); 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 Map<String, String> attributes = session.attributes(); attributes.forEach((k, v) -> doc.append(encode(k), v)); sessions.updateOne(filter, new Document("$set", doc), new UpdateOptions().upsert(true)); } @Override public void create(final Session session) { save(session); } private void syncTtl() { if (!ttlSync.get()) { ttlSync.set(true); if (timeout <= 0) { return; } 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 Document("_accessedAt", 1), new IndexOptions() .name(SESSION_IDX) .expireAfter(timeout, TimeUnit.SECONDS)); } } } @Override public void delete(final String id) { sessions.deleteOne(new Document("_id", id)); } private static long seconds(final String value) { try { return Long.parseLong(value); } catch (NumberFormatException ex) { Config config = ConfigFactory.empty() .withValue("timeout", ConfigValueFactory.fromAnyRef(value)); return config.getDuration("timeout", TimeUnit.SECONDS); } } private boolean existsIdx(final String name) { MongoCursor<Document> iterator = sessions.listIndexes().iterator(); while (iterator.hasNext()) { Document doc = iterator.next(); if (doc.getString("name").equals(name)) { return true; } } return false; } private String encode(final String key) { String value = key; if (value.charAt(0) == DOLLAR) { value = UDOLLAR + value.substring(1); } return value.replace(DOT, UDOT); } private String decode(final String key) { String value = key; if (value.charAt(0) == UDOLLAR) { value = DOLLAR + value.substring(1); } return value.replace(UDOT, DOT); } }