001/* 002 GRANITE DATA SERVICES 003 Copyright (C) 2011 GRANITE DATA SERVICES S.A.S. 004 005 This file is part of Granite Data Services. 006 007 Granite Data Services is free software; you can redistribute it and/or modify 008 it under the terms of the GNU Library General Public License as published by 009 the Free Software Foundation; either version 2 of the License, or (at your 010 option) any later version. 011 012 Granite Data Services is distributed in the hope that it will be useful, but 013 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 014 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License 015 for more details. 016 017 You should have received a copy of the GNU Library General Public License 018 along with this library; if not, see <http://www.gnu.org/licenses/>. 019*/ 020 021package org.granite.datanucleus; 022 023import java.io.ByteArrayInputStream; 024import java.io.ByteArrayOutputStream; 025import java.io.IOException; 026import java.io.ObjectInput; 027import java.io.ObjectInputStream; 028import java.io.ObjectOutput; 029import java.io.ObjectOutputStream; 030import java.lang.annotation.Annotation; 031import java.lang.reflect.Field; 032import java.lang.reflect.InvocationTargetException; 033import java.lang.reflect.Method; 034import java.lang.reflect.ParameterizedType; 035import java.lang.reflect.Type; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.BitSet; 039import java.util.Collection; 040import java.util.HashMap; 041import java.util.HashSet; 042import java.util.Iterator; 043import java.util.List; 044import java.util.Map; 045import java.util.Set; 046import java.util.SortedMap; 047import java.util.SortedSet; 048import java.util.TreeMap; 049import java.util.TreeSet; 050 051import javax.jdo.annotations.EmbeddedOnly; 052import javax.jdo.annotations.Extension; 053import javax.jdo.spi.Detachable; 054import javax.jdo.spi.PersistenceCapable; 055import javax.jdo.spi.StateManager; 056import javax.persistence.Version; 057 058import org.granite.config.GraniteConfig; 059import org.granite.context.GraniteContext; 060import org.granite.logging.Logger; 061import org.granite.messaging.amf.io.convert.Converters; 062import org.granite.messaging.amf.io.util.ClassGetter; 063import org.granite.messaging.amf.io.util.MethodProperty; 064import org.granite.messaging.amf.io.util.Property; 065import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer; 066import org.granite.messaging.amf.io.util.externalizer.annotation.ExternalizedProperty; 067import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection; 068import org.granite.messaging.persistence.ExternalizablePersistentList; 069import org.granite.messaging.persistence.ExternalizablePersistentMap; 070import org.granite.messaging.persistence.ExternalizablePersistentSet; 071import org.granite.util.TypeUtil; 072import org.granite.util.Reflections; 073import org.granite.util.StringUtil; 074 075 076/** 077 * @author Stephen MORE 078 * @author William DRAI 079 */ 080@SuppressWarnings("unchecked") 081public class DataNucleusExternalizer extends DefaultExternalizer { 082 083 private static final Logger log = Logger.getLogger(DataNucleusExternalizer.class); 084 085 private static final Integer NULL_ID = Integer.valueOf(0); 086 087 private static boolean jpaEnabled; 088 private static Class<? extends Annotation> entityAnnotation; 089 private static Class<? extends Annotation> mappedSuperClassAnnotation; 090 private static Class<? extends Annotation> embeddableAnnotation; 091 private static Class<? extends Annotation> idClassAnnotation; 092 static { 093 try { 094 ClassLoader cl = DataNucleusExternalizer.class.getClassLoader(); 095 entityAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.Entity"); 096 mappedSuperClassAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.MappedSuperclass"); 097 embeddableAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.Embeddable"); 098 idClassAnnotation = (Class<? extends Annotation>)cl.loadClass("javax.persistence.IdClass"); 099 jpaEnabled = true; 100 } 101 catch (Exception e) { 102 // JPA not present 103 entityAnnotation = null; 104 mappedSuperClassAnnotation = null; 105 embeddableAnnotation = null; 106 idClassAnnotation = null; 107 jpaEnabled = false; 108 } 109 } 110 111 112 @Override 113 public Object newInstance(String type, ObjectInput in) 114 throws IOException, ClassNotFoundException, InstantiationException, InvocationTargetException, IllegalAccessException { 115 116 // If type is not an entity (@Embeddable for example), we don't read initialized/detachedState 117 // and we fall back to DefaultExternalizer behavior. 118 Class<?> clazz = TypeUtil.forName(type); 119 if (!isRegularEntity(clazz)) 120 return super.newInstance(type, in); 121 122 // Read initialized flag. 123 boolean initialized = ((Boolean)in.readObject()).booleanValue(); 124 125 // Read detachedState. 126 String detachedState = (String)in.readObject(); 127 128 // New entity. 129 if (initialized && detachedState == null) 130 return super.newInstance(type, in); 131 132 // Pseudo-proxy (uninitialized entity). 133 if (!initialized) { 134 Object id = in.readObject(); 135 if (id != null && jpaEnabled) { 136 // Is there something similar for JDO ?? 137 boolean error = !clazz.isAnnotationPresent(idClassAnnotation); 138 if (!error) { 139 Object idClass = clazz.getAnnotation(idClassAnnotation); 140 try { 141 Method m = idClass.getClass().getMethod("value"); 142 error = !id.getClass().equals(m.invoke(idClass)); 143 } 144 catch (Exception e) { 145 log.error(e, "Could not get idClass annotation value"); 146 error = true; 147 } 148 } 149 if (error) 150 throw new RuntimeException("Id for DataNucleus pseudo-proxy should be null (" + type + ")"); 151 } 152 return null; 153 } 154 155 // Existing entity. 156 Object entity = clazz.newInstance(); 157 if (detachedState.length() > 0) { 158 byte[] data = StringUtil.hexStringToBytes(detachedState); 159 deserializeDetachedState((Detachable)entity, data); 160 } 161 return entity; 162 } 163 164 @Override 165 public void readExternal(Object o, ObjectInput in) throws IOException, ClassNotFoundException, IllegalAccessException { 166 167 if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { 168 log.debug("Delegating non regular entity reading to DefaultExternalizer..."); 169 super.readExternal(o, in); 170 } 171 // Regular @Entity or @MappedSuperclass 172 else { 173 GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig(); 174 175 Converters converters = config.getConverters(); 176 ClassGetter classGetter = config.getClassGetter(); 177 Class<?> oClass = classGetter.getClass(o); 178 ParameterizedType[] declaringTypes = TypeUtil.getDeclaringTypes(oClass); 179 Object[] detachedState = getDetachedState((Detachable)o); 180 181 List<Property> fields = findOrderedFields(oClass, detachedState != null); 182 log.debug("Reading entity %s with fields %s", oClass.getName(), fields); 183 for (Property field : fields) { 184 if (field.getName().equals("jdoDetachedState")) 185 continue; 186 187 Object value = in.readObject(); 188 189 if (!(field instanceof MethodProperty && field.isAnnotationPresent(ExternalizedProperty.class, true))) { 190 191 // (Un)Initialized collections/maps. 192 if (value instanceof AbstractExternalizablePersistentCollection) 193 value = newCollection((AbstractExternalizablePersistentCollection)value, field); 194 else { 195 Type targetType = TypeUtil.resolveTypeVariable(field.getType(), field.getDeclaringClass(), declaringTypes); 196 value = converters.convert(value, targetType); 197 } 198 199 field.setProperty(o, value, false); 200 } 201 } 202 } 203 } 204 205 protected Object newCollection(AbstractExternalizablePersistentCollection value, Property field) { 206 final Type target = field.getType(); 207 final boolean initialized = value.isInitialized(); 208 // final boolean dirty = value.isDirty(); 209 final Object[] content = value.getContent(); 210 final boolean sorted = ( 211 SortedSet.class.isAssignableFrom(TypeUtil.classOfType(target)) || 212 SortedMap.class.isAssignableFrom(TypeUtil.classOfType(target)) 213 ); 214 215 Object coll = null; 216 if (value instanceof ExternalizablePersistentSet) { 217 if (initialized) { 218 if (content != null) 219 coll = ((ExternalizablePersistentSet)value).getContentAsSet(target); 220 } 221 else 222 coll = (sorted ? new TreeSet<Object>() : new HashSet<Object>()); 223 } 224 else if (value instanceof ExternalizablePersistentList) { 225 if (initialized) { 226 if (content != null) 227 coll = ((ExternalizablePersistentList)value).getContentAsList(target); 228 } 229 else 230 coll = new ArrayList<Object>(); 231 } 232 else if (value instanceof ExternalizablePersistentMap) { 233 if (initialized) { 234 if (content != null) 235 coll = ((ExternalizablePersistentMap)value).getContentAsMap(target); 236 } 237 else 238 coll = (sorted ? new TreeMap<Object, Object>() : new HashMap<Object, Object>()); 239 } 240 else { 241 throw new RuntimeException("Illegal externalizable persitent class: " + value); 242 } 243 244 return coll; 245 } 246 247 @Override 248 public void writeExternal(Object o, ObjectOutput out) throws IOException, IllegalAccessException { 249 250 ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter(); 251 Class<?> oClass = classGetter.getClass(o); 252 253 if (!isRegularEntity(o.getClass()) && !isEmbeddable(o.getClass())) { // @Embeddable or others... 254 log.debug("Delegating non regular entity writing to DefaultExternalizer..."); 255 super.writeExternal(o, out); 256 } 257 else { 258 Detachable pco = (Detachable)o; 259 preSerialize((PersistenceCapable)pco); 260 Object[] detachedState = getDetachedState(pco); 261 262 if (isRegularEntity(o.getClass())) { 263 // Pseudo-proxy created for uninitialized entities (see below). 264 if (detachedState != null && detachedState[0] == NULL_ID) { 265 // Write initialized flag. 266 out.writeObject(Boolean.FALSE); 267 // Write detached state. 268 out.writeObject(null); 269 // Write id. 270 out.writeObject(null); 271 return; 272 } 273 274 // Write initialized flag. 275 out.writeObject(Boolean.TRUE); 276 277 if (detachedState != null) { 278 // Write detached state as a String, in the form of an hex representation 279 // of the serialized detached state. 280 Object version = getVersion(pco); 281 if (version != null) 282 detachedState[1] = version; 283 byte[] binDetachedState = serializeDetachedState(detachedState); 284 char[] hexDetachedState = StringUtil.bytesToHexChars(binDetachedState); 285 out.writeObject(new String(hexDetachedState)); 286 } 287 else 288 out.writeObject(null); 289 } 290 291 // Externalize entity fields. 292 List<Property> fields = findOrderedFields(oClass); 293 Map<String, Boolean> loadedState = getLoadedState(detachedState, oClass); 294 log.debug("Writing entity %s with fields %s", o.getClass().getName(), fields); 295 for (Property field : fields) { 296 if (field.getName().equals("jdoDetachedState")) 297 continue; 298 299 Object value = field.getProperty(o); 300 if (isValueIgnored(value)) { 301 out.writeObject(null); 302 continue; 303 } 304 305 // Uninitialized associations. 306 if (loadedState.containsKey(field.getName()) && !loadedState.get(field.getName())) { 307 Class<?> fieldClass = TypeUtil.classOfType(field.getType()); 308 309 // Create a "pseudo-proxy" for uninitialized entities: detached state is set to "0" (uninitialized flag). 310 if (Detachable.class.isAssignableFrom(fieldClass)) { 311 try { 312 value = fieldClass.newInstance(); 313 } catch (Exception e) { 314 throw new RuntimeException("Could not create DataNucleus pseudo-proxy for: " + field, e); 315 } 316 setDetachedState((Detachable)value, new Object[] { NULL_ID, null, null, null }); 317 } 318 // Create pseudo-proxy for collections (set or list). 319 else if (Collection.class.isAssignableFrom(fieldClass)) { 320 if (Set.class.isAssignableFrom(fieldClass)) 321 value = new ExternalizablePersistentSet((Set<?>)null, false, false); 322 else 323 value = new ExternalizablePersistentList((List<?>)null, false, false); 324 } 325 // Create pseudo-proxy for maps. 326 else if (Map.class.isAssignableFrom(fieldClass)) { 327 value = new ExternalizablePersistentMap((Map<?, ?>)null, false, false); 328 } 329 } 330 331 // Initialized collections. 332 else if (value instanceof Set<?>) { 333 value = new ExternalizablePersistentSet(((Set<?>)value).toArray(), true, false); 334 } 335 else if (value instanceof List<?>) { 336 value = new ExternalizablePersistentList(((List<?>)value).toArray(), true, false); 337 } 338 else if (value instanceof Map<?, ?>) { 339 value = new ExternalizablePersistentMap((Map<?, ?>)null, true, false); 340 ((ExternalizablePersistentMap)value).setContentFromMap((Map<?, ?>)value); 341 } 342 out.writeObject(value); 343 } 344 } 345 } 346 347 @Override 348 public int accept(Class<?> clazz) { 349 return ( 350 clazz.isAnnotationPresent(entityAnnotation) || 351 clazz.isAnnotationPresent(mappedSuperClassAnnotation) || 352 clazz.isAnnotationPresent(embeddableAnnotation) || 353 clazz.isAnnotationPresent(javax.jdo.annotations.PersistenceCapable.class) 354 ) ? 1 : -1; 355 } 356 357 protected boolean isRegularEntity(Class<?> clazz) { 358 if (jpaEnabled) { 359 return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && !clazz.isAnnotationPresent(EmbeddedOnly.class)) 360 || clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation)) 361 && !(clazz.isAnnotationPresent(embeddableAnnotation)); 362 } 363 return PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && !clazz.isAnnotationPresent(EmbeddedOnly.class); 364 } 365 366 protected boolean isEmbeddable(Class<?> clazz) { 367 if (jpaEnabled) { 368 return ((PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(EmbeddedOnly.class)) 369 || clazz.isAnnotationPresent(embeddableAnnotation)) 370 && !(clazz.isAnnotationPresent(entityAnnotation) || clazz.isAnnotationPresent(mappedSuperClassAnnotation)); 371 } 372 return PersistenceCapable.class.isAssignableFrom(clazz) && Detachable.class.isAssignableFrom(clazz) && clazz.isAnnotationPresent(EmbeddedOnly.class); 373 } 374 375 @Override 376 public List<Property> findOrderedFields(final Class<?> clazz, boolean returnSettersWhenAvailable) { 377 List<Property> orderedFields = super.findOrderedFields(clazz, returnSettersWhenAvailable); 378 if (clazz.isAnnotationPresent(EmbeddedOnly.class) || (jpaEnabled && clazz.isAnnotationPresent(embeddableAnnotation))) { 379 Iterator<Property> ifield = orderedFields.iterator(); 380 while (ifield.hasNext()) { 381 Property field = ifield.next(); 382 if (field.getName().equals("jdoDetachedState")) 383 ifield.remove(); 384 } 385 } 386 return orderedFields; 387 } 388 389 390 private static void preSerialize(PersistenceCapable o) { 391 try { 392 Class<?> baseClass = o.getClass(); 393 while (baseClass.getSuperclass() != Object.class && 394 baseClass.getSuperclass() != null && 395 PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) { 396 baseClass = baseClass.getSuperclass(); 397 } 398 Field f = baseClass.getDeclaredField("jdoStateManager"); 399 f.setAccessible(true); 400 StateManager sm = (StateManager)f.get(o); 401 if (sm != null) { 402 setDetachedState((Detachable)o, null); 403 sm.preSerialize(o); 404 } 405 } 406 catch (Exception e) { 407 throw new RuntimeException("Cannot access jdoDetachedState for detached object", e); 408 } 409 } 410 411 private static Object[] getDetachedState(javax.jdo.spi.Detachable o) { 412 try { 413 Class<?> baseClass = o.getClass(); 414 while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) 415 baseClass = baseClass.getSuperclass(); 416 Field f = baseClass.getDeclaredField("jdoDetachedState"); 417 f.setAccessible(true); 418 return (Object[])f.get(o); 419 } 420 catch (Exception e) { 421 throw new RuntimeException("Cannot access jdoDetachedState for detached object", e); 422 } 423 } 424 425 private static void setDetachedState(javax.jdo.spi.Detachable o, Object[] detachedState) { 426 try { 427 Class<?> baseClass = o.getClass(); 428 while (baseClass.getSuperclass() != Object.class && baseClass.getSuperclass() != null && PersistenceCapable.class.isAssignableFrom(baseClass.getSuperclass())) 429 baseClass = baseClass.getSuperclass(); 430 Field f = baseClass.getDeclaredField("jdoDetachedState"); 431 f.setAccessible(true); 432 f.set(o, detachedState); 433 } 434 catch (Exception e) { 435 throw new RuntimeException("Cannot access jdoDetachedState for detached object", e); 436 } 437 } 438 439 440 static Map<String, Boolean> getLoadedState(Detachable pc, Class<?> clazz) { 441 return getLoadedState(getDetachedState(pc), clazz); 442 } 443 444 static Map<String, Boolean> getLoadedState(Object[] detachedState, Class<?> clazz) { 445 try { 446 BitSet loaded = detachedState != null ? (BitSet)detachedState[2] : null; 447 448 List<String> fieldNames = new ArrayList<String>(); 449 for (Class<?> c = clazz; c != null && PersistenceCapable.class.isAssignableFrom(c); c = c.getSuperclass()) { 450 Field pcFieldNames = c.getDeclaredField("jdoFieldNames"); 451 pcFieldNames.setAccessible(true); 452 fieldNames.addAll(0, Arrays.asList((String[])pcFieldNames.get(null))); 453 } 454 455 Map<String, Boolean> loadedState = new HashMap<String, Boolean>(); 456 for (int i = 0; i < fieldNames.size(); i++) 457 loadedState.put(fieldNames.get(i), (loaded != null && loaded.size() > i ? loaded.get(i) : true)); 458 return loadedState; 459 } 460 catch (Exception e) { 461 throw new RuntimeException("Could not get loaded state for: " + detachedState); 462 } 463 } 464 465 protected byte[] serializeDetachedState(Object[] detachedState) { 466 try { 467 // Force version 468 ByteArrayOutputStream baos = new ByteArrayOutputStream(256); 469 ObjectOutputStream oos = new ObjectOutputStream(baos); 470 oos.writeObject(detachedState); 471 return baos.toByteArray(); 472 } catch (Exception e) { 473 throw new RuntimeException("Could not serialize detached state for: " + detachedState); 474 } 475 } 476 477 protected void deserializeDetachedState(Detachable pc, byte[] data) { 478 try { 479 ByteArrayInputStream baos = new ByteArrayInputStream(data); 480 ObjectInputStream oos = new ObjectInputStream(baos); 481 Object[] state = (Object[])oos.readObject(); 482 setDetachedState(pc, state); 483 } catch (Exception e) { 484 throw new RuntimeException("Could not deserialize detached state for: " + data); 485 } 486 } 487 488 protected static Object getVersion(Object entity) { 489 Class<?> entityClass = entity.getClass(); 490 491 if (jpaEnabled && entityClass.isAnnotationPresent(entityAnnotation)) { 492 for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass()) { 493 for (Method method : clazz.getDeclaredMethods()) { 494 if (method.isAnnotationPresent(Version.class)) { 495 return Reflections.invokeAndWrap(method, entity); 496 } 497 } 498 } 499 500 for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass()) { 501 for (Field field : clazz.getDeclaredFields()) { 502 if (field.isAnnotationPresent(Version.class)) { 503 if (!field.isAccessible()) 504 field.setAccessible(true); 505 return Reflections.getAndWrap(field, entity); 506 } 507 } 508 } 509 510 return null; 511 } 512 else if (!jpaEnabled && entity instanceof PersistenceCapable) { 513 if (entityClass.isAnnotationPresent(javax.jdo.annotations.Version.class)) { 514 javax.jdo.annotations.Version version = entityClass.getAnnotation(javax.jdo.annotations.Version.class); 515 for (Extension extension : version.extensions()) { 516 if (extension.vendorName().equals("datanucleus") && extension.key().equals("field-name")) { 517 String versionFieldName = extension.value(); 518 519 try { 520 Method versionGetter = entityClass.getMethod("get" + versionFieldName.substring(0, 1).toUpperCase() + versionFieldName.substring(1)); 521 return Reflections.invokeAndWrap(versionGetter, entity); 522 } 523 catch (NoSuchMethodException e) { 524 for (Class<?> clazz = entityClass; clazz != Object.class; clazz = clazz.getSuperclass()) { 525 for (Field field : clazz.getDeclaredFields()) { 526 if (field.getName().equals(versionFieldName)) { 527 if (!field.isAccessible()) 528 field.setAccessible(true); 529 return Reflections.getAndWrap(field, entity); 530 } 531 } 532 } 533 } 534 } 535 536 } 537 } 538 } 539 540 return null; 541 } 542}