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