OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 part of gcloud.db; |
| 6 |
| 7 /// An implementation of [ModelDB] based on model class annotations. |
| 8 /// |
| 9 /// The two constructors will scan loaded dart libraries for classes with a |
| 10 /// [Kind] annotation. |
| 11 /// |
| 12 /// An example on how to write a model class is: |
| 13 /// @Kind |
| 14 /// class Person extends db.Model { |
| 15 /// @StringProperty |
| 16 /// String name; |
| 17 /// |
| 18 /// @IntProperty |
| 19 /// int age; |
| 20 /// |
| 21 /// @DateTimeProperty |
| 22 /// DateTime dateOfBirth; |
| 23 /// } |
| 24 /// |
| 25 /// These classes must either extend [Model] or [ExpandoModel]. Furthermore |
| 26 /// they must have an empty default constructor which can be used to construct |
| 27 /// model objects when doing lookups/queries from datastore. |
| 28 class ModelDBImpl implements ModelDB { |
| 29 final Map<_ModelDescription, Map<String, Property>> _modelDesc2Properties = {}
; |
| 30 final Map<String, _ModelDescription> _kind2ModelDesc = {}; |
| 31 final Map<_ModelDescription, mirrors.ClassMirror> _modelDesc2ClassMirror = {}; |
| 32 final Map<_ModelDescription, Type> _type2ModelDesc = {}; |
| 33 final Map<Type, _ModelDescription> _modelDesc2Type = {}; |
| 34 |
| 35 /// Initializes a new [ModelDB] from all libraries. |
| 36 /// |
| 37 /// This will scan all libraries for classes with a [Kind] annotation. |
| 38 /// |
| 39 /// In case an error is encountered (e.g. two model classes with the same kind |
| 40 /// name) a [StateError] will be thrown. |
| 41 ModelDBImpl() { |
| 42 // WARNING: This is O(n) of the source code, which is very bad! |
| 43 // Would be nice to have: `currentMirrorSystem().subclassesOf(Model)` |
| 44 _initialize(mirrors.currentMirrorSystem().libraries.values); |
| 45 } |
| 46 |
| 47 /// Initializes a new [ModelDB] from all libraries. |
| 48 /// |
| 49 /// This will scan the given [librarySymnbol] for classes with a [Kind] |
| 50 /// annotation. |
| 51 /// |
| 52 /// In case an error is encountered (e.g. two model classes with the same kind |
| 53 /// name) a [StateError] will be thrown. |
| 54 ModelDBImpl.fromLibrary(Symbol librarySymbol) { |
| 55 _initialize([mirrors.currentMirrorSystem().findLibrary(librarySymbol)]); |
| 56 } |
| 57 |
| 58 /// Converts a [datastore.Key] to a [Key]. |
| 59 Key fromDatastoreKey(datastore.Key datastoreKey) { |
| 60 var namespace = new Partition(datastoreKey.partition.namespace); |
| 61 Key key = namespace.emptyKey; |
| 62 for (var element in datastoreKey.elements) { |
| 63 var type = _type2ModelDesc[_kind2ModelDesc[element.kind]]; |
| 64 assert (type != null); |
| 65 key = key.append(type, id: element.id); |
| 66 } |
| 67 return key; |
| 68 } |
| 69 |
| 70 /// Converts a [Key] to a [datastore.Key]. |
| 71 datastore.Key toDatastoreKey(Key dbKey) { |
| 72 List<datastore.KeyElement> elements = []; |
| 73 var currentKey = dbKey; |
| 74 while (!currentKey.isEmpty) { |
| 75 var id = currentKey.id; |
| 76 |
| 77 var modelDescription = _modelDescriptionForType(currentKey.type); |
| 78 var kind = modelDescription.kindName(this); |
| 79 |
| 80 bool useIntegerId = modelDescription.useIntegerId; |
| 81 |
| 82 if (useIntegerId && id != null && id is! int) { |
| 83 throw new ArgumentError('Expected an integer id property but ' |
| 84 'id was of type ${id.runtimeType}'); |
| 85 } |
| 86 if (!useIntegerId && (id != null && id is! String)) { |
| 87 throw new ArgumentError('Expected a string id property but ' |
| 88 'id was of type ${id.runtimeType}'); |
| 89 } |
| 90 |
| 91 elements.add(new datastore.KeyElement(kind, id)); |
| 92 currentKey = currentKey.parent; |
| 93 } |
| 94 Partition partition = currentKey._parent; |
| 95 return new datastore.Key( |
| 96 elements.reversed.toList(), |
| 97 partition: new datastore.Partition(partition.namespace)); |
| 98 } |
| 99 |
| 100 /// Converts a [Model] instance to a [datastore.Entity]. |
| 101 datastore.Entity toDatastoreEntity(Model model) { |
| 102 try { |
| 103 var modelDescription = _modelDescriptionForType(model.runtimeType); |
| 104 return modelDescription.encodeModel(this, model); |
| 105 } catch (error, stack) { |
| 106 throw |
| 107 new ArgumentError('Error while encoding entity ($error, $stack).'); |
| 108 } |
| 109 } |
| 110 |
| 111 /// Converts a [datastore.Entity] to a [Model] instance. |
| 112 Model fromDatastoreEntity(datastore.Entity entity) { |
| 113 if (entity == null) return null; |
| 114 |
| 115 Key key = fromDatastoreKey(entity.key); |
| 116 var kind = entity.key.elements.last.kind; |
| 117 var modelDescription = _kind2ModelDesc[kind]; |
| 118 if (modelDescription == null) { |
| 119 throw new StateError('Trying to deserialize entity of kind ' |
| 120 '$kind, but no Model class available for it.'); |
| 121 } |
| 122 |
| 123 try { |
| 124 return modelDescription.decodeEntity(this, key, entity); |
| 125 } catch (error, stack) { |
| 126 throw new StateError('Error while decoding entity ($error, $stack).'); |
| 127 } |
| 128 } |
| 129 |
| 130 /// Returns the string representation of the kind of model class [type]. |
| 131 /// |
| 132 /// If the model class `type` is not found it will throw an `ArgumentError`. |
| 133 String kindName(Type type) { |
| 134 var kind = _modelDesc2Type[type].kind; |
| 135 if (kind == null) { |
| 136 throw new ArgumentError( |
| 137 'The class $type was not associated with a kind.'); |
| 138 } |
| 139 return kind; |
| 140 } |
| 141 |
| 142 /// Returns the name of the property corresponding to the kind [kind] and |
| 143 /// [fieldName]. |
| 144 String fieldNameToPropertyName(String kind, String fieldName) { |
| 145 var modelDescription = _kind2ModelDesc[kind]; |
| 146 if (modelDescription == null) { |
| 147 throw new ArgumentError('The kind $kind is unknown.'); |
| 148 } |
| 149 return modelDescription.fieldNameToPropertyName(fieldName); |
| 150 } |
| 151 |
| 152 |
| 153 Iterable<_ModelDescription> get _modelDescriptions { |
| 154 return _modelDesc2Type.values; |
| 155 } |
| 156 |
| 157 Map<String, Property> _propertiesForModel( |
| 158 _ModelDescription modelDescription) { |
| 159 return _modelDesc2Properties[modelDescription]; |
| 160 } |
| 161 |
| 162 _ModelDescription _modelDescriptionForType(Type type) { |
| 163 return _modelDesc2Type[type]; |
| 164 } |
| 165 |
| 166 mirrors.ClassMirror _modelClass(_ModelDescription md) { |
| 167 return _modelDesc2ClassMirror[md]; |
| 168 } |
| 169 |
| 170 |
| 171 void _initialize(Iterable<mirrors.LibraryMirror> libraries) { |
| 172 libraries.forEach((mirrors.LibraryMirror lm) { |
| 173 lm.declarations.values |
| 174 .where((d) => d is mirrors.ClassMirror && d.hasReflectedType) |
| 175 .forEach((mirrors.ClassMirror declaration) { |
| 176 _tryLoadNewModelClass(declaration); |
| 177 }); |
| 178 }); |
| 179 |
| 180 // Ask every [ModelDescription] to compute whatever global state it wants |
| 181 // to have. |
| 182 for (var modelDescription in _modelDescriptions) { |
| 183 modelDescription.initialize(this); |
| 184 } |
| 185 |
| 186 // Ask every [ModelDescription] whether we should register it with a given |
| 187 // kind name. |
| 188 for (var modelDescription in _modelDescriptions) { |
| 189 var kindName = modelDescription.kindName(this); |
| 190 if (_kind2ModelDesc.containsKey(kindName)) { |
| 191 throw new StateError( |
| 192 'Cannot have two ModelDescriptions ' |
| 193 'with the same kind ($kindName)'); |
| 194 } |
| 195 _kind2ModelDesc[kindName] = modelDescription; |
| 196 } |
| 197 } |
| 198 |
| 199 void _tryLoadNewModelClass(mirrors.ClassMirror classMirror) { |
| 200 Kind kindAnnotation; |
| 201 for (mirrors.InstanceMirror instance in classMirror.metadata) { |
| 202 if (instance.reflectee.runtimeType == Kind) { |
| 203 if (kindAnnotation != null) { |
| 204 throw new StateError( |
| 205 'Cannot have more than one ModelMetadata() annotation ' |
| 206 'on a Model class'); |
| 207 } |
| 208 kindAnnotation = instance.reflectee; |
| 209 } |
| 210 } |
| 211 |
| 212 if (kindAnnotation != null) { |
| 213 var name = kindAnnotation.name; |
| 214 var integerId = kindAnnotation.idType == IdType.Integer; |
| 215 var stringId = kindAnnotation.idType == IdType.String; |
| 216 |
| 217 // Fall back to the class name. |
| 218 if (name == null) { |
| 219 name = mirrors.MirrorSystem.getName(classMirror.simpleName); |
| 220 } |
| 221 |
| 222 // This constraint should be guaranteed by the Kind() const constructor. |
| 223 assert ((integerId && !stringId) || (!integerId && stringId)); |
| 224 |
| 225 _tryLoadNewModelClassFull(classMirror, name, integerId); |
| 226 } |
| 227 } |
| 228 |
| 229 void _tryLoadNewModelClassFull(mirrors.ClassMirror modelClass, |
| 230 String name, |
| 231 bool useIntegerId) { |
| 232 assert (!_modelDesc2Type.containsKey(modelClass.reflectedType)); |
| 233 |
| 234 var modelDesc; |
| 235 if (_isExpandoClass(modelClass)) { |
| 236 modelDesc = new _ExpandoModelDescription(name, useIntegerId); |
| 237 } else { |
| 238 modelDesc = new _ModelDescription(name, useIntegerId); |
| 239 } |
| 240 |
| 241 _type2ModelDesc[modelDesc] = modelClass.reflectedType; |
| 242 _modelDesc2Type[modelClass.reflectedType] = modelDesc; |
| 243 _modelDesc2ClassMirror[modelDesc] = modelClass; |
| 244 _modelDesc2Properties[modelDesc] = |
| 245 _propertiesFromModelDescription(modelClass); |
| 246 |
| 247 // Ensure we have an empty constructor. |
| 248 bool defaultConstructorFound = false; |
| 249 for (var declaration in modelClass.declarations.values) { |
| 250 if (declaration is mirrors.MethodMirror) { |
| 251 if (declaration.isConstructor && |
| 252 declaration.constructorName == const Symbol('') && |
| 253 declaration.parameters.length == 0) { |
| 254 defaultConstructorFound = true; |
| 255 break; |
| 256 } |
| 257 } |
| 258 } |
| 259 if (!defaultConstructorFound) { |
| 260 throw new StateError( |
| 261 'Class ${modelClass.simpleName} does not have a default ' |
| 262 'constructor.'); |
| 263 } |
| 264 } |
| 265 |
| 266 Map<String, Property> _propertiesFromModelDescription( |
| 267 mirrors.ClassMirror modelClassMirror) { |
| 268 var properties = new Map<String, Property>(); |
| 269 var propertyNames = new Set<String>(); |
| 270 |
| 271 // Loop over all classes in the inheritence path up to the Object class. |
| 272 while (modelClassMirror.superclass != null) { |
| 273 var memberMap = modelClassMirror.instanceMembers; |
| 274 // Loop over all declarations (which includes fields) |
| 275 modelClassMirror.declarations.forEach((Symbol fieldSymbol, |
| 276 mirrors.DeclarationMirror decl) { |
| 277 // Look if the symbol is a getter and we have metadata attached to it. |
| 278 if (memberMap.containsKey(fieldSymbol) && |
| 279 memberMap[fieldSymbol].isGetter && |
| 280 decl.metadata != null) { |
| 281 var propertyAnnotations = decl.metadata |
| 282 .map((mirrors.InstanceMirror mirror) => mirror.reflectee) |
| 283 .where((Object property) => property is Property) |
| 284 .toList(); |
| 285 |
| 286 if (propertyAnnotations.length > 1) { |
| 287 throw new StateError( |
| 288 'Cannot have more than one Property annotation on a model ' |
| 289 'field.'); |
| 290 } else if (propertyAnnotations.length == 1) { |
| 291 var property = propertyAnnotations.first; |
| 292 |
| 293 // Get a String representation of the field and the value. |
| 294 var fieldName = mirrors.MirrorSystem.getName(fieldSymbol); |
| 295 |
| 296 // Determine the name to use for the property in datastore. |
| 297 var propertyName = (property as Property).propertyName; |
| 298 if (propertyName == null) propertyName = fieldName; |
| 299 |
| 300 if (properties.containsKey(fieldName)) { |
| 301 throw new StateError( |
| 302 'Cannot have two Property objects describing the same field ' |
| 303 'in a model object class hierarchy.'); |
| 304 } |
| 305 |
| 306 if (propertyNames.contains(propertyName)) { |
| 307 throw new StateError( |
| 308 'Cannot have two Property objects mapping to the same ' |
| 309 'datastore property name "$propertyName".'); |
| 310 } |
| 311 properties[fieldName] = property; |
| 312 propertyNames.add(propertyName); |
| 313 } |
| 314 } |
| 315 }); |
| 316 modelClassMirror = modelClassMirror.superclass; |
| 317 } |
| 318 |
| 319 return properties; |
| 320 } |
| 321 |
| 322 bool _isExpandoClass(mirrors.ClassMirror modelClass) { |
| 323 while (modelClass.superclass != modelClass) { |
| 324 if (modelClass.reflectedType == ExpandoModel) { |
| 325 return true; |
| 326 } else if (modelClass.reflectedType == Model) { |
| 327 return false; |
| 328 } |
| 329 modelClass = modelClass.superclass; |
| 330 } |
| 331 throw new StateError('This should be unreachable.'); |
| 332 } |
| 333 } |
| 334 |
| 335 class _ModelDescription { |
| 336 final HashMap<String, String> _property2FieldName = |
| 337 new HashMap<String, String>(); |
| 338 final HashMap<String, String> _field2PropertyName = |
| 339 new HashMap<String, String>(); |
| 340 final Set<String> _indexedProperties = new Set<String>(); |
| 341 final Set<String> _unIndexedProperties = new Set<String>(); |
| 342 |
| 343 final String kind; |
| 344 final bool useIntegerId; |
| 345 |
| 346 _ModelDescription(this.kind, this.useIntegerId); |
| 347 |
| 348 void initialize(ModelDBImpl db) { |
| 349 // Compute propertyName -> fieldName mapping. |
| 350 db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
| 351 // The default of a datastore property name is the fieldName. |
| 352 // It can be overridden with [Property.propertyName]. |
| 353 String propertyName = prop.propertyName; |
| 354 if (propertyName == null) propertyName = fieldName; |
| 355 |
| 356 _property2FieldName[propertyName] = fieldName; |
| 357 _field2PropertyName[fieldName] = propertyName; |
| 358 }); |
| 359 |
| 360 // Compute properties & unindexed properties |
| 361 db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
| 362 String propertyName = prop.propertyName; |
| 363 if (propertyName == null) propertyName = fieldName; |
| 364 |
| 365 if (prop.indexed) { |
| 366 _indexedProperties.add(propertyName); |
| 367 } else { |
| 368 _unIndexedProperties.add(propertyName); |
| 369 } |
| 370 }); |
| 371 } |
| 372 |
| 373 String kindName(ModelDBImpl db) => kind; |
| 374 |
| 375 datastore.Entity encodeModel(ModelDBImpl db, Model model) { |
| 376 var key = db.toDatastoreKey(model.key); |
| 377 |
| 378 var properties = {}; |
| 379 var mirror = mirrors.reflect(model); |
| 380 |
| 381 db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
| 382 _encodeProperty(db, model, mirror, properties, fieldName, prop); |
| 383 }); |
| 384 |
| 385 return new datastore.Entity( |
| 386 key, properties, unIndexedProperties: _unIndexedProperties); |
| 387 } |
| 388 |
| 389 _encodeProperty(ModelDBImpl db, Model model, mirrors.InstanceMirror mirror, |
| 390 Map properties, String fieldName, Property prop) { |
| 391 String propertyName = prop.propertyName; |
| 392 if (propertyName == null) propertyName = fieldName; |
| 393 |
| 394 var value = mirror.getField( |
| 395 mirrors.MirrorSystem.getSymbol(fieldName)).reflectee; |
| 396 if (!prop.validate(db, value)) { |
| 397 throw new StateError('Property validation failed for ' |
| 398 'property $fieldName while trying to serialize entity of kind ' |
| 399 '${model.runtimeType}. '); |
| 400 } |
| 401 properties[propertyName] = prop.encodeValue(db, value); |
| 402 } |
| 403 |
| 404 Model decodeEntity(ModelDBImpl db, Key key, datastore.Entity entity) { |
| 405 if (entity == null) return null; |
| 406 |
| 407 // NOTE: this assumes a default constructor for the model classes! |
| 408 var classMirror = db._modelClass(this); |
| 409 var mirror = classMirror.newInstance(const Symbol(''), []); |
| 410 |
| 411 // Set the id and the parent key |
| 412 mirror.reflectee.id = key.id; |
| 413 mirror.reflectee.parentKey = key.parent; |
| 414 |
| 415 db._propertiesForModel(this).forEach((String fieldName, Property prop) { |
| 416 _decodeProperty(db, entity, mirror, fieldName, prop); |
| 417 }); |
| 418 return mirror.reflectee; |
| 419 } |
| 420 |
| 421 _decodeProperty(ModelDBImpl db, datastore.Entity entity, |
| 422 mirrors.InstanceMirror mirror, String fieldName, |
| 423 Property prop) { |
| 424 String propertyName = fieldNameToPropertyName(fieldName); |
| 425 |
| 426 var rawValue = entity.properties[propertyName]; |
| 427 var value = prop.decodePrimitiveValue(db, rawValue); |
| 428 |
| 429 if (!prop.validate(db, value)) { |
| 430 throw new StateError('Property validation failed while ' |
| 431 'trying to deserialize entity of kind ' |
| 432 '${entity.key.elements.last.kind} (property name: $prop)'); |
| 433 } |
| 434 |
| 435 mirror.setField(mirrors.MirrorSystem.getSymbol(fieldName), value); |
| 436 } |
| 437 |
| 438 String fieldNameToPropertyName(String fieldName) { |
| 439 return _field2PropertyName[fieldName]; |
| 440 } |
| 441 |
| 442 String propertyNameToFieldName(ModelDBImpl db, String propertySearchName) { |
| 443 return _property2FieldName[propertySearchName]; |
| 444 } |
| 445 |
| 446 Object encodeField(ModelDBImpl db, String fieldName, Object value) { |
| 447 Property property = db._propertiesForModel(this)[fieldName]; |
| 448 if (property != null) return property.encodeValue(db, value); |
| 449 return null; |
| 450 } |
| 451 } |
| 452 |
| 453 // NOTE/TODO: |
| 454 // Currently expanded properties are only |
| 455 // * decoded if there are no clashes in [usedNames] |
| 456 // * encoded if there are no clashes in [usedNames] |
| 457 // We might want to throw an error if there are clashes, because otherwise |
| 458 // - we may end up removing properties after a read-write cycle |
| 459 // - we may end up dropping added properties in a write |
| 460 // ([usedNames] := [realFieldNames] + [realPropertyNames]) |
| 461 class _ExpandoModelDescription extends _ModelDescription { |
| 462 Set<String> realFieldNames; |
| 463 Set<String> realPropertyNames; |
| 464 Set<String> usedNames; |
| 465 |
| 466 _ExpandoModelDescription(String kind, bool useIntegerId) |
| 467 : super(kind, useIntegerId); |
| 468 |
| 469 void initialize(ModelDBImpl db) { |
| 470 super.initialize(db); |
| 471 |
| 472 realFieldNames = new Set<String>.from(_field2PropertyName.keys); |
| 473 realPropertyNames = new Set<String>.from(_property2FieldName.keys); |
| 474 usedNames = new Set()..addAll(realFieldNames)..addAll(realPropertyNames); |
| 475 } |
| 476 |
| 477 datastore.Entity encodeModel(ModelDBImpl db, ExpandoModel model) { |
| 478 var entity = super.encodeModel(db, model); |
| 479 var properties = entity.properties; |
| 480 model.additionalProperties.forEach((String key, Object value) { |
| 481 // NOTE: All expanded properties will be indexed. |
| 482 if (!usedNames.contains(key)) { |
| 483 properties[key] = value; |
| 484 } |
| 485 }); |
| 486 return entity; |
| 487 } |
| 488 |
| 489 Model decodeEntity(ModelDBImpl db, Key key, datastore.Entity entity) { |
| 490 if (entity == null) return null; |
| 491 |
| 492 ExpandoModel model = super.decodeEntity(db, key, entity); |
| 493 var properties = entity.properties; |
| 494 properties.forEach((String key, Object value) { |
| 495 if (!usedNames.contains(key)) { |
| 496 model.additionalProperties[key] = value; |
| 497 } |
| 498 }); |
| 499 return model; |
| 500 } |
| 501 |
| 502 String fieldNameToPropertyName(String fieldName) { |
| 503 String propertyName = super.fieldNameToPropertyName(fieldName); |
| 504 // If the ModelDescription doesn't know about [fieldName], it's an |
| 505 // expanded property, where propertyName == fieldName. |
| 506 if (propertyName == null) propertyName = fieldName; |
| 507 return propertyName; |
| 508 } |
| 509 |
| 510 String propertyNameToFieldName(ModelDBImpl db, String propertyName) { |
| 511 String fieldName = super.propertyNameToFieldName(db, propertyName); |
| 512 // If the ModelDescription doesn't know about [propertyName], it's an |
| 513 // expanded property, where propertyName == fieldName. |
| 514 if (fieldName == null) fieldName = propertyName; |
| 515 return fieldName; |
| 516 } |
| 517 |
| 518 Object encodeField(ModelDBImpl db, String fieldName, Object value) { |
| 519 Object primitiveValue = super.encodeField(db, fieldName, value); |
| 520 // If superclass can't encode field, we return value here (and assume |
| 521 // it's primitive) |
| 522 // NOTE: Implicit assumption: |
| 523 // If value != null then superclass will return != null. |
| 524 // TODO: Ensure [value] is primitive in this case. |
| 525 if (primitiveValue == null) primitiveValue = value; |
| 526 return primitiveValue; |
| 527 } |
| 528 } |
OLD | NEW |