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 }