Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1140
diff -u -p -r1.1140 common.inc
--- includes/common.inc	31 Mar 2010 19:10:39 -0000	1.1140
+++ includes/common.inc	1 Apr 2010 18:11:17 -0000
@@ -1929,6 +1929,10 @@ function format_username($account) {
  *     dependent URL requires so.
  *   - 'prefix': Only used internally, to modify the path when a language
  *     dependent URL requires so.
+ *   - 'cache': Whether to statically cache the result for the passed in $path
+ *     and $options. This is most commonly TRUE for entity URIs, for which url()
+ *     is likely to be called multiple times. By default, it is not TRUE for
+ *     other URIs to save on cache miss overhead.
  *
  * @return
  *   A string containing a URL to the given path.
@@ -1937,6 +1941,35 @@ function format_username($account) {
  * alternative than url().
  */
 function url($path = NULL, array $options = array()) {
+  // Optimization: sometimes url() is called many times for the same path and
+  // options combination.
+  if (!empty($options['cache'])) {
+    // Use the advanced drupal_static() pattern, since this is called very often.
+    static $drupal_static_fast;
+    if (!isset($drupal_static_fast)) {
+      $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
+    }
+    $cache = &$drupal_static_fast['cache'];
+
+    // l() adds these options keys, but they are not used by url(), so remove
+    // them to optimize the cache.
+    unset($options['attributes']);
+    unset($options['html']);
+
+    // $options['cache'] does not affect the resulting URL so it too can be
+    // removed, both to optimize $cache_key and to allow url() to be called
+    // during a cache miss without causing infinite recursion.
+    unset($options['cache']);
+    $cache_key = $path;
+    if (!empty($options)) {
+      $cache_key .= ':' . serialize($options);
+    }
+    if (!isset($cache[$cache_key])) {
+      $cache[$cache_key] = url($path, $options);
+    }
+    return $cache[$cache_key];
+  }
+
   // Merge in defaults.
   $options += array(
     'fragment' => '',
@@ -1992,19 +2025,6 @@ function url($path = NULL, array $option
   }
 
   global $base_url, $base_secure_url, $base_insecure_url;
-  // Use the advanced drupal_static() pattern, since this is called very often.
-  static $drupal_static_fast;
-  if (!isset($drupal_static_fast)) {
-    $drupal_static_fast['script'] = &drupal_static(__FUNCTION__);
-  }
-  $script = &$drupal_static_fast['script'];
-
-  if (!isset($script)) {
-    // On some web servers, such as IIS, we can't omit "index.php". So, we
-    // generate "index.php?q=foo" instead of "?q=foo" on anything that is not
-    // Apache.
-    $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : '';
-  }
 
   // The base_url might be rewritten from the language rewrite in domain mode.
   if (!isset($options['base_url'])) {
@@ -2061,6 +2081,16 @@ function url($path = NULL, array $option
       $query += $options['query'];
     }
     if ($query) {
+      // On some web servers, such as IIS, we can't omit "index.php". So, we
+      // generate "index.php?q=foo" instead of "?q=foo" on anything that is not
+      // Apache. strpos() is fast, so there is no performance benefit to
+      // statically caching its result.
+      // @todo This needs to be re-evaluated with modern web servers. Since we
+      //   do not add $script when there aren't query parameters, we're already
+      //   assuming that index.php is setup as a default document on the web
+      //   server. If that's the case, it should be possible to omit "index.php"
+      //   even when there are query parameters: http://drupal.org/node/437228.
+      $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : '';
       return $base . $script . '?' . drupal_http_build_query($query) . $options['fragment'];
     }
     else {
@@ -6430,11 +6460,49 @@ function entity_prepare_view($entity_typ
  *   uri of its own.
  */
 function entity_uri($entity_type, $entity) {
-  $info = entity_get_info($entity_type);
-  if (isset($info['uri callback']) && function_exists($info['uri callback'])) {
-    return $info['uri callback']($entity) + array('options' => array());
+  // This check enables the URI of an entity to be easily overridden from what
+  // the callback for the entity type or bundle would return, and it helps
+  // minimize performance overhead when entity_uri() is called multiple times
+  // for the same entity.
+  if (!isset($entity->uri)) {
+    $info = entity_get_info($entity_type);
+    list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+    // A bundle-specific callback takes precedence over the generic one for the
+    // entity type.
+    if (isset($info['bundles'][$bundle]['uri callback'])) {
+      $uri_callback = $info['bundles'][$bundle]['uri callback'];
+    }
+    elseif (isset($info['uri callback'])) {
+      $uri_callback = $info['uri callback'];
+    }
+    else {
+      $uri_callback = NULL;
+    }
+
+    // Invoke the callback to get the URI. If there is no callback, set the
+    // entity's 'uri' property to FALSE to indicate that it is known to not have
+    // a URI.
+    if (isset($uri_callback) && function_exists($uri_callback)) {
+      $entity->uri = $uri_callback($entity);
+      if (!isset($entity->uri['options'])) {
+        $entity->uri['options'] = array();
+      }
+      // By default, url() does not statically cache its results, because the
+      // risk of cache miss overhead outweighs the benefit of cache hit savings.
+      // Entity URIs, however, are more likely to pass through url() multiple
+      // times, especially with RDF enabled.
+      if (!isset($entity->uri['options']['cache'])) {
+        $entity->uri['options']['cache'] = TRUE;
+      }
+    }
+    else {
+      $entity->uri = FALSE;
+    }
   }
+  return $entity->uri ? $entity->uri : NULL;
 }
+
 /**
  * Invokes entity insert/update hooks.
  *
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.859
diff -u -p -r1.859 comment.module
--- modules/comment/comment.module	31 Mar 2010 11:49:50 -0000	1.859
+++ modules/comment/comment.module	1 Apr 2010 18:11:18 -0000
@@ -2156,8 +2156,10 @@ function template_preprocess_comment(&$v
   $variables['new']       = !empty($comment->new) ? t('new') : '';
   $variables['picture']   = theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', array('account' => $comment)) : '';
   $variables['signature'] = $comment->signature;
-  $variables['title']     = l($comment->subject, 'comment/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
-  $variables['permalink'] = l('#', 'comment/' . $comment->cid, array('fragment' => "comment-$comment->cid"));
+
+  $uri = entity_uri('comment', $comment);
+  $variables['title']     = l($comment->subject, $uri['path'], $uri['options']);
+  $variables['permalink'] = l('#', $uri['path'], $uri['options']);
 
   // Preprocess fields.
   field_attach_preprocess('comment', $comment, $variables['elements'], $variables);
Index: modules/forum/forum.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/forum/forum.module,v
retrieving revision 1.559
diff -u -p -r1.559 forum.module
--- modules/forum/forum.module	26 Mar 2010 17:14:45 -0000	1.559
+++ modules/forum/forum.module	1 Apr 2010 18:11:18 -0000
@@ -219,6 +219,38 @@ function forum_init() {
 }
 
 /**
+ * Implements hook_entity_info().
+ */
+function forum_entity_info() {
+  // Take over URI constuction for taxonomy terms that are forums.
+  $return = array();
+  if ($vid = variable_get('forum_nav_vocabulary', 0)) {
+    // Within hook_entity_info(), we can't invoke entity_load() as that would
+    // cause infinite recursion, so we call taxonomy_vocabulary_get_names()
+    // instead of taxonomy_vocabulary_load(). All we need is the machine name
+    // of $vid, so retrieving and iterating all the vocabulary names is somewhat
+    // inefficient, but entity info is cached across page requests, and an
+    // iteration of all vocabularies once per cache clearing isn't a big deal.
+    // It's done as part of taxonomy_entity_info() anyway.
+    foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
+      if ($vid == $vocabulary->vid) {
+        $return['taxonomy_term']['bundles'][$machine_name]['uri callback'] = 'forum_uri';
+      }
+    }
+  }
+  return $return;
+}
+
+/**
+ * Entity URI callback.
+ */
+function forum_uri($forum) {
+  return array(
+    'path' => 'forum/' . $forum->tid,
+  );
+}
+
+/**
  * Check whether a content type can be used in a forum.
  *
  * @param $node
@@ -682,18 +714,6 @@ function forum_form($node, $form_state) 
 }
 
 /**
- * Implements hook_url_outbound_alter().
- */
-function forum_url_outbound_alter(&$path, &$options, $original_path) {
-  if (preg_match('!^taxonomy/term/(\d+)!', $path, $matches)) {
-    $term = taxonomy_term_load($matches[1]);
-    if ($term && $term->vocabulary_machine_name == 'forums') {
-      $path = 'forum/' . $matches[1];
-    }
-  }
-}
-
-/**
  * Returns a list of all forums for a given taxonomy id
  *
  * Forum objects contain the following fields
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1253
diff -u -p -r1.1253 node.module
--- modules/node/node.module	31 Mar 2010 13:55:25 -0000	1.1253
+++ modules/node/node.module	1 Apr 2010 18:11:19 -0000
@@ -1340,7 +1340,9 @@ function template_preprocess_node(&$vari
 
   $variables['date']      = format_date($node->created);
   $variables['name']      = theme('username', array('account' => $node));
-  $variables['node_url']  = url('node/' . $node->nid);
+
+  $uri = entity_uri('node', $node);
+  $variables['node_url']  = url($uri['path'], $uri['options']);
   $variables['node_title'] = check_plain($node->title);
   $variables['page']      = node_is_page($node);
 
@@ -1581,8 +1583,9 @@ function node_search_execute($keys = NUL
 
     $extra = module_invoke_all('node_search_result', $node);
 
+    $uri = entity_uri('node', $node);
     $results[] = array(
-      'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
+      'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE))),
       'type' => check_plain(node_type_get_name($node)),
       'title' => $node->title,
       'user' => theme('username', array('account' => $node)),
@@ -2500,10 +2503,11 @@ function node_page_default() {
  */
 function node_page_view($node) {
   drupal_set_title($node->title);
+  $uri = entity_uri('node', $node);
   // Set the node path as the canonical URL to prevent duplicate content.
-  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)), TRUE);
+  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
   // Set the non-aliased path as a default shortlink.
-  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url('node/' . $node->nid, array('alias' => TRUE))), TRUE);
+  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
   return node_show($node);
 }
 
Index: modules/rdf/rdf.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/rdf/rdf.module,v
retrieving revision 1.33
diff -u -p -r1.33 rdf.module
--- modules/rdf/rdf.module	27 Mar 2010 05:52:50 -0000	1.33
+++ modules/rdf/rdf.module	1 Apr 2010 18:11:19 -0000
@@ -534,29 +534,31 @@ function rdf_preprocess_field(&$variable
  * Implements MODULE_preprocess_HOOK().
  */
 function rdf_preprocess_user_profile(&$variables) {
-  // Adds RDFa markup to the user profile page. Fields displayed in this page
-  // will automatically describe the user.
   $account = $variables['elements']['#account'];
+  $uri = entity_uri('user', $account);
+
+  // Adds RDFa markup to the user profile page. Fields displayed in this page
+  // will automatically describe the user.  
   if (!empty($account->rdf_mapping['rdftype'])) {
     $variables['attributes_array']['typeof'] = $account->rdf_mapping['rdftype'];
-    $variables['attributes_array']['about'] = url('user/' . $account->uid);
+    $variables['attributes_array']['about'] = url($uri['path'], $uri['options']);
   }
   // Adds the relationship between the sioc:User and the foaf:Person who holds
   // the account.
   $account_holder_meta = array(
     '#tag' => 'meta',
     '#attributes' => array(
-      'about' => url('user/' . $account->uid, array('fragment' => 'me')),
+      'about' => url($uri['path'], array_merge($uri['options'], array('fragment' => 'me'))),
       'typeof' => array('foaf:Person'),
       'rel' => array('foaf:account'),
-      'resource' => url('user/' . $account->uid),
+      'resource' => url($uri['path'], $uri['options']),
     ),
   );
   // Adds the markup for username.
   $username_meta = array(
     '#tag' => 'meta',
     '#attributes' => array(
-      'about' => url('user/' . $account->uid),
+      'about' => url($uri['path'], $uri['options']),
       'property' => $account->rdf_mapping['name']['predicates'],
       'content' => $account->name,
     )
@@ -629,7 +631,8 @@ function rdf_preprocess_comment(&$variab
     // Adds RDFa markup to the comment container. The about attribute specifies
     // the URI of the resource described within the HTML element, while the
     // typeof attribute indicates its RDF type (e.g. sioc:Post, etc.).
-    $variables['attributes_array']['about'] = url('comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid));
+    $uri = entity_uri('comment', $comment);
+    $variables['attributes_array']['about'] = url($uri['path'], $uri['options']);
     $variables['attributes_array']['typeof'] = $comment->rdf_mapping['rdftype'];
   }
 
Index: modules/simpletest/tests/path.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/path.test,v
retrieving revision 1.4
diff -u -p -r1.4 path.test
--- modules/simpletest/tests/path.test	29 Jan 2010 22:40:41 -0000	1.4
+++ modules/simpletest/tests/path.test	1 Apr 2010 18:11:19 -0000
@@ -177,11 +177,10 @@ class UrlAlterFunctionalTest extends Dru
     $this->assertUrlInboundAlter("user/$uid", "user/$uid");
     $this->assertUrlOutboundAlter("user/$uid", "user/$uid");
 
-    // Test that 'forum' is altered to 'community' correctly.
+    // Test that 'forum' is altered to 'community' correctly, both at the root
+    // level and for a specific existing forum.
     $this->assertUrlInboundAlter('community', 'forum');
     $this->assertUrlOutboundAlter('forum', 'community');
-
-    // Add a forum to test url altering.
     $forum_vid = db_query("SELECT vid FROM {taxonomy_vocabulary} WHERE module = 'forum'")->fetchField();
     $tid = db_insert('taxonomy_term_data')
       ->fields(array(
@@ -189,15 +188,8 @@ class UrlAlterFunctionalTest extends Dru
         'vid' => $forum_vid,
       ))
       ->execute();
-
-    // Test that a existing forum URL is altered.
     $this->assertUrlInboundAlter("community/$tid", "forum/$tid");
-    $this->assertUrlOutboundAlter("taxonomy/term/$tid", "community/$tid");
-
-    // Test that a non-existant forum URL is not altered.
-    $tid++;
-    $this->assertUrlInboundAlter("taxonomy/term/$tid", "taxonomy/term/$tid");
-    $this->assertUrlOutboundAlter("taxonomy/term/$tid", "taxonomy/term/$tid");
+    $this->assertUrlOutboundAlter("forum/$tid", "community/$tid");
   }
 
   /**
Index: modules/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.148
diff -u -p -r1.148 system.api.php
--- modules/system/system.api.php	27 Mar 2010 18:41:14 -0000	1.148
+++ modules/system/system.api.php	1 Apr 2010 18:11:20 -0000
@@ -100,6 +100,10 @@ function hook_hook_info() {
  *     Keys are bundles machine names, as found in the objects' 'bundle'
  *     property (defined in the 'entity keys' entry above). Elements:
  *     - label: The human-readable name of the bundle.
+ *     - uri callback: Same as the 'uri callback' key documented above for the
+ *       entity type, but for the bundle only. When determining the URI of an
+ *       entity, if a 'uri callback' is defined for both the entity type and
+ *       the bundle, the one for the bundle is used.
  *     - admin: An array of information that allows Field UI pages to attach
  *       themselves to the existing administration pages for the bundle.
  *       Elements:
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.583
diff -u -p -r1.583 taxonomy.module
--- modules/taxonomy/taxonomy.module	28 Mar 2010 11:39:35 -0000	1.583
+++ modules/taxonomy/taxonomy.module	1 Apr 2010 18:11:20 -0000
@@ -609,7 +609,8 @@ function template_preprocess_taxonomy_te
   $variables['term'] = $variables['elements']['#term'];
   $term = $variables['term'];
 
-  $variables['term_url']  = url('taxonomy/term/' . $term->tid);
+  $uri = entity_uri('taxonomy_term', $term);
+  $variables['term_url']  = url($uri['path'], $uri['options']);
   $variables['term_name'] = check_plain($term->name);
   $variables['page']      = taxonomy_term_is_page($term);
 
@@ -1207,10 +1208,12 @@ function taxonomy_field_formatter_view($
     case 'taxonomy_term_reference_link':
       foreach ($items as $delta => $item) {
         $term = $item['taxonomy_term'];
+        $uri = entity_uri('taxonomy_term', $term);
         $element[$delta] = array(
           '#type' => 'link',
           '#title' => $term->name,
-          '#href' => 'taxonomy/term/' . $term->tid,
+          '#href' => $uri['path'],
+          '#options' => $uri['options'],
         );
       }
       break;
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.1151
diff -u -p -r1.1151 user.module
--- modules/user/user.module	1 Apr 2010 12:22:39 -0000	1.1151
+++ modules/user/user.module	1 Apr 2010 18:11:21 -0000
@@ -3418,8 +3418,9 @@ function user_register_submit($form, &$f
   $account->password = $pass;
 
   // New administrative account without notification.
+  $uri = entity_uri('user', $account);
   if ($admin && !$notify) {
-    drupal_set_message(t('Created a new user account for <a href="@url">%name</a>. No e-mail has been sent.', array('@url' => url("user/$account->uid"), '%name' => $account->name)));
+    drupal_set_message(t('Created a new user account for <a href="@url">%name</a>. No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
   }
   // No e-mail verification required; log in user immediately.
   elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) {
@@ -3434,7 +3435,7 @@ function user_register_submit($form, &$f
     $op = $notify ? 'register_admin_created' : 'register_no_approval_required';
     _user_mail_notify($op, $account);
     if ($notify) {
-      drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user <a href="@url">%name</a>.', array('@url' => url("user/$account->uid"), '%name' => $account->name)));
+      drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user <a href="@url">%name</a>.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
     }
     else {
       drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));
