001/**
002 * Copyright 2005-2018 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.listener;
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/**
031 * A session listener that detects when a non-serializable attributes is added to session.
032 *
033 * @author Kuali Rice Team (rice.collab@kuali.org)
034 */
035public class NonSerializableSessionListener implements HttpSessionAttributeListener {
036    private static final Log LOG = LogFactory.getLog(NonSerializableSessionListener.class);
037    private static final String ENABLE_SERIALIZATION_CHECK = "enableSerializationCheck";
038    private Boolean serializationCheckEnabled;
039
040    @Override
041    public void attributeAdded(HttpSessionBindingEvent se) {
042        logSerializationViolations(se, "added");
043    }
044
045    @Override
046    public void attributeRemoved(HttpSessionBindingEvent se) {
047        //do nothing
048    }
049
050    @Override
051    public void attributeReplaced(HttpSessionBindingEvent se) {
052        logSerializationViolations(se, "replaced");
053    }
054
055    /**
056     * Tests and logs serialization violations in non-production environments
057     */
058    private void logSerializationViolations(HttpSessionBindingEvent se, String action) {
059        if (!productionEnvironmentDetected() && isSerializationCheckEnabled()) {
060            checkSerialization(se, action);
061        }
062    }
063
064    /**
065     * Determines whether we are running in a production environment.  Factored out for testability.
066     */
067    private static boolean productionEnvironmentDetected() {
068        Config c = ConfigContext.getCurrentContextConfig();
069        return c != null && c.isProductionEnvironment();
070    }
071
072    /**
073     * Determines whether we are running in a production environment.  Factored out for testability.
074     */
075    private Boolean isSerializationCheckEnabled() {
076        if (serializationCheckEnabled == null) {
077            Config c = ConfigContext.getCurrentContextConfig();
078            serializationCheckEnabled = c != null && c.getBooleanProperty(ENABLE_SERIALIZATION_CHECK);
079        }
080        return serializationCheckEnabled;
081    }
082
083
084
085    /**
086     * Tests whether the attribute value is serializable and logs an error if it isn't.  Note, this can be expensive
087     * so we avoid it in production environments.
088     * @param se the session binding event
089     * @param action the listener event for logging purposes (added or replaced)
090     */
091    protected void checkSerialization(final HttpSessionBindingEvent se, String action) {
092        final Object o = se.getValue();
093        if(o != null) {
094            if (!isSerializable(o)) {
095                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());
096            } else if (!canBeSerialized((Serializable) o)){
097                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");
098            }
099        }
100    }
101
102    /**
103     * Simply tests whether the object implements the Serializable interface
104     */
105    private static boolean isSerializable(Object o) {
106        return o instanceof Serializable;
107    }
108
109    /**
110     * Performs an expensive test of serializability by attempting to serialize the object graph
111     */
112    private static boolean canBeSerialized(Serializable o) {
113        ByteArrayOutputStream baos = null;
114        ObjectOutputStream out = null;
115        try {
116            baos = new ByteArrayOutputStream(512);
117            out = new ObjectOutputStream(baos);
118            out.writeObject((Serializable) o);
119            return true;
120        } catch (IOException e) {
121            LOG.warn("error serializing object" , e);
122        } finally {
123            try {
124                if (baos != null) {
125                    try {
126                        baos.close();
127                    } catch (IOException e) {
128                        LOG.warn("error closing stream" , e);
129                    }
130                }
131            } finally {
132                if (out != null) {
133                    try {
134                        out.close();
135                    } catch (IOException e) {
136                         LOG.warn("error closing stream" , e);
137                    }
138                }
139            }
140        }
141
142        return false;
143    }
144}