/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.wicket.serialize.java;
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.ObjectStreamClass;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import org.apache.wicket.Application;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.core.util.objects.checker.CheckingObjectOutputStream;
import org.apache.wicket.core.util.objects.checker.ObjectSerializationChecker;
import org.apache.wicket.serialize.ISerializer;
import org.apache.wicket.settings.ApplicationSettings;
import org.apache.wicket.util.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An implementation of {@link ISerializer} based on Java Serialization (ObjectOutputStream,
* ObjectInputStream)
*
* Requires the application key to enable serialization and deserialisation outside thread in which
* application thread local is set
*/
public class JavaSerializer implements ISerializer
{
private static final Logger log = LoggerFactory.getLogger(JavaSerializer.class);
/**
* The key of the application which can be used later to find the proper {@link IClassResolver}
*/
private final String applicationKey;
/**
* Construct.
*
* @param applicationKey
* the name of the application
*/
public JavaSerializer(final String applicationKey)
{
this.applicationKey = applicationKey;
}
@Override
public byte[] serialize(final Object object)
{
try
{
final ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
try
{
oos = newObjectOutputStream(out);
oos.writeObject(applicationKey);
oos.writeObject(object);
}
finally
{
try
{
IOUtils.close(oos);
}
finally
{
out.close();
}
}
return out.toByteArray();
}
catch (Exception e)
{
log.error("Error serializing object " + object.getClass() + " [object=" + object + "]",
e);
}
return null;
}
@Override
public Object deserialize(final byte[] data)
{
ThreadContext old = ThreadContext.get(false);
final ByteArrayInputStream in = new ByteArrayInputStream(data);
ObjectInputStream ois = null;
try
{
Application oldApplication = ThreadContext.getApplication();
try
{
ois = newObjectInputStream(in);
String applicationName = (String)ois.readObject();
if (applicationName != null)
{
Application app = Application.get(applicationName);
if (app != null)
{
ThreadContext.setApplication(app);
}
}
return ois.readObject();
}
finally
{
try
{
ThreadContext.setApplication(oldApplication);
IOUtils.close(ois);
}
finally
{
in.close();
}
}
}
catch (ClassNotFoundException | IOException cnfx)
{
throw new RuntimeException("Could not deserialize object from byte[]", cnfx);
}
finally
{
ThreadContext.restore(old);
}
}
/**
* Gets a new instance of an {@link ObjectInputStream} with the provided {@link InputStream}.
*
* @param in
* The input stream that should be used for the reading
* @return a new object input stream instance
* @throws IOException
* if an I/O error occurs while reading stream header
*/
protected ObjectInputStream newObjectInputStream(InputStream in) throws IOException
{
return new ClassResolverObjectInputStream(in);
}
/**
* Gets a new instance of an {@link ObjectOutputStream} with the provided {@link OutputStream}.
*
* @param out
* The output stream that should be used for the writing
* @return a new object output stream instance
* @throws IOException
* if an I/O error occurs while writing stream header
*/
protected ObjectOutputStream newObjectOutputStream(OutputStream out) throws IOException
{
return new SerializationCheckerObjectOutputStream(out);
}
/**
* Extend {@link ObjectInputStream} to add framework class resolution logic.
*/
private static class ClassResolverObjectInputStream extends ObjectInputStream
{
public ClassResolverObjectInputStream(InputStream in) throws IOException
{
super(in);
}
// This override is required to resolve classes inside in different bundle, i.e.
// The classes can be resolved by OSGI classresolver implementation
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
ClassNotFoundException
{
try
{
return super.resolveClass(desc);
}
catch (ClassNotFoundException cnfEx)
{
// ignore this exception.
log.debug(
"Class not found by the object outputstream itself, trying the IClassResolver");
Class< ? > candidate = resolveClassInWicket(desc.getName());
if (candidate == null)
{
throw cnfEx;
}
return candidate;
}
}
/*
* resolves a class by name, first using the default Class.forName, but looking in the
* Wicket ClassResolvers as well.
*/
private Class<?> resolveClassByName(String className, ClassLoader latestUserDefined)
throws ClassNotFoundException
{
try
{
return Class.forName(className, false, latestUserDefined);
}
catch (ClassNotFoundException cnfEx)
{
Class<?> ret = resolveClassInWicket(className);
if (ret == null)
throw cnfEx;
return ret;
}
}
/*
* Resolves a class from Wicket's ClassResolver
*/
private Class<?> resolveClassInWicket(String className) throws ClassNotFoundException
{
Class<?> candidate;
try
{
Application application = Application.get();
ApplicationSettings applicationSettings = application.getApplicationSettings();
IClassResolver classResolver = applicationSettings.getClassResolver();
candidate = classResolver.resolveClass(className);
}
catch (WicketRuntimeException ex)
{
if (ex.getCause() instanceof ClassNotFoundException)
{
throw (ClassNotFoundException)ex.getCause();
}
else
{
ClassNotFoundException wrapperCnf = new ClassNotFoundException();
wrapperCnf.initCause(ex);
throw wrapperCnf;
}
}
return candidate;
}
/*
* This method is an a copy of the super-method, with Class.forName replaced with a call to
* resolveClassByName.
*/
@Override
protected Class<?> resolveProxyClass(String[] interfaces)
throws ClassNotFoundException, IOException
{
try
{
return super.resolveProxyClass(interfaces);
}
catch (ClassNotFoundException cnfEx)
{
// ignore this exception.
log.debug(
"Proxy Class not found by the object outputstream itself, trying the IClassResolver");
ClassLoader latestLoader = latestUserDefinedLoader();
ClassLoader nonPublicLoader = null;
boolean hasNonPublicInterface = false;
// define proxy in class loader of non-public interface(s), if any
Class<?>[] classObjs = new Class<?>[interfaces.length];
for (int i = 0; i < interfaces.length; i++)
{
Class<?> cl = resolveClassByName(interfaces[i], latestLoader);
if ((cl.getModifiers() & Modifier.PUBLIC) == 0)
{
if (hasNonPublicInterface)
{
if (nonPublicLoader != cl.getClassLoader())
{
throw new IllegalAccessError(
"conflicting non-public interface class loaders");
}
}
else
{
nonPublicLoader = cl.getClassLoader();
hasNonPublicInterface = true;
}
}
classObjs[i] = cl;
}
try
{
return Proxy.getProxyClass(
hasNonPublicInterface ? nonPublicLoader : latestLoader, classObjs);
}
catch (IllegalArgumentException e)
{
throw new ClassNotFoundException(null, e);
}
}
}
/*
* Method in the superclass is private, call it via reflection.
*/
private static ClassLoader latestUserDefinedLoader()
{
try
{
Method originalMethod =
ObjectInputStream.class.getDeclaredMethod("latestUserDefinedLoader");
originalMethod.setAccessible(true);
return (ClassLoader) originalMethod.invoke(null);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e)
{
// should not happen
throw new WicketRuntimeException(e);
}
}
}
/**
* Write objects to the wrapped output stream and log a meaningful message for serialization
* problems.
*
* <p>
* Note: the checking functionality is used only if the serialization fails with NotSerializableException.
* This is done so to save some CPU time to make the checks for no reason.
* </p>
*/
private static class SerializationCheckerObjectOutputStream extends ObjectOutputStream
{
private final OutputStream outputStream;
private final ObjectOutputStream oos;
private SerializationCheckerObjectOutputStream(OutputStream outputStream) throws IOException
{
this.outputStream = outputStream;
oos = new ObjectOutputStream(outputStream);
}
@Override
protected final void writeObjectOverride(Object obj) throws IOException
{
try
{
oos.writeObject(obj);
}
catch (NotSerializableException nsx)
{
if (CheckingObjectOutputStream.isAvailable())
{
try
{
// trigger serialization again, but this time gather some more info
CheckingObjectOutputStream checkingObjectOutputStream =
new CheckingObjectOutputStream(outputStream, new ObjectSerializationChecker(nsx));
checkingObjectOutputStream.writeObject(obj);
}
catch (Exception x)
{
if (x instanceof CheckingObjectOutputStream.ObjectCheckException)
{
throw (CheckingObjectOutputStream.ObjectCheckException) x;
}
else
{
x.initCause(nsx);
throw new WicketRuntimeException("A problem occurred while trying to collect debug information about not serializable object", x);
}
}
// if we get here, we didn't fail, while we should
throw nsx;
}
throw nsx;
}
catch (Exception e)
{
log.error("error writing object " + obj + ": " + e.getMessage(), e);
throw new WicketRuntimeException(e);
}
}
@Override
public void flush() throws IOException
{
oos.flush();
}
@Override
public void close() throws IOException
{
oos.close();
}
}
}