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