org.jooby.mongodb.MongoSessionStore.java Source code

Java tutorial

Introduction

Here is the source code for org.jooby.mongodb.MongoSessionStore.java

Source

/**
 * 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);
    }

}