package com.googlecode.objectify; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceConfig; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.ReadPolicy; import com.google.appengine.api.datastore.Transaction; import com.googlecode.objectify.impl.CachingDatastoreService; import com.googlecode.objectify.impl.EntityMetadata; import com.googlecode.objectify.impl.ObjectifyImpl; import com.googlecode.objectify.impl.SessionCachingObjectifyImpl; /** * <p>Factory which allows us to construct implementations of the Objectify interface. * Just call {@code begin()}.</p> * * <p>Note that unlike the DatastoreService, there is no implicit transaction management. * You either create an Objectify without a transaction (by calling {@code begin()} or you * create one with a transaction (by calling {@code beginTransaction()}. If you create * an Objectify with a transaction, you should use it like this:</p> * <code><pre> * Objectify data = factory.beginTransaction() * try { * // do work * data.getTxn().commit(); * } * finally { * if (data.getTxn().isActive()) data.getTxn().rollback(); * } * </pre></code> * * <p>It would be fairly easy for someone to implement a ScanningObjectifyFactory * on top of this class that looks for @Entity annotations based on Scannotation or * Reflections, but this would add extra dependency jars and need a hook for * application startup.</p> * * @author Jeff Schnitzer <jeff@infohazard.org> */ public class ObjectifyFactory { /** */ protected Map<String, EntityMetadata<?>> types = new ConcurrentHashMap<String, EntityMetadata<?>>(); /** True if any @Cached entities have been registered */ protected boolean hasCachedEntities; /** * Override this in your factory if you wish to use a different impl, say, * one based on the ObjectifyWrapper. * * @param ds the DatastoreService * @param opts the options for creating this Objectify * @return an instance of Objectify configured appropriately */ protected Objectify createObjectify(DatastoreService ds, ObjectifyOpts opts) { Transaction txn = (opts.getBeginTransaction()) ? ds.beginTransaction() : null; if (opts.getSessionCache()) return new SessionCachingObjectifyImpl(this, ds, txn); else return new ObjectifyImpl(this, ds, txn); } /** * @return a DatastoreService which *might* be a caching version if any cached * entities have been registered. Delegates to getRawDatastoreService() to * actually obtain the instance from appengine. */ protected DatastoreService getDatastoreService(ObjectifyOpts opts) { DatastoreServiceConfig cfg = DatastoreServiceConfig.Builder.withReadPolicy(new ReadPolicy(opts.getConsistency())); if (opts.getDeadline() != null) cfg.deadline(opts.getDeadline()); if (opts.getGlobalCache() && this.hasCachedEntities) return new CachingDatastoreService(this, this.getRawDatastoreService(cfg)); else return this.getRawDatastoreService(cfg); } /** * You can override this to add behavior at the raw datastoreservice level. */ protected DatastoreService getRawDatastoreService(DatastoreServiceConfig cfg) { return DatastoreServiceFactory.getDatastoreService(cfg); } /** * Create a lightweight Objectify instance with the default options. * Equivalent to begin(new ObjectifyOpts()). */ public Objectify begin() { return this.begin(new ObjectifyOpts()); } /** * @return an Objectify from the DatastoreService with the specified options. * This is a lightweight operation and can be used freely. */ public Objectify begin(ObjectifyOpts opts) { DatastoreService ds = this.getDatastoreService(opts); return this.createObjectify(ds, opts); } /** * @return an Objectify which uses a transaction. Be careful, you cannot * access entities across differing entity groups. */ public Objectify beginTransaction() { return this.begin(new ObjectifyOpts().setBeginTransaction(true)); } /** * <p>Registers a class with the system so that we can recompose an * object from its key kind. The default kind is the simplename * of the class, overridden by the @Entity annotation.</p> * * <p>This method must be called in a single-threaded mode, around the * time of app initialization. After all types are registered, the * get() method can be called.</p> */ public <T> void register(Class<T> clazz) { String kind = getKind(clazz); EntityMetadata<T> meta = new EntityMetadata<T>(this, clazz); this.types.put(kind, meta); if (meta.getCached() != null) this.hasCachedEntities = true; } // // Stuff which should only be necessary internally, but might be useful to others. // /** * @return the kind associated with a particular entity class, checking both @Entity * annotations and defaulting to the class' simplename. */ public String getKind(Class<?> clazz) { com.googlecode.objectify.annotation.Entity ourAnn = clazz.getAnnotation(com.googlecode.objectify.annotation.Entity.class); if (ourAnn != null && ourAnn.name() != null && ourAnn.name().length() != 0) return ourAnn.name(); javax.persistence.Entity jpaAnn = clazz.getAnnotation(javax.persistence.Entity.class); if (jpaAnn != null && jpaAnn.name() != null && jpaAnn.name().length() != 0) return jpaAnn.name(); return clazz.getSimpleName(); } /** * @return the kind associated with a particular entity class */ public String getKind(String className) { try { Class<?> clazz = Class.forName(className); return this.getKind(clazz); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * @return the metadata for a kind of entity based on its key * @throws IllegalArgumentException if the kind has not been registered */ public <T> EntityMetadata<T> getMetadata(com.google.appengine.api.datastore.Key key) { return this.getMetadata(key.getKind()); } /** * @return the metadata for a kind of entity based on its key * @throws IllegalArgumentException if the kind has not been registered */ @SuppressWarnings("unchecked") public <T> EntityMetadata<T> getMetadata(Key<T> key) { // I would love to know why this produces a warning return (EntityMetadata<T>)this.getMetadata(this.getKind(key.getKindClassName())); } /** * @return the metadata for a kind of typed object * @throws IllegalArgumentException if the kind has not been registered */ public <T> EntityMetadata<? extends T> getMetadata(Class<T> clazz) { return this.getMetadata(this.getKind(clazz)); } /** * Named differently so you don't accidentally use the Object form * @return the metadata for a kind of typed object. * @throws IllegalArgumentException if the kind has not been registered */ @SuppressWarnings("unchecked") public <T> EntityMetadata<T> getMetadataForEntity(T obj) { // Type erasure sucks ass return (EntityMetadata<T>)this.getMetadata(obj.getClass()); } /** */ @SuppressWarnings("unchecked") protected <T> EntityMetadata<T> getMetadata(String kind) { EntityMetadata<T> metadata = (EntityMetadata<T>)types.get(kind); if (metadata == null) throw new IllegalArgumentException("No registered type for kind " + kind); else return metadata; } /** * Converts a typed Key<?> into a raw datastore Key. * @param typedKey can be null, resulting in a null Key */ public com.google.appengine.api.datastore.Key typedKeyToRawKey(Key<?> typedKey) { if (typedKey == null) return null; if (typedKey.getName() != null) return KeyFactory.createKey(this.typedKeyToRawKey(typedKey.getParent()), this.getKind(typedKey.getKindClassName()), typedKey.getName()); else return KeyFactory.createKey(this.typedKeyToRawKey(typedKey.getParent()), this.getKind(typedKey.getKindClassName()), typedKey.getId()); } /** * Converts a raw datastore Key into a typed Key<?>. * @param rawKey can be null, resulting in a null Key */ public <T> Key<T> rawKeyToTypedKey(com.google.appengine.api.datastore.Key rawKey) { if (rawKey == null) return null; EntityMetadata<T> meta = this.getMetadata(rawKey); Class<T> entityClass = meta.getEntityClass(); if (rawKey.getName() != null) return new Key<T>(this.rawKeyToTypedKey(rawKey.getParent()), entityClass, rawKey.getName()); else return new Key<T>(this.rawKeyToTypedKey(rawKey.getParent()), entityClass, rawKey.getId()); } /** * <p>Gets the Key<T> given an object that might be a Key, Key<T>, or entity.</p> * * @param keyOrEntity must be a Key, Key<T>, or registered entity. * @throws NullPointerException if keyOrEntity is null * @throws IllegalArgumentException if keyOrEntity is not a Key, Key<T>, or registered entity */ @SuppressWarnings("unchecked") public <T> Key<T> getKey(Object keyOrEntity) { if (keyOrEntity instanceof com.google.appengine.api.datastore.Key) return this.rawKeyToTypedKey((com.google.appengine.api.datastore.Key)keyOrEntity); else if (keyOrEntity instanceof Key<?>) return (Key<T>)keyOrEntity; else return this.rawKeyToTypedKey(this.getMetadataForEntity(keyOrEntity).getKey(keyOrEntity)); } /** * <p>Gets the raw datstore Key given an object that might be a Key, Key<T>, or entity.</p> * * @param keyOrEntity must be a Key, Key<T>, or registered entity. * @throws NullPointerException if keyOrEntity is null * @throws IllegalArgumentException if keyOrEntity is not a Key, Key<T>, or registered entity */ public com.google.appengine.api.datastore.Key getRawKey(Object keyOrEntity) { if (keyOrEntity instanceof com.google.appengine.api.datastore.Key) return (com.google.appengine.api.datastore.Key)keyOrEntity; else if (keyOrEntity instanceof Key<?>) return this.typedKeyToRawKey((Key<?>)keyOrEntity); else return this.getMetadataForEntity(keyOrEntity).getKey(keyOrEntity); } /** * Translate Key<?> or Entity objects into something that can be used in a filter clause. * Anything unknown (including null) is simply returned as-is and we hope that the filter works. * * @return whatever can be put into a filter clause. */ public Object makeFilterable(Object keyOrEntityOrOther) { if (keyOrEntityOrOther == null) { return null; } else if (keyOrEntityOrOther instanceof Key<?>) { return this.typedKeyToRawKey((Key<?>)keyOrEntityOrOther); } else if (keyOrEntityOrOther instanceof Iterable<?>) { List<Object> all = (keyOrEntityOrOther instanceof Collection<?>) ? new ArrayList<Object>(((Collection<?>)keyOrEntityOrOther).size()) : new ArrayList<Object>(); for (Object obj: ((Iterable<?>)keyOrEntityOrOther)) all.add(this.makeFilterable(obj)); return all; } else if (keyOrEntityOrOther instanceof Object[]) { return this.makeFilterable(Arrays.asList((Object[])keyOrEntityOrOther)); } else { // Unfortunately we can't use getRawKey() because it throws IllegalArgumentException String kind = this.getKind(keyOrEntityOrOther.getClass()); EntityMetadata<?> meta = this.types.get(kind); if (meta == null) return keyOrEntityOrOther; else return meta.getKey(keyOrEntityOrOther); } } /** * <p>Converts a Key<?> into a web-safe string suitable for http parameters * in URLs. Note that you can convert back and forth with the {@code keyToString()} * and {@code stringToKey()} methods.</p> * * <p>The String is actually generated by using the KeyFactory {@code keyToString()} * method on a raw version of the datastore key. You can, if you wanted, use * these web safe strings interchangeably.</p> * * @param key is any Objectify key * @return a simple String which does not need urlencoding */ public String keyToString(Key<?> key) { return KeyFactory.keyToString(this.typedKeyToRawKey(key)); } /** * Converts a String generated with {@code keyToString()} back into an Objectify * Key. The String could also have been generated by the GAE {@code KeyFactory}. * * @param stringifiedKey is generated by either {@code ObjectifyFactory.keyToString()} or * {@code KeyFactory.keyToString()}. * @return a Key<?> */ public <T> Key<T> stringToKey(String stringifiedKey) { return this.rawKeyToTypedKey(KeyFactory.stringToKey(stringifiedKey)); } /** * Preallocate a contiguous range of unique ids within the namespace of the * specified entity class. These ids can be used in concert with the normal * automatic allocation of ids when put()ing entities with null Long id fields. * * @param clazz must be a registered entity class with a Long or long id field. * @param num must be >= 1 and <= 1 billion */ public <T> KeyRange<T> allocateIds(Class<T> clazz, long num) { // Feels a little weird going directly to the DatastoreServiceFactory but the // allocateIds() method really is optionless. String kind = this.getKind(clazz); return new KeyRange<T>(this, DatastoreServiceFactory.getDatastoreService().allocateIds(kind, num)); } /** * Preallocate a contiguous range of unique ids within the namespace of the * specified entity class and the parent key. These ids can be used in concert with the normal * automatic allocation of ids when put()ing entities with null Long id fields. * * @param parent must be a legitimate parent key for the class type. It need not * point to an existent entity, but it must be the correct type for clazz. * @param clazz must be a registered entity class with a Long or long id field, and * a parent key of the correct type. * @param num must be >= 1 and <= 1 billion */ public <T> KeyRange<T> allocateIds(Key<?> parent, Class<T> clazz, long num) { // Feels a little weird going directly to the DatastoreServiceFactory but the // allocateIds() method really is optionless. com.google.appengine.api.datastore.Key rawParent = this.typedKeyToRawKey(parent); String kind = this.getKind(clazz); return new KeyRange<T>(this, DatastoreServiceFactory.getDatastoreService().allocateIds(rawParent, kind, num)); } }