001 /*
002 * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003 * Copyright (C) 2011 NightLabs Consulting GmbH
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018 package org.cumulus4j.store.model;
019
020 import java.util.ArrayList;
021 import java.util.Collection;
022 import java.util.HashMap;
023 import java.util.HashSet;
024 import java.util.Map;
025 import java.util.Set;
026
027 import javax.jdo.JDOHelper;
028 import javax.jdo.PersistenceManager;
029 import javax.jdo.annotations.Column;
030 import javax.jdo.annotations.Discriminator;
031 import javax.jdo.annotations.DiscriminatorStrategy;
032 import javax.jdo.annotations.IdGeneratorStrategy;
033 import javax.jdo.annotations.IdentityType;
034 import javax.jdo.annotations.NotPersistent;
035 import javax.jdo.annotations.NullValue;
036 import javax.jdo.annotations.PersistenceCapable;
037 import javax.jdo.annotations.Persistent;
038 import javax.jdo.annotations.PrimaryKey;
039 import javax.jdo.annotations.Queries;
040 import javax.jdo.annotations.Query;
041 import javax.jdo.annotations.Unique;
042 import javax.jdo.annotations.Version;
043 import javax.jdo.annotations.VersionStrategy;
044 import javax.jdo.listener.DetachCallback;
045 import javax.jdo.listener.LoadCallback;
046 import javax.jdo.listener.StoreCallback;
047
048 import org.datanucleus.ExecutionContext;
049 import org.datanucleus.metadata.AbstractClassMetaData;
050 import org.slf4j.Logger;
051 import org.slf4j.LoggerFactory;
052
053 /**
054 * Persistent meta-data for a persistence-capable {@link Class}. Since class names are very long,
055 * we use the {@link #getClassID() classID} instead in our index and data entities (e.g. in the relation
056 * {@link DataEntry#getClassMeta() DataEntry.classMeta}).
057 *
058 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
059 */
060 @PersistenceCapable(identityType=IdentityType.APPLICATION, detachable="true")
061 @Discriminator(
062 strategy=DiscriminatorStrategy.VALUE_MAP, value="ClassMeta",
063 columns=@Column(name="discriminator", defaultValue="ClassMeta", length=100)
064 )
065 @Version(strategy=VersionStrategy.VERSION_NUMBER)
066 @Unique(name="ClassMeta_fullyQualifiedClassName", members={"uniqueScope", "packageName", "simpleClassName"})
067 //@FetchGroups({
068 // @FetchGroup(name=FetchGroupsMetaData.ALL, members={
069 // @Persistent(name="superClassMeta", recursionDepth=-1)
070 // })
071 //})
072 @Queries({
073 // // We cannot use pm.getObjectById(...), because GAE uses a GAE-specific identity
074 // // instead of the long-ID. We therefore must use a query instead.
075 // @Query(
076 // name=ClassMeta.NamedQueries.getClassMetaByClassID,
077 // value="SELECT UNIQUE WHERE this.classID == :classID"
078 // ),
079 @Query(
080 name=ClassMeta.NamedQueries.getClassMetaByPackageNameAndSimpleClassName,
081 value="SELECT UNIQUE WHERE this.uniqueScope == :uniqueScope && this.packageName == :packageName && this.simpleClassName == :simpleClassName"
082 )
083 })
084 public class ClassMeta
085 implements DetachCallback, StoreCallback, LoadCallback
086 {
087 private static final Logger logger = LoggerFactory.getLogger(ClassMeta.class);
088
089 protected static final String UNIQUE_SCOPE_CLASS_META = "ClassMeta";
090
091 protected static class NamedQueries {
092 public static final String getClassMetaByPackageNameAndSimpleClassName = "getClassMetaByPackageNameAndSimpleClassName";
093 // public static final String getClassMetaByClassID = "getClassMetaByClassID";
094 }
095
096 @PrimaryKey
097 @Persistent(valueStrategy=IdGeneratorStrategy.NATIVE, sequence="ClassMetaSequence")
098 private Long classID;
099
100 // /**
101 // * This is needed due to GAE compatibility. package.jdo is responsible
102 // * for the correct usage if this field.
103 // */
104 //// @NotPersistent // not persistent for non-GAE-datastores
105 // private String classIDString;
106
107 @NotPersistent
108 private transient volatile String className;
109
110 @Persistent(nullValue=NullValue.EXCEPTION)
111 @Column(length=255, defaultValue=UNIQUE_SCOPE_CLASS_META)
112 private String uniqueScope;
113
114 @Persistent(nullValue=NullValue.EXCEPTION)
115 @Column(length=255)
116 private String packageName;
117
118 @Persistent(nullValue=NullValue.EXCEPTION)
119 @Column(length=255)
120 private String simpleClassName;
121
122 @Column(name="superClassMeta_classID_oid") // for downward-compatibility.
123 private Long superClassMeta_classID;
124
125 @NotPersistent
126 private ClassMeta superClassMeta;
127
128 /**
129 * Meta data for all persistent fields of the class referenced by this <code>ClassMeta</code>.
130 * <p>
131 * This map is manually managed (e.g. lazy-loaded by {@link #getFieldName2FieldMeta()} or manually detached
132 * in {@link #jdoPostDetach(Object)}) because of constraints in GAE. We simulate the behaviour of:
133 * <p>
134 * <pre>
135 * @Persistent(mappedBy="classMeta", dependentValue="true")
136 * @Key(mappedBy="fieldName")
137 * </pre>
138 */
139 @NotPersistent
140 private Map<String, FieldMeta> fieldName2FieldMeta;
141
142 @NotPersistent
143 private Map<Long, FieldMeta> fieldID2FieldMeta;
144
145 protected ClassMeta() { }
146
147 public ClassMeta(Class<?> clazz) {
148 this.packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName();
149 this.simpleClassName = clazz.getSimpleName();
150 setUniqueScope(ClassMeta.UNIQUE_SCOPE_CLASS_META);
151 }
152
153 public long getClassID() {
154 // if(classIDString != null && classID == null){
155 // classID = KeyFactory.getInstance().stringToKey(classIDString).getId();
156 // }
157 return classID == null ? -1 : classID;
158 }
159
160 protected String getUniqueScope() {
161 return uniqueScope;
162 }
163
164 protected void setUniqueScope(String uniqueScope) {
165 this.uniqueScope = uniqueScope;
166 }
167
168 /**
169 * Get the package name or an empty <code>String</code> for the default package.
170 * @return the package name (maybe empty, but never <code>null</code>).
171 */
172 public String getPackageName() {
173 return packageName;
174 }
175
176 public String getSimpleClassName() {
177 return simpleClassName;
178 }
179
180 /**
181 * Get the fully qualified class name (composed of {@link #getPackageName() packageName} and {@link #getSimpleClassName() simpleClassName}).
182 * @return the fully qualified class name.
183 */
184 public String getClassName()
185 {
186 String cn = className;
187 if (cn == null)
188 className = cn = getClassName(packageName, simpleClassName);
189
190 return cn;
191 }
192
193 public static String getClassName(String packageName, String simpleClassName) {
194 if (packageName == null)
195 throw new IllegalArgumentException("packageName == null");
196 if (simpleClassName == null)
197 throw new IllegalArgumentException("simpleClassName == null");
198
199 if (packageName.isEmpty())
200 return simpleClassName;
201 else
202 return packageName + '.' + simpleClassName;
203 }
204
205 /**
206 * The super-class' meta-data or <code>null</code>, if there is no <b>persistence-capable</b> super-class.
207 * @return the super-class' meta-data or <code>null</code>.
208 */
209 public ClassMeta getSuperClassMeta() {
210 if (superClassMeta_classID == null)
211 return null;
212
213 if (superClassMeta == null)
214 superClassMeta = new ClassMetaDAO(getPersistenceManager()).getClassMeta(superClassMeta_classID, true);
215
216 return superClassMeta;
217 }
218
219 public void setSuperClassMeta(ClassMeta superClassMeta) {
220 if (superClassMeta != null)
221 superClassMeta = getPersistenceManager().makePersistent(superClassMeta);
222
223 this.superClassMeta = superClassMeta;
224 this.superClassMeta_classID = superClassMeta == null ? null : superClassMeta.getClassID();
225 if (this.superClassMeta_classID != null && this.superClassMeta_classID.longValue() < 0)
226 throw new IllegalStateException("this.superClassMeta_classID < 0");
227 }
228
229 /**
230 * Get the {@link PersistenceManager} assigned to <code>this</code>. If there is none, this method checks, if
231 * <code>this</code> is new. If <code>this</code> was persisted before, it must have one or an {@link IllegalStateException}
232 * is thrown.
233 * @return the {@link PersistenceManager} assigned to this or <code>null</code>.
234 */
235 protected PersistenceManager getPersistenceManager() {
236 PersistenceManager pm = JDOHelper.getPersistenceManager(this);
237 if (pm == null) {
238 // if (JDOHelper.getObjectId(this) != null)
239 // throw new IllegalStateException("This ClassMeta instance is not new, but JDOHelper.getPersistenceManager(this) returned null! " + this);
240 throw new IllegalStateException("JDOHelper.getPersistenceManager(this) returned null! " + this);
241 }
242 return pm;
243 }
244
245 public Map<String, FieldMeta> getFieldName2FieldMeta() {
246 Map<String, FieldMeta> result = this.fieldName2FieldMeta;
247
248 if (result == null) {
249 logger.debug("getFieldName2FieldMeta: this.fieldName2FieldMeta == null => populating. this={}", this);
250 result = new HashMap<String, FieldMeta>();
251 PersistenceManager pm = getPersistenceManager();
252 if (pm != null) {
253 Collection<FieldMeta> fieldMetas = new FieldMetaDAO(pm).getFieldMetasForClassMeta(this);
254 for (FieldMeta fieldMeta : fieldMetas)
255 result.put(fieldMeta.getFieldName(), fieldMeta);
256 }
257 this.fieldName2FieldMeta = result;
258 }
259 else
260 logger.trace("getFieldName2FieldMeta: this.fieldName2FieldMeta != null (already populated). this={}", this);
261
262 return result;
263 }
264
265 /**
266 * Get all {@link FieldMeta} instances known to this instance. This is the meta-data for all fields
267 * <b>directly declared</b> in the class referenced by this <code>ClassMeta</code> <b>not
268 * including super-classes</b>.
269 * @return Collection of FieldMeta objects for this class
270 */
271 public Collection<FieldMeta> getFieldMetas() {
272 return getFieldName2FieldMeta().values();
273 }
274
275 /**
276 * Get the {@link FieldMeta} for a field that is <b>directly declared</b> in the class referenced by
277 * this <code>ClassMeta</code>. This method thus does not take super-classes into account.
278 *
279 * @param fieldName the simple field name (no class prefix).
280 * @return the {@link FieldMeta} corresponding to the specified <code>fieldName</code> or <code>null</code>, if no such field
281 * exists.
282 * @see #getFieldMeta(long)
283 * @see #getFieldMeta(String, String)
284 */
285 public FieldMeta getFieldMeta(String fieldName) {
286 return getFieldName2FieldMeta().get(fieldName);
287 }
288
289 /**
290 * <p>
291 * Get the {@link FieldMeta} for a field that is either directly declared in the class referenced by this
292 * <code>ClassMeta</code> or in a super-class.
293 * </p>
294 * <p>
295 * If <code>className</code> is <code>null</code>, this method
296 * searches recursively in the inheritance hierarchy upwards (i.e. first this class then the super-class,
297 * then the next super-class etc.) until it finds a field matching the given <code>fieldName</code>.
298 * </p>
299 * <p>
300 * If <code>className</code> is not <code>null</code>, this method searches only in the specified class.
301 * If <code>className</code> is neither the current class nor any super-class, this method always returns
302 * <code>null</code>.
303 * </p>
304 *
305 * @param className the fully qualified class-name of the class referenced by this <code>ClassMeta</code>
306 * or any super-class. <code>null</code> to search the entire class hierarchy upwards (through all super-classes
307 * until the field is found or the last super-class was investigated).
308 * @param fieldName the simple field name (no class prefix).
309 * @return the {@link FieldMeta} matching the given criteria or <code>null</code> if no such field could be found.
310 */
311 public FieldMeta getFieldMeta(String className, String fieldName) {
312 if (className == null) {
313 FieldMeta fieldMeta = getFieldMeta(fieldName);
314 if (fieldMeta != null)
315 return fieldMeta;
316
317 if (superClassMeta != null)
318 return superClassMeta.getFieldMeta(className, fieldName);
319 else
320 return null;
321 }
322 else {
323 if (getClassName().equals(className))
324 return getFieldMeta(fieldName);
325 else if (superClassMeta != null)
326 return superClassMeta.getFieldMeta(className, fieldName);
327 else
328 return null;
329 }
330 }
331
332 /**
333 * Get the {@link FieldMeta} with the specified {@link FieldMeta#getFieldID() fieldID}. It does not matter, if
334 * this field is directly in the class referenced by this <code>ClassMeta</code> or in a super-class.
335 * @param fieldID the {@link FieldMeta#getFieldID() fieldID} of the <code>FieldMeta</code> to be found.
336 * @return the {@link FieldMeta} referenced by the given <code>fieldID</code> or <code>null</code>, if no such
337 * field exists in the class or any super-class.
338 */
339 public FieldMeta getFieldMeta(long fieldID)
340 {
341 Map<Long, FieldMeta> m = fieldID2FieldMeta;
342
343 if (m == null) {
344 m = new HashMap<Long, FieldMeta>(getFieldName2FieldMeta().size());
345 for (FieldMeta fieldMeta : getFieldName2FieldMeta().values())
346 m.put(fieldMeta.getFieldID(), fieldMeta);
347
348 fieldID2FieldMeta = m;
349 }
350
351 FieldMeta fieldMeta = m.get(fieldID);
352 if (fieldMeta != null)
353 return fieldMeta;
354
355 if (superClassMeta != null)
356 return superClassMeta.getFieldMeta(fieldID);
357 else
358 return null;
359 }
360
361 public void addFieldMeta(FieldMeta fieldMeta) {
362 if (!this.equals(fieldMeta.getClassMeta()))
363 throw new IllegalArgumentException("fieldMeta.classMeta != this");
364
365 PersistenceManager pm = getPersistenceManager();
366 if (pm != null) // If the pm is null, the fieldMeta is persisted later (see jdoPreStore() below).
367 fieldMeta = pm.makePersistent(fieldMeta);
368
369 getFieldName2FieldMeta().put(fieldMeta.getFieldName(), fieldMeta);
370 fieldID2FieldMeta = null;
371 }
372
373 public void removeFieldMeta(FieldMeta fieldMeta) {
374 if (!this.equals(fieldMeta.getClassMeta()))
375 throw new IllegalArgumentException("fieldMeta.classMeta != this");
376
377 getFieldName2FieldMeta().remove(fieldMeta.getFieldName());
378 fieldID2FieldMeta = null;
379
380 PersistenceManager pm = getPersistenceManager();
381 if (pm != null)
382 pm.deletePersistent(fieldMeta);
383 }
384
385 @Override
386 public int hashCode() {
387 long classID = getClassID();
388 return (int) (classID ^ (classID >>> 32));
389 }
390
391 @Override
392 public boolean equals(Object obj) {
393 if (this == obj) return true;
394 if (obj == null) return false;
395 if (getClass() != obj.getClass()) return false;
396 ClassMeta other = (ClassMeta) obj;
397 // if not yet persisted (id == null), it is only equal to the same instance (checked above, already).
398 // return this.classID == null ? false : this.classID.equals(other.classID);
399 return this.getClassID() < 0 ? false : this.getClassID() == other.getClassID();
400 }
401
402 @Override
403 public String toString() {
404 return (
405 this.getClass().getName()
406 + '@'
407 + Integer.toHexString(System.identityHashCode(this))
408 + '[' + classID + ',' + getClassName() + ']'
409 );
410 }
411
412 @NotPersistent
413 private AbstractClassMetaData dataNucleusClassMetaData;
414
415 public AbstractClassMetaData getDataNucleusClassMetaData(ExecutionContext executionContext)
416 {
417 if (dataNucleusClassMetaData != null)
418 return dataNucleusClassMetaData;
419
420 AbstractClassMetaData dnClassMetaData = executionContext.getMetaDataManager().getMetaDataForClass(getClassName(), executionContext.getClassLoaderResolver());
421 if (dnClassMetaData == null)
422 throw new IllegalStateException("DataNucleus does not know any meta-data for this class: classID=" + getClassID() + " className=" + getClassName());
423
424 dataNucleusClassMetaData = dnClassMetaData;
425 return dnClassMetaData;
426 }
427
428 @Override
429 public void jdoPreDetach() { }
430
431 protected static final ThreadLocal<Set<ClassMeta>> attachedClassMetasInPostDetachThreadLocal = new ThreadLocal<Set<ClassMeta>>() {
432 @Override
433 protected Set<ClassMeta> initialValue() {
434 return new HashSet<ClassMeta>();
435 }
436 };
437
438 @Override
439 public void jdoPostDetach(Object o) {
440 final PostDetachRunnableManager postDetachRunnableManager = PostDetachRunnableManager.getInstance();
441 postDetachRunnableManager.enterScope();
442 try {
443 final ClassMeta attached = (ClassMeta) o;
444 final ClassMeta detached = this;
445 logger.debug("jdoPostDetach: attached={}", attached);
446
447 if (!JDOHelper.isDetached(detached))
448 throw new IllegalStateException("detached ist not detached!");
449
450 if (JDOHelper.getPersistenceManager(detached) != null)
451 throw new IllegalStateException("detached has a PersistenceManager assigned!");
452
453 final DetachedClassMetaModel detachedClassMetaModel = DetachedClassMetaModel.getInstance();
454 if (detachedClassMetaModel != null)
455 detachedClassMetaModel.registerClassMetaCurrentlyDetaching(detached);
456
457 Set<ClassMeta> attachedClassMetasInPostDetach = attachedClassMetasInPostDetachThreadLocal.get();
458 if (!attachedClassMetasInPostDetach.add(attached)) {
459 logger.debug("jdoPostDetach: Already in detachment => Skipping detachment of this.fieldName2FieldMeta! attached={}", attached);
460 return;
461 }
462 try {
463
464 final PersistenceManager pm = attached.getPersistenceManager();
465 if (pm == null)
466 throw new IllegalStateException("attached.getPersistenceManager() returned null!");
467
468 // The following fields should already be null, but we better ensure that we never
469 // contain *AT*tached objects inside a *DE*tached container.
470 detached.fieldName2FieldMeta = null;
471 detached.fieldID2FieldMeta = null;
472
473 Set<?> fetchGroups = pm.getFetchPlan().getGroups();
474 if (fetchGroups.contains(javax.jdo.FetchGroup.ALL)) {
475 logger.debug("jdoPostDetach: Detaching this.fieldName2FieldMeta: attached={}", attached);
476
477 // if the fetch-groups say we should detach the FieldMetas, we do it.
478 HashMap<String, FieldMeta> map = new HashMap<String, FieldMeta>();
479 Collection<FieldMeta> attachedFieldMetas = new ArrayList<FieldMeta>(attached.getFieldMetas());
480 Collection<FieldMeta> detachedFieldMetas = pm.detachCopyAll(attachedFieldMetas);
481 for (final FieldMeta detachedFieldMeta : detachedFieldMetas) {
482 // detachedFieldMeta.setClassMeta(detached); // ensure, it's the identical (not only equal) ClassMeta.
483 // The above is not necessary and might cause problems (because this callback might be called while the detached instance is currently
484 // BEING detached, i.e. not yet finished detaching. Marco.
485
486 postDetachRunnableManager.addRunnable(new Runnable() {
487 @Override
488 public void run() {
489 detachedFieldMeta.setClassMeta(detached); // ensure, it's the identical (not only equal) ClassMeta.
490 }
491 });
492
493 map.put(detachedFieldMeta.getFieldName(), detachedFieldMeta);
494 }
495 detached.fieldName2FieldMeta = map;
496
497 postDetachRunnableManager.addRunnable(new Runnable() {
498 @Override
499 public void run() {
500 if (attached.superClassMeta_classID != null) {
501 detached.superClassMeta = detachedClassMetaModel == null ? null : detachedClassMetaModel.getClassMeta(attached.superClassMeta_classID, false);
502 if (detached.superClassMeta == null)
503 detached.superClassMeta = pm.detachCopy(attached.getSuperClassMeta());
504 }
505 }
506 });
507 }
508
509 } finally {
510 attachedClassMetasInPostDetach.remove(attached);
511 }
512 } finally {
513 postDetachRunnableManager.exitScope();
514 }
515 }
516
517 @Override
518 public void jdoPreStore() {
519 logger.debug("jdoPreStore: {}", this);
520 // PostStoreRunnableManager.getInstance().addRunnable(new Runnable() {
521 // @Override
522 // public void run() {
523 // if (fieldName2FieldMeta != null) {
524 // final PersistenceManager pm = JDOHelper.getPersistenceManager(ClassMeta.this);
525 // Map<String, FieldMeta> persistentFieldName2FieldMeta = new HashMap<String, FieldMeta>(fieldName2FieldMeta.size());
526 // for (FieldMeta fieldMeta : fieldName2FieldMeta.values()) {
527 // // Usually the persistentFieldMeta is the same instance as fieldMeta, but this is dependent on the configuration.
528 // // This code here should work with all possible configurations. Marco :-)
529 // FieldMeta persistentFieldMeta = pm.makePersistent(fieldMeta);
530 // persistentFieldName2FieldMeta.put(persistentFieldMeta.getFieldName(), persistentFieldMeta);
531 // }
532 // fieldName2FieldMeta = persistentFieldName2FieldMeta;
533 // pm.flush();
534 // }
535 //// fieldID2FieldMeta = null; // not necessary IMHO, because we assign the persistent instances above.
536 // }
537 // });
538 }
539
540 @Override
541 public void jdoPostLoad() {
542 getClassName();
543 }
544 }