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