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