1# -*- test-case-name: nevow.test.test_athena -*-
2
3import itertools, os, re, warnings, StringIO
4
5from zope.interface import implements
6
7from twisted.internet import defer, error, reactor
8from twisted.python import log, failure, context
9from twisted.python.util import sibpath
10from twisted import plugin
11
12from nevow import inevow, plugins, flat, _flat
13from nevow import rend, loaders, static
14from nevow import json, util, tags, guard, stan
15from nevow.util import CachedFile
16from nevow.useragent import UserAgent, browsers
17from nevow.url import here, URL
18
19from nevow.page import Element, renderer
20
21ATHENA_XMLNS_URI = "http://divmod.org/ns/athena/0.7"
22ATHENA_RECONNECT = "__athena_reconnect__"
23
24expose = util.Expose(
25    """
26    Allow one or more methods to be invoked by the client::
27
28    | class Foo(LiveElement):
29    |     def twiddle(self, x, y):
30    |         ...
31    |     def frob(self, a, b):
32    |         ...
33    |     expose(twiddle, frob)
34
35    The Widget for Foo will be allowed to invoke C{twiddle} and C{frob}.
36    """)
37
38
39
40class OrphanedFragment(Exception):
41    """
42    Raised when an operation requiring a parent is attempted on an unattached
43    child.
44    """
45
46
47
48class LivePageError(Exception):
49    """
50    Base exception for LivePage errors.
51    """
52    jsClass = u'Divmod.Error'
53
54
55
56class NoSuchMethod(LivePageError):
57    """
58    Raised when an attempt is made to invoke a method which is not defined or
59    exposed.
60    """
61    jsClass = u'Nevow.Athena.NoSuchMethod'
62
63    def __init__(self, objectID, methodName):
64        self.objectID = objectID
65        self.methodName = methodName
66        LivePageError.__init__(self, objectID, methodName)
67
68
69
70def neverEverCache(request):
71    """
72    Set headers to indicate that the response to this request should never,
73    ever be cached.
74    """
75    request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
76    request.setHeader('Pragma', 'no-cache')
77
78
79def activeChannel(request):
80    """
81    Mark this connection as a 'live' channel by setting the Connection: close
82    header and flushing all headers immediately.
83    """
84    request.setHeader("Connection", "close")
85    request.write('')
86
87
88
89class MappingResource(object):
90    """
91    L{inevow.IResource} which looks up segments in a mapping between symbolic
92    names and the files they correspond to.
93
94    @type mapping: C{dict}
95    @ivar mapping: A map between symbolic, requestable names (eg,
96    'Nevow.Athena') and C{str} instances which name files containing data
97    which should be served in response.
98    """
99    implements(inevow.IResource)
100
101    def __init__(self, mapping):
102        self.mapping = mapping
103
104
105    def renderHTTP(self, ctx):
106        return rend.FourOhFour()
107
108
109    def resourceFactory(self, fileName):
110        """
111        Retrieve an L{inevow.IResource} which will render the contents of
112        C{fileName}.
113        """
114        return static.File(fileName)
115
116
117    def locateChild(self, ctx, segments):
118        try:
119            impl = self.mapping[segments[0]]
120        except KeyError:
121            return rend.NotFound
122        else:
123            return self.resourceFactory(impl), []
124
125
126
127def _dependencyOrdered(coll, memo):
128    """
129    @type coll: iterable of modules
130    @param coll: The initial sequence of modules.
131
132    @type memo: C{dict}
133    @param memo: A dictionary mapping module names to their dependencies that
134                 will be used as a mutable cache.
135    """
136
137
138
139class AthenaModule(object):
140    """
141    A representation of a chunk of stuff in a file which can depend on other
142    chunks of stuff in other files.
143    """
144    _modules = {}
145
146    lastModified = 0
147    deps = None
148    packageDeps = []
149
150    def getOrCreate(cls, name, mapping):
151        # XXX This implementation of getOrCreate precludes the
152        # simultaneous co-existence of several different package
153        # namespaces.
154        if name in cls._modules:
155            return cls._modules[name]
156        mod = cls._modules[name] = cls(name, mapping)
157        return mod
158    getOrCreate = classmethod(getOrCreate)
159
160
161    def __init__(self, name, mapping):
162        self.name = name
163        self.mapping = mapping
164
165        if '.' in name:
166            parent = '.'.join(name.split('.')[:-1])
167            self.packageDeps = [self.getOrCreate(parent, mapping)]
168
169        self._cache = CachedFile(self.mapping[self.name], self._getDeps)
170
171
172    def __repr__(self):
173        return '%s(%r)' % (self.__class__.__name__, self.name,)
174
175
176    _importExpression = re.compile('^// import (.+)$', re.MULTILINE)
177    def _extractImports(self, fileObj):
178        s = fileObj.read()
179        for m in self._importExpression.finditer(s):
180            yield self.getOrCreate(m.group(1).decode('ascii'), self.mapping)
181
182
183
184    def _getDeps(self, jsFile):
185        """
186        Calculate our dependencies given the path to our source.
187        """
188        depgen = self._extractImports(file(jsFile, 'rU'))
189        return self.packageDeps + dict.fromkeys(depgen).keys()
190
191
192    def dependencies(self):
193        """
194        Return a list of names of other JavaScript modules we depend on.
195        """
196        return self._cache.load()
197
198
199    def allDependencies(self, memo=None):
200        """
201        Return the transitive closure of dependencies, including this module.
202
203        The transitive dependencies for this module will be ordered such that
204        any particular module is located after all of its dependencies, with no
205        module occurring more than once.
206
207        The dictionary passed in for C{memo} will be modified in-place; if it
208        is reused across multiple calls, dependencies calculated during a
209        previous invocation will not be recalculated again.
210
211        @type memo: C{dict} of C{str: list of AthenaModule}
212        @param memo: A dictionary mapping module names to the modules they
213                     depend on that will be used as a mutable cache.
214
215        @rtype: C{list} of C{AthenaModule}
216        """
217        if memo is None:
218            memo = {}
219        ordered = []
220
221        def _getDeps(dependent):
222            if dependent.name in memo:
223                deps = memo[dependent.name]
224            else:
225                memo[dependent.name] = deps = dependent.dependencies()
226            return deps
227
228        def _insertDep(dependent):
229            if dependent not in ordered:
230                for dependency in _getDeps(dependent):
231                    _insertDep(dependency)
232                ordered.append(dependent)
233
234        _insertDep(self)
235        return ordered
236
237
238
239class JSModule(AthenaModule):
240    """
241    L{AthenaModule} subclass for dealing with Javascript modules.
242    """
243    _modules= {}
244
245
246
247class CSSModule(AthenaModule):
248    """
249    L{AthenaModule} subclass for dealing with CSS modules.
250    """
251    _modules = {}
252
253
254
255class JSPackage(object):
256    """
257    A Javascript package.
258
259    @type mapping: C{dict}
260    @ivar mapping: Mapping between JS module names and C{str} representing
261    filesystem paths containing their implementations.
262    """
263    implements(plugin.IPlugin, inevow.IJavascriptPackage)
264
265    def __init__(self, mapping):
266        self.mapping = mapping
267
268
269
270def _collectPackageBelow(baseDir, extension):
271    """
272    Assume a filesystem package hierarchy starting at C{baseDir}.  Collect all
273    files within it ending with C{extension} into a mapping between
274    dot-separated symbolic module names and their corresponding filesystem
275    paths.
276
277    Note that module/package names beginning with . are ignored.
278
279    @type baseDir: C{str}
280    @param baseDir: A path to the root of a package hierarchy on a filesystem.
281
282    @type extension: C{str}
283    @param extension: The filename extension we're interested in (e.g. 'css'
284    or 'js').
285
286    @rtype: C{dict}
287    @return: Mapping between C{unicode} module names and their corresponding
288    C{str} filesystem paths.
289    """
290    mapping = {}
291    EMPTY = sibpath(__file__, 'empty-module.' + extension)
292
293    _revMap = {baseDir: ''}
294    for (root, dirs, filenames) in os.walk(baseDir):
295        stem = _revMap[root]
296        dirs[:] = [d for d in dirs if not d.startswith('.')]
297
298        for dir in dirs:
299            name = stem + dir
300            path = os.path.join(root, dir, '__init__.' + extension)
301            if not os.path.exists(path):
302                path = EMPTY
303            mapping[unicode(name, 'ascii')] = path
304            _revMap[os.path.join(root, dir)] = name + '.'
305
306        for fn in filenames:
307            if fn.startswith('.'):
308                continue
309
310            if fn == '__init__.' + extension:
311                continue
312
313            if not fn.endswith('.' + extension):
314                continue
315
316            name = stem + fn[:-(len(extension) + 1)]
317            path = os.path.join(root, fn)
318            mapping[unicode(name, 'ascii')] = path
319    return mapping
320
321
322
323class AutoJSPackage(object):
324    """
325    A L{inevow.IJavascriptPackage} implementation that scans an on-disk
326    hierarchy locating modules and packages.
327
328    @type baseDir: C{str}
329    @ivar baseDir: A path to the root of a JavaScript packages/modules
330    filesystem hierarchy.
331    """
332    implements(plugin.IPlugin, inevow.IJavascriptPackage)
333
334    def __init__(self, baseDir):
335        self.mapping = _collectPackageBelow(baseDir, 'js')
336
337
338
339class AutoCSSPackage(object):
340    """
341    Like L{AutoJSPackage}, but for CSS packages.  Modules within this package
342    can be referenced by L{LivePage.cssModule} or L{LiveElement.cssModule}.
343    """
344    implements(plugin.IPlugin, inevow.ICSSPackage)
345
346    def __init__(self, baseDir):
347        self.mapping = _collectPackageBelow(baseDir, 'css')
348
349
350
351def allJavascriptPackages():
352    """
353    Return a dictionary mapping JavaScript module names to local filenames
354    which implement those modules.  This mapping is constructed from all the
355    C{IJavascriptPackage} plugins available on the system.  It also includes
356    C{Nevow.Athena} as a special case.
357    """
358    d = {}
359    for p in plugin.getPlugIns(inevow.IJavascriptPackage, plugins):
360        d.update(p.mapping)
361    return d
362
363
364
365def allCSSPackages():
366    """
367    Like L{allJavascriptPackages}, but for CSS packages.
368    """
369    d = {}
370    for p in plugin.getPlugIns(inevow.ICSSPackage, plugins):
371        d.update(p.mapping)
372    return d
373
374
375
376class JSDependencies(object):
377    """
378    Keeps track of which JavaScript files depend on which other
379    JavaScript files (because JavaScript is a very poor language and
380    cannot do this itself).
381    """
382    _loadPlugins = False
383
384    def __init__(self, mapping=None):
385        if mapping is None:
386            self.mapping = {}
387            self._loadPlugins = True
388        else:
389            self.mapping = mapping
390
391
392    def getModuleForName(self, className):
393        """
394        Return the L{JSModule} most likely to define the given name.
395        """
396        if self._loadPlugins:
397            self.mapping.update(allJavascriptPackages())
398            self._loadPlugins = False
399
400        jsMod = className
401        while jsMod:
402            try:
403                self.mapping[jsMod]
404            except KeyError:
405                if '.' not in jsMod:
406                    break
407                jsMod = '.'.join(jsMod.split('.')[:-1])
408            else:
409                return JSModule.getOrCreate(jsMod, self.mapping)
410        raise RuntimeError("Unknown class: %r" % (className,))
411    getModuleForClass = getModuleForName
412
413
414jsDeps = JSDependencies()
415
416
417
418class CSSRegistry(object):
419    """
420    Keeps track of a set of CSS modules.
421    """
422    def __init__(self, mapping=None):
423        if mapping is None:
424            mapping = {}
425            loadPlugins = True
426        else:
427            loadPlugins = False
428        self.mapping = mapping
429        self._loadPlugins = loadPlugins
430
431
432    def getModuleForName(self, moduleName):
433        """
434        Turn a CSS module name into an L{AthenaModule}.
435
436        @type moduleName: C{unicode}
437
438        @rtype: L{CSSModule}
439        """
440        if self._loadPlugins:
441            self.mapping.update(allCSSPackages())
442            self._loadPlugins = False
443        try:
444            self.mapping[moduleName]
445        except KeyError:
446            raise RuntimeError('Unknown CSS module: %r' % (moduleName,))
447        return CSSModule.getOrCreate(moduleName, self.mapping)
448
449_theCSSRegistry = CSSRegistry()
450
451
452
453class JSException(Exception):
454    """
455    Exception class to wrap remote exceptions from JavaScript.
456    """
457
458
459
460class JSCode(object):
461    """
462    Class for mock code objects in mock JS frames.
463    """
464    def __init__(self, name, filename):
465        self.co_name = name
466        self.co_filename = filename
467
468
469
470class JSFrame(object):
471    """
472    Class for mock frame objects in JS client-side traceback wrappers.
473    """
474    def __init__(self, func, fname, ln):
475        self.f_back = None
476        self.f_locals = {}
477        self.f_globals = {}
478        self.f_code = JSCode(func, fname)
479        self.f_lineno = ln
480
481
482
483class JSTraceback(object):
484    """
485    Class for mock traceback objects representing client-side JavaScript
486    tracebacks.
487    """
488    def __init__(self, frame, ln):
489        self.tb_frame = frame
490        self.tb_lineno = ln
491        self.tb_next = None
492
493
494
495def parseStack(stack):
496    """
497    Extract function name, file name, and line number information from the
498    string representation of a JavaScript trace-back.
499    """
500    frames = []
501    for line in stack.split('\n'):
502        if '@' not in line:
503            continue
504        func, rest = line.split('@', 1)
505        if ':' not in rest:
506            continue
507
508        divide = rest.rfind(':')
509        if divide == -1:
510            fname, ln = rest, ''
511        else:
512            fname, ln = rest[:divide], rest[divide + 1:]
513        ln = int(ln)
514        frames.insert(0, (func, fname, ln))
515    return frames
516
517
518
519def buildTraceback(frames, modules):
520    """
521    Build a chain of mock traceback objects from a serialized Error (or other
522    exception) object, and return the head of the chain.
523    """
524    last = None
525    first = None
526    for func, fname, ln in frames:
527        fname = modules.get(fname.split('/')[-1], fname)
528        frame = JSFrame(func, fname, ln)
529        tb = JSTraceback(frame, ln)
530        if last:
531            last.tb_next = tb
532        else:
533            first = tb
534        last = tb
535    return first
536
537
538
539def getJSFailure(exc, modules):
540    """
541    Convert a serialized client-side exception to a Failure.
542    """
543    text = '%s: %s' % (exc[u'name'], exc[u'message'])
544
545    frames = []
546    if u'stack' in exc:
547        frames = parseStack(exc[u'stack'])
548
549    return failure.Failure(JSException(text), exc_tb=buildTraceback(frames, modules))
550
551
552
553class LivePageTransport(object):
554    implements(inevow.IResource)
555
556    def __init__(self, messageDeliverer, useActiveChannels=True):
557        self.messageDeliverer = messageDeliverer
558        self.useActiveChannels = useActiveChannels
559
560
561    def locateChild(self, ctx, segments):
562        return rend.NotFound
563
564
565    def renderHTTP(self, ctx):
566        req = inevow.IRequest(ctx)
567        neverEverCache(req)
568        if self.useActiveChannels:
569            activeChannel(req)
570
571        requestContent = req.content.read()
572        messageData = json.parse(requestContent)
573
574        response = self.messageDeliverer.basketCaseReceived(ctx, messageData)
575        response.addCallback(json.serialize)
576        req.notifyFinish().addErrback(lambda err: self.messageDeliverer._unregisterDeferredAsOutputChannel(response))
577        return response
578
579
580
581class LivePageFactory:
582    noisy = True
583
584    def __init__(self):
585        self.clients = {}
586
587    def addClient(self, client):
588        clientID = self._newClientID()
589        self.clients[clientID] = client
590        if self.noisy:
591            log.msg("Rendered new LivePage %r: %r" % (client, clientID))
592        return clientID
593
594    def getClient(self, clientID):
595        return self.clients[clientID]
596
597    def removeClient(self, clientID):
598        # State-tracking bugs may make it tempting to make the next line a
599        # 'pop', but it really shouldn't be; if the Page instance with this
600        # client ID is already gone, then it should be gone, which means that
601        # this method can't be called with that argument.
602        del self.clients[clientID]
603        if self.noisy:
604            log.msg("Disconnected old LivePage %r" % (clientID,))
605
606    def _newClientID(self):
607        return guard._sessionCookie()
608
609
610_thePrivateAthenaResource = static.File(util.resource_filename('nevow', 'athena_private'))
611
612
613class ConnectFailed(Exception):
614    pass
615
616
617class ConnectionLost(Exception):
618    pass
619
620
621CLOSE = u'close'
622UNLOAD = u'unload'
623
624class ReliableMessageDelivery(object):
625    """
626    A reliable message delivery abstraction over a possibly unreliable transport.
627
628    @type livePage: L{LivePage}
629    @ivar livePage: The page this delivery is associated with.
630
631    @type connectTimeout: C{int}
632    @ivar connectTimeout: The amount of time (in seconds) to wait for the
633        initial connection, before timing out.
634
635    @type transportlessTimeout: C{int}
636    @ivar transportlessTimeout: The amount of time (in seconds) to wait for
637        another transport to connect if none are currently connected, before
638        timing out.
639
640    @type idleTimeout: C{int}
641    @ivar idleTimeout: The maximum amount of time (in seconds) to leave a
642        connected transport, before sending a noop response.
643
644    @type connectionLost: callable or C{None}
645    @ivar connectionLost: A callback invoked with a L{failure.Failure} if the
646        connection with the client is lost (due to a timeout, for example).
647
648    @type scheduler: callable or C{None}
649    @ivar scheduler: If passed, this is used in place of C{reactor.callLater}.
650
651    @type connectionMade: callable or C{None}
652    @ivar connectionMade: A callback invoked with no arguments when it first
653        becomes possible to to send a message to the client.
654    """
655    _paused = 0
656    _stopped = False
657    _connected = False
658
659    outgoingAck = -1            # sequence number which has been acknowledged
660                                # by this end of the connection.
661
662    outgoingSeq = -1            # sequence number of the next message to be
663                                # added to the outgoing queue.
664
665    def __init__(self,
666                 livePage,
667                 connectTimeout=60, transportlessTimeout=30, idleTimeout=300,
668                 connectionLost=None,
669                 scheduler=None,
670                 connectionMade=None):
671        self.livePage = livePage
672        self.messages = []
673        self.outputs = []
674        self.connectTimeout = connectTimeout
675        self.transportlessTimeout = transportlessTimeout
676        self.idleTimeout = idleTimeout
677        if scheduler is None:
678            scheduler = reactor.callLater
679        self.scheduler = scheduler
680        self._transportlessTimeoutCall = self.scheduler(self.connectTimeout, self._connectTimedOut)
681        self.connectionMade = connectionMade
682        self.connectionLost = connectionLost
683
684
685    def _connectTimedOut(self):
686        self._transportlessTimeoutCall = None
687        self.connectionLost(failure.Failure(ConnectFailed("Timeout")))
688
689
690    def _transportlessTimedOut(self):
691        self._transportlessTimeoutCall = None
692        self.connectionLost(failure.Failure(ConnectionLost("Timeout")))
693
694
695    def _idleTimedOut(self):
696        output, timeout = self.outputs.pop(0)
697        if not self.outputs:
698            self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
699        output([self.outgoingAck, []])
700
701
702    def _sendMessagesToOutput(self, output):
703        log.msg(athena_send_messages=True, count=len(self.messages))
704        output([self.outgoingAck, self.messages])
705
706
707    def pause(self):
708        self._paused += 1
709
710
711    def _trySendMessages(self):
712        """
713        If we have pending messages and there is an available transport, then
714        consume it to send the messages.
715        """
716        if self.messages and self.outputs:
717            output, timeout = self.outputs.pop(0)
718            timeout.cancel()
719            if not self.outputs:
720                self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
721            self._sendMessagesToOutput(output)
722
723
724    def unpause(self):
725        """
726        Decrement the pause counter and if the resulting state is not still
727        paused try to flush any pending messages and expend excess outputs.
728        """
729        self._paused -= 1
730        if self._paused == 0:
731            self._trySendMessages()
732            self._flushOutputs()
733
734
735    def addMessage(self, msg):
736        if self._stopped:
737            return
738
739        self.outgoingSeq += 1
740        self.messages.append((self.outgoingSeq, msg))
741        if not self._paused and self.outputs:
742            output, timeout = self.outputs.pop(0)
743            timeout.cancel()
744            if not self.outputs:
745                self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
746            self._sendMessagesToOutput(output)
747
748
749    def addOutput(self, output):
750        if self._transportlessTimeoutCall is not None:
751            self._transportlessTimeoutCall.cancel()
752            self._transportlessTimeoutCall = None
753        if not self._paused and self.messages:
754            self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
755            self._sendMessagesToOutput(output)
756        else:
757            if self._stopped:
758                self._sendMessagesToOutput(output)
759            else:
760                self.outputs.append((output, self.scheduler(self.idleTimeout, self._idleTimedOut)))
761
762
763    def close(self):
764        assert not self._stopped, "Cannot multiply stop ReliableMessageDelivery"
765        self.addMessage((CLOSE, []))
766        self._stopped = True
767        while self.outputs:
768            output, timeout = self.outputs.pop(0)
769            timeout.cancel()
770            self._sendMessagesToOutput(output)
771        self.outputs = None
772        if self._transportlessTimeoutCall is not None:
773            self._transportlessTimeoutCall.cancel()
774            self._transportlessTimeoutCall = None
775
776
777    def _unregisterDeferredAsOutputChannel(self, deferred):
778        for i in xrange(len(self.outputs)):
779            if self.outputs[i][0].im_self is deferred:
780                output, timeout = self.outputs.pop(i)
781                timeout.cancel()
782                break
783        else:
784            return
785        if not self.outputs:
786            self._transportlessTimeoutCall = self.scheduler(self.transportlessTimeout, self._transportlessTimedOut)
787
788
789    def _createOutputDeferred(self):
790        """
791        Create a new deferred, attaching it as an output.  If the current
792        state is not paused, try to flush any pending messages and expend
793        any excess outputs.
794        """
795        d = defer.Deferred()
796        self.addOutput(d.callback)
797        if not self._paused and self.outputs:
798            self._trySendMessages()
799            self._flushOutputs()
800        return d
801
802
803    def _flushOutputs(self):
804        """
805        Use up all except for one output.
806
807        This provides ideal behavior for the default HTTP client
808        configuration, since only a maximum of two simultaneous connections
809        are allowed.  The remaining one output will let us signal the client
810        at will without preventing the client from establishing new
811        connections.
812        """
813        if self.outputs is None:
814            return
815        while len(self.outputs) > 1:
816            output, timeout = self.outputs.pop(0)
817            timeout.cancel()
818            output([self.outgoingAck, []])
819
820
821    def basketCaseReceived(self, ctx, basketCase):
822        """
823        This is called when some random JSON data is received from an HTTP
824        request.
825
826        A 'basket case' is currently a data structure of the form [ackNum, [[1,
827        message], [2, message], [3, message]]]
828
829        Its name is highly informal because unless you are maintaining this
830        exact code path, you should not encounter it.  If you do, something has
831        gone *badly* wrong.
832        """
833        if not self._connected:
834            self._connected = True
835            self.connectionMade()
836
837        ack, incomingMessages = basketCase
838
839        outgoingMessages = self.messages
840
841        # dequeue messages that our client certainly knows about.
842        while outgoingMessages and outgoingMessages[0][0] <= ack:
843            outgoingMessages.pop(0)
844
845        if incomingMessages:
846            log.msg(athena_received_messages=True, count=len(incomingMessages))
847            if incomingMessages[0][0] == UNLOAD:
848                # Page-unload messages are special, because they are not part
849                # of the normal message stream: they are a notification that
850                # the message stream can't continue.  Browser bugs force us to
851                # handle this as quickly as possible, since the browser can
852                # lock up hard while waiting for a response to this message
853                # (and the user has already navigated away from the page, so
854                # there's no useful communication that can take place any more)
855                # so only one message is allowed.  In the actual Athena JS,
856                # only one is ever sent, so there is no need to handle more.
857                # The structure of the packet is preserved for symmetry,
858                # however, if we ever need to expand on it.  Realistically, the
859                # only message that can be usefully processed here is CLOSE.
860                msg = incomingMessages[0][1]
861                self.livePage.liveTransportMessageReceived(ctx, msg)
862                return self._createOutputDeferred()
863            elif self.outgoingAck + 1 >= incomingMessages[0][0]:
864                lastSentAck = self.outgoingAck
865                self.outgoingAck = max(incomingMessages[-1][0], self.outgoingAck)
866                self.pause()
867                try:
868                    for (seq, msg) in incomingMessages:
869                        if seq > lastSentAck:
870                            self.livePage.liveTransportMessageReceived(ctx, msg)
871                    d = self._createOutputDeferred()
872                finally:
873                    self.unpause()
874            else:
875                d = defer.succeed([self.outgoingAck, []])
876                log.msg(
877                    "Sequence gap! %r went from %s to %s" %
878                    (self.livePage.clientID,
879                     self.outgoingAck,
880                     incomingMessages[0][0]))
881        else:
882            d = self._createOutputDeferred()
883
884        return d
885
886
887BOOTSTRAP_NODE_ID = 'athena:bootstrap'
888BOOTSTRAP_STATEMENT = ("eval(document.getElementById('" + BOOTSTRAP_NODE_ID +
889                       "').getAttribute('payload'));")
890
891class _HasJSClass(object):
892    """
893    A utility to share some code between the L{LivePage}, L{LiveElement}, and
894    L{LiveFragment} classes which all have a jsClass attribute that represents
895    a JavaScript class.
896
897    @ivar jsClass: a JavaScript class.
898    @type jsClass: L{unicode}
899    """
900
901    def _getModuleForClass(self):
902        """
903        Get a L{JSModule} object for the class specified by this object's
904        jsClass string.
905        """
906        return jsDeps.getModuleForClass(self.jsClass)
907
908
909    def _getRequiredModules(self, memo):
910        """
911        Return a list of two-tuples containing module names and URLs at which
912        those modules are accessible.  All of these modules must be loaded into
913        the page before this Fragment's widget can be instantiated.  modules
914        are accessible.
915        """
916        return [
917            (dep.name, self.page.getJSModuleURL(dep.name))
918            for dep
919            in self._getModuleForClass().allDependencies(memo)
920            if self.page._shouldInclude(dep.name)]
921
922
923
924def jsModuleDeclaration(name):
925    """
926    Generate Javascript for a module declaration.
927    """
928    var = ''
929    if '.' not in name:
930        var = 'var '
931    return '%s%s = {"__name__": "%s"};' % (var, name, name)
932
933
934
935class _HasCSSModule(object):
936    """
937    C{cssModule}-handling code common to L{LivePage}, L{LiveElement} and
938    L{LiveFragment}.
939
940    @ivar cssModule: A CSS module name.
941    @type cssModule: C{unicode} or C{NoneType}
942    """
943    def _getRequiredCSSModules(self, memo):
944        """
945        Return a list of CSS module URLs.
946
947        @rtype: C{list} of L{url.URL}
948        """
949        if self.cssModule is None:
950            return []
951        module = self.page.cssModules.getModuleForName(self.cssModule)
952        return [
953            self.page.getCSSModuleURL(dep.name)
954            for dep in module.allDependencies(memo)
955            if self.page._shouldIncludeCSSModule(dep.name)]
956
957
958    def getStylesheetStan(self, modules):
959        """
960        Get some stan which will include the given modules.
961
962        @type modules: C{list} or L{url.URL}
963
964        @rtype: Stan
965        """
966        return [
967            tags.link(
968                rel='stylesheet', type='text/css', href=url)
969            for url in modules]
970
971
972
973class LivePage(rend.Page, _HasJSClass, _HasCSSModule):
974    """
975    A resource which can receive messages from and send messages to the client
976    after the initial page load has completed and which can send messages.
977
978    @ivar requiredBrowserVersions: A dictionary mapping User-Agent browser
979        names to the minimum supported version of those browsers.  Clients
980        using these browsers which are below the minimum version will be shown
981        an alternate page explaining this rather than the normal page content.
982
983    @ivar unsupportedBrowserLoader: A document loader which will be used to
984        generate the content shown to unsupported browsers.
985
986    @type _cssDepsMemo: C{dict}
987    @ivar _cssDepsMemo: A cache for CSS module dependencies; by default, this
988                        will only be shared within a single page instance.
989
990    @type _jsDepsMemo: C{dict}
991    @ivar _jsDepsMemo: A cache for JS module dependencies; by default, this
992                       will only be shared within a single page instance.
993
994    @type _didConnect: C{bool}
995    @ivar _didConnect: Initially C{False}, set to C{True} if connectionMade has
996        been invoked.
997
998    @type _didDisconnect: C{bool}
999    @ivar _didDisconnect: Initially C{False}, set to C{True} if _disconnected
1000        has been invoked.
1001
1002    @type _localObjects: C{dict} of C{int} : widget
1003    @ivar _localObjects: Mapping from an object ID to a Python object that will
1004        accept messages from the client.
1005
1006    @type _localObjectIDCounter: C{callable} returning C{int}
1007    @ivar _localObjectIDCounter: A callable that will return a new
1008        locally-unique object ID each time it is called.
1009    """
1010    jsClass = u'Nevow.Athena.PageWidget'
1011    cssModule = None
1012
1013    factory = LivePageFactory()
1014    _rendered = False
1015    _didConnect = False
1016    _didDisconnect = False
1017
1018    useActiveChannels = True
1019
1020    # This is the number of seconds that is acceptable for a LivePage to be
1021    # considered 'connected' without any transports still active.  In other
1022    # words, if the browser cannot make requests for more than this timeout
1023    # (due to network problems, blocking javascript functions, or broken
1024    # proxies) then deferreds returned from notifyOnDisconnect() will be
1025    # errbacked with ConnectionLost, and the LivePage will be removed from the
1026    # factory's cache, and then likely garbage collected.
1027    TRANSPORTLESS_DISCONNECT_TIMEOUT = 30
1028
1029    # This is the amount of time that each 'transport' request will remain open
1030    # to the server.  Although the underlying transport, i.e. the conceptual
1031    # connection established by the sequence of requests, remains alive, it is
1032    # necessary to periodically cancel requests to avoid browser and proxy
1033    # bugs.
1034    TRANSPORT_IDLE_TIMEOUT = 300
1035
1036    page = property(lambda self: self)
1037
1038    # Modules needed to bootstrap
1039    BOOTSTRAP_MODULES = ['Divmod', 'Divmod.Base', 'Divmod.Defer',
1040                         'Divmod.Runtime', 'Nevow', 'Nevow.Athena']
1041
1042    # Known minimum working versions of certain browsers.
1043    requiredBrowserVersions = {
1044        browsers.GECKO: (20051111,),
1045        browsers.INTERNET_EXPLORER: (6, 0),
1046        browsers.WEBKIT: (523,),
1047        browsers.OPERA: (9,)}
1048
1049    unsupportedBrowserLoader = loaders.stan(
1050        tags.html[
1051            tags.body[
1052                'Your browser is not supported by the Athena toolkit.']])
1053
1054
1055    def __init__(self, iface=None, rootObject=None, jsModules=None,
1056                 jsModuleRoot=None, transportRoot=None, cssModules=None,
1057                 cssModuleRoot=None, *a, **kw):
1058        super(LivePage, self).__init__(*a, **kw)
1059
1060        self.iface = iface
1061        self.rootObject = rootObject
1062        if jsModules is None:
1063            jsModules = JSPackage(jsDeps.mapping)
1064        self.jsModules = jsModules
1065        self.jsModuleRoot = jsModuleRoot
1066        if transportRoot is None:
1067            transportRoot = here
1068        self.transportRoot = transportRoot
1069        self.cssModuleRoot = cssModuleRoot
1070        if cssModules is None:
1071            cssModules = _theCSSRegistry
1072        self.cssModules = cssModules
1073        self.liveFragmentChildren = []
1074        self._includedModules = []
1075        self._includedCSSModules = []
1076        self._disconnectNotifications = []
1077        self._jsDepsMemo = {}
1078        self._cssDepsMemo = {}
1079
1080
1081    def _shouldInclude(self, moduleName):
1082        if moduleName not in self._includedModules:
1083            self._includedModules.append(moduleName)
1084            return True
1085        return False
1086
1087
1088    def _shouldIncludeCSSModule(self, moduleName):
1089        """
1090        Figure out whether the named CSS module has already been included.
1091
1092        @type moduleName: C{unicode}
1093
1094        @rtype: C{bool}
1095        """
1096        if moduleName not in self._includedCSSModules:
1097            self._includedCSSModules.append(moduleName)
1098            return True
1099        return False
1100
1101
1102    # Child lookup may be dependent on the application state
1103    # represented by a LivePage.  In this case, it is preferable to
1104    # dispatch child lookup on the same LivePage instance as performed
1105    # the initial rendering of the page.  Override the default
1106    # implementation of locateChild to do this where appropriate.
1107    def locateChild(self, ctx, segments):
1108        try:
1109            client = self.factory.getClient(segments[0])
1110        except KeyError:
1111            return super(LivePage, self).locateChild(ctx, segments)
1112        else:
1113            return client, segments[1:]
1114
1115
1116    def child___athena_private__(self, ctx):
1117        return _thePrivateAthenaResource
1118
1119
1120    # A note on timeout/disconnect logic: whenever a live client goes from some
1121    # transports to no transports, a timer starts; whenever it goes from no
1122    # transports to some transports, the timer is stopped; if the timer ever
1123    # expires the connection is considered lost; every time a transport is
1124    # added a timer is started; when the transport is used up, the timer is
1125    # stopped; if the timer ever expires, the transport has a no-op sent down
1126    # it; if an idle transport is ever disconnected, the connection is
1127    # considered lost; this lets the server notice clients who actively leave
1128    # (closed window, browser navigates away) and network congestion/errors
1129    # (unplugged ethernet cable, etc)
1130    def _becomeLive(self, location):
1131        """
1132        Assign this LivePage a clientID, associate it with a factory, and begin
1133        tracking its state.  This only occurs when a LivePage is *rendered*,
1134        not when it is instantiated.
1135        """
1136        self.clientID = self.factory.addClient(self)
1137
1138        if self.jsModuleRoot is None:
1139            self.jsModuleRoot = location.child(self.clientID).child('jsmodule')
1140        if self.cssModuleRoot is None:
1141            self.cssModuleRoot = location.child(self.clientID).child('cssmodule')
1142
1143        self._requestIDCounter = itertools.count().next
1144
1145        self._messageDeliverer = ReliableMessageDelivery(
1146            self,
1147            self.TRANSPORTLESS_DISCONNECT_TIMEOUT * 2,
1148            self.TRANSPORTLESS_DISCONNECT_TIMEOUT,
1149            self.TRANSPORT_IDLE_TIMEOUT,
1150            self._disconnected,
1151            connectionMade=self._connectionMade)
1152        self._remoteCalls = {}
1153        self._localObjects = {}
1154        self._localObjectIDCounter = itertools.count().next
1155
1156        self.addLocalObject(self)
1157
1158
1159    def _supportedBrowser(self, request):
1160        """
1161        Determine whether a known-unsupported browser is making a request.
1162
1163        @param request: The L{IRequest} being made.
1164
1165        @rtype: C{bool}
1166        @return: False if the user agent is known to be unsupported by Athena,
1167            True otherwise.
1168        """
1169        agentString = request.getHeader("user-agent")
1170        if agentString is None:
1171            return True
1172        agent = UserAgent.fromHeaderValue(agentString)
1173        if agent is None:
1174            return True
1175
1176        requiredVersion = self.requiredBrowserVersions.get(agent.browser, None)
1177        if requiredVersion is not None:
1178            return agent.version >= requiredVersion
1179        return True
1180
1181
1182    def renderUnsupported(self, ctx):
1183        """
1184        Render a notification to the user that his user agent is
1185        unsupported by this LivePage.
1186
1187        @param ctx: The current rendering context.
1188
1189        @return: Something renderable (same behavior as L{renderHTTP})
1190        """
1191        return flat.flatten(self.unsupportedBrowserLoader.load())
1192
1193
1194    def renderHTTP(self, ctx):
1195        """
1196        Attach this livepage to its transport, and render it and all of its
1197        attached widgets to the browser.  During rendering, the page is
1198        attached to its factory, acquires a clientID, and has headers set
1199        appropriately to prevent a browser from ever caching the page, since
1200        the clientID it gives to the browser is transient and changes every
1201        time.
1202
1203        These state changes associated with rendering mean that L{LivePage}s
1204        can only be rendered once, because they are attached to a particular
1205        user's browser, and it must be unambiguous what browser
1206        L{LivePage.callRemote} will invoke the method in.
1207
1208        The page's contents are rendered according to its docFactory, as with a
1209        L{Page}, unless the user-agent requesting this LivePage is determined
1210        to be unsupported by the JavaScript runtime required by Athena.  In
1211        that case, a static page is rendered by this page's
1212        C{renderUnsupported} method.
1213
1214        If a special query argument is set in the URL, "__athena_reconnect__",
1215        the page will instead render the JSON-encoded clientID by itself as the
1216        page's content.  This allows an existing live page in a browser to
1217        programmatically reconnect without re-rendering and re-loading the
1218        entire page.
1219
1220        @see: L{LivePage.renderUnsupported}
1221
1222        @see: L{Page.renderHTTP}
1223
1224        @param ctx: a L{WovenContext} with L{IRequest} remembered.
1225
1226        @return: a string (the content of the page) or a Deferred which will
1227        fire with the same.
1228
1229        @raise RuntimeError: if the page has already been rendered, or this
1230        page has not been given a factory.
1231        """
1232        if self._rendered:
1233            raise RuntimeError("Cannot render a LivePage more than once")
1234        if self.factory is None:
1235            raise RuntimeError("Cannot render a LivePage without a factory")
1236
1237        self._rendered = True
1238        request = inevow.IRequest(ctx)
1239        if not self._supportedBrowser(request):
1240            request.write(self.renderUnsupported(ctx))
1241            return ''
1242
1243        self._becomeLive(URL.fromString(flat.flatten(here, ctx)))
1244
1245        neverEverCache(request)
1246        if request.args.get(ATHENA_RECONNECT):
1247            return json.serialize(self.clientID.decode("ascii"))
1248        return rend.Page.renderHTTP(self, ctx)
1249
1250
1251    def _connectionMade(self):
1252        """
1253        Invoke connectionMade on all attached widgets.
1254        """
1255        for widget in self._localObjects.values():
1256            widget.connectionMade()
1257        self._didConnect = True
1258
1259
1260    def _disconnected(self, reason):
1261        """
1262        Callback invoked when the L{ReliableMessageDelivery} is disconnected.
1263
1264        If the page has not already disconnected, fire any deferreds created
1265        with L{notifyOnDisconnect}; if the page was already connected, fire
1266        C{connectionLost} methods on attached widgets.
1267        """
1268        if not self._didDisconnect:
1269            self._didDisconnect = True
1270
1271            notifications = self._disconnectNotifications
1272            self._disconnectNotifications = None
1273            for d in notifications:
1274                d.errback(reason)
1275            calls = self._remoteCalls
1276            self._remoteCalls = {}
1277            for (reqID, resD) in calls.iteritems():
1278                resD.errback(reason)
1279            if self._didConnect:
1280                for widget in self._localObjects.values():
1281                    widget.connectionLost(reason)
1282            self.factory.removeClient(self.clientID)
1283
1284
1285    def connectionMade(self):
1286        """
1287        Callback invoked when the transport is first connected.
1288        """
1289
1290
1291    def connectionLost(self, reason):
1292        """
1293        Callback invoked when the transport is disconnected.
1294
1295        This method will only be called if connectionMade was called.
1296
1297        Override this.
1298        """
1299
1300
1301    def addLocalObject(self, obj):
1302        objID = self._localObjectIDCounter()
1303        self._localObjects[objID] = obj
1304        return objID
1305
1306
1307    def removeLocalObject(self, objID):
1308        """
1309        Remove an object from the page's mapping of IDs that can receive
1310        messages.
1311
1312        @type  objID: C{int}
1313        @param objID: The ID returned by L{LivePage.addLocalObject}.
1314        """
1315        del self._localObjects[objID]
1316
1317
1318    def callRemote(self, methodName, *args):
1319        requestID = u's2c%i' % (self._requestIDCounter(),)
1320        message = (u'call', (unicode(methodName, 'ascii'), requestID, args))
1321        resultD = defer.Deferred()
1322        self._remoteCalls[requestID] = resultD
1323        self.addMessage(message)
1324        return resultD
1325
1326
1327    def addMessage(self, message):
1328        self._messageDeliverer.addMessage(message)
1329
1330
1331    def notifyOnDisconnect(self):
1332        """
1333        Return a Deferred which will fire or errback when this LivePage is
1334        no longer connected.
1335
1336        Note that if a LivePage never establishes a connection in the first
1337        place, the Deferreds this returns will never fire.
1338
1339        @rtype: L{defer.Deferred}
1340        """
1341        d = defer.Deferred()
1342        self._disconnectNotifications.append(d)
1343        return d
1344
1345
1346    def getJSModuleURL(self, moduleName):
1347        return self.jsModuleRoot.child(moduleName)
1348
1349
1350    def getCSSModuleURL(self, moduleName):
1351        """
1352        Return a URL rooted a L{cssModuleRoot} from which the CSS module named
1353        C{moduleName} can be fetched.
1354
1355        @type moduleName: C{unicode}
1356
1357        @rtype: C{str}
1358        """
1359        return self.cssModuleRoot.child(moduleName)
1360
1361
1362    def getImportStan(self, moduleName):
1363        moduleDef = jsModuleDeclaration(moduleName);
1364        return [tags.script(type='text/javascript')[tags.raw(moduleDef)],
1365                tags.script(type='text/javascript', src=self.getJSModuleURL(moduleName))]
1366
1367
1368    def render_liveglue(self, ctx, data):
1369        bootstrapString = '\n'.join(
1370            [self._bootstrapCall(method, args) for
1371             method, args in self._bootstraps(ctx)])
1372        return ctx.tag[
1373            self.getStylesheetStan(self._getRequiredCSSModules(self._cssDepsMemo)),
1374
1375            # Hit jsDeps.getModuleForName to force it to load some plugins :/
1376            # This really needs to be redesigned.
1377            [self.getImportStan(jsDeps.getModuleForName(name).name)
1378             for (name, url)
1379             in self._getRequiredModules(self._jsDepsMemo)],
1380            tags.script(type='text/javascript',
1381                        id=BOOTSTRAP_NODE_ID,
1382                        payload=bootstrapString)[
1383                BOOTSTRAP_STATEMENT]
1384        ]
1385
1386
1387    def _bootstraps(self, ctx):
1388        """
1389        Generate a list of 2-tuples of (methodName, arguments) representing the
1390        methods which need to be invoked as soon as all the bootstrap modules
1391        are loaded.
1392
1393        @param: a L{WovenContext} that can render an URL.
1394        """
1395        return [
1396            ("Divmod.bootstrap",
1397             [flat.flatten(self.transportRoot, ctx).decode("ascii")]),
1398            ("Nevow.Athena.bootstrap",
1399             [self.jsClass, self.clientID.decode('ascii')])]
1400
1401
1402    def _bootstrapCall(self, methodName, args):
1403        """
1404        Generate a string to call a 'bootstrap' function in an Athena JavaScript
1405        module client-side.
1406
1407        @param methodName: the name of the method.
1408
1409        @param args: a list of objects that will be JSON-serialized as
1410        arguments to the named method.
1411        """
1412        return '%s(%s);' % (
1413            methodName, ', '.join([json.serialize(arg) for arg in args]))
1414
1415
1416    def child_jsmodule(self, ctx):
1417        return MappingResource(self.jsModules.mapping)
1418
1419
1420    def child_cssmodule(self, ctx):
1421        """
1422        Return a L{MappingResource} wrapped around L{cssModules}.
1423        """
1424        return MappingResource(self.cssModules.mapping)
1425
1426
1427    _transportResource = None
1428    def child_transport(self, ctx):
1429        if self._transportResource is None:
1430            self._transportResource = LivePageTransport(
1431                self._messageDeliverer,
1432                self.useActiveChannels)
1433        return self._transportResource
1434
1435
1436    def locateMethod(self, ctx, methodName):
1437        if methodName in self.iface:
1438            return getattr(self.rootObject, methodName)
1439        raise AttributeError(methodName)
1440
1441
1442    def liveTransportMessageReceived(self, ctx, (action, args)):
1443        """
1444        A message was received from the reliable transport layer.  Process it by
1445        dispatching it first to myself, then later to application code if
1446        applicable.
1447        """
1448        method = getattr(self, 'action_' + action)
1449        method(ctx, *args)
1450
1451
1452    def action_call(self, ctx, requestId, method, objectID, args, kwargs):
1453        """
1454        Handle a remote call initiated by the client.
1455        """
1456        localObj = self._localObjects[objectID]
1457        try:
1458            func = localObj.locateMethod(ctx, method)
1459        except AttributeError:
1460            result = defer.fail(NoSuchMethod(objectID, method))
1461        else:
1462            result = defer.maybeDeferred(func, *args, **kwargs)
1463        def _cbCall(result):
1464            success = True
1465            if isinstance(result, failure.Failure):
1466                log.msg("Sending error to browser:")
1467                log.err(result)
1468                success = False
1469                if result.check(LivePageError):
1470                    result = (
1471                        result.value.jsClass,
1472                        result.value.args)
1473                else:
1474                    result = (
1475                        u'Divmod.Error',
1476                        [u'%s: %s' % (
1477                                result.type.__name__.decode('ascii'),
1478                                result.getErrorMessage().decode('ascii'))])
1479            message = (u'respond', (unicode(requestId), success, result))
1480            self.addMessage(message)
1481        result.addBoth(_cbCall)
1482
1483
1484    def action_respond(self, ctx, responseId, success, result):
1485        """
1486        Handle the response from the client to a call initiated by the server.
1487        """
1488        callDeferred = self._remoteCalls.pop(responseId)
1489        if success:
1490            callDeferred.callback(result)
1491        else:
1492            callDeferred.errback(getJSFailure(result, self.jsModules.mapping))
1493
1494
1495    def action_noop(self, ctx):
1496        """
1497        Handle noop, used to initialise and ping the live transport.
1498        """
1499
1500
1501    def action_close(self, ctx):
1502        """
1503        The client is going away.  Clean up after them.
1504        """
1505        self._messageDeliverer.close()
1506        self._disconnected(error.ConnectionDone("Connection closed"))
1507
1508
1509
1510handler = stan.Proto('athena:handler')
1511_handlerFormat = "return Nevow.Athena.Widget.handleEvent(this, %(event)s, %(handler)s);"
1512
1513def _rewriteEventHandlerToAttribute(tag):
1514    """
1515    Replace athena:handler children of the given tag with attributes on the tag
1516    which correspond to those event handlers.
1517    """
1518    if isinstance(tag, stan.Tag):
1519        extraAttributes = {}
1520        for i in xrange(len(tag.children) - 1, -1, -1):
1521            if isinstance(tag.children[i], stan.Tag) and tag.children[i].tagName == 'athena:handler':
1522                info = tag.children.pop(i)
1523                name = info.attributes['event'].encode('ascii')
1524                handler = info.attributes['handler']
1525                extraAttributes[name] = _handlerFormat % {
1526                    'handler': json.serialize(handler.decode('ascii')),
1527                    'event': json.serialize(name.decode('ascii'))}
1528                tag(**extraAttributes)
1529    return tag
1530
1531
1532def rewriteEventHandlerNodes(root):
1533    """
1534    Replace all the athena:handler nodes in a given document with onfoo
1535    attributes.
1536    """
1537    stan.visit(root, _rewriteEventHandlerToAttribute)
1538    return root
1539
1540
1541def _mangleId(oldId):
1542    """
1543    Return a consistently mangled form of an id that is unique to the widget
1544    within which it occurs.
1545    """
1546    return ['athenaid:', tags.slot('athena:id'), '-', oldId]
1547
1548
1549def _rewriteAthenaId(tag):
1550    """
1551    Rewrite id attributes to be prefixed with the ID of the widget the node is
1552    contained by. Also rewrite label "for" attributes which must match the id of
1553    their form element.
1554    """
1555    if isinstance(tag, stan.Tag):
1556        elementId = tag.attributes.pop('id', None)
1557        if elementId is not None:
1558            tag.attributes['id'] = _mangleId(elementId)
1559        if tag.tagName == "label":
1560            elementFor = tag.attributes.pop('for', None)
1561            if elementFor is not None:
1562                tag.attributes['for'] = _mangleId(elementFor)
1563        if tag.tagName in ('td', 'th'):
1564            headers = tag.attributes.pop('headers', None)
1565            if headers is not None:
1566                ids = headers.split()
1567                headers = [_mangleId(headerId) for headerId in ids]
1568                for n in xrange(len(headers) - 1, 0, -1):
1569                    headers.insert(n, ' ')
1570                tag.attributes['headers'] = headers
1571    return tag
1572
1573
1574def rewriteAthenaIds(root):
1575    """
1576    Rewrite id attributes to be unique to the widget they're in.
1577    """
1578    stan.visit(root, _rewriteAthenaId)
1579    return root
1580
1581
1582class _LiveMixin(_HasJSClass, _HasCSSModule):
1583    jsClass = u'Nevow.Athena.Widget'
1584    cssModule = None
1585
1586    preprocessors = [rewriteEventHandlerNodes, rewriteAthenaIds]
1587
1588    fragmentParent = None
1589
1590    _page = None
1591
1592    # Reference to the result of a call to _structured, if one has been made,
1593    # otherwise None.  This is used to make _structured() idempotent.
1594    _structuredCache = None
1595
1596    def __init__(self, *a, **k):
1597        super(_LiveMixin, self).__init__(*a, **k)
1598        self.liveFragmentChildren = []
1599
1600    def page():
1601        def get(self):
1602            if self._page is None:
1603                if self.fragmentParent is not None:
1604                    self._page = self.fragmentParent.page
1605            return self._page
1606        def set(self, value):
1607            self._page = value
1608        doc = """
1609        The L{LivePage} instance which is the topmost container of this
1610        fragment.
1611        """
1612        return get, set, None, doc
1613    page = property(*page())
1614
1615
1616    def getInitialArguments(self):
1617        """
1618        Return a C{tuple} or C{list} of arguments to be passed to this
1619        C{LiveFragment}'s client-side Widget.
1620
1621        This will be called during the rendering process.  Whatever it
1622        returns will be serialized into the page and passed to the
1623        C{__init__} method of the widget specified by C{jsClass}.
1624
1625        @rtype: C{list} or C{tuple}
1626        """
1627        return ()
1628
1629
1630    def _prepare(self, tag):
1631        """
1632        Check for clearly incorrect settings of C{self.jsClass} and
1633        C{self.page}, add this object to the page and fill the I{athena:id}
1634        slot with this object's Athena identifier.
1635        """
1636        assert isinstance(self.jsClass, unicode), "jsClass must be a unicode string"
1637
1638        if self.page is None:
1639            raise OrphanedFragment(self)
1640        self._athenaID = self.page.addLocalObject(self)
1641        if self.page._didConnect:
1642            self.connectionMade()
1643        tag.fillSlots('athena:id', str(self._athenaID))
1644
1645
1646    def setFragmentParent(self, fragmentParent):
1647        """
1648        Sets the L{LiveFragment} (or L{LivePage}) which is the logical parent
1649        of this fragment.  This should parallel the client-side hierarchy.
1650
1651        All LiveFragments must have setFragmentParent called on them before
1652        they are rendered for the client; otherwise, they will be unable to
1653        properly hook up to the page.
1654
1655        LiveFragments should have their own setFragmentParent called before
1656        calling setFragmentParent on any of their own children.  The normal way
1657        to accomplish this is to instantiate your fragment children during the
1658        render pass.
1659
1660        If that isn't feasible, instead override setFragmentParent and
1661        instantiate your children there.
1662
1663        This architecture might seem contorted, but what it allows that is
1664        interesting is adaptation of foreign objects to LiveFragment.  Anywhere
1665        you adapt to LiveFragment, setFragmentParent is the next thing that
1666        should be called.
1667        """
1668        self.fragmentParent = fragmentParent
1669        self.page = fragmentParent.page
1670        fragmentParent.liveFragmentChildren.append(self)
1671
1672
1673    def _flatten(self, what):
1674        """
1675        Synchronously flatten C{what} and return the result as a C{str}.
1676        """
1677        # Nested import because in a significant stroke of misfortune,
1678        # nevow.testutil already depends on nevow.athena.  It makes more sense
1679        # for the dependency to go from nevow.athena to nevow.testutil.
1680        # Perhaps a sane way to fix this would be to move FakeRequest to a
1681        # different module from whence nevow.athena and nevow.testutil could
1682        # import it. -exarkun
1683        from nevow.testutil import FakeRequest
1684        s = StringIO.StringIO()
1685        for _ in _flat.flatten(FakeRequest(), s.write, what, False, False):
1686            pass
1687        return s.getvalue()
1688
1689
1690    def _structured(self):
1691        """
1692        Retrieve an opaque object which may be usable to construct the
1693        client-side Widgets which correspond to this fragment and all of its
1694        children.
1695        """
1696        if self._structuredCache is not None:
1697            return self._structuredCache
1698
1699        children = []
1700        requiredModules = []
1701        requiredCSSModules = []
1702
1703        # Using the context here is terrible but basically necessary given the
1704        # /current/ architecture of Athena and flattening.  A better
1705        # implementation which was not tied to the rendering system could avoid
1706        # this.
1707        markup = context.call(
1708            {'children': children,
1709             'requiredModules': requiredModules,
1710             'requiredCSSModules': requiredCSSModules},
1711            self._flatten, tags.div(xmlns="http://www.w3.org/1999/xhtml")[self]).decode('utf-8')
1712
1713        del children[0]
1714
1715        self._structuredCache = {
1716            u'requiredModules': [(name, flat.flatten(url).decode('utf-8'))
1717                                 for (name, url) in requiredModules],
1718            u'requiredCSSModules': [flat.flatten(url).decode('utf-8')
1719                                    for url in requiredCSSModules],
1720            u'class': self.jsClass,
1721            u'id': self._athenaID,
1722            u'initArguments': tuple(self.getInitialArguments()),
1723            u'markup': markup,
1724            u'children': children}
1725        return self._structuredCache
1726
1727
1728    def liveElement(self, request, tag):
1729        """
1730        Render framework-level boilerplate for making sure the Widget for this
1731        Element is created and added to the page properly.
1732        """
1733        requiredModules = self._getRequiredModules(self.page._jsDepsMemo)
1734        requiredCSSModules = self._getRequiredCSSModules(self.page._cssDepsMemo)
1735
1736        # Add required attributes to the top widget node
1737        tag(**{'xmlns:athena': ATHENA_XMLNS_URI,
1738               'id': 'athena:%d' % self._athenaID,
1739               'athena:class': self.jsClass})
1740
1741        # This will only be set if _structured() is being run.
1742        if context.get('children') is not None:
1743            context.get('children').append({
1744                    u'class': self.jsClass,
1745                    u'id': self._athenaID,
1746                    u'initArguments': self.getInitialArguments()})
1747            context.get('requiredModules').extend(requiredModules)
1748            context.get('requiredCSSModules').extend(requiredCSSModules)
1749            return tag
1750
1751        return (
1752            self.getStylesheetStan(requiredCSSModules),
1753
1754            # Import stuff
1755            [self.getImportStan(name) for (name, url) in requiredModules],
1756
1757            # Dump some data for our client-side __init__ into a text area
1758            # where it can easily be found.
1759            tags.textarea(id='athena-init-args-' + str(self._athenaID),
1760                          style="display: none")[
1761                json.serialize(self.getInitialArguments())],
1762
1763            # Arrange to be instantiated
1764            tags.script(type='text/javascript')[
1765                """
1766                Nevow.Athena.Widget._widgetNodeAdded(%(athenaID)d);
1767                """ % {'athenaID': self._athenaID,
1768                       'pythonClass': self.__class__.__name__}],
1769
1770            # Okay, application stuff, plus metadata
1771            tag,
1772            )
1773    renderer(liveElement)
1774
1775
1776    def render_liveFragment(self, ctx, data):
1777        return self.liveElement(inevow.IRequest(ctx), ctx.tag)
1778
1779
1780    def getImportStan(self, moduleName):
1781        return self.page.getImportStan(moduleName)
1782
1783
1784    def locateMethod(self, ctx, methodName):
1785        remoteMethod = expose.get(self, methodName, None)
1786        if remoteMethod is None:
1787            raise AttributeError(self, methodName)
1788        return remoteMethod
1789
1790
1791    def callRemote(self, methodName, *varargs):
1792        return self.page.callRemote(
1793            "Nevow.Athena.callByAthenaID",
1794            self._athenaID,
1795            unicode(methodName, 'ascii'),
1796            varargs)
1797
1798
1799    def _athenaDetachServer(self):
1800        """
1801        Locally remove this from its parent.
1802
1803        @raise OrphanedFragment: if not attached to a parent.
1804        """
1805        if self.fragmentParent is None:
1806            raise OrphanedFragment(self)
1807        for ch in list(self.liveFragmentChildren):
1808            ch._athenaDetachServer()
1809        self.fragmentParent.liveFragmentChildren.remove(self)
1810        self.fragmentParent = None
1811        page = self.page
1812        self.page = None
1813        page.removeLocalObject(self._athenaID)
1814        if page._didConnect:
1815            self.connectionLost(ConnectionLost('Detached'))
1816        self.detached()
1817    expose(_athenaDetachServer)
1818
1819
1820    def detach(self):
1821        """
1822        Remove this from its parent after notifying the client that this is
1823        happening.
1824
1825        This function will *not* work correctly if the parent/child
1826        relationships of this widget do not exactly match the parent/child
1827        relationships of the corresponding fragments or elements on the server.
1828
1829        @return: A L{Deferred} which will fire when the detach completes.
1830        """
1831        d = self.callRemote('_athenaDetachClient')
1832        d.addCallback(lambda ign: self._athenaDetachServer())
1833        return d
1834
1835
1836    def detached(self):
1837        """
1838        Application-level callback invoked when L{detach} succeeds or when the
1839        client invokes the detach logic from its side.
1840
1841        This is invoked after this fragment has been disassociated from its
1842        parent and from the page.
1843
1844        Override this.
1845        """
1846
1847
1848    def connectionMade(self):
1849        """
1850        Callback invoked when the transport is first available to this widget.
1851
1852        Override this.
1853        """
1854
1855
1856    def connectionLost(self, reason):
1857        """
1858        Callback invoked once the transport is no longer available to this
1859        widget.
1860
1861        This method will only be called if connectionMade was called.
1862
1863        Override this.
1864        """
1865
1866
1867
1868class LiveFragment(_LiveMixin, rend.Fragment):
1869    """
1870    This class is deprecated because it relies on context objects
1871    U{which are being removed from Nevow<http://divmod.org/trac/wiki/WitherContext>}.
1872
1873    @see: L{LiveElement}
1874    """
1875    def __init__(self, *a, **kw):
1876        super(LiveFragment, self).__init__(*a, **kw)
1877        warnings.warn("[v0.10] LiveFragment has been superceded by LiveElement.",
1878                      category=DeprecationWarning,
1879                      stacklevel=2)
1880
1881
1882    def rend(self, context, data):
1883        """
1884        Hook into the rendering process in order to check preconditions and
1885        make sure the document will actually be renderable by satisfying
1886        certain Athena requirements.
1887        """
1888        context = rend.Fragment.rend(self, context, data)
1889        self._prepare(context.tag)
1890        return context
1891
1892
1893
1894
1895class LiveElement(_LiveMixin, Element):
1896    """
1897    Base-class for a portion of a LivePage.  When being rendered, a LiveElement
1898    has a special ID attribute added to its top-level tag.  This attribute is
1899    used to dispatch calls from the client onto the correct object (this one).
1900
1901    A LiveElement must use the `liveElement' renderer somewhere in its document
1902    template.  The node given this renderer will be the node used to construct
1903    a Widget instance in the browser (where it will be saved as the `node'
1904    property on the widget object).
1905
1906    JavaScript handlers for elements inside this node can use
1907    C{Nevow.Athena.Widget.get} to retrieve the widget associated with this
1908    LiveElement.  For example::
1909
1910        <form onsubmit="Nevow.Athena.Widget.get(this).callRemote('foo', bar); return false;">
1911
1912    Methods of the JavaScript widget class can also be bound as event handlers
1913    using the handler tag type in the Athena namespace::
1914
1915        <form xmlns:athena="http://divmod.org/ns/athena/0.7">
1916            <athena:handler event="onsubmit" handler="doFoo" />
1917        </form>
1918
1919    This will invoke the C{doFoo} method of the widget which contains the form
1920    node.
1921
1922    Because this mechanism sets up error handling and otherwise reduces the
1923    required boilerplate for handling events, it is preferred and recommended
1924    over directly including JavaScript in the event handler attribute of a
1925    node.
1926
1927    The C{jsClass} attribute of a LiveElement instance determines the
1928    JavaScript class used to construct its corresponding Widget.  This appears
1929    as the 'athena:class' attribute.
1930
1931    JavaScript modules may import other JavaScript modules by using a special
1932    comment which Athena recognizes::
1933
1934        // import Module.Name
1935
1936    Different imports must be placed on different lines.  No other comment
1937    style is supported for these directives.  Only one space character must
1938    appear between the string 'import' and the name of the module to be
1939    imported.  No trailing whitespace or non-whitespace is allowed.  There must
1940    be exactly one space between '//' and 'import'.  There must be no
1941    preceeding whitespace on the line.
1942
1943    C{Nevow.Athena.Widget.callRemote} can be given permission to invoke methods
1944    on L{LiveElement} instances by passing the functions which implement those
1945    methods to L{nevow.athena.expose} in this way::
1946
1947        class SomeElement(LiveElement):
1948            def someMethod(self, ...):
1949                ...
1950            expose(someMethod)
1951
1952    Only methods exposed in this way will be accessible.
1953
1954    L{LiveElement.callRemote} can be used to invoke any method of the widget on
1955    the client.
1956
1957    XML elements with id attributes will be rewritten so that the id is unique
1958    to that particular instance. The client-side
1959    C{Nevow.Athena.Widget.nodeById} API is provided to locate these later
1960    on. For example::
1961
1962        <div id="foo" />
1963
1964    and then::
1965
1966        var node = self.nodyById('foo');
1967
1968    On most platforms, this API will be much faster than similar techniques
1969    using C{Nevow.Athena.Widget.nodeByAttribute} etc.
1970
1971    Similarly to how Javascript classes are specified, L{LiveElement}
1972    instances may also identify a CSS module which provides appropriate styles
1973    with the C{cssModule} attribute (a unicode string naming a module within a
1974    L{inevow.ICSSPackage}).
1975
1976    The referenced CSS modules are treated as regular CSS, with the exception
1977    of support for the same::
1978
1979        // import CSSModule.Name
1980
1981    syntax as is provided for Javascript modules.
1982    """
1983    def render(self, request):
1984        """
1985        Hook into the rendering process in order to check preconditions and
1986        make sure the document will actually be renderable by satisfying
1987        certain Athena requirements.
1988        """
1989        document = tags.invisible[Element.render(self, request)]
1990        self._prepare(document)
1991        return document
1992
1993
1994
1995class IntrospectionFragment(LiveFragment):
1996    """
1997    Utility for developers which provides detailed information about
1998    the state of a live page.
1999    """
2000
2001    jsClass = u'Nevow.Athena.IntrospectionWidget'
2002
2003    docFactory = loaders.stan(
2004        tags.span(render=tags.directive('liveFragment'))[
2005        tags.a(
2006        href="#DEBUG_ME",
2007        class_='toggle-debug')["Debug"]])
2008
2009
2010
2011__all__ = [
2012    # Constants
2013    'ATHENA_XMLNS_URI',
2014
2015    # Errors
2016    'LivePageError', 'OrphanedFragment', 'ConnectFailed', 'ConnectionLost',
2017
2018    # JS support
2019    'MappingResource', 'JSModule', 'JSPackage', 'AutoJSPackage',
2020    'allJavascriptPackages', 'JSDependencies', 'JSException', 'JSCode',
2021    'JSFrame', 'JSTraceback',
2022
2023    # CSS support
2024    'CSSRegistry', 'CSSModule',
2025
2026    # Core objects
2027    'LivePage', 'LiveFragment', 'LiveElement', 'IntrospectionFragment',
2028
2029    # Decorators
2030    'expose', 'handler',
2031    ]
2032