1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
19 package org.apache.catalina.session;
20
21 import java.beans.PropertyChangeEvent;
22 import java.beans.PropertyChangeListener;
23 import java.io.BufferedInputStream;
24 import java.io.BufferedOutputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileNotFoundException;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.ObjectInputStream;
31 import java.io.ObjectOutputStream;
32 import java.security.AccessController;
33 import java.security.PrivilegedActionException;
34 import java.security.PrivilegedExceptionAction;
35 import java.util.ArrayList;
36 import java.util.Iterator;
37 import javax.servlet.ServletContext;
38 import org.apache.catalina.Container;
39 import org.apache.catalina.Context;
40 import org.apache.catalina.Globals;
41 import org.apache.catalina.Lifecycle;
42 import org.apache.catalina.LifecycleException;
43 import org.apache.catalina.LifecycleListener;
44 import org.apache.catalina.Loader;
45 import org.apache.catalina.Session;
46 import org.apache.catalina.util.CustomObjectInputStream;
47 import org.apache.catalina.util.LifecycleSupport;
48
49 import org.apache.catalina.security.SecurityUtil;
50 /**
51 * Standard implementation of the <b>Manager</b> interface that provides
52 * simple session persistence across restarts of this component (such as
53 * when the entire server is shut down and restarted, or when a particular
54 * web application is reloaded.
55 * <p>
56 * <b>IMPLEMENTATION NOTE</b>: Correct behavior of session storing and
57 * reloading depends upon external calls to the <code>start()</code> and
58 * <code>stop()</code> methods of this class at the correct times.
59 *
60 * @author Craig R. McClanahan
61 * @author Jean-Francois Arcand
62 * @version $Revision: 612761 $ $Date: 2008-01-17 09:48:45 +0100 (jeu., 17 janv. 2008) $
63 */
64
65 public class StandardManager
66 extends ManagerBase
67 implements Lifecycle, PropertyChangeListener {
68
69 // ---------------------------------------------------- Security Classes
70 private class PrivilegedDoLoad
71 implements PrivilegedExceptionAction {
72
73 PrivilegedDoLoad() {
74 }
75
76 public Object run() throws Exception{
77 doLoad();
78 return null;
79 }
80 }
81
82 private class PrivilegedDoUnload
83 implements PrivilegedExceptionAction {
84
85 PrivilegedDoUnload() {
86 }
87
88 public Object run() throws Exception{
89 doUnload();
90 return null;
91 }
92
93 }
94
95
96 // ----------------------------------------------------- Instance Variables
97
98
99 /**
100 * The descriptive information about this implementation.
101 */
102 protected static final String info = "StandardManager/1.0";
103
104
105 /**
106 * The lifecycle event support for this component.
107 */
108 protected LifecycleSupport lifecycle = new LifecycleSupport(this);
109
110
111 /**
112 * The maximum number of active Sessions allowed, or -1 for no limit.
113 */
114 protected int maxActiveSessions = -1;
115
116
117 /**
118 * The descriptive name of this Manager implementation (for logging).
119 */
120 protected static String name = "StandardManager";
121
122
123 /**
124 * Path name of the disk file in which active sessions are saved
125 * when we stop, and from which these sessions are loaded when we start.
126 * A <code>null</code> value indicates that no persistence is desired.
127 * If this pathname is relative, it will be resolved against the
128 * temporary working directory provided by our context, available via
129 * the <code>javax.servlet.context.tempdir</code> context attribute.
130 */
131 protected String pathname = "SESSIONS.ser";
132
133
134 /**
135 * Has this component been started yet?
136 */
137 protected boolean started = false;
138
139
140 /**
141 * Number of session creations that failed due to maxActiveSessions.
142 */
143 protected int rejectedSessions = 0;
144
145
146 /**
147 * Processing time during session expiration.
148 */
149 protected long processingTime = 0;
150
151
152 // ------------------------------------------------------------- Properties
153
154
155 /**
156 * Set the Container with which this Manager has been associated. If
157 * it is a Context (the usual case), listen for changes to the session
158 * timeout property.
159 *
160 * @param container The associated Container
161 */
162 public void setContainer(Container container) {
163
164 // De-register from the old Container (if any)
165 if ((this.container != null) && (this.container instanceof Context))
166 ((Context) this.container).removePropertyChangeListener(this);
167
168 // Default processing provided by our superclass
169 super.setContainer(container);
170
171 // Register with the new Container (if any)
172 if ((this.container != null) && (this.container instanceof Context)) {
173 setMaxInactiveInterval
174 ( ((Context) this.container).getSessionTimeout()*60 );
175 ((Context) this.container).addPropertyChangeListener(this);
176 }
177
178 }
179
180
181 /**
182 * Return descriptive information about this Manager implementation and
183 * the corresponding version number, in the format
184 * <code><description>/<version></code>.
185 */
186 public String getInfo() {
187
188 return (info);
189
190 }
191
192
193 /**
194 * Return the maximum number of active Sessions allowed, or -1 for
195 * no limit.
196 */
197 public int getMaxActiveSessions() {
198
199 return (this.maxActiveSessions);
200
201 }
202
203
204 /** Number of session creations that failed due to maxActiveSessions
205 *
206 * @return The count
207 */
208 public int getRejectedSessions() {
209 return rejectedSessions;
210 }
211
212
213 public void setRejectedSessions(int rejectedSessions) {
214 this.rejectedSessions = rejectedSessions;
215 }
216
217
218 /**
219 * Set the maximum number of actives Sessions allowed, or -1 for
220 * no limit.
221 *
222 * @param max The new maximum number of sessions
223 */
224 public void setMaxActiveSessions(int max) {
225
226 int oldMaxActiveSessions = this.maxActiveSessions;
227 this.maxActiveSessions = max;
228 support.firePropertyChange("maxActiveSessions",
229 new Integer(oldMaxActiveSessions),
230 new Integer(this.maxActiveSessions));
231
232 }
233
234
235 /**
236 * Return the descriptive short name of this Manager implementation.
237 */
238 public String getName() {
239
240 return (name);
241
242 }
243
244
245 /**
246 * Return the session persistence pathname, if any.
247 */
248 public String getPathname() {
249
250 return (this.pathname);
251
252 }
253
254
255 /**
256 * Set the session persistence pathname to the specified value. If no
257 * persistence support is desired, set the pathname to <code>null</code>.
258 *
259 * @param pathname New session persistence pathname
260 */
261 public void setPathname(String pathname) {
262
263 String oldPathname = this.pathname;
264 this.pathname = pathname;
265 support.firePropertyChange("pathname", oldPathname, this.pathname);
266
267 }
268
269
270 // --------------------------------------------------------- Public Methods
271
272 /**
273 * Construct and return a new session object, based on the default
274 * settings specified by this Manager's properties. The session
275 * id will be assigned by this method, and available via the getId()
276 * method of the returned session. If a new session cannot be created
277 * for any reason, return <code>null</code>.
278 *
279 * @exception IllegalStateException if a new session cannot be
280 * instantiated for any reason
281 */
282 public Session createSession(String sessionId) {
283
284 if ((maxActiveSessions >= 0) &&
285 (sessions.size() >= maxActiveSessions)) {
286 rejectedSessions++;
287 throw new IllegalStateException
288 (sm.getString("standardManager.createSession.ise"));
289 }
290
291 return (super.createSession(sessionId));
292
293 }
294
295
296 /**
297 * Load any currently active sessions that were previously unloaded
298 * to the appropriate persistence mechanism, if any. If persistence is not
299 * supported, this method returns without doing anything.
300 *
301 * @exception ClassNotFoundException if a serialized class cannot be
302 * found during the reload
303 * @exception IOException if an input/output error occurs
304 */
305 public void load() throws ClassNotFoundException, IOException {
306 if (SecurityUtil.isPackageProtectionEnabled()){
307 try{
308 AccessController.doPrivileged( new PrivilegedDoLoad() );
309 } catch (PrivilegedActionException ex){
310 Exception exception = ex.getException();
311 if (exception instanceof ClassNotFoundException){
312 throw (ClassNotFoundException)exception;
313 } else if (exception instanceof IOException){
314 throw (IOException)exception;
315 }
316 if (log.isDebugEnabled())
317 log.debug("Unreported exception in load() "
318 + exception);
319 }
320 } else {
321 doLoad();
322 }
323 }
324
325
326 /**
327 * Load any currently active sessions that were previously unloaded
328 * to the appropriate persistence mechanism, if any. If persistence is not
329 * supported, this method returns without doing anything.
330 *
331 * @exception ClassNotFoundException if a serialized class cannot be
332 * found during the reload
333 * @exception IOException if an input/output error occurs
334 */
335 protected void doLoad() throws ClassNotFoundException, IOException {
336 if (log.isDebugEnabled())
337 log.debug("Start: Loading persisted sessions");
338
339 // Initialize our internal data structures
340 sessions.clear();
341
342 // Open an input stream to the specified pathname, if any
343 File file = file();
344 if (file == null)
345 return;
346 if (log.isDebugEnabled())
347 log.debug(sm.getString("standardManager.loading", pathname));
348 FileInputStream fis = null;
349 ObjectInputStream ois = null;
350 Loader loader = null;
351 ClassLoader classLoader = null;
352 try {
353 fis = new FileInputStream(file.getAbsolutePath());
354 BufferedInputStream bis = new BufferedInputStream(fis);
355 if (container != null)
356 loader = container.getLoader();
357 if (loader != null)
358 classLoader = loader.getClassLoader();
359 if (classLoader != null) {
360 if (log.isDebugEnabled())
361 log.debug("Creating custom object input stream for class loader ");
362 ois = new CustomObjectInputStream(bis, classLoader);
363 } else {
364 if (log.isDebugEnabled())
365 log.debug("Creating standard object input stream");
366 ois = new ObjectInputStream(bis);
367 }
368 } catch (FileNotFoundException e) {
369 if (log.isDebugEnabled())
370 log.debug("No persisted data file found");
371 return;
372 } catch (IOException e) {
373 log.error(sm.getString("standardManager.loading.ioe", e), e);
374 if (ois != null) {
375 try {
376 ois.close();
377 } catch (IOException f) {
378 ;
379 }
380 ois = null;
381 }
382 throw e;
383 }
384
385 // Load the previously unloaded active sessions
386 synchronized (sessions) {
387 try {
388 Integer count = (Integer) ois.readObject();
389 int n = count.intValue();
390 if (log.isDebugEnabled())
391 log.debug("Loading " + n + " persisted sessions");
392 for (int i = 0; i < n; i++) {
393 StandardSession session = getNewSession();
394 session.readObjectData(ois);
395 session.setManager(this);
396 sessions.put(session.getIdInternal(), session);
397 session.activate();
398 sessionCounter++;
399 }
400 } catch (ClassNotFoundException e) {
401 log.error(sm.getString("standardManager.loading.cnfe", e), e);
402 if (ois != null) {
403 try {
404 ois.close();
405 } catch (IOException f) {
406 ;
407 }
408 ois = null;
409 }
410 throw e;
411 } catch (IOException e) {
412 log.error(sm.getString("standardManager.loading.ioe", e), e);
413 if (ois != null) {
414 try {
415 ois.close();
416 } catch (IOException f) {
417 ;
418 }
419 ois = null;
420 }
421 throw e;
422 } finally {
423 // Close the input stream
424 try {
425 if (ois != null)
426 ois.close();
427 } catch (IOException f) {
428 // ignored
429 }
430
431 // Delete the persistent storage file
432 if (file != null && file.exists() )
433 file.delete();
434 }
435 }
436
437 if (log.isDebugEnabled())
438 log.debug("Finish: Loading persisted sessions");
439 }
440
441
442 /**
443 * Save any currently active sessions in the appropriate persistence
444 * mechanism, if any. If persistence is not supported, this method
445 * returns without doing anything.
446 *
447 * @exception IOException if an input/output error occurs
448 */
449 public void unload() throws IOException {
450 if (SecurityUtil.isPackageProtectionEnabled()){
451 try{
452 AccessController.doPrivileged( new PrivilegedDoUnload() );
453 } catch (PrivilegedActionException ex){
454 Exception exception = ex.getException();
455 if (exception instanceof IOException){
456 throw (IOException)exception;
457 }
458 if (log.isDebugEnabled())
459 log.debug("Unreported exception in unLoad() "
460 + exception);
461 }
462 } else {
463 doUnload();
464 }
465 }
466
467
468 /**
469 * Save any currently active sessions in the appropriate persistence
470 * mechanism, if any. If persistence is not supported, this method
471 * returns without doing anything.
472 *
473 * @exception IOException if an input/output error occurs
474 */
475 protected void doUnload() throws IOException {
476
477 if (log.isDebugEnabled())
478 log.debug("Unloading persisted sessions");
479
480 // Open an output stream to the specified pathname, if any
481 File file = file();
482 if (file == null)
483 return;
484 if (log.isDebugEnabled())
485 log.debug(sm.getString("standardManager.unloading", pathname));
486 FileOutputStream fos = null;
487 ObjectOutputStream oos = null;
488 try {
489 fos = new FileOutputStream(file.getAbsolutePath());
490 oos = new ObjectOutputStream(new BufferedOutputStream(fos));
491 } catch (IOException e) {
492 log.error(sm.getString("standardManager.unloading.ioe", e), e);
493 if (oos != null) {
494 try {
495 oos.close();
496 } catch (IOException f) {
497 ;
498 }
499 oos = null;
500 }
501 throw e;
502 }
503
504 // Write the number of active sessions, followed by the details
505 ArrayList list = new ArrayList();
506 synchronized (sessions) {
507 if (log.isDebugEnabled())
508 log.debug("Unloading " + sessions.size() + " sessions");
509 try {
510 oos.writeObject(new Integer(sessions.size()));
511 Iterator elements = sessions.values().iterator();
512 while (elements.hasNext()) {
513 StandardSession session =
514 (StandardSession) elements.next();
515 list.add(session);
516 ((StandardSession) session).passivate();
517 session.writeObjectData(oos);
518 }
519 } catch (IOException e) {
520 log.error(sm.getString("standardManager.unloading.ioe", e), e);
521 if (oos != null) {
522 try {
523 oos.close();
524 } catch (IOException f) {
525 ;
526 }
527 oos = null;
528 }
529 throw e;
530 }
531 }
532
533 // Flush and close the output stream
534 try {
535 oos.flush();
536 oos.close();
537 oos = null;
538 } catch (IOException e) {
539 if (oos != null) {
540 try {
541 oos.close();
542 } catch (IOException f) {
543 ;
544 }
545 oos = null;
546 }
547 throw e;
548 }
549
550 // Expire all the sessions we just wrote
551 if (log.isDebugEnabled())
552 log.debug("Expiring " + list.size() + " persisted sessions");
553 Iterator expires = list.iterator();
554 while (expires.hasNext()) {
555 StandardSession session = (StandardSession) expires.next();
556 try {
557 session.expire(false);
558 } catch (Throwable t) {
559 ;
560 } finally {
561 session.recycle();
562 }
563 }
564
565 if (log.isDebugEnabled())
566 log.debug("Unloading complete");
567
568 }
569
570
571 // ------------------------------------------------------ Lifecycle Methods
572
573
574 /**
575 * Add a lifecycle event listener to this component.
576 *
577 * @param listener The listener to add
578 */
579 public void addLifecycleListener(LifecycleListener listener) {
580
581 lifecycle.addLifecycleListener(listener);
582
583 }
584
585
586 /**
587 * Get the lifecycle listeners associated with this lifecycle. If this
588 * Lifecycle has no listeners registered, a zero-length array is returned.
589 */
590 public LifecycleListener[] findLifecycleListeners() {
591
592 return lifecycle.findLifecycleListeners();
593
594 }
595
596
597 /**
598 * Remove a lifecycle event listener from this component.
599 *
600 * @param listener The listener to remove
601 */
602 public void removeLifecycleListener(LifecycleListener listener) {
603
604 lifecycle.removeLifecycleListener(listener);
605
606 }
607
608 /**
609 * Prepare for the beginning of active use of the public methods of this
610 * component. This method should be called after <code>configure()</code>,
611 * and before any of the public methods of the component are utilized.
612 *
613 * @exception LifecycleException if this component detects a fatal error
614 * that prevents this component from being used
615 */
616 public void start() throws LifecycleException {
617
618 if( ! initialized )
619 init();
620
621 // Validate and update our current component state
622 if (started) {
623 return;
624 }
625 lifecycle.fireLifecycleEvent(START_EVENT, null);
626 started = true;
627
628 // Force initialization of the random number generator
629 if (log.isDebugEnabled())
630 log.debug("Force random number initialization starting");
631 String dummy = generateSessionId();
632 if (log.isDebugEnabled())
633 log.debug("Force random number initialization completed");
634
635 // Load unloaded sessions, if any
636 try {
637 load();
638 } catch (Throwable t) {
639 log.error(sm.getString("standardManager.managerLoad"), t);
640 }
641
642 }
643
644
645 /**
646 * Gracefully terminate the active use of the public methods of this
647 * component. This method should be the last one called on a given
648 * instance of this component.
649 *
650 * @exception LifecycleException if this component detects a fatal error
651 * that needs to be reported
652 */
653 public void stop() throws LifecycleException {
654
655 if (log.isDebugEnabled())
656 log.debug("Stopping");
657
658 // Validate and update our current component state
659 if (!started)
660 throw new LifecycleException
661 (sm.getString("standardManager.notStarted"));
662 lifecycle.fireLifecycleEvent(STOP_EVENT, null);
663 started = false;
664
665 // Write out sessions
666 try {
667 unload();
668 } catch (Throwable t) {
669 log.error(sm.getString("standardManager.managerUnload"), t);
670 }
671
672 // Expire all active sessions
673 Session sessions[] = findSessions();
674 for (int i = 0; i < sessions.length; i++) {
675 Session session = sessions[i];
676 try {
677 if (session.isValid()) {
678 session.expire();
679 }
680 } catch (Throwable t) {
681 ;
682 } finally {
683 // Measure against memory leaking if references to the session
684 // object are kept in a shared field somewhere
685 session.recycle();
686 }
687 }
688
689 // Require a new random number generator if we are restarted
690 this.random = null;
691
692 if( initialized ) {
693 destroy();
694 }
695 }
696
697
698 // ----------------------------------------- PropertyChangeListener Methods
699
700
701 /**
702 * Process property change events from our associated Context.
703 *
704 * @param event The property change event that has occurred
705 */
706 public void propertyChange(PropertyChangeEvent event) {
707
708 // Validate the source of this event
709 if (!(event.getSource() instanceof Context))
710 return;
711 Context context = (Context) event.getSource();
712
713 // Process a relevant property change
714 if (event.getPropertyName().equals("sessionTimeout")) {
715 try {
716 setMaxInactiveInterval
717 ( ((Integer) event.getNewValue()).intValue()*60 );
718 } catch (NumberFormatException e) {
719 log.error(sm.getString("standardManager.sessionTimeout",
720 event.getNewValue().toString()));
721 }
722 }
723
724 }
725
726
727 // ------------------------------------------------------ Protected Methods
728
729
730 /**
731 * Return a File object representing the pathname to our
732 * persistence file, if any.
733 */
734 protected File file() {
735
736 if ((pathname == null) || (pathname.length() == 0))
737 return (null);
738 File file = new File(pathname);
739 if (!file.isAbsolute()) {
740 if (container instanceof Context) {
741 ServletContext servletContext =
742 ((Context) container).getServletContext();
743 File tempdir = (File)
744 servletContext.getAttribute(Globals.WORK_DIR_ATTR);
745 if (tempdir != null)
746 file = new File(tempdir, pathname);
747 }
748 }
749 // if (!file.isAbsolute())
750 // return (null);
751 return (file);
752
753 }
754 }