package com.googlecode.objectify.impl;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Transaction;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.impl.ref.LiveRef;
import com.googlecode.objectify.impl.translate.LoadContext;
import com.googlecode.objectify.util.ResultCache;
import lombok.extern.java.Log;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Level;
/**
* Represents one "batch" of loading. Get a number of Result<?> objects, then execute(). Some work is done
* right away, some work is done on the first get(). There might be multiple rounds of execution to process
* all the @Load groups, but that is invisible outside this class.
*
* @author Jeff Schnitzer <jeff@infohazard.org>
*/
@Log
public class LoadEngine
{
/** */
final ObjectifyImpl<?> ofy;
private final AsyncDatastoreService ads;
private final Session session;
private final LoadArrangement loadArrangement;
/** The current round, replaced whenever the round executes */
Round round;
/**
*/
public LoadEngine(ObjectifyImpl<?> ofy, Session session, AsyncDatastoreService ads, LoadArrangement loadArrangement) {
this.ofy = ofy;
this.session = session;
this.ads = ads;
this.loadArrangement = loadArrangement;
this.round = new Round(this, 0);
if (log.isLoggable(Level.FINEST))
log.finest("Starting load engine with groups " + loadArrangement);
}
/**
* Gets the result, possibly from the session, putting it in the session if necessary.
* Also will recursively prepare the session with @Load parents as appropriate.
* @throws NullPointerException if key is null
*/
public <T> Result<T> load(Key<T> key) {
if (key == null)
throw new NullPointerException("You tried to load a null key!");
Result<T> result = round.get(key);
// If we are running a transaction, enlist the result so that it gets processed on commit even
// if the client never materializes the result.
if (ofy.getTransaction() != null)
ofy.getTransaction().enlist(result);
// Now check to see if we need to recurse and add our parent(s) to the round
if (key.getParent() != null) {
KeyMetadata<?> meta = ofy.factory().keys().getMetadata(key);
// Is it really possible for this to be null?
if (meta != null) {
if (meta.shouldLoadParent(loadArrangement)) {
load(key.getParent());
}
}
}
return result;
}
/**
* Starts asychronous fetching of the batch.
*/
public void execute() {
if (round.needsExecution()) {
Round old = round;
round = old.next();
old.execute();
}
}
/**
* Create a Ref for the key, and maybe start a load operation depending on current load groups.
*
* @param rootEntity is the entity key which holds this property (possibly through some level of embedded objects)
*/
public <T> Ref<T> makeRef(Key<?> rootEntity, LoadConditions loadConditions, Key<T> key) {
Ref<T> ref = new LiveRef<>(key, ofy);
if (shouldLoad(loadConditions)) {
load(key);
}
return ref;
}
/**
* @return true if the specified property should be loaded in this batch
*/
public boolean shouldLoad(LoadConditions loadConditions) {
return loadConditions.shouldLoad(loadArrangement, ofy.getTransaction() != null);
}
/**
* Stuffs an Entity into a place where values in the round can be obtained instead of going to the datastore.
* Called by non-hybrid queries to add results and eliminate batch fetching.
*/
public void stuff(Entity ent) {
round.stuff(ent);
}
/**
* Asynchronously translate raw to processed; might produce successive load operations as refs are filled in
*/
public Result<Map<Key<?>, Object>> translate(final Result<Map<com.google.appengine.api.datastore.Key, Entity>> raw) {
return new ResultCache<Map<Key<?>, Object>>() {
/** */
LoadContext ctx;
/** */
@Override
public Map<Key<?>, Object> nowUncached() {
Map<Key<?>, Object> result = new HashMap<>(raw.now().size() * 2);
ctx = new LoadContext(LoadEngine.this);
for (Entity ent: raw.now().values()) {
Key<?> key = Key.create(ent.getKey());
Object entity = load(ent, ctx);
result.put(key, entity);
}
return result;
}
/**
* We need to execute the done() after the translated value has been set, otherwise we
* can produce an infinite recursion problem.
*/
@Override
protected void postExecuteHook() {
ctx.done();
ctx = null;
}
};
}
/**
* Fetch the keys from the async datastore using the current transaction context
*/
public Result<Map<com.google.appengine.api.datastore.Key, Entity>> fetch(Set<com.google.appengine.api.datastore.Key> keys) {
Transaction txn = (ofy.getTransaction() == null) ? null : ofy.getTransaction().getRaw();
log.log(Level.FINER, "Fetching " + keys.size() + " keys" + (txn == null ? ": " : " in txn: ") + keys);
Future<Map<com.google.appengine.api.datastore.Key, Entity>> fut = ads.get(txn, keys);
return ResultAdapter.create(fut);
}
/**
* Converts a datastore entity into a typed pojo object
* @return an assembled pojo, or the Entity itself if the kind is not registered, or null if the input value was null
*/
@SuppressWarnings("unchecked")
public <T> T load(Entity ent, LoadContext ctx) {
if (ent == null)
return null;
EntityMetadata<T> meta = ofy.factory().getMetadata(ent.getKind());
if (meta == null)
return (T)ent;
else
return meta.load(ent, ctx);
}
/** */
public Session getSession() {
return session;
}
/** */
public LoadArrangement getLoadArrangement() {
return loadArrangement;
}
}