package com.googlecode.objectify.impl; import com.google.appengine.api.datastore.AsyncDatastoreService; import com.google.appengine.api.datastore.DatastoreServiceConfig; import com.google.appengine.api.datastore.ReadPolicy; import com.google.appengine.api.datastore.ReadPolicy.Consistency; import com.googlecode.objectify.Key; import com.googlecode.objectify.Objectify; import com.googlecode.objectify.ObjectifyFactory; import com.googlecode.objectify.TxnType; import com.googlecode.objectify.Work; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.cmd.Deferred; import com.googlecode.objectify.cmd.Deleter; import com.googlecode.objectify.cmd.Loader; import com.googlecode.objectify.cmd.Saver; import com.googlecode.objectify.impl.translate.CreateContext; import com.googlecode.objectify.impl.translate.SaveContext; import com.googlecode.objectify.impl.translate.Translator; import com.googlecode.objectify.impl.translate.TypeKey; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; /** * <p>Implementation of the Objectify interface. This is also suitable for subclassing; you * can return your own subclass by overriding ObjectifyFactory.begin().</p> * * <p>Note we *always* use the AsyncDatastoreService * methods that use transactions to avoid the confusion of implicit transactions.</p> * * @author Jeff Schnitzer <jeff@infohazard.org> */ public class ObjectifyImpl<O extends Objectify> implements Objectify, Cloneable { /** The factory that produced us */ protected ObjectifyFactory factory; /** Our options */ protected boolean cache = true; protected Consistency consistency = Consistency.STRONG; protected Double deadline; protected boolean mandatoryTransactions = false; /** */ protected Transactor<O> transactor = new TransactorNo<>(this); /** */ public ObjectifyImpl(ObjectifyFactory fact) { this.factory = fact; } /** Copy constructor */ public ObjectifyImpl(ObjectifyImpl<O> other) { this.factory = other.factory; this.cache = other.cache; this.consistency = other.consistency; this.deadline = other.deadline; this.transactor = other.transactor; this.mandatoryTransactions = other.mandatoryTransactions; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#getFactory() */ public ObjectifyFactory factory() { return this.factory; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#find() */ @Override public Loader load() { return new LoaderImpl<>(this); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#put() */ @Override public Saver save() { return new SaverImpl(this); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#delete() */ @Override public Deleter delete() { return new DeleterImpl(this); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#defer() */ @Override public Deferred defer() { return new DeferredImpl(this); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#consistency(com.google.appengine.api.datastore.ReadPolicy.Consistency) */ @Override @SuppressWarnings("unchecked") public O consistency(Consistency value) { if (value == null) throw new IllegalArgumentException("Consistency cannot be null"); ObjectifyImpl<O> clone = this.clone(); clone.consistency = value; return (O)clone; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#deadline(java.lang.Double) */ @Override @SuppressWarnings("unchecked") public O deadline(Double value) { ObjectifyImpl<O> clone = this.clone(); clone.deadline = value; return (O)clone; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#cache(boolean) */ @Override @SuppressWarnings("unchecked") public O cache(boolean value) { ObjectifyImpl<O> clone = this.clone(); clone.cache = value; return (O)clone; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#mandatoryTransactions(boolean) */ @Override @SuppressWarnings("unchecked") public O mandatoryTransactions(boolean value) { ObjectifyImpl<O> clone = this.clone(); clone.mandatoryTransactions = value; return (O)clone; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#transactionless() */ @Override @SuppressWarnings("unchecked") public O transactionless() { return (O)transactor.transactionless(this); } /* (non-Javadoc) * @see java.lang.Object#clone() */ @SuppressWarnings("unchecked") protected ObjectifyImpl<O> clone() { try { return (ObjectifyImpl<O>)super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); // impossible } } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#getTxn() */ public TransactionImpl getTransaction() { return transactor.getTransaction(); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#execute(com.googlecode.objectify.TxnType, com.googlecode.objectify.Work) */ @Override public <R> R execute(TxnType txnType, Work<R> work) { return transactor.execute(this, txnType, work); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#transact(com.googlecode.objectify.Work) */ @Override public <R> R transact(Work<R> work) { return transactor.transact(this, work); } @Override public void transact(final Runnable work) { transact(new Work<Void>() { @Override public Void run() { work.run(); return null; } }); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#transact(com.googlecode.objectify.Work) */ @Override public <R> R transactNew(Work<R> work) { return this.transactNew(Integer.MAX_VALUE, work); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#transactNew(com.googlecode.objectify.Work) */ @Override public <R> R transactNew(int limitTries, Work<R> work) { return transactor.transactNew(this, limitTries, work); } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#clear() */ @Override public void clear() { transactor.getSession().clear(); } /** * Make a datastore service config that corresponds to our options. */ protected DatastoreServiceConfig createDatastoreServiceConfig() { DatastoreServiceConfig cfg = DatastoreServiceConfig.Builder.withReadPolicy(new ReadPolicy(consistency)); if (deadline != null) cfg.deadline(deadline); return cfg; } /** * Make a datastore service config that corresponds to our options. */ protected AsyncDatastoreService createAsyncDatastoreService() { return factory.createAsyncDatastoreService(this.createDatastoreServiceConfig(), cache); } /** * Use this once for one operation and then throw it away * @return a fresh engine that handles fundamental datastore operations for saving and deleting */ protected WriteEngine createWriteEngine() { if (mandatoryTransactions && getTransaction() == null) throw new IllegalStateException("You have attempted save/delete outside of a transaction, but you have enabled ofy().mandatoryTransactions(true). Perhaps you wanted to start a transaction first?"); return new WriteEngine(this, createAsyncDatastoreService(), transactor.getSession(), transactor.getDeferrer()); } /** * <p>Translates the value of a filter clause into something the datastore understands. Key<?> goes to native Key, * entities go to native Key, java.sql.Date goes to java.util.Date, etc. It uses the same translation system * that is used for standard entity fields, but does no checking to see if the value is appropriate for the field.</p> * * <p>Unrecognized types are returned as-is.</p> * * <p>A future version of this method might check for type validity.</p> * * @return whatever can be put into a filter clause. */ protected Object makeFilterable(Object value) { if (value == null) return null; // This is really quite a dilemma. We need to convert that value into something we can filter by, but we don't // really have a lot of information about it. We could use type information from the matched field, but there's // no guarantee that there is a field to check - it could be a typeless query or a query on an old property value. // The only real solution is to create a (non root!) translator on the fly. Even that is not straightforward, // because erasure wipes out any component type information in a collection. We don't know what the collection // contains. // // The answer: Check for collections explicitly. Create a separate translator for every item in the collection; // after all, it could be a heterogeneous list. This is not especially efficient but GAE only allows a handful of // items in a IN operation and at any rate processing will still be negligible compared to the cost of a query. // If this is an array, make life easier by turning it into a list first. Because of primitive // mismatching we can't trust Arrays.asList(). if (value.getClass().isArray()) { int len = Array.getLength(value); List<Object> asList = new ArrayList<>(len); for (int i=0; i<len; i++) asList.add(Array.get(value, i)); value = asList; } if (value instanceof Iterable) { List<Object> result = new ArrayList<>(50); // hard limit is 30, but wth for (Object obj: (Iterable<?>)value) result.add(makeFilterable(obj)); return result; } else { // Special case entity pojos that become keys if (value.getClass().isAnnotationPresent(Entity.class)) { return factory().keys().getMetadataSafe(value).getRawKey(value); } else { // Run it through a translator Translator<Object, Object> translator = factory().getTranslators().get(new TypeKey<>(value.getClass()), new CreateContext(factory()), Path.root()); return translator.save(value, false, new SaveContext(), Path.root()); } } } /** */ protected Session getSession() { return this.transactor.getSession(); } /** @return true if cache is enabled */ public boolean getCache() { return cache; } /* (non-Javadoc) * @see com.googlecode.objectify.Objectify#isLoaded(com.googlecode.objectify.Key) */ @Override public boolean isLoaded(Key<?> key) { return transactor.getSession().contains(key); } @Override public void flush() { transactor.getDeferrer().flush(); } /** * Defer the saving of one entity. Updates the session cache with this new value. */ void deferSave(Object entity) { transactor.getDeferrer().deferSave(entity); } /** * Defer the deletion of one entity. Updates the session cache with this new value. */ void deferDelete(Key<?> key) { transactor.getDeferrer().deferDelete(key); } }