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
021 package org.granite.datanucleus;
022
023 import java.io.ByteArrayInputStream;
024 import java.io.ByteArrayOutputStream;
025 import java.io.IOException;
026 import java.io.ObjectInput;
027 import java.io.ObjectInputStream;
028 import java.io.ObjectOutput;
029 import java.io.ObjectOutputStream;
030 import java.lang.annotation.Annotation;
031 import java.lang.reflect.Field;
032 import java.lang.reflect.InvocationTargetException;
033 import java.lang.reflect.Method;
034 import java.lang.reflect.ParameterizedType;
035 import java.lang.reflect.Type;
036 import java.util.ArrayList;
037 import java.util.Arrays;
038 import java.util.BitSet;
039 import java.util.Collection;
040 import java.util.HashMap;
041 import java.util.HashSet;
042 import java.util.Iterator;
043 import java.util.List;
044 import java.util.Map;
045 import java.util.Set;
046 import java.util.SortedMap;
047 import java.util.SortedSet;
048 import java.util.TreeMap;
049 import java.util.TreeSet;
050
051 import javax.jdo.annotations.EmbeddedOnly;
052 import javax.jdo.annotations.Extension;
053 import javax.jdo.spi.Detachable;
054 import javax.jdo.spi.PersistenceCapable;
055 import javax.jdo.spi.StateManager;
056 import javax.persistence.Version;
057
058 import org.granite.config.GraniteConfig;
059 import org.granite.context.GraniteContext;
060 import org.granite.logging.Logger;
061 import org.granite.messaging.amf.io.convert.Converters;
062 import org.granite.messaging.amf.io.util.ClassGetter;
063 import org.granite.messaging.amf.io.util.MethodProperty;
064 import org.granite.messaging.amf.io.util.Property;
065 import org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer;
066 import org.granite.messaging.amf.io.util.externalizer.annotation.ExternalizedProperty;
067 import org.granite.messaging.persistence.AbstractExternalizablePersistentCollection;
068 import org.granite.messaging.persistence.ExternalizablePersistentList;
069 import org.granite.messaging.persistence.ExternalizablePersistentMap;
070 import org.granite.messaging.persistence.ExternalizablePersistentSet;
071 import org.granite.util.TypeUtil;
072 import org.granite.util.Reflections;
073 import org.granite.util.StringUtil;
074
075
076 /**
077 * @author Stephen MORE
078 * @author William DRAI
079 */
080 @SuppressWarnings("unchecked")
081 public 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 }