Index: pkg/gcloud/lib/src/db/model_db_impl.dart |
diff --git a/pkg/gcloud/lib/src/db/model_db_impl.dart b/pkg/gcloud/lib/src/db/model_db_impl.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3b44cdeccda84877196ee854ebbd4c636d8e90c8 |
--- /dev/null |
+++ b/pkg/gcloud/lib/src/db/model_db_impl.dart |
@@ -0,0 +1,528 @@ |
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+part of gcloud.db; |
+ |
+/// An implementation of [ModelDB] based on model class annotations. |
+/// |
+/// The two constructors will scan loaded dart libraries for classes with a |
+/// [Kind] annotation. |
+/// |
+/// An example on how to write a model class is: |
+/// @Kind |
+/// class Person extends db.Model { |
+/// @StringProperty |
+/// String name; |
+/// |
+/// @IntProperty |
+/// int age; |
+/// |
+/// @DateTimeProperty |
+/// DateTime dateOfBirth; |
+/// } |
+/// |
+/// These classes must either extend [Model] or [ExpandoModel]. Furthermore |
+/// they must have an empty default constructor which can be used to construct |
+/// model objects when doing lookups/queries from datastore. |
+class ModelDBImpl implements ModelDB { |
+ final Map<_ModelDescription, Map<String, Property>> _modelDesc2Properties = {}; |
+ final Map<String, _ModelDescription> _kind2ModelDesc = {}; |
+ final Map<_ModelDescription, mirrors.ClassMirror> _modelDesc2ClassMirror = {}; |
+ final Map<_ModelDescription, Type> _type2ModelDesc = {}; |
+ final Map<Type, _ModelDescription> _modelDesc2Type = {}; |
+ |
+ /// Initializes a new [ModelDB] from all libraries. |
+ /// |
+ /// This will scan all libraries for classes with a [Kind] annotation. |
+ /// |
+ /// In case an error is encountered (e.g. two model classes with the same kind |
+ /// name) a [StateError] will be thrown. |
+ ModelDBImpl() { |
+ // WARNING: This is O(n) of the source code, which is very bad! |
+ // Would be nice to have: `currentMirrorSystem().subclassesOf(Model)` |
+ _initialize(mirrors.currentMirrorSystem().libraries.values); |
+ } |
+ |
+ /// Initializes a new [ModelDB] from all libraries. |
+ /// |
+ /// This will scan the given [librarySymnbol] for classes with a [Kind] |
+ /// annotation. |
+ /// |
+ /// In case an error is encountered (e.g. two model classes with the same kind |
+ /// name) a [StateError] will be thrown. |
+ ModelDBImpl.fromLibrary(Symbol librarySymbol) { |
+ _initialize([mirrors.currentMirrorSystem().findLibrary(librarySymbol)]); |
+ } |
+ |
+ /// Converts a [datastore.Key] to a [Key]. |
+ Key fromDatastoreKey(datastore.Key datastoreKey) { |
+ var namespace = new Partition(datastoreKey.partition.namespace); |
+ Key key = namespace.emptyKey; |
+ for (var element in datastoreKey.elements) { |
+ var type = _type2ModelDesc[_kind2ModelDesc[element.kind]]; |
+ assert (type != null); |
+ key = key.append(type, id: element.id); |
+ } |
+ return key; |
+ } |
+ |
+ /// Converts a [Key] to a [datastore.Key]. |
+ datastore.Key toDatastoreKey(Key dbKey) { |
+ List<datastore.KeyElement> elements = []; |
+ var currentKey = dbKey; |
+ while (!currentKey.isEmpty) { |
+ var id = currentKey.id; |
+ |
+ var modelDescription = _modelDescriptionForType(currentKey.type); |
+ var kind = modelDescription.kindName(this); |
+ |
+ bool useIntegerId = modelDescription.useIntegerId; |
+ |
+ if (useIntegerId && id != null && id is! int) { |
+ throw new ArgumentError('Expected an integer id property but ' |
+ 'id was of type ${id.runtimeType}'); |
+ } |
+ if (!useIntegerId && (id != null && id is! String)) { |
+ throw new ArgumentError('Expected a string id property but ' |
+ 'id was of type ${id.runtimeType}'); |
+ } |
+ |
+ elements.add(new datastore.KeyElement(kind, id)); |
+ currentKey = currentKey.parent; |
+ } |
+ Partition partition = currentKey._parent; |
+ return new datastore.Key( |
+ elements.reversed.toList(), |
+ partition: new datastore.Partition(partition.namespace)); |
+ } |
+ |
+ /// Converts a [Model] instance to a [datastore.Entity]. |
+ datastore.Entity toDatastoreEntity(Model model) { |
+ try { |
+ var modelDescription = _modelDescriptionForType(model.runtimeType); |
+ return modelDescription.encodeModel(this, model); |
+ } catch (error, stack) { |
+ throw |
+ new ArgumentError('Error while encoding entity ($error, $stack).'); |
+ } |
+ } |
+ |
+ /// Converts a [datastore.Entity] to a [Model] instance. |
+ Model fromDatastoreEntity(datastore.Entity entity) { |
+ if (entity == null) return null; |
+ |
+ Key key = fromDatastoreKey(entity.key); |
+ var kind = entity.key.elements.last.kind; |
+ var modelDescription = _kind2ModelDesc[kind]; |
+ if (modelDescription == null) { |
+ throw new StateError('Trying to deserialize entity of kind ' |
+ '$kind, but no Model class available for it.'); |
+ } |
+ |
+ try { |
+ return modelDescription.decodeEntity(this, key, entity); |
+ } catch (error, stack) { |
+ throw new StateError('Error while decoding entity ($error, $stack).'); |
+ } |
+ } |
+ |
+ /// Returns the string representation of the kind of model class [type]. |
+ /// |
+ /// If the model class `type` is not found it will throw an `ArgumentError`. |
+ String kindName(Type type) { |
+ var kind = _modelDesc2Type[type].kind; |
+ if (kind == null) { |
+ throw new ArgumentError( |
+ 'The class $type was not associated with a kind.'); |
+ } |
+ return kind; |
+ } |
+ |
+ /// Returns the name of the property corresponding to the kind [kind] and |
+ /// [fieldName]. |
+ String fieldNameToPropertyName(String kind, String fieldName) { |
+ var modelDescription = _kind2ModelDesc[kind]; |
+ if (modelDescription == null) { |
+ throw new ArgumentError('The kind $kind is unknown.'); |
+ } |
+ return modelDescription.fieldNameToPropertyName(fieldName); |
+ } |
+ |
+ |
+ Iterable<_ModelDescription> get _modelDescriptions { |
+ return _modelDesc2Type.values; |
+ } |
+ |
+ Map<String, Property> _propertiesForModel( |
+ _ModelDescription modelDescription) { |
+ return _modelDesc2Properties[modelDescription]; |
+ } |
+ |
+ _ModelDescription _modelDescriptionForType(Type type) { |
+ return _modelDesc2Type[type]; |
+ } |
+ |
+ mirrors.ClassMirror _modelClass(_ModelDescription md) { |
+ return _modelDesc2ClassMirror[md]; |
+ } |
+ |
+ |
+ void _initialize(Iterable<mirrors.LibraryMirror> libraries) { |
+ libraries.forEach((mirrors.LibraryMirror lm) { |
+ lm.declarations.values |
+ .where((d) => d is mirrors.ClassMirror && d.hasReflectedType) |
+ .forEach((mirrors.ClassMirror declaration) { |
+ _tryLoadNewModelClass(declaration); |
+ }); |
+ }); |
+ |
+ // Ask every [ModelDescription] to compute whatever global state it wants |
+ // to have. |
+ for (var modelDescription in _modelDescriptions) { |
+ modelDescription.initialize(this); |
+ } |
+ |
+ // Ask every [ModelDescription] whether we should register it with a given |
+ // kind name. |
+ for (var modelDescription in _modelDescriptions) { |
+ var kindName = modelDescription.kindName(this); |
+ if (_kind2ModelDesc.containsKey(kindName)) { |
+ throw new StateError( |
+ 'Cannot have two ModelDescriptions ' |
+ 'with the same kind ($kindName)'); |
+ } |
+ _kind2ModelDesc[kindName] = modelDescription; |
+ } |
+ } |
+ |
+ void _tryLoadNewModelClass(mirrors.ClassMirror classMirror) { |
+ Kind kindAnnotation; |
+ for (mirrors.InstanceMirror instance in classMirror.metadata) { |
+ if (instance.reflectee.runtimeType == Kind) { |
+ if (kindAnnotation != null) { |
+ throw new StateError( |
+ 'Cannot have more than one ModelMetadata() annotation ' |
+ 'on a Model class'); |
+ } |
+ kindAnnotation = instance.reflectee; |
+ } |
+ } |
+ |
+ if (kindAnnotation != null) { |
+ var name = kindAnnotation.name; |
+ var integerId = kindAnnotation.idType == IdType.Integer; |
+ var stringId = kindAnnotation.idType == IdType.String; |
+ |
+ // Fall back to the class name. |
+ if (name == null) { |
+ name = mirrors.MirrorSystem.getName(classMirror.simpleName); |
+ } |
+ |
+ // This constraint should be guaranteed by the Kind() const constructor. |
+ assert ((integerId && !stringId) || (!integerId && stringId)); |
+ |
+ _tryLoadNewModelClassFull(classMirror, name, integerId); |
+ } |
+ } |
+ |
+ void _tryLoadNewModelClassFull(mirrors.ClassMirror modelClass, |
+ String name, |
+ bool useIntegerId) { |
+ assert (!_modelDesc2Type.containsKey(modelClass.reflectedType)); |
+ |
+ var modelDesc; |
+ if (_isExpandoClass(modelClass)) { |
+ modelDesc = new _ExpandoModelDescription(name, useIntegerId); |
+ } else { |
+ modelDesc = new _ModelDescription(name, useIntegerId); |
+ } |
+ |
+ _type2ModelDesc[modelDesc] = modelClass.reflectedType; |
+ _modelDesc2Type[modelClass.reflectedType] = modelDesc; |
+ _modelDesc2ClassMirror[modelDesc] = modelClass; |
+ _modelDesc2Properties[modelDesc] = |
+ _propertiesFromModelDescription(modelClass); |
+ |
+ // Ensure we have an empty constructor. |
+ bool defaultConstructorFound = false; |
+ for (var declaration in modelClass.declarations.values) { |
+ if (declaration is mirrors.MethodMirror) { |
+ if (declaration.isConstructor && |
+ declaration.constructorName == const Symbol('') && |
+ declaration.parameters.length == 0) { |
+ defaultConstructorFound = true; |
+ break; |
+ } |
+ } |
+ } |
+ if (!defaultConstructorFound) { |
+ throw new StateError( |
+ 'Class ${modelClass.simpleName} does not have a default ' |
+ 'constructor.'); |
+ } |
+ } |
+ |
+ Map<String, Property> _propertiesFromModelDescription( |
+ mirrors.ClassMirror modelClassMirror) { |
+ var properties = new Map<String, Property>(); |
+ var propertyNames = new Set<String>(); |
+ |
+ // Loop over all classes in the inheritence path up to the Object class. |
+ while (modelClassMirror.superclass != null) { |
+ var memberMap = modelClassMirror.instanceMembers; |
+ // Loop over all declarations (which includes fields) |
+ modelClassMirror.declarations.forEach((Symbol fieldSymbol, |
+ mirrors.DeclarationMirror decl) { |
+ // Look if the symbol is a getter and we have metadata attached to it. |
+ if (memberMap.containsKey(fieldSymbol) && |
+ memberMap[fieldSymbol].isGetter && |
+ decl.metadata != null) { |
+ var propertyAnnotations = decl.metadata |
+ .map((mirrors.InstanceMirror mirror) => mirror.reflectee) |
+ .where((Object property) => property is Property) |
+ .toList(); |
+ |
+ if (propertyAnnotations.length > 1) { |
+ throw new StateError( |
+ 'Cannot have more than one Property annotation on a model ' |
+ 'field.'); |
+ } else if (propertyAnnotations.length == 1) { |
+ var property = propertyAnnotations.first; |
+ |
+ // Get a String representation of the field and the value. |
+ var fieldName = mirrors.MirrorSystem.getName(fieldSymbol); |
+ |
+ // Determine the name to use for the property in datastore. |
+ var propertyName = (property as Property).propertyName; |
+ if (propertyName == null) propertyName = fieldName; |
+ |
+ if (properties.containsKey(fieldName)) { |
+ throw new StateError( |
+ 'Cannot have two Property objects describing the same field ' |
+ 'in a model object class hierarchy.'); |
+ } |
+ |
+ if (propertyNames.contains(propertyName)) { |
+ throw new StateError( |
+ 'Cannot have two Property objects mapping to the same ' |
+ 'datastore property name "$propertyName".'); |
+ } |
+ properties[fieldName] = property; |
+ propertyNames.add(propertyName); |
+ } |
+ } |
+ }); |
+ modelClassMirror = modelClassMirror.superclass; |
+ } |
+ |
+ return properties; |
+ } |
+ |
+ bool _isExpandoClass(mirrors.ClassMirror modelClass) { |
+ while (modelClass.superclass != modelClass) { |
+ if (modelClass.reflectedType == ExpandoModel) { |
+ return true; |
+ } else if (modelClass.reflectedType == Model) { |
+ return false; |
+ } |
+ modelClass = modelClass.superclass; |
+ } |
+ throw new StateError('This should be unreachable.'); |
+ } |
+} |
+ |
+class _ModelDescription { |
+ final HashMap<String, String> _property2FieldName = |
+ new HashMap<String, String>(); |
+ final HashMap<String, String> _field2PropertyName = |
+ new HashMap<String, String>(); |
+ final Set<String> _indexedProperties = new Set<String>(); |
+ final Set<String> _unIndexedProperties = new Set<String>(); |
+ |
+ final String kind; |
+ final bool useIntegerId; |
+ |
+ _ModelDescription(this.kind, this.useIntegerId); |
+ |
+ void initialize(ModelDBImpl db) { |
+ // Compute propertyName -> fieldName mapping. |
+ db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
+ // The default of a datastore property name is the fieldName. |
+ // It can be overridden with [Property.propertyName]. |
+ String propertyName = prop.propertyName; |
+ if (propertyName == null) propertyName = fieldName; |
+ |
+ _property2FieldName[propertyName] = fieldName; |
+ _field2PropertyName[fieldName] = propertyName; |
+ }); |
+ |
+ // Compute properties & unindexed properties |
+ db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
+ String propertyName = prop.propertyName; |
+ if (propertyName == null) propertyName = fieldName; |
+ |
+ if (prop.indexed) { |
+ _indexedProperties.add(propertyName); |
+ } else { |
+ _unIndexedProperties.add(propertyName); |
+ } |
+ }); |
+ } |
+ |
+ String kindName(ModelDBImpl db) => kind; |
+ |
+ datastore.Entity encodeModel(ModelDBImpl db, Model model) { |
+ var key = db.toDatastoreKey(model.key); |
+ |
+ var properties = {}; |
+ var mirror = mirrors.reflect(model); |
+ |
+ db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
+ _encodeProperty(db, model, mirror, properties, fieldName, prop); |
+ }); |
+ |
+ return new datastore.Entity( |
+ key, properties, unIndexedProperties: _unIndexedProperties); |
+ } |
+ |
+ _encodeProperty(ModelDBImpl db, Model model, mirrors.InstanceMirror mirror, |
+ Map properties, String fieldName, Property prop) { |
+ String propertyName = prop.propertyName; |
+ if (propertyName == null) propertyName = fieldName; |
+ |
+ var value = mirror.getField( |
+ mirrors.MirrorSystem.getSymbol(fieldName)).reflectee; |
+ if (!prop.validate(db, value)) { |
+ throw new StateError('Property validation failed for ' |
+ 'property $fieldName while trying to serialize entity of kind ' |
+ '${model.runtimeType}. '); |
+ } |
+ properties[propertyName] = prop.encodeValue(db, value); |
+ } |
+ |
+ Model decodeEntity(ModelDBImpl db, Key key, datastore.Entity entity) { |
+ if (entity == null) return null; |
+ |
+ // NOTE: this assumes a default constructor for the model classes! |
+ var classMirror = db._modelClass(this); |
+ var mirror = classMirror.newInstance(const Symbol(''), []); |
+ |
+ // Set the id and the parent key |
+ mirror.reflectee.id = key.id; |
+ mirror.reflectee.parentKey = key.parent; |
+ |
+ db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
+ _decodeProperty(db, entity, mirror, fieldName, prop); |
+ }); |
+ return mirror.reflectee; |
+ } |
+ |
+ _decodeProperty(ModelDBImpl db, datastore.Entity entity, |
+ mirrors.InstanceMirror mirror, String fieldName, |
+ Property prop) { |
+ String propertyName = fieldNameToPropertyName(fieldName); |
+ |
+ var rawValue = entity.properties[propertyName]; |
+ var value = prop.decodePrimitiveValue(db, rawValue); |
+ |
+ if (!prop.validate(db, value)) { |
+ throw new StateError('Property validation failed while ' |
+ 'trying to deserialize entity of kind ' |
+ '${entity.key.elements.last.kind} (property name: $prop)'); |
+ } |
+ |
+ mirror.setField(mirrors.MirrorSystem.getSymbol(fieldName), value); |
+ } |
+ |
+ String fieldNameToPropertyName(String fieldName) { |
+ return _field2PropertyName[fieldName]; |
+ } |
+ |
+ String propertyNameToFieldName(ModelDBImpl db, String propertySearchName) { |
+ return _property2FieldName[propertySearchName]; |
+ } |
+ |
+ Object encodeField(ModelDBImpl db, String fieldName, Object value) { |
+ Property property = db._propertiesForModel(this)[fieldName]; |
+ if (property != null) return property.encodeValue(db, value); |
+ return null; |
+ } |
+} |
+ |
+// NOTE/TODO: |
+// Currently expanded properties are only |
+// * decoded if there are no clashes in [usedNames] |
+// * encoded if there are no clashes in [usedNames] |
+// We might want to throw an error if there are clashes, because otherwise |
+// - we may end up removing properties after a read-write cycle |
+// - we may end up dropping added properties in a write |
+// ([usedNames] := [realFieldNames] + [realPropertyNames]) |
+class _ExpandoModelDescription extends _ModelDescription { |
+ Set<String> realFieldNames; |
+ Set<String> realPropertyNames; |
+ Set<String> usedNames; |
+ |
+ _ExpandoModelDescription(String kind, bool useIntegerId) |
+ : super(kind, useIntegerId); |
+ |
+ void initialize(ModelDBImpl db) { |
+ super.initialize(db); |
+ |
+ realFieldNames = new Set<String>.from(_field2PropertyName.keys); |
+ realPropertyNames = new Set<String>.from(_property2FieldName.keys); |
+ usedNames = new Set()..addAll(realFieldNames)..addAll(realPropertyNames); |
+ } |
+ |
+ datastore.Entity encodeModel(ModelDBImpl db, ExpandoModel model) { |
+ var entity = super.encodeModel(db, model); |
+ var properties = entity.properties; |
+ model.additionalProperties.forEach((String key, Object value) { |
+ // NOTE: All expanded properties will be indexed. |
+ if (!usedNames.contains(key)) { |
+ properties[key] = value; |
+ } |
+ }); |
+ return entity; |
+ } |
+ |
+ Model decodeEntity(ModelDBImpl db, Key key, datastore.Entity entity) { |
+ if (entity == null) return null; |
+ |
+ ExpandoModel model = super.decodeEntity(db, key, entity); |
+ var properties = entity.properties; |
+ properties.forEach((String key, Object value) { |
+ if (!usedNames.contains(key)) { |
+ model.additionalProperties[key] = value; |
+ } |
+ }); |
+ return model; |
+ } |
+ |
+ String fieldNameToPropertyName(String fieldName) { |
+ String propertyName = super.fieldNameToPropertyName(fieldName); |
+ // If the ModelDescription doesn't know about [fieldName], it's an |
+ // expanded property, where propertyName == fieldName. |
+ if (propertyName == null) propertyName = fieldName; |
+ return propertyName; |
+ } |
+ |
+ String propertyNameToFieldName(ModelDBImpl db, String propertyName) { |
+ String fieldName = super.propertyNameToFieldName(db, propertyName); |
+ // If the ModelDescription doesn't know about [propertyName], it's an |
+ // expanded property, where propertyName == fieldName. |
+ if (fieldName == null) fieldName = propertyName; |
+ return fieldName; |
+ } |
+ |
+ Object encodeField(ModelDBImpl db, String fieldName, Object value) { |
+ Object primitiveValue = super.encodeField(db, fieldName, value); |
+ // If superclass can't encode field, we return value here (and assume |
+ // it's primitive) |
+ // NOTE: Implicit assumption: |
+ // If value != null then superclass will return != null. |
+ // TODO: Ensure [value] is primitive in this case. |
+ if (primitiveValue == null) primitiveValue = value; |
+ return primitiveValue; |
+ } |
+} |