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     */
019    package org.apache.shiro.session.mgt;
020    
021    import org.apache.shiro.session.ExpiredSessionException;
022    import org.apache.shiro.session.InvalidSessionException;
023    import org.apache.shiro.session.StoppedSessionException;
024    import org.apache.shiro.util.CollectionUtils;
025    import org.slf4j.Logger;
026    import org.slf4j.LoggerFactory;
027    
028    import java.io.IOException;
029    import java.io.ObjectInputStream;
030    import java.io.ObjectOutputStream;
031    import java.io.Serializable;
032    import java.text.DateFormat;
033    import 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     */
042    public 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    }