001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2017-2018 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.http;
020
021import java.time.Duration;
022import java.time.Instant;
023import java.util.Iterator;
024import java.util.LinkedHashMap;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Optional;
028
029import org.jgrapes.core.Channel;
030import org.jgrapes.http.events.Request;
031
032/**
033 * A in memory session manager. 
034 */
035public class InMemorySessionManager extends SessionManager {
036
037    @SuppressWarnings({ "serial", "PMD.UseConcurrentHashMap" })
038    private final Map<String, Session> sessionsById
039        = new LinkedHashMap<String, Session>(16, 0.75f, true) {
040
041            @Override
042            protected boolean removeEldestEntry(Entry<String, Session> eldest) {
043                return maxSessions() > 0 && size() > maxSessions();
044            }
045        };
046
047    /**
048     * Creates a new session manager with its channel set to
049     * itself and the path set to "/". The manager handles
050     * all {@link Request} events.
051     */
052    public InMemorySessionManager() {
053        this("/");
054    }
055
056    /**
057     * Creates a new session manager with its channel set to
058     * itself and the path set to the given path. The manager
059     * handles all requests that match the given path, using the
060     * same rules as browsers do for selecting the cookies that
061     * are to be sent.
062     * 
063     * @param path the path
064     */
065    public InMemorySessionManager(String path) {
066        this(Channel.SELF, path);
067    }
068
069    /**
070     * Creates a new session manager with its channel set to
071     * the given channel and the path to "/". The manager handles
072     * all {@link Request} events.
073     * 
074     * @param componentChannel the component channel
075     */
076    public InMemorySessionManager(Channel componentChannel) {
077        this(componentChannel, "/");
078    }
079
080    /**
081     * Creates a new session manager with the given channel and path.
082     * The manager handles all requests that match the given path, using
083     * the same rules as browsers do for selecting the cookies that
084     * are to be sent.
085     *  
086     * @param componentChannel the component channel
087     * @param path the path
088     */
089    public InMemorySessionManager(Channel componentChannel, String path) {
090        this(componentChannel, derivePattern(path), 1000, path);
091    }
092
093    /**
094     * Creates a new session manager using the given channel and path.
095     * The manager handles only requests that match the given pattern.
096     * The {@link Request} handler is registered with the given priority.
097     * 
098     * This constructor can be used if special handling of top level
099     * requests is needed.
100     *
101     * @param componentChannel the component channel
102     * @param pattern the path part of a {@link ResourcePattern}
103     * @param priority the priority
104     * @param path the path
105     */
106    public InMemorySessionManager(Channel componentChannel, String pattern,
107            int priority, String path) {
108        super(componentChannel, pattern, priority, path);
109    }
110
111    @Override
112    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
113    protected Session createSession(String sessionId) {
114        Session session = new InMemorySession(sessionId);
115        Instant now = Instant.now();
116        synchronized (this) {
117            if (absoluteTimeout() > 0) {
118                // Quick check for sessions that are too old
119                for (Iterator<Entry<String, Session>> iter = sessionsById
120                    .entrySet().iterator(); iter.hasNext();) {
121                    Entry<String, Session> entry = iter.next();
122                    if (Duration.between(entry.getValue().createdAt(), now)
123                        .getSeconds() > absoluteTimeout()) {
124                        iter.remove();
125                    } else {
126                        break;
127                    }
128                }
129            }
130            if (sessionsById.size() >= maxSessions() && absoluteTimeout() > 0) {
131                // Thorough search for sessions that are too old
132                sessionsById.entrySet().removeIf(entry -> {
133                    return Duration.between(entry.getValue().createdAt(),
134                        now).getSeconds() > absoluteTimeout()
135                        || Duration.between(entry.getValue().lastUsedAt(),
136                            now).getSeconds() > idleTimeout();
137                });
138            }
139            sessionsById.put(sessionId, session);
140        }
141        return session;
142    }
143
144    @Override
145    protected Optional<Session> lookupSession(String sessionId) {
146        return Optional.ofNullable(sessionsById.get(sessionId));
147    }
148
149    @Override
150    protected void removeSession(String sessionId) {
151        synchronized (this) {
152            sessionsById.remove(sessionId);
153        }
154    }
155
156    /*
157     * (non-Javadoc)
158     * 
159     * @see org.jgrapes.http.SessionManager#sessionCount()
160     */
161    @Override
162    protected int sessionCount() {
163        return sessionsById.size();
164    }
165
166}