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.metadata.AbstractClassMetaData; 049 import org.datanucleus.store.ExecutionContext; 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 }