/*
* Copyright 2009 Martin Grotzke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package de.javakaffee.web.msm;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.catalina.session.StandardSession;
import org.apache.catalina.util.CustomObjectInputStream;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import de.javakaffee.web.msm.MemcachedSessionService.SessionManager;
/**
* A {@link SessionAttributesTranscoder} that serializes catalina {@link StandardSession}s using
* java serialization (and the serialization logic of {@link StandardSession} as
* found in {@link StandardSession#writeObjectData(ObjectOutputStream)} and
* {@link StandardSession#readObjectData(ObjectInputStream)}).
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
* @version $Id$
*/
public class JavaSerializationTranscoder implements SessionAttributesTranscoder {
private static final Log LOG = LogFactory.getLog( JavaSerializationTranscoder.class );
private static final String EMPTY_ARRAY[] = new String[0];
/**
* The dummy attribute value serialized when a NotSerializableException is
* encountered in <code>writeObject()</code>.
*/
protected static final String NOT_SERIALIZED = "___NOT_SERIALIZABLE_EXCEPTION___";
private final SessionManager _manager;
/**
* Constructor.
*
* @param manager
* the manager
*/
public JavaSerializationTranscoder() {
this( null );
}
/**
* Constructor.
*
* @param manager
* the manager
*/
public JavaSerializationTranscoder( final SessionManager manager ) {
_manager = manager;
}
/**
* {@inheritDoc}
*/
@Override
public byte[] serializeAttributes( final MemcachedBackupSession session, final ConcurrentMap<String, Object> attributes ) {
if ( attributes == null ) {
throw new NullPointerException( "Can't serialize null" );
}
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream( bos );
writeAttributes( session, attributes, oos );
return bos.toByteArray();
} catch ( final IOException e ) {
throw new IllegalArgumentException( "Non-serializable object", e );
} finally {
closeSilently( bos );
closeSilently( oos );
}
}
private void writeAttributes( final MemcachedBackupSession session, final Map<String, Object> attributes,
final ObjectOutputStream oos ) throws IOException {
// Accumulate the names of serializable and non-serializable attributes
final String keys[] = attributes.keySet().toArray( EMPTY_ARRAY );
final List<String> saveNames = new ArrayList<String>();
final List<Object> saveValues = new ArrayList<Object>();
for ( int i = 0; i < keys.length; i++ ) {
final Object value = attributes.get( keys[i] );
if ( value == null || session.exclude( keys[i], value ) ) {
continue;
} else if ( value instanceof Serializable ) {
saveNames.add( keys[i] );
saveValues.add( value );
} else {
if ( LOG.isDebugEnabled() ) {
LOG.debug( "Ignoring attribute '" + keys[i] + "' as it does not implement Serializable" );
}
}
}
// Serialize the attribute count and the Serializable attributes
final int n = saveNames.size();
oos.writeObject( Integer.valueOf( n ) );
for ( int i = 0; i < n; i++ ) {
oos.writeObject( saveNames.get( i ) );
try {
oos.writeObject( saveValues.get( i ) );
if ( LOG.isDebugEnabled() ) {
LOG.debug( " storing attribute '" + saveNames.get( i ) + "' with value '" + saveValues.get( i ) + "'" );
}
} catch ( final NotSerializableException e ) {
LOG.warn( _manager.getString( "standardSession.notSerializable", saveNames.get( i ), session.getIdInternal() ), e );
oos.writeObject( NOT_SERIALIZED );
if ( LOG.isDebugEnabled() ) {
LOG.debug( " storing attribute '" + saveNames.get( i ) + "' with value NOT_SERIALIZED" );
}
}
}
}
/**
* Get the object represented by the given serialized bytes.
*
* @param in
* the bytes to deserialize
* @return the resulting object
*/
@Override
public ConcurrentMap<String, Object> deserializeAttributes(final byte[] in ) {
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
bis = new ByteArrayInputStream( in );
ois = createObjectInputStream( bis );
final ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<String, Object>();
final int n = ( (Integer) ois.readObject() ).intValue();
for ( int i = 0; i < n; i++ ) {
final String name = (String) ois.readObject();
final Object value = ois.readObject();
if ( ( value instanceof String ) && ( value.equals( NOT_SERIALIZED ) ) ) {
continue;
}
if ( LOG.isDebugEnabled() ) {
LOG.debug( " loading attribute '" + name + "' with value '" + value + "'" );
}
attributes.put( name, value );
}
return attributes;
} catch ( final ClassNotFoundException e ) {
LOG.warn( "Caught CNFE decoding "+ in.length +" bytes of data", e );
throw new TranscoderDeserializationException( "Caught CNFE decoding data", e );
} catch ( final IOException e ) {
LOG.warn( "Caught IOException decoding "+ in.length +" bytes of data", e );
throw new TranscoderDeserializationException( "Caught IOException decoding data", e );
} finally {
closeSilently( bis );
closeSilently( ois );
}
}
private ObjectInputStream createObjectInputStream( final ByteArrayInputStream bis ) throws IOException {
final ObjectInputStream ois;
ClassLoader classLoader = null;
if ( _manager != null && _manager.getContext() != null ) {
classLoader = _manager.getContainerClassLoader();
}
if ( classLoader != null ) {
ois = new CustomObjectInputStream( bis, classLoader );
} else {
ois = new ObjectInputStream( bis );
}
return ois;
}
private void closeSilently( final OutputStream os ) {
if ( os != null ) {
try {
os.close();
} catch ( final IOException f ) {
// fail silently
}
}
}
private void closeSilently( final InputStream is ) {
if ( is != null ) {
try {
is.close();
} catch ( final IOException f ) {
// fail silently
}
}
}
}