diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py
index 3bc8408a..dae64316 100644
--- a/swh/web/ui/service.py
+++ b/swh/web/ui/service.py
@@ -1,709 +1,722 @@
 # Copyright (C) 2015  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from collections import defaultdict
 
 from swh.core import hashutil
 from swh.web.ui import converters, query, backend
 from swh.web.ui.exc import NotFoundExc
 
 
 def lookup_multiple_hashes(hashes):
     """Lookup the passed hashes in a single DB connection, using batch
     processing.
 
     Args:
         An array of {filename: X, sha1: Y}, string X, hex sha1 string Y.
     Returns:
         The same array with elements updated with elem['found'] = true if
         the hash is present in storage, elem['found'] = false if not.
 
     """
     hashlist = [hashutil.hex_to_hash(elem['sha1']) for elem in hashes]
     content_missing = backend.content_missing_per_sha1(hashlist)
     missing = [hashutil.hash_to_hex(x) for x in content_missing]
     for x in hashes:
         x.update({'found': True})
     for h in hashes:
         if h['sha1'] in missing:
             h['found'] = False
     return hashes
 
 
 def lookup_hash(q):
     """Checks if the storage contains a given content checksum
 
     Args: query string of the form <hash_algo:hash>
 
     Returns: Dict with key found containing the hash info if the
     hash is present, None if not.
 
     """
     algo, hash = query.parse_hash(q)
     found = backend.content_find(algo, hash)
     return {'found': found,
             'algo': algo}
 
 
 def search_hash(q):
     """Checks if the storage contains a given content checksum
 
     Args: query string of the form <hash_algo:hash>
 
     Returns: Dict with key found to True or False, according to
         whether the checksum is present or not
 
     """
     algo, hash = query.parse_hash(q)
     found = backend.content_find(algo, hash)
     return {'found': found is not None}
 
 
 def lookup_content_provenance(q):
     """Return provenance information from a specified content.
 
     Args:
         q: query string of the form <hash_algo:hash>
 
     Yields:
         provenance information (dict) list if the content is found.
 
     """
     algo, hash = query.parse_hash(q)
     provenances = backend.content_find_provenance(algo, hash)
     if not provenances:
         return None
     return (converters.from_provenance(p) for p in provenances)
 
 
-def lookup_content_filetype(q):
-    """Return filetype information from a specified content.
+def _lookup_content_sha1(q):
+    """Given a possible input, query for the content's sha1.
 
     Args:
         q: query string of the form <hash_algo:hash>
 
-    Yields:
-        filetype information (dict) list if the content is found.
+    Returns:
+        binary sha1 if found or None
 
     """
     algo, hash = query.parse_hash(q)
     if algo != 'sha1':
         hashes = backend.content_find(algo, hash)
-        hash = hashes['sha1']
+        if not hashes:
+            return None
+        return hashes['sha1']
+    return hash
+
+
+def lookup_content_filetype(q):
+    """Return filetype information from a specified content.
 
-    filetype = backend.content_filetype_get(hash)
+    Args:
+        q: query string of the form <hash_algo:hash>
+
+    Yields:
+        filetype information (dict) list if the content is found.
+
+    """
+    sha1 = _lookup_content_sha1(q)
+    if not sha1:
+        return None
+    filetype = backend.content_filetype_get(sha1)
     if not filetype:
         return None
     return converters.from_filetype(filetype)
 
 
 def lookup_content_language(q):
     """Return language information from a specified content.
 
     Args:
         q: query string of the form <hash_algo:hash>
 
     Yields:
         language information (dict) list if the content is found.
 
     """
-    algo, hash = query.parse_hash(q)
-    if algo != 'sha1':
-        hashes = backend.content_find(algo, hash)
-        hash = hashes['sha1']
-
-    lang = backend.content_language_get(hash)
+    sha1 = _lookup_content_sha1(q)
+    if not sha1:
+        return None
+    lang = backend.content_language_get(sha1)
     if not lang:
         return None
     return converters.from_swh(lang, hashess={'id'})
 
 
 def lookup_content_license(q):
     """Return license information from a specified content.
 
     Args:
         q: query string of the form <hash_algo:hash>
 
     Yields:
         license information (dict) list if the content is found.
 
     """
-    algo, hash = query.parse_hash(q)
-    if algo != 'sha1':
-        hashes = backend.content_find(algo, hash)
-        hash = hashes['sha1']
-
-    lang = backend.content_license_get(hash)
+    sha1 = _lookup_content_sha1(q)
+    if not sha1:
+        return None
+    lang = backend.content_license_get(sha1)
     if not lang:
         return None
     return converters.from_swh(lang, hashess={'id'})
 
 
 def lookup_origin(origin):
     """Return information about the origin matching dict origin.
 
     Args:
         origin: origin's dict with keys either 'id' or
         ('type' AND 'url')
 
     Returns:
         origin information as dict.
 
     """
     return backend.origin_get(origin)
 
 
 def lookup_person(person_id):
     """Return information about the person with id person_id.
 
     Args:
         person_id as string
 
     Returns:
         person information as dict.
 
     """
     person = backend.person_get(person_id)
     return converters.from_person(person)
 
 
 def lookup_directory(sha1_git):
     """Return information about the directory with id sha1_git.
 
     Args:
         sha1_git as string
 
     Returns:
         directory information as dict.
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         sha1_git,
         ['sha1'],  # HACK: sha1_git really
         'Only sha1_git is supported.')
 
     dir = backend.directory_get(sha1_git_bin)
     if not dir:
         return None
 
     directory_entries = backend.directory_ls(sha1_git_bin)
     return map(converters.from_directory_entry, directory_entries)
 
 
 def lookup_directory_with_path(directory_sha1_git, path_string):
     """Return directory information for entry with path path_string w.r.t.
     root directory pointed by directory_sha1_git
 
     Args:
         - directory_sha1_git: sha1_git corresponding to the directory
         to which we append paths to (hopefully) find the entry
         - the relative path to the entry starting from the directory pointed by
         directory_sha1_git
 
     Raises:
         NotFoundExc if the directory entry is not found
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         directory_sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
 
     queried_dir = backend.directory_entry_get_by_path(
         sha1_git_bin, path_string)
 
     if not queried_dir:
         raise NotFoundExc(('Directory entry with path %s from %s not found') %
                           (path_string, directory_sha1_git))
 
     return converters.from_directory_entry(queried_dir)
 
 
 def lookup_release(release_sha1_git):
     """Return information about the release with sha1 release_sha1_git.
 
     Args:
         release_sha1_git: The release's sha1 as hexadecimal
 
     Returns:
         Release information as dict.
 
     Raises:
         ValueError if the identifier provided is not of sha1 nature.
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         release_sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
     res = backend.release_get(sha1_git_bin)
     return converters.from_release(res)
 
 
 def lookup_revision(rev_sha1_git):
     """Return information about the revision with sha1 revision_sha1_git.
 
     Args:
         revision_sha1_git: The revision's sha1 as hexadecimal
 
     Returns:
         Revision information as dict.
 
     Raises:
         ValueError if the identifier provided is not of sha1 nature.
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         rev_sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
 
     revision = backend.revision_get(sha1_git_bin)
     return converters.from_revision(revision)
 
 
 def lookup_revision_multiple(sha1_git_list):
     """Return information about the revision with sha1 revision_sha1_git.
 
     Args:
         revision_sha1_git: The revision's sha1 as hexadecimal
 
     Returns:
         Revision information as dict.
 
     Raises:
         ValueError if the identifier provided is not of sha1 nature.
 
     """
     def to_sha1_bin(sha1_hex):
         _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
             sha1_hex,
             ['sha1'],
             'Only sha1_git is supported.')
         return sha1_git_bin
 
     sha1_bin_list = (to_sha1_bin(x) for x in sha1_git_list)
     revisions = backend.revision_get_multiple(sha1_bin_list)
     return (converters.from_revision(x) for x in revisions)
 
 
 def lookup_revision_message(rev_sha1_git):
     """Return the raw message of the revision with sha1 revision_sha1_git.
 
     Args:
         revision_sha1_git: The revision's sha1 as hexadecimal
 
     Returns:
         Decoded revision message as dict {'message': <the_message>}
 
     Raises:
         ValueError if the identifier provided is not of sha1 nature.
         NotFoundExc if the revision is not found, or if it has no message
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         rev_sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
 
     revision = backend.revision_get(sha1_git_bin)
     if not revision:
         raise NotFoundExc('Revision with sha1_git %s not found.'
                           % rev_sha1_git)
     if 'message' not in revision:
         raise NotFoundExc('No message for revision with sha1_git %s.'
                           % rev_sha1_git)
     res = {'message': revision['message']}
     return res
 
 
 def lookup_revision_by(origin_id,
                        branch_name="refs/heads/master",
                        timestamp=None):
     """Lookup revisions by origin_id, branch_name and timestamp.
 
     If:
     - branch_name is not provided, lookup using 'refs/heads/master' as default.
     - ts is not provided, use the most recent
 
     Args:
         - origin_id: origin of the revision.
         - branch_name: revision's branch.
         - timestamp: revision's time frame.
 
     Yields:
         The revisions matching the criterions.
 
     """
     res = backend.revision_get_by(origin_id, branch_name, timestamp)
     return converters.from_revision(res)
 
 
 def lookup_revision_log(rev_sha1_git, limit):
     """Return information about the revision with sha1 revision_sha1_git.
 
     Args:
         revision_sha1_git: The revision's sha1 as hexadecimal
         limit: the maximum number of revisions returned
 
     Returns:
         Revision information as dict.
 
     Raises:
         ValueError if the identifier provided is not of sha1 nature.
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         rev_sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
 
     revision_entries = backend.revision_log(sha1_git_bin, limit)
     return map(converters.from_revision, revision_entries)
 
 
 def lookup_revision_log_by(origin_id, branch_name, timestamp, limit):
     """Return information about the revision with sha1 revision_sha1_git.
 
     Args:
         origin_id: origin of the revision
         branch_name: revision's branch
         timestamp: revision's time frame
         limit: the maximum number of revisions returned
 
     Returns:
         Revision information as dict.
 
     Raises:
         NotFoundExc if no revision corresponds to the criterion
         NotFoundExc if the corresponding revision has no log
 
     """
     revision_entries = backend.revision_log_by(origin_id,
                                                branch_name,
                                                timestamp,
                                                limit)
     if not revision_entries:
         return None
     return map(converters.from_revision, revision_entries)
 
 
 def lookup_revision_with_context_by(origin_id, branch_name, ts, sha1_git,
                                     limit=100):
     """Return information about revision sha1_git, limited to the
     sub-graph of all transitive parents of sha1_git_root.
     sha1_git_root being resolved through the lookup of a revision by origin_id,
     branch_name and ts.
 
     In other words, sha1_git is an ancestor of sha1_git_root.
 
     Args:
         - origin_id: origin of the revision.
         - branch_name: revision's branch.
         - timestamp: revision's time frame.
         - sha1_git: one of sha1_git_root's ancestors.
         - limit: limit the lookup to 100 revisions back.
 
     Returns:
         Pair of (root_revision, revision).
         Information on sha1_git if it is an ancestor of sha1_git_root
         including children leading to sha1_git_root
 
     Raises:
         - BadInputExc in case of unknown algo_hash or bad hash.
         - NotFoundExc if either revision is not found or if sha1_git is not an
         ancestor of sha1_git_root.
 
     """
     rev_root = backend.revision_get_by(origin_id, branch_name, ts)
     if not rev_root:
         raise NotFoundExc('Revision with (origin_id: %s, branch_name: %s'
                           ', ts: %s) not found.' % (origin_id,
                                                     branch_name,
                                                     ts))
 
     return (converters.from_revision(rev_root),
             lookup_revision_with_context(rev_root, sha1_git, limit))
 
 
 def lookup_revision_with_context(sha1_git_root, sha1_git, limit=100):
     """Return information about revision sha1_git, limited to the
     sub-graph of all transitive parents of sha1_git_root.
 
     In other words, sha1_git is an ancestor of sha1_git_root.
 
     Args:
         sha1_git_root: latest revision. The type is either a sha1 (as an hex
         string) or a non converted dict.
         sha1_git: one of sha1_git_root's ancestors
         limit: limit the lookup to 100 revisions back
 
     Returns:
         Information on sha1_git if it is an ancestor of sha1_git_root
         including children leading to sha1_git_root
 
     Raises:
         BadInputExc in case of unknown algo_hash or bad hash
         NotFoundExc if either revision is not found or if sha1_git is not an
         ancestor of sha1_git_root
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
 
     revision = backend.revision_get(sha1_git_bin)
     if not revision:
         raise NotFoundExc('Revision %s not found' % sha1_git)
 
     if isinstance(sha1_git_root, str):
         _, sha1_git_root_bin = query.parse_hash_with_algorithms_or_throws(
             sha1_git_root,
             ['sha1'],
             'Only sha1_git is supported.')
 
         revision_root = backend.revision_get(sha1_git_root_bin)
         if not revision_root:
             raise NotFoundExc('Revision root %s not found' % sha1_git_root)
     else:
         sha1_git_root_bin = sha1_git_root['id']
 
     revision_log = backend.revision_log(sha1_git_root_bin, limit)
 
     parents = {}
     children = defaultdict(list)
 
     for rev in revision_log:
         rev_id = rev['id']
         parents[rev_id] = []
         for parent_id in rev['parents']:
             parents[rev_id].append(parent_id)
             children[parent_id].append(rev_id)
 
     if revision['id'] not in parents:
         raise NotFoundExc('Revision %s is not an ancestor of %s' %
                           (sha1_git, sha1_git_root))
 
     revision['children'] = children[revision['id']]
 
     return converters.from_revision(revision)
 
 
 def lookup_directory_with_revision(sha1_git, dir_path=None, with_data=False):
     """Return information on directory pointed by revision with sha1_git.
     If dir_path is not provided, display top level directory.
     Otherwise, display the directory pointed by dir_path (if it exists).
 
     Args:
         sha1_git: revision's hash.
         dir_path: optional directory pointed to by that revision.
         with_data: boolean that indicates to retrieve the raw data if the path
         resolves to a content. Default to False (for the api)
 
     Returns:
         Information on the directory pointed to by that revision.
 
     Raises:
         BadInputExc in case of unknown algo_hash or bad hash.
         NotFoundExc either if the revision is not found or the path referenced
         does not exist.
         NotImplementedError in case of dir_path exists but do not reference a
         type 'dir' or 'file'.
 
     """
     _, sha1_git_bin = query.parse_hash_with_algorithms_or_throws(
         sha1_git,
         ['sha1'],
         'Only sha1_git is supported.')
 
     revision = backend.revision_get(sha1_git_bin)
     if not revision:
         raise NotFoundExc('Revision %s not found' % sha1_git)
 
     dir_sha1_git_bin = revision['directory']
 
     if dir_path:
         entity = backend.directory_entry_get_by_path(dir_sha1_git_bin,
                                                      dir_path)
 
         if not entity:
             raise NotFoundExc(
                 "Directory or File '%s' pointed to by revision %s not found"
                 % (dir_path, sha1_git))
     else:
         entity = {'type': 'dir', 'target': dir_sha1_git_bin}
 
     if entity['type'] == 'dir':
         directory_entries = backend.directory_ls(entity['target'])
 
         return {'type': 'dir',
                 'path': '.' if not dir_path else dir_path,
                 'revision': sha1_git,
                 'content': map(converters.from_directory_entry,
                                directory_entries)}
     elif entity['type'] == 'file':  # content
         content = backend.content_find('sha1_git', entity['target'])
         if with_data:
             content['data'] = backend.content_get(content['sha1'])['data']
 
         return {'type': 'file',
                 'path': '.' if not dir_path else dir_path,
                 'revision': sha1_git,
                 'content': converters.from_content(content)}
     else:
         raise NotImplementedError('Entity of type %s not implemented.'
                                   % entity['type'])
 
 
 def lookup_content(q):
     """Lookup the content designed by q.
 
     Args:
         q: The release's sha1 as hexadecimal
 
     """
     algo, hash = query.parse_hash(q)
     c = backend.content_find(algo, hash)
     return converters.from_content(c)
 
 
 def lookup_content_raw(q):
     """Lookup the content defined by q.
 
     Args:
         q: query string of the form <hash_algo:hash>
 
     Returns:
         dict with 'sha1' and 'data' keys.
         data representing its raw data decoded.
 
     """
     algo, hash = query.parse_hash(q)
     c = backend.content_find(algo, hash)
     if not c:
         return None
 
     content = backend.content_get(c['sha1'])
     return converters.from_content(content)
 
 
 def stat_counters():
     """Return the stat counters for Software Heritage
 
     Returns:
         A dict mapping textual labels to integer values.
     """
     return backend.stat_counters()
 
 
 def lookup_origin_visits(origin_id):
     """Yields the origin origin_ids' visits.
 
     Args:
         origin_id: origin to list visits for
 
     Yields:
        Dictionaries of origin_visit for that origin
 
     """
     visits = backend.lookup_origin_visits(origin_id)
     for visit in visits:
         yield converters.from_origin_visit(visit)
 
 
 def lookup_origin_visit(origin_id, visit_id):
     """Return information about visit visit_id with origin origin_id.
 
     Args:
         origin_id: origin concerned by the visit
         visit_id: the visit identifier to lookup
 
     Yields:
        The dict origin_visit concerned
 
     """
     visit = backend.lookup_origin_visit(origin_id, visit_id)
     return converters.from_origin_visit(visit)
 
 
 def lookup_entity_by_uuid(uuid):
     """Return the entity's hierarchy from its uuid.
 
     Args:
         uuid: entity's identifier.
 
     Returns:
         List of hierarchy entities from the entity with uuid.
 
     """
     uuid = query.parse_uuid4(uuid)
     return backend.entity_get(uuid)
 
 
 def lookup_revision_through(revision, limit=100):
     """Retrieve a revision from the criterion stored in revision dictionary.
 
     Args:
         revision: Dictionary of criterion to lookup the revision with.
         Here are the supported combination of possible values:
         - origin_id, branch_name, ts, sha1_git
         - origin_id, branch_name, ts
         - sha1_git_root, sha1_git
         - sha1_git
 
     Returns:
         None if the revision is not found or the actual revision.
 
     """
     if 'origin_id' in revision and \
        'branch_name' in revision and \
        'ts' in revision and \
        'sha1_git' in revision:
         return lookup_revision_with_context_by(revision['origin_id'],
                                                revision['branch_name'],
                                                revision['ts'],
                                                revision['sha1_git'],
                                                limit)
     if 'origin_id' in revision and \
        'branch_name' in revision and \
        'ts' in revision:
         return lookup_revision_by(revision['origin_id'],
                                   revision['branch_name'],
                                   revision['ts'])
     if 'sha1_git_root' in revision and \
        'sha1_git' in revision:
         return lookup_revision_with_context(revision['sha1_git_root'],
                                             revision['sha1_git'],
                                             limit)
     if 'sha1_git' in revision:
         return lookup_revision(revision['sha1_git'])
 
     # this should not happen
     raise NotImplementedError('Should not happen!')
 
 
 def lookup_directory_through_revision(revision, path=None,
                                       limit=100, with_data=False):
     """Retrieve the directory information from the revision.
 
     Args:
         revision: dictionary of criterion representing a revision to lookup
         path: directory's path to lookup.
         limit: optional query parameter to limit the revisions log.
         (default to 100). For now, note that this limit could impede the
         transitivity conclusion about sha1_git not being an ancestor of.
         with_data: indicate to retrieve the content's raw data if path resolves
         to a content.
 
     Returns:
         The directory pointing to by the revision criterions at path.
 
     """
     rev = lookup_revision_through(revision, limit)
 
     if not rev:
         raise NotFoundExc('Revision with criterion %s not found!' % revision)
     return (rev['id'],
             lookup_directory_with_revision(rev['id'], path, with_data))
diff --git a/swh/web/ui/utils.py b/swh/web/ui/utils.py
index 7e58bc1b..a1cb27fa 100644
--- a/swh/web/ui/utils.py
+++ b/swh/web/ui/utils.py
@@ -1,372 +1,382 @@
 # Copyright (C) 2015  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import datetime
 import flask
 import re
 
 from dateutil import parser
 
 
 def filter_endpoints(url_map, prefix_url_rule, blacklist=[]):
     """Filter endpoints by prefix url rule.
 
     Args:
         - url_map: Url Werkzeug.Map of rules
         - prefix_url_rule: prefix url string
         - blacklist: blacklist of some url
 
     Returns:
         Dictionary of url_rule with values methods and endpoint.
 
         The key is the url, the associated value is a dictionary of
         'methods' (possible http methods) and 'endpoint' (python function)
 
     """
     out = {}
     for r in url_map:
         rule = r['rule']
         if rule == prefix_url_rule or rule in blacklist:
             continue
 
         if rule.startswith(prefix_url_rule):
             out[rule] = {'methods': sorted(map(str, r['methods'])),
                          'endpoint': r['endpoint']}
     return out
 
 
 def fmap(f, data):
     """Map f to data at each level.
 
     This must keep the origin data structure type:
     - map -> map
     - dict -> dict
     - list -> list
     - None -> None
 
     Args:
         f: function that expects one argument.
         data: data to traverse to apply the f function.
               list, map, dict or bare value.
 
     Returns:
         The same data-structure with modified values by the f function.
 
     """
     if not data:
         return data
     if isinstance(data, map):
         return map(lambda y: fmap(f, y), (x for x in data))
     if isinstance(data, list):
         return [fmap(f, x) for x in data]
     if isinstance(data, dict):
         return {k: fmap(f, v) for (k, v) in data.items()}
     return f(data)
 
 
 def prepare_data_for_view(data, encoding='utf-8'):
     def prepare_data(s):
         # Note: can only be 'data' key with bytes of raw content
         if isinstance(s, bytes):
             try:
                 return s.decode(encoding)
             except:
                 return "Cannot decode the data bytes, try and set another " \
                        "encoding in the url (e.g. ?encoding=utf8) or " \
                        "download directly the " \
                        "content's raw data."
         if isinstance(s, str):
             return re.sub(r'/api/1/', r'/browse/', s)
 
         return s
 
     return fmap(prepare_data, data)
 
 
 def filter_field_keys(data, field_keys):
     """Given an object instance (directory or list), and a csv field keys
     to filter on.
 
     Return the object instance with filtered keys.
 
     Note: Returns obj as is if it's an instance of types not in (dictionary,
     list)
 
     Args:
         - data: one object (dictionary, list...) to filter.
         - field_keys: csv or set of keys to filter the object on
 
     Returns:
         obj filtered on field_keys
 
     """
     if isinstance(data, map):
         return (filter_field_keys(x, field_keys) for x in data)
     if isinstance(data, list):
         return [filter_field_keys(x, field_keys) for x in data]
     if isinstance(data, dict):
         return {k: v for (k, v) in data.items() if k in field_keys}
     return data
 
 
 def person_to_string(person):
     """Map a person (person, committer, tagger, etc...) to a string.
 
     """
     return ''.join([person['name'], ' <', person['email'], '>'])
 
 
 def parse_timestamp(timestamp):
     """Given a time or timestamp (as string), parse the result as datetime.
 
     Returns:
         a timezone-aware datetime representing the parsed value. If the parsed
         value doesn't specify a timezone, UTC is assumed.
 
     Samples:
         - 2016-01-12
         - 2016-01-12T09:19:12+0100
         - Today is January 1, 2047 at 8:21:00AM
         - 1452591542
     """
     default_timestamp = datetime.datetime.utcfromtimestamp(0).replace(
         tzinfo=datetime.timezone.utc)
     try:
         res = parser.parse(timestamp, ignoretz=False, fuzzy=True,
                            default=default_timestamp)
     except:
         res = datetime.datetime.utcfromtimestamp(float(timestamp)).replace(
             tzinfo=datetime.timezone.utc)
     return res
 
 
 def enrich_object(object):
     """Enrich an object (revision, release) with link to the 'target' of
     type 'target_type'.
 
     Args:
         object: An object with target and target_type keys
         (e.g. release, revision)
 
     Returns:
         Object enriched with target_url pointing to the right
         swh.web.ui.api urls for the pointing object (revision,
         release, content, directory)
 
     """
     obj = object.copy()
     if 'target' in obj and 'target_type' in obj:
         if obj['target_type'] == 'revision':
             obj['target_url'] = flask.url_for('api_revision',
                                               sha1_git=obj['target'])
         elif obj['target_type'] == 'release':
             obj['target_url'] = flask.url_for('api_release',
                                               sha1_git=obj['target'])
         elif obj['target_type'] == 'content':
             obj['target_url'] = flask.url_for(
                 'api_content_metadata',
                 q='sha1_git:' + obj['target'])
 
         elif obj['target_type'] == 'directory':
             obj['target_url'] = flask.url_for('api_directory',
                                               q=obj['target'])
 
     return obj
 
 
 enrich_release = enrich_object
 
 
 def enrich_directory(directory, context_url=None):
     """Enrich directory with url to content or directory.
 
     """
     if 'type' in directory:
         target_type = directory['type']
         target = directory['target']
         if target_type == 'file':
             directory['target_url'] = flask.url_for('api_content_metadata',
                                                     q='sha1_git:%s' % target)
             if context_url:
                 directory['file_url'] = context_url + directory['name'] + '/'
         else:
             directory['target_url'] = flask.url_for('api_directory',
                                                     sha1_git=target)
             if context_url:
                 directory['dir_url'] = context_url + directory['name'] + '/'
 
     return directory
 
 
+def enrich_metadata_endpoint(content):
+    """Enrich metadata endpoint with link to the upper metadata endpoint.
+
+    """
+    c = content.copy()
+    c['content_url'] = flask.url_for('api_content_metadata',
+                                     q='sha1:%s' % c['id'])
+    return c
+
+
 def enrich_content(content):
     """Enrich content with links to:
         - data_url: its raw data
         - filetype_url: its filetype information
 
     """
     for h in ['sha1', 'sha1_git', 'sha256']:
         if h in content:
             q = '%s:%s' % (h, content[h])
             content['data_url'] = flask.url_for('api_content_raw', q=q)
             content['filetype_url'] = flask.url_for('api_content_filetype',
                                                     q=q)
             content['language_url'] = flask.url_for('api_content_language',
                                                     q=q)
             content['license_url'] = flask.url_for('api_content_license',
                                                    q=q)
             break
 
     return content
 
 
 def enrich_entity(entity):
     """Enrich entity with
 
     """
     if 'uuid' in entity:
         entity['uuid_url'] = flask.url_for('api_entity_by_uuid',
                                            uuid=entity['uuid'])
 
     if 'parent' in entity and entity['parent']:
         entity['parent_url'] = flask.url_for('api_entity_by_uuid',
                                              uuid=entity['parent'])
     return entity
 
 
 def _get_path_list(path_string):
     """Helper for enrich_revision: get a list of the sha1 id of the navigation
     breadcrumbs, ordered from the oldest to the most recent.
 
     Args:
         path_string: the path as a '/'-separated string
 
     Returns:
         The navigation context as a list of sha1 revision ids
     """
     return path_string.split('/')
 
 
 def _get_revision_contexts(rev_id, context):
     """Helper for enrich_revision: retrieve for the revision id and potentially
     the navigation breadcrumbs the context to pass to parents and children of
     of the revision.
 
     Args:
         rev_id: the revision's sha1 id
         context: the current navigation context
 
     Returns:
         The context for parents, children and the url of the direct child as a
         tuple in that order.
     """
     context_for_parents = None
     context_for_children = None
     url_direct_child = None
 
     if not context:
         return (rev_id, None, None)
 
     path_list = _get_path_list(context)
     context_for_parents = '%s/%s' % (context, rev_id)
     prev_for_children = path_list[:-1]
     if len(prev_for_children) > 0:
         context_for_children = '/'.join(prev_for_children)
     child_id = path_list[-1]
     # This commit is not the first commit in the path
     if context_for_children:
         url_direct_child = flask.url_for(
             'api_revision',
             sha1_git=child_id,
             context=context_for_children)
     # This commit is the first commit in the path
     else:
         url_direct_child = flask.url_for(
             'api_revision',
             sha1_git=child_id)
 
     return (context_for_parents, context_for_children, url_direct_child)
 
 
 def _make_child_url(rev_children, context):
     """Helper for enrich_revision: retrieve the list of urls corresponding
     to the children of the current revision according to the navigation
     breadcrumbs.
 
     Args:
         rev_children: a list of revision id
         context: the '/'-separated navigation breadcrumbs
 
     Returns:
         the list of the children urls according to the context
     """
     children = []
     for child in rev_children:
         if context and child != _get_path_list(context)[-1]:
             children.append(flask.url_for('api_revision', sha1_git=child))
         elif not context:
             children.append(flask.url_for('api_revision', sha1_git=child))
     return children
 
 
 def enrich_revision(revision, context=None):
     """Enrich revision with links where it makes sense (directory, parents).
     Keep track of the navigation breadcrumbs if they are specified.
 
     Args:
         revision: the revision as a dict
         context: the navigation breadcrumbs as a /-separated string of revision
         sha1_git
     """
 
     ctx_parents, ctx_children, url_direct_child = _get_revision_contexts(
         revision['id'], context)
 
     revision['url'] = flask.url_for('api_revision', sha1_git=revision['id'])
     revision['history_url'] = flask.url_for('api_revision_log',
                                             sha1_git=revision['id'])
     if context:
         revision['history_context_url'] = flask.url_for(
             'api_revision_log',
             sha1_git=revision['id'],
             prev_sha1s=context)
 
     if 'author' in revision:
         author = revision['author']
         revision['author_url'] = flask.url_for('api_person',
                                                person_id=author['id'])
 
     if 'committer' in revision:
         committer = revision['committer']
         revision['committer_url'] = flask.url_for('api_person',
                                                   person_id=committer['id'])
 
     if 'directory' in revision:
         revision['directory_url'] = flask.url_for(
             'api_directory',
             sha1_git=revision['directory'])
 
     if 'parents' in revision:
         parents = []
         for parent in revision['parents']:
             parents.append(flask.url_for('api_revision',
                                          sha1_git=parent,
                                          context=ctx_parents))
         revision['parent_urls'] = parents
 
     if 'children' in revision:
         children = _make_child_url(revision['children'], context)
         if url_direct_child:
             children.append(url_direct_child)
         revision['children_urls'] = children
     else:
         if url_direct_child:
             revision['children_urls'] = [url_direct_child]
 
     if 'message_decoding_failed' in revision:
         revision['message_url'] = flask.url_for(
             'api_revision_raw_message',
             sha1_git=revision['id'])
 
     return revision
diff --git a/swh/web/ui/views/api.py b/swh/web/ui/views/api.py
index a80b6f57..ea064692 100644
--- a/swh/web/ui/views/api.py
+++ b/swh/web/ui/views/api.py
@@ -1,938 +1,920 @@
 # Copyright (C) 2015  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 from types import GeneratorType
 
 from flask import request, url_for
 
 from swh.web.ui import service, utils, apidoc as doc
 from swh.web.ui.exc import NotFoundExc
 from swh.web.ui.main import app
 
 
 @app.route('/api/1/stat/counters/')
 @doc.route('/api/1/stat/counters/', noargs=True)
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="A dictionary of SWH's most important statistics")
 def api_stats():
     """Return statistics on SWH storage.
 
     """
     return service.stat_counters()
 
 
 @app.route('/api/1/origin/<int:origin_id>/visits/')
 @doc.route('/api/1/origin/visits/')
 @doc.arg('origin_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc='The requested SWH origin identifier')
 @doc.returns(rettype=doc.rettypes.list,
              retdoc="""All instances of visits of the origin pointed by
              origin_id as POSIX time since epoch (if visit_id is not defined)
 """)
 def api_origin_visits(origin_id):
     """Return a list of origin visit (dict) for that particular origin
        including date (visit date as posix timestamp), target,
        target_type, status, ...
 
     """
     def _enrich_origin_visit(origin_visit):
         ov = origin_visit.copy()
         ov['origin_visit_url'] = url_for('api_origin_visit',
                                          origin_id=ov['origin'],
                                          visit_id=ov['visit'])
         return ov
 
     return _api_lookup(
         origin_id,
         service.lookup_origin_visits,
         error_msg_if_not_found='No origin %s found' % origin_id,
         enrich_fn=_enrich_origin_visit)
 
 
 @app.route('/api/1/origin/<int:origin_id>/visits/<int:visit_id>/')
 @doc.route('/api/1/origin/visits/id/')
 @doc.arg('origin_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc='The requested SWH origin identifier')
 @doc.arg('visit_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc='The requested SWH origin visit identifier')
 @doc.returns(rettype=doc.rettypes.list,
              retdoc="""The single instance visit visit_id of the origin pointed
              by origin_id as POSIX time since epoch""")
 def api_origin_visit(origin_id, visit_id):
     """Return origin visit (dict) for that particular origin including
        (but not limited to) date (visit date as posix timestamp),
        target, target_type, status, ...
 
     """
     def _enrich_origin_visit(origin_visit):
         ov = origin_visit.copy()
         ov['origin_url'] = url_for('api_origin', origin_id=ov['origin'])
         if 'occurrences' in ov:
             ov['occurrences'] = {
                 k: utils.enrich_object(v)
                 for k, v in ov['occurrences'].items()
             }
         return ov
 
     return _api_lookup(
         origin_id,
         service.lookup_origin_visit,
         'No visit %s for origin %s found' % (visit_id, origin_id),
         _enrich_origin_visit,
         visit_id)
 
 
 @app.route('/api/1/content/search/', methods=['POST'])
 @app.route('/api/1/content/search/<string:q>/')
 @doc.route('/api/1/content/search/')
 @doc.arg('q',
          default='sha1:adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""An algo_hash:hash string, where algo_hash is one of sha1,
          sha1_git or sha256 and hash is the hash to search for in SWH""")
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if q is not well formed')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""A dict with keys:
 
              - search_res: a list of dicts corresponding to queried content
                with key 'found' to True if found, 'False' if not
              - search_stats: a dict containing number of files searched and
                percentage of files found
              """)
 def api_search(q=None):
     """Search a content per hash.
 
     This may take the form of:
 
     - a GET request with a single checksum
     - a POST request with many hashes, with the request body containing
       identifiers (typically filenames) as keys and corresponding hashes as
       values.
     """
 
     response = {'search_res': None,
                 'search_stats': None}
     search_stats = {'nbfiles': 0, 'pct': 0}
     search_res = None
 
     # Single hash request route
     if q:
         r = service.search_hash(q)
         search_res = [{'filename': None,
                        'sha1': q,
                        'found': r['found']}]
         search_stats['nbfiles'] = 1
         search_stats['pct'] = 100 if r['found'] else 0
 
     # Post form submission with many hash requests
     elif request.method == 'POST':
         data = request.form
         queries = []
         # Remove potential inputs with no associated value
         for k, v in data.items():
             if v is not None:
                 if k == 'q' and len(v) > 0:
                     queries.append({'filename': None, 'sha1': v})
                 elif v != '':
                     queries.append({'filename': k, 'sha1': v})
 
         if len(queries) > 0:
             lookup = service.lookup_multiple_hashes(queries)
             result = []
             for el in lookup:
                 result.append({'filename': el['filename'],
                                'sha1': el['sha1'],
                                'found': el['found']})
             search_res = result
             nbfound = len([x for x in lookup if x['found']])
             search_stats['nbfiles'] = len(queries)
             search_stats['pct'] = (nbfound / len(queries))*100
 
     response['search_res'] = search_res
     response['search_stats'] = search_stats
     return response
 
 
 def _api_lookup(criteria,
                 lookup_fn,
                 error_msg_if_not_found,
                 enrich_fn=lambda x: x,
                 *args):
     """Capture a redundant behavior of:
     - looking up the backend with a criteria (be it an identifier or checksum)
     passed to the function lookup_fn
     - if nothing is found, raise an NotFoundExc exception with error
     message error_msg_if_not_found.
     - Otherwise if something is returned:
         - either as list, map or generator, map the enrich_fn function to it
         and return the resulting data structure as list.
         - either as dict and pass to enrich_fn and return the dict enriched.
 
     Args:
         - criteria: discriminating criteria to lookup
         - lookup_fn: function expects one criteria and optional supplementary
         *args.
         - error_msg_if_not_found: if nothing matching the criteria is found,
         raise NotFoundExc with this error message.
         - enrich_fn: Function to use to enrich the result returned by
         lookup_fn. Default to the identity function if not provided.
         - *args: supplementary arguments to pass to lookup_fn.
 
     Raises:
         NotFoundExp or whatever `lookup_fn` raises.
 
     """
     res = lookup_fn(criteria, *args)
     if not res:
         raise NotFoundExc(error_msg_if_not_found)
     if isinstance(res, (map, list, GeneratorType)):
         return [enrich_fn(x) for x in res]
     return enrich_fn(res)
 
 
 @app.route('/api/1/origin/<int:origin_id>/')
 @app.route('/api/1/origin/<string:origin_type>/url/<path:origin_url>/')
 @doc.route('/api/1/origin/')
 @doc.arg('origin_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc="The origin's SWH origin_id.")
 @doc.arg('origin_type',
          default='git',
          argtype=doc.argtypes.str,
          argdoc="The origin's type (git, svn..)")
 @doc.arg('origin_url',
          default='https://github.com/hylang/hy',
          argtype=doc.argtypes.path,
          argdoc="The origin's URL.")
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if origin_id does not correspond to an origin in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc='The metadata of the origin identified by origin_id')
 def api_origin(origin_id=None, origin_type=None, origin_url=None):
     """Return information about the origin matching the passed criteria.
 
     Criteria may be:
       - An SWH-specific ID, if you already know it
       - An origin type and its URL, if you do not have the origin's SWH
         identifier
     """
     ori_dict = {
         'id': origin_id,
         'type': origin_type,
         'url': origin_url
     }
     ori_dict = {k: v for k, v in ori_dict.items() if ori_dict[k]}
     if 'id' in ori_dict:
         error_msg = 'Origin with id %s not found.' % ori_dict['id']
     else:
         error_msg = 'Origin with type %s and URL %s not found' % (
             ori_dict['type'], ori_dict['url'])
 
     def _enrich_origin(origin):
         if 'id' in origin:
             o = origin.copy()
             o['origin_visits_url'] = url_for('api_origin_visits',
                                              origin_id=o['id'])
             return o
 
         return origin
 
     return _api_lookup(
         ori_dict, lookup_fn=service.lookup_origin,
         error_msg_if_not_found=error_msg,
         enrich_fn=_enrich_origin)
 
 
 @app.route('/api/1/person/<int:person_id>/')
 @doc.route('/api/1/person/')
 @doc.arg('person_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc="The person's SWH identifier")
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if person_id does not correspond to an origin in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc='The metadata of the person identified by person_id')
 def api_person(person_id):
     """Return information about person with identifier person_id.
     """
     return _api_lookup(
         person_id, lookup_fn=service.lookup_person,
         error_msg_if_not_found='Person with id %s not found.' % person_id)
 
 
 @app.route('/api/1/release/<string:sha1_git>/')
 @doc.route('/api/1/release/')
 @doc.arg('sha1_git',
          default='8b137891791fe96927ad78e64b0aad7bded08bdc',
          argtype=doc.argtypes.sha1_git,
          argdoc="The release's sha1_git identifier")
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if the argument is not a sha1')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if sha1_git does not correspond to a release in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc='The metadata of the release identified by sha1_git')
 def api_release(sha1_git):
     """Return information about release with id sha1_git.
     """
     error_msg = 'Release with sha1_git %s not found.' % sha1_git
     return _api_lookup(
         sha1_git,
         lookup_fn=service.lookup_release,
         error_msg_if_not_found=error_msg,
         enrich_fn=utils.enrich_release)
 
 
 def _revision_directory_by(revision, path, request_path,
                            limit=100, with_data=False):
     """Compute the revision matching criterion's directory or content data.
 
     Args:
         revision: dictionary of criterions representing a revision to lookup
         path: directory's path to lookup
         request_path: request path which holds the original context to
         limit: optional query parameter to limit the revisions log
         (default to 100). For now, note that this limit could impede the
         transitivity conclusion about sha1_git not being an ancestor of
         with_data: indicate to retrieve the content's raw data if path resolves
         to a content.
 
     """
     def enrich_directory_local(dir, context_url=request_path):
         return utils.enrich_directory(dir, context_url)
 
     rev_id, result = service.lookup_directory_through_revision(
         revision, path, limit=limit, with_data=with_data)
 
     content = result['content']
     if result['type'] == 'dir':  # dir_entries
         result['content'] = list(map(enrich_directory_local, content))
     else:  # content
         result['content'] = utils.enrich_content(content)
 
     return result
 
 
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/directory/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/directory/<path:path>/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>'
            '/directory/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>'
            '/directory/<path:path>/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>'
            '/ts/<string:ts>'
            '/directory/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>'
            '/ts/<string:ts>'
            '/directory/<path:path>/')
 @doc.route('/api/1/revision/origin/directory/')
 @doc.arg('origin_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc="The revision's origin's SWH identifier")
 @doc.arg('branch_name',
          default='refs/heads/master',
          argtype=doc.argtypes.path,
          argdoc="""The optional branch for the given origin (default
          to master""")
 @doc.arg('ts',
          default='2000-01-17T11:23:54+00:00',
          argtype=doc.argtypes.ts,
          argdoc="""Optional timestamp (default to the nearest time
          crawl of timestamp)""")
 @doc.arg('path',
          default='.',
          argtype=doc.argtypes.path,
          argdoc='The path to the directory or file to display')
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a revision matching the passed criteria was
             not found""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The metadata of the revision corresponding to the
              passed criteria""")
 def api_directory_through_revision_origin(origin_id,
                                           branch_name="refs/heads/master",
                                           ts=None,
                                           path=None,
                                           with_data=False):
     """Display directory or content information through a revision identified
     by origin/branch/timestamp.
     """
     if ts:
         ts = utils.parse_timestamp(ts)
 
     return _revision_directory_by(
         {
             'origin_id': origin_id,
             'branch_name': branch_name,
             'ts': ts
         },
         path,
         request.path,
         with_data=with_data)
 
 
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>'
            '/ts/<string:ts>/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/ts/<string:ts>/')
 @doc.route('/api/1/revision/origin/')
 @doc.arg('origin_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc="The queried revision's origin identifier in SWH")
 @doc.arg('branch_name',
          default='refs/heads/master',
          argtype=doc.argtypes.path,
          argdoc="""The optional branch for the given origin (default
          to master)""")
 @doc.arg('ts',
          default='2000-01-17T11:23:54+00:00',
          argtype=doc.argtypes.ts,
          argdoc="The time at which the queried revision should be constrained")
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a revision matching given criteria was not found
             in SWH""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The metadata of the revision identified by the given
              criteria""")
 def api_revision_with_origin(origin_id,
                              branch_name="refs/heads/master",
                              ts=None):
     """Display revision information through its identification by
     origin/branch/timestamp.
     """
     if ts:
         ts = utils.parse_timestamp(ts)
 
     return _api_lookup(
         origin_id,
         service.lookup_revision_by,
         'Revision with (origin_id: %s, branch_name: %s'
         ', ts: %s) not found.' % (origin_id,
                                   branch_name,
                                   ts),
         utils.enrich_revision,
         branch_name,
         ts)
 
 
 @app.route('/api/1/revision/<string:sha1_git>/')
 @app.route('/api/1/revision/<string:sha1_git>/prev/<path:context>/')
 @doc.route('/api/1/revision/')
 @doc.arg('sha1_git',
          default='ec72c666fb345ea5f21359b7bc063710ce558e39',
          argtype=doc.argtypes.sha1_git,
          argdoc="The revision's sha1_git identifier")
 @doc.arg('context',
          default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a',
          argtype=doc.argtypes.path,
          argdoc='The navigation breadcrumbs -- use at your own risk')
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if sha1_git is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if a revision matching sha1_git was not found in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc='The metadata of the revision identified by sha1_git')
 def api_revision(sha1_git, context=None):
     """Return information about revision with id sha1_git.
     """
     def _enrich_revision(revision, context=context):
         return utils.enrich_revision(revision, context)
 
     return _api_lookup(
         sha1_git,
         service.lookup_revision,
         'Revision with sha1_git %s not found.' % sha1_git,
         _enrich_revision)
 
 
 @app.route('/api/1/revision/<string:sha1_git>/raw/')
 @doc.route('/api/1/revision/raw/')
 @doc.arg('sha1_git',
          default='ec72c666fb345ea5f21359b7bc063710ce558e39',
          argtype=doc.argtypes.sha1_git,
          argdoc="The queried revision's sha1_git identifier")
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if sha1_git is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if a revision matching sha1_git was not found in SWH')
 @doc.returns(rettype=doc.rettypes.octet_stream,
              retdoc="""The message of the revision identified by sha1_git
              as a downloadable octet stream""")
 def api_revision_raw_message(sha1_git):
     """Return the raw data of the message of revision identified by sha1_git
     """
     raw = service.lookup_revision_message(sha1_git)
     return app.response_class(raw['message'],
                               headers={'Content-disposition': 'attachment;'
                                        'filename=rev_%s_raw' % sha1_git},
                               mimetype='application/octet-stream')
 
 
 @app.route('/api/1/revision/<string:sha1_git>/directory/')
 @app.route('/api/1/revision/<string:sha1_git>/directory/<path:dir_path>/')
 @doc.route('/api/1/revision/directory/')
 @doc.arg('sha1_git',
          default='ec72c666fb345ea5f21359b7bc063710ce558e39',
          argtype=doc.argtypes.sha1_git,
          argdoc="The revision's sha1_git identifier.")
 @doc.arg('dir_path',
          default='.',
          argtype=doc.argtypes.path,
          argdoc='The path from the top level directory')
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if sha1_git is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a revision matching sha1_git was not found in SWH
             , or if the path specified does not exist""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The metadata of the directory pointed by revision id
              sha1-git and dir_path""")
 def api_revision_directory(sha1_git,
                            dir_path=None,
                            with_data=False):
     """Return information on directory pointed by revision with sha1_git.
     If dir_path is not provided, display top level directory.
     Otherwise, display the directory pointed by dir_path (if it exists).
     """
     return _revision_directory_by(
         {
             'sha1_git': sha1_git
         },
         dir_path,
         request.path,
         with_data=with_data)
 
 
 @app.route('/api/1/revision/<string:sha1_git>/log/')
 @app.route('/api/1/revision/<string:sha1_git>/prev/<path:prev_sha1s>/log/')
 @doc.route('/api/1/revision/log/')
 @doc.arg('sha1_git',
          default='ec72c666fb345ea5f21359b7bc063710ce558e39',
          argtype=doc.argtypes.sha1_git,
          argdoc='The sha1_git of the revision queried')
 @doc.arg('prev_sha1s',
          default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a',
          argtype=doc.argtypes.path,
          argdoc='The navigation breadcrumbs -- use at your own risk!')
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if sha1_git or prev_sha1s is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if a revision matching sha1_git was not found in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The log data starting at the revision identified by
              sha1_git, completed with the navigation breadcrumbs,
              if any""")
 def api_revision_log(sha1_git, prev_sha1s=None):
     """Show all revisions (~git log) starting from sha1_git.
     The first element returned is the given sha1_git, or the first
     breadcrumb, if any.
 
     """
     limit = app.config['conf']['max_log_revs']
 
     response = {'revisions': None, 'next_revs_url': None}
     revisions = None
     next_revs_url = None
 
     def lookup_revision_log_with_limit(s, limit=limit+1):
         return service.lookup_revision_log(s, limit)
 
     error_msg = 'Revision with sha1_git %s not found.' % sha1_git
     rev_get = _api_lookup(sha1_git,
                           lookup_fn=lookup_revision_log_with_limit,
                           error_msg_if_not_found=error_msg,
                           enrich_fn=utils.enrich_revision)
 
     if len(rev_get) == limit+1:
         rev_backward = rev_get[:-1]
         next_revs_url = url_for('api_revision_log',
                                 sha1_git=rev_get[-1]['id'])
     else:
         rev_backward = rev_get
 
     if not prev_sha1s:  # no nav breadcrumbs, so we're done
         revisions = rev_backward
 
     else:
         rev_forward_ids = prev_sha1s.split('/')
         rev_forward = _api_lookup(rev_forward_ids,
                                   lookup_fn=service.lookup_revision_multiple,
                                   error_msg_if_not_found=error_msg,
                                   enrich_fn=utils.enrich_revision)
         revisions = rev_forward + rev_backward
 
     response['revisions'] = revisions
     response['next_revs_url'] = next_revs_url
 
     return response
 
 
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>/log/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>/log/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/branch/<path:branch_name>'
            '/ts/<string:ts>/log/')
 @app.route('/api/1/revision'
            '/origin/<int:origin_id>'
            '/ts/<string:ts>/log/')
 @doc.route('/api/1/revision/origin/log/')
 @doc.arg('origin_id',
          default=1,
          argtype=doc.argtypes.int,
          argdoc="The revision's SWH origin identifier")
 @doc.arg('branch_name',
          default='refs/heads/master',
          argtype=doc.argtypes.path,
          argdoc="The revision's branch name within the origin specified")
 @doc.arg('ts',
          default='2000-01-17T11:23:54+00:00',
          argtype=doc.argtypes.ts,
          argdoc="""A time or timestamp string to parse""")
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a revision matching the given criteria was not
             found in SWH""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The metadata of the revision log starting at the revision
              matching the given criteria.""")
 def api_revision_log_by(origin_id,
                         branch_name='refs/heads/master',
                         ts=None):
     """Show all revisions (~git log) starting from the revision
     described by its origin_id, optional branch name and timestamp.
     The first element returned is the described revision.
 
     """
     limit = app.config['conf']['max_log_revs']
     response = {'revisions': None, 'next_revs_url': None}
     next_revs_url = None
 
     if ts:
         ts = utils.parse_timestamp(ts)
 
     def lookup_revision_log_by_with_limit(o_id, br, ts, limit=limit+1):
         return service.lookup_revision_log_by(o_id, br, ts, limit)
 
     error_msg = 'No revision matching origin %s ' % origin_id
     error_msg += ', branch name %s' % branch_name
     error_msg += (' and time stamp %s.' % ts) if ts else '.'
 
     rev_get = _api_lookup(origin_id,
                           lookup_revision_log_by_with_limit,
                           error_msg,
                           utils.enrich_revision,
                           branch_name,
                           ts)
     if len(rev_get) == limit+1:
         revisions = rev_get[:-1]
         next_revs_url = url_for('api_revision_log',
                                 sha1_git=rev_get[-1]['id'])
     else:
         revisions = rev_get
     response['revisions'] = revisions
     response['next_revs_url'] = next_revs_url
 
     return response
 
 
 @app.route('/api/1/directory/<string:sha1_git>/')
 @app.route('/api/1/directory/<string:sha1_git>/<path:path>/')
 @doc.route('/api/1/directory/')
 @doc.arg('sha1_git',
          default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
          argtype=doc.argtypes.sha1_git,
          argdoc="The queried directory's corresponding sha1_git hash")
 @doc.arg('path',
          default='.',
          argtype=doc.argtypes.path,
          argdoc="A path relative to the queried directory's top level")
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if sha1_git is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if a directory matching sha1_git was not found in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The metadata and contents of the release identified by
              sha1_git""")
 def api_directory(sha1_git,
                   path=None):
     """Return information about release with id sha1_git.
 
     """
     if path:
         error_msg_path = ('Entry with path %s relative to directory '
                           'with sha1_git %s not found.') % (path, sha1_git)
         return _api_lookup(
             sha1_git,
             service.lookup_directory_with_path,
             error_msg_path,
             utils.enrich_directory,
             path)
     else:
         error_msg_nopath = 'Directory with sha1_git %s not found.' % sha1_git
         return _api_lookup(
             sha1_git,
             service.lookup_directory,
             error_msg_nopath,
             utils.enrich_directory)
 
 
 @app.route('/api/1/provenance/<string:q>/')
 @doc.route('/api/1/provenance/')
 @doc.arg('q',
          default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""The queried content's corresponding hash (supported hash
  algorithms: sha1_git, sha1, sha256)""")
 @doc.raises(exc=doc.excs.badinput,
             doc="""Raised if hash algorithm is incorrect  or if the hash
  value is badly formatted.""")
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a content matching the hash was not found
  in SWH""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""List of provenance information (dict) for the matched
 content.""")
 def api_content_provenance(q):
     """Return content's provenance information if any.
 
     """
     def _enrich_revision(provenance):
         p = provenance.copy()
         p['revision_url'] = url_for('api_revision',
                                     sha1_git=provenance['revision'])
         p['content_url'] = url_for('api_content_metadata',
                                    q='sha1_git:%s' % provenance['content'])
         p['origin_url'] = url_for('api_origin',
                                   origin_id=provenance['origin'])
         p['origin_visits_url'] = url_for('api_origin_visits',
                                          origin_id=provenance['origin'])
         p['origin_visit_url'] = url_for('api_origin_visit',
                                         origin_id=provenance['origin'],
                                         visit_id=provenance['visit'])
         return p
 
     return _api_lookup(
         q,
         lookup_fn=service.lookup_content_provenance,
         error_msg_if_not_found='Content with %s not found.' % q,
         enrich_fn=_enrich_revision)
 
 
 @app.route('/api/1/filetype/<string:q>/')
 @doc.route('/api/1/filetype/')
 @doc.arg('q',
          default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""The queried content's corresponding hash (supported hash
  algorithms: sha1_git, sha1, sha256)""")
 @doc.raises(exc=doc.excs.badinput,
             doc="""Raised if hash algorithm is incorrect or if the hash
  value is badly formatted.""")
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a content matching the hash was not found
  in SWH""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""Filetype information (dict) for the matched
 content.""")
 def api_content_filetype(q):
     """Return content's filetype information if any.
 
     """
-    def _enrich_filetype(content):
-        c = content.copy()
-        c['content_url'] = url_for('api_content_metadata',
-                                   q='sha1:%s' % c['id'])
-        return c
-
     return _api_lookup(
         q,
         lookup_fn=service.lookup_content_filetype,
         error_msg_if_not_found='No filetype information found '
         'for content %s.' % q,
-        enrich_fn=_enrich_filetype)
+        enrich_fn=utils.enrich_metadata_endpoint)
 
 
 @app.route('/api/1/language/<string:q>/')
 @doc.route('/api/1/language/')
 @doc.arg('q',
          default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""The queried content's corresponding hash (supported hash
  algorithms: sha1_git, sha1, sha256)""")
 @doc.raises(exc=doc.excs.badinput,
             doc="""Raised if hash algorithm is incorrect or if the hash
  value is badly formatted.""")
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a content matching the hash was not found
  in SWH""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""Language information (dict) for the matched
 content.""")
 def api_content_language(q):
     """Return content's language information if any.
 
     """
-    def _enrich_language(content):
-        c = content.copy()
-        c['content_url'] = url_for('api_content_metadata',
-                                   q='sha1:%s' % c['id'])
-        return c
-
     return _api_lookup(
         q,
         lookup_fn=service.lookup_content_language,
         error_msg_if_not_found='No language information found '
         'for content %s.' % q,
-        enrich_fn=_enrich_language)
+        enrich_fn=utils.enrich_metadata_endpoint)
 
 
 @app.route('/api/1/license/<string:q>/')
 @doc.route('/api/1/license/')
 @doc.arg('q',
          default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""The queried content's corresponding hash (supported hash
  algorithms: sha1_git, sha1, sha256)""")
 @doc.raises(exc=doc.excs.badinput,
             doc="""Raised if hash algorithm is incorrect or if the hash
  value is badly formatted.""")
 @doc.raises(exc=doc.excs.notfound,
             doc="""Raised if a content matching the hash was not found
  in SWH""")
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""License information (dict) for the matched
 content.""")
 def api_content_license(q):
     """Return content's license information if any.
 
     """
-    def _enrich_license(content):
-        c = content.copy()
-        c['content_url'] = url_for('api_content_metadata',
-                                   q='sha1:%s' % c['id'])
-        return c
-
     return _api_lookup(
         q,
         lookup_fn=service.lookup_content_license,
         error_msg_if_not_found='No license information found '
         'for content %s.' % q,
-        enrich_fn=_enrich_license)
+        enrich_fn=utils.enrich_metadata_endpoint)
 
 
 @app.route('/api/1/content/<string:q>/raw/')
 @doc.route('/api/1/content/raw/')
 @doc.arg('q',
          default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""An algo_hash:hash string, where algo_hash is one of sha1,
          sha1_git or sha256 and hash is the hash to search for in SWH. Defaults
          to sha1 in the case of a missing algo_hash
          """)
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if q is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if a content matching q was not found in SWH')
 @doc.returns(rettype=doc.rettypes.octet_stream,
              retdoc='The raw content data as an octet stream')
 def api_content_raw(q):
     """Return content's raw data if content is found.
 
     """
     def generate(content):
         yield content['data']
 
     content = service.lookup_content_raw(q)
     if not content:
         raise NotFoundExc('Content with %s not found.' % q)
 
     return app.response_class(generate(content),
                               headers={'Content-disposition': 'attachment;'
                                        'filename=content_%s_raw' % q},
                               mimetype='application/octet-stream')
 
 
 @app.route('/api/1/content/<string:q>/')
 @doc.route('/api/1/content/')
 @doc.arg('q',
          default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc',
          argtype=doc.argtypes.algo_and_hash,
          argdoc="""An algo_hash:hash string, where algo_hash is one of sha1,
          sha1_git or sha256 and hash is the hash to search for in SWH. Defaults
          to sha1 in the case of a missing algo_hash
          """)
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if q is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if a content matching q was not found in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc="""The metadata of the content identified by q. If content
              decoding was successful, it also returns the data""")
 def api_content_metadata(q):
     """Return content information if content is found.
 
     """
     return _api_lookup(
         q,
         lookup_fn=service.lookup_content,
         error_msg_if_not_found='Content with %s not found.' % q,
         enrich_fn=utils.enrich_content)
 
 
 @app.route('/api/1/entity/<string:uuid>/')
 @doc.route('/api/1/entity/')
 @doc.arg('uuid',
          default='5f4d4c51-498a-4e28-88b3-b3e4e8396cba',
          argtype=doc.argtypes.uuid,
          argdoc="The entity's uuid identifier")
 @doc.raises(exc=doc.excs.badinput,
             doc='Raised if uuid is not well formed')
 @doc.raises(exc=doc.excs.notfound,
             doc='Raised if an entity matching uuid was not found in SWH')
 @doc.returns(rettype=doc.rettypes.dict,
              retdoc='The metadata of the entity identified by uuid')
 def api_entity_by_uuid(uuid):
     """Return content information if content is found.
 
     """
     return _api_lookup(
         uuid,
         lookup_fn=service.lookup_entity_by_uuid,
         error_msg_if_not_found="Entity with uuid '%s' not found." % uuid,
         enrich_fn=utils.enrich_entity)