utils.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Some utilities for CubicWeb server/clients."""
       
    19 
       
    20 from __future__ import division
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 
       
    24 import decimal
       
    25 import datetime
       
    26 import random
       
    27 import re
       
    28 import json
       
    29 
       
    30 from operator import itemgetter
       
    31 from inspect import getargspec
       
    32 from itertools import repeat
       
    33 from uuid import uuid4
       
    34 from warnings import warn
       
    35 from threading import Lock
       
    36 from logging import getLogger
       
    37 
       
    38 from six import text_type
       
    39 from six.moves.urllib.parse import urlparse
       
    40 
       
    41 from logilab.mtconverter import xml_escape
       
    42 from logilab.common.deprecation import deprecated
       
    43 from logilab.common.date import ustrftime
       
    44 
       
    45 _MARKER = object()
       
    46 
       
    47 # initialize random seed from current time
       
    48 random.seed()
       
    49 
       
    50 def admincnx(appid):
       
    51     from cubicweb.cwconfig import CubicWebConfiguration
       
    52     from cubicweb.server.repository import Repository
       
    53     from cubicweb.server.utils import TasksManager
       
    54     config = CubicWebConfiguration.config_for(appid)
       
    55 
       
    56     login = config.default_admin_config['login']
       
    57     password = config.default_admin_config['password']
       
    58 
       
    59     repo = Repository(config, TasksManager())
       
    60     session = repo.new_session(login, password=password)
       
    61     return session.new_cnx()
       
    62 
       
    63 
       
    64 def make_uid(key=None):
       
    65     """Return a unique identifier string.
       
    66 
       
    67     if specified, `key` is used to prefix the generated uid so it can be used
       
    68     for instance as a DOM id or as sql table name.
       
    69 
       
    70     See uuid.uuid4 documentation for the shape of the generated identifier, but
       
    71     this is basically a 32 bits hexadecimal string.
       
    72     """
       
    73     if key is None:
       
    74         return uuid4().hex
       
    75     return str(key) + uuid4().hex
       
    76 
       
    77 
       
    78 def support_args(callable, *argnames):
       
    79     """return true if the callable support given argument names"""
       
    80     if isinstance(callable, type):
       
    81         callable = callable.__init__
       
    82     argspec = getargspec(callable)
       
    83     if argspec[2]:
       
    84         return True
       
    85     for argname in argnames:
       
    86         if argname not in argspec[0]:
       
    87             return False
       
    88     return True
       
    89 
       
    90 
       
    91 class wrap_on_write(object):
       
    92     """ Sometimes it is convenient to NOT write some container element
       
    93     if it happens that there is nothing to be written within,
       
    94     but this cannot be known beforehand.
       
    95     Hence one can do this:
       
    96 
       
    97     .. sourcecode:: python
       
    98 
       
    99        with wrap_on_write(w, '<div class="foo">', '</div>') as wow:
       
   100            component.render_stuff(wow)
       
   101     """
       
   102     def __init__(self, w, tag, closetag=None):
       
   103         self.written = False
       
   104         self.tag = text_type(tag)
       
   105         self.closetag = closetag
       
   106         self.w = w
       
   107 
       
   108     def __enter__(self):
       
   109         return self
       
   110 
       
   111     def __call__(self, data):
       
   112         if self.written is False:
       
   113             self.w(self.tag)
       
   114             self.written = True
       
   115         self.w(data)
       
   116 
       
   117     def __exit__(self, exctype, value, traceback):
       
   118         if self.written is True:
       
   119             if self.closetag:
       
   120                 self.w(text_type(self.closetag))
       
   121             else:
       
   122                 self.w(self.tag.replace('<', '</', 1))
       
   123 
       
   124 
       
   125 # use networkX instead ?
       
   126 # http://networkx.lanl.gov/reference/algorithms.traversal.html#module-networkx.algorithms.traversal.astar
       
   127 def transitive_closure_of(entity, rtype, _seen=None):
       
   128     """return transitive closure *for the subgraph starting from the given
       
   129     entity* (eg 'parent' entities are not included in the results)
       
   130     """
       
   131     if _seen is None:
       
   132         _seen = set()
       
   133     _seen.add(entity.eid)
       
   134     yield entity
       
   135     for child in getattr(entity, rtype):
       
   136         if child.eid in _seen:
       
   137             continue
       
   138         for subchild in transitive_closure_of(child, rtype, _seen):
       
   139             yield subchild
       
   140 
       
   141 
       
   142 class RepeatList(object):
       
   143     """fake a list with the same element in each row"""
       
   144     __slots__ = ('_size', '_item')
       
   145     def __init__(self, size, item):
       
   146         self._size = size
       
   147         self._item = item
       
   148     def __repr__(self):
       
   149         return '<cubicweb.utils.RepeatList at %s item=%s size=%s>' % (
       
   150             id(self), self._item, self._size)
       
   151     def __len__(self):
       
   152         return self._size
       
   153     def __iter__(self):
       
   154         return repeat(self._item, self._size)
       
   155     def __getitem__(self, index):
       
   156         if isinstance(index, slice):
       
   157             # XXX could be more efficient, but do we bother?
       
   158             return ([self._item] * self._size)[index]
       
   159         return self._item
       
   160     def __delitem__(self, idc):
       
   161         assert self._size > 0
       
   162         self._size -= 1
       
   163     def __add__(self, other):
       
   164         if isinstance(other, RepeatList):
       
   165             if other._item == self._item:
       
   166                 return RepeatList(self._size + other._size, self._item)
       
   167             return ([self._item] * self._size) + other[:]
       
   168         return ([self._item] * self._size) + other
       
   169     def __radd__(self, other):
       
   170         if isinstance(other, RepeatList):
       
   171             if other._item == self._item:
       
   172                 return RepeatList(self._size + other._size, self._item)
       
   173             return other[:] + ([self._item] * self._size)
       
   174         return other[:] + ([self._item] * self._size)
       
   175     def __eq__(self, other):
       
   176         if isinstance(other, RepeatList):
       
   177             return other._size == self._size and other._item == self._item
       
   178         return self[:] == other
       
   179     def __ne__(self, other):
       
   180         return not (self == other)
       
   181     def __hash__(self):
       
   182         raise NotImplementedError
       
   183     def pop(self, i):
       
   184         self._size -= 1
       
   185 
       
   186 
       
   187 class UStringIO(list):
       
   188     """a file wrapper which automatically encode unicode string to an encoding
       
   189     specifed in the constructor
       
   190     """
       
   191 
       
   192     def __init__(self, tracewrites=False, *args, **kwargs):
       
   193         self.tracewrites = tracewrites
       
   194         super(UStringIO, self).__init__(*args, **kwargs)
       
   195 
       
   196     def __bool__(self):
       
   197         return True
       
   198 
       
   199     __nonzero__ = __bool__
       
   200 
       
   201     def write(self, value):
       
   202         assert isinstance(value, text_type), u"unicode required not %s : %s"\
       
   203                                      % (type(value).__name__, repr(value))
       
   204         if self.tracewrites:
       
   205             from traceback import format_stack
       
   206             stack = format_stack(None)[:-1]
       
   207             escaped_stack = xml_escape(json_dumps(u'\n'.join(stack)))
       
   208             escaped_html = xml_escape(value).replace('\n', '<br/>\n')
       
   209             tpl = u'<span onclick="alert(%s)">%s</span>'
       
   210             value = tpl % (escaped_stack, escaped_html)
       
   211         self.append(value)
       
   212 
       
   213     def getvalue(self):
       
   214         return u''.join(self)
       
   215 
       
   216     def __repr__(self):
       
   217         return '<%s at %#x>' % (self.__class__.__name__, id(self))
       
   218 
       
   219 
       
   220 class HTMLHead(UStringIO):
       
   221     """wraps HTML header's stream
       
   222 
       
   223     Request objects use a HTMLHead instance to ease adding of
       
   224     javascripts and stylesheets
       
   225     """
       
   226     js_unload_code = u'''if (typeof(pageDataUnloaded) == 'undefined') {
       
   227     jQuery(window).unload(unloadPageData);
       
   228     pageDataUnloaded = true;
       
   229 }'''
       
   230     script_opening = u'<script type="text/javascript">\n'
       
   231     script_closing = u'\n</script>'
       
   232 
       
   233     def __init__(self, req, *args, **kwargs):
       
   234         super(HTMLHead, self).__init__(*args, **kwargs)
       
   235         self.jsvars = []
       
   236         self.jsfiles = []
       
   237         self.cssfiles = []
       
   238         self.ie_cssfiles = []
       
   239         self.post_inlined_scripts = []
       
   240         self.pagedata_unload = False
       
   241         self._cw = req
       
   242         self.datadir_url = req.datadir_url
       
   243 
       
   244     def add_raw(self, rawheader):
       
   245         self.write(rawheader)
       
   246 
       
   247     def define_var(self, var, value, override=True):
       
   248         """adds a javascript var declaration / assginment in the header
       
   249 
       
   250         :param var: the variable name
       
   251         :param value: the variable value (as a raw python value,
       
   252                       it will be jsonized later)
       
   253         :param override: if False, don't set the variable value if the variable
       
   254                          is already defined. Default is True.
       
   255         """
       
   256         self.jsvars.append( (var, value, override) )
       
   257 
       
   258     def add_post_inline_script(self, content):
       
   259         self.post_inlined_scripts.append(content)
       
   260 
       
   261     def add_onload(self, jscode):
       
   262         self.add_post_inline_script(u"""$(cw).one('server-response', function(event) {
       
   263 %s});""" % jscode)
       
   264 
       
   265 
       
   266     def add_js(self, jsfile):
       
   267         """adds `jsfile` to the list of javascripts used in the webpage
       
   268 
       
   269         This function checks if the file has already been added
       
   270         :param jsfile: the script's URL
       
   271         """
       
   272         if jsfile not in self.jsfiles:
       
   273             self.jsfiles.append(jsfile)
       
   274 
       
   275     def add_css(self, cssfile, media='all'):
       
   276         """adds `cssfile` to the list of javascripts used in the webpage
       
   277 
       
   278         This function checks if the file has already been added
       
   279         :param cssfile: the stylesheet's URL
       
   280         """
       
   281         if (cssfile, media) not in self.cssfiles:
       
   282             self.cssfiles.append( (cssfile, media) )
       
   283 
       
   284     def add_ie_css(self, cssfile, media='all', iespec=u'[if lt IE 8]'):
       
   285         """registers some IE specific CSS"""
       
   286         if (cssfile, media, iespec) not in self.ie_cssfiles:
       
   287             self.ie_cssfiles.append( (cssfile, media, iespec) )
       
   288 
       
   289     def add_unload_pagedata(self):
       
   290         """registers onunload callback to clean page data on server"""
       
   291         if not self.pagedata_unload:
       
   292             self.post_inlined_scripts.append(self.js_unload_code)
       
   293             self.pagedata_unload = True
       
   294 
       
   295     def concat_urls(self, urls):
       
   296         """concatenates urls into one url usable by Apache mod_concat
       
   297 
       
   298         This method returns the url without modifying it if there is only
       
   299         one element in the list
       
   300         :param urls: list of local urls/filenames to concatenate
       
   301         """
       
   302         if len(urls) == 1:
       
   303             return urls[0]
       
   304         len_prefix = len(self.datadir_url)
       
   305         concated = u','.join(url[len_prefix:] for url in urls)
       
   306         return (u'%s??%s' % (self.datadir_url, concated))
       
   307 
       
   308     def group_urls(self, urls_spec):
       
   309         """parses urls_spec in order to generate concatenated urls
       
   310         for js and css includes
       
   311 
       
   312         This method checks if the file is local and if it shares options
       
   313         with direct neighbors
       
   314         :param urls_spec: entire list of urls/filenames to inspect
       
   315         """
       
   316         concatable = []
       
   317         prev_islocal = False
       
   318         prev_key = None
       
   319         for url, key in urls_spec:
       
   320             islocal = url.startswith(self.datadir_url)
       
   321             if concatable and (islocal != prev_islocal or key != prev_key):
       
   322                 yield (self.concat_urls(concatable), prev_key)
       
   323                 del concatable[:]
       
   324             if not islocal:
       
   325                 yield (url, key)
       
   326             else:
       
   327                 concatable.append(url)
       
   328             prev_islocal = islocal
       
   329             prev_key = key
       
   330         if concatable:
       
   331             yield (self.concat_urls(concatable), prev_key)
       
   332 
       
   333 
       
   334     def getvalue(self, skiphead=False):
       
   335         """reimplement getvalue to provide a consistent (and somewhat browser
       
   336         optimzed cf. http://stevesouders.com/cuzillion) order in external
       
   337         resources declaration
       
   338         """
       
   339         w = self.write
       
   340         # 1/ variable declaration if any
       
   341         if self.jsvars:
       
   342             if skiphead:
       
   343                 w(u'<cubicweb:script>')
       
   344             else:
       
   345                 w(self.script_opening)
       
   346             for var, value, override in self.jsvars:
       
   347                 vardecl = u'%s = %s;' % (var, json.dumps(value))
       
   348                 if not override:
       
   349                     vardecl = (u'if (typeof %s == "undefined") {%s}' %
       
   350                                (var, vardecl))
       
   351                 w(vardecl + u'\n')
       
   352             if skiphead:
       
   353                 w(u'</cubicweb:script>')
       
   354             else:
       
   355                 w(self.script_closing)
       
   356         # 2/ css files
       
   357         ie_cssfiles = ((x, (y, z)) for x, y, z in self.ie_cssfiles)
       
   358         if self.datadir_url and self._cw.vreg.config['concat-resources']:
       
   359             cssfiles = self.group_urls(self.cssfiles)
       
   360             ie_cssfiles = self.group_urls(ie_cssfiles)
       
   361             jsfiles = (x for x, _ in self.group_urls((x, None) for x in self.jsfiles))
       
   362         else:
       
   363             cssfiles = self.cssfiles
       
   364             jsfiles = self.jsfiles
       
   365         for cssfile, media in cssfiles:
       
   366             w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
       
   367               (media, xml_escape(cssfile)))
       
   368         # 3/ ie css if necessary
       
   369         if self.ie_cssfiles: # use self.ie_cssfiles because `ie_cssfiles` is a genexp
       
   370             for cssfile, (media, iespec) in ie_cssfiles:
       
   371                 w(u'<!--%s>\n' % iespec)
       
   372                 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
       
   373                   (media, xml_escape(cssfile)))
       
   374             w(u'<![endif]--> \n')
       
   375         # 4/ js files
       
   376         for jsfile in jsfiles:
       
   377             if skiphead:
       
   378                 # Don't insert <script> tags directly as they would be
       
   379                 # interpreted directly by some browsers (e.g. IE).
       
   380                 # Use <cubicweb:script> tags instead and let
       
   381                 # `loadAjaxHtmlHead` handle the script insertion / execution.
       
   382                 w(u'<cubicweb:script src="%s"></cubicweb:script>\n' %
       
   383                   xml_escape(jsfile))
       
   384                 # FIXME: a probably better implementation might be to add
       
   385                 #        JS or CSS urls in a JS list that loadAjaxHtmlHead
       
   386                 #        would iterate on and postprocess:
       
   387                 #            cw._ajax_js_scripts.push('myscript.js')
       
   388                 #        Then, in loadAjaxHtmlHead, do something like:
       
   389                 #            jQuery.each(cw._ajax_js_script, jQuery.getScript)
       
   390             else:
       
   391                 w(u'<script type="text/javascript" src="%s"></script>\n' %
       
   392                   xml_escape(jsfile))
       
   393         # 5/ post inlined scripts (i.e. scripts depending on other JS files)
       
   394         if self.post_inlined_scripts:
       
   395             if skiphead:
       
   396                 for script in self.post_inlined_scripts:
       
   397                     w(u'<cubicweb:script>')
       
   398                     w(xml_escape(script))
       
   399                     w(u'</cubicweb:script>')
       
   400             else:
       
   401                 w(self.script_opening)
       
   402                 w(u'\n\n'.join(self.post_inlined_scripts))
       
   403                 w(self.script_closing)
       
   404         # at the start of this function, the parent UStringIO may already have
       
   405         # data in it, so we can't w(u'<head>\n') at the top. Instead, we create
       
   406         # a temporary UStringIO to get the same debugging output formatting
       
   407         # if debugging is enabled.
       
   408         headtag = UStringIO(tracewrites=self.tracewrites)
       
   409         if not skiphead:
       
   410             headtag.write(u'<head>\n')
       
   411             w(u'</head>\n')
       
   412         return headtag.getvalue() + super(HTMLHead, self).getvalue()
       
   413 
       
   414 
       
   415 class HTMLStream(object):
       
   416     """represents a HTML page.
       
   417 
       
   418     This is used my main templates so that HTML headers can be added
       
   419     at any time during the page generation.
       
   420 
       
   421     HTMLStream uses the (U)StringIO interface to be compliant with
       
   422     existing code.
       
   423     """
       
   424 
       
   425     def __init__(self, req):
       
   426         self.tracehtml = req.tracehtml
       
   427         # stream for <head>
       
   428         self.head = req.html_headers
       
   429         # main stream
       
   430         self.body = UStringIO(tracewrites=req.tracehtml)
       
   431         # this method will be assigned to self.w in views
       
   432         self.write = self.body.write
       
   433         self.doctype = u''
       
   434         self._htmlattrs = [('lang', req.lang)]
       
   435         # keep main_stream's reference on req for easier text/html demoting
       
   436         req.main_stream = self
       
   437 
       
   438     @deprecated('[3.17] there are no namespaces in html, xhtml is not served any longer')
       
   439     def add_namespace(self, prefix, uri):
       
   440         pass
       
   441 
       
   442     @deprecated('[3.17] there are no namespaces in html, xhtml is not served any longer')
       
   443     def set_namespaces(self, namespaces):
       
   444         pass
       
   445 
       
   446     def add_htmlattr(self, attrname, attrvalue):
       
   447         self._htmlattrs.append( (attrname, attrvalue) )
       
   448 
       
   449     def set_htmlattrs(self, attrs):
       
   450         self._htmlattrs = attrs
       
   451 
       
   452     def set_doctype(self, doctype, reset_xmldecl=None):
       
   453         self.doctype = doctype
       
   454         if reset_xmldecl is not None:
       
   455             warn('[3.17] xhtml is no more supported',
       
   456                  DeprecationWarning, stacklevel=2)
       
   457 
       
   458     @property
       
   459     def htmltag(self):
       
   460         attrs = ' '.join('%s="%s"' % (attr, xml_escape(value))
       
   461                          for attr, value in self._htmlattrs)
       
   462         if attrs:
       
   463             return '<html xmlns:cubicweb="http://www.cubicweb.org" %s>' % attrs
       
   464         return '<html xmlns:cubicweb="http://www.cubicweb.org">'
       
   465 
       
   466     def getvalue(self):
       
   467         """writes HTML headers, closes </head> tag and writes HTML body"""
       
   468         if self.tracehtml:
       
   469             css = u'\n'.join((u'span {',
       
   470                               u'  font-family: monospace;',
       
   471                               u'  word-break: break-all;',
       
   472                               u'  word-wrap: break-word;',
       
   473                               u'}',
       
   474                               u'span:hover {',
       
   475                               u'  color: red;',
       
   476                               u'  text-decoration: underline;',
       
   477                               u'}'))
       
   478             style = u'<style type="text/css">\n%s\n</style>\n' % css
       
   479             return (u'<!DOCTYPE html>\n'
       
   480                     + u'<html>\n<head>\n%s\n</head>\n' % style
       
   481                     + u'<body>\n'
       
   482                     + u'<span>' + xml_escape(self.doctype) + u'</span><br/>'
       
   483                     + u'<span>' + xml_escape(self.htmltag) + u'</span><br/>'
       
   484                     + self.head.getvalue()
       
   485                     + self.body.getvalue()
       
   486                     + u'<span>' + xml_escape(u'</html>') + u'</span>'
       
   487                     + u'</body>\n</html>')
       
   488         return u'%s\n%s\n%s\n%s\n</html>' % (self.doctype,
       
   489                                              self.htmltag,
       
   490                                              self.head.getvalue(),
       
   491                                              self.body.getvalue())
       
   492 
       
   493 
       
   494 class CubicWebJsonEncoder(json.JSONEncoder):
       
   495     """define a json encoder to be able to encode yams std types"""
       
   496 
       
   497     def default(self, obj):
       
   498         if hasattr(obj, '__json_encode__'):
       
   499             return obj.__json_encode__()
       
   500         if isinstance(obj, datetime.datetime):
       
   501             return ustrftime(obj, '%Y/%m/%d %H:%M:%S')
       
   502         elif isinstance(obj, datetime.date):
       
   503             return ustrftime(obj, '%Y/%m/%d')
       
   504         elif isinstance(obj, datetime.time):
       
   505             return obj.strftime('%H:%M:%S')
       
   506         elif isinstance(obj, datetime.timedelta):
       
   507             return (obj.days * 24 * 60 * 60) + obj.seconds
       
   508         elif isinstance(obj, decimal.Decimal):
       
   509             return float(obj)
       
   510         try:
       
   511             return json.JSONEncoder.default(self, obj)
       
   512         except TypeError:
       
   513             # we never ever want to fail because of an unknown type,
       
   514             # just return None in those cases.
       
   515             return None
       
   516 
       
   517 def json_dumps(value, **kwargs):
       
   518     return json.dumps(value, cls=CubicWebJsonEncoder, **kwargs)
       
   519 
       
   520 
       
   521 class JSString(str):
       
   522     """use this string sub class in values given to :func:`js_dumps` to
       
   523     insert raw javascript chain in some JSON string
       
   524     """
       
   525 
       
   526 def _dict2js(d, predictable=False):
       
   527     if predictable:
       
   528         it = sorted(d.items())
       
   529     else:
       
   530         it = d.items()
       
   531     res = [key + ': ' + js_dumps(val, predictable)
       
   532            for key, val in it]
       
   533     return '{%s}' % ', '.join(res)
       
   534 
       
   535 def _list2js(l, predictable=False):
       
   536     return '[%s]' % ', '.join([js_dumps(val, predictable) for val in l])
       
   537 
       
   538 def js_dumps(something, predictable=False):
       
   539     """similar as :func:`json_dumps`, except values which are instances of
       
   540     :class:`JSString` are expected to be valid javascript and will be output
       
   541     as is
       
   542 
       
   543     >>> js_dumps({'hop': JSString('$.hop'), 'bar': None}, predictable=True)
       
   544     '{bar: null, hop: $.hop}'
       
   545     >>> js_dumps({'hop': '$.hop'})
       
   546     '{hop: "$.hop"}'
       
   547     >>> js_dumps({'hip': {'hop': JSString('momo')}})
       
   548     '{hip: {hop: momo}}'
       
   549     """
       
   550     if isinstance(something, dict):
       
   551         return _dict2js(something, predictable)
       
   552     if isinstance(something, list):
       
   553         return _list2js(something, predictable)
       
   554     if isinstance(something, JSString):
       
   555         return something
       
   556     return json_dumps(something, sort_keys=predictable)
       
   557 
       
   558 PERCENT_IN_URLQUOTE_RE = re.compile(r'%(?=[0-9a-fA-F]{2})')
       
   559 def js_href(javascript_code):
       
   560     """Generate a "javascript: ..." string for an href attribute.
       
   561 
       
   562     Some % which may be interpreted in a href context will be escaped.
       
   563 
       
   564     In an href attribute, url-quotes-looking fragments are interpreted before
       
   565     being given to the javascript engine. Valid url quotes are in the form
       
   566     ``%xx`` with xx being a byte in hexadecimal form. This means that ``%toto``
       
   567     will be unaltered but ``%babar`` will be mangled because ``ba`` is the
       
   568     hexadecimal representation of 186.
       
   569 
       
   570     >>> js_href('alert("babar");')
       
   571     'javascript: alert("babar");'
       
   572     >>> js_href('alert("%babar");')
       
   573     'javascript: alert("%25babar");'
       
   574     >>> js_href('alert("%toto %babar");')
       
   575     'javascript: alert("%toto %25babar");'
       
   576     >>> js_href('alert("%1337%");')
       
   577     'javascript: alert("%251337%");'
       
   578     """
       
   579     return 'javascript: ' + PERCENT_IN_URLQUOTE_RE.sub(r'%25', javascript_code)
       
   580 
       
   581 
       
   582 def parse_repo_uri(uri):
       
   583     """ transform a command line uri into a (protocol, hostport, appid), e.g:
       
   584     <myapp>                      -> 'inmemory', None, '<myapp>'
       
   585     inmemory://<myapp>           -> 'inmemory', None, '<myapp>'
       
   586     """
       
   587     parseduri = urlparse(uri)
       
   588     scheme = parseduri.scheme
       
   589     if scheme == '':
       
   590         return ('inmemory', None, parseduri.path)
       
   591     if scheme == 'inmemory':
       
   592         return (scheme, None, parseduri.netloc)
       
   593     raise NotImplementedError('URI protocol not implemented for `%s`' % uri)
       
   594 
       
   595 
       
   596 
       
   597 logger = getLogger('cubicweb.utils')
       
   598 
       
   599 class QueryCache(object):
       
   600     """ a minimalist dict-like object to be used by the querier
       
   601     and native source (replaces lgc.cache for this very usage)
       
   602 
       
   603     To be efficient it must be properly used. The usage patterns are
       
   604     quite specific to its current clients.
       
   605 
       
   606     The ceiling value should be sufficiently high, else it will be
       
   607     ruthlessly inefficient (there will be warnings when this happens).
       
   608     A good (high enough) value can only be set on a per-application
       
   609     value. A default, reasonnably high value is provided but tuning
       
   610     e.g `rql-cache-size` can certainly help.
       
   611 
       
   612     There are two kinds of elements to put in this cache:
       
   613     * frequently used elements
       
   614     * occasional elements
       
   615 
       
   616     The former should finish in the _permanent structure after some
       
   617     warmup.
       
   618 
       
   619     Occasional elements can be buggy requests (server-side) or
       
   620     end-user (web-ui provided) requests. These have to be cleaned up
       
   621     when they fill the cache, without evicting the useful, frequently
       
   622     used entries.
       
   623     """
       
   624     # quite arbitrary, but we want to never
       
   625     # immortalize some use-a-little query
       
   626     _maxlevel = 15
       
   627 
       
   628     def __init__(self, ceiling=3000):
       
   629         self._max = ceiling
       
   630         # keys belonging forever to this cache
       
   631         self._permanent = set()
       
   632         # mapping of key (that can get wiped) to getitem count
       
   633         self._transient = {}
       
   634         self._data = {}
       
   635         self._lock = Lock()
       
   636 
       
   637     def __len__(self):
       
   638         with self._lock:
       
   639             return len(self._data)
       
   640 
       
   641     def __getitem__(self, k):
       
   642         with self._lock:
       
   643             if k in self._permanent:
       
   644                 return self._data[k]
       
   645             v = self._transient.get(k, _MARKER)
       
   646             if v is _MARKER:
       
   647                 self._transient[k] = 1
       
   648                 return self._data[k]
       
   649             if v > self._maxlevel:
       
   650                 self._permanent.add(k)
       
   651                 self._transient.pop(k, None)
       
   652             else:
       
   653                 self._transient[k] += 1
       
   654             return self._data[k]
       
   655 
       
   656     def __setitem__(self, k, v):
       
   657         with self._lock:
       
   658             if len(self._data) >= self._max:
       
   659                 self._try_to_make_room()
       
   660             self._data[k] = v
       
   661 
       
   662     def pop(self, key, default=_MARKER):
       
   663         with self._lock:
       
   664             try:
       
   665                 if default is _MARKER:
       
   666                     return self._data.pop(key)
       
   667                 return self._data.pop(key, default)
       
   668             finally:
       
   669                 if key in self._permanent:
       
   670                     self._permanent.remove(key)
       
   671                 else:
       
   672                     self._transient.pop(key, None)
       
   673 
       
   674     def clear(self):
       
   675         with self._lock:
       
   676             self._clear()
       
   677 
       
   678     def _clear(self):
       
   679         self._permanent = set()
       
   680         self._transient = {}
       
   681         self._data = {}
       
   682 
       
   683     def _try_to_make_room(self):
       
   684         current_size = len(self._data)
       
   685         items = sorted(self._transient.items(), key=itemgetter(1))
       
   686         level = 0
       
   687         for k, v in items:
       
   688             self._data.pop(k, None)
       
   689             self._transient.pop(k, None)
       
   690             if v > level:
       
   691                 datalen = len(self._data)
       
   692                 if datalen == 0:
       
   693                     return
       
   694                 if (current_size - datalen) / datalen > .1:
       
   695                     break
       
   696                 level = v
       
   697         else:
       
   698             # we removed cruft but everything is permanent
       
   699             if len(self._data) >= self._max:
       
   700                 logger.warning('Cache %s is full.' % id(self))
       
   701                 self._clear()
       
   702 
       
   703     def _usage_report(self):
       
   704         with self._lock:
       
   705             return {'itemcount': len(self._data),
       
   706                     'transientcount': len(self._transient),
       
   707                     'permanentcount': len(self._permanent)}
       
   708 
       
   709     def popitem(self):
       
   710         raise NotImplementedError()
       
   711 
       
   712     def setdefault(self, key, default=None):
       
   713         raise NotImplementedError()
       
   714 
       
   715     def update(self, other):
       
   716         raise NotImplementedError()