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             * &#64;Persistent(mappedBy="classMeta", dependentValue="true")
136             * &#64;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    }