source: documentViewer/MpiwgXmlTextServer.py @ 576:b2c7e272e075

Last change on this file since 576:b2c7e272e075 was 576:b2c7e272e075, checked in by casties, 12 years ago

new w-tag solution with etree. search works now.

File size: 26.1 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                        logging.debug("got pageNumbers=%s"%repr(pages))
163                                   
164                    # toc
165                    elif lt == 'toc' or lt == 'figures' or lt == 'handwritten':
166                        # contains tags with table of contents/figures
167                        # <item n="2.1." lv="2">CAP.I. <ref o="119">132</ref></item>
168                        tocs = []
169                        for te in l:
170                            if te.tag == 'item':
171                                toc = {}
172                                toc['level-string'] = te.get('n')
173                                toc['level'] = te.get('lv')
174                                toc['content'] = te.text.strip()
175                                ref = te.find('ref')
176                                toc['pn'] = getInt(ref.text)
177                                toc['no'] = ref.get('o')
178                                toc['non'] = ref.get('o-norm')
179                                tocs.append(toc)
180                       
181                        # save as full_toc/full_figures
182                        docinfo['full_%s'%mode] = tocs
183
184        return docinfo
185       
186         
187    def getTextPage(self, mode="text", pn=1, docinfo=None, pageinfo=None):
188        """returns single page from fulltext"""
189       
190        logging.debug("getTextPage mode=%s, pn=%s"%(mode,pn))
191        startTime = datetime.now()
192        # check for cached text -- but ideally this shouldn't be called twice
193        if pageinfo.has_key('textPage'):
194            logging.debug("getTextPage: using cached text")
195            return pageinfo['textPage']
196       
197        docpath = docinfo.get('textURLPath', None)
198        if not docpath:
199            return None
200       
201        # just checking
202        if pageinfo['current'] != pn:
203            logging.warning("getTextPage: current!=pn!")
204           
205        # stuff for constructing full urls
206        selfurl = docinfo['viewerUrl']
207        textParams = {'docId': docpath,
208                      'page': pn}
209       
210        normMode = pageinfo.get('characterNormalization', 'reg')
211        # TODO: change values in form
212        if normMode == 'regPlusNorm':
213            normMode = 'norm'
214       
215        # TODO: this should not be necessary when the backend is fixed               
216        textParams['normalization'] = normMode
217       
218        if not mode:
219            # default is dict
220            mode = 'text'
221
222        modes = mode.split(',')
223        # check for multiple layers
224        if len(modes) > 1:
225            logging.debug("getTextPage: more than one mode=%s"%mode)
226                       
227        # search mode
228        if 'search' in modes:
229            # add highlighting
230            highlightQuery = pageinfo.get('highlightQuery', None)
231            if highlightQuery:
232                textParams['highlightQuery'] = highlightQuery
233                textParams['highlightElem'] = pageinfo.get('highlightElement', '')
234                textParams['highlightElemPos'] = pageinfo.get('highlightElementPos', '')
235               
236            # ignore mode in the following
237            modes.remove('search')
238                           
239        # pundit mode
240        punditMode = False
241        if 'pundit' in modes:
242            punditMode = True
243            # ignore mode in the following
244            modes.remove('pundit')
245                           
246        # other modes don't combine
247        if 'dict' in modes:
248            textmode = 'dict'
249            textParams['outputFormat'] = 'html'
250        elif 'xml' in modes:
251            textmode = 'xml'
252            textParams['outputFormat'] = 'xmlDisplay'
253            normMode = 'orig'
254        elif 'gis' in modes:
255            #FIXME!
256            textmode = 'gis'
257        else:
258            # text is default mode
259            textmode = 'plain'
260            textParams['outputFormat'] = 'html'
261       
262        try:
263            # fetch the page
264            pagexml = self.getServerData("query/GetPage",urllib.urlencode(textParams))
265            dom = ET.fromstring(pagexml)
266        except Exception, e:
267            logging.error("Error reading page: %s"%e)
268            return None
269       
270        # plain text or text-with-links mode
271        if textmode == "plain" or textmode == "dict":
272            # the text is in div@class=text
273            pagediv = dom.find(".//div[@class='text']")
274            logging.debug("pagediv: %s"%repr(pagediv))
275            if pagediv is not None:
276                # add textmode and normMode classes
277                pagediv.set('class', 'text %s %s'%(textmode, normMode))
278                self._processWTags(textmode, normMode, pagediv)
279                #self._processPbTag(pagediv, pageinfo)
280                self._processFigures(pagediv, docinfo)
281                #self._fixEmptyDivs(pagediv)
282                # get full url assuming documentViewer is parent
283                selfurl = self.getLink()
284                # check all a-tags
285                links = pagediv.findall('.//a')
286                for l in links:
287                    href = l.get('href')
288                    if href:
289                        # is link with href
290                        linkurl = urlparse.urlparse(href)
291                        if linkurl.path.endswith('GetDictionaryEntries'):
292                            #TODO: replace wordInfo page
293                            # add target to open new page
294                            l.set('target', '_blank')
295                       
296                if punditMode:
297                    self._addPunditAttributes(pagediv, pageinfo, docinfo)
298                 
299                s = serialize(pagediv)
300                logging.debug("getTextPage done in %s"%(datetime.now()-startTime))   
301                return s
302           
303        # xml mode
304        elif textmode == "xml":
305            # the text is in body
306            pagediv = dom.find(".//body")
307            logging.debug("pagediv: %s"%repr(pagediv))
308            if pagediv is not None:
309                return serialize(pagediv)
310           
311        # pureXml mode WTF?
312        elif textmode == "pureXml":
313            # the text is in body
314            pagediv = dom.find(".//body")
315            logging.debug("pagediv: %s"%repr(pagediv))
316            if pagediv is not None:
317                return serialize(pagediv)
318                 
319        # gis mode FIXME!
320        elif textmode == "gis":
321            # the text is in div@class=text
322            pagediv = dom.find(".//div[@class='text']")
323            logging.debug("pagediv: %s"%repr(pagediv))
324            if pagediv is not None:
325                # fix empty div tags
326                self._fixEmptyDivs(pagediv)
327                # check all a-tags
328                links = pagediv.findall(".//a")
329                # add our URL as backlink
330                selfurl = self.getLink()
331                doc = base64.b64encode(selfurl)
332                for l in links:
333                    href = l.get('href')
334                    if href:
335                        if href.startswith('http://mappit.mpiwg-berlin.mpg.de'):
336                            l.set('href', re.sub(r'doc=[\w+/=]+', 'doc=%s'%doc, href))
337                            l.set('target', '_blank')
338                           
339                return serialize(pagediv)
340                   
341        logging.error("getTextPage: error in text mode %s or text!"%(textmode))
342        return None
343
344    def _processWTags(self, textMode, normMode, pagediv):
345        """selects the necessary information from w-spans and removes the rest from pagediv"""
346        logging.debug("processWTags(textMode=%s,norm=%s,pagediv"%(repr(textMode),repr(normMode)))
347        startTime = datetime.now()
348        wtags = pagediv.findall(".//span[@class='w']")
349        for wtag in wtags:
350            if textMode == 'dict':
351                # delete non-a-tags
352                wtag.remove(wtag.find("span[@class='nodictionary orig']"))
353                wtag.remove(wtag.find("span[@class='nodictionary reg']"))
354                wtag.remove(wtag.find("span[@class='nodictionary norm']"))
355                # delete non-matching children of a-tag and suppress remaining tag name
356                atag = wtag.find("a[@class='dictionary']")
357                if normMode == 'orig':
358                    atag.remove(atag.find("span[@class='reg']"))
359                    atag.remove(atag.find("span[@class='norm']"))
360                    atag.find("span[@class='orig']").tag = None
361                elif normMode == 'reg':
362                    atag.remove(atag.find("span[@class='orig']"))
363                    atag.remove(atag.find("span[@class='norm']"))
364                    atag.find("span[@class='reg']").tag = None
365                elif normMode == 'norm':
366                    atag.remove(atag.find("span[@class='orig']"))
367                    atag.remove(atag.find("span[@class='reg']"))
368                    atag.find("span[@class='norm']").tag = None
369                   
370            else:
371                # delete a-tag
372                wtag.remove(wtag.find("a[@class='dictionary']"))
373                # delete non-matching children and suppress remaining tag name
374                if normMode == 'orig':
375                    wtag.remove(wtag.find("span[@class='nodictionary reg']"))
376                    wtag.remove(wtag.find("span[@class='nodictionary norm']"))
377                    wtag.find("span[@class='nodictionary orig']").tag = None
378                elif normMode == 'reg':
379                    wtag.remove(wtag.find("span[@class='nodictionary orig']"))
380                    wtag.remove(wtag.find("span[@class='nodictionary norm']"))
381                    wtag.find("span[@class='nodictionary reg']").tag = None
382                elif normMode == 'norm':
383                    wtag.remove(wtag.find("span[@class='nodictionary orig']"))
384                    wtag.remove(wtag.find("span[@class='nodictionary reg']"))
385                    wtag.find("span[@class='nodictionary norm']").tag = None
386               
387            # suppress w-tag name
388            wtag.tag = None
389           
390        logging.debug("processWTags in %s"%(datetime.now()-startTime))
391        return pagediv
392       
393    def _processPbTag(self, pagediv, pageinfo):
394        """extracts information from pb-tag and removes it from pagediv"""
395        pbdiv = pagediv.find(".//span[@class='pb']")
396        if pbdiv is None:
397            logging.warning("getTextPage: no pb-span!")
398            return pagediv
399       
400        # extract running head
401        rh = pbdiv.find(".//span[@class='rhead']")
402        if rh is not None:
403            pageinfo['pageHeaderTitle'] = getText(rh)
404           
405        # remove pb-div from parent
406        ppdiv = pagediv.find(".//span[@class='pb']/..")
407        ppdiv.remove(pbdiv)       
408        return pagediv
409   
410    def _addPunditAttributes(self, pagediv, pageinfo, docinfo):
411        """add about attributes for pundit annotation tool"""
412        textid = docinfo.get('DRI', "fn=%s"%docinfo.get('documentPath', '???'))
413        pn = pageinfo.get('pn', '1')
414        #  TODO: use pn as well?
415        # check all div-tags
416        divs = pagediv.findall(".//div")
417        for d in divs:
418            id = d.get('id')
419            if id:
420                # TODO: check path (cf RFC2396)
421                d.set('about', "http://echo.mpiwg-berlin.mpg.de/%s/pn=%s/#%s"%(textid,pn,id))
422                cls = d.get('class','')
423                cls += ' pundit-content'
424                d.set('class', cls.strip())
425
426        return pagediv
427
428    def _processFigures(self, pagediv, docinfo):
429        """processes figure-tags"""
430        # unfortunately etree can not select class.startswith('figure')
431        divs = pagediv.findall(".//span[@class]")
432        scalerUrl = docinfo['digilibScalerUrl']
433        viewerUrl = docinfo['digilibViewerUrl']
434        for d in divs:
435            if not d.get('class').startswith('figure'):
436                continue
437           
438            try:
439                a = d.find('a')
440                img = a.find('img')
441                imgsrc = img.get('src')
442                imgurl = urlparse.urlparse(imgsrc)
443                imgq = imgurl.query
444                imgparams = urlparse.parse_qs(imgq)
445                fn = imgparams.get('fn', None)
446                if fn is not None:
447                    # parse_qs puts parameters in lists
448                    fn = fn[0]
449                    # TODO: check valid path
450                    # fix img@src
451                    newsrc = '%s?fn=%s&dw=200&dh=200'%(scalerUrl,fn)
452                    img.set('src', newsrc)
453                    # fix a@href
454                    newlink = '%s?fn=%s'%(viewerUrl,fn)
455                    a.set('href', newlink)
456                    a.set('target', '_blank')
457                   
458            except:
459                logging.warn("processFigures: strange figure!")
460               
461   
462    def _fixEmptyDivs(self, pagediv):
463        """fixes empty div-tags by inserting a space"""
464        divs = pagediv.findall('.//div')
465        for d in divs:
466            if len(d) == 0 and not d.text:
467                # make empty divs non-empty
468                d.text = ' '
469 
470        return pagediv
471
472
473    def getSearchResults(self, mode, query=None, pageinfo=None, docinfo=None):
474        """loads list of search results and stores XML in docinfo"""
475       
476        logging.debug("getSearchResults mode=%s query=%s"%(mode, query))
477        if mode == "none":
478            return docinfo
479             
480        #TODO: put mode into query
481       
482        cachedQuery = docinfo.get('cachedQuery', None)
483        if cachedQuery is not None:
484            # cached search result
485            if cachedQuery == '%s_%s'%(mode,query):
486                # same query
487                return docinfo
488           
489            else:
490                # different query
491                del docinfo['resultSize']
492                del docinfo['results']
493       
494        # cache query
495        docinfo['cachedQuery'] = '%s_%s'%(mode,query)
496       
497        # fetch full results
498        docpath = docinfo['textURLPath']
499        params = {'docId': docpath,
500                  'query': query,
501                  'pageSize': 1000,
502                  'page': 1,
503                  'outputFormat': 'html'}
504        pagexml = self.getServerData("query/QueryDocument",urllib.urlencode(params))
505        results = []
506        try:
507            dom = ET.fromstring(pagexml)
508            # page content is currently in multiple <td align=left>
509            alldivs = dom.findall(".//tr[@class='hit']")
510            for div in alldivs:
511                # change tr to div
512                div.tag = 'div'
513                # change td to span
514                for d in div.findall('td'):
515                    d.tag = 'span'
516                   
517                # TODO: can we put etree in the session?
518                results.append(div)
519       
520        except Exception, e:
521            logging.error("GetSearchResults: Error parsing search result: %s"%e)
522               
523        # store results in docinfo
524        docinfo['resultSize'] = len(results)
525        docinfo['results'] = results
526
527        return docinfo
528   
529
530    def getResultsPage(self, mode="text", query=None, pn=None, start=None, size=None, pageinfo=None, docinfo=None):
531        """returns single page from the table of contents"""
532        logging.debug("getResultsPage mode=%s, pn=%s"%(mode,pn))
533        # get (cached) result
534        self.getSearchResults(mode=mode, query=query, pageinfo=pageinfo, docinfo=docinfo)
535           
536        resultxml = docinfo.get('results', None)
537        if not resultxml:
538            logging.error("getResultPage: unable to find results")
539            return "Error: no result!"
540       
541        if size is None:
542            size = pageinfo.get('resultPageSize', 10)
543           
544        if start is None:
545            start = (pn - 1) * size
546
547        #fullresult = ET.fromstring(resultxml)
548        #fullresult = resultxml
549        #logging.debug("resultxml=%s"%repr(resultxml))
550       
551        if resultxml is not None:
552            # paginate
553            first = start-1
554            last = first+size
555            tocdivs = resultxml[first:last]
556            #del fullresult[:first]
557            #del fullresult[len:]
558            #tocdivs = fullresult
559           
560            toc = ET.Element('div', attrib={'class':'queryResultPage'})
561            for div in tocdivs:
562                # check all a-tags
563                links = div.findall(".//a")
564                for l in links:
565                    href = l.get('href')
566                    if href:
567                        # assume all links go to pages
568                        linkUrl = urlparse.urlparse(href)
569                        linkParams = urlparse.parse_qs(linkUrl.query)
570                        # take some parameters (make sure it works even if the link was already parsed)
571                        params = {'pn': linkParams.get('page',linkParams.get('pn', None)),
572                                  'highlightQuery': linkParams.get('highlightQuery',None),
573                                  'highlightElement': linkParams.get('highlightElem',linkParams.get('highlightElement',None)),
574                                  'highlightElementPos': linkParams.get('highlightElemPos',linkParams.get('highlightElementPos',None))
575                                  }
576                        if not params['pn']:
577                            logging.warn("getResultsPage: link has no page: %s"%href)
578                           
579                        url = self.getLink(params=params)
580                        l.set('href', url)
581                       
582                toc.append(div)
583                       
584            return serialize(toc)
585       
586        return "ERROR: no results!"
587
588
589    def getToc(self, mode='text', docinfo=None):
590        """returns list of table of contents from docinfo"""
591        logging.debug("getToc mode=%s"%mode)
592        if mode == 'text':
593            queryType = 'toc'
594        else:
595            queryType = mode
596           
597        if not 'full_%s'%queryType in docinfo:
598            # get new toc
599            docinfo = self.getTextInfo(queryType, docinfo)
600           
601        return docinfo.get('full_%s'%queryType, [])
602
603
604    def getTocPage(self, mode='text', pn=None, start=None, size=None, pageinfo=None, docinfo=None):
605        """returns single page from the table of contents"""
606        logging.debug("getTocPage mode=%s, pn=%s start=%s size=%s"%(mode,repr(pn),repr(start),repr(size)))
607        fulltoc = self.getToc(mode=mode, docinfo=docinfo)
608        if len(fulltoc) < 1:
609            logging.error("getTocPage: unable to find toc!")
610            return "Error: no table of contents!"       
611       
612        if size is None:
613            size = pageinfo.get('tocPageSize', 30)
614           
615        if start is None:
616            start = (pn - 1) * size
617
618        # paginate
619        first = (start - 1)
620        last = first + size
621        tocs = fulltoc[first:last]
622        tp = '<div>'
623        for toc in tocs:
624            pageurl = self.getLink('pn', toc['pn'])
625            tp += '<div class="tocline">'
626            content = toc['content']
627            if content:
628                tp += '<div class="toc name">[%s] %s</div>'%(toc['level-string'], toc['content'])
629            else:
630                tp += '<div class="toc name">[Figure %s]</div>'%(toc['level-string'])
631           
632            if toc.get('no', None):
633                tp += '<div class="toc page"><a href="%s">Page: %s (%s)</a></div>'%(pageurl, toc['pn'], toc['no'])
634            else:
635                tp += '<div class="toc page"><a href="%s">Page: %s</a></div>'%(pageurl, toc['pn'])
636               
637            tp += '</div>\n'
638           
639        tp += '</div>\n'
640       
641        return tp
642           
643   
644    def manage_changeMpiwgXmlTextServer(self,title="",serverUrl="http://mpdl-text.mpiwg-berlin.mpg.de/mpdl/interface/",timeout=40,repositoryType=None,RESPONSE=None):
645        """change settings"""
646        self.title=title
647        self.timeout = timeout
648        self.serverUrl = serverUrl
649        if repositoryType:
650            self.repositoryType = repositoryType
651        if RESPONSE is not None:
652            RESPONSE.redirect('manage_main')
653       
654# management methods
655def manage_addMpiwgXmlTextServerForm(self):
656    """Form for adding"""
657    pt = PageTemplateFile("zpt/manage_addMpiwgXmlTextServer", globals()).__of__(self)
658    return pt()
659
660def manage_addMpiwgXmlTextServer(self,id,title="",serverUrl="http://mpdl-text.mpiwg-berlin.mpg.de/mpdl/interface/",timeout=40,RESPONSE=None):
661#def manage_addMpiwgXmlTextServer(self,id,title="",serverUrl="http://mpdl-text.mpiwg-berlin.mpg.de:30030/mpdl/interface/",timeout=40,RESPONSE=None):   
662    """add zogiimage"""
663    newObj = MpiwgXmlTextServer(id=id,title=title,serverUrl=serverUrl,timeout=timeout)
664    self.Destination()._setObject(id, newObj)
665    if RESPONSE is not None:
666        RESPONSE.redirect('manage_main')
667       
668       
Note: See TracBrowser for help on using the repository browser.