001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.web.session;
017
018import org.apache.commons.logging.Log;
019import org.apache.commons.logging.LogFactory;
020import org.kuali.rice.core.api.config.property.Config;
021import org.kuali.rice.core.api.config.property.ConfigContext;
022
023import javax.servlet.http.HttpSessionAttributeListener;
024import javax.servlet.http.HttpSessionBindingEvent;
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.ObjectOutputStream;
028import java.io.Serializable;
029
030//TODO: May want to have a way to turn this off that way we aren't incurring the overhead
031//of testing every session object if it is serializable by actually serializing it!
032
033/** A session listener that detects when a non-serializable attributes is added to session. **/
034public class NonSerializableSessionListener implements HttpSessionAttributeListener {
035    private static final Log LOG = LogFactory.getLog(NonSerializableSessionListener.class);
036    private static final String ENABLE_SERIALIZATION_CHECK = "enableSerializationCheck";
037    private Boolean serializationCheckEnabled;
038
039    @Override
040    public void attributeAdded(HttpSessionBindingEvent se) {
041        logSerializationViolations(se, "added");
042    }
043
044    @Override
045    public void attributeRemoved(HttpSessionBindingEvent se) {
046        //do nothing
047    }
048
049    @Override
050    public void attributeReplaced(HttpSessionBindingEvent se) {
051        logSerializationViolations(se, "replaced");
052    }
053
054    /**
055     * Tests and logs serialization violations in non-production environments
056     */
057    private void logSerializationViolations(HttpSessionBindingEvent se, String action) {
058        if (!productionEnvironmentDetected() && isSerializationCheckEnabled()) {
059            checkSerialization(se, action);
060        }
061    }
062
063    /**
064     * Determines whether we are running in a production environment.  Factored out for testability.
065     */
066    private static boolean productionEnvironmentDetected() {
067        Config c = ConfigContext.getCurrentContextConfig();
068        return c != null && c.isProductionEnvironment();
069    }
070
071    /**
072     * Determines whether we are running in a production environment.  Factored out for testability.
073     */
074    private Boolean isSerializationCheckEnabled() {
075        if (serializationCheckEnabled == null) {
076            Config c = ConfigContext.getCurrentContextConfig();
077            serializationCheckEnabled = c != null && c.getBooleanProperty(ENABLE_SERIALIZATION_CHECK);
078        }
079        return serializationCheckEnabled;
080    }
081
082
083
084    /**
085     * Tests whether the attribute value is serializable and logs an error if it isn't.  Note, this can be expensive
086     * so we avoid it in production environments.
087     * @param se the session binding event
088     * @param action the listener event for logging purposes (added or replaced)
089     */
090    protected void checkSerialization(final HttpSessionBindingEvent se, String action) {
091        final Object o = se.getValue();
092        if(o != null) {
093            if (!isSerializable(o)) {
094                LOG.error("Attribute of class " + o.getClass().getName() + " with name " + se.getName() + " from source " + se.getSource().getClass().getName() + " was " + action + " to session and does not implement " + Serializable.class.getName());
095            } else if (!canBeSerialized((Serializable) o)){
096                LOG.error("Attribute of class " + o.getClass().getName() + " with name " + se.getName() + " from source " + se.getSource().getClass().getName() + " was " + action + " to session and cannot be Serialized");
097            }
098        }
099    }
100
101    /**
102     * Simply tests whether the object implements the Serializable interface
103     */
104    private static boolean isSerializable(Object o) {
105        return o instanceof Serializable;
106    }
107
108    /**
109     * Performs an expensive test of serializability by attempting to serialize the object graph
110     */
111    private static boolean canBeSerialized(Serializable o) {
112        ByteArrayOutputStream baos = null;
113        ObjectOutputStream out = null;
114        try {
115            baos = new ByteArrayOutputStream(512);
116            out = new ObjectOutputStream(baos);
117            out.writeObject((Serializable) o);
118            return true;
119        } catch (IOException e) {
120            LOG.warn("error serializing object" , e);
121        } finally {
122            try {
123                if (baos != null) {
124                    try {
125                        baos.close();
126                    } catch (IOException e) {
127                        LOG.warn("error closing stream" , e);
128                    }
129                }
130            } finally {
131                if (out != null) {
132                    try {
133                        out.close();
134                    } catch (IOException e) {
135                         LOG.warn("error closing stream" , e);
136                    }
137                }
138            }
139        }
140
141        return false;
142    }
143}