001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.session.mgt;
020
021import org.apache.shiro.cache.CacheManager;
022import org.apache.shiro.cache.CacheManagerAware;
023import org.apache.shiro.session.Session;
024import org.apache.shiro.session.UnknownSessionException;
025import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
026import org.apache.shiro.session.mgt.eis.SessionDAO;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import java.io.Serializable;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.Date;
034
035/**
036 * Default business-tier implementation of a {@link ValidatingSessionManager}.  All session CRUD operations are
037 * delegated to an internal {@link SessionDAO}.
038 *
039 * @since 0.1
040 */
041public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {
042
043    //TODO - complete JavaDoc
044
045    private static final Logger log = LoggerFactory.getLogger(DefaultSessionManager.class);
046
047    private SessionFactory sessionFactory;
048
049    protected SessionDAO sessionDAO;  //todo - move SessionDAO up to AbstractValidatingSessionManager?
050
051    private CacheManager cacheManager;
052
053    private boolean deleteInvalidSessions;
054
055    public DefaultSessionManager() {
056        this.deleteInvalidSessions = true;
057        this.sessionFactory = new SimpleSessionFactory();
058        this.sessionDAO = new MemorySessionDAO();
059    }
060
061    public void setSessionDAO(SessionDAO sessionDAO) {
062        this.sessionDAO = sessionDAO;
063        applyCacheManagerToSessionDAO();
064    }
065
066    public SessionDAO getSessionDAO() {
067        return this.sessionDAO;
068    }
069
070    /**
071     * Returns the {@code SessionFactory} used to generate new {@link Session} instances.  The default instance
072     * is a {@link SimpleSessionFactory}.
073     *
074     * @return the {@code SessionFactory} used to generate new {@link Session} instances.
075     * @since 1.0
076     */
077    public SessionFactory getSessionFactory() {
078        return sessionFactory;
079    }
080
081    /**
082     * Sets the {@code SessionFactory} used to generate new {@link Session} instances.  The default instance
083     * is a {@link SimpleSessionFactory}.
084     *
085     * @param sessionFactory the {@code SessionFactory} used to generate new {@link Session} instances.
086     * @since 1.0
087     */
088    public void setSessionFactory(SessionFactory sessionFactory) {
089        this.sessionFactory = sessionFactory;
090    }
091
092    /**
093     * Returns {@code true} if sessions should be automatically deleted after they are discovered to be invalid,
094     * {@code false} if invalid sessions will be manually deleted by some process external to Shiro's control.  The
095     * default is {@code true} to ensure no orphans exist in the underlying data store.
096     * <h4>Usage</h4>
097     * It is ok to set this to {@code false} <b><em>ONLY</em></b> if you have some other process that you manage yourself
098     * that periodically deletes invalid sessions from the backing data store over time, such as via a Quartz or Cron
099     * job.  If you do not do this, the invalid sessions will become 'orphans' and fill up the data store over time.
100     * <p/>
101     * This property is provided because some systems need the ability to perform querying/reporting against sessions in
102     * the data store, even after they have stopped or expired.  Setting this attribute to {@code false} will allow
103     * such querying, but with the caveat that the application developer/configurer deletes the sessions themselves by
104     * some other means (cron, quartz, etc).
105     *
106     * @return {@code true} if sessions should be automatically deleted after they are discovered to be invalid,
107     *         {@code false} if invalid sessions will be manually deleted by some process external to Shiro's control.
108     * @since 1.0
109     */
110    public boolean isDeleteInvalidSessions() {
111        return deleteInvalidSessions;
112    }
113
114    /**
115     * Sets whether or not sessions should be automatically deleted after they are discovered to be invalid.  Default
116     * value is {@code true} to ensure no orphans will exist in the underlying data store.
117     * <h4>WARNING</h4>
118     * Only set this value to {@code false} if you are manually going to delete sessions yourself by some process
119     * (quartz, cron, etc) external to Shiro's control.  See the
120     * {@link #isDeleteInvalidSessions() isDeleteInvalidSessions()} JavaDoc for more.
121     *
122     * @param deleteInvalidSessions whether or not sessions should be automatically deleted after they are discovered
123     *                              to be invalid.
124     * @since 1.0
125     */
126    @SuppressWarnings({"UnusedDeclaration"})
127    public void setDeleteInvalidSessions(boolean deleteInvalidSessions) {
128        this.deleteInvalidSessions = deleteInvalidSessions;
129    }
130
131    public void setCacheManager(CacheManager cacheManager) {
132        this.cacheManager = cacheManager;
133        applyCacheManagerToSessionDAO();
134    }
135
136    /**
137     * Sets the internal {@code CacheManager} on the {@code SessionDAO} if it implements the
138     * {@link org.apache.shiro.cache.CacheManagerAware CacheManagerAware} interface.
139     * <p/>
140     * This method is called after setting a cacheManager via the
141     * {@link #setCacheManager(org.apache.shiro.cache.CacheManager) setCacheManager} method <em>em</em> when
142     * setting a {@code SessionDAO} via the {@link #setSessionDAO} method to allow it to be propagated
143     * in either case.
144     *
145     * @since 1.0
146     */
147    private void applyCacheManagerToSessionDAO() {
148        if (this.cacheManager != null && this.sessionDAO != null && this.sessionDAO instanceof CacheManagerAware) {
149            ((CacheManagerAware) this.sessionDAO).setCacheManager(this.cacheManager);
150        }
151    }
152
153    protected Session doCreateSession(SessionContext context) {
154        Session s = newSessionInstance(context);
155        if (log.isTraceEnabled()) {
156            log.trace("Creating session for host {}", s.getHost());
157        }
158        create(s);
159        return s;
160    }
161
162    protected Session newSessionInstance(SessionContext context) {
163        return getSessionFactory().createSession(context);
164    }
165
166    /**
167     * Persists the given session instance to an underlying EIS (Enterprise Information System).  This implementation
168     * delegates and calls
169     * <code>this.{@link SessionDAO sessionDAO}.{@link SessionDAO#create(org.apache.shiro.session.Session) create}(session);<code>
170     *
171     * @param session the Session instance to persist to the underlying EIS.
172     */
173    protected void create(Session session) {
174        if (log.isDebugEnabled()) {
175            log.debug("Creating new EIS record for new session instance [" + session + "]");
176        }
177        sessionDAO.create(session);
178    }
179
180    @Override
181    protected void onStop(Session session) {
182        if (session instanceof SimpleSession) {
183            SimpleSession ss = (SimpleSession) session;
184            Date stopTs = ss.getStopTimestamp();
185            ss.setLastAccessTime(stopTs);
186        }
187        onChange(session);
188    }
189
190    @Override
191    protected void afterStopped(Session session) {
192        if (isDeleteInvalidSessions()) {
193            delete(session);
194        }
195    }
196
197    protected void onExpiration(Session session) {
198        if (session instanceof SimpleSession) {
199            ((SimpleSession) session).setExpired(true);
200        }
201        onChange(session);
202    }
203
204    @Override
205    protected void afterExpired(Session session) {
206        if (isDeleteInvalidSessions()) {
207            delete(session);
208        }
209    }
210
211    protected void onChange(Session session) {
212        sessionDAO.update(session);
213    }
214
215    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
216        Serializable sessionId = getSessionId(sessionKey);
217        if (sessionId == null) {
218            log.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a " +
219                    "session could not be found.", sessionKey);
220            return null;
221        }
222        Session s = retrieveSessionFromDataSource(sessionId);
223        if (s == null) {
224            //session ID was provided, meaning one is expected to be found, but we couldn't find one:
225            String msg = "Could not find session with ID [" + sessionId + "]";
226            throw new UnknownSessionException(msg);
227        }
228        return s;
229    }
230
231    protected Serializable getSessionId(SessionKey sessionKey) {
232        return sessionKey.getSessionId();
233    }
234
235    protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
236        return sessionDAO.readSession(sessionId);
237    }
238
239    protected void delete(Session session) {
240        sessionDAO.delete(session);
241    }
242
243    protected Collection<Session> getActiveSessions() {
244        Collection<Session> active = sessionDAO.getActiveSessions();
245        return active != null ? active : Collections.<Session>emptySet();
246    }
247
248}