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.session.ExpiredSessionException;
022import org.apache.shiro.session.InvalidSessionException;
023import org.apache.shiro.session.StoppedSessionException;
024import org.apache.shiro.util.CollectionUtils;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import java.io.IOException;
029import java.io.ObjectInputStream;
030import java.io.ObjectOutputStream;
031import java.io.Serializable;
032import java.text.DateFormat;
033import java.util.*;
034
035
036/**
037 * Simple {@link org.apache.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the
038 * business/server tier.
039 *
040 * @since 0.1
041 */
042public class SimpleSession implements ValidatingSession, Serializable {
043
044    // Serialization reminder:
045    // You _MUST_ change this number if you introduce a change to this class
046    // that is NOT serialization backwards compatible.  Serialization-compatible
047    // changes do not require a change to this number.  If you need to generate
048    // a new number in this case, use the JDK's 'serialver' program to generate it.
049    private static final long serialVersionUID = -7125642695178165650L;
050
051    //TODO - complete JavaDoc
052    private transient static final Logger log = LoggerFactory.getLogger(SimpleSession.class);
053
054    protected static final long MILLIS_PER_SECOND = 1000;
055    protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
056    protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
057
058    //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED!
059    static int bitIndexCounter = 0;
060    private static final int ID_BIT_MASK = 1 << bitIndexCounter++;
061    private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
062    private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
063    private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++;
064    private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++;
065    private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++;
066    private static final int HOST_BIT_MASK = 1 << bitIndexCounter++;
067    private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++;
068
069    // ==============================================================
070    // NOTICE:
071    //
072    // The following fields are marked as transient to avoid double-serialization.
073    // They are in fact serialized (even though 'transient' usually indicates otherwise),
074    // but they are serialized explicitly via the writeObject and readObject implementations
075    // in this class.
076    //
077    // If we didn't declare them as transient, the out.defaultWriteObject(); call in writeObject would
078    // serialize all non-transient fields as well, effectively doubly serializing the fields (also
079    // doubling the serialization size).
080    //
081    // This finding, with discussion, was covered here:
082    //
083    // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E
084    //
085    // ==============================================================
086    private transient Serializable id;
087    private transient Date startTimestamp;
088    private transient Date stopTimestamp;
089    private transient Date lastAccessTime;
090    private transient long timeout;
091    private transient boolean expired;
092    private transient String host;
093    private transient Map<Object, Object> attributes;
094
095    public SimpleSession() {
096        this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager
097        this.startTimestamp = new Date();
098        this.lastAccessTime = this.startTimestamp;
099    }
100
101    public SimpleSession(String host) {
102        this();
103        this.host = host;
104    }
105
106    public Serializable getId() {
107        return this.id;
108    }
109
110    public void setId(Serializable id) {
111        this.id = id;
112    }
113
114    public Date getStartTimestamp() {
115        return startTimestamp;
116    }
117
118    public void setStartTimestamp(Date startTimestamp) {
119        this.startTimestamp = startTimestamp;
120    }
121
122    /**
123     * Returns the time the session was stopped, or <tt>null</tt> if the session is still active.
124     * <p/>
125     * A session may become stopped under a number of conditions:
126     * <ul>
127     * <li>If the user logs out of the system, their current session is terminated (released).</li>
128     * <li>If the session expires</li>
129     * <li>The application explicitly calls {@link #stop()}</li>
130     * <li>If there is an internal system error and the session state can no longer accurately
131     * reflect the user's behavior, such in the case of a system crash</li>
132     * </ul>
133     * <p/>
134     * Once stopped, a session may no longer be used.  It is locked from all further activity.
135     *
136     * @return The time the session was stopped, or <tt>null</tt> if the session is still
137     *         active.
138     */
139    public Date getStopTimestamp() {
140        return stopTimestamp;
141    }
142
143    public void setStopTimestamp(Date stopTimestamp) {
144        this.stopTimestamp = stopTimestamp;
145    }
146
147    public Date getLastAccessTime() {
148        return lastAccessTime;
149    }
150
151    public void setLastAccessTime(Date lastAccessTime) {
152        this.lastAccessTime = lastAccessTime;
153    }
154
155    /**
156     * Returns true if this session has expired, false otherwise.  If the session has
157     * expired, no further user interaction with the system may be done under this session.
158     *
159     * @return true if this session has expired, false otherwise.
160     */
161    public boolean isExpired() {
162        return expired;
163    }
164
165    public void setExpired(boolean expired) {
166        this.expired = expired;
167    }
168
169    public long getTimeout() {
170        return timeout;
171    }
172
173    public void setTimeout(long timeout) {
174        this.timeout = timeout;
175    }
176
177    public String getHost() {
178        return host;
179    }
180
181    public void setHost(String host) {
182        this.host = host;
183    }
184
185    public Map<Object, Object> getAttributes() {
186        return attributes;
187    }
188
189    public void setAttributes(Map<Object, Object> attributes) {
190        this.attributes = attributes;
191    }
192
193    public void touch() {
194        this.lastAccessTime = new Date();
195    }
196
197    public void stop() {
198        if (this.stopTimestamp == null) {
199            this.stopTimestamp = new Date();
200        }
201    }
202
203    protected boolean isStopped() {
204        return getStopTimestamp() != null;
205    }
206
207    protected void expire() {
208        stop();
209        this.expired = true;
210    }
211
212    /**
213     * @since 0.9
214     */
215    public boolean isValid() {
216        return !isStopped() && !isExpired();
217    }
218
219    /**
220     * Determines if this session is expired.
221     *
222     * @return true if the specified session has expired, false otherwise.
223     */
224    protected boolean isTimedOut() {
225
226        if (isExpired()) {
227            return true;
228        }
229
230        long timeout = getTimeout();
231
232        if (timeout >= 0l) {
233
234            Date lastAccessTime = getLastAccessTime();
235
236            if (lastAccessTime == null) {
237                String msg = "session.lastAccessTime for session with id [" +
238                        getId() + "] is null.  This value must be set at " +
239                        "least once, preferably at least upon instantiation.  Please check the " +
240                        getClass().getName() + " implementation and ensure " +
241                        "this value will be set (perhaps in the constructor?)";
242                throw new IllegalStateException(msg);
243            }
244
245            // Calculate at what time a session would have been last accessed
246            // for it to be expired at this point.  In other words, subtract
247            // from the current time the amount of time that a session can
248            // be inactive before expiring.  If the session was last accessed
249            // before this time, it is expired.
250            long expireTimeMillis = System.currentTimeMillis() - timeout;
251            Date expireTime = new Date(expireTimeMillis);
252            return lastAccessTime.before(expireTime);
253        } else {
254            if (log.isTraceEnabled()) {
255                log.trace("No timeout for session with id [" + getId() +
256                        "].  Session is not considered expired.");
257            }
258        }
259
260        return false;
261    }
262
263    public void validate() throws InvalidSessionException {
264        //check for stopped:
265        if (isStopped()) {
266            //timestamp is set, so the session is considered stopped:
267            String msg = "Session with id [" + getId() + "] has been " +
268                    "explicitly stopped.  No further interaction under this session is " +
269                    "allowed.";
270            throw new StoppedSessionException(msg);
271        }
272
273        //check for expiration
274        if (isTimedOut()) {
275            expire();
276
277            //throw an exception explaining details of why it expired:
278            Date lastAccessTime = getLastAccessTime();
279            long timeout = getTimeout();
280
281            Serializable sessionId = getId();
282
283            DateFormat df = DateFormat.getInstance();
284            String msg = "Session with id [" + sessionId + "] has expired. " +
285                    "Last access time: " + df.format(lastAccessTime) +
286                    ".  Current time: " + df.format(new Date()) +
287                    ".  Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" +
288                    timeout / MILLIS_PER_MINUTE + " minutes)";
289            if (log.isTraceEnabled()) {
290                log.trace(msg);
291            }
292            throw new ExpiredSessionException(msg);
293        }
294    }
295
296    private Map<Object, Object> getAttributesLazy() {
297        Map<Object, Object> attributes = getAttributes();
298        if (attributes == null) {
299            attributes = new HashMap<Object, Object>();
300            setAttributes(attributes);
301        }
302        return attributes;
303    }
304
305    public Collection<Object> getAttributeKeys() throws InvalidSessionException {
306        Map<Object, Object> attributes = getAttributes();
307        if (attributes == null) {
308            return Collections.emptySet();
309        }
310        return attributes.keySet();
311    }
312
313    public Object getAttribute(Object key) {
314        Map<Object, Object> attributes = getAttributes();
315        if (attributes == null) {
316            return null;
317        }
318        return attributes.get(key);
319    }
320
321    public void setAttribute(Object key, Object value) {
322        if (value == null) {
323            removeAttribute(key);
324        } else {
325            getAttributesLazy().put(key, value);
326        }
327    }
328
329    public Object removeAttribute(Object key) {
330        Map<Object, Object> attributes = getAttributes();
331        if (attributes == null) {
332            return null;
333        } else {
334            return attributes.remove(key);
335        }
336    }
337
338    /**
339     * Returns {@code true} if the specified argument is an {@code instanceof} {@code SimpleSession} and both
340     * {@link #getId() id}s are equal.  If the argument is a {@code SimpleSession} and either 'this' or the argument
341     * does not yet have an ID assigned, the value of {@link #onEquals(SimpleSession) onEquals} is returned, which
342     * does a necessary attribute-based comparison when IDs are not available.
343     * <p/>
344     * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
345     * avoid the more expensive attributes-based comparison.
346     *
347     * @param obj the object to compare with this one for equality.
348     * @return {@code true} if this object is equivalent to the specified argument, {@code false} otherwise.
349     */
350    @Override
351    public boolean equals(Object obj) {
352        if (this == obj) {
353            return true;
354        }
355        if (obj instanceof SimpleSession) {
356            SimpleSession other = (SimpleSession) obj;
357            Serializable thisId = getId();
358            Serializable otherId = other.getId();
359            if (thisId != null && otherId != null) {
360                return thisId.equals(otherId);
361            } else {
362                //fall back to an attribute based comparison:
363                return onEquals(other);
364            }
365        }
366        return false;
367    }
368
369    /**
370     * Provides an attribute-based comparison (no ID comparison) - incurred <em>only</em> when 'this' or the
371     * session object being compared for equality do not have a session id.
372     *
373     * @param ss the SimpleSession instance to compare for equality.
374     * @return true if all the attributes, except the id, are equal to this object's attributes.
375     * @since 1.0
376     */
377    protected boolean onEquals(SimpleSession ss) {
378        return (getStartTimestamp() != null ? getStartTimestamp().equals(ss.getStartTimestamp()) : ss.getStartTimestamp() == null) &&
379                (getStopTimestamp() != null ? getStopTimestamp().equals(ss.getStopTimestamp()) : ss.getStopTimestamp() == null) &&
380                (getLastAccessTime() != null ? getLastAccessTime().equals(ss.getLastAccessTime()) : ss.getLastAccessTime() == null) &&
381                (getTimeout() == ss.getTimeout()) &&
382                (isExpired() == ss.isExpired()) &&
383                (getHost() != null ? getHost().equals(ss.getHost()) : ss.getHost() == null) &&
384                (getAttributes() != null ? getAttributes().equals(ss.getAttributes()) : ss.getAttributes() == null);
385    }
386
387    /**
388     * Returns the hashCode.  If the {@link #getId() id} is not {@code null}, its hashcode is returned immediately.
389     * If it is {@code null}, an attributes-based hashCode will be calculated and returned.
390     * <p/>
391     * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
392     * avoid the more expensive attributes-based calculation.
393     *
394     * @return this object's hashCode
395     * @since 1.0
396     */
397    @Override
398    public int hashCode() {
399        Serializable id = getId();
400        if (id != null) {
401            return id.hashCode();
402        }
403        int hashCode = getStartTimestamp() != null ? getStartTimestamp().hashCode() : 0;
404        hashCode = 31 * hashCode + (getStopTimestamp() != null ? getStopTimestamp().hashCode() : 0);
405        hashCode = 31 * hashCode + (getLastAccessTime() != null ? getLastAccessTime().hashCode() : 0);
406        hashCode = 31 * hashCode + Long.valueOf(Math.max(getTimeout(), 0)).hashCode();
407        hashCode = 31 * hashCode + Boolean.valueOf(isExpired()).hashCode();
408        hashCode = 31 * hashCode + (getHost() != null ? getHost().hashCode() : 0);
409        hashCode = 31 * hashCode + (getAttributes() != null ? getAttributes().hashCode() : 0);
410        return hashCode;
411    }
412
413    /**
414     * Returns the string representation of this SimpleSession, equal to
415     * <code>getClass().getName() + &quot;,id=&quot; + getId()</code>.
416     *
417     * @return the string representation of this SimpleSession, equal to
418     *         <code>getClass().getName() + &quot;,id=&quot; + getId()</code>.
419     * @since 1.0
420     */
421    @Override
422    public String toString() {
423        StringBuilder sb = new StringBuilder();
424        sb.append(getClass().getName()).append(",id=").append(getId());
425        return sb.toString();
426    }
427
428    /**
429     * Serializes this object to the specified output stream for JDK Serialization.
430     *
431     * @param out output stream used for Object serialization.
432     * @throws IOException if any of this object's fields cannot be written to the stream.
433     * @since 1.0
434     */
435    private void writeObject(ObjectOutputStream out) throws IOException {
436        out.defaultWriteObject();
437        short alteredFieldsBitMask = getAlteredFieldsBitMask();
438        out.writeShort(alteredFieldsBitMask);
439        if (id != null) {
440            out.writeObject(id);
441        }
442        if (startTimestamp != null) {
443            out.writeObject(startTimestamp);
444        }
445        if (stopTimestamp != null) {
446            out.writeObject(stopTimestamp);
447        }
448        if (lastAccessTime != null) {
449            out.writeObject(lastAccessTime);
450        }
451        if (timeout != 0l) {
452            out.writeLong(timeout);
453        }
454        if (expired) {
455            out.writeBoolean(expired);
456        }
457        if (host != null) {
458            out.writeUTF(host);
459        }
460        if (!CollectionUtils.isEmpty(attributes)) {
461            out.writeObject(attributes);
462        }
463    }
464
465    /**
466     * Reconstitutes this object based on the specified InputStream for JDK Serialization.
467     *
468     * @param in the input stream to use for reading data to populate this object.
469     * @throws IOException            if the input stream cannot be used.
470     * @throws ClassNotFoundException if a required class needed for instantiation is not available in the present JVM
471     * @since 1.0
472     */
473    @SuppressWarnings({"unchecked"})
474    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
475        in.defaultReadObject();
476        short bitMask = in.readShort();
477
478        if (isFieldPresent(bitMask, ID_BIT_MASK)) {
479            this.id = (Serializable) in.readObject();
480        }
481        if (isFieldPresent(bitMask, START_TIMESTAMP_BIT_MASK)) {
482            this.startTimestamp = (Date) in.readObject();
483        }
484        if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) {
485            this.stopTimestamp = (Date) in.readObject();
486        }
487        if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) {
488            this.lastAccessTime = (Date) in.readObject();
489        }
490        if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) {
491            this.timeout = in.readLong();
492        }
493        if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) {
494            this.expired = in.readBoolean();
495        }
496        if (isFieldPresent(bitMask, HOST_BIT_MASK)) {
497            this.host = in.readUTF();
498        }
499        if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) {
500            this.attributes = (Map<Object, Object>) in.readObject();
501        }
502    }
503
504    /**
505     * Returns a bit mask used during serialization indicating which fields have been serialized. Fields that have been
506     * altered (not null and/or not retaining the class defaults) will be serialized and have 1 in their respective
507     * index, fields that are null and/or retain class default values have 0.
508     *
509     * @return a bit mask used during serialization indicating which fields have been serialized.
510     * @since 1.0
511     */
512    private short getAlteredFieldsBitMask() {
513        int bitMask = 0;
514        bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask;
515        bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask;
516        bitMask = stopTimestamp != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask;
517        bitMask = lastAccessTime != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask;
518        bitMask = timeout != 0l ? bitMask | TIMEOUT_BIT_MASK : bitMask;
519        bitMask = expired ? bitMask | EXPIRED_BIT_MASK : bitMask;
520        bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask;
521        bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask;
522        return (short) bitMask;
523    }
524
525    /**
526     * Returns {@code true} if the given {@code bitMask} argument indicates that the specified field has been
527     * serialized and therefore should be read during deserialization, {@code false} otherwise.
528     *
529     * @param bitMask      the aggregate bitmask for all fields that have been serialized.  Individual bits represent
530     *                     the fields that have been serialized.  A bit set to 1 means that corresponding field has
531     *                     been serialized, 0 means it hasn't been serialized.
532     * @param fieldBitMask the field bit mask constant identifying which bit to inspect (corresponds to a class attribute).
533     * @return {@code true} if the given {@code bitMask} argument indicates that the specified field has been
534     *         serialized and therefore should be read during deserialization, {@code false} otherwise.
535     * @since 1.0
536     */
537    private static boolean isFieldPresent(short bitMask, int fieldBitMask) {
538        return (bitMask & fieldBitMask) != 0;
539    }
540
541}