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.Collection; 021 import java.util.HashMap; 022 import java.util.HashSet; 023 import java.util.Map; 024 import java.util.Set; 025 026 import javax.jdo.FetchPlan; 027 import javax.jdo.JDOHelper; 028 import javax.jdo.PersistenceManager; 029 import javax.jdo.annotations.Column; 030 import javax.jdo.annotations.FetchGroup; 031 import javax.jdo.annotations.FetchGroups; 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 046 import org.datanucleus.metadata.AbstractClassMetaData; 047 import org.datanucleus.store.ExecutionContext; 048 import org.slf4j.Logger; 049 import org.slf4j.LoggerFactory; 050 051 /** 052 * Persistent meta-data for a persistence-capable {@link Class}. Since class names are very long, 053 * we use the {@link #getClassID() classID} instead in our index and data entities (e.g. in the relation 054 * {@link DataEntry#getClassMeta() DataEntry.classMeta}). 055 * 056 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de 057 */ 058 @PersistenceCapable(identityType=IdentityType.APPLICATION, detachable="true") 059 @Version(strategy=VersionStrategy.VERSION_NUMBER) 060 @Unique(name="ClassMeta_fullyQualifiedClassName", members={"packageName", "simpleClassName"}) 061 @FetchGroups({ 062 @FetchGroup(name=FetchPlan.ALL, members={ 063 @Persistent(name="superClassMeta", recursionDepth=-1) 064 }) 065 }) 066 @Queries({ 067 @Query( 068 name="getClassMetaByPackageNameAndSimpleClassName", 069 value="SELECT UNIQUE WHERE this.packageName == :packageName && this.simpleClassName == :simpleClassName" 070 ) 071 }) 072 public class ClassMeta 073 implements DetachCallback 074 { 075 private static final Logger logger = LoggerFactory.getLogger(ClassMeta.class); 076 077 protected static class NamedQueries { 078 public static final String getClassMetaByPackageNameAndSimpleClassName = "getClassMetaByPackageNameAndSimpleClassName"; 079 } 080 081 @PrimaryKey 082 @Persistent(valueStrategy=IdGeneratorStrategy.NATIVE, sequence="ClassMetaSequence") 083 private long classID = -1; 084 085 @NotPersistent 086 private transient String className; 087 088 @Persistent(nullValue=NullValue.EXCEPTION) 089 @Column(length=255) 090 private String packageName; 091 092 @Persistent(nullValue=NullValue.EXCEPTION) 093 @Column(length=255) 094 private String simpleClassName; 095 096 private ClassMeta superClassMeta; 097 098 /** 099 * Meta data for all persistent fields of the class referenced by this <code>ClassMeta</code>. 100 * <p> 101 * This map is manually managed (e.g. lazy-loaded by {@link #getFieldName2FieldMeta()} or manually detached 102 * in {@link #jdoPostDetach(Object)}) because of constraints in GAE. We simulate the behaviour of: 103 * <p> 104 * <pre> 105 * @Persistent(mappedBy="classMeta", dependentValue="true") 106 * @Key(mappedBy="fieldName") 107 * </pre> 108 */ 109 @NotPersistent 110 private Map<String, FieldMeta> fieldName2FieldMeta; 111 112 @NotPersistent 113 private Map<Long, FieldMeta> fieldID2FieldMeta; 114 115 protected ClassMeta() { } 116 117 public ClassMeta(Class<?> clazz) { 118 this.packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName(); 119 this.simpleClassName = clazz.getSimpleName(); 120 } 121 122 public long getClassID() { 123 return classID; 124 } 125 126 /** 127 * Get the package name or an empty <code>String</code> for the default package. 128 * @return the package name (maybe empty, but never <code>null</code>). 129 */ 130 public String getPackageName() { 131 return packageName; 132 } 133 134 public String getSimpleClassName() { 135 return simpleClassName; 136 } 137 138 /** 139 * Get the fully qualified class name (composed of {@link #getPackageName() packageName} and {@link #getSimpleClassName() simpleClassName}). 140 * @return the fully qualified class name. 141 */ 142 public String getClassName() 143 { 144 String cn = className; 145 if (cn == null) { 146 if (packageName.isEmpty()) 147 cn = simpleClassName; 148 else 149 cn = packageName + '.' + simpleClassName; 150 151 className = cn; 152 } 153 return cn; 154 } 155 156 /** 157 * The super-class' meta-data or <code>null</code>, if there is no <b>persistence-capable</b> super-class. 158 * @return the super-class' meta-data or <code>null</code>. 159 */ 160 public ClassMeta getSuperClassMeta() { 161 return superClassMeta; 162 } 163 164 public void setSuperClassMeta(ClassMeta superClassMeta) { 165 this.superClassMeta = superClassMeta; 166 } 167 168 /** 169 * Get the {@link PersistenceManager} assigned to <code>this</code>. If there is none, this method checks, if 170 * <code>this</code> is new. If <code>this</code> was persisted before, it must have one or an {@link IllegalStateException} 171 * is thrown. 172 * @return the {@link PersistenceManager} assigned to this or <code>null</code>. 173 */ 174 protected PersistenceManager getPersistenceManager() { 175 PersistenceManager pm = JDOHelper.getPersistenceManager(this); 176 if (pm == null) { 177 if (JDOHelper.getObjectId(this) != null) 178 throw new IllegalStateException("This ClassMeta instance is not new, but JDOHelper.getPersistenceManager(this) returned null! " + this); 179 } 180 return pm; 181 } 182 183 public Map<String, FieldMeta> getFieldName2FieldMeta() { 184 Map<String, FieldMeta> result = this.fieldName2FieldMeta; 185 186 if (result == null) { 187 logger.debug("getFieldName2FieldMeta: this.fieldName2FieldMeta == null => populating. this={}", this); 188 result = new HashMap<String, FieldMeta>(); 189 PersistenceManager pm = getPersistenceManager(); 190 if (pm != null) { 191 Collection<FieldMeta> fieldMetas = new FieldMetaDAO(pm).getFieldMetasForClassMeta(this); 192 for (FieldMeta fieldMeta : fieldMetas) 193 result.put(fieldMeta.getFieldName(), fieldMeta); 194 } 195 this.fieldName2FieldMeta = result; 196 } 197 else 198 logger.trace("getFieldName2FieldMeta: this.fieldName2FieldMeta != null (already populated). this={}", this); 199 200 return result; 201 } 202 203 /** 204 * Get all {@link FieldMeta} instances known to this instance. This is the meta-data for all fields 205 * <b>directly declared</b> in the class referenced by this <code>ClassMeta</code> <b>not 206 * including super-classes</b>. 207 * @return Collection of FieldMeta objects for this class 208 */ 209 public Collection<FieldMeta> getFieldMetas() { 210 return getFieldName2FieldMeta().values(); 211 } 212 213 /** 214 * Get the {@link FieldMeta} for a field that is <b>directly declared</b> in the class referenced by 215 * this <code>ClassMeta</code>. This method thus does not take super-classes into account. 216 * 217 * @param fieldName the simple field name (no class prefix). 218 * @return the {@link FieldMeta} corresponding to the specified <code>fieldName</code> or <code>null</code>, if no such field 219 * exists. 220 * @see #getFieldMeta(long) 221 * @see #getFieldMeta(String, String) 222 */ 223 public FieldMeta getFieldMeta(String fieldName) { 224 return getFieldName2FieldMeta().get(fieldName); 225 } 226 227 /** 228 * <p> 229 * Get the {@link FieldMeta} for a field that is either directly declared in the class referenced by this 230 * <code>ClassMeta</code> or in a super-class. 231 * </p> 232 * <p> 233 * If <code>className</code> is <code>null</code>, this method 234 * searches recursively in the inheritance hierarchy upwards (i.e. first this class then the super-class, 235 * then the next super-class etc.) until it finds a field matching the given <code>fieldName</code>. 236 * </p> 237 * <p> 238 * If <code>className</code> is not <code>null</code>, this method searches only in the specified class. 239 * If <code>className</code> is neither the current class nor any super-class, this method always returns 240 * <code>null</code>. 241 * </p> 242 * 243 * @param className the fully qualified class-name of the class referenced by this <code>ClassMeta</code> 244 * or any super-class. <code>null</code> to search the entire class hierarchy upwards (through all super-classes 245 * until the field is found or the last super-class was investigated). 246 * @param fieldName the simple field name (no class prefix). 247 * @return the {@link FieldMeta} matching the given criteria or <code>null</code> if no such field could be found. 248 */ 249 public FieldMeta getFieldMeta(String className, String fieldName) { 250 if (className == null) { 251 FieldMeta fieldMeta = getFieldMeta(fieldName); 252 if (fieldMeta != null) 253 return fieldMeta; 254 255 if (superClassMeta != null) 256 return superClassMeta.getFieldMeta(className, fieldName); 257 else 258 return null; 259 } 260 else { 261 if (getClassName().equals(className)) 262 return getFieldMeta(fieldName); 263 else if (superClassMeta != null) 264 return superClassMeta.getFieldMeta(className, fieldName); 265 else 266 return null; 267 } 268 } 269 270 /** 271 * Get the {@link FieldMeta} with the specified {@link FieldMeta#getFieldID() fieldID}. It does not matter, if 272 * this field is directly in the class referenced by this <code>ClassMeta</code> or in a super-class. 273 * @param fieldID the {@link FieldMeta#getFieldID() fieldID} of the <code>FieldMeta</code> to be found. 274 * @return the {@link FieldMeta} referenced by the given <code>fieldID</code> or <code>null</code>, if no such 275 * field exists in the class or any super-class. 276 */ 277 public FieldMeta getFieldMeta(long fieldID) 278 { 279 Map<Long, FieldMeta> m = fieldID2FieldMeta; 280 281 if (m == null) { 282 m = new HashMap<Long, FieldMeta>(getFieldName2FieldMeta().size()); 283 for (FieldMeta fieldMeta : getFieldName2FieldMeta().values()) 284 m.put(fieldMeta.getFieldID(), fieldMeta); 285 286 fieldID2FieldMeta = m; 287 } 288 289 FieldMeta fieldMeta = m.get(fieldID); 290 if (fieldMeta != null) 291 return fieldMeta; 292 293 if (superClassMeta != null) 294 return superClassMeta.getFieldMeta(fieldID); 295 else 296 return null; 297 } 298 299 public void addFieldMeta(FieldMeta fieldMeta) { 300 if (!this.equals(fieldMeta.getClassMeta())) 301 throw new IllegalArgumentException("fieldMeta.classMeta != this"); 302 303 PersistenceManager pm = getPersistenceManager(); 304 if (pm != null) 305 fieldMeta = pm.makePersistent(fieldMeta); 306 307 getFieldName2FieldMeta().put(fieldMeta.getFieldName(), fieldMeta); 308 fieldID2FieldMeta = null; 309 } 310 311 public void removeFieldMeta(FieldMeta fieldMeta) { 312 if (!this.equals(fieldMeta.getClassMeta())) 313 throw new IllegalArgumentException("fieldMeta.classMeta != this"); 314 315 getFieldName2FieldMeta().remove(fieldMeta.getFieldName()); 316 fieldID2FieldMeta = null; 317 318 PersistenceManager pm = getPersistenceManager(); 319 if (pm != null) 320 pm.deletePersistent(fieldMeta); 321 } 322 323 @Override 324 public int hashCode() { 325 return (int) (classID ^ (classID >>> 32)); 326 } 327 328 @Override 329 public boolean equals(Object obj) { 330 if (this == obj) return true; 331 if (obj == null) return false; 332 if (getClass() != obj.getClass()) return false; 333 ClassMeta other = (ClassMeta) obj; 334 return this.classID == other.classID; 335 } 336 337 @Override 338 public String toString() { 339 return ( 340 this.getClass().getName() 341 + '@' 342 + Integer.toHexString(System.identityHashCode(this)) 343 + '[' + classID + ',' + getClassName() + ']' 344 ); 345 } 346 347 @NotPersistent 348 private AbstractClassMetaData dataNucleusClassMetaData; 349 350 public AbstractClassMetaData getDataNucleusClassMetaData(ExecutionContext executionContext) 351 { 352 if (dataNucleusClassMetaData != null) 353 return dataNucleusClassMetaData; 354 355 AbstractClassMetaData dnClassMetaData = executionContext.getMetaDataManager().getMetaDataForClass(getClassName(), executionContext.getClassLoaderResolver()); 356 if (dnClassMetaData == null) 357 throw new IllegalStateException("DataNucleus does not know any meta-data for this class: classID=" + getClassID() + " className=" + getClassName()); 358 359 dataNucleusClassMetaData = dnClassMetaData; 360 return dnClassMetaData; 361 } 362 363 @Override 364 public void jdoPreDetach() { } 365 366 protected static final ThreadLocal<Set<ClassMeta>> attachedClassMetasInPostDetachThreadLocal = new ThreadLocal<Set<ClassMeta>>() { 367 @Override 368 protected Set<ClassMeta> initialValue() { 369 return new HashSet<ClassMeta>(); 370 } 371 }; 372 373 @Override 374 public void jdoPostDetach(Object o) { 375 final ClassMeta attached = (ClassMeta) o; 376 final ClassMeta detached = this; 377 logger.debug("jdoPostDetach: attached={}", attached); 378 379 Set<ClassMeta> attachedClassMetasInPostDetach = attachedClassMetasInPostDetachThreadLocal.get(); 380 if (!attachedClassMetasInPostDetach.add(attached)) { 381 logger.debug("jdoPostDetach: Already in detachment => Skipping detachment of this.fieldName2FieldMeta! attached={}", attached); 382 return; 383 } 384 try { 385 386 PersistenceManager pm = attached.getPersistenceManager(); 387 if (pm == null) 388 throw new IllegalStateException("attached.getPersistenceManager() returned null!"); 389 390 // The following fields should already be null, but we better ensure that we never 391 // contain *AT*tached objects inside a *DE*tached container. 392 detached.fieldName2FieldMeta = null; 393 detached.fieldID2FieldMeta = null; 394 395 Set<?> fetchGroups = pm.getFetchPlan().getGroups(); 396 if (fetchGroups.contains(javax.jdo.FetchGroup.ALL)) { 397 logger.debug("jdoPostDetach: Detaching this.fieldName2FieldMeta: attached={}", attached); 398 399 // if the fetch-groups say we should detach the FieldMetas, we do it. 400 HashMap<String, FieldMeta> map = new HashMap<String, FieldMeta>(); 401 Collection<FieldMeta> detachedFieldMetas = pm.detachCopyAll(attached.getFieldMetas()); 402 for (FieldMeta detachedFieldMeta : detachedFieldMetas) { 403 detachedFieldMeta.setClassMeta(this); // ensure, it's the identical (not only equal) ClassMeta. 404 map.put(detachedFieldMeta.getFieldName(), detachedFieldMeta); 405 } 406 detached.fieldName2FieldMeta = map; 407 } 408 409 } finally { 410 attachedClassMetasInPostDetach.remove(attached); 411 } 412 } 413 }