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}