Package Products :: Package ZenUtils :: Package extdirect :: Module router |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenUtils.extdirect.router

      1  ##############################################################################
      2  #
      3  # Copyright (C) Zenoss, Inc. 2009, all rights reserved.
      4  #
      5  # This content is made available according to terms specified in
      6  # License.zenoss under the directory where your Zenoss product is installed.
      7  #
      8  ##############################################################################
      9
     10
     11  import inspect
     12  import logging
     13  from Products.ZenUtils.jsonutils import json, unjson
     14  import transaction
     15  from uuid import uuid4
     16
     17  log = logging.getLogger('extdirect')
     18
     19 -class DirectException(Exception):
     20      pass
     21
     22
     23
     24 -class DirectResponse(object):
     25      """
     26      Encapsulation of the simple protocol used to send results and messages to
     27      the front end.
     28      """
     29      _data = None
     30 -    def __init__(self, msg=None, success=True, **kwargs):
     31          self._data = {}
     32          self._data.update(kwargs)
     33          self._data['success'] = success
     34          if msg:
     35              self._data['msg'] = msg
     36
     37      @property
     38 -    def type(self):
     39          return self._data.get('type', None)
     40
     41      @property
     42 -    def data(self):
     43          return self._data
     44
     45 -    def __setitem__(self, key, value):
     46          self._data[key] = value
     47
     48      @staticmethod
     49 -    def exception(exception, message=None, **kwargs):
     50          """
     51          If an exception is raised, use this! It will cause the transaction to be rolled
     52          back.
     53          """
     54          msg = exception.__class__.__name__ + ': ' + str(exception)
     55          if message:
     56              msg = '%s: %s' % (message, msg)
     57          return DirectResponse(msg, success=False, type='exception', **kwargs)
     58
     59      @staticmethod
     60 -    def fail(msg=None, **kwargs):
     61          """
     62          If the request has failed for a non-database-impacting reason
     63          (e.g., "device already exists"), use this.
     64          """
     65          return DirectResponse(msg, success=False, type='error', **kwargs)
     66
     67      @staticmethod
     68 -    def succeed(msg=None, **kwargs):
     69          return DirectResponse(msg, success=True, **kwargs)
     70
     71 -    def __json__(self):
     72          return self.data
     73
     74 -class DirectMethodResponse(object):
     75      """
     76      Encapsulation of the response of a method call for Ext Direct.
     77      """
     78 -    def __init__(self, tid, action, method, uuid):
     79          self.tid = tid
     80          self.type = 'rpc'
     81          self.action = action
     82          self.method = method
     83          self.uuid = uuid
     84          self.result = None
     85
     86 -    def __json__(self):
     87          return {
     88              'tid': self.tid,
     89              'type' : self.type,
     90              'action': self.action,
     91              'method': self.method,
     92              'uuid': self.uuid,
     93              'result': self.result
     94          }
     95
     96 -class DirectRouter(object):
     97      """
     98      Basic Ext.Direct router class.
     99
    100      Ext.Direct allows one to create an API that communicates with a single URL,
    101      which then routes requests to the appropriate method. The client-side API
    102      object matches the server-side API object.
    103
    104      This base class parses an Ext.Direct request, which contains the name of
    105      the method and any data that should be passed, and routes the data to the
    106      approriate method. It then receives the output of that call and puts it
    107      into the data structure expected by Ext.Direct.
    108
    109      Call an instance of this class with the JSON from an Ext.Direct request.
    110      """
    111
    112      @json
    113 -    def __call__(self, body):
    114          # Decode the request data
    115          try:
    116              body = unjson(body)
    117          except Exception:
    118              raise DirectException("Request body is not unjson()-able: %s" % body)
    119          self._body = body
    120
    121          if isinstance(body, list):
    122              directRequests = body
    123          elif isinstance(body, dict):
    124              directRequests = [body]
    125          else:
    126              raise DirectException("Body is not a supported type: %s" % body)
    127
    128          directResponses = []
    129          for directRequest in directRequests:
    130              directResponses.append(self._processDirectRequest(directRequest))
    131
    132          if len(directResponses) == 1:
    133              directResponses = directResponses[0]
    134
    135          return directResponses
    136
    137 -    def _processDirectRequest(self, directRequest):
    138          savepoint = transaction.savepoint()
    139
    140          # Add a UUID so we can track it in the logs
    141          uuid = str(uuid4())
    142
    143          # Double-check that this request is meant for this class
    144          action = directRequest.get('action')
    145          clsname = self.__class__.__name__
    146          if action != clsname:
    147              raise DirectException(("Action specified in request ('%s') is"
    148                                    " not named %s.") % (action, clsname))
    149
    150          # Pull out the method name and make sure it exists on this class
    151          method = directRequest.get('method')
    152          if not method:
    153              raise DirectException("No method specified. Is this a valid"
    154                                    " Ext.Direct request?")
    155          try:
    156              _targetfn = getattr(self, method)
    157          except AttributeError:
    158              raise DirectException("'%s' is not the name of a method on %s" % (
    159                  method, clsname
    160              ))
    161
    162          # Pull out any arguments. Sent as an array containing a hash map, so
    163          # get the first member.
    164          data = directRequest.get('data')
    165          if not data:
    166              data = {}
    167          else:
    168              data = data[0]
    169
    170          if isinstance(data, (int, basestring)):
    171              data = {'id': data}
    172
    173          # Cast all keys as strings, in case of encoding or other wrinkles
    174          data = dict((str(k), v) for k,v in data.iteritems())
    175          self._data = data
    176          response = DirectMethodResponse(tid=directRequest['tid'], method=method, action=action, uuid=uuid)
    177
    178          # Finally, call the target method, passing in the data
    179          try:
    180              response.result = _targetfn(**data)
    181          except Exception as e:
    182              import traceback
    183              log.info("Direct request failed: {0}: {1[action]}.{1[method]} {1[data]}".format(e, directRequest))
    184              log.info("DirectRouter suppressed the following exception (Response {0}):\n{1}".format(response.uuid, traceback.format_exc()))
    185              response.result = DirectResponse.exception(e)
    186
    187          if isinstance(response.result, DirectResponse) and response.result.type == 'exception':
    188              savepoint.rollback()
    189
    190          return response
    191
    192
    193 -class DirectProviderDefinition(object):
    194      """
    195      Turns a L{DirectRouter} subclass into JavaScript object representing the
    196      config of the client-side API.
    197
    198      Inspects the given subclass and retrieves the names of all public methods,
    199      then defines those as actions on the Ext.Direct provider, and creates the
    200      JS that adds the provider.
    201
    202      See http://www.sencha.com/products/extjs/extdirect for a full explanation of
    203      protocols and features of Ext.Direct.
    204      """
    205 -    def __init__(self, routercls, url, timeout, ns=None):
    206          """
    207          @param routercls: A L{DirectRouter} subclass
    208          @type routercls: class
    209          @param url: The url at which C{routercls} is available
    210          @type url: str
    211          @param ns: The client-side namespace in which the provider should live.
    212                     The provider will be available at [ns].[routercls.__name__].
    213                     For example, if ns is 'Zenoss.remote' and routercls is named
    214                     'EventConsole', client-side code would call
    215                     C{Zenoss.remote.EventConsole.my_method(params, callback)}.
    216          """
    217          self.routercls = routercls
    218          self.url = url
    219          self.ns = ns
    220          self.timeout = timeout
    221
    222 -    def _gen_public_methods(self):
    223          for name, value in inspect.getmembers(self.routercls):
    224              if name.startswith("_"):
    225                  continue
    226              if inspect.ismethod(value):
    227                  yield name, value
    228
    229 -    def _gen_method_infos(self, strategy):
    230          for name, method in self._gen_public_methods():
    231              method_info = {'name': name}
    232              strategy(method_info, method)
    233              yield method_info
    234
    235 -    def _extdirect_config(self):
    236          def strategy(method_info, method):
    237              method_info["len"] = 1
    238          method_infos = self._gen_method_infos(strategy)
    239          config = {
    240              'id': self.routercls.__name__,
    241              'type': 'remoting',
    242              'url': self.url,
    243              'timeout': self.timeout,
    244              'enableBuffer': 100,
    245              'actions': {
    246                  self.routercls.__name__: list(method_infos)
    247              }
    248          }
    249          if self.ns:
    250              config['namespace'] = self.ns
    251          return config
    252
    253 -    def _jsonapi_config(self):
    254          def strategy(method_info, method):
    255              method_info['args'] = inspect.formatargspec(*inspect.getargspec(method))
    256              method_info['doc'] = method.__doc__
    257          method_infos = self._gen_method_infos(strategy)
    258          config = {"action": self.routercls.__name__,
    259                    "url": self.url,
    260                    "methods": list(method_infos),
    261                   }
    262          return config
    263
    264 -    def render(self):
    265          """
    266          Generate and return an Ext.Direct provider definition, wrapped in a
    267          <script> tag and ready for inclusion in an HTML document.
    268          """
    269          config = self._extdirect_config()
    270          source = "\nExt.Direct.addProvider(%s);\n" % json(config)
    271          return source.strip()
    272
    273 -    def render_jsonapi(self):
    274          """
    275          Generate an Ext.Direct provider definition for the python client
    276          """
    277          config = self._jsonapi_config()
    278          source = json(config)
    279          return source
    280