entity properties

From: fago <nuppla@zites.net>


---

 includes/common.inc                        |   50 +++++
 includes/entity.inc                        |    4 
 includes/module.inc                        |   12 +
 includes/properties.inc                    |  304 ++++++++++++++++++++++++++++
 includes/token.inc                         |  165 ++++++---------
 modules/book/book.module                   |   44 ++++
 modules/comment/comment.entity.inc         |  147 +++++++++++++
 modules/comment/comment.info               |    2 
 modules/comment/comment.module             |   90 +++++---
 modules/comment/comment.tokens.inc         |  248 -----------------------
 modules/field/field.default.inc            |    2 
 modules/field/field.info.inc               |  126 +++++++++++
 modules/field/field.module                 |   17 +-
 modules/field/modules/number/number.module |    3 
 modules/field/modules/text/text.module     |    4 
 modules/node/node.entity.inc               |  120 +++++++++++
 modules/node/node.info                     |    2 
 modules/node/node.module                   |   62 ++----
 modules/node/node.tokens.inc               |  204 -------------------
 modules/simpletest/tests/field_test.module |    4 
 modules/system/system.entity.inc           |  113 ++++++++++
 modules/system/system.info                 |    2 
 modules/system/system.module               |  117 +++++++++--
 modules/system/system.test                 |  171 +++++++++++++++-
 modules/system/system.tokens.inc           |  308 ----------------------------
 modules/taxonomy/taxonomy.test             |   18 +-
 modules/upload/upload.info                 |    3 
 modules/upload/upload.module               |   44 ++++
 modules/upload/upload.tokens.inc           |   45 ----
 modules/user/user.entity.inc               |   74 +++++++
 modules/user/user.info                     |    2 
 modules/user/user.module                   |   37 +--
 modules/user/user.tokens.inc               |  129 ------------
 33 files changed, 1495 insertions(+), 1178 deletions(-)
 create mode 100644 includes/properties.inc
 create mode 100644 modules/comment/comment.entity.inc
 delete mode 100644 modules/comment/comment.tokens.inc
 create mode 100644 modules/node/node.entity.inc
 delete mode 100644 modules/node/node.tokens.inc
 create mode 100644 modules/system/system.entity.inc
 delete mode 100644 modules/system/system.tokens.inc
 delete mode 100644 modules/upload/upload.tokens.inc
 create mode 100644 modules/user/user.entity.inc
 delete mode 100644 modules/user/user.tokens.inc


diff --git includes/common.inc includes/common.inc
index 796c34e..01ca9d1 100644
--- includes/common.inc
+++ includes/common.inc
@@ -5354,6 +5354,34 @@ function drupal_check_incompatibility($v, $current_version) {
 }
 
 /**
+ * Returns some default metadata for properties by type.
+ *
+ * @param $type
+ *   The type, e.g. date, for which the info shall be returned, or NULL
+ *   to return an array with info about all types.
+ *
+ * @see hook_property_info()
+ * @see hook_property_info_alter()
+ */
+function drupal_get_property_info($type = NULL) {
+  $data = &drupal_static(__FUNCTION__);
+  if (!isset($data)) {
+    if ($cache = cache_get('property_info')) {
+      $data = $cache->data;
+    }
+    else {
+      $data = module_invoke_all('property_info');
+      drupal_alter('property_info', $data);
+      cache_set('property_info', $data);
+    }
+  }
+  if (!empty($type)) {
+    return isset($data[$type]) ? $data[$type] : array();
+  }
+  return $data;
+}
+
+/**
  * Get the entity info array of an entity type.
  *
  * @see hook_entity_info()
@@ -5372,6 +5400,7 @@ function entity_get_info($entity_type = NULL) {
       $entity_info = $cache->data;
     }
     else {
+      module_load_all_includes('inc', 'entity');
       $entity_info = module_invoke_all('entity_info');
       // Merge in default values.
       foreach ($entity_info as $name => $data) {
@@ -5470,3 +5499,24 @@ function xmlrpc($url) {
   return call_user_func_array('_xmlrpc', $args);
 }
 
+/**
+ * Returns a DrupalPropertyEntityWrapper or DrupalPropertyFormatWrapper
+ * dependent whether the passed data is an entity.
+ *
+ * @param $type
+ *   The type of the passed data.
+ * @param $data
+ *   The data to wrap.
+ * @param $options
+ *   (optional) A keyed array of options. The supported options vary by class.
+ * @param $context
+ *   (optional) An array of contextual information which format callbacks can
+ *   make use of.
+ * @return
+ *   An instance of DrupalPropertyWrapperInterface.
+ */
+function drupal_get_property_wrapper($type, $data, array $options = array(), $context = array()) {
+  $entity_info = entity_get_info();
+  $class = isset($entity_info[$type]) ? 'DrupalPropertyEntityWrapper' : 'DrupalPropertyFormatWrapper';
+  return new $class($type, $data, $options, $context);
+}
diff --git includes/entity.inc includes/entity.inc
index 66b1eb4..323e302 100644
--- includes/entity.inc
+++ includes/entity.inc
@@ -240,6 +240,10 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
         field_attach_load($this->entityType, $queried_entities);
       }
     }
+    // Initialize the entity tags array.
+    foreach ($queried_entities as $entity) {
+      $entity->entityTags = array();
+    }
 
     // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
     // always the queried entities, followed by additional arguments set in
diff --git includes/module.inc includes/module.inc
index bde7f5d..40031ed 100644
--- includes/module.inc
+++ includes/module.inc
@@ -176,11 +176,17 @@ function module_load_include($type, $module, $name = NULL) {
 /**
  * Load an include file for each of the modules that have been enabled in
  * the system table.
+ *
+ * @param $type
+ *   The include file's type (file extension).
+ * @param $name
+ *   Optionally, specify the base file name to include "$module.$name.$type".
+ *   If not set, "$module.$type" is used.
  */
 function module_load_all_includes($type, $name = NULL) {
-  $modules = module_list();
-  foreach ($modules as $module) {
-    module_load_include($type, $module, $name);
+  $name = isset($name) ? '.' . $name : '';
+  foreach (module_list() as $module) {
+    module_load_include($type, $module, $module . $name);
   }
 }
 
diff --git includes/properties.inc includes/properties.inc
new file mode 100644
index 0000000..2efb3c6
--- /dev/null
+++ includes/properties.inc
@@ -0,0 +1,304 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides the drupal property wrapper classes.
+ */
+
+/**
+ * A common interface for all property wrappers. Each wrapper can be used
+ * to derive some values depending on the implementation. If possible another
+ * instance of DrupalPropertyWrapperInterface should be returned to allow
+ * chained usage like
+ * @code
+ *   echo $wrapper->node->author->name;
+ * @endcode
+ */
+interface DrupalPropertyWrapperInterface extends IteratorAggregate {
+  
+  /**
+   * Constructor.
+   *
+   * @see drupal_get_property_wrapper()
+   */
+  public function __construct($type, $data, array $options = array());
+  
+  /**
+   * Get the data wrapped by this object.
+   */
+  public function get();
+  
+  /**
+   * Magic method: Get a derived value.
+   */
+  public function __get($name);
+  
+  /**
+   * Magic method: Check whether a derived value of this name is supported.
+   */
+  public function __isset($name);
+
+}
+
+/**
+ * Provides a wrapper for entities which eases dealing with entity properties.
+ */
+class DrupalPropertyEntityWrapper implements DrupalPropertyWrapperInterface {
+  
+  protected $entityType;
+  protected $entity;
+  protected $info;
+  protected $options;
+  protected $cache = array();
+  
+  /**
+   * Construct a new DrupalPropertyEntityWrapper object.
+   *
+   * @param $entityType
+   *   The type of the passed entity.
+   * @param $entity
+   *   The entity with which properties we deal.
+   * @param $options
+   *   (optional) A keyed array of options. Supported are:
+   *   - language: A language object to be used when getting locale-sensitive
+   *     properties.
+   *   - sanitize: A boolean flag indicating that textual properties should be
+   *     sanitized for display to a web browser. Defaults to FALSE.
+   */
+  public function __construct($entityType, $entity, array $options = array()) {
+    $this->entityType = $entityType;
+    $this->entity = $entity;
+    $this->info = entity_get_info($entityType) + array('properties' => array(), 'default tags' => array());
+    $this->options = $options + array('sanitize' => FALSE, 'language' => NULL);
+    
+    // Add in properties from the bundle or tags.
+    if (!empty($this->info['fieldable'])) {
+      list($id, $vid, $bundle) = field_extract_ids($entityType, $entity);
+      $bundle_info = $this->info['bundles'][$bundle] + array('properties' => array(), 'default tags' => array());
+      $this->info['properties'] += $bundle_info['properties'];
+      $this->info['default tags'] = array_merge($this->info['default tags'], $bundle_info['default tags']);
+    }
+    foreach ($this->getTags() as $tag) {
+      if (isset($this->info['tags'][$tag]['properties'])) {
+        $this->info['properties'] += $this->info['tags'][$tag]['properties'];
+      }
+    }
+  }
+
+  /**
+   * Returns all tags assigned to the wrapped entity.
+   *
+   * @return
+   *   An array of tags.
+   */
+  public function getTags() {
+    if (is_object($this->entity) && isset($this->entity->entityTags)) {
+      return $this->entity->entityTags;
+    }
+    elseif (is_array($this->entity) && isset($this->entity['entity tags'])) {
+      return $this->entity['entity tags'];
+    }
+    // In case the entity doesn't have tags set, fallback to the defaults.
+    return $this->info['default tags'];
+  }
+
+  /**
+   * Gets the info about the given property.
+   *
+   * @param $name
+   *   The name of the property.
+   * @throws DrupalPropertyWrapperException
+   *   If there is no such property.
+   * @return
+   *   An array of info about the property.
+   */
+  public function getPropertyInfo($name) {
+    if (!isset($this->info['properties'][$name])) {
+      throw new DrupalPropertyWrapperException('Unknown entity property ' . check_plain($name) . '.');
+    }
+    $info = $this->info['properties'][$name] + array(
+      'type' => 'text',
+      'bundle' => NULL,
+      'tags' => array(),
+      'formats' => array(),
+      'default format' => NULL,
+    );
+    $info += array('sanitize' => (empty($info['getter callback']) && $info['type'] == 'text' ? 'check_plain' : NULL));
+    if ($type_info = drupal_get_property_info($info['type'])) {
+      $info += $type_info;
+    }
+    return $info;
+  }
+  
+  /**
+   * Gets the info about the wrapped entity.
+   */
+  public function getInfo() {
+    return $this->info;
+  }
+
+  /**
+   * Magic method: Get a property.
+   *
+   * @return
+   *   An instance of DrupalPropertyWrapperInterface.
+   */
+  public function __get($name) {
+    // Look it up in the cache if possible.
+    if (!array_key_exists($name, $this->cache)) {
+      $info = $this->getPropertyInfo($name);
+
+      if (!empty($info['getter callback']) && function_exists($info['getter callback'])) {
+        $this->cache[$name] = $info['getter callback']($this->entity, $this->options, $name, $this->entityType);
+      }
+      elseif (is_array($this->entity) && isset($this->entity[$name])) {
+        $this->cache[$name] = $this->entity[$name];
+      }
+      elseif (is_object($this->entity) && isset($this->entity->$name)) {
+        $this->cache[$name] = $this->entity->$name;
+      }
+      else {
+        throw new DrupalPropertyEntityWrapper('Entity property ' . check_plain($name) . " isn't set.");
+      }
+      // Sanitize values.
+      if (isset($this->cache[$name]) && !empty($this->options['sanitize']) && !empty($info['sanitize']) && function_exists($info['sanitize'])) {
+        $this->cache[$name] = $info['sanitize']($this->cache[$name]);
+      }
+      // Return another wrapper to support chained usage.
+      $options = array_intersect_key($info, drupal_map_assoc(array('default format', 'formats'))) + $this->options;
+      $context = array('entity' => $this->entity, 'entity type' => $this->entityType, 'property' => $name);
+      $this->cache[$name] = drupal_get_property_wrapper($info['type'], $this->cache[$name], $options, $context);
+    }
+    return $this->cache[$name];
+  }
+  
+  /**
+   * Magic method: Set a property.
+   */
+  public function __set($name, $value) {
+    $info = $this->getPropertyInfo($name);
+    if (!empty($info['setter callback']) && function_exists($info['setter callback'])) {
+      unset($this->cache[$name]);
+      return $info['setter callback']($this->entity, $name, $value, $this->entityType);
+    }
+    throw new DrupalPropertyWrapperException('Entity property ' . check_plain($name) . " doesn't support writing.");
+  }
+  
+  /**
+   * Magic method: isset() can be used to check if a property is known.
+   */
+  public function __isset($name) {
+    return isset($this->info['properties'][$name]);
+  }
+  
+  /**
+   * Get the entity wrapped by this object.
+   */
+  public function get() {
+    return $this->entity;
+  }
+  
+  public function getIterator() {
+    return new ArrayIterator(array_keys($this->info['properties']));
+  }
+ 
+  public function __toString() {
+    return $this->entityType;
+  }
+}
+
+
+/**
+ * Class that eases applying formats for returned properties.
+ */
+class DrupalPropertyFormatWrapper implements DrupalPropertyWrapperInterface {
+  
+  protected $type;
+  protected $data;
+  protected $options;
+  protected $context;
+  
+  /**
+   * Construct a new DrupalPropertyFormatWrapper object.
+   *
+   * @param $type
+   *   The type of the passed data.
+   * @param $data
+   *   The data to format.
+   * @param $options
+   *   (optional) A keyed array of options. Supported are:
+   *   - language: A language object to be used when generating
+   *     locale-sensitive formats.
+   *   - formats: An array of further formats for this property
+   *     as defined in hook_entity_info().
+   *   - default format: Customize the default format.
+   * @param $context
+   *   (optional) An array of contextual information which format callbacks
+   *   can make use of. Contains 'entity', 'entity_type' and 'property' when
+   *   constructed by the DrupalPropertyEntityWrapper.
+   */
+  public function __construct($type, $data, array $options = array(), $context = array()) {
+    $this->type = $type;
+    $this->data = $data;
+    $this->options = $options + array('formats' => array());
+    $type_info = drupal_get_property_info($type) + array('formats' => array(), 'default format' => NULL);
+    $this->options += $type_info;
+    $this->options['formats'] += $type_info['formats'];
+    $this->context = $context;
+  }
+  
+  /**
+   * Magic method: Format the data.
+   */
+  public function __get($name) {
+    if (!isset($this->options['formats'][$name])) {
+      throw new DrupalPropertyWrapperException('Unknown format ' . check_plain($name) . '.');
+    }
+    return token_format($this->data, $this->type, $name, $this->options, $this->context);
+  }
+
+  /**
+   * Magic method: isset() can be used to check if a format is known.
+   */
+  public function __isset($name) {
+    return isset($this->options['formats'][$name]);
+  }
+  
+  /**
+   * Get the data wrapped by this object.
+   */
+  public function get() {
+    return $this->data;
+  }
+  
+  public function getIterator() {
+    return new ArrayIterator(array_keys($this->options['formats']));
+  }
+  
+  /**
+   * For converting to a string use the default format, if any.
+   */
+  public function __toString() {
+    return (string)token_format($this->data, $this->type, NULL, $this->options, $this->context);
+  }
+}
+
+/**
+ * Provide a separate Exception so it can be caught separately.
+ */
+class DrupalPropertyWrapperException extends Exception {
+  
+}
+
+/**
+ * Sets the property to the given value. May be used as 'setter callback'.
+ */
+function drupal_property_verbatim_set(&$entity, $name, $value) {
+  if (is_array($entity)) {
+    $entity[$name] = $value;
+  }
+  elseif (is_object($entity)) {
+    $entity->$name = $value;
+  }
+}
diff --git includes/token.inc includes/token.inc
index 6f832b7..331a39d 100644
--- includes/token.inc
+++ includes/token.inc
@@ -42,8 +42,8 @@
  * and 'mail' is a placeholder available for any 'user'.
  *
  * @see token_replace()
- * @see hook_tokens()
- * @see hook_token_info()
+ * @see hook_token_format_info()
+ * @see hook_entity_info()
  */
 
 /**
@@ -54,7 +54,7 @@
  * @param $data
  *   (optional) An array of keyed objects. For simple replacement scenarios
  *   'node', 'user', and others are common keys, with an accompanying node or
- *   user object being the value. Some token types, like 'site', do not require
+ *   user object being the value. Some token types, like 'system', do not require
  *   any explicit information from $data and can be replaced even if it is empty.
  * @param $options
  *   (optional) A keyed array of settings and flags to control the token
@@ -71,14 +71,16 @@
  *     display to a web browser. Defaults to TRUE. Developers who set this option
  *     to FALSE assume responsibility for running filter_xss(), check_plain() or
  *     other appropriate scrubbing functions before displaying data to users.
+ * @param $types
+ *   (optional) An array assigning types to the objects of $data, keyed liked
+ *   $data. If there is no assigned type for a data object, the key specified in
+ *   $data is used as type.
  * @return
  *   Text with tokens replaced.
  */
-function token_replace($text, array $data = array(), array $options = array()) {
-  $replacements = array();
-  foreach (token_scan($text) as $type => $tokens) {
-    $replacements += token_generate($type, $tokens, $data, $options);
-  }
+function token_replace($text, array $data = array(), array $options = array(), array $types = array()) {
+  $token_list = token_scan($text);
+  $replacements = token_generate($token_list, $data, $options, $types);
 
   // Optionally alter the list of replacement values.
   if (!empty($options['callback']) && function_exists($options['callback'])) {
@@ -122,15 +124,12 @@ function token_scan($text) {
 /**
  * Generate replacement values for a list of tokens.
  *
- * @param $type
- *   The type of token being replaced. 'node', 'user', and 'date' are common.
- * @param $tokens
- *   An array of tokens to be replaced, keyed by the literal text of the token
- *   as it appeared in the source text.
+ * @param $raw_tokens
+ *   A keyed array of tokens, and their original raw form in the source text.
  * @param $data
  *   (optional) An array of keyed objects. For simple replacement scenarios
  *   'node', 'user', and others are common keys, with an accompanying node or
- *   user object being the value. Some token types, like 'site', do not require
+ *   user object being the value. Some token types, like 'system', do not require
  *   any explicit information from $data and can be replaced even if it is empty.
  * @param $options
  *   (optional) A keyed array of settings and flags to control the token
@@ -145,107 +144,83 @@ function token_scan($text) {
  *     display to a web browser. Developers who set this option to FALSE assume
  *     responsibility for running filter_xss(), check_plain() or other
  *     appropriate scrubbing functions before displaying data to users.
+ * @param $types
+ *   (optional) An array assigning types to the objects of $data, keyed liked
+ *   $data. If there is no assigned type for a data object, the key specified in
+ *   $data is used as type.
  * @return
  *   An associative array of replacement values, keyed by the original 'raw'
  *   tokens that were found in the source text. For example:
  *   $results['[node:title]'] = 'My new node';
  */
-function token_generate($type, array $tokens, array $data = array(), array $options = array()) {
+function token_generate(array $raw_tokens, array $data = array(), array $options = array(), array $types = array()) {
   $results = array();
   $options += array('sanitize' => TRUE);
-  _token_initialize();
-
-  $result = module_invoke_all('tokens', $type, $tokens, $data, $options);
-  foreach ($result as $original => $replacement) {
-    $results[$original] = $replacement;
-  }
+  // Add in the current date and the global user by default.
+  $data += array('date' => REQUEST_TIME, 'current-user' => $GLOBALS['user']);
+  $types += array('current-user' => 'user');
 
-  return $results;
-}
+  foreach ($raw_tokens as $key => $tokens) {
+    $types += array($key => $key);
 
-/**
- * Given a list of tokens, return those that begin with a specific prefix.
- *
- * Used to extract a group of 'chained' tokens (such as [node:author:name]) from
- * the full list of tokens found in text. For example:
- * @code
- *   $data = array(
- *     'author:name' => '[node:author:name]',
- *     'title'       => '[node:title]',
- *     'created'     => '[node:author:name]',
- *   );
- *   $results = token_find_with_prefix($data, 'author');
- *   $results == array('name' => '[node:author:name]');
- * @endcode
- *
- * @param $tokens
- *   A keyed array of tokens, and their original raw form in the source text.
- * @param $prefix
- *   A textual string to be matched at the beginning of the token.
- * @param $delimiter
- *   An optional string containing the character that separates the prefix from
- *   the rest of the token. Defaults to ':'.
- * @return
- *   An associative array of discovered tokens, with the prefix and delimiter
- *   stripped from the key.
- */
-function token_find_with_prefix(array $tokens, $prefix, $delimiter = ':') {
-  $results = array();
-  foreach ($tokens as $token => $raw) {
-    $parts = split($delimiter, $token, 2);
-    if (count($parts) == 2 && $parts[0] == $prefix) {
-      $results[$parts[1]] = $raw;
+    if (isset($data[$key]) && ($wrapper = drupal_get_property_wrapper($types[$key], $data[$key], $options))) {
+      foreach ($tokens as $token => $original) {
+        try {
+          $results[$original] = _token_get_replacement($wrapper, $token);
+        }
+        catch (DrupalPropertyWrapperException $e) {
+          // A token has not been found, so ignore it.
+        }
+      }
     }
   }
   return $results;
 }
 
 /**
- * Returns metadata describing supported tokens.
- *
- * The metadata array contains token type, name, and description data as well as
- * an optional pointer indicating that the token chains to another set of tokens.
- * For example:
- * @code
- *   $data['types']['node'] = array(
- *     'name' => t('Nodes'),
- *     'description' => t('Tokens related to node objects.'),
- *   );
- *   $data['tokens']['node']['title'] = array(
- *     'name' => t('Title'),
- *     'description' => t('The title of the current node.'),
- *   );
- *   $data['tokens']['node']['author'] = array(
- *     'name' => t('Author'),
- *     'description' => t('The author of the current node.'),
- *     'type' => 'user',
- *   );
- * @endcode
- * @return
- *   An associative array of token information, grouped by token type.
+ * Applies chained tokens by getting properties of the given wrapper.
  */
-function token_info() {
-  $data = &drupal_static(__FUNCTION__);
-  if (!isset($data)) {
-    _token_initialize();
-    $data = module_invoke_all('token_info');
-    drupal_alter('token_info', $data);
+function _token_get_replacement(DrupalPropertyWrapperInterface $wrapper, $token) {
+  foreach (explode(':', $token) as $i => $name) {
+    $wrapper = $wrapper->$name;
   }
-  return $data;
+  // If no format was given apply the default just by converting it to string.
+  return (string)$wrapper;
 }
 
+
 /**
- * Load modulename.tokens.inc for all enabled modules.
+ * Formats a token by using a token format as defined in an implementation of
+ * hook_property_info().
+ *
+ * @param $value
+ *   The token value to format.
+ * @param $type
+ *   The type of the passed token.
+ * @param $format
+ *   (optional) The format to apply. If unset, the default format will be used.
+ * @param $options
+ *   (optional) A keyed array of options. Supported are:
+ *   - language: A language object to be used when generating locale-sensitive
+ *     formats.
+ *   - formats: Only used internally to add in further formats.
+ *   - default format: Only used internally to customize the default format.
+ * @param $context
+ *   (optional) An array of contextual information which format callbacks
+ *   can make use of. Used internally.
  */
-function _token_initialize() {
-  $initialized = &drupal_static(__FUNCTION__);
-  if (!$initialized) {
-    foreach (module_list() as $module) {
-      $filename = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "/$module.tokens.inc";
-      if (file_exists($filename)) {
-        include_once $filename;
-      }
-    }
-    $initialized = TRUE;
+function token_format($value, $type, $format = NULL, array $options = array(), array $context = array()) {
+  $info = drupal_get_property_info($type) + array('formats' => array(), 'default format' => NULL);
+  $options += array('language' => NULL, 'formats' => array());
+  $formats = $options['formats'] + $info['formats'];
+  if (!isset($format)) {
+    $format = isset($options['default format']) ? $options['default format'] : $info['default format'];
+  }
+  // Now apply the format.
+  if (isset($formats[$format]['callback']) && function_exists($function = $formats[$format]['callback'])) {
+    return $function($value, $format, $options['language'], $context);
+  }
+  elseif (!isset($format)) {
+    return $value;
   }
 }
diff --git modules/book/book.module modules/book/book.module
index 7ac3a3b..43a3bd4 100644
--- modules/book/book.module
+++ modules/book/book.module
@@ -721,6 +721,7 @@ function book_menu_name($bid) {
 function book_node_load($nodes, $types) {
   $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' =>  array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
   foreach ($result as $record) {
+    $nodes[$record['nid']]->entityTags[] = 'book';
     $nodes[$record['nid']]->book = $record;
     $nodes[$record['nid']]->book['href'] = $record['link_path'];
     $nodes[$record['nid']]->book['title'] = $record['link_title'];
@@ -1222,3 +1223,46 @@ function book_menu_subtree_data($link) {
 
   return $tree[$cid];
 }
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function book_entity_info_alter(&$entity_info) {
+  $entity_info['node']['tags']['book'] = array(
+    'label' => t('Book page'),
+  );
+  $entity_info['node']['default tags'][] = 'book';
+
+  $properties = &$entity_info['node']['tags']['book']['properties'];
+  $properties['book-id'] = array(
+    'label' => t("Book ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of this page's book."),
+    'getter callback' => 'book_get_properties',
+  );
+  $properties['book'] = array(
+    'label' => t("Book"),
+    'type' => 'node',
+    'description' => t("The book to which this book page belongs."),
+    'getter callback' => 'book_get_properties',
+  );
+
+}
+
+/**
+ * Callback for getting book node properties.
+ * @see book_entity_info_alter()
+ */
+function book_get_properties($node, array $options, $name, $entity_type) {
+  if (!isset($node->book['bid'])) {
+    throw new DrupalPropertyWrapperException('This node is no book page.');
+  }
+
+  switch ($name) {
+    case 'book-id':
+      return $node->book['bid'];
+
+    case 'book':
+      return node_load($node->book['bid']);
+  }
+}
diff --git modules/comment/comment.entity.inc modules/comment/comment.entity.inc
new file mode 100644
index 0000000..1c6841f
--- /dev/null
+++ modules/comment/comment.entity.inc
@@ -0,0 +1,147 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the comment entity.
+ */
+
+
+/**
+ * Implement hook_entity_info() {
+ */
+function comment_entity_info() {
+  $return =  array(
+    'comment' => array(
+      'label' => t('Comment'),
+      'base table' => 'comment',
+      'fieldable' => TRUE,
+      'controller class' => 'CommentController',
+      'object keys' => array(
+        'id' => 'cid',
+        'bundle' => 'node_type',
+      ),
+      'bundle keys' => array(
+        'bundle' => 'type',
+      ),
+      'bundles' => array(),
+      'static cache' => FALSE,
+    ),
+  );
+
+  foreach (node_type_get_names() as $type => $name) {
+    $return['comment']['bundles']['comment_node_' . $type] = array(
+      'label' => $name,
+    );
+  }
+
+  // Add meta-data about the basic comment properties.
+  $properties = &$return['comment']['properties'];
+
+  $properties['cid'] = array(
+    'label' => t("Comment ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the comment."),
+  );
+  $properties['pid'] = array(
+    'label' => t("Parent ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the comment's parent, if comment threading is active."),
+  );
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node the comment was posted to."),
+  );
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the user who posted the comment."),
+  );
+  $properties['hostname'] = array(
+    'label' => t("IP Address"),
+    'description' => t("The IP address of the computer the comment was posted from."),
+  );
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The name left by the comment author."),
+    'getter callback' => 'comment_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['mail'] = array(
+    'label' => t("Email address"),
+    'description' => t("The email address left by the comment author."),
+    'getter callback' => 'comment_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['homepage'] = array(
+    'label' => t("Home page"),
+    'description' => t("The home page URL left by the comment author."),
+    'sanitize' => 'filter_xss_bad_protocol',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['title'] = array(
+    'label' => t("Title"),
+    'description' => t("The title of the comment."),
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['body'] = array(
+    'label' => t("Content"),
+    'description' => t("The formatted content of the comment itself."),
+    'getter callback' => 'comment_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the comment."),
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['edit-url'] = array(
+    'label' => t("Edit URL"),
+    'description' => t("The URL of the comment's edit page."),
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['created'] = array(
+    'label' => t("Date created"),
+    'description' => t("The date the comment was posted."),
+    'type' => 'date',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['parent'] = array(
+    'label' => t("Parent"),
+    'description' => t("The comment's parent, if comment threading is active."),
+    'type' => 'comment',
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['node'] = array(
+    'label' => t("Node"),
+    'description' => t("The node the comment was posted to."),
+    'type' => 'node',
+    'getter callback' => 'comment_get_properties',
+  );
+  $properties['author'] = array(
+    'label' => t("Author"),
+    'description' => t("The author of the comment, if they were logged in."),
+    'type' => 'user',
+    'getter callback' => 'comment_get_properties',
+  );
+  return $return;
+}
+
+/**
+ * Implement hook_entity_info_alter().
+ */
+function comment_entity_info_alter(&$entity_info) {
+  $properties = &$entity_info['node']['properties'];
+
+  $properties['comment-count'] = array(
+    'label' => t("Comment count"),
+    'description' => t("The number of comments posted on a node."),
+    'getter callback' => 'comment_get_node_properties',
+  );
+  $properties['comment-count-new'] = array(
+    'label' => t("New comment count"),
+    'description' => t("The number of comments posted on a node since the reader last viewed it."),
+    'getter callback' => 'comment_get_node_properties',
+  );
+}
diff --git modules/comment/comment.info modules/comment/comment.info
index 278980a..874bc74 100644
--- modules/comment/comment.info
+++ modules/comment/comment.info
@@ -10,4 +10,4 @@ files[] = comment.admin.inc
 files[] = comment.pages.inc
 files[] = comment.install
 files[] = comment.test
-files[] = comment.tokens.inc
+files[] = comment.entity.inc
diff --git modules/comment/comment.module modules/comment/comment.module
index 3789651..16259fa 100644
--- modules/comment/comment.module
+++ modules/comment/comment.module
@@ -96,37 +96,6 @@ function comment_help($path, $arg) {
 }
 
 /**
- * Implement hook_entity_info() {
- */
-function comment_entity_info() {
-  $return =  array(
-    'comment' => array(
-      'label' => t('Comment'),
-      'base table' => 'comment',
-      'fieldable' => TRUE,
-      'controller class' => 'CommentController',
-      'object keys' => array(
-        'id' => 'cid',
-        'bundle' => 'node_type',
-      ),
-      'bundle keys' => array(
-        'bundle' => 'type',
-      ),
-      'bundles' => array(),
-      'static cache' => FALSE,
-    ),
-  );
-
-  foreach (node_type_get_names() as $type => $name) {
-    $return['comment']['bundles']['comment_node_' . $type] = array(
-      'label' => $name,
-    );
-  }
-
-  return $return;
-}
-
-/**
  * Implement hook_theme().
  */
 function comment_theme() {
@@ -2447,3 +2416,62 @@ function comment_filter_format_delete($format, $default) {
     ->execute();
 }
 
+/**
+ * Callback for getting comment properties.
+ * @see comment_entity_info()
+ */
+function comment_get_properties($comment, array $options, $name) {
+  switch ($name) {
+    case 'name':
+      $name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name;
+      return $options['sanitize'] ? filter_xss($name) : $name;
+
+    case 'mail':
+      if ($comment->uid != 0) {
+        $account = user_load($comment->uid);
+        $mail = $account->mail;
+      }
+      else {
+        $mail = $comment->mail;
+      }
+      return $options['sanitize'] ? check_plain($mail) : $mail;
+
+    case 'title':
+      return $options['sanitize'] ? filter_xss($comment->subject) : $comment->subject;
+
+    case 'body':
+      return $options['sanitize'] ? check_markup($comment->comment, $comment->format) : $comment->comment;
+
+    case 'url':
+      return url('comment/' . $comment->cid, array('absolute' => TRUE, 'fragment' => 'comment-' . $comment->cid) + $options);
+
+    case 'edit-url':
+      return url('comment/edit/' . $comment->cid, array('absolute' => TRUE) + $options);
+    
+    case 'node':
+      return node_load($comment->nid);
+      
+    case 'parent':
+      if ($parent = comment_load($comment->pid)) {
+        return $parent;
+      }
+      throw new DrupalPropertyWrapperException('This comment has no parent comment.');
+
+    case 'author':
+      return user_load($comment->uid);
+  }
+}
+
+/**
+ * Callback for getting node properties.
+ * @see comment_entity_info_alter()
+ */
+function comment_get_node_properties($node, array $options, $name) {
+  switch ($name) {
+    case 'comment-count':
+      return $node->comment_count;
+
+    case 'comment-count-new':
+      return comment_num_new($node->nid);
+  }
+}
diff --git modules/comment/comment.tokens.inc modules/comment/comment.tokens.inc
deleted file mode 100644
index 135c5ad..0000000
--- modules/comment/comment.tokens.inc
+++ /dev/null
@@ -1,248 +0,0 @@
-<?php
-// $Id: comment.tokens.inc,v 1.1 2009/08/19 20:19:36 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for comment-related data.
- */
-
-/**
- * Implement hook_token_info().
- */
-function comment_token_info() {
-  $type = array(
-    'name' => t('Comments'),
-    'description' => t('Tokens for comments posted on the site.'),
-    'needs-data' => 'comment',
-  );
-
-  // Comment-related tokens for nodes
-  $node['comment-count'] = array(
-    'name' => t("Comment count"),
-    'description' => t("The number of comments posted on a node."),
-  );
-  $node['comment-count-new'] = array(
-    'name' => t("New comment count"),
-    'description' => t("The number of comments posted on a node since the reader last viewed it."),
-  );
-
-  // Core comment tokens
-  $comment['cid'] = array(
-    'name' => t("Comment ID"),
-    'description' => t("The unique ID of the comment."),
-  );
-  $comment['pid'] = array(
-    'name' => t("Parent ID"),
-    'description' => t("The unique ID of the comment's parent, if comment threading is active."),
-  );
-  $comment['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node the comment was posted to."),
-  );
-  $comment['uid'] = array(
-    'name' => t("User ID"),
-    'description' => t("The unique ID of the user who posted the comment."),
-  );
-  $comment['hostname'] = array(
-    'name' => t("IP Address"),
-    'description' => t("The IP address of the computer the comment was posted from."),
-  );
-  $comment['name'] = array(
-    'name' => t("Name"),
-    'description' => t("The name left by the comment author."),
-  );
-  $comment['mail'] = array(
-    'name' => t("Email address"),
-    'description' => t("The email address left by the comment author."),
-  );
-  $comment['homepage'] = array(
-    'name' => t("Home page"),
-    'description' => t("The home page URL left by the comment author."),
-  );
-  $comment['title'] = array(
-    'name' => t("Title"),
-    'description' => t("The title of the comment."),
-  );
-  $comment['body'] = array(
-    'name' => t("Content"),
-    'description' => t("The formatted content of the comment itself."),
-  );
-  $comment['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the comment."),
-  );
-  $comment['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the comment's edit page."),
-  );
-
-  // Chained tokens for comments
-  $comment['created'] = array(
-    'name' => t("Date created"),
-    'description' => t("The date the comment was posted."),
-    'type' => 'date',
-  );
-  $comment['parent'] = array(
-    'name' => t("Parent"),
-    'description' => t("The comment's parent, if comment threading is active."),
-    'type' => 'comment',
-  );
-  $comment['node'] = array(
-    'name' => t("Node"),
-    'description' => t("The node the comment was posted to."),
-    'type' => 'node',
-  );
-  $comment['author'] = array(
-    'name' => t("Author"),
-    'description' => t("The author of the comment, if they were logged in."),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => array('comment' => $type),
-    'tokens' => array(
-      'node' => $node,
-      'comment' => $comment,
-    ),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function comment_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['language'])) {
-    $url_options['language'] = $language;
-    $language_code = $language->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'comment' && !empty($data['comment'])) {
-    $comment = $data['comment'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the comment.
-        case 'cid':
-          $replacements[$original] = $comment->cid;
-          break;
-
-        case 'nid':
-          $replacements[$original] = $comment->nid;
-          break;
-
-        case 'uid':
-          $replacements[$original] = $comment->uid;
-          break;
-
-        case 'pid':
-          $replacements[$original] = $comment->pid;
-          break;
-
-        // Poster identity information for comments
-        case 'hostname':
-          $replacements[$original] = $sanitize ? check_plain($comment->hostname) : $comment->hostname;
-          break;
-
-        case 'name':
-          $name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name;
-          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
-          break;
-
-        case 'mail':
-          if ($comment->uid != 0) {
-            $account = user_load($comment->uid);
-            $mail = $account->mail;
-          }
-          else {
-            $mail = $comment->mail;
-          }
-          $replacements[$original] = $sanitize ? check_plain($mail) : $mail;
-          break;
-
-        case 'homepage':
-          $replacements[$original] = $sanitize ? filter_xss_bad_protocol($comment->homepage) : $comment->homepage;
-          break;
-
-        case 'title':
-          $replacements[$original] = $sanitize ? filter_xss($comment->subject) : $comment->subject;
-          break;
-
-        case 'body':
-          $replacements[$original] = $sanitize ? check_markup($comment->comment, $comment->format) : $replacements[$original] = $comment->comment;
-          break;
-
-        // Comment related URLs.
-        case 'url':
-          $replacements[$original] = url('comment/' . $comment->cid, array('absolute' => TRUE, 'fragment' => 'comment-' . $comment->cid));
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url('comment/edit/' . $comment->cid, array('absolute' => TRUE));
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $replacements[$original] = $sanitize ? filter_xss($comment->name) : $comment->name;
-          break;
-
-        case 'parent':
-          if (!empty($comment->pid)) {
-            $parent = comment_load($comment->pid);
-            $replacements[$original] = $sanitize ? filter_xss($parent->subject) : $parent->subject;
-          }
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($comment->timestamp, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'node':
-          $node = node_load($comment->nid);
-          $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
-          break;
-      }
-    }
-
-    // Chained token relationships.
-    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
-      $node = node_load($comment->nid);
-      $replacements += token_generate('node', $node_tokens, array('node' => $node), $options);
-    }
-
-    if ($date_tokens = token_find_with_prefix($tokens, 'created')) {
-      $replacements += token_generate('date', $date_tokens, array('date' => $comment->timestamp), $options);
-    }
-
-    if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) {
-      $replacements += token_generate('comment', $parent_tokens, array('comment' => $parent), $options);
-    }
-
-    if (($author_tokens = token_find_with_prefix($tokens, 'author')) && $account = user_load($comment->uid)) {
-      $replacements += token_generate('user', $author_tokens, array('user' => $account), $options);
-    }
-  }
-  elseif ($type == 'node' & !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      switch($name) {
-        case 'comment-count':
-          $replacements[$original] = $node->comment_count;
-          break;
-
-        case 'comment-count-new':
-          $replacements[$original] = comment_num_new($node->nid);
-          break;
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/field/field.default.inc modules/field/field.default.inc
index 77a61d6..ded22ef 100644
--- modules/field/field.default.inc
+++ modules/field/field.default.inc
@@ -66,7 +66,7 @@ function field_default_view($obj_type, $object, $field, $instance, $langcode, $i
 
   if ($display['type'] !== 'hidden') {
     $theme = 'field_formatter_' . $display['type'];
-    $single = (field_behaviors_formatter('multiple values', $display) == FIELD_BEHAVIOR_DEFAULT);
+    $single = (field_behaviors_formatter('multiple values', $display['type']) == FIELD_BEHAVIOR_DEFAULT);
 
     $label_display = $display['label'];
     if ($build_mode == 'search_index') {
diff --git modules/field/field.info.inc modules/field/field.info.inc
index 1f430d8..c977b93 100644
--- modules/field/field.info.inc
+++ modules/field/field.info.inc
@@ -27,6 +27,8 @@ function field_info_cache_clear() {
   _field_info_collate_types(TRUE);
   drupal_static_reset('field_build_modes');
   _field_info_collate_fields(TRUE);
+  drupal_static_reset('entity_get_info');
+  cache_clear_all('entity_info', 'cache');
 }
 
 /**
@@ -125,6 +127,7 @@ function _field_info_collate_types($reset = FALSE) {
       drupal_alter('field_formatter_info', $info['formatter types']);
 
       // Populate information about 'fieldable' entities.
+      module_load_all_includes('inc', 'entity');
       foreach (module_implements('entity_info') as $module) {
         $entities = (array) module_invoke($module, 'entity_info');
         foreach ($entities as $name => $entity_info) {
@@ -151,6 +154,12 @@ function _field_info_collate_types($reset = FALSE) {
         }
       }
       drupal_alter('entity_info', $info['fieldable types']);
+      // Remove all not fieldable types added by drupal alter.
+      foreach ($info['fieldable types'] as $name => $entity_info) {
+        if (empty($entity_info['fieldable'])) {
+          unset($info['fieldable types'][$name]);
+        }
+      }
 
       cache_set('field_info_types', $info, 'cache_field');
     }
@@ -328,15 +337,15 @@ function field_behaviors_widget($op, $instance) {
  *  @param $op
  *    The name of the operation.
  *    Currently supported: 'multiple values'
- *  @param $display
- *    The $instance['display'][$build_mode] array.
+ *  @param $formatter_type
+ *    The formatter type.
  *  @return
  *    FIELD_BEHAVIOR_NONE    - do nothing for this operation.
  *    FIELD_BEHAVIOR_CUSTOM  - use the formatter's callback function.
  *    FIELD_BEHAVIOR_DEFAULT - use field module default behavior.
  */
-function field_behaviors_formatter($op, $display) {
-  $info = field_info_formatter_types($display['type']);
+function field_behaviors_formatter($op, $formatter_type) {
+  $info = field_info_formatter_types($formatter_type);
   return isset($info['behaviors'][$op]) ? $info['behaviors'][$op] : FIELD_BEHAVIOR_DEFAULT;
 }
 
@@ -473,7 +482,7 @@ function field_info_bundle_entity($bundle) {
  */
 function field_info_fields() {
   $info = _field_info_collate_fields();
-  return $info['fields'];
+  return isset($info['fields']) ? $info['fields'] : array();
 }
 
 /**
@@ -597,5 +606,112 @@ function field_info_formatter_settings($type) {
 }
 
 /**
+ * Implement hook_entity_info_alter().
+ *
+ * Add metadata about all properties provided by fields.
+ */
+function field_entity_info_alter(&$entity_info) {
+  // Loop over all field instance and add them as property.
+  foreach (field_info_fields() as $field_name => $field) {
+    $field += array('bundles' => array());
+    $field_type = field_info_field_types($field['type']) + array('property_callbacks' => array());
+    // Add in our default callback as the first one.
+    array_unshift($field_type['property_callbacks'], 'field_default_property_callback');
+
+    foreach ($field['bundles'] as $bundle) {
+      $instance = field_info_instance($field_name, $bundle);
+      if (empty($instance['deleted']) && $entity_type = field_info_bundle_entity($bundle)) {
+        foreach ($field_type['property_callbacks'] as $callback) {
+          $callback($entity_info, $entity_type, $field, $instance, $field_type);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Callback to add in property info per field instance.
+ * @see field_entity_info_alter().
+ */
+function field_default_property_callback(&$entity_info, $entity_type, $field, $instance, $field_type) {
+  if (!empty($field_type['property_type'])) {
+    // Add in compatible formatters.
+    $formatters = array();
+    foreach (field_info_formatter_types() as $name => $formatter) {
+      if (in_array($field['type'], $formatter['field types'])) {
+        $formatters[$name] = array(
+          'label' => $formatter['label'],
+          'callback' => 'field_format_property',
+        );
+      }
+    }
+    $property = &$entity_info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
+    $property = array(
+      'label' => $instance['label'],
+      'type' => $field_type['property_type'],
+      'description' => $instance['description'],
+      'getter callback' => 'field_property_get',
+      'setter callback' => 'field_property_set',
+      'formats' => $formatters,
+      'default format' => $field_type['default_formatter'],
+    );
+    $instance += array('property info' => array());
+    $property += $instance['property info'];
+  }
+}
+
+/**
+ * Callback for applying custom property formats.
+ */
+function field_format_property($value, $formatter_type, $language, array $context) {
+  $field_name = $context['property'];
+  $langcode = _field_property_get_langcode($context['entity'], $language, $field_name);
+  $single = (field_behaviors_formatter('multiple values', $formatter_type) == FIELD_BEHAVIOR_DEFAULT);
+  $item = $single ? $context['entity']->{$field_name}[$langcode][0] : $context['entity']->$field_name;
+  $field = field_info_field($field_name);
+  return field_format($context['entity type'], $context['entity'], $field, $langcode, $item, $formatter_type);
+}
+
+/**
+ * Callback for getting field property values.
+ */
+function field_property_get($object, array $options, $name, $obj_type) {
+  $langcode = _field_property_get_langcode($object, $options['language'], $name);
+  if ($options['sanitize'] && !isset($object->{$name}[$langcode][0]['safe'])) {
+    _field_refresh_sanitized_values($object, $obj_type, $name, $langcode);
+  }
+  return $options['sanitize'] ? $object->{$name}[$langcode][0]['safe'] : $object->{$name}[$langcode][0]['value'];
+}
+
+function _field_property_get_langcode($object, $language, $name) {
+  $langcode = FIELD_LANGUAGE_NONE;
+  if (isset($language) && isset($object->{$name}[$language->language])) {
+    $langcode = $language->language;
+  }
+  return $langcode;
+}
+
+function _field_refresh_sanitized_values($object, $obj_type, $field_name, $langcode) {
+  $field = field_info_field($field_name);
+  list($id, $vid, $bundle) = field_extract_ids($obj_type, $object);
+  $instance = field_info_instance($field_name, $bundle);
+
+  $function = $field['module'] . '_field_sanitize';
+  if (function_exists($function)) {
+    $function($obj_type, $object, $field, $instance, $langcode, $object->{$field_name}[$langcode]);
+  }
+}
+
+/**
+ * Callback for setting field property values.
+ */
+function field_property_set(&$object, $name, $value, $obj_type) {
+  $langcode = array_shift(array_keys($object->$name));
+  $object->{$name}[$langcode][0]['value'] = $value;
+  unset($object->{$name}[$langcode][0]['safe']);
+}
+
+
+/**
  * @} End of "defgroup field_info"
  */
diff --git modules/field/field.module modules/field/field.module
index a746e00..7f30f2a 100644
--- modules/field/field.module
+++ modules/field/field.module
@@ -465,11 +465,6 @@ function _field_filter_xss_display_allowed_tags() {
 /**
  * Format a field item for display.
  *
- * TODO D7 : do we still need field_format ?
- * - backwards compatibility of templates - check what fallbacks we can propose...
- * - was used by Views integration in CCK in D6 - do we need now?
- * At least needs a little rehaul/update...
- *
  * Used to display a field's values outside the context of the $node, as
  * when fields are displayed in Views, or to display a field in a template
  * using a different formatter than the one set up on the Display Fields tab
@@ -477,6 +472,8 @@ function _field_filter_xss_display_allowed_tags() {
  *
  * @param $field
  *   Either a field array or the name of the field.
+ * @param $langcode
+ *   The language code of the formatted field items.
  * @param $item
  *   The field item(s) to be formatted (such as $node->field_foo[0],
  *   or $node->field_foo if the formatter handles multiple values itself)
@@ -491,7 +488,7 @@ function _field_filter_xss_display_allowed_tags() {
  *   It will have been passed through the necessary check_plain() or check_markup()
  *   functions as necessary.
  */
-function field_format($obj_type, $object, $field, $item, $formatter_type = NULL, $formatter_settings = array()) {
+function field_format($obj_type, $object, $field, $langcode, $item, $formatter_type = NULL, $formatter_settings = array()) {
   if (!is_array($field)) {
     $field = field_info_field($field);
   }
@@ -510,7 +507,7 @@ function field_format($obj_type, $object, $field, $item, $formatter_type = NULL,
     $display['settings'] += field_info_formatter_settings($display['type']);
 
     if ($display['type'] !== 'hidden') {
-      $theme = $formatter['module'] . '_formatter_' . $display['type'];
+      $theme = 'field_formatter_' . $display['type'];
 
       $element = array(
         '#theme' => $theme,
@@ -523,14 +520,14 @@ function field_format($obj_type, $object, $field, $item, $formatter_type = NULL,
         '#delta' => isset($item['#delta']) ? $item['#delta'] : NULL,
       );
 
-      if (field_behaviors_formatter('multiple values', $display) == FIELD_BEHAVIOR_DEFAULT) {
+      if (field_behaviors_formatter('multiple values', $display['type']) == FIELD_BEHAVIOR_DEFAULT) {
         // Single value formatter.
 
         // hook_field('sanitize') expects an array of items, so we build one.
         $items = array($item);
         $function = $field['module'] . '_field_sanitize';
         if (function_exists($function)) {
-          $function($obj_type, $object, $field, $instance, $items);
+          $function($obj_type, $object, $field, $instance, $langcode, $items);
         }
 
         $element['#item'] = $items[0];
@@ -540,7 +537,7 @@ function field_format($obj_type, $object, $field, $item, $formatter_type = NULL,
         $items = $item;
         $function = $field['module'] . '_field_sanitize';
         if (function_exists($function)) {
-          $function($obj_type, $object, $field, $instance, $items);
+          $function($obj_type, $object, $field, $instance, $langcode, $items);
         }
 
         foreach ($items as $delta => $item) {
diff --git modules/field/modules/number/number.module modules/field/modules/number/number.module
index fa8f325..3522e42 100644
--- modules/field/modules/number/number.module
+++ modules/field/modules/number/number.module
@@ -35,6 +35,7 @@ function number_field_info() {
       'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
       'default_widget' => 'number',
       'default_formatter' => 'number_integer',
+      'property_type' => 'integer',
     ),
     'number_decimal' => array(
       'label' => t('Decimal'),
@@ -43,6 +44,7 @@ function number_field_info() {
       'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
       'default_widget' => 'number',
       'default_formatter' => 'number_integer',
+      'property_type' => 'decimal',
     ),
     'number_float' => array(
       'label' => t('Float'),
@@ -50,6 +52,7 @@ function number_field_info() {
       'instance_settings' => array('min' => '', 'max' => '', 'prefix' => '', 'suffix' => ''),
       'default_widget' => 'number',
       'default_formatter' => 'number_integer',
+      'property_type' => 'decimal',
     ),
   );
 }
diff --git modules/field/modules/text/text.module modules/field/modules/text/text.module
index 220c5aa..0022b88 100644
--- modules/field/modules/text/text.module
+++ modules/field/modules/text/text.module
@@ -40,6 +40,7 @@ function text_field_info() {
       'instance_settings' => array('text_processing' => 0),
       'default_widget' => 'text_textfield',
       'default_formatter' => 'text_default',
+      'property_type' => 'text',
     ),
     'text_long' => array(
       'label' => t('Long text'),
@@ -48,6 +49,7 @@ function text_field_info() {
       'instance_settings' => array('text_processing' => 0),
       'default_widget' => 'text_textarea',
       'default_formatter' => 'text_default',
+      'property_type' => 'text',
     ),
     'text_with_summary' => array(
       'label' => t('Long text and summary'),
@@ -56,6 +58,8 @@ function text_field_info() {
       'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
       'default_widget' => 'text_textarea_with_summary',
       'default_formatter' => 'text_summary_or_trimmed',
+      'property_type' => 'text',
+      'property_callbacks' => array(),
     ),
   );
 }
diff --git modules/node/node.entity.inc modules/node/node.entity.inc
new file mode 100644
index 0000000..7d16b86
--- /dev/null
+++ modules/node/node.entity.inc
@@ -0,0 +1,120 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the node entity.
+ */
+
+
+/**
+ * Implement hook_entity_info().
+ */
+function node_entity_info() {
+  $return = array(
+    'node' => array(
+      'label' => t('Node'),
+      'controller class' => 'NodeController',
+      'base table' => 'node',
+      'revision table' => 'node_revision',
+      'fieldable' => TRUE,
+      'object keys' => array(
+        'id' => 'nid',
+        'revision' => 'vid',
+        'bundle' => 'type',
+      ),
+      // Node.module handles its own caching.
+      // 'cacheable' => FALSE,
+      'bundles' => array(),
+    ),
+  );
+  // Bundles must provide a human readable name so we can create help and error
+  // messages, and the path to attach Field admin pages to.
+  foreach (node_type_get_names() as $type => $name) {
+    $return['node']['bundles'][$type] = array(
+      'label' => $name,
+      'admin' => array(
+        'path' => 'admin/structure/node-type/' . str_replace('_', '-', $type),
+        'access arguments' => array('administer content types'),
+      ),
+    );
+  }
+  // Add meta-data about the basic node properties.
+  $properties = &$return['node']['properties'];
+  
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node."),
+  );
+  $properties['vid'] = array(
+    'label' => t("Revision ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the node's latest revision."),
+  );
+  $properties['tnid'] = array(
+    'label' => t("Translation set ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the original-language version of this node, if one exists."),
+  );
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the author of the node."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['type'] = array(
+    'label' => t("Content type"),
+    'description' => t("The type of the node."),
+  );
+  $properties['type-name'] = array(
+    'label' => t("Content type name"),
+    'description' => t("The human-readable name of the node type."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['title'] = array(
+    'label' => t("Title"),
+    'description' => t("The title of the node."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['language'] = array(
+    'label' => t("Language"),
+    'description' => t("The language the node is written in."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the node."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['edit-url'] = array(
+    'label' => t("Edit URL"),
+    'description' => t("The URL of the node's edit page."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['created'] = array(
+    'label' => t("Date created"),
+    'type' => 'date',
+    'description' => t("The date the node was posted."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['changed'] = array(
+    'label' => t("Date changed"),
+    'type' => 'date',
+    'description' => t("The date the node was most recently updated."),
+  );
+  $properties['author-name'] = array(
+    'label' => t("Author name"),
+    'description' => t("The node author's name."),
+    'getter callback' => 'node_get_properties',
+  );
+  $properties['author'] = array(
+    'label' => t("Author"),
+    'type' => 'user',
+    'description' => t("The author of the node."),
+    'getter callback' => 'node_get_properties',
+  );
+
+  return $return;
+}
+
diff --git modules/node/node.info modules/node/node.info
index 6a690d2..0550c19 100644
--- modules/node/node.info
+++ modules/node/node.info
@@ -10,5 +10,5 @@ files[] = node.admin.inc
 files[] = node.pages.inc
 files[] = node.install
 files[] = node.test
-files[] = node.tokens.inc
+files[] = node.entity.inc
 required = TRUE
diff --git modules/node/node.module modules/node/node.module
index 665f0b1..4bcecb3 100644
--- modules/node/node.module
+++ modules/node/node.module
@@ -174,42 +174,6 @@ function node_cron() {
 }
 
 /**
- * Implement hook_entity_info().
- */
-function node_entity_info() {
-  $return = array(
-    'node' => array(
-      'label' => t('Node'),
-      'controller class' => 'NodeController',
-      'base table' => 'node',
-      'revision table' => 'node_revision',
-      'fieldable' => TRUE,
-      'object keys' => array(
-        'id' => 'nid',
-        'revision' => 'vid',
-        'bundle' => 'type',
-      ),
-      // Node.module handles its own caching.
-      // 'cacheable' => FALSE,
-      'bundles' => array(),
-    ),
-  );
-  // Bundles must provide a human readable name so we can create help and error
-  // messages, and the path to attach Field admin pages to.
-  foreach (node_type_get_names() as $type => $name) {
-    $return['node']['bundles'][$type] = array(
-      'label' => $name,
-      'admin' => array(
-        'path' => 'admin/structure/node-type/' . str_replace('_', '-', $type),
-        'access arguments' => array('administer content types'),
-      ),
-    );
-  }
-  return $return;
-}
-
-
-/**
  * Implement hook_field_build_modes().
  */
 function node_field_build_modes($obj_type) {
@@ -3128,6 +3092,32 @@ function node_requirements($phase) {
 }
 
 /**
+ * Callback for getting node properties.
+ * @see node_entity_info()
+ */
+function node_get_properties($node, array $options, $name, $entity_type) {
+
+  switch ($name) {
+    case 'type-name':
+      $type_name = node_type_get_name($node->type);
+      return $options['sanitize'] ? check_plain($type_name) : $type_name;
+
+    case 'url':
+      return url('node/' . $node->nid, $options + array('absolute' => TRUE));
+
+    case 'edit-url':
+      return url('node/' . $node->nid . '/edit', $options + array('absolute' => TRUE));
+
+    case 'author-name':
+      $name = ($node->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $node->name;
+      return $options['sanitize'] ? filter_xss($name) : $name;
+      
+    case 'author':
+      return user_load($node->uid);
+  }
+}
+
+/**
  * Controller class for nodes.
  *
  * This extends the DrupalDefaultEntityController class, adding required
diff --git modules/node/node.tokens.inc modules/node/node.tokens.inc
deleted file mode 100644
index ffc085d..0000000
--- modules/node/node.tokens.inc
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-// $Id: node.tokens.inc,v 1.2 2009/08/23 13:02:38 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for node-related data.
- */
-
-
-
-/**
- * Implement hook_token_info().
- */
-function node_token_info() {
-  $type = array(
-    'name' => t('Nodes'),
-    'description' => t('Tokens related to individual nodes.'),
-    'needs-data' => 'node',
-  );
-
-  // Core tokens for nodes.
-  $node['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node."),
-  );
-  $node['vid'] = array(
-    'name' => t("Revision ID"),
-    'description' => t("The unique ID of the node's latest revision."),
-  );
-  $node['tnid'] = array(
-    'name' => t("Translation set ID"),
-    'description' => t("The unique ID of the original-language version of this node, if one exists."),
-  );
-  $node['uid'] = array(
-    'name' => t("User ID"),
-    'description' => t("The unique ID of the user who posted the node."),
-  );
-  $node['type'] = array(
-    'name' => t("Content type"),
-    'description' => t("The type of the node."),
-  );
-  $node['type-name'] = array(
-    'name' => t("Content type name"),
-    'description' => t("The human-readable name of the node type."),
-  );
-  $node['title'] = array(
-    'name' => t("Title"),
-    'description' => t("The title of the node."),
-  );
-  $node['body'] = array(
-    'name' => t("Body"),
-    'description' => t("The main body text of the node."),
-  );
-  $node['summary'] = array(
-    'name' => t("Summary"),
-    'description' => t("The summary of the node's main body text."),
-  );
-  $node['language'] = array(
-    'name' => t("Language"),
-    'description' => t("The language the node is written in."),
-  );
-  $node['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the node."),
-  );
-  $node['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the node's edit page."),
-  );
-
-  // Chained tokens for nodes.
-  $node['created'] = array(
-    'name' => t("Date created"),
-    'description' => t("The date the node was posted."),
-    'type' => 'date',
-  );
-  $node['changed'] = array(
-    'name' => t("Date changed"),
-    'description' => t("The date the node was most recently updated."),
-    'type' => 'date',
-  );
-  $node['author'] = array(
-    'name' => t("Author"),
-    'description' => t("The author of the node."),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => array('node' => $type),
-    'tokens' => array('node' => $node),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function node_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['language'])) {
-    $url_options['language'] = $language;
-    $language_code = $language->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'node' && !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the node.
-        case 'nid':
-          $replacements[$original] = $node->nid;
-          break;
-
-        case 'vid':
-          $replacements[$original] = $node->vid;
-          break;
-
-        case 'tnid':
-          $replacements[$original] = $node->tnid;
-          break;
-
-        case 'uid':
-          $replacements[$original] = $node->uid;
-          break;
-
-        case 'name':
-          $replacements[$original] = $sanitize ? check_plain($node->name) : $node->name;
-          break;
-
-        case 'title':
-          $replacements[$original] = $sanitize ? check_plain($node->title) : $node->title;
-          break;
-
-        case 'body':
-          if (!empty($node->body)) {
-            $replacements[$original] = $sanitize ? $node->body[0]['safe'] : $node->body[0]['value'];
-          }
-          break;
-
-        case 'summary':
-          if (!empty($node->body)) {
-            $replacements[$original] = $sanitize ? $node->body[0]['safe_summary'] : $node->body[0]['summary'];
-          }
-          break;
-
-        case 'type':
-          $replacements[$original] = $sanitize ? check_plain($node->type) : $node->type;
-          break;
-
-        case 'type-name':
-          $type_name = node_get_types('name', $node->type);
-          $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
-          break;
-
-        case 'language':
-          $replacements[$original] = $sanitize ? check_plain($node->language) : $node->language;
-          break;
-
-        case 'url':
-          $replacements[$original] = url('node/' . $node->nid, array('absolute' => TRUE));
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url('node/' . $node->nid . '/edit', array('absolute' => TRUE));
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $name = ($node->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $node->name;
-          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($node->created, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'changed':
-          $replacements[$original] = format_date($node->changed, 'medium', '', NULL, $language_code);
-          break;
-      }
-    }
-
-    if ($author_tokens = token_find_with_prefix($tokens, 'author')) {
-      $author = user_load($node->uid);
-      $replacements += token_generate('user', $author_tokens, array('user' => $author), $options);
-    }
-
-    if ($created_tokens = token_find_with_prefix($tokens, 'created')) {
-      $replacements += token_generate('date', $created_tokens, array('date' => $node->created), $options);
-    }
-
-    if ($changed_tokens = token_find_with_prefix($tokens, 'changed')) {
-      $replacements += token_generate('date', $changed_tokens, array('date' => $node->changed), $options);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/simpletest/tests/field_test.module modules/simpletest/tests/field_test.module
index a0bac02..f41c74c 100644
--- modules/simpletest/tests/field_test.module
+++ modules/simpletest/tests/field_test.module
@@ -64,7 +64,7 @@ function field_test_entity_info() {
   $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
   return array(
     'test_entity' => array(
-      'name' => t('Test Entity'),
+      'label' => t('Test Entity'),
       'object keys' => array(
         'id' => 'ftid',
         'revision' => 'ftvid',
@@ -76,7 +76,7 @@ function field_test_entity_info() {
     ),
     // This entity type doesn't get form handling for now...
     'test_cacheable_entity' => array(
-      'name' => t('Test Entity, cacheable'),
+      'label' => t('Test Entity, cacheable'),
       'object keys' => array(
         'id' => 'ftid',
         'revision' => 'ftvid',
diff --git modules/system/system.entity.inc modules/system/system.entity.inc
new file mode 100644
index 0000000..f65806c
--- /dev/null
+++ modules/system/system.entity.inc
@@ -0,0 +1,113 @@
+<?php
+// $Id: system.tokens.inc,v 1.2 2009/08/25 10:27:15 dries Exp $
+
+/**
+ * @file
+ * Provides info about system-wide entities.
+ */
+
+/**
+ * Implement hook_entity_info().
+ */
+function system_entity_info() {
+  // Create a entity for dealing with drupal variables as properties.
+  $types['system'] = array(
+    'label' => t("System information"),
+  );
+  
+  $properties = &$types['system']['properties'];
+  
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The name of the site."),
+    'getter callback' => 'system_get_properties',
+    'sanitize' => 'check_plain',
+  );
+  $properties['slogan'] = array(
+    'label' => t("Slogan"),
+    'description' => t("The slogan of the site."),
+    'getter callback' => 'system_get_properties',
+    'sanitize' => 'check_plain',
+  );
+  $properties['mission'] = array(
+    'label' => t("Mission"),
+    'description' => t("The optional 'mission' of the site."),
+    'getter callback' => 'system_get_properties',
+    'sanitize' => 'filter_xss',
+  );
+  $properties['mail'] = array(
+    'label' => t("Email"),
+    'description' => t("The administrative email address for the site."),
+    'getter callback' => 'system_get_properties',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the site's front page."),
+    'getter callback' => 'system_get_properties',
+  );
+  $properties['login-url'] = array(
+    'label' => t("Login page"),
+    'description' => t("The URL of the site's login page."),
+    'getter callback' => 'system_get_properties',
+  );
+
+  // Describe files.
+  $types['file'] = array(
+    'label' => t('File'),
+    'base table' => 'file',
+    'object keys' => array(
+      'id' => 'fid',
+    ),
+    'static cache' => FALSE,
+  );
+  
+  $properties = &$types['file']['properties'];
+
+  $properties['fid'] = array(
+    'label' => t("File ID"),
+    'description' => t("The unique ID of the uploaded file."),
+  );
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'description' => t("The unique ID of the user who owns the file."),
+  );
+  $properties['name'] = array(
+    'label' => t("File name"),
+    'description' => t("The name of the file on disk."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['path'] = array(
+    'label' => t("Path"),
+    'description' => t("The location of the file on disk."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['mime'] = array(
+    'label' => t("MIME type"),
+    'description' => t("The MIME type of the file."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['size'] = array(
+    'label' => t("File size"),
+    'description' => t("The size of the file, in kilobytes."),
+    'getter callback' => 'system_get_file_properties',
+    'type' => 'integer',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The web-accessible URL for the file."),
+    'getter callback' => 'system_get_file_properties',
+  );
+  $properties['timestamp'] = array(
+    'label' => t("Timestamp"),
+    'description' => t("The date the file was most recently changed."),
+    'type' => 'date',
+  );
+  $properties['owner'] = array(
+    'label' => t("Owner"),
+    'description' => t("The user who originally uploaded the file."),
+    'type' => 'user',
+    'getter callback' => 'system_get_file_properties',
+  );
+
+  return $types;
+}
diff --git modules/system/system.info modules/system/system.info
index 7183f87..ede880f 100644
--- modules/system/system.info
+++ modules/system/system.info
@@ -11,6 +11,6 @@ files[] = image.gd.inc
 files[] = system.install
 files[] = system.test
 files[] = system.tar.inc
-files[] = system.tokens.inc
+files[] = system.entity.inc
 files[] = mail.sending.inc
 required = TRUE
diff --git modules/system/system.module modules/system/system.module
index 4f29fd4..a0cc0ac 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -263,22 +263,6 @@ function system_rdf_namespaces() {
 }
 
 /**
- * Implement hook_entity_info().
- */
-function system_entity_info() {
-  return array(
-    'file' => array(
-      'label' => t('File'),
-      'base table' => 'file',
-      'object keys' => array(
-        'id' => 'fid',
-      ),
-      'static cache' => FALSE,
-    ),
-  );
-}
-
-/**
  * Implement hook_element_info().
  */
 function system_element_info() {
@@ -3019,6 +3003,107 @@ function system_image_toolkits() {
 }
 
 /**
+ * Implement hook_property_info().
+ */
+function system_property_info() {
+  // Specify date related formats.
+  $date['short'] = array(
+    'label' => t("Short format"),
+    'description' => t("A date in 'short' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'short'))),
+    'callback' => 'system_format_date',
+  );
+  $date['medium'] = array(
+    'label' => t("Medium format"),
+    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
+    'callback' => 'system_format_date',
+  );
+  $date['long'] = array(
+    'label' => t("Long format"),
+    'description' => t("A date in 'long' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'long'))),
+    'callback' => 'system_format_date',
+  );
+  $date['since'] = array(
+    'label' => t("Time-since"),
+    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
+    'callback' => 'system_format_date',
+  );
+  $date['raw'] = array(
+    'label' => t("Raw timestamp"),
+    'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
+    'callback' => 'system_format_date',
+  );
+  return array('date' => array('default format' => 'medium', 'formats' => $date));
+}
+
+/**
+ * Callback for formating date tokens.
+ * @see system_token_format_info()
+ */
+function system_format_date($date, $format, $language) {
+  $langcode = isset($language) ? $language->language : NULL;
+
+  switch ($format) {
+    case 'raw':
+      return filter_xss($date);
+
+    case 'short':
+    case 'medium':
+    case 'long':
+      return format_date($date, $format, '', NULL, $langcode);
+
+    case 'since':
+      return format_interval((REQUEST_TIME - $date), 2, $langcode);
+  }
+}
+
+/**
+ * Callback for getting system properties.
+ * @see system_entity_info()
+ */
+function system_get_properties($data = FALSE, array $options, $name) {
+  switch ($name) {
+    case 'name':
+      return variable_get('site_name', 'Drupal');
+
+    default:
+      return variable_get('site_' . $name, '');
+
+    case 'url':
+      return url('<front>', array('absolute' => TRUE) + $options);
+
+    case 'login-url':
+      return url('user', array('absolute' => TRUE) + $options);
+  }
+}
+
+/**
+ * Callback for getting file properties.
+ * @see system_entity_info()
+ */
+function system_get_file_properties($data, array $options, $name) {
+  switch ($name) {
+    case 'name':
+      return $options['sanitize'] ? check_plain($file->filename) : $file->filename;
+
+    case 'path':
+      return $options['sanitize'] ? filter_xss($file->filepath) : $file->filepath;
+
+    case 'mime':
+      return $options['sanitize'] ? filter_xss($file->filemime) : $file->filemime;
+
+    case 'size':
+      return $file->filesize;
+
+    case 'url':
+      return url(file_create_url($file->filepath), array('absolute' => TRUE) + $options);
+
+    case 'owner':
+      return user_load($file->uid);
+  }
+}
+
+
+/**
  * Attempts to get a file using drupal_http_request and to store it locally.
  *
  * @param $url
diff --git modules/system/system.test modules/system/system.test
index b3e2400..e06807b 100644
--- modules/system/system.test
+++ modules/system/system.test
@@ -1081,6 +1081,134 @@ class SystemThemeFunctionalTest extends DrupalWebTestCase {
   }
 }
 
+/**
+ * Test entity propert wrapper.
+ */
+class DrupalPropertyWrapperTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Property wrappers',
+      'description' => 'Test using the drupal property wrappers.',
+      'group' => 'System',
+    );
+  }
+  
+  function setUp() {
+    parent::setUp('book');
+    // Clear the node load cache.
+    node_load_multiple(array(), array(), TRUE);
+  }
+
+  /**
+   * Creates a user and a node, then tests getting the properties.
+   */
+  function testEntityPropertyWrapper() {
+    $account = $this->drupalCreateUser();
+    // For testing sanitizing give the user a malicious user name
+    $account = user_save($account, array('name' => '<b>BadName</b>'));
+    $node = $this->drupalCreateNode(array('uid' => $account->uid, 'name' => $account->name, 'title' => '<b>Is it bold?<b>'));
+    
+    // First test without sanitizing.
+    $wrapper = drupal_get_property_wrapper('node', $node);
+    
+    $this->assertEqual($node->title, $wrapper->title, 'Getting property.');
+    $this->assertEqual($node->name, $wrapper->{'author-name'}, 'Getting property with getter callback.');
+    
+    // Test sanitized output.
+    $wrapper = drupal_get_property_wrapper('node', $node, array('sanitize' => TRUE));
+    
+    $this->assertEqual(check_plain($node->title), $wrapper->title, 'Getting sanitized property.');
+    $this->assertEqual(filter_xss($node->name), $wrapper->{'author-name'}, 'Getting sanitized property with getter callback.');
+    
+    // Test getting an not existing property
+    try {
+      echo $wrapper->dummy;
+      $this->fail('Getting an not existing property.');
+    }
+    catch (DrupalPropertyWrapperException $e) {
+      $this->pass('Getting an not existing property.');
+    }
+    
+    // Test setting.
+    $wrapper->title = 'test';
+    $this->assertEqual('test', $wrapper->title, 'Setting a property.');
+    try {
+      $wrapper->type = 'dummy';
+      $this->fail('Setting an unsupported property.');
+    }
+    catch (DrupalPropertyWrapperException $e) {
+      $this->pass('Setting an unsupported property.');
+    }
+    
+    // Test chaining
+    $this->assertEqual(check_plain($account->mail), $wrapper->author->mail, 'Testing chained usage.');
+    $this->assertEqual(filter_xss($account->name), $wrapper->author->name, 'Testing chained usage with callback and sanitizing.');
+    
+    // Test iterator
+    $type_info = entity_get_info('node');
+    $this->assertFalse(array_diff(array_keys($type_info['properties']), iterator_to_array($wrapper->getIterator())), 'Iterator is working.');
+
+  }
+  
+  /**
+   * Tests the property format wrapper.
+   */
+  function testPropertyFormatWrapper() {
+    $date = REQUEST_TIME;
+    $wrapper = drupal_get_property_wrapper('date', $date);
+    
+    $this->assertEqual(format_date($date), (string)$wrapper, 'Apply the default format.');
+    $this->assertEqual(format_date($date, 'long'), $wrapper->long, 'Apply a certain format.');
+
+    // Test applying a not existing format.
+    try {
+      echo $wrapper->dummy;
+      $this->fail('Testing a not existing format.');
+    }
+    catch (DrupalPropertyWrapperException $e) {
+      $this->pass('Testing a not existing format.');
+    }
+    
+    // Test iterator
+    $info = drupal_get_property_info('date');
+    $this->assertEqual(iterator_to_array($wrapper->getIterator()), array_keys($info['formats']), 'Iterator is working.');
+  }
+  
+  /**
+   * Test field API support.
+   */
+  function testFieldPropertyWrappers() {
+    // Test the body field and so if auto-adding bundle properties works.
+    $body = array();
+    $body[FIELD_LANGUAGE_NONE][0] = array('value' => '<b>The body.</b>', 'summary' => 'The summary.');
+    $node = $this->drupalCreateNode(array('body' => $body));
+    $wrapper = drupal_get_property_wrapper('node', $node, array('sanitize' => FALSE));
+    
+    $this->assertEqual('<b>The body.</b>', $wrapper->body->get(), 'Getting body property.');
+    $this->assertEqual("<p>The summary.</p>\n", $wrapper->body, "Default field formatter applied.");
+    $this->assertEqual("The body.\n", $wrapper->body->text_plain, "Specified field formatter applied.");
+
+    $wrapper->body = "<b>The second body.</b>";
+    $this->assertEqual("The second body.\n", $wrapper->body->text_plain, "Setting a field value and reading it again.");
+  }
+  
+  /**
+   * Test entity tags by using the 'book' default tag of the book module.
+   */
+  function testTaggedEntities() {
+    $node = $this->drupalCreateNode(array('title' => "Book 1", 'type' => 'book'));
+    $node2 = $this->drupalCreateNode(array('title' => "Book page", 'type' => 'book', 'book' => array('bid' => $node->nid)));
+    $node3 = $this->drupalCreateNode(array('title' => "Book page", 'type' => 'book', 'book' => array('bid' => $node2->nid)));
+
+    // Test whether the tag is detected.
+    $wrapper = drupal_get_property_wrapper('node', $node2);
+    $this->assertEqual("Book 1", $wrapper->book->title, "Entity is tagged.");
+
+    // Make sure the returned book has the book properties too.
+    $wrapper = drupal_get_property_wrapper('node', $node3);
+    $this->assertEqual("Book 1", $wrapper->book->book->title, "Retrieved entity is tagged.");
+  }
+}
 
 /**
  * Test the basic queue functionality.
@@ -1184,7 +1312,13 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
       'group' => 'System',
     );
   }
-
+  
+  function setUp() {
+    parent::setUp('book');
+    // Clear the node load cache.
+    node_load_multiple(array(), array(), TRUE);
+  }
+  
   /**
    * Creates a user and a node, then tests the tokens generated from them.
    */
@@ -1198,6 +1332,7 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
     $source  = '[node:title]';         // Title of the node we passed in
     $source .= '[node:author:name]';   // Node author's name
     $source .= '[node:created:since]'; // Time since the node was created
+    $source .= '[node:changed]';       // Last update time using the default format.
     $source .= '[current-user:name]';  // Current user's name
     $source .= '[user:name]';          // No user passed in, should be untouched
     $source .= '[date:short]';         // Short date format of REQUEST_TIME
@@ -1206,24 +1341,46 @@ class TokenReplaceTestCase extends DrupalWebTestCase {
     $target  = check_plain($node->title);
     $target .= check_plain($account->name);
     $target .= format_interval(REQUEST_TIME - $node->created, 2);
+    $target .= format_date($node->changed, 'medium');
     $target .= check_plain($user->name);
     $target .= '[user:name]';
     $target .= format_date(REQUEST_TIME, 'short');
     $target .= '[bogus:token]';
 
     $result = token_replace($source, array('node' => $node));
-
+    $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens replaced.'));
+    
     // Check that the results of token_generate are sanitized properly. This does NOT
     // test the cleanliness of every token -- just that the $sanitize flag is being
     // passed properly through the call stack and being handled correctly by a 'known'
     // token, [node:title].
-    $this->assertFalse(strcmp($target, $result), t('Basic placeholder tokens replaced.'));
-    
-    $raw_tokens = array('title' => '[node:title]');
-    $generated = token_generate('node', $raw_tokens, array('node' => $node));
+    $raw_tokens = array('node' => array('title' => '[node:title]'));
+    $generated = token_generate($raw_tokens, array('node' => $node));
     $this->assertFalse(strcmp($generated['[node:title]'], check_plain($node->title)), t('Token sanitized.'));
 
-    $generated = token_generate('node', $raw_tokens, array('node' => $node), array('sanitize' => FALSE));
+    $generated = token_generate($raw_tokens, array('node' => $node), array('sanitize' => FALSE));
     $this->assertFalse(strcmp($generated['[node:title]'], $node->title), t('Unsanitized token generated properly.'));
   }
+
+  
+  /**
+   * Tests using bundle/tag specific tokens and specifying custom data types.
+   */
+  function testContextualTokens() {
+    // Create the initial objects.
+    $body[FIELD_LANGUAGE_NONE][0] = array('value' => '<b>The body.</b>');
+    $node = $this->drupalCreateNode(array('title' => "Book 1", 'type' => 'book', 'body' => $body));
+    $node2 = $this->drupalCreateNode(array('title' => "Book page", 'type' => 'book', 'book' => array('bid' => $node->nid)));
+ 
+    // Test body, which is bundle specific.
+    $source  = '[node:body]';
+    // Test book tokens, which is specific to nodes tagged as "book".
+    $source .= '[bookpage:book:title]';
+
+    $target  = "<p>The body.</p>\n";
+    $target .= "Book 1";
+
+    $result = token_replace($source, array('node' => $node, 'bookpage' => $node2), array(), array('bookpage' => 'node'));
+    $this->assertFalse(strcmp($target, $result), t('Contextual placeholder tokens replaced.'));
+  }
 }
diff --git modules/system/system.tokens.inc modules/system/system.tokens.inc
deleted file mode 100644
index 186e74f..0000000
--- modules/system/system.tokens.inc
+++ /dev/null
@@ -1,308 +0,0 @@
-<?php
-// $Id: system.tokens.inc,v 1.2 2009/08/25 10:27:15 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens system-wide data.
- *
- * This file handles tokens for the global 'site' token type, as well as
- * 'date' and 'file' tokens.
- */
-
-/**
- * Implement hook_token_info().
- */
-function system_token_info() {
-  $types['site'] = array(
-    'name' => t("Site information"),
-    'description' => t("Tokens for site-wide settings and other global information."),
-  );
-  $types['date'] = array(
-    'name' => t("Dates"),
-    'description' => t("Tokens related to times and dates."),
-  );
-  $types['file'] = array(
-    'name' => t("Files"),
-    'description' => t("Tokens related to uploaded files."),
-    'needs-data' => 'file',
-  );
-
-  // Site-wide global tokens.
-  $site['name'] = array(
-    'name' => t("Name"),
-    'description' => t("The name of the site."),
-  );
-  $site['slogan'] = array(
-    'name' => t("Slogan"),
-    'description' => t("The slogan of the site."),
-  );
-  $site['mission'] = array(
-    'name' => t("Mission"),
-    'description' => t("The optional 'mission' of the site."),
-  );
-  $site['mail'] = array(
-    'name' => t("Email"),
-    'description' => t("The administrative email address for the site."),
-  );
-  $site['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the site's front page."),
-  );
-  $site['login-url'] = array(
-    'name' => t("Login page"),
-    'description' => t("The URL of the site's login page."),
-  );
-
-  // Date related tokens.
-  $date['short'] = array(
-    'name' => t("Short format"),
-    'description' => t("A date in 'short' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'short'))),
-  );
-  $date['medium'] = array(
-    'name' => t("Medium format"),
-    'description' => t("A date in 'medium' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'medium'))),
-  );
-  $date['long'] = array(
-    'name' => t("Long format"),
-    'description' => t("A date in 'long' format. (%date)", array('%date' => format_date(REQUEST_TIME, 'long'))),
-  );
-  $date['custom'] = array(
-    'name' => t("Custom format"),
-    'description' => t("A date in a custom format. See !php-date for details.", array('!php-date' => l(t('the PHP documentation'), 'http://php.net/manual/en/function.date.php'))),
-  );
-  $date['since'] = array(
-    'name' => t("Time-since"),
-    'description' => t("A data in 'time-since' format. (%date)", array('%date' => format_interval(REQUEST_TIME - 360, 2))),
-  );
-  $date['raw'] = array(
-    'name' => t("Raw timestamp"),
-    'description' => t("A date in UNIX timestamp format (%date)", array('%date' => REQUEST_TIME)),
-  );
-
-
-  // File related tokens.
-  $file['fid'] = array(
-    'name' => t("File ID"),
-    'description' => t("The unique ID of the uploaded file."),
-  );
-  $file['uid'] = array(
-    'name' => t("User ID"),
-    'description' => t("The unique ID of the user who owns the file."),
-  );
-  $file['nid'] = array(
-    'name' => t("Node ID"),
-    'description' => t("The unique ID of the node the file is attached to."),
-  );
-  $file['name'] = array(
-    'name' => t("File name"),
-    'description' => t("The name of the file on disk."),
-  );
-  $file['description'] = array(
-    'name' => t("Description"),
-    'description' => t("An optional human-readable description of the file."),
-  );
-  $file['path'] = array(
-    'name' => t("Path"),
-    'description' => t("The location of the file on disk."),
-  );
-  $file['mime'] = array(
-    'name' => t("MIME type"),
-    'description' => t("The MIME type of the file."),
-  );
-  $file['size'] = array(
-    'name' => t("File size"),
-    'description' => t("The size of the file, in kilobytes."),
-  );
-  $file['path'] = array(
-    'name' => t("URL"),
-    'description' => t("The web-accessible URL for the file."),
-  );
-  $file['timestamp'] = array(
-    'name' => t("Timestamp"),
-    'description' => t("The date the file was most recently changed."),
-    'type' => 'date',
-  );
-  $file['node'] = array(
-    'name' => t("Node"),
-    'description' => t("The node the file is attached to."),
-    'type' => 'date',
-  );
-  $file['owner'] = array(
-    'name' => t("Owner"),
-    'description' => t("The user who originally uploaded the file."),
-    'type' => 'user',
-  );
-
-  return array(
-    'types' => $types,
-    'tokens' => array(
-      'site' => $site,
-      'date' => $date,
-      'file' => $file,
-    ),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function system_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $url_options = array('absolute' => TRUE);
-  if (isset($language)) {
-    $url_options['language'] = $language;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'site') {
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'name':
-          $site_name = variable_get('site_name', 'Drupal');
-          $replacements[$original] = $sanitize ? check_plain($site_name) : $site_name;
-          break;
-
-        case 'slogan':
-          $slogan = variable_get('site_slogan', '');
-          $replacements[$original] = $sanitize ? check_plain($slogan) : $slogan;
-          break;
-
-        case 'mission':
-          $mission = variable_get('site_mission', '');
-          $replacements[$original] = $sanitize ? filter_xss($mission) : $mission;
-          break;
-
-        case 'mail':
-          $replacements[$original] = variable_get('site_mail', '');
-          break;
-
-        case 'url':
-          $replacements[$original] = url('<front>', $url_options);
-          break;
-
-        case 'login-url':
-          $replacements[$original] = url('user', $url_options);
-          break;
-      }
-    }
-  }
-
-  elseif ($type == 'date') {
-    if (empty($data['date'])) {
-      $date = REQUEST_TIME;
-    }
-    else {
-      $date = $data['date'];
-    }
-    $langcode = (isset($language) ? $language->language : NULL);
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'raw':
-          $replacements[$original] = filter_xss($date);
-          break;
-
-        case 'short':
-          $replacements[$original] = format_date($date, 'short', '', NULL, $langcode);
-          break;
-
-        case 'medium':
-          $replacements[$original] = format_date($date, 'medium', '', NULL, $langcode);
-          break;
-
-        case 'long':
-          $replacements[$original] = format_date($date, 'long', '', NULL, $langcode);
-          break;
-
-        case 'since':
-          $replacements[$original] = format_interval((REQUEST_TIME - $date), 2, $langcode);
-          break;
-      }
-    }
-
-    if ($created_tokens = token_find_with_prefix($tokens, 'custom')) {
-      foreach ($created_tokens as $name => $original) {
-        $replacements[$original] = format_date($date, 'custom', $name, NULL, $langcode);
-      }
-    }
-  }
-
-  elseif ($type == 'file' && !empty($data['file'])) {
-    $file = $data['file'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Basic keys and values.
-        case 'fid':
-          $replacements[$original] = $file->fid;
-          break;
-
-        case 'uid':
-          $replacements[$original] = $file->uid;
-          break;
-
-        case 'nid':
-          $replacements[$original] = $file->nid;
-          break;
-
-        // Essential file data
-        case 'name':
-          $replacements[$original] = $sanitize ? check_plain($file->filename) : $file->filename;
-          break;
-
-        case 'description':
-          $replacements[$original] = $sanitize ? filter_xss($file->description) : $file->description;
-          break;
-
-        case 'path':
-          $replacements[$original] = $sanitize ? filter_xss($file->filepath) : $file->filepath;
-          break;
-
-        case 'mime':
-          $replacements[$original] = $sanitize ? filter_xss($file->filemime) : $file->filemime;
-          break;
-
-        case 'size':
-          $replacements[$original] = format_size($file->filesize);
-          break;
-
-        case 'url':
-          $replacements[$original] = url(file_create_url($file->filepath), $url_options);
-          break;
-
-        // These tokens are default variations on the chained tokens handled below.
-        case 'node':
-          if ($nid = $file->nid) {
-            $node = node_load($file->nid);
-            $replacements[$original] = $sanitize ? filter_xss($node->title) : $node->title;
-          }
-          break;
-
-        case 'timestamp':
-          $replacements[$original] = format_date($file->timestamp, 'medium', '', NULL, (isset($language) ? $language->language : NULL));
-          break;
-
-        case 'owner':
-          $account = user_load($file->uid);
-          $replacements[$original] = $sanitize ? filter_xss($user->name) : $user->name;
-          break;
-      }
-    }
-
-    if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
-      $node = node_load($file->nid);
-      $replacements += token_generate('node', $node_tokens, array('node' => $node), $language, $sanitize);
-    }
-
-    if ($date_tokens = token_find_with_prefix($tokens, 'timestamp')) {
-      $replacements += token_generate('date', $date_tokens, array('date' => $file->timestamp), $language, $sanitize);
-    }
-
-    if (($owner_tokens = token_find_with_prefix($tokens, 'owner')) && $account = user_load($file->uid)) {
-      $replacements += token_generate('user', $owner_tokens, array('user' => $account), $language, $sanitize);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/taxonomy/taxonomy.test modules/taxonomy/taxonomy.test
index ae792ec..1dd88bf 100644
--- modules/taxonomy/taxonomy.test
+++ modules/taxonomy/taxonomy.test
@@ -276,27 +276,27 @@ class TaxonomyVocabularyUnitTest extends TaxonomyWebTestCase {
     // Fetch all of the vocabularies using taxonomy_get_vocabularies().
     // Confirm that the vocabularies are ordered by weight.
     $vocabularies = taxonomy_get_vocabularies();
-    $this->assertEqual(array_shift($vocabularies), $vocabulary1, t('Vocabulary was found in the vocabularies array.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary2, t('Vocabulary was found in the vocabularies array.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary3, t('Vocabulary was found in the vocabularies array.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary was found in the vocabularies array.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary was found in the vocabularies array.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary was found in the vocabularies array.'));
 
     // Fetch the vocabularies with taxonomy_vocabulary_load_multiple(), specifying IDs.
     // Ensure they are returned in the same order as the original array.
     $vocabularies = taxonomy_vocabulary_load_multiple(array($vocabulary3->vid, $vocabulary2->vid, $vocabulary1->vid));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary3, t('Vocabulary loaded successfully by ID.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary2, t('Vocabulary loaded successfully by ID.'));
-    $this->assertEqual(array_shift($vocabularies), $vocabulary1, t('Vocabulary loaded successfully by ID.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary3->vid, t('Vocabulary loaded successfully by ID.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary2->vid, t('Vocabulary loaded successfully by ID.'));
+    $this->assertEqual(array_shift($vocabularies)->vid, $vocabulary1->vid, t('Vocabulary loaded successfully by ID.'));
 
     // Fetch vocabulary 1 by name.
-    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name.'));
+    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array(), array('name' => $vocabulary1->name)))->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name.'));
 
     // Fetch vocabulary 1 by name and ID.
-    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name))) == $vocabulary1, t('Vocabulary loaded successfully by name and ID.'));
+    $this->assertTrue(current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('name' => $vocabulary1->name)))->vid == $vocabulary1->vid, t('Vocabulary loaded successfully by name and ID.'));
 
     // Fetch vocabulary 1 with specified node type.
     entity_get_controller('taxonomy_vocabulary')->resetCache();
     $vocabulary_node_type = current(taxonomy_vocabulary_load_multiple(array($vocabulary1->vid), array('type' => 'article')));
-    $this->assertEqual($vocabulary_node_type, $vocabulary1, t('Vocabulary with specified node type loaded successfully.'));
+    $this->assertEqual($vocabulary_node_type->vid, $vocabulary1->vid, t('Vocabulary with specified node type loaded successfully.'));
   }
 }
 
diff --git modules/upload/upload.info modules/upload/upload.info
index 0f3d6a5..ed1f562 100644
--- modules/upload/upload.info
+++ modules/upload/upload.info
@@ -7,5 +7,4 @@ core = 7.x
 files[] = upload.module
 files[] = upload.admin.inc
 files[] = upload.install
-files[] = upload.test
-files[] = upload.tokens.inc
+files[] = upload.test
\ No newline at end of file
diff --git modules/upload/upload.module modules/upload/upload.module
index 5e46cc3..29fa819 100644
--- modules/upload/upload.module
+++ modules/upload/upload.module
@@ -634,6 +634,50 @@ function theme_upload_form_new($form) {
 }
 
 /**
+ * Implement hook_entity_info_alter().
+ */
+function upload_entity_info_alter(&$entity_info) {
+  $entity_info['file']['tags']['upload'] = array(
+    'label' => t('File upload'),
+  );
+  $properties = &$entity_info['file']['tags']['upload']['properties'];
+  
+  $properties['nid'] = array(
+    'label' => t("Node ID"),
+    'description' => t("The unique ID of the node the file is attached to."),
+  );
+  $properties['node'] = array(
+    'label' => t("Node"),
+    'description' => t("The node the file is attached to."),
+    'type' => 'node',
+    'getter callback' => 'upload_file_get_node',
+  );
+  $properties['description'] = array(
+    'label' => t("Description"),
+    'description' => t("An optional human-readable description of the file."),
+  );
+
+  $entity_info['node']['properties'] = array(
+    'upload' => array(
+      'label' => t('File attachment'),
+      'description' => t('The first file attached to a node, if one exists.'),
+      'type' => 'file',
+      'tags' => array('upload'),
+    ),
+  );
+}
+
+/**
+ * Gets the node associated to an uploaded file.
+ *
+ * @return
+ *   The associated node object.
+ */
+function upload_file_get_node($file) {
+  return node_load($file->nid);
+}
+
+/**
  * Menu-callback for JavaScript-based uploads.
  */
 function upload_js() {
diff --git modules/upload/upload.tokens.inc modules/upload/upload.tokens.inc
deleted file mode 100644
index bb7488f..0000000
--- modules/upload/upload.tokens.inc
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-// $Id: upload.tokens.inc,v 1.1 2009/08/19 20:19:37 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for uploaded files attached to nodes.
- */
-
-/**
- * Implement hook_token_info().
- */
-function upload_token_info() {
-  $results['tokens']['node'] = array(
-  'upload' => array(
-    'name' => t('File attachment'),
-    'description' => t('The first file attached to a node, if one exists.'),
-    'type' => 'file',
-    )
-  );
-  return $results;
-}
-
-/**
- * Implement hook_tokens().
- */
-function upload_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  $replacements = array();
-
-  if ($type == 'node' && !empty($data['node'])) {
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      if ($name == 'upload') {
-        $upload = array_shift($node->files);
-        $replacements[$original] = file_create_url($upload->filepath);
-      }
-    }
-
-    if (($upload_tokens = token_find_with_prefix($tokens, 'upload')) && !empty($node->files) && $upload = array_shift($node->files)) {
-      $replacements += token_generate('file', $upload_tokens, array('file' => $upload), $options);
-    }
-  }
-
-  return $replacements;
-}
diff --git modules/user/user.entity.inc modules/user/user.entity.inc
new file mode 100644
index 0000000..f4d10e7
--- /dev/null
+++ modules/user/user.entity.inc
@@ -0,0 +1,74 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides info about the user entity.
+ */
+
+/**
+ * Implement hook_entity_info().
+ */
+function user_entity_info() {
+  $return = array(
+    'user' => array(
+      'label' => t('User'),
+      'controller class' => 'UserController',
+      'base table' => 'users',
+      'fieldable' => TRUE,
+      'object keys' => array(
+        'id' => 'uid',
+      ),
+      'bundles' => array(
+        'user' => array(
+          'label' => t('User'),
+          'admin' => array(
+            'path' => 'admin/config/people/accounts',
+            'access arguments' => array('administer users'),
+          ),
+        ),
+      ),
+    ),
+  );
+  // Add meta-data about the user properties.
+  $properties = &$return['user']['properties'];
+
+  $properties['uid'] = array(
+    'label' => t("User ID"),
+    'type' => 'integer',
+    'description' => t("The unique ID of the user account."),
+  );
+  $properties['name'] = array(
+    'label' => t("Name"),
+    'description' => t("The login name of the user account."),
+    'getter callback' => 'user_get_properties',
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['mail'] = array(
+    'label' => t("Email"),
+    'description' => t("The email address of the user account."),
+    'setter callback' => 'drupal_property_verbatim_set',
+  );
+  $properties['url'] = array(
+    'label' => t("URL"),
+    'description' => t("The URL of the account profile page."),
+    'getter callback' => 'user_get_properties',
+  );
+  $properties['edit-url'] = array(
+    'label' => t("Edit URL"),
+    'description' => t("The url of the account edit page."),
+    'getter callback' => 'user_get_properties',
+  );
+  $properties['login'] = array(
+    'label' => t("Last login"),
+    'description' => t("The date the user last logged in to the site."),
+    'type' => 'date',
+  );
+  $properties['created'] = array(
+    'label' => t("Created"),
+    'description' => t("The date the user account was created."),
+    'type' => 'date',
+  );
+  return $return;
+}
+
diff --git modules/user/user.info modules/user/user.info
index 54e288e..a828f42 100644
--- modules/user/user.info
+++ modules/user/user.info
@@ -9,5 +9,5 @@ files[] = user.admin.inc
 files[] = user.pages.inc
 files[] = user.install
 files[] = user.test
-files[] = user.tokens.inc
+files[] = user.entity.inc
 required = TRUE
diff --git modules/user/user.module modules/user/user.module
index 60da692..f1d121d 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -84,30 +84,21 @@ function user_theme() {
 }
 
 /**
- * Implement hook_entity_info().
+ * Callback for getting user properties.
+ * @see user_entity_info()
  */
-function user_entity_info() {
-  $return = array(
-    'user' => array(
-      'label' => t('User'),
-      'controller class' => 'UserController',
-      'base table' => 'users',
-      'fieldable' => TRUE,
-      'object keys' => array(
-        'id' => 'uid',
-      ),
-      'bundles' => array(
-        'user' => array(
-          'label' => t('User'),
-          'admin' => array(
-            'path' => 'admin/config/people/accounts',
-            'access arguments' => array('administer users'),
-          ),
-        ),
-      ),
-    ),
-  );
-  return $return;
+function user_get_properties($account, array $options, $name, $entity_type) {
+  switch ($name) {
+    case 'name':
+      $name = ($account->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $account->name;
+      return $options['sanitize'] ? filter_xss($name) : $name;
+
+    case 'url':
+      return url("user/$account->uid", $options + array('absolute' => TRUE));
+
+    case 'edit-url':
+      return url("user/$account->uid/edit", $options + array('absolute' => TRUE));
+  }
 }
 
 /**
diff --git modules/user/user.tokens.inc modules/user/user.tokens.inc
deleted file mode 100644
index e55a442..0000000
--- modules/user/user.tokens.inc
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-// $Id: user.tokens.inc,v 1.1 2009/08/19 20:19:37 dries Exp $
-
-/**
- * @file
- * Builds placeholder replacement tokens for user-related data.
- */
-
-/**
- * Implement hook_token_info().
- */
-function user_token_info() {
-  $types['user'] = array(
-    'name' => t('Users'),
-    'description' => t('Tokens related to individual user accounts.'),
-    'needs-data' => 'user',
-  );
-  $types['current-user'] = array(
-    'name' => t('Current user'),
-    'description' => t('Tokens related to the currently logged in user.'),
-    'type' => 'user',
-  );
-
-  $user['uid'] = array(
-    'name' => t('User ID'),
-    'description' => t("The unique ID of the user account."),
-  );
-  $user['name'] = array(
-    'name' => t("Name"),
-    'description' => t("The login name of the user account."),
-  );
-  $user['mail'] = array(
-    'name' => t("Email"),
-    'description' => t("The email address of the user account."),
-  );
-  $user['url'] = array(
-    'name' => t("URL"),
-    'description' => t("The URL of the account profile page."),
-  );
-  $user['edit-url'] = array(
-    'name' => t("Edit URL"),
-    'description' => t("The url of the account edit page."),
-  );
-  $user['last-login'] = array(
-    'name' => t("Last login"),
-    'description' => t("The date the user last logged in to the site."),
-    'type' => 'date',
-  );
-  $user['created'] = array(
-    'name' => t("Created"),
-    'description' => t("The date the user account was created."),
-    'type' => 'date',
-  );
-
-  return array(
-    'types' => array('user' => $types),
-    'tokens' => array('user' => $user),
-  );
-}
-
-/**
- * Implement hook_tokens().
- */
-function user_tokens($type, $tokens, array $data = array(), array $options = array()) {
-  global $user;
-  $url_options = array('absolute' => TRUE);
-  if (isset($options['language'])) {
-    $url_options['language'] = $language;
-    $language_code = $language->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-  $sanitize = !empty($options['sanitize']);
-
-  $replacements = array();
-
-  if ($type == 'user' && !empty($data['user'])) {
-    $account = $data['user'];
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Basic user account information.
-        case 'uid':
-          $replacements[$original] = $account->uid;
-          break;
-
-        case 'name':
-          $name = ($account->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $account->name;
-          $replacements[$original] = $sanitize ? filter_xss($name) : $name;
-          break;
-
-        case 'mail':
-          $replacements[$original] = $sanitize ? check_plain($account->mail) : $account->mail;
-          break;
-
-        case 'url':
-          $replacements[$original] = url("user/$account->uid", $url_options);
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = url("user/$account->uid/edit", $url_options);
-          break;
-
-        // These tokens are default variations on the chained tokens handled below.
-        case 'last-login':
-          $replacements[$original] = format_date($account->login, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'created':
-          $replacements[$original] = format_date($account->created, 'medium', '', NULL, $language_code);
-          break;
-      }
-    }
-
-    if ($login_tokens = token_find_with_prefix($tokens, 'last-login')) {
-      $replacements += token_generate('date', $login_tokens, array('date' => $account->login), $options);
-    }
-
-    if ($registered_tokens = token_find_with_prefix($tokens, 'created')) {
-      $replacements += token_generate('date', $registered_tokens, array('date' => $account->created), $options);
-    }
-  }
-  if ($type == 'current-user') {
-    global $user;
-    $replacements += token_generate('user', $tokens, array('user' => $user), $options);
-  }
-
-  return $replacements;
-}
