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