PageRenderTime 67ms CodeModel.GetById 33ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 1ms

/src/com/google/appengine/datanucleus/DatastoreManager.java

http://datanucleus-appengine.googlecode.com/
Java | 711 lines | 408 code | 89 blank | 214 comment | 43 complexity | 6d741ab7e3c12429441561bcf6bce138 MD5 | raw file
  1/**********************************************************************
  2Copyright (c) 2009 Google Inc.
  3
  4Licensed under the Apache License, Version 2.0 (the "License");
  5you may not use this file except in compliance with the License.
  6You may obtain a copy of the License at
  7
  8http://www.apache.org/licenses/LICENSE-2.0
  9
 10Unless required by applicable law or agreed to in writing, software
 11distributed under the License is distributed on an "AS IS" BASIS,
 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13See the License for the specific language governing permissions and
 14limitations under the License.
 15**********************************************************************/
 16package com.google.appengine.datanucleus;
 17
 18import com.google.appengine.api.datastore.DatastoreService;
 19import com.google.appengine.api.datastore.DatastoreServiceConfig;
 20import com.google.appengine.api.datastore.ReadPolicy;
 21import com.google.appengine.api.datastore.ReadPolicy.Consistency;
 22import com.google.appengine.api.datastore.TransactionOptions;
 23import com.google.appengine.datanucleus.mapping.DatastoreTable;
 24import com.google.appengine.datanucleus.scostore.FKListStore;
 25import com.google.appengine.datanucleus.scostore.FKSetStore;
 26
 27import org.datanucleus.ClassLoaderResolver;
 28import org.datanucleus.flush.FlushOrdered;
 29import org.datanucleus.NucleusContext;
 30import org.datanucleus.PersistenceConfiguration;
 31import org.datanucleus.PropertyNames;
 32import org.datanucleus.api.ApiAdapter;
 33import org.datanucleus.exceptions.NucleusFatalUserException;
 34import org.datanucleus.metadata.AbstractClassMetaData;
 35import org.datanucleus.metadata.AbstractMemberMetaData;
 36import org.datanucleus.metadata.ClassMetaData;
 37import org.datanucleus.metadata.InheritanceStrategy;
 38import org.datanucleus.metadata.RelationType;
 39import org.datanucleus.store.DefaultCandidateExtent;
 40import org.datanucleus.ExecutionContext;
 41import org.datanucleus.store.Extent;
 42import org.datanucleus.store.NucleusConnection;
 43import org.datanucleus.store.NucleusConnectionImpl;
 44import org.datanucleus.state.ObjectProvider;
 45import org.datanucleus.store.StoreData;
 46import org.datanucleus.store.connection.ConnectionFactory;
 47import org.datanucleus.store.connection.ManagedConnection;
 48import org.datanucleus.store.exceptions.NoExtentException;
 49import org.datanucleus.store.mapped.exceptions.NoTableManagedException;
 50import org.datanucleus.store.fieldmanager.FieldManager;
 51import org.datanucleus.store.mapped.DatastoreContainerObject;
 52import org.datanucleus.store.mapped.MappedStoreData;
 53import org.datanucleus.store.mapped.MappedStoreManager;
 54import org.datanucleus.store.mapped.StatementClassMapping;
 55import org.datanucleus.store.mapped.mapping.JavaTypeMapping;
 56import org.datanucleus.store.scostore.ListStore;
 57import org.datanucleus.store.scostore.SetStore;
 58import org.datanucleus.util.ClassUtils;
 59import org.datanucleus.util.Localiser;
 60import org.datanucleus.util.NucleusLogger;
 61
 62import java.util.Arrays;
 63import java.util.Collection;
 64import java.util.Collections;
 65import java.util.HashSet;
 66import java.util.Map;
 67import java.util.Set;
 68import java.util.concurrent.ConcurrentHashMap;
 69
 70/**
 71 * StoreManager for GAE/J with DataNucleus.
 72 * When the user specifies "datanucleus.ConnectionURL" as "appengine", an instance of this class is created.
 73 * <h3>Information stored for each object</h3>
 74 * <p>
 75 * When an object of a class is stored, we can store the following information as properties of the Entity.
 76 * <ul>
 77 * <li>Primary key (which includes parentKey where required)</li>
 78 * <li>Discriminator (if defined)</li>
 79 * <li>Version (if defined)</li>
 80 * <li>Each non-null field
 81 * <ul>
 82 * <li>Unowned relation fields will store the Key(s) of related objects (dependent on StorageVersion)</li>
 83 * <li>Owned relation fields at parent side will store the Key(s) of child objects (dependent on StorageVersion)</li>
 84 * <li>Owned relation fields at child side will not store anything about the parent</li>
 85 * <li>Non-relation fields store the value (as long as supported type).</li>
 86 * </ul></li>
 87 * <li>Field(s) of embedded object.</li>
 88 * </ul>
 89 * </p>
 90 * <h3>Transaction Restrictions</h3>
 91 * <p>
 92 * The GAE datastore has some restrictions to be aware of when inspecting the code in this plugin. These
 93 * restrictions impact on the design of the plugin.
 94 * <ul>
 95 * <li>When we PUT an object it must have its parent assigned then (create of Entity), and not later. In particular
 96 * see the handling of owned relations in DatastorePersistenceHandler, and the order of PUTs, making sure we have
 97 * the parent first before any child is PUT.</li>
 98 * <li>All objects have to be in the same entity-group, except when using XG transactions.</li>
 99 * </ul>
100 * </p>
101 * 
102 * @author Max Ross <maxr@google.com>
103 */
104public class DatastoreManager extends MappedStoreManager {
105  protected static final Localiser GAE_LOCALISER = Localiser.getInstance(
106      "com.google.appengine.datanucleus.Localisation", DatastoreManager.class.getClassLoader());
107
108  private static final String EXTENSION_PREFIX = "gae.";
109
110  /** The name of the metadata extension that marks a field as a parent. */
111  public static final String PARENT_PK = EXTENSION_PREFIX + "parent-pk";
112
113  /** The name of the metadata extension that marks a field as an encoded pk. */
114  public static final String ENCODED_PK = EXTENSION_PREFIX + "encoded-pk";
115
116  /** The name of the metadata extension that marks a field as a primary key name. */
117  public static final String PK_NAME = EXTENSION_PREFIX + "pk-name";
118
119  /** The name of the metadata extension that marks a field as a primary key id. */
120  public static final String PK_ID = EXTENSION_PREFIX + "pk-id";
121
122  /** The name of the metadata extension that marks a field as unindexed. */
123  public static final String UNINDEXED_PROPERTY = EXTENSION_PREFIX + "unindexed";
124
125  public static final String DATASTORE_READ_CONSISTENCY_PROPERTY =
126      "datanucleus.appengine.datastoreReadConsistency";
127
128  public static final String DATASTORE_ENABLE_XG_TXNS_PROPERTY =
129      "datanucleus.appengine.datastoreEnableXGTransactions";
130
131  public static final String GET_EXTENT_CAN_RETURN_SUBCLASSES_PROPERTY =
132      "datanucleus.appengine.getExtentCanReturnSubclasses";
133
134  /** Property to set the default type of relations ("owned", "unowned") with default as "owned". */
135  public static final String RELATION_DEFAULT_MODE = "datanucleus.appengine.relationDefault";
136
137  /** Property allowing the user to turn off GAE/J-specific validation of metadata and assume its ok. */
138  public static final String VALIDATE_METADATA = "datanucleus.appengine.validateMetaData";
139
140  /**
141   * A property that is expected to be set to either "String" or "Double". The
142   * default is Double.
143   *
144   *  Double indicates that BigDecimals should be stored as a double, and
145   * therefore lose precision when stored.
146   *
147   * String indicates they should be serialized to a string using the
148   * {@link BigDecimals} class. This will result in retaining their precision
149   * when decoded. It is still possible to use inequality queries against them.
150   * For this reason string is generally preferable.
151   *
152   *
153   *  Inequality queries for fields where some values are doubles and some are
154   * encoded as strings will not work.
155   */
156  public static final String BIG_DECIMALS_ENCODEING = "datanucleus.appengine.BigDecimalsEncoding";
157
158  /**
159   * The name of the extension that indicates the return value of the batch
160   * delete query should be as accurate as possible at the expense of
161   * fulfilling the query less efficiently. If this extension is set and
162   * {@code true}, the query will be fulfilled by first fetching the entities
163   * and then deleting those that are returned.  This involves an additional
164   * roundtrip to the datastore but allows us to return a more accurate count
165   * of the number of records that were deleted (we say more accurate as
166   * opposed to accurate because it's possible that entity was deleted in
167   * between the fetch and the delete and we wouldn't have any way of knowing).
168   * If this extension is not set or is set with a value of {@code false},
169   * we'll just execute a batch delete directly and use the number of entities
170   * we were asked to delete as the return value.
171   */
172  public static final String QUERYEXT_SLOW_BUT_MORE_ACCURATE_JPQL_DELETE =
173      EXTENSION_PREFIX + "slow-but-more-accurate-jpql-delete-query";
174
175  /** Query extension that indicates the query should be excluded from the current transaction. */
176  public static final String QUERYEXT_EXCLUDE_FROM_TXN = EXTENSION_PREFIX + "exclude-query-from-txn";
177
178  /** Query extension to enable/disable use of in-memory evaluation when some syntax is unsupported in datastore. */
179  public static final String QUERYEXT_INMEMORY_WHEN_UNSUPPORTED = EXTENSION_PREFIX + "inmemory-when-unsupported";
180
181  /**
182   * Classes whose metadata we've validated.  This set gets hit on every
183   * insert, update, and fetch.  I don't expect it to be a bottleneck but
184   * if we're seeing contention we should look here.
185   */
186  private final Set<String> validatedClasses = Collections.synchronizedSet(new HashSet<String>());
187
188  /** Map of the metadata for the member of this class storing the parent ("gae.parent-pk") keyed by class name. */
189  private final Map<String, AbstractMemberMetaData> parentMemberMetaDataByClass =
190    new ConcurrentHashMap<String, AbstractMemberMetaData>();
191
192  private final boolean defaultToOwnedRelations;
193  private final TypeConversionUtils typeConversionUtils;
194  private final StorageVersion storageVersion;
195  private final DatastoreServiceConfig defaultDatastoreServiceConfigPrototypeForReads;
196  private final DatastoreServiceConfig defaultDatastoreServiceConfigPrototypeForWrites;
197  private final TransactionOptions defaultDatastoreTransactionOptionsPrototype;
198
199  private final DatastoreService datastoreServiceForReads;
200
201  protected SerializationManager serializationMgr = null;
202
203  MetaDataValidator metadataValidator;
204
205  /**
206   * Construct a DatastoreManager.
207   * @param clr The ClassLoaderResolver
208   * @param nucContext The NucleusContext
209   * @param props Properties to store on this StoreManager
210   */
211  public DatastoreManager(ClassLoaderResolver clr, NucleusContext nucContext, Map<String, Object> props)
212      throws NoSuchFieldException, IllegalAccessException {
213    super("appengine", clr, nucContext, props);
214
215    // Override some of the default property values for AppEngine
216    PersistenceConfiguration conf = nucContext.getPersistenceConfiguration();
217    conf.setProperty("datanucleus.attachSameDatastore", Boolean.TRUE.toString()); // Always only one datastore
218    // We'd like to respect the user's selection here, but the default value is 1.
219    // This is problematic for us in the situation where, for example, an embedded object
220    // gets updated more than once in a txn because we end up putting the same entity twice.
221    // TODO(maxr) Remove this once we support multiple puts
222    conf.setProperty("datanucleus.datastoreTransactionFlushLimit", Integer.MAX_VALUE);
223    // Install our key translator
224    conf.setProperty("datanucleus.identityKeyTranslatorType", "appengine");
225
226    // Check if datastore api is in CLASSPATH.  Don't let the hard-coded
227    // jar name upset you, it's just used for error messages.  The check will
228    // succeed so long as the class is available on the classpath
229    ClassUtils.assertClassForJarExistsInClasspath(
230        clr, "com.google.appengine.api.datastore.DatastoreService", "appengine-api.jar");
231
232    defaultDatastoreServiceConfigPrototypeForReads =
233        createDatastoreServiceConfigPrototypeForReads(nucContext.getPersistenceConfiguration());
234    defaultDatastoreServiceConfigPrototypeForWrites =
235        createDatastoreServiceConfigPrototypeForWrites(nucContext.getPersistenceConfiguration());
236    defaultDatastoreTransactionOptionsPrototype =
237        createDatastoreTransactionOptionsPrototype(nucContext.getPersistenceConfiguration());
238
239    // Handler for persistence process
240    persistenceHandler = new DatastorePersistenceHandler(this);
241    flushProcess = new FlushOrdered();
242
243    dba = new DatastoreAdapter();
244    initialiseIdentifierFactory(nucContext);
245
246    storageVersion = StorageVersion.fromStoreManager(this);
247
248    String defaultRelationMode = getStringProperty(RELATION_DEFAULT_MODE);
249    defaultToOwnedRelations = defaultRelationMode.equalsIgnoreCase("unowned") ? false : true;
250
251    String bigDecimalsEncoding = getStringProperty(BIG_DECIMALS_ENCODEING);
252    typeConversionUtils =
253        "String".equalsIgnoreCase(bigDecimalsEncoding) ? new TypeConversionUtils(true)
254            : new TypeConversionUtils(false);
255
256    // Add listener so we can check all metadata for unsupported features and required schema
257    metadataValidator = new MetaDataValidator(this, getMetaDataManager(), clr);
258
259    logConfiguration();
260
261    datastoreServiceForReads = DatastoreServiceFactoryInternal.getDatastoreService(
262        getDefaultDatastoreServiceConfigForReads());
263  }
264
265  @Override
266  public void close() {
267    validatedClasses.clear();
268    super.close();
269  }
270
271  public SerializationManager getSerializationManager() {
272    if (serializationMgr == null) {
273      serializationMgr = new SerializationManager();
274    }
275    return serializationMgr;
276  }
277
278  public boolean isDefaultToOwnedRelations() {
279    return defaultToOwnedRelations;
280  }
281
282  /**
283   * Convenience method to log the configuration of this store manager.
284   */
285  protected void logConfiguration()
286  {
287    super.logConfiguration();
288
289    if (NucleusLogger.DATASTORE.isDebugEnabled())
290    {
291        NucleusLogger.DATASTORE.debug("StorageVersion : " + storageVersion.toString());
292        NucleusLogger.DATASTORE.debug("Default Relation Mode : " + getStringProperty(RELATION_DEFAULT_MODE));
293        NucleusLogger.DATASTORE.debug("===========================================================");
294    }
295  }
296
297  @Override
298  public void transactionStarted(ExecutionContext ec) {
299    if (connectionFactoryIsAutoCreateTransaction()) {
300      // Obtain the ManagedConnection, triggering XAResource.start, so calling DatastoreService.beginTransaction()
301      getConnection(ec);
302    }
303  }
304
305  @Override
306  public NucleusConnection getNucleusConnection(ExecutionContext ec) {
307    ConnectionFactory cf = connectionMgr.lookupConnectionFactory(primaryConnectionFactoryName);
308
309    final ManagedConnection mc;
310    final boolean enlisted;
311    enlisted = ec.getTransaction().isActive();
312    mc = cf.getConnection(enlisted ? ec : null, ec.getTransaction(), null); // Will throw exception if already locked
313
314    // Lock the connection now that it is in use by the user
315    mc.lock();
316
317    Runnable closeRunnable = new Runnable() {
318      public void run() {
319        // Unlock the connection now that the user has finished with it
320        mc.unlock();
321        if (!enlisted) {
322          // TODO Anything to do here?
323        }
324      }
325    };
326    return new NucleusConnectionImpl(mc.getConnection(), closeRunnable);
327  }
328
329  // TODO Remove this when we support subclasses from a query
330  @Override
331  public Extent getExtent(ExecutionContext ec, Class c, boolean subclasses) {
332    AbstractClassMetaData cmd = getMetaDataManager().getMetaDataForClass(c, ec.getClassLoaderResolver());
333    validateMetaDataForClass(cmd);
334    if (!cmd.isRequiresExtent()) {
335      throw new NoExtentException(c.getName());
336    }
337    if (!getBooleanProperty(GET_EXTENT_CAN_RETURN_SUBCLASSES_PROPERTY, false)) {
338      subclasses = false;
339    }
340    // In order to avoid breaking existing apps I'm hard-coding subclasses to
341    // be false.  This breaks spec compliance since the no-arg overload of
342    // PersistenceManager.getExtent() is supposed to return subclasses.
343    return new DefaultCandidateExtent(ec, c, subclasses, cmd);
344  }
345
346  /**
347   * Method to return the default identifier case.
348   * @return Identifier case to use if not specified by the user
349   */
350  public String getDefaultIdentifierCase() {
351    return "PreserveCase";
352  }
353
354  @Override
355  public Collection<String> getSupportedOptions() {
356    Set<String> opts = new HashSet<String>();
357    opts.add("TransactionIsolationLevel.read-committed");
358    opts.add("BackedSCO"); // TODO Only use this when using original StorageVersion
359    opts.add("ApplicationIdentity");
360    opts.add("DatastoreIdentity");
361    opts.add("OptimisticTransaction");
362    opts.add("ORM");
363    return opts;
364  }
365
366  @Override
367  public DatastoreContainerObject newJoinDatastoreContainerObject(AbstractMemberMetaData fmd,
368      ClassLoaderResolver clr) {
369    return null;
370  }
371
372  /**
373   * This method requires synchronization so that we don't end up registering
374   * the same property more than once.
375   */
376  @Override
377  protected synchronized StoreData newStoreData(ClassMetaData cmd, ClassLoaderResolver clr) {
378    InheritanceStrategy strat = cmd.getInheritanceMetaData().getStrategy();
379
380    // The overarching rule for supported inheritance strategies is that we
381    // don't split the state of an object across multiple entities.
382    // TODO This is all nonsense. This datastore only allows "COMPLETE_TABLE" really so ignore the inheritance
383    // and remove the need for DatastoreClass
384    if (strat == InheritanceStrategy.SUBCLASS_TABLE) {
385      // Table mapped into the table(s) of subclass(es)
386      // Just add the SchemaData entry with no table - managed by subclass
387      return buildStoreDataWithNoTable(cmd);
388    } else if (strat == InheritanceStrategy.COMPLETE_TABLE) {
389      if (cmd.isAbstract()) {
390        // Abstract class with "complete-table" so gets no table
391        return buildStoreDataWithNoTable(cmd);
392      }
393      return buildStoreData(cmd, clr);
394    } else if (strat == InheritanceStrategy.NEW_TABLE &&
395               (cmd.getSuperAbstractClassMetaData() == null ||
396                cmd.getSuperAbstractClassMetaData().getInheritanceMetaData().getStrategy() == InheritanceStrategy.SUBCLASS_TABLE)) {
397      // New Table means you store your fields and your fields only (no fields from superclasses).  
398      // This only ok if you don't have a persistent superclass or your persistent superclass has 
399      // delegated responsibility for storing its fields to you.
400      return buildStoreData(cmd, clr);
401    } else if (MetaDataUtils.isNewOrSuperclassTableInheritanceStrategy(cmd)) {
402      // Table mapped into table of superclass
403      // Find the superclass - should have been created first
404      AbstractClassMetaData[] managingCmds = getClassesManagingTableForClass(cmd, clr);
405      DatastoreTable superTable;
406      if (managingCmds != null && managingCmds.length == 1) {
407        MappedStoreData superData = (MappedStoreData) storeDataMgr.get(managingCmds[0].getFullClassName());
408        if (superData != null) {
409          // Specify the table if it already exists
410          superTable = (DatastoreTable) superData.getDatastoreContainerObject();
411          return buildStoreDataWithTable(cmd, superTable);
412        }
413      }
414    }
415
416    // TODO Don't do this. GAE basically supports "complete-table" always so we should just ignore any
417    // inheritance metadata that implies otherwise.
418    boolean jpa = getApiAdapter().getName().equalsIgnoreCase("JPA");
419    String unsupportedMsg = GAE_LOCALISER.msg(jpa ? "AppEngine.BadInheritance.JPA" : "AppEngine.BadInheritance.JDO", 
420        cmd.getInheritanceMetaData().getStrategy().toString(), cmd.getFullClassName(), getApiAdapter().getName());
421    throw new UnsupportedInheritanceStrategyException(unsupportedMsg);
422  }
423
424  private StoreData buildStoreDataWithNoTable(ClassMetaData cmd) {
425    StoreData sdNew = new MappedStoreData(cmd, null, false);
426    registerStoreData(sdNew);
427    return sdNew;
428  }
429
430  private StoreData buildStoreData(ClassMetaData cmd, ClassLoaderResolver clr) {
431    String kindName = EntityUtils.getKindName(getIdentifierFactory(), cmd);
432    DatastoreTable table = new DatastoreTable(kindName, this, cmd, clr, dba);
433    StoreData sd = new MappedStoreData(cmd, table, true);
434    registerStoreData(sd);
435    // needs to be called after we register the store data to avoid stack overflow
436    table.buildMapping();
437    return sd;
438  }
439  
440  private StoreData buildStoreDataWithTable(ClassMetaData cmd, DatastoreTable table) {
441    MappedStoreData sd = new MappedStoreData(cmd, table, false);
442    registerStoreData(sd);
443    sd.setDatastoreContainerObject(table);
444    table.manageClass(cmd);
445    return sd;
446  }
447
448  @Override
449  public Object getResultValueAtPosition(Object key, JavaTypeMapping mapping, int position) {
450    // this is the key, and we're only using this for keys, so just return it.
451    return key;
452  }
453
454  @Override
455  public boolean allowsBatching() {
456    return false;
457  }
458
459  public FieldManager getFieldManagerForResultProcessing(ObjectProvider op, Object resultSet,
460      StatementClassMapping resultMappings) {
461    ExecutionContext ec = op.getExecutionContext();
462    Class<?> cls = ec.getClassLoaderResolver().classForName(op.getClassMetaData().getFullClassName());
463    Object internalKey = EntityUtils.idToInternalKey(ec, cls, resultSet, true);
464    // Need to provide this to the field manager in the form of the pk
465    // of the type: Key, Long, encoded String, or unencoded String
466    return new KeyOnlyFieldManager(internalKey);
467  }
468
469  @Override
470  public FieldManager getFieldManagerForResultProcessing(ExecutionContext ec, Object resultSet, 
471      StatementClassMapping resultMappings, AbstractClassMetaData cmd) {
472    Class<?> cls = ec.getClassLoaderResolver().classForName(cmd.getFullClassName());
473    Object internalKey = EntityUtils.idToInternalKey(ec, cls, resultSet, true);
474    // Need to provide this to the field manager in the form of the pk
475    // of the type: Key, Long, encoded String, or unencoded String
476    return new KeyOnlyFieldManager(internalKey);
477  }
478
479  protected ListStore newFKListStore(AbstractMemberMetaData ammd, ClassLoaderResolver clr) {
480    // TODO Delete this when we drop old storage version
481    return new FKListStore(ammd, this, clr);
482  }
483
484  @Override
485  protected SetStore newFKSetStore(AbstractMemberMetaData ammd, ClassLoaderResolver clr) {
486    // TODO Delete this when we drop old storage version
487    return new FKSetStore(ammd, this, clr);
488  }
489
490  public boolean usesBackedSCOWrappers() {
491    // TODO Use simple SCO wrappers at some point in future since we have no need for the backed variants now
492    /*if (storageVersionAtLeast(StorageVersion.READ_OWNED_CHILD_KEYS_FROM_PARENTS)) {
493      return false;
494    }*/
495    return true;
496  }
497
498  public boolean useBackedSCOWrapperForMember(AbstractMemberMetaData mmd, ExecutionContext ec) {
499    // Use backed SCO wrapper on relation field (to support legacy), and use simple SCO wrapper on others
500    return (mmd.getRelationType(ec.getClassLoaderResolver()) == RelationType.NONE ? false : true);
501  }
502
503  public boolean storageVersionAtLeast(StorageVersion storageVersion) {
504    return getStorageVersion().ordinal() >= storageVersion.ordinal();
505  }
506
507  public TransactionOptions getDefaultDatastoreTransactionOptions() {
508    return copyTransactionOptions(defaultDatastoreTransactionOptionsPrototype);
509  }
510
511  @Override
512  public DatastorePersistenceHandler getPersistenceHandler() {
513    return (DatastorePersistenceHandler) super.getPersistenceHandler();
514  }
515
516  @Override
517  public DatastoreTable getDatastoreClass(String className, ClassLoaderResolver clr) {
518    try {
519      // We see the occasional race condition when multiple threads concurrently
520      // perform an operation using a persistence-capable class for which DataNucleus
521      // has not yet generated the meta-data.  The result is usually
522      // AbstractMemberMetaData objects with the same column listed twice in the meta-data.
523      // Locking at this level is a bit more coarse-grained than I'd like but once the
524      // meta-data has been built this will return super fast so it shouldn't be an issue.
525      synchronized(this) {
526        return (DatastoreTable) super.getDatastoreClass(className, clr);
527      }
528    } catch (NoTableManagedException e) {
529      // Our parent class throws this when the class isn't PersistenceCapable also.
530      Class cls = clr.classForName(className);
531      ApiAdapter api = getApiAdapter();
532      if (cls != null && !cls.isInterface() && !api.isPersistable(cls)) {
533        throw new NoTableManagedException(
534            "Class " + className + " does not seem to have been enhanced. You may want to rerun " +
535            "the enhancer and check for errors in the output.");
536        // Suggest you address why this method is being called before any check on whether it is persistable
537        // then you can remove this error message
538      }
539      throw e;
540    }
541  }
542
543  /**
544   * Perform appengine-specific validation on the provided meta data.
545   * Also generates cached information that is needed by persistence.
546   * @param cmd The metadata to validate.
547   */
548  public void validateMetaDataForClass(AbstractClassMetaData cmd) {
549    // Only validate each meta data once
550    if (validatedClasses.add(cmd.getFullClassName())) {
551      if (getBooleanProperty(VALIDATE_METADATA, true)) {
552        // Only do if the persistence property is not set to false
553        metadataValidator.validate(cmd);
554      }
555
556      AbstractMemberMetaData parentPkMmd = MetaDataUtils.getParentPkMemberMetaDataForClass(cmd, getMetaDataManager(),
557          getNucleusContext().getClassLoaderResolver(cmd.getClass().getClassLoader()));
558      if (parentPkMmd != null) {
559        parentMemberMetaDataByClass.put(cmd.getFullClassName(), parentPkMmd);
560      }
561    }
562  }
563
564  /**
565   * Accessor for the metadata of the member of this class marked as "gae.parent-pk".
566   * @param cmd Metadata for the class
567   * @return The member marked as "gae.parent-pk".
568   */
569  public AbstractMemberMetaData getMetaDataForParentPK(AbstractClassMetaData cmd) {
570    return parentMemberMetaDataByClass.get(cmd.getFullClassName());
571  }
572
573  public StorageVersion getStorageVersion() {
574    return storageVersion;
575  }
576  
577  public static final class UnsupportedInheritanceStrategyException extends NucleusFatalUserException {
578    UnsupportedInheritanceStrategyException(String msg) {
579      super(msg);
580    }
581  }
582
583  /**
584   * Accessor for the current DatastoreTransaction for this ExecutionContext.
585   * Each PM/EM has its own DatastoreService, and consequently can have a current DatastoreTransaction.
586   * @param ec ExecutionContext
587   * @return The DatastoreTransaction if active, or null
588   */
589  public DatastoreTransaction getDatastoreTransaction(ExecutionContext ec) {
590    ManagedConnection mconn = ec.getStoreManager().getConnection(ec);
591    return ((EmulatedXAResource) mconn.getXAResource()).getCurrentTransaction();
592  }
593
594  /**
595   * Accessor for the current DatastoreService for this ExecutionContext.
596   * Each PM/EM has its own DatastoreService.
597   * @param ec ExecutionContext
598   * @return The DatastoreService
599   */
600  public DatastoreService getDatastoreService(ExecutionContext ec) {
601    ManagedConnection mconn = ec.getStoreManager().getConnection(ec);
602    return ((EmulatedXAResource) mconn.getXAResource()).getDatastoreService();
603  }
604
605  /**
606   * Accessor for the DatastoreService to use for reads for this ExecutionContext.
607   * We currently just use the same DatastoreService for all ExecutionContext, but parameter passed in so
608   * we can extend in the future.
609   * @param ec ExecutionContext
610   * @return The DatastoreService
611   */
612  public DatastoreService getDatastoreServiceForReads(ExecutionContext ec) {
613    return datastoreServiceForReads;
614  }
615
616  /**
617   * Helper method to determine if the connection factory associated with this
618   * manager is AutoCreateTransaction.
619   */
620  public boolean connectionFactoryIsAutoCreateTransaction() {
621    DatastoreConnectionFactoryImpl connFactory = 
622        (DatastoreConnectionFactoryImpl) connectionMgr.lookupConnectionFactory(primaryConnectionFactoryName);
623    return connFactory.isAutoCreateTransaction();
624  }
625
626  /**
627   * Returns a fresh instance so that callers can make changes without
628   * impacting global state.
629   */
630  public DatastoreServiceConfig getDefaultDatastoreServiceConfigForReads() {
631    return copyDatastoreServiceConfig(defaultDatastoreServiceConfigPrototypeForReads);
632  }
633
634  /**
635   * Returns a fresh instance so that callers can make changes without
636   * impacting global state.
637   */
638  public DatastoreServiceConfig getDefaultDatastoreServiceConfigForWrites() {
639    return copyDatastoreServiceConfig(defaultDatastoreServiceConfigPrototypeForWrites);
640  }
641
642  private DatastoreServiceConfig createDatastoreServiceConfigPrototypeForReads(
643      PersistenceConfiguration persistenceConfig) {
644    return createDatastoreServiceConfigPrototype(persistenceConfig, PropertyNames.PROPERTY_DATASTORE_READ_TIMEOUT);
645  }
646
647  private DatastoreServiceConfig createDatastoreServiceConfigPrototypeForWrites(
648      PersistenceConfiguration persistenceConfig) {
649    return createDatastoreServiceConfigPrototype(persistenceConfig, PropertyNames.PROPERTY_DATASTORE_WRITE_TIMEOUT);
650  }
651
652  private DatastoreServiceConfig createDatastoreServiceConfigPrototype(
653      PersistenceConfiguration persistenceConfiguration, String... timeoutProps) {
654    DatastoreServiceConfig datastoreServiceConfig = DatastoreServiceConfig.Builder.withDefaults();
655
656    for (String timeoutProp : timeoutProps) {
657      int defaultDeadline = persistenceConfiguration.getIntProperty(timeoutProp);
658      if (defaultDeadline > 0) {
659        datastoreServiceConfig.deadline(defaultDeadline / 1000d);
660      }
661    }
662    String defaultReadConsistencyStr = persistenceConfiguration.getStringProperty(
663        DATASTORE_READ_CONSISTENCY_PROPERTY);
664    if (defaultReadConsistencyStr != null) {
665      try {
666        datastoreServiceConfig.readPolicy(new ReadPolicy(Consistency.valueOf(defaultReadConsistencyStr)));
667      } catch (IllegalArgumentException iae) {
668        throw new NucleusFatalUserException(
669            "Illegal value for " + DATASTORE_READ_CONSISTENCY_PROPERTY +
670            ".  Valid values are " + Arrays.toString(Consistency.values()));
671      }
672    }
673    return datastoreServiceConfig;
674  }
675
676  @SuppressWarnings("deprecation")
677  private TransactionOptions createDatastoreTransactionOptionsPrototype(
678      PersistenceConfiguration persistenceConfig) {
679//    return TransactionOptions.Builder.withXG(
680    return TransactionOptions.Builder.allowMultipleEntityGroups(
681        persistenceConfig.getBooleanProperty(DATASTORE_ENABLE_XG_TXNS_PROPERTY));
682    }
683
684  public TypeConversionUtils getTypeConversionUtils() {
685    return typeConversionUtils;
686  }
687
688  // visible for testing
689  static DatastoreServiceConfig copyDatastoreServiceConfig(DatastoreServiceConfig config) {
690    DatastoreServiceConfig newConfig = DatastoreServiceConfig.Builder.
691        withImplicitTransactionManagementPolicy(config.getImplicitTransactionManagementPolicy()).
692        readPolicy(config.getReadPolicy());
693
694    if (config.getDeadline() != null) {
695      newConfig.deadline(config.getDeadline());
696    }
697    return newConfig;
698  }
699
700  // visible for testing
701  static TransactionOptions copyTransactionOptions(TransactionOptions txnOpts) {
702    // Maintenance nightmare, use clone() once it's available in the sdk
703    TransactionOptions options = TransactionOptions.Builder.withDefaults();
704    if (txnOpts.isXG()) {
705      options.setXG(txnOpts.isXG());
706    } else {
707      options.clearXG();
708    }
709    return txnOpts;
710  }
711}