source: documentViewer/MpiwgXmlTextServer.py @ 577:9251719154a3

Last change on this file since 577:9251719154a3 was 577:9251719154a3, checked in by casties, 12 years ago

toc with list of handwritten notes.

File size: 25.9 KB
Line 
1from OFS.SimpleItem import SimpleItem
2from Products.PageTemplates.PageTemplateFile import PageTemplateFile
3
4import xml.etree.ElementTree as ET
5
6import re
7import logging
8import urllib
9import urlparse
10import base64
11
12from datetime import datetime
13
14from SrvTxtUtils import getInt, getText, getHttpData
15
16def serialize(node):
17    """returns a string containing an XML snippet of node"""
18    s = ET.tostring(node, 'UTF-8')
19    # snip off XML declaration
20    if s.startswith('<?xml'):
21        i = s.find('?>')
22        return s[i+3:]
23
24    return s
25
26
27class MpiwgXmlTextServer(SimpleItem):
28    """TextServer implementation for MPIWG-XML server"""
29    meta_type="MPIWG-XML TextServer"
30
31    manage_options=(
32        {'label':'Config','action':'manage_changeMpiwgXmlTextServerForm'},
33       )+SimpleItem.manage_options
34   
35    manage_changeMpiwgXmlTextServerForm = PageTemplateFile("zpt/manage_changeMpiwgXmlTextServer", globals())
36       
37    def __init__(self,id,title="",serverUrl="http://mpdl-text.mpiwg-berlin.mpg.de/mpiwg-mpdl-cms-web/", timeout=40, serverName=None, repositoryType='production'):
38        """constructor"""
39        self.id=id
40        self.title=title
41        self.timeout = timeout
42        self.repositoryType = repositoryType
43        if serverName is None:
44            self.serverUrl = serverUrl
45        else:
46            self.serverUrl = "http://%s/mpiwg-mpdl-cms-web/"%serverName
47       
48    def getHttpData(self, url, data=None):
49        """returns result from url+data HTTP request"""
50        return getHttpData(url,data,timeout=self.timeout)
51   
52    def getServerData(self, method, data=None):
53        """returns result from text server for method+data"""
54        url = self.serverUrl+method
55        return getHttpData(url,data,timeout=self.timeout)
56
57
58    def getRepositoryType(self):
59        """returns the repository type, e.g. 'production'"""
60        return getattr(self, 'repositoryType', None)
61
62    def getTextDownloadUrl(self, type='xml', docinfo=None):
63        """returns a URL to download the current text"""
64        docpath = docinfo.get('textURLPath', None)
65        if not docpath:
66            return None
67
68        docpath = docpath.replace('.xml','.'+type)
69        url = '%sdoc/GetDocument?id=%s'%(self.serverUrl.replace('interface/',''), docpath)
70        return url
71
72
73    def getPlacesOnPage(self, docinfo=None, pn=None):
74        """Returns list of GIS places of page pn"""
75        #FIXME!
76        docpath = docinfo.get('textURLPath',None)
77        if not docpath:
78            return None
79
80        places=[]
81        text=self.getServerData("xpath.xql", "document=%s&xpath=//place&pn=%s"%(docpath,pn))
82        dom = ET.fromstring(text)
83        result = dom.findall(".//resultPage/place")
84        for l in result:
85            id = l.get("id")
86            name = l.text
87            place = {'id': id, 'name': name}
88            places.append(place)
89
90        return places
91   
92         
93    def getTextInfo(self, mode=None, docinfo=None):
94        """reads document info, including page concordance, from text server"""
95        logging.debug("getTextInfo mode=%s"%mode)
96       
97        field = ''
98        if mode in ['pages', 'toc', 'figures', 'handwritten']:
99            # translate mode to field param
100            field = '&field=%s'%mode
101        else:
102            mode = None
103
104        # check cached info
105        if mode:
106            # cached toc-request?
107            if 'full_%s'%mode in docinfo:
108                return docinfo
109           
110        else:
111            # cached but no toc-request?
112            if 'numTextPages' in docinfo:
113                return docinfo
114               
115        docpath = docinfo.get('textURLPath', None)
116        if docpath is None:
117            logging.error("getTextInfo: no textURLPath!")
118            return docinfo
119               
120        # fetch docinfo           
121        pagexml = self.getServerData("query/GetDocInfo","docId=%s%s"%(docpath,field))
122        dom = ET.fromstring(pagexml)
123        # all info in tag <doc>
124        doc = dom
125        if doc is None:
126            logging.error("getTextInfo: unable to find document-tag!")
127        else:
128            if mode is None:
129                # get general info from system-tag
130                sys = doc.find('system')
131                if sys is not None:
132                    docinfo['numTextPages'] = getInt(getText(sys.find('countPages'))) 
133                    docinfo['numFigureEntries'] = getInt(getText(sys.find('countFigures'))) 
134                    docinfo['numHandwritten'] = getInt(getText(sys.find('countHandwritten'))) 
135                    docinfo['numTocEntries'] = getInt(getText(sys.find('countTocEntries'))) 
136                   
137            else:
138                # result is in list-tag
139                l = doc.find('list')
140                if l is not None:
141                    lt = l.get('type')
142                    # pageNumbers
143                    if lt == 'pages':
144                        # contains tags with page numbers
145                        # <item n="14" o="2" o-norm="2" file="0014"/>
146                        # n=scan number, o=original page no, on=normalized original page no
147                        # pageNumbers is a dict indexed by scan number
148                        pages = {}
149                        for i in l:
150                            page = {}
151                            pn = getInt(i.get('n'))
152                            page['pn'] = pn
153                            no = i.get('o')
154                            page['no'] = no
155                            non = i.get('o-norm')
156                            page['non'] = non
157                                   
158                            if pn > 0:
159                                pages[pn] = page
160                           
161                        docinfo['pageNumbers'] = pages
162                                   
163                    # toc
164                    elif lt == 'toc' or lt == 'figures' or lt == 'handwritten':
165                        # contains tags with table of contents/figures
166                        # <item n="2.1." lv="2">CAP.I. <ref o="119">132</ref></item>
167                        tocs = []
168                        for te in l:
169                            if te.tag == 'item':
170                                toc = {}
171                                toc['level-string'] = te.get('n')
172                                toc['level'] = te.get('lv')
173                                toc['content'] = te.text.strip()
174                                ref = te.find('ref')
175                                toc['pn'] = getInt(ref.text)
176                                toc['no'] = ref.get('o')
177                                toc['non'] = ref.get('o-norm')
178                                tocs.append(toc)
179                       
180                        # save as full_toc/full_figures
181                        docinfo['full_%s'%mode] = tocs
182
183        return docinfo
184       
185         
186    def getTextPage(self, mode="text", pn=1, docinfo=None, pageinfo=None):
187        """returns single page from fulltext"""
188       
189        logging.debug("getTextPage mode=%s, pn=%s"%(mode,pn))
190        startTime = datetime.now()
191        # check for cached text -- but ideally this shouldn't be called twice
192        if pageinfo.has_key('textPage'):
193            logging.debug("getTextPage: using cached text")
194            return pageinfo['textPage']
195       
196        docpath = docinfo.get('textURLPath', None)
197        if not docpath:
198            return None
199       
200        # just checking
201        if pageinfo['current'] != pn:
202            logging.warning("getTextPage: current!=pn!")
203           
204        # stuff for constructing full urls
205        selfurl = docinfo['viewerUrl']
206        textParams = {'docId': docpath,
207                      'page': pn}
208       
209        normMode = pageinfo.get('characterNormalization', 'reg')
210        # TODO: change values in form
211        if normMode == 'regPlusNorm':
212            normMode = 'norm'
213       
214        # TODO: this should not be necessary when the backend is fixed               
215        textParams['normalization'] = normMode
216       
217        if not mode:
218            # default is dict
219            mode = 'text'
220
221        modes = mode.split(',')
222        # check for multiple layers
223        if len(modes) > 1:
224            logging.debug("getTextPage: more than one mode=%s"%mode)
225                       
226        # search mode
227        if 'search' in modes:
228            # add highlighting
229            highlightQuery = pageinfo.get('highlightQuery', None)
230            if highlightQuery:
231                textParams['highlightQuery'] = highlightQuery
232                textParams['highlightElem'] = pageinfo.get('highlightElement', '')
233                textParams['highlightElemPos'] = pageinfo.get('highlightElementPos', '')
234               
235            # ignore mode in the following
236            modes.remove('search')
237                           
238        # pundit mode
239        punditMode = False
240        if 'pundit' in modes:
241            punditMode = True
242            # ignore mode in the following
243            modes.remove('pundit')
244                           
245        # other modes don't combine
246        if 'dict' in modes:
247            textmode = 'dict'
248            textParams['outputFormat'] = 'html'
249        elif 'xml' in modes:
250            textmode = 'xml'
251            textParams['outputFormat'] = 'xmlDisplay'
252            normMode = 'orig'
253        elif 'gis' in modes:
254            #FIXME!
255            textmode = 'gis'
256        else:
257            # text is default mode
258            textmode = 'plain'
259            textParams['outputFormat'] = 'html'
260       
261        try:
262            # fetch the page
263            pagexml = self.getServerData("query/GetPage",urllib.urlencode(textParams))
264            dom = ET.fromstring(pagexml)
265        except Exception, e:
266            logging.error("Error reading page: %s"%e)
267            return None
268       
269        # plain text or text-with-links mode
270        if textmode == "plain" or textmode == "dict":
271            # the text is in div@class=text
272            pagediv = dom.find(".//div[@class='text']")
273            logging.debug("pagediv: %s"%repr(pagediv))
274            if pagediv is not None:
275                # add textmode and normMode classes
276                pagediv.set('class', 'text %s %s'%(textmode, normMode))
277                self._processWTags(textmode, normMode, pagediv)
278                #self._processPbTag(pagediv, pageinfo)
279                self._processFigures(pagediv, docinfo)
280                #self._fixEmptyDivs(pagediv)
281                # get full url assuming documentViewer is parent
282                selfurl = self.getLink()
283                # check all a-tags
284                links = pagediv.findall('.//a')
285                for l in links:
286                    href = l.get('href')
287                    if href:
288                        # is link with href
289                        linkurl = urlparse.urlparse(href)
290                        if linkurl.path.endswith('GetDictionaryEntries'):
291                            #TODO: replace wordInfo page
292                            # add target to open new page
293                            l.set('target', '_blank')
294                       
295                if punditMode:
296                    self._addPunditAttributes(pagediv, pageinfo, docinfo)
297                   
298                # TODO: move empty page text
299                ep = dom.find(".//div[@class='emptyPage']")
300                if ep is not None:
301                    pagediv.append(ep)
302                 
303                s = serialize(pagediv)
304                logging.debug("getTextPage done in %s"%(datetime.now()-startTime))   
305                return s
306           
307        # xml mode
308        elif textmode == "xml":
309            # the text is in body
310            pagediv = dom.find(".//body")
311            logging.debug("pagediv: %s"%repr(pagediv))
312            if pagediv is not None:
313                return serialize(pagediv)
314           
315        # pureXml mode WTF?
316        elif textmode == "pureXml":
317            # the text is in body
318            pagediv = dom.find(".//body")
319            logging.debug("pagediv: %s"%repr(pagediv))
320            if pagediv is not None:
321                return serialize(pagediv)
322                 
323        # gis mode FIXME!
324        elif textmode == "gis":
325            # the text is in div@class=text
326            pagediv = dom.find(".//div[@class='text']")
327            logging.debug("pagediv: %s"%repr(pagediv))
328            if pagediv is not None:
329                # fix empty div tags
330                self._fixEmptyDivs(pagediv)
331                # check all a-tags
332                links = pagediv.findall(".//a")
333                # add our URL as backlink
334                selfurl = self.getLink()
335                doc = base64.b64encode(selfurl)
336                for l in links:
337                    href = l.get('href')
338                    if href:
339                        if href.startswith('http://mappit.mpiwg-berlin.mpg.de'):
340                            l.set('href', re.sub(r'doc=[\w+/=]+', 'doc=%s'%doc, href))
341                            l.set('target', '_blank')
342                           
343                return serialize(pagediv)
344                   
345        logging.error("getTextPage: error in text mode %s or text!"%(textmode))
346        return None
347
348    def _processWTags(self, textMode, normMode, pagediv):
349        """selects the necessary information from w-spans and removes the rest from pagediv"""
350        logging.debug("processWTags(textMode=%s,norm=%s,pagediv"%(repr(textMode),repr(normMode)))
351        startTime = datetime.now()
352        wtags = pagediv.findall(".//span[@class='w']")
353        for wtag in wtags:
354            if textMode == 'dict':
355                # delete non-a-tags
356                wtag.remove(wtag.find("span[@class='nodictionary orig']"))
357                wtag.remove(wtag.find("span[@class='nodictionary reg']"))
358                wtag.remove(wtag.find("span[@class='nodictionary norm']"))
359                # delete non-matching children of a-tag and suppress remaining tag name
360                atag = wtag.find("a[@class='dictionary']")
361                if normMode == 'orig':
362                    atag.remove(atag.find("span[@class='reg']"))
363                    atag.remove(atag.find("span[@class='norm']"))
364                    atag.find("span[@class='orig']").tag = None
365                elif normMode == 'reg':
366                    atag.remove(atag.find("span[@class='orig']"))
367                    atag.remove(atag.find("span[@class='norm']"))
368                    atag.find("span[@class='reg']").tag = None
369                elif normMode == 'norm':
370                    atag.remove(atag.find("span[@class='orig']"))
371                    atag.remove(atag.find("span[@class='reg']"))
372                    atag.find("span[@class='norm']").tag = None
373                   
374            else:
375                # delete a-tag
376                wtag.remove(wtag.find("a[@class='dictionary']"))
377                # delete non-matching children and suppress remaining tag name
378                if normMode == 'orig':
379                    wtag.remove(wtag.find("span[@class='nodictionary reg']"))
380                    wtag.remove(wtag.find("span[@class='nodictionary norm']"))
381                    wtag.find("span[@class='nodictionary orig']").tag = None
382                elif normMode == 'reg':
383                    wtag.remove(wtag.find("span[@class='nodictionary orig']"))
384                    wtag.remove(wtag.find("span[@class='nodictionary norm']"))
385                    wtag.find("span[@class='nodictionary reg']").tag = None
386                elif normMode == 'norm':
387                    wtag.remove(wtag.find("span[@class='nodictionary orig']"))
388                    wtag.remove(wtag.find("span[@class='nodictionary reg']"))
389                    wtag.find("span[@class='nodictionary norm']").tag = None
390               
391            # suppress w-tag name
392            wtag.tag = None
393           
394        logging.debug("processWTags in %s"%(datetime.now()-startTime))
395        return pagediv
396       
397    def _processPbTag(self, pagediv, pageinfo):
398        """extracts information from pb-tag and removes it from pagediv"""
399        pbdiv = pagediv.find(".//span[@class='pb']")
400        if pbdiv is None:
401            logging.warning("getTextPage: no pb-span!")
402            return pagediv
403       
404        # extract running head
405        rh = pbdiv.find(".//span[@class='rhead']")
406        if rh is not None:
407            pageinfo['pageHeaderTitle'] = getText(rh)
408           
409        # remove pb-div from parent
410        ppdiv = pagediv.find(".//span[@class='pb']/..")
411        ppdiv.remove(pbdiv)       
412        return pagediv
413   
414    def _addPunditAttributes(self, pagediv, pageinfo, docinfo):
415        """add about attributes for pundit annotation tool"""
416        textid = docinfo.get('DRI', "fn=%s"%docinfo.get('documentPath', '???'))
417        pn = pageinfo.get('pn', '1')
418        #  TODO: use pn as well?
419        # check all div-tags
420        divs = pagediv.findall(".//div")
421        for d in divs:
422            id = d.get('id')
423            if id:
424                # TODO: check path (cf RFC2396)
425                d.set('about', "http://echo.mpiwg-berlin.mpg.de/%s/pn=%s/#%s"%(textid,pn,id))
426                cls = d.get('class','')
427                cls += ' pundit-content'
428                d.set('class', cls.strip())
429
430        return pagediv
431
432    def _processFigures(self, pagediv, docinfo):
433        """processes figure-tags"""
434        # unfortunately etree can not select class.startswith('figure')
435        divs = pagediv.findall(".//span[@class]")
436        scalerUrl = docinfo['digilibScalerUrl']
437        viewerUrl = docinfo['digilibViewerUrl']
438        for d in divs:
439            if not d.get('class').startswith('figure'):
440                continue
441           
442            try:
443                a = d.find('a')
444                img = a.find('img')
445                imgsrc = img.get('src')
446                imgurl = urlparse.urlparse(imgsrc)
447                imgq = imgurl.query
448                imgparams = urlparse.parse_qs(imgq)
449                fn = imgparams.get('fn', None)
450                if fn is not None:
451                    # parse_qs puts parameters in lists
452                    fn = fn[0]
453                    # TODO: check valid path
454                    # fix img@src
455                    newsrc = '%s?fn=%s&dw=200&dh=200'%(scalerUrl,fn)
456                    img.set('src', newsrc)
457                    # fix a@href
458                    newlink = '%s?fn=%s'%(viewerUrl,fn)
459                    a.set('href', newlink)
460                    a.set('target', '_blank')
461                   
462            except:
463                logging.warn("processFigures: strange figure!")
464               
465   
466    def _fixEmptyDivs(self, pagediv):
467        """fixes empty div-tags by inserting a space"""
468        divs = pagediv.findall('.//div')
469        for d in divs:
470            if len(d) == 0 and not d.text:
471                # make empty divs non-empty
472                d.text = ' '
473 
474        return pagediv
475
476
477    def getSearchResults(self, mode, query=None, pageinfo=None, docinfo=None):
478        """loads list of search results and stores XML in docinfo"""
479       
480        logging.debug("getSearchResults mode=%s query=%s"%(mode, query))
481        if mode == "none":
482            return docinfo
483             
484        #TODO: put mode into query
485       
486        cachedQuery = docinfo.get('cachedQuery', None)
487        if cachedQuery is not None:
488            # cached search result
489            if cachedQuery == '%s_%s'%(mode,query):
490                # same query
491                return docinfo
492           
493            else:
494                # different query
495                del docinfo['resultSize']
496                del docinfo['results']
497       
498        # cache query
499        docinfo['cachedQuery'] = '%s_%s'%(mode,query)
500       
501        # fetch full results
502        docpath = docinfo['textURLPath']
503        params = {'docId': docpath,
504                  'query': query,
505                  'pageSize': 1000,
506                  'page': 1,
507                  'outputFormat': 'html'}
508        pagexml = self.getServerData("query/QueryDocument",urllib.urlencode(params))
509        results = []
510        try:
511            dom = ET.fromstring(pagexml)
512            # page content is currently in multiple <td align=left>
513            alldivs = dom.findall(".//tr[@class='hit']")
514            for div in alldivs:
515                # change tr to div
516                div.tag = 'div'
517                # change td to span
518                for d in div.findall('td'):
519                    d.tag = 'span'
520                   
521                # TODO: can we put etree in the session?
522                results.append(div)
523       
524        except Exception, e:
525            logging.error("GetSearchResults: Error parsing search result: %s"%e)
526               
527        # store results in docinfo
528        docinfo['resultSize'] = len(results)
529        docinfo['results'] = results
530
531        return docinfo
532   
533
534    def getResultsPage(self, mode="text", query=None, pn=None, start=None, size=None, pageinfo=None, docinfo=None):
535        """returns single page from the table of contents"""
536        logging.debug("getResultsPage mode=%s, pn=%s"%(mode,pn))
537        # get (cached) result
538        self.getSearchResults(mode=mode, query=query, pageinfo=pageinfo, docinfo=docinfo)
539           
540        resultxml = docinfo.get('results', None)
541        if not resultxml:
542            logging.error("getResultPage: unable to find results")
543            return "Error: no result!"
544       
545        if size is None:
546            size = pageinfo.get('resultPageSize', 10)
547           
548        if start is None:
549            start = (pn - 1) * size
550
551        if resultxml is not None:
552            # paginate
553            first = start-1
554            last = first+size
555            tocdivs = resultxml[first:last]
556           
557            toc = ET.Element('div', attrib={'class':'queryResultPage'})
558            for div in tocdivs:
559                # check all a-tags
560                links = div.findall(".//a")
561                for l in links:
562                    href = l.get('href')
563                    if href:
564                        # assume all links go to pages
565                        linkUrl = urlparse.urlparse(href)
566                        linkParams = urlparse.parse_qs(linkUrl.query)
567                        # take some parameters (make sure it works even if the link was already parsed)
568                        params = {'pn': linkParams.get('page',linkParams.get('pn', None)),
569                                  'highlightQuery': linkParams.get('highlightQuery',None),
570                                  'highlightElement': linkParams.get('highlightElem',linkParams.get('highlightElement',None)),
571                                  'highlightElementPos': linkParams.get('highlightElemPos',linkParams.get('highlightElementPos',None))
572                                  }
573                        if not params['pn']:
574                            logging.warn("getResultsPage: link has no page: %s"%href)
575                           
576                        url = self.getLink(params=params)
577                        l.set('href', url)
578                       
579                toc.append(div)
580                       
581            return serialize(toc)
582       
583        return "ERROR: no results!"
584
585
586    def getToc(self, mode='text', docinfo=None):
587        """returns list of table of contents from docinfo"""
588        logging.debug("getToc mode=%s"%mode)
589        if mode == 'text':
590            queryType = 'toc'
591        else:
592            queryType = mode
593           
594        if not 'full_%s'%queryType in docinfo:
595            # get new toc
596            docinfo = self.getTextInfo(queryType, docinfo)
597           
598        return docinfo.get('full_%s'%queryType, [])
599
600
601    def getTocPage(self, mode='text', pn=None, start=None, size=None, pageinfo=None, docinfo=None):
602        """returns single page from the table of contents"""
603        logging.debug("getTocPage mode=%s, pn=%s start=%s size=%s"%(mode,repr(pn),repr(start),repr(size)))
604        fulltoc = self.getToc(mode=mode, docinfo=docinfo)
605        if len(fulltoc) < 1:
606            logging.error("getTocPage: unable to find toc!")
607            return "Error: no table of contents!"       
608       
609        if size is None:
610            size = pageinfo.get('tocPageSize', 30)
611           
612        if start is None:
613            start = (pn - 1) * size
614
615        # paginate
616        first = (start - 1)
617        last = first + size
618        tocs = fulltoc[first:last]
619        tp = '<div>'
620        label = {'figures': 'Figure', 'handwritten': 'Handwritten note'}.get(mode, 'Item')
621        for toc in tocs:
622            pageurl = self.getLink('pn', toc['pn'])
623            tp += '<div class="tocline">'
624            content = toc['content']
625            if content:
626                tp += '<div class="toc name">[%s] %s</div>'%(toc['level-string'], toc['content'])
627            else:
628                tp += '<div class="toc name">[%s %s]</div>'%(label, toc['level-string'])
629           
630            if toc.get('no', None):
631                tp += '<div class="toc page"><a href="%s">Page: %s (%s)</a></div>'%(pageurl, toc['pn'], toc['no'])
632            else:
633                tp += '<div class="toc page"><a href="%s">Page: %s</a></div>'%(pageurl, toc['pn'])
634               
635            tp += '</div>\n'
636           
637        tp += '</div>\n'
638       
639        return tp
640           
641   
642    def manage_changeMpiwgXmlTextServer(self,title="",serverUrl="http://mpdl-text.mpiwg-berlin.mpg.de/mpdl/interface/",timeout=40,repositoryType=None,RESPONSE=None):
643        """change settings"""
644        self.title=title
645        self.timeout = timeout
646        self.serverUrl = serverUrl
647        if repositoryType:
648            self.repositoryType = repositoryType
649        if RESPONSE is not None:
650            RESPONSE.redirect('manage_main')
651       
652# management methods
653def manage_addMpiwgXmlTextServerForm(self):
654    """Form for adding"""
655    pt = PageTemplateFile("zpt/manage_addMpiwgXmlTextServer", globals()).__of__(self)
656    return pt()
657
658def manage_addMpiwgXmlTextServer(self,id,title="",serverUrl="http://mpdl-text.mpiwg-berlin.mpg.de/mpdl/interface/",timeout=40,RESPONSE=None):
659    """add MpiwgXmlTextServer"""
660    newObj = MpiwgXmlTextServer(id=id,title=title,serverUrl=serverUrl,timeout=timeout)
661    self.Destination()._setObject(id, newObj)
662    if RESPONSE is not None:
663        RESPONSE.redirect('manage_main')
664       
665       
Note: See TracBrowser for help on using the repository browser.