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.wiki.auth;
020
021import org.apache.log4j.Logger;
022import org.apache.wiki.api.core.Engine;
023import org.apache.wiki.api.core.Session;
024import org.apache.wiki.api.spi.Wiki;
025import org.apache.wiki.event.WikiEventListener;
026import org.apache.wiki.event.WikiEventManager;
027import org.apache.wiki.event.WikiSecurityEvent;
028import org.apache.wiki.util.comparators.PrincipalComparator;
029
030import javax.servlet.http.HttpServletRequest;
031import javax.servlet.http.HttpSession;
032import javax.servlet.http.HttpSessionEvent;
033import javax.servlet.http.HttpSessionListener;
034import java.security.Principal;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.Collection;
038import java.util.Map;
039import java.util.WeakHashMap;
040import java.util.concurrent.ConcurrentHashMap;
041
042/**
043 *  <p>Manages Sessions for different Engines.</p>
044 *  <p>The Sessions are stored both in the remote user HttpSession and in the SessionMonitor for the Engine.
045 *  This class must be configured as a session listener in the web.xml for the wiki web application.</p>
046 */
047public class SessionMonitor implements HttpSessionListener {
048
049    private static final Logger log = Logger.getLogger( SessionMonitor.class );
050
051    /** Map with Engines as keys, and SessionMonitors as values. */
052    private static final ConcurrentHashMap< Engine, SessionMonitor > c_monitors = new ConcurrentHashMap<>();
053
054    /** Weak hashmap with HttpSessions as keys, and WikiSessions as values. */
055    private final Map< String, Session > m_sessions = new WeakHashMap<>();
056
057    private Engine m_engine;
058
059    private final PrincipalComparator m_comparator = new PrincipalComparator();
060
061    /**
062     * Returns the instance of the SessionMonitor for this wiki. Only one SessionMonitor exists per Engine.
063     *
064     * @param engine the wiki engine
065     * @return the session monitor
066     */
067    public static SessionMonitor getInstance( final Engine engine ) {
068        if( engine == null ) {
069            throw new IllegalArgumentException( "Engine cannot be null." );
070        }
071        SessionMonitor monitor = c_monitors.get( engine );
072        if( monitor == null ) {
073            monitor = new SessionMonitor( engine );
074            c_monitors.put( engine, monitor );
075        }
076
077        return monitor;
078    }
079
080    /** Construct the SessionListener */
081    public SessionMonitor() {
082    }
083
084    private SessionMonitor( final Engine engine ) {
085        m_engine = engine;
086    }
087
088    /**
089     *  Just looks for a WikiSession; does not create a new one.
090     * This method may return <code>null</code>, <em>and
091     * callers should check for this value</em>.
092     *
093     *  @param session the user's HTTP session
094     *  @return the WikiSession, if found
095     */
096    private Session findSession( final HttpSession session ) {
097        final String sid = ( session == null ) ? "(null)" : session.getId();
098        return findSession( sid );
099    }
100
101    /**
102     *  Just looks for a WikiSession; does not create a new one.
103     * This method may return <code>null</code>, <em>and
104     * callers should check for this value</em>.
105     *
106     *  @param sessionId the user's HTTP session id
107     *  @return the WikiSession, if found
108     */
109    private Session findSession( final String sessionId ) {
110        Session wikiSession = null;
111        final String sid = ( sessionId == null ) ? "(null)" : sessionId;
112        final Session storedSession = m_sessions.get( sid );
113
114        // If the weak reference returns a wiki session, return it
115        if( storedSession != null ) {
116            if( log.isDebugEnabled() ) {
117                log.debug( "Looking up WikiSession for session ID=" + sid + "... found it" );
118            }
119            wikiSession = storedSession;
120        }
121
122        return wikiSession;
123    }
124
125    /**
126     * <p>Looks up the wiki session associated with a user's Http session and adds it to the session cache. This method will return the
127     * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently
128     * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
129     * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are
130     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
131     *
132     * @param session the HTTP session
133     * @return the wiki session
134     */
135    public final Session find( final HttpSession session ) {
136        final Session wikiSession = findSession( session );
137        final String sid = ( session == null ) ? "(null)" : session.getId();
138        if( wikiSession == null ) {
139            return createGuestSessionFor( sid );
140        }
141
142        return wikiSession;
143    }
144
145    /**
146     * <p>Looks up the wiki session associated with a user's Http session and adds it to the session cache. This method will return the
147     * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently
148     * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
149     * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are
150     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
151     *
152     * @param sessionId the HTTP session
153     * @return the wiki session
154     */
155    public final Session find( final String sessionId ) {
156        final Session wikiSession = findSession( sessionId );
157        if( wikiSession == null ) {
158            return createGuestSessionFor( sessionId );
159        }
160
161        return wikiSession;
162    }
163
164    /**
165     * Creates a new session and stashes it
166     *
167     * @param sessionId id looked for before creating the guest session
168     * @return a new guest session
169     */
170    private Session createGuestSessionFor( final String sessionId ) {
171        if( log.isDebugEnabled() ) {
172            log.debug( "Session for session ID=" + sessionId + "... not found. Creating guestSession()" );
173        }
174        final Session wikiSession = Wiki.session().guest( m_engine );
175        synchronized( m_sessions ) {
176            m_sessions.put( sessionId, wikiSession );
177        }
178        return wikiSession;
179    }
180
181    /**
182     * Removes the wiki session associated with the user's HttpRequest from the session cache.
183     *
184     * @param request the user's HTTP request
185     */
186    public final void remove( final HttpServletRequest request ) {
187        if( request == null ) {
188            throw new IllegalArgumentException( "Request cannot be null." );
189        }
190        remove( request.getSession() );
191    }
192
193    /**
194     * Removes the wiki session associated with the user's HttpSession from the session cache.
195     *
196     * @param session the user's HTTP session
197     */
198    public final void remove( final HttpSession session ) {
199        if( session == null ) {
200            throw new IllegalArgumentException( "Session cannot be null." );
201        }
202        synchronized( m_sessions ) {
203            m_sessions.remove( session.getId() );
204        }
205    }
206
207    /**
208     * Returns the current number of active wiki sessions.
209     * @return the number of sessions
210     */
211    public final int sessions()
212    {
213        return userPrincipals().length;
214    }
215
216    /**
217     * <p>Returns the current wiki users as a sorted array of Principal objects. The principals are those returned by
218     * each WikiSession's {@link Session#getUserPrincipal()}'s method.</p>
219     * <p>To obtain the list of current WikiSessions, we iterate through our session Map and obtain the list of values,
220     * which are WikiSessions wrapped in {@link java.lang.ref.WeakReference} objects. Those <code>WeakReference</code>s
221     * whose <code>get()</code> method returns non-<code>null</code> values are valid sessions.</p>
222     *
223     * @return the array of user principals
224     */
225    public final Principal[] userPrincipals() {
226        final Collection<Principal> principals = new ArrayList<>();
227        synchronized ( m_sessions ) {
228            for ( final Session session : m_sessions.values()) {
229                principals.add( session.getUserPrincipal() );
230            }
231        }
232        final Principal[] p = principals.toArray( new Principal[ principals.size() ] );
233        Arrays.sort( p, m_comparator );
234        return p;
235    }
236
237    /**
238     * Registers a WikiEventListener with this instance.
239     *
240     * @param listener the event listener
241     * @since 2.4.75
242     */
243    public final synchronized void addWikiEventListener( final WikiEventListener listener ) {
244        WikiEventManager.addWikiEventListener( this, listener );
245    }
246
247    /**
248     * Un-registers a WikiEventListener with this instance.
249     *
250     * @param listener the event listener
251     * @since 2.4.75
252     */
253    public final synchronized void removeWikiEventListener( final WikiEventListener listener ) {
254        WikiEventManager.removeWikiEventListener( this, listener );
255    }
256
257    /**
258     * Fires a WikiSecurityEvent to all registered listeners.
259     *
260     * @param type  the event type
261     * @param principal the user principal associated with this session
262     * @param session the wiki session
263     * @since 2.4.75
264     */
265    protected final void fireEvent( final int type, final Principal principal, final Session session ) {
266        if( WikiEventManager.isListening( this ) ) {
267            WikiEventManager.fireEvent( this, new WikiSecurityEvent( this, type, principal, session ) );
268        }
269    }
270
271    /**
272     * Fires when the web container creates a new HTTP session.
273     * 
274     * @param se the HTTP session event
275     */
276    @Override
277    public void sessionCreated( final HttpSessionEvent se ) {
278        final HttpSession session = se.getSession();
279        log.debug( "Created session: " + session.getId() + "." );
280    }
281
282    /**
283     * Removes the user's WikiSession from the internal session cache when the web
284     * container destroys an HTTP session.
285     * @param se the HTTP session event
286     */
287    @Override
288    public void sessionDestroyed( final HttpSessionEvent se ) {
289        final HttpSession session = se.getSession();
290        for( final SessionMonitor monitor : c_monitors.values() ) {
291            final Session storedSession = monitor.findSession( session );
292            monitor.remove( session );
293            log.debug( "Removed session " + session.getId() + "." );
294            if( storedSession != null ) {
295                fireEvent( WikiSecurityEvent.SESSION_EXPIRED, storedSession.getLoginPrincipal(), storedSession );
296            }
297        }
298    }
299
300}