PageRenderTime 44ms CodeModel.GetById 10ms app.highlight 29ms RepoModel.GetById 1ms app.codeStats 0ms

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

http://datanucleus-appengine.googlecode.com/
Java | 462 lines | 345 code | 50 blank | 67 comment | 147 complexity | caca2dd66cb67c220bf3e1c866bec669 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.Key;
 19
 20import org.datanucleus.ClassLoaderResolver;
 21import org.datanucleus.metadata.AbstractClassMetaData;
 22import org.datanucleus.metadata.AbstractMemberMetaData;
 23import org.datanucleus.metadata.ColumnMetaData;
 24import org.datanucleus.metadata.IdentityStrategy;
 25import org.datanucleus.metadata.IdentityType;
 26import org.datanucleus.metadata.InvalidMetaDataException;
 27import org.datanucleus.metadata.MetaDataManager;
 28import org.datanucleus.metadata.OrderMetaData;
 29import org.datanucleus.metadata.RelationType;
 30import org.datanucleus.metadata.SequenceMetaData;
 31import org.datanucleus.util.Localiser;
 32import org.datanucleus.util.NucleusLogger;
 33
 34import java.util.Map;
 35import java.util.Set;
 36
 37/**
 38 * AppEngine-specific rules validator for Meta Data.
 39 *
 40 * @author Max Ross <maxr@google.com>
 41 */
 42public class MetaDataValidator {
 43  protected static final Localiser GAE_LOCALISER = Localiser.getInstance(
 44        "com.google.appengine.datanucleus.Localisation", DatastoreManager.class.getClassLoader());
 45
 46  private static final Set<String> ONE_OR_ZERO_EXTENSIONS =
 47      Utils.newHashSet(
 48          DatastoreManager.PK_ID,
 49          DatastoreManager.ENCODED_PK,
 50          DatastoreManager.PK_NAME,
 51          DatastoreManager.PARENT_PK);
 52
 53  private static final Set<String> NOT_PRIMARY_KEY_EXTENSIONS =
 54      Utils.newHashSet(
 55          DatastoreManager.PK_ID,
 56          DatastoreManager.PK_NAME,
 57          DatastoreManager.PARENT_PK);
 58
 59  private static final Set<String> REQUIRES_ENCODED_STRING_PK_EXTENSIONS =
 60      Utils.newHashSet(
 61          DatastoreManager.PK_ID,
 62          DatastoreManager.PK_NAME);
 63
 64  /**
 65   * Defines the various actions we can take when we encounter ignorable meta-data.
 66   */
 67  enum IgnorableMetaDataBehavior {
 68    NONE, // Do nothing at all.
 69    WARN, // Log a warning.
 70    ERROR;// Throw an exception.
 71
 72    private static IgnorableMetaDataBehavior valueOf(String val, IgnorableMetaDataBehavior returnIfNull) {
 73      if (val == null) {
 74        return returnIfNull;
 75      }
 76      return valueOf(val);
 77    }
 78  }
 79
 80  /**
 81   * Config property that determines the action we take when we encounter ignorable meta-data.
 82   */
 83  private static final String IGNORABLE_META_DATA_BEHAVIOR_PROPERTY = "datanucleus.appengine.ignorableMetaDataBehavior";
 84
 85  /**
 86   * This message is appended to every ignorable meta-data warning so users
 87   * know they can configure it.
 88   */
 89  static final String ADJUST_WARNING_MSG =
 90      String.format("You can modify this warning by setting the %s property in your config.  "
 91                    + "A value of %s will silence the warning.  "
 92                    + "A value of %s will turn the warning into an exception.",
 93                    IGNORABLE_META_DATA_BEHAVIOR_PROPERTY,
 94                    IgnorableMetaDataBehavior.NONE,
 95                    IgnorableMetaDataBehavior.ERROR);
 96
 97  private static final String ALLOW_MULTIPLE_RELATIONS_OF_SAME_TYPE =
 98      "datanucleus.appengine.allowMultipleRelationsOfSameType";
 99
100  private final DatastoreManager storeMgr;
101  private final MetaDataManager metaDataManager;
102  private final ClassLoaderResolver clr;
103
104  public MetaDataValidator(DatastoreManager storeMgr, MetaDataManager metaDataManager, ClassLoaderResolver clr) {
105    this.storeMgr = storeMgr;
106    this.metaDataManager = metaDataManager;
107    this.clr = clr;
108  }
109
110  /**
111   * validate the metadata for the provided class to add GAE/J restrictions.
112   * @param acmd Metadata for the class to validate
113   */
114  public void validate(AbstractClassMetaData acmd) {
115    if (acmd.isEmbeddedOnly()) {
116      // Nothing to check
117      return;
118    }
119
120    NucleusLogger.METADATA.info("Performing appengine-specific metadata validation for " + acmd.getFullClassName());
121
122    // validate inheritance
123    // TODO Put checks on supported inheritance here
124
125    AbstractMemberMetaData pkMemberMetaData = null;
126    Class<?> pkType = null;
127    boolean noParentAllowed = false;
128
129    if (acmd.getIdentityType() == IdentityType.DATASTORE) {
130      pkType = Key.class;
131      ColumnMetaData colmd = acmd.getIdentityMetaData().getColumnMetaData();
132      if (colmd != null) {
133        if ("varchar".equalsIgnoreCase(colmd.getJdbcType()) || "char".equalsIgnoreCase(colmd.getJdbcType())) {
134          pkType = String.class;
135        } else  if ("integer".equalsIgnoreCase(colmd.getJdbcType()) || "numeric".equalsIgnoreCase(colmd.getJdbcType())) {
136          pkType = Long.class;
137        }
138      }
139      if (pkType == Long.class) {
140        noParentAllowed = true;
141      }
142    } else if (acmd.getIdentityType() == IdentityType.APPLICATION) {
143      // Validate primary-key
144      int[] pkPositions = acmd.getPKMemberPositions();
145      if (pkPositions == null) {
146        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.NoPkFields", acmd.getFullClassName());
147      }
148      if (pkPositions.length != 1) {
149        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.CompositePKNotSupported", acmd.getFullClassName());
150      }
151
152      // TODO Support composite PKs
153      int pkPos = pkPositions[0];
154      pkMemberMetaData = acmd.getMetaDataForManagedMemberAtAbsolutePosition(pkPos);
155
156      pkType = pkMemberMetaData.getType();
157      if (pkType.equals(Long.class) || pkType.equals(long.class) || 
158          pkType.equals(Integer.class) || pkType.equals(int.class)) {
159        // Allow Long, long, Integer, int numeric PK types
160        noParentAllowed = true;
161      } else if (pkType.equals(String.class)) {
162        if (!MetaDataUtils.isEncodedPKField(acmd, pkPos)) {
163          noParentAllowed = true;
164        } else {
165          // encoded string pk
166          if (hasIdentityStrategy(IdentityStrategy.SEQUENCE, pkMemberMetaData)) {
167            throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.SequenceInvalidForEncodedStringPK",
168                pkMemberMetaData.getFullFieldName());
169          }
170        }
171      } else if (pkType.equals(Key.class)) {
172        if (hasIdentityStrategy(IdentityStrategy.SEQUENCE, pkMemberMetaData)) {
173          throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.SequenceInvalidForPKType",
174              pkMemberMetaData.getFullFieldName(), Key.class.getName());
175        }
176      } else {
177        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.InvalidPKTypeForField", 
178            pkMemberMetaData.getFullFieldName(), pkType.getName());
179      }
180    }
181
182    // Validate fields
183    Set<String> foundOneOrZeroExtensions = Utils.newHashSet();
184    Map<Class<?>, String> nonRepeatableRelationTypes = Utils.newHashMap();
185
186    // the constraints that we check across all fields apply to the entire
187    // persistent class hierarchy so we're going to validate every field
188    // at every level of the hierarchy.  As an example, this lets us detect
189    // multiple one-to-many relationships at different levels of the class hierarchy
190    AbstractClassMetaData curCmd = acmd;
191    do {
192      for (AbstractMemberMetaData ammd : curCmd.getManagedMembers()) {
193        validateField(acmd, pkMemberMetaData, noParentAllowed, pkType, foundOneOrZeroExtensions, 
194            nonRepeatableRelationTypes, ammd);
195      }
196      curCmd = curCmd.getSuperAbstractClassMetaData();
197    } while (curCmd != null);
198
199    // Look for uniqueness constraints.  Not supported but not necessarily an error
200    if (acmd.getUniqueMetaData() != null && acmd.getUniqueMetaData().length > 0) {
201      handleIgnorableMapping(acmd, null, "AppEngine.MetaData.UniqueConstraintsNotSupported", 
202      "The constraint definition will be ignored.");
203    }
204
205    NucleusLogger.METADATA.info("Finished performing appengine-specific metadata validation for " + acmd.getFullClassName());
206  }
207
208  private void validateField(AbstractClassMetaData acmd, AbstractMemberMetaData pkMemberMetaData, boolean noParentAllowed,
209                             Class<?> pkClass, Set<String> foundOneOrZeroExtensions,
210                             Map<Class<?>, String> nonRepeatableRelationTypes, AbstractMemberMetaData ammd) {
211
212    // can only have one field with this extension
213    for (String extension : ONE_OR_ZERO_EXTENSIONS) {
214      if (ammd.hasExtension(extension)) {
215        if (!foundOneOrZeroExtensions.add(extension)) {
216          throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.MoreThanOneFieldWithExtension",
217              acmd.getFullClassName(), extension);
218        }
219      }
220    }
221
222    if (ammd.hasExtension(DatastoreManager.ENCODED_PK)) {
223      if (!ammd.isPrimaryKey() || !ammd.getType().equals(String.class)) {
224        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ExtensionForStringPK",
225            ammd.getFullFieldName(), DatastoreManager.ENCODED_PK);
226      }
227    }
228
229    if (ammd.hasExtension(DatastoreManager.PK_NAME)) {
230      if (!ammd.getType().equals(String.class)) {
231        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ExtensionForStringField",
232            ammd.getFullFieldName(), DatastoreManager.PK_NAME);
233      }
234    }
235
236    if (ammd.hasExtension(DatastoreManager.PK_ID)) {
237      if (!ammd.getType().equals(Long.class) && !ammd.getType().equals(long.class)) {
238        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ExtensionForLongField",
239            ammd.getFullFieldName(), DatastoreManager.PK_ID);
240      }
241    }
242
243    if (ammd.hasExtension(DatastoreManager.PARENT_PK)) {
244      if (noParentAllowed) {
245        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.PKAndParentPKInvalid",
246            ammd.getFullFieldName(), pkClass.getName());
247      }
248      if (!ammd.getType().equals(String.class) && !ammd.getType().equals(Key.class)) {
249        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ParentPKType",
250            ammd.getFullFieldName());
251      }
252    }
253
254    for (String extension : NOT_PRIMARY_KEY_EXTENSIONS) {
255      if (ammd.hasExtension(extension) && ammd.isPrimaryKey()) {
256        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.FieldWithExtensionNotPK",
257            ammd.getFullFieldName(), extension);
258      }
259    }
260
261    // pk-name and pk-id only supported in conjunction with an encoded string
262    if (pkMemberMetaData != null) {
263      for (String extension : REQUIRES_ENCODED_STRING_PK_EXTENSIONS) {
264        if (ammd.hasExtension(extension)) {
265          if (!pkMemberMetaData.hasExtension(DatastoreManager.ENCODED_PK)) {
266            // we've already verified that encoded-pk is on a a String pk field
267            // so we don't need to check the type of the pk here.
268            throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.FieldWithExtensionForEncodedString",
269                ammd.getFullFieldName(), extension);
270          }
271        }
272      }
273    }
274
275    if (ammd.hasCollection() && ammd.getCollection().isSerializedElement()) {
276      throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.CollectionWithSerializedElementInvalid", 
277          ammd.getFullFieldName());
278    }
279    else if (ammd.hasArray() && ammd.getArray().isSerializedElement()) {
280      throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ArrayWithSerializedElementInvalid", 
281          ammd.getFullFieldName());
282    }
283
284
285    checkForIllegalChildField(ammd, noParentAllowed);
286
287    if (ammd.getRelationType(clr) != RelationType.NONE) {
288      // Look for "eager" relationships.  Not supported but not necessarily an error
289      // since we can always fall back to "lazy."
290      if (ammd.isDefaultFetchGroup() && !ammd.isEmbedded()) {
291          warn(String.format(
292              "Meta-data warning for %s.%s: %s  %s  %s",
293              acmd.getFullClassName(), ammd.getName(), GAE_LOCALISER.msg("AppEngine.MetaData.JoinsNotSupported", ammd.getFullFieldName()), "The field will be fetched lazily on first access.", ADJUST_WARNING_MSG));
294//        handleIgnorableMapping(acmd, ammd, "AppEngine.MetaData.JoinsNotSupported", "The field will be fetched lazily on first access.");
295      }
296
297      if (ammd.getRelationType(clr) == RelationType.MANY_TO_MANY_BI && MetaDataUtils.isOwnedRelation(ammd, storeMgr)) {
298        // We only support many-to-many for unowned relations
299        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ManyToManyRelationNotSupported",
300            ammd.getFullFieldName());
301      }
302
303      RelationType relType = ammd.getRelationType(clr);
304      if (ammd.getEmbeddedMetaData() == null &&
305          (relType == RelationType.ONE_TO_ONE_UNI || relType == RelationType.ONE_TO_ONE_BI ||
306           relType == RelationType.ONE_TO_MANY_UNI || relType == RelationType.ONE_TO_MANY_BI) &&
307          !getBooleanConfigProperty(ALLOW_MULTIPLE_RELATIONS_OF_SAME_TYPE) &&
308          !storeMgr.storageVersionAtLeast(StorageVersion.READ_OWNED_CHILD_KEYS_FROM_PARENTS)) {
309        // Check on multiple relations of the same type for early storage versions
310        Class<?> relationClass;
311        if (ammd.getCollection() != null) {
312          relationClass = clr.classForName(ammd.getCollection().getElementType());
313        } else if (ammd.getArray() != null) {
314          relationClass = clr.classForName(ammd.getArray().getElementType());
315        } else {
316          relationClass = clr.classForName(ammd.getTypeName());
317        }
318
319        // Add the actual type of the field to the list of types that can't
320        // repeat.  If that type was already present, problem.
321        for (Class<?> existingRelationClass : nonRepeatableRelationTypes.keySet()) {
322          if (existingRelationClass.isAssignableFrom(relationClass) ||
323              relationClass.isAssignableFrom(existingRelationClass)) {
324            throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ClassWithMultipleFieldsOfType",
325                acmd.getFullClassName(), relationClass.getName(), ammd.getName(), nonRepeatableRelationTypes.get(existingRelationClass));
326          }
327        }
328        nonRepeatableRelationTypes.put(relationClass, ammd.getName());
329      }
330    }
331
332    if (ammd.getValueGeneratorName() != null) {
333      SequenceMetaData sequenceMetaData = metaDataManager.getMetaDataForSequence(clr, ammd.getValueGeneratorName());
334      if (sequenceMetaData != null && sequenceMetaData.getInitialValue() != 1) {
335        handleIgnorableMapping(acmd, ammd, "AppEngine.MetaData.SequenceInitialSizeNotSupported",
336            "The first value for this sequence will be 1.");
337      }
338    }
339  }
340
341  private void checkForIllegalChildField(AbstractMemberMetaData ammd, boolean noParentAllowed) {
342    if (!MetaDataUtils.isOwnedRelation(ammd, storeMgr)) {
343      // The check only applies to owned relations
344      return;
345    }
346
347    // Figure out if this field is the owning side of a one to one or a one to
348    // many.  If it is, look at the mapping of the child class and make sure their
349    // pk isn't Long or unencoded String.
350    RelationType relationType = ammd.getRelationType(clr);
351    if (relationType == RelationType.NONE || ammd.isEmbedded()) {
352      return;
353    }
354    AbstractClassMetaData childAcmd = null;
355    if (relationType == RelationType.ONE_TO_MANY_BI || relationType == RelationType.ONE_TO_MANY_UNI) {
356      if (ammd.getCollection() != null) {
357        childAcmd = ammd.getCollection().getElementClassMetaData(clr, metaDataManager);
358      } else if (ammd.getArray() != null) {
359        childAcmd = ammd.getArray().getElementClassMetaData(clr, metaDataManager);
360      } else {
361        // don't know how to verify
362        NucleusLogger.METADATA.warn("Unable to validate one-to-many relation " + ammd.getFullFieldName());
363      }
364      if (ammd.getOrderMetaData() != null) {
365        verifyOneToManyOrderBy(ammd, childAcmd);
366      }
367    } else if (relationType == RelationType.ONE_TO_ONE_BI || relationType == RelationType.ONE_TO_ONE_UNI) {
368      childAcmd = metaDataManager.getMetaDataForClass(ammd.getType(), clr);
369    }
370    if (childAcmd == null) {
371      return;
372    }
373
374    // Get the type of the primary key of the child
375    if (childAcmd.getIdentityType() == IdentityType.DATASTORE) {
376      Class pkType = Long.class;
377      ColumnMetaData colmd = childAcmd.getIdentityMetaData().getColumnMetaData();
378      if (colmd != null && 
379          ("varchar".equalsIgnoreCase(colmd.getJdbcType()) || "char".equalsIgnoreCase(colmd.getJdbcType()))) {
380        pkType = String.class;
381      }
382      if (noParentAllowed && pkType.equals(Long.class)) {
383        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ChildWithPKTypeInvalid",
384            childAcmd.getFullClassName()+".[ID]", pkType.getName(), ammd.getFullFieldName());
385      }
386    } else {
387      int[] pkPositions = childAcmd.getPKMemberPositions();
388      if (pkPositions == null) {
389        // don't know how to verify
390        NucleusLogger.METADATA.warn("Unable to validate relation " + ammd.getFullFieldName());
391        return;
392      }
393      int pkPos = pkPositions[0];
394      AbstractMemberMetaData pkMemberMetaData = childAcmd.getMetaDataForManagedMemberAtAbsolutePosition(pkPos);
395      Class<?> pkType = pkMemberMetaData.getType();
396      if (noParentAllowed && (pkType.equals(Long.class) || pkType.equals(long.class) ||
397          (pkType.equals(String.class) && !pkMemberMetaData.hasExtension(DatastoreManager.ENCODED_PK)))) {
398        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.ChildWithPKTypeInvalid",
399            pkMemberMetaData.getFullFieldName(), pkType.getName(), ammd.getFullFieldName());
400      }
401    }
402  }
403
404  private void verifyOneToManyOrderBy(AbstractMemberMetaData ammd, AbstractClassMetaData childAcmd) {
405    OrderMetaData omd = ammd.getOrderMetaData();
406    OrderMetaData.FieldOrder[] fieldOrders = omd.getFieldOrders();
407    if (fieldOrders == null) {
408      return;
409    }
410    for (OrderMetaData.FieldOrder fieldOrder : omd.getFieldOrders()) {
411      String propertyName = fieldOrder.getFieldName();
412      AbstractMemberMetaData orderField = childAcmd.getMetaDataForMember(propertyName);
413      if (orderField.hasExtension(DatastoreManager.PK_ID) ||
414          orderField.hasExtension(DatastoreManager.PK_NAME)) {
415        throw new InvalidMetaDataException(GAE_LOCALISER, "AppEngine.MetaData.OrderPartOfPK",
416            ammd.getFullFieldName(), propertyName);
417      }
418    }
419  }
420
421  private static boolean hasIdentityStrategy(IdentityStrategy strat, AbstractMemberMetaData ammd) {
422    return ammd.getValueStrategy() != null && ammd.getValueStrategy().equals(strat);
423  }
424
425  private boolean getBooleanConfigProperty(String configProperty) {
426    return metaDataManager.getNucleusContext().getPersistenceConfiguration().getBooleanProperty(configProperty);
427  }
428
429  private IgnorableMetaDataBehavior getIgnorableMetaDataBehavior() {
430    return IgnorableMetaDataBehavior.valueOf(
431        metaDataManager.getNucleusContext().getStoreManager()
432            .getStringProperty(IGNORABLE_META_DATA_BEHAVIOR_PROPERTY), IgnorableMetaDataBehavior.WARN);
433  }
434
435  void handleIgnorableMapping(AbstractClassMetaData acmd, AbstractMemberMetaData ammd, String localiserKey, String warningOnlyMsg) {
436    switch (getIgnorableMetaDataBehavior()) {
437      case WARN:
438        if (ammd == null) {
439          warn(String.format(
440              "Meta-data warning for %s: %s  %s  %s",
441              acmd.getFullClassName(), GAE_LOCALISER.msg(localiserKey), warningOnlyMsg, ADJUST_WARNING_MSG));          
442        } else {
443          warn(String.format(
444              "Meta-data warning for %s.%s: %s  %s  %s",
445              acmd.getFullClassName(), ammd.getName(), GAE_LOCALISER.msg(localiserKey, ammd.getFullFieldName()), warningOnlyMsg, ADJUST_WARNING_MSG));
446        }
447        break;
448      case ERROR:
449        if (ammd == null) {
450          throw new InvalidMetaDataException(GAE_LOCALISER, localiserKey, acmd.getFullClassName());
451        }
452        throw new InvalidMetaDataException(GAE_LOCALISER, localiserKey, ammd.getFullFieldName());
453      case NONE:
454        // Do nothing
455    }
456  }
457
458  // broken out for testing
459  void warn(String msg) {
460    NucleusLogger.METADATA.warn(msg);
461  }
462}