001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.serialize.java;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.NotSerializableException;
024import java.io.ObjectInputStream;
025import java.io.ObjectOutputStream;
026import java.io.ObjectStreamClass;
027import java.io.OutputStream;
028import java.lang.reflect.InvocationHandler;
029import java.lang.reflect.Modifier;
030import java.lang.reflect.Proxy;
031import java.security.AccessController;
032import java.security.PrivilegedAction;
033import java.util.Objects;
034
035import org.apache.wicket.Application;
036import org.apache.wicket.ThreadContext;
037import org.apache.wicket.WicketRuntimeException;
038import org.apache.wicket.application.IClassResolver;
039import org.apache.wicket.core.util.objects.checker.CheckingObjectOutputStream;
040import org.apache.wicket.core.util.objects.checker.ObjectSerializationChecker;
041import org.apache.wicket.serialize.ISerializer;
042import org.apache.wicket.settings.ApplicationSettings;
043import org.apache.wicket.util.io.IOUtils;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * An implementation of {@link ISerializer} based on Java Serialization (ObjectOutputStream,
049 * ObjectInputStream)
050 * 
051 * Requires the application key to enable serialization and deserialisation outside thread in which
052 * application thread local is set
053 */
054public class JavaSerializer implements ISerializer
055{
056        private static final Logger log = LoggerFactory.getLogger(JavaSerializer.class);
057
058        private static final StackWalker STACKWALKER;
059        private static final ClassLoader PLATFORM_CLASS_LOADER;
060
061        static {
062                PrivilegedAction<StackWalker> pa1 =
063                                () -> StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
064                PrivilegedAction<ClassLoader> pa2 = ClassLoader::getPlatformClassLoader;
065                STACKWALKER = AccessController.doPrivileged(pa1);
066                PLATFORM_CLASS_LOADER = AccessController.doPrivileged(pa2);
067        }
068
069
070        /**
071         * The key of the application which can be used later to find the proper {@link IClassResolver}
072         */
073        private final String applicationKey;
074
075        /**
076         * Construct.
077         * 
078         * @param applicationKey
079         *      the name of the application
080         */
081        public JavaSerializer(final String applicationKey)
082        {
083                this.applicationKey = applicationKey;
084        }
085
086        @Override
087        public byte[] serialize(final Object object)
088        {
089                try
090                {
091                        final ByteArrayOutputStream out = new ByteArrayOutputStream();
092                        ObjectOutputStream oos = null;
093                        try
094                        {
095                                oos = newObjectOutputStream(out);
096                                oos.writeObject(applicationKey);
097                                oos.writeObject(object);
098                        }
099                        finally
100                        {
101                                try
102                                {
103                                        IOUtils.close(oos);
104                                }
105                                finally
106                                {
107                                        out.close();
108                                }
109                        }
110                        return out.toByteArray();
111                }
112                catch (Exception e)
113                {
114                        log.error("Error serializing object {} [object={}]",
115                                  object.getClass(), object, e);
116                }
117                return null;
118        }
119
120        @Override
121        public Object deserialize(final byte[] data)
122        {
123                ThreadContext old = ThreadContext.get(false);
124                final ByteArrayInputStream in = new ByteArrayInputStream(data);
125                ObjectInputStream ois = null;
126                try
127                {
128                        Application oldApplication = ThreadContext.getApplication();
129                        try
130                        {
131                                ois = newObjectInputStream(in);
132                                String applicationName = (String)ois.readObject();
133                                if (applicationName != null)
134                                {
135                                        Application app = Application.get(applicationName);
136                                        if (app != null)
137                                        {
138                                                ThreadContext.setApplication(app);
139                                        }
140                                }
141                                return ois.readObject();
142                        }
143                        finally
144                        {
145                                try
146                                {
147                                        ThreadContext.setApplication(oldApplication);
148                                        IOUtils.close(ois);
149                                }
150                                finally
151                                {
152                                        in.close();
153                                }
154                        }
155                }
156                catch (ClassNotFoundException | IOException cnfx)
157                {
158                        throw new WicketRuntimeException("Could not deserialize object from byte[]", cnfx);
159                }
160                finally
161                {
162                        ThreadContext.restore(old);
163                }
164        }
165
166        /**
167         * Gets a new instance of an {@link ObjectInputStream} with the provided {@link InputStream}.
168         * 
169         * @param in
170         *            The input stream that should be used for the reading
171         * @return a new object input stream instance
172         * @throws IOException
173         *             if an I/O error occurs while reading stream header
174         */
175        protected ObjectInputStream newObjectInputStream(InputStream in) throws IOException
176        {
177                return new ClassResolverObjectInputStream(in);
178        }
179
180        /**
181         * Gets a new instance of an {@link ObjectOutputStream} with the provided {@link OutputStream}.
182         * 
183         * @param out
184         *            The output stream that should be used for the writing
185         * @return a new object output stream instance
186         * @throws IOException
187         *             if an I/O error occurs while writing stream header
188         */
189        protected ObjectOutputStream newObjectOutputStream(OutputStream out) throws IOException
190        {
191                return new SerializationCheckerObjectOutputStream(out);
192        }
193
194        /**
195         * Extend {@link ObjectInputStream} to add framework class resolution logic.
196         */
197        private static class ClassResolverObjectInputStream extends ObjectInputStream
198        {
199                public ClassResolverObjectInputStream(InputStream in) throws IOException
200                {
201                        super(in);
202                }
203
204                // This override is required to resolve classes inside in different bundle, i.e.
205                // The classes can be resolved by OSGI classresolver implementation
206                @Override
207                protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
208                        ClassNotFoundException
209                {
210                        try
211                        {
212                                return super.resolveClass(desc);
213                        }
214                        catch (ClassNotFoundException cnfEx)
215                        {
216                                // ignore this exception.
217                                log.debug(
218                                        "Class not found by the object outputstream itself, trying the IClassResolver");
219
220                                Class< ? > candidate = resolveClassInWicket(desc.getName());
221                                if (candidate == null)
222                                {
223                                        throw cnfEx;
224                                }
225                                return candidate;
226                        }
227                }
228
229                /*
230                 * resolves a class by name, first using the default Class.forName, but looking in the
231                 * Wicket ClassResolvers as well.
232                 */
233                private Class<?> resolveClassByName(String className, ClassLoader latestUserDefined)
234                        throws ClassNotFoundException
235                {
236                        try
237                        {
238                                return Class.forName(className, false, latestUserDefined);
239                        }
240                        catch (ClassNotFoundException cnfEx)
241                        {
242                                Class<?> ret = resolveClassInWicket(className);
243                                if (ret == null)
244                                        throw cnfEx;
245                                return ret;
246                        }
247                }
248
249                /*
250                 * Resolves a class from Wicket's ClassResolver
251                 */
252                private Class<?> resolveClassInWicket(String className) throws ClassNotFoundException
253                {
254                        Class<?> candidate;
255                        try
256                        {
257                                Application application = Application.get();
258                                ApplicationSettings applicationSettings = application.getApplicationSettings();
259                                IClassResolver classResolver = applicationSettings.getClassResolver();
260
261                                candidate = classResolver.resolveClass(className);
262                        }
263                        catch (WicketRuntimeException ex)
264                        {
265                                if (ex.getCause() instanceof ClassNotFoundException)
266                                {
267                                        throw (ClassNotFoundException)ex.getCause();
268                                }
269                                else
270                                {
271                                        ClassNotFoundException wrapperCnf = new ClassNotFoundException();
272                                        wrapperCnf.initCause(ex);
273                                        throw wrapperCnf;
274                                }
275                        }
276                        return candidate;
277                }
278
279                /*
280                 * This method is an a copy of the super-method, with Class.forName replaced with a call to
281                 * resolveClassByName.
282                 */
283                @Override
284                protected Class<?> resolveProxyClass(String[] interfaces)
285                        throws ClassNotFoundException, IOException
286                {
287                        try
288                        {
289                                return super.resolveProxyClass(interfaces);
290                        }
291                        catch (ClassNotFoundException cnfEx)
292                        {
293                                // ignore this exception.
294                                log.debug(
295                                        "Proxy Class not found by the ObjectOutputStream itself, trying the IClassResolver");
296
297                                ClassLoader latestLoader = latestUserDefinedLoader();
298                                ClassLoader nonPublicLoader = null;
299                                boolean hasNonPublicInterface = false;
300
301                                // define proxy in class loader of non-public interface(s), if any
302                                Class<?>[] classObjs = new Class<?>[interfaces.length];
303                                for (int i = 0; i < interfaces.length; i++)
304                                {
305                                        Class<?> cl = resolveClassByName(interfaces[i], latestLoader);
306                                        if ((cl.getModifiers() & Modifier.PUBLIC) == 0)
307                                        {
308                                                if (hasNonPublicInterface)
309                                                {
310                                                        if (nonPublicLoader != cl.getClassLoader())
311                                                        {
312                                                                throw new IllegalAccessError(
313                                                                        "conflicting non-public interface class loaders");
314                                                        }
315                                                }
316                                                else
317                                                {
318                                                        nonPublicLoader = cl.getClassLoader();
319                                                        hasNonPublicInterface = true;
320                                                }
321                                        }
322                                        classObjs[i] = cl;
323                                }
324                                try
325                                {
326                                        final InvocationHandler invocationHandler = (proxy, method, args) -> null;
327                                        final Object proxyInstance = Proxy.newProxyInstance(
328                                                        hasNonPublicInterface ? nonPublicLoader : latestLoader, classObjs, invocationHandler);
329                                        return proxyInstance.getClass();
330                                }
331                                catch (IllegalArgumentException e)
332                                {
333                                        throw new ClassNotFoundException(null, e);
334                                }
335                        }
336                }
337
338                private static ClassLoader latestUserDefinedLoader()
339                {
340                        try
341                        {
342                                return STACKWALKER.walk(s ->
343                                                s.map(StackWalker.StackFrame::getDeclaringClass)
344                                                .map(Class::getClassLoader)
345                                                .filter(Objects::nonNull)
346                                                .filter(cl -> !PLATFORM_CLASS_LOADER.equals(cl))
347                                                .findFirst()
348                                                .orElse(PLATFORM_CLASS_LOADER));
349                        }
350                        catch (IllegalArgumentException | SecurityException e)
351                        {
352                                throw new WicketRuntimeException(e);
353                        }
354                }
355        }
356
357        /**
358         * Write objects to the wrapped output stream and log a meaningful message for serialization
359         * problems.
360         *
361         *  <p>
362         *     Note: the checking functionality is used only if the serialization fails with NotSerializableException.
363         *     This is done so to save some CPU time to make the checks for no reason.
364         * </p>
365         */
366        private static class SerializationCheckerObjectOutputStream extends ObjectOutputStream
367        {
368                private final OutputStream outputStream;
369
370                private final ObjectOutputStream oos;
371
372                private SerializationCheckerObjectOutputStream(OutputStream outputStream) throws IOException
373                {
374                        this.outputStream = outputStream;
375                        oos = new ObjectOutputStream(outputStream);
376                }
377
378                @Override
379                protected final void writeObjectOverride(Object obj) throws IOException
380                {
381                        try
382                        {
383                                oos.writeObject(obj);
384                        }
385                        catch (NotSerializableException nsx)
386                        {
387                                if (CheckingObjectOutputStream.isAvailable())
388                                {
389                                        try
390                                        {
391                                                // trigger serialization again, but this time gather some more info
392                                                CheckingObjectOutputStream checkingObjectOutputStream =
393                                                        new CheckingObjectOutputStream(outputStream, new ObjectSerializationChecker(nsx));
394                                                checkingObjectOutputStream.writeObject(obj);
395                                        }
396                                        catch (CheckingObjectOutputStream.ObjectCheckException x)
397                                        {
398                                                throw x;
399                                        }
400                                        catch (Exception x)
401                                        {
402                                                x.initCause(nsx);
403                                                throw new WicketRuntimeException("A problem occurred while trying to collect debug information about not serializable object", x);
404                                        }
405
406                                        // if we get here, we didn't fail, while we should
407                                        throw nsx;
408                                }
409                                throw nsx;
410                        }
411                        catch (Exception e)
412                        {
413                                log.error("error writing object {} : {}", obj, e.getMessage(), e);
414                                throw new WicketRuntimeException(e);
415                        }
416                }
417
418                @Override
419                public void flush() throws IOException
420                {
421                        oos.flush();
422                }
423
424                @Override
425                public void close() throws IOException
426                {
427                        oos.close();
428                }
429        }
430}