source: TI01-discovery-Ingest/trunk/v4n_MEDIN/ingestAutomation-upgrade/OAIBatch/ExtractISO.py @ 7052

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI01-discovery-Ingest/trunk/v4n_MEDIN/ingestAutomation-upgrade/OAIBatch/ExtractISO.py@7052
Revision 7052, 34.3 KB checked in by sdonegan, 11 years ago (diff)

Added code to support requirement to extract DataFormats? from ISO

Line 
1
2from xml.etree import ElementTree as ET
3import logging,urllib,os,sys,inspect
4from Utilities import IsoIngestListUtilities # so can use those useful getSingleVal and getMultipleVal methods for discerning lists etc (esp useful when using the isoVal to dbColumn methods..)
5
6
7class ExtractISO:
8
9        def __init__(self,filenameIP,isoFormat):                               
10                               
11                self.inputXMLfile = filenameIP
12                self.isoFormatToExtract = isoFormat
13               
14                #show actual values extracted from the XML in logging.info
15                self.showXMLvalues = True
16               
17                logging.info("Have initialised ISO XML Extractor")
18               
19                #self.createISOdataStructure()
20               
21               
22        '''
23        Method to control re-writing of ISO xml with NDG redirect URL's
24        '''
25        def generateNDGredirectURL(self, ndgRedirectURL, xmlOutputFile, changeUrl):
26               
27                logging.info("Generating XML copy with URLS converted to NDG redirect URL's")
28               
29                self.redirectBaseURL = ndgRedirectURL
30               
31                rewriteIsoXml = self.changeElementUrlVal(self.isoModel.urlsToChange(), xmlOutputFile, changeUrl)
32                               
33                return rewriteIsoXml
34       
35       
36       
37        '''
38        Method to map ISO variables to Discovery Database columns - use in PostgresDAO.py with getattr!
39       
40        returns a dictionary where key is
41    '''
42        def mapIsoFieldsToDBcolumns(self):
43               
44                return {'originalFormat':'original_format_name', \
45        'originalFormatVersion':'original_format_version',\
46        'datasetAbstract':'dataset_abstract', \
47        'resourceType_text':'resource_type', \
48        'resourceType_tsvector':'resource_type_ts_vector', \
49        'topicCategory_text':'topic_category', \
50        'topicCategory_tsvector':'topic_category_ts_vector', \
51        'lineage_text':'lineage', \
52        'lineage_tsvector':'lineage_ts_vector', \
53        'publicAccessLimitations_text':'limitations_public_access', \
54        'publicAccessLimitations_tsvector':'limitations_public_access_ts_vector', \
55        'dataOriginator_text':'data_originator',\
56        'dataOriginator_tsvector':'data_originator_tsvector',\
57        'metadataUpdateDate':'dataset_metadata_update_date', \
58        'dataFormat':'original_format_name',
59        'createDate':'dataset_metadata_creation_date',
60        'publicationDate':'dataset_metadata_publication_date',
61        'authors_text':'authors','parameters_text':'parameters',
62        'dataFormats_text':'data_formats','dataFormats_ts_vector':'data_formats_tsvector'}
63               
64               
65        '''
66                Method to match the columns found in mapIsoFieldsToDBcolumns to the postgres data types
67        '''
68        def mapDBcolumnsToIsoDataTypes(self):
69               
70                return {'original_format_name':'text', \
71        'original_format_version':'text',\
72        'dataset_abstract':'text', \
73        'resource_type':'text', \
74        'resource_type_ts_vector':'tsvector', \
75        'topic_category':'text', \
76        'topic_category_ts_vector':'tsvector', \
77        'lineage':'text', \
78        'limitations_public_access':'text', \
79        'lineage_ts_vector':'tsvector', \
80        'limitations_public_access_ts_vector':'tsvector', \
81        'data_originator':'text',\
82        'data_originator_tsvector':'tsvector',\
83        'dataset_metadata_update_date':'timestamp', \
84        'original_format_name':'text',
85        'dataset_metadata_creation_date':'timestamp',
86        'dataset_metadata_publication_date':'timestamp',
87        'authors':'text','parameters':'text',
88        'data_formats':'text','data_formats_tsvector':'tsvector'}
89       
90               
91        '''
92        Simple method to allow return information on success of extraction ( __init__ method cannot return anything so use this method)
93        '''
94        def createISOdataStructure(self):
95               
96                logging.info("")
97                logging.info("********************************************************************************************************************************************")
98                logging.info("****************** Creating ISO data structure from " + self.inputXMLfile + "****************** ")
99                logging.info("********************************************************************************************************************************************")
100                logging.info("")
101               
102                #set processing message and processing docs
103                self.processingMsg = ''
104                self.validDoc = True
105               
106                self.root = self.getIsoXML(self.inputXMLfile)
107               
108                self.isoFileLocation = self.inputXMLfile       
109               
110                #self.root = self.getIsoXML('/misc/glue1/sdonegan/NDG3_workspace/buildouts/oai_document_ingester_MEDIN/ingestAutomation-upgrade/OAIBatch//data/NEODC/stub_iso/neodc.nerc.ac.uk__stubISO__dataent_11658383444211836_DIF.xml')
111               
112                if self.root is None:
113                        logging.error("Detected possible problem with input xml - cannot parse it!")
114                        self.processingMsg = 'Problem with the XML structure!'
115                        self.validDoc = False
116                        return False
117               
118                #choose the ISO xpath definition class depending on format profile required
119                self.importISOxmlDefinition(self.isoFormatToExtract)
120                                                       
121                #get all the required information from the chosen xpath definition class
122                self.datasetID = self.getElementVal(self.isoModel.datasetID())
123                if self.datasetID == 'None':
124                        self.processingMsg = 'No entry for Dataset ID so record not valid and cannot/will not Ingest!'
125                        logging.error(self.processingMsg)
126                        self.validDoc = False
127               
128                else:
129                        #noticed some MEDIN centres allowing space characters in a unique id.. change to single underscore! (remember only 1 allowed!)
130                        self.datasetID[0][0] = self.datasetID[0][0].replace(" ", "_")
131               
132               
133                self.datasetName = self.getElementVal(self.isoModel.dataSetName())
134                if self.datasetName == 'None':
135                        self.processingMsg = 'No entry for Dataset Name so record not valid and cannot/will not Ingest!'
136                        logging.error(self.processingMsg)
137                        self.validDoc = False
138               
139                self.datasetAbstract = self.getElementVal(self.isoModel.dataSetAbstract())
140                               
141                self.revisionDate = self.getElementVal(self.isoModel.metadataRevisionDate())
142               
143                self.createDate = self.getElementVal(self.isoModel.metadataCreationDate())
144                               
145                self.datasetName = self.getElementVal(self.isoModel.dataSetName())             
146               
147                self.boundingDates = self.getElementVal(self.isoModel.boundingDates())
148               
149                self.boundingDatesRange = self.boundingDateRange(self.boundingDates)
150                                       
151                self.originalFormat = self.getElementVal(self.isoModel.originalFormat())
152               
153                self.dataFormats = self.getElementVal(self.isoModel.dataFormats())
154                               
155                if self.dataFormats != 'None':
156                       
157                        self.dataFormatsOb = IsoIngestListUtilities(self.dataFormats,True)
158                        self.dataFormats_text = self.dataFormatsOb.getDelimitedStringFromList(self.dataFormatsOb.listVals)
159                       
160                        #will be nothing in there due to way we've caught NoneTypes in Utilities.py - so set to 'None' here so can be nulled in sql
161                        if len(self.dataFormats_text) == 0:
162                                self.dataFormats_text = 'None'
163                       
164                        self.dataFormats_ts_vector = self.dataFormats_text
165                                               
166                else:
167                        self.dataFormats_text = 'None'
168                        self.dataFormats_ts_vector = 'None'
169                       
170                self.authors = self.getElementVal(self.isoModel.authors())
171               
172                if self.authors == 'None':
173                        self.processingMsg = 'No entry for Authors so record not valid and cannot/will not Ingest!'
174                        logging.error(self.processingMsg)
175                        self.validDoc = False
176               
177                else:
178                        #need to provide a list for updating relevant db column - keep distinct from other authors based vars just in case of problems..               
179                        self.authorOb = IsoIngestListUtilities(self.authors,True)               
180                        self.authors_text = self.authorOb.getDelimitedStringFromList(self.authorOb.listVals)           
181                        self.authors_ts_vector = self.authors_text
182                               
183                self.datacentreName = self.getElementVal(self.isoModel.dataCentreName())
184                               
185                if self.datacentreName == 'None':
186                        self.processingMsg = 'No entry for Data Centre name so record not valid and cannot/will not Ingest!'
187                        logging.error(self.processingMsg)
188                        self.validDoc = False
189               
190               
191                self.keywords = self.getElementVal(self.isoModel.keywords())
192               
193                self.keywordsList = self.listify(self.keywords)
194               
195                if self.keywords == 'None':
196                        logging.warn("No parameter info for record!")
197                        self.parameters_text = ''
198                        self.parameters_tsvector = ''
199                else:           
200                        self.parametersOb = IsoIngestListUtilities(self.keywords,True)                   
201                        self.parameters_text = self.parametersOb.getDelimitedStringFromList(self.parametersOb.listVals)
202                        self.parameters_tsvector = self.parameters_text
203               
204               
205               
206                coords = self.getElementVal(self.isoModel.coordinates())
207                if coords == 'None':                   
208                        self.boundingBoxCoordinates = None
209                else:
210                        self.boundingBoxCoordinates = coords   
211               
212                #These are the MEDIN requested extra fields - make optional
213                try:
214                        self.originalFormatVersion = self.getElementVal(self.isoModel.originalFormatVersion())
215                except:
216                        self.originalFormatVersion = None
217               
218                try:
219                        self.publicationDate = self.getElementVal(self.isoModel.metadataPublicationDate())
220                except:
221                        self.publicationDate = None
222                       
223                try:
224                        self.resourceType = self.getElementVal(self.isoModel.resourceType())
225                        self.resourceType_text = self.resourceType
226                        self.resourceType_tsvector = self.resourceType
227                except:
228                        self.resourceType = None
229                        self.resourceType_text = None
230                        self.resourceType_tsvector = None
231               
232                try:
233                        self.topicCategory = self.getElementVal(self.isoModel.topicCategory())
234                        self.topicCategory_text = self.topicCategory
235                        self.topicCategory_tsvector = self.topicCategory
236                except:
237                        self.topicCategory = None
238                        self.topicCategory_text = None
239                        self.topicCategory_tsvector = None
240               
241                try:
242                        self.lineage = self.getElementVal(self.isoModel.lineage())
243                        self.lineage_text = self.lineage
244                        self.lineage_tsvector = self.lineage
245                except:
246                        self.lineage = None
247                        self.lineage_text = None
248                        self.lineage_tsvector = None
249               
250                try:
251                        self.publicAccessLimitations = self.getElementVal(self.isoModel.publicAccessLimitations())
252                        self.publicAccessLimitations_text = self.publicAccessLimitations
253                        self.publicAccessLimitations_tsvector = self.publicAccessLimitations
254                except:
255                        self.publicAccessLimitations = None
256                        self.publicAccessLimitations_text = None
257                        self.publicAccessLimitations_tsvector = None
258               
259               
260                try:
261                        self.dataOriginator= self.getElementVal(self.isoModel.dataOriginator())
262                                               
263                except:
264                        self.dataOriginator = None
265                        self.dataOriginator_text = None
266                        self.dataOriginator_tsvector = None
267                       
268                       
269                #check presence of originator - Mandatory!
270                if self.dataOriginator == 'None' or self.dataOriginator is None:
271                        self.processingMsg = 'No entry for Data Originator so record not valid and cannot/will not Ingest!'
272                        logging.error(self.processingMsg)
273                        self.validDoc = False
274               
275                else:                   
276                        self.dataOriginatorOb = IsoIngestListUtilities(self.dataOriginator,True)
277                       
278                        #add some extra specifications to deal with MEDIn requirement to have both searchable field (vector) and return as a text field
279                        self.dataOriginator_text = self.dataOriginatorOb.getDelimitedStringFromList(self.dataOriginatorOb.listVals)
280                        self.dataOriginator_tsvector = self.dataOriginator_text
281               
282                try:
283                        self.dataFormat = self.getElementVal(self.isoModel.originalFormat())
284                except:
285                        self.dataFormat = None
286               
287                try:
288                        self.metadataUpdateDate = self.getElementVal(self.isoModel.metadataUpdateDate())
289                except:
290                        self.metadataUpdateDate = None
291                       
292                       
293                try:
294                        self.originalFormatName = self.getElementVal(self.isoModel.originalFormat())
295                except:
296                        self.originalFormatName = None
297                       
298                try:
299                        self.originalFormatVersion = self.getElementVal(self.isoModel.originalFormatVersion())
300                except:
301                        self.originalFormatVersion = None
302                       
303               
304                logging.info("")
305                logging.info(" ****************** Completed rendering ISO xml into data structure! ****************** ")
306                logging.info("")
307               
308                return True
309       
310       
311        '''
312        Method to generate a simple list object from a compound list set
313        '''
314        def listify(self,listObject):
315               
316                list = []
317               
318                for outer in listObject:
319                        for inner in outer:                             
320                                list.append(inner)
321                                               
322                return list
323       
324        '''
325        Method to check date formats to ensure correct insertion into database - if doesnt match minimum YYYY-MM-DD format then is false,
326        so date should be set to None in calling method.  Will adjust self.processingMsg too for return info
327        '''
328        def checkDate(self,inputDate):
329               
330                if inputDate is None:
331                        return False
332               
333               
334                dateStrArr = inputDate.split('-')
335               
336                #check if long date string or short - if long, will have a T in! (should do..)
337                #just stick with date values now..
338                if 'T' in dateStrArr:
339                        logging.info("*********************************** LONG VALUE DATE STRING ALERT!")
340                        dateStrArr = dateStrArr[:3]
341                               
342                if len(dateStrArr)< 3:
343                        msg = "Invalid date format (%s) - insufficient length!\n" %inputDate
344                        logging.warn(msg)
345                        self.processingMsg = self.processingMsg + msg
346                        return False
347               
348                else:
349                        yearStr = dateStrArr[0]
350                        monthStr = dateStrArr[1]
351                        dayStr = dateStrArr[2]
352                               
353                #check days, months etc - Years can be anything... just do simple test for now
354                if int(monthStr) > 12 or int(monthStr) < 1:
355                        msg = "Invalid Month value (%s) \n" %monthStr
356                        logging.warn(msg)
357                        self.processingMsg = self.processingMsg + msg
358                        return False
359                               
360                elif monthStr in ['01','03','05','07','08','10','12']:
361                        if int(dayStr) > 31 or int(dayStr) < 1:
362                                msg = "(31 ck) Invalid Day value (%s) for month (%s)\n" %(dayStr,monthStr)
363                                logging.warn(msg)
364                                self.processingMsg = self.processingMsg + msg
365                                return False
366                        else:
367                                logging.info("(31 ck) Date value seems ok...(%s) " %inputDate)
368                                return True
369                       
370                elif monthStr in ['04','06','09','11']:
371                        if int(dayStr) > 30 or int(dayStr) < 1:
372                                msg = "(30 ck) Invalid Day value (%s) for month (%s)\n" %(dayStr,monthStr)
373                                logging.warn(msg)
374                                self.processingMsg = self.processingMsg + msg
375                                return False
376                        else:
377                                logging.info("(30 ck) Date value seems ok...(%s) " %inputDate)
378                                return True
379                                       
380                elif monthStr == '02':
381                        if int(yearStr) % 4 == 0:
382                                if int(dayStr) > 29 or int(dayStr) < 1:
383                                        msg = "Invalid Day value (%s) for month (%s)\n" %(dayStr,monthStr)
384                                        logging.warn(msg)
385                                        self.processingMsg = self.processingMsg + msg
386                                        return False
387                                else:
388                                        logging.info("(29 ck) Date value seems ok...(%s) " %inputDate)
389                                        return True
390                                                                       
391                        else:                           
392                                if int(dayStr) > 28 or int(dayStr) < 1:
393                                        msg = "Invalid Day value (%s) for month (%s)\n" %(dayStr,monthStr)
394                                        logging.warn(msg)
395                                        self.processingMsg = self.processingMsg + msg
396                                        return False
397                                else:
398                                        logging.info("(28 ck) Date value seems ok...(%s) " %inputDate)
399                                        return True                                                                     
400                               
401                else:
402                        msg = "Invalid month value (%s)" %monthStr
403                        logging.warn(msg)
404                        self.processingMsg = self.processingMsg + msg
405                        return False
406               
407        '''
408        Method to return start and end dates from a sequence of dates such as that found returned using the xpath defn from boundingDates
409        '''
410        def boundingDateRange(self,boundingDatesList):
411               
412                #generate a simple list of all dates
413                allDates = []
414                returnDates = {}
415               
416                       
417                #remember, need to sort out whether there was a start and end date in the first place, or just one so can copy values over
418                if len(boundingDatesList) == 1:
419                        logging.info("Only 1 set of start and stop dates, so setting range to this..")
420                        returnDates['start'] = boundingDatesList[0]['start']
421                        returnDates['end'] = boundingDatesList[0]['end']
422                       
423                       
424                       
425                       
426                        #check date validity
427                        for date in returnDates.keys():                                 
428                                if not self.checkDate(returnDates[date]):
429                                        returnDates[date] = 'None'
430                       
431                        #convert None to 'None'
432                        for date in returnDates.keys():
433                                if returnDates[date] is None:
434                                        returnDates[date] = 'None'
435                               
436                        return returnDates
437               
438                for outer in boundingDatesList:
439                        for inner in outer.keys():
440                                if outer[inner] is not None:
441                                        if outer[inner] != 'None':
442                                                #some other test here to ensure proper date object
443                                                allDates.append(outer[inner])
444                                               
445                               
446                #min and max seem to work for now (to get it working) - assuming proper dateformat (restrict to YYYY-MM-DD for now)
447                #TODO: use datetime library to do this properly
448               
449                #check correct formatting for dates - if invalid, set to None - use checkDate method           
450                for date in returnDates.keys():                 
451                        if not self.checkDate(returnDates[date]):
452                                returnDates[date] = 'None'
453                                       
454                returnDates['start'] = min(allDates)
455                returnDates['end'] = max(allDates)
456               
457               
458                return returnDates
459       
460       
461               
462        '''
463        Method to import specific ISO xpath definition class depending on the profile used
464        '''
465        def importISOxmlDefinition(self,format):
466               
467                '''
468                MEDIN Profile v2.3 (date? - MEDIN upgrade dev)
469                '''
470               
471                logging.info("Format chosen: " + format)
472               
473               
474               
475                #Dynamically import module of correct profile...
476                import_string = "from " + format + " import " + format + " as iso"
477               
478                try:
479                        exec import_string
480                except:
481                        logging.error("Could not import xpath class for: " + format)
482                        sys.exit()
483               
484                self.isoModel = iso()
485               
486               
487        '''
488        Method to govern the changing of an element value (in the first instance change a url to a NDG redirect URL
489       
490        NOTE: will only change Urls if changeUrl set to True - this method is also used to ready the xml for writing into db
491       
492        '''
493        def changeElementUrlVal(self,keyMethod,outputFile,changeUrl):
494               
495       
496                logging.info("******************************************************************************************")
497                logging.info("Extracting xpath information for data extraction type (to change URL..):" + keyMethod[0])
498                logging.info("******************************************************************************************")
499                               
500               
501                #first need to interpret the contents of the tuple we've been passed everything is a dictionary in the tuple apart from element 0:
502                #build a map of it - do this as a dictionary with the key as the position in the tuple and the value as the element val, be it str or dictionary key
503                dataStruct = self.interpretTuple(keyMethod)
504                cnt = 0
505               
506                #this is similar to getElementVal - but no interest in ordering or returning the actual value..
507               
508                #if changeUrl set to true, activate changes, just write xml as is if not
509                if changeUrl:           
510                        logging.info("changing urls now...")
511                        for i in dataStruct.keys()[1:]:
512                                               
513                                thisData = keyMethod[i][dataStruct[i]]
514                                                               
515                                #is dicionary a dependant value or straight xpath?
516                                if 'baseXpath' in thisData.keys():
517                                        logging.info("Extracting xml using COMPLEX dependant method for xpath def: " + str(cnt))                       
518                                        self.changeDependantURLElementVal(thisData['baseXpath'],thisData['elValXpath'],thisData['depValXpath'],thisData['depVal'])                             
519                                                               
520                                if 'xpath' in thisData.keys():         
521                                        logging.info("Extracting xml using SIMPLE method for xpath def: " + str(cnt))                                           
522                                        self.changeSimpleURLElementVal(thisData['xpath'])       
523                                       
524                else:
525                        logging.info("URL's not set to change!")
526                                                                               
527                #having returned the completed xml tree... write it to the OP file dictated..
528                try:
529                        #rewrite this back into the xml doc                             
530                        self.etree.write(outputFile)
531                        logging.info("Have successfully generated new ISO file with rewritten urls to NDG redirect format!")
532                       
533                        return True
534               
535                except:
536                        logging.error("Could not rewrite NDG url ISO xml to; " + outputFile)
537                       
538                        return False
539               
540               
541        '''
542        Method to change an ISO element val - simple xpath value
543        '''
544        def changeSimpleURLElementVal(self, xpath):
545               
546                #Note using single path namespath appender rather than list version.. (keeps it simpler)       
547                xpathNS = self.appendNameSpace(xpath)           
548               
549                resElementVal = []
550               
551                try:   
552                        rootEl = self.root.findall(xpathNS)
553                       
554                except:
555                        logging.error("Could not find element for this xpath: " + xpath)                       
556                       
557                                               
558                #use a text 'None' rather than a None type                     
559                for elVal in rootEl:
560                        if elVal is None: 
561                                originalUrl = 'None' 
562                        else:
563                                try:                   
564                                        originalUrl = elVal.text       
565                                        redirectURL = self.redirectBaseURL + originalUrl + '&docID=' + urllib.quote(self.datasetID[0][0]) + '&docTitle=' + urllib.quote(self.datasetName[0][0])
566                               
567                                        #reassign...
568                                        elVal.text = redirectURL                                       
569                                        logging.info("Have successfuly rewritten ndg redirect URL: " + redirectURL)
570                                       
571                                except:
572                                        logging.error("Could not change original (simple) url!")
573               
574                       
575                #no need to return anything -if we rewrite the doc, any successful changes will be incorporated, otherwise will be ignored
576       
577        '''
578        Method to change an ISO element val - compound dependant xpath values
579        '''
580        def changeDependantURLElementVal(self,baseXpath,elXpath,depXpath,depValReqd):
581               
582                #Note using single path namespath appender rather than list version.. (keeps it simpler)       
583                baseXpath = self.appendNameSpace(baseXpath)
584                               
585                #for path in baseXpaths:
586                try:                           
587                        rootEl = self.root.findall(baseXpath) #returns an elementree object of elements in a list                               
588                except:
589                        logging.error("Could not find element for this xpath: " + baseXpath)
590                        return 'None'
591                       
592                for el in rootEl:
593                        thisElXpth = self.appendNameSpace(elXpath)
594                                                               
595                        thisEl = self.doFindall(el,thisElXpth)
596                               
597                        #if there's a value for the actual xpath we want, go and get the dependant value
598                        if thisEl != 'None':
599                                       
600                                elVal = thisEl[0]
601                                elValTxt = elVal.text #NOTE take first one as we expect this to be the value we want
602                                                                       
603                                thisEldepXpth = self.appendNameSpace(depXpath)                                 
604                                thisDepEl = self.doFindall(el,thisEldepXpth)                                                           
605                                       
606                                if thisDepEl != 'None':
607                                                                               
608                                        depVal = thisDepEl[0].text
609                                                                       
610                                        if depVal == depValReqd:
611                                               
612                                                try:
613                                                        originalUrl = elValTxt                                         
614                                                        redirectURL = self.redirectBaseURL + originalUrl + '&docID=' + urllib.quote(self.datasetID[0][0]) + '&docTitle=' + urllib.quote(self.datasetName[0][0])
615                                                                                       
616                                                        #reassign...
617                                                        elVal.text = redirectURL                                                                                       
618                                                        logging.info("Have successfuly rewritten ndg redirect URL: " + redirectURL)
619                                                       
620                                                except:
621                                                        logging.error("Could not change original (complex) url!")
622                                       
623                                                                                                                       
624                #no need to return anything -if we rewrite the doc, any successful changes will be incorporated, otherwise will be ignored
625               
626       
627        '''
628        Method to initially interpret the xpath dictionary type passed
629        '''
630        def interpretTuple(self,keyMethod):
631               
632                dataStruct = {}
633                counter=0
634                for i in keyMethod:
635                        if type(i) is dict:
636                               
637                                #loop through the top level (remember its a dictionary of dictionaries!)
638                                for j in i.keys():                             
639                                        dataStruct[counter]=j
640                       
641                        #Only other data type present should be a string
642                        if type(i) is str:
643                                dataStruct[counter] = i
644                               
645                        counter = counter + 1
646                               
647                return dataStruct
648       
649               
650        '''
651        Method to aid passing and extraction info from complex xpath tuples
652       
653        Will work out whether complex or simple xpath extraction required depending on type of xpath method sent
654       
655        If 'order' key present then will return a dictionary of values with the extracted value using the specified key.
656       
657        '''
658        def getElementVal(self,keyMethod):
659               
660                #develop handler method to simplify
661                returnValList=[]                               
662                returnVal = 'None'
663               
664                logging.info("******************************************************************************************")
665                logging.info("Extracting xpath information for data extraction type:"  + keyMethod[0])
666                logging.info("******************************************************************************************")
667               
668                               
669                showValue = self.showXMLvalues
670               
671                #first need to interpret the contents of the tuple we've been passed everything is a dictionary in the tuple apart from element 0:
672                #build a map of it - do this as a dictionary with the key as the position in the tuple and the value as the element val, be it str or dictionary key
673                dataStruct = self.interpretTuple(keyMethod)
674                '''
675                dataStruct = {}
676                counter=0
677                for i in keyMethod:
678                        if type(i) is dict:
679                               
680                                #loop through the top level (remember its a dictionary of dictionaries!)
681                                for j in i.keys():                             
682                                        dataStruct[counter]=j
683                       
684                        #Only other data type present should be a string
685                        if type(i) is str:
686                                dataStruct[counter] = i
687                               
688                        counter = counter + 1
689                '''
690               
691                #now iterate through the key list in dataStruct but miss First one (as its a name val!)                 
692                valueList = []
693                ordering = False
694               
695                cnt = 1
696               
697                for i in dataStruct.keys()[1:]:
698                                               
699                        thisData = keyMethod[i][dataStruct[i]]
700                                                               
701                        #is dicionary a dependant value or straight xpath?
702                        if 'baseXpath' in thisData.keys():
703                                logging.info("Extracting xml using COMPLEX dependant method for xpath def: " + str(cnt))                       
704                                returnVal = self.returnDependantElementVal(thisData['baseXpath'],thisData['elValXpath'],thisData['depValXpath'],thisData['depVal'],showValue)                           
705                                valueList.append(returnVal)
706                                                               
707                        if 'xpath' in thisData.keys():         
708                                logging.info("Extracting xml using SIMPLE method for xpath def: " + str(cnt))                                           
709                                returnVal =  self.returnSimpleElementVal(thisData['xpath'],showValue)                                                           
710                                valueList.append(returnVal)
711                                                               
712                        #is there any ordering info required - if so, order the return list according to the ordering tuple
713                        if 'order' in keyMethod[i].keys():
714                                logging.info("Returning a dictionary of paired values as ordering requested for xpath (number of xpath defs MUST match ordering vals available")                       
715                                ordering = True                         
716                                orderingList = thisData
717                               
718                        else:
719                                logging.info("Returning a simple list of values for xpath def: " + str (cnt))
720                               
721                        cnt = cnt + 1
722                       
723                #check theres any data..
724                dataPresent=False
725                for values in valueList:
726                       
727                        #as valueList is a list of lists, length of valueList will be > 1 even if no data in sub-lists..
728                        #..so check through all subLists and if not data at all then can return None
729                        if len(values) > 0:
730                                dataPresent = True
731                               
732                if not dataPresent:                     
733                        logging.warning("No data to extract from the xml for %s" %keyMethod[0])
734                        return 'None'
735                       
736                #if ordering is required, create a corresponding dictionary of ordered values                                   
737                if ordering:
738                       
739                        #logging.INFO("Ordering data by specified order information!")
740                        logging.info("ordering information now!")
741                       
742                        '''FIRST need to group information together in correct order (i.e. elementtree groups all starts in one list, then all ends etc
743                        i.e. [[start1,start2],[end1,end2]] to [[start1,end1],[start2,end2]]
744                        '''
745                        #import pdb
746                        #pdb.set_trace()
747                        #check that number of number of elements in ordering list is not less than number of items in list to be ordered!
748                        #if multiple sets to be ordered, check numbers and return as dictionary
749                        checkCompLnth = len(valueList[0])
750                        for list in valueList:
751                                if len(list) != checkCompLnth:
752                                        logging.error("Sub component lists are of unequal length - CANNOT order!!")
753                                        return 'None'
754                               
755                        #outer loop should be the number of elements in the sublists..
756                        outer = []
757                        for localPos in range(0,checkCompLnth):
758                                inner=[]
759                                #inner loop should be the number of lists corresponding to repeating subelements                       
760                                for listPos in range(0,len(valueList)):
761                                       
762                                                inner.append(valueList[listPos][localPos])
763                                #append to outer loop
764                                outer.append(inner)                                                     
765                       
766                        for returnedList in outer:
767                               
768                                #create a disctionary for this round (will append it later to the return version)
769                                orderedValsSub = {}
770                                                                               
771                                if len(returnedList) != len(orderingList):
772                                        logging.error("Ordering List length does not match length of list to be ordered! (NOTE: can only order where lengths match!)")
773                                        returnValList.append('None')
774                               
775                                else:           
776                                        #go through valueList and extract values in correct position and build dictionary
777                                        for i in orderingList.keys():
778                                               
779                                                orderedValsSub[i] = returnedList[orderingList[i]-1] # remember there's an offset as '0' not used in these vals
780                               
781                               
782                                        returnValList.append(orderedValsSub)
783                       
784                        return returnValList
785                       
786                else:
787                        return valueList
788                       
789                       
790        '''
791        Method to return a list of values of an element value dependant on another element value held within the local tree
792        i.e. return a specific string val depending on a local code val in ISO
793       
794        if element exists and any dependant value present matches the expected value, the element value is returned as part of a list,
795        otherwise an empty list is returned
796        '''
797        def returnDependantElementVal(self,baseXpath,elXpath,depXpath,depValReqd,showValue=False):
798                       
799                #Note using single path namespath appender rather than list version.. (keeps it simpler)       
800                baseXpath = self.appendNameSpace(baseXpath)
801               
802               
803                resDependantVal = []
804               
805                #for path in baseXpaths:
806                try:                           
807                        rootEl = self.root.findall(baseXpath) #returns an elementree object of elements in a list                               
808                except:
809                        logging.error("Could not find element for this xpath: " + baseXpath)
810                        return 'None'
811               
812               
813                for el in rootEl:
814                       
815                        thisElXpth = self.appendNameSpace(elXpath)                                             
816                        thisEl = self.doFindall(el,thisElXpth)
817                       
818                        #if there's a value for the actual xpath we want, go and get the dependant value
819                        if thisEl != 'None':
820                                       
821                                elVal = thisEl[0].text #NOTE take first one as we expect this to be the value we want
822                               
823                                #catch any nones here..
824                                #if elVal is None:
825                                #       import pdb
826                                #       pdb.set_trace()
827                                #       return 'None'
828                                                                       
829                                thisEldepXpth = self.appendNameSpace(depXpath)                                 
830                                thisDepEl = self.doFindall(el,thisEldepXpth)                                                           
831                               
832                                if thisDepEl != 'None':
833                                                                               
834                                        depVal = thisDepEl[0].text
835                                                                       
836                                        if depVal == depValReqd:
837                                               
838                                                if showValue is True:
839                                                        logging.info("")
840                                                        logging.info("          Element Value: " + elVal)
841                                                        logging.info("")
842                                               
843                                                resDependantVal.append(elVal)
844                                               
845                                                                                                                                               
846                return resDependantVal
847       
848        '''
849        Method to extract a value from the doc using a simple xpath.  If no element found or no data present then None is returned
850        '''
851        def returnSimpleElementVal(self,xpath,showValue=False):
852               
853                #Note using single path namespath appender rather than list version.. (keeps it simpler)       
854                xpathNS = self.appendNameSpace(xpath)           
855               
856                resElementVal = []
857               
858                try:   
859                        rootEl = self.root.findall(xpathNS)
860                       
861                except:
862                        logging.error("Could not find element for this xpath: " + xpath)                       
863                        return ['None']
864                                               
865                #use a text 'None' rather than a None type
866               
867                for elVal in rootEl:
868                        if elVal is None:
869                                resElementVal.append('None')
870                        else:
871                                value = elVal.text
872                                if (showValue is True) and (value is not None):
873                                        logging.info("")
874                                        logging.info("          Element Value: " + value)
875                                        logging.info("")
876                                       
877                                resElementVal.append(value)
878                '''     
879                if rootEl[0].text is None:                                     
880                        resElementVal.append('None')
881                else:
882                        for el in rootEl:                                       
883                                resElementVal.append(el.text)   
884                               
885                                       
886                '''
887               
888                return resElementVal
889                               
890       
891        '''
892        Method to run an elementtree findall on ns appended path in the supplied element and return list
893        returns None if nothing found
894        '''
895        def doFindall(self,el,thisElXpth):
896                               
897                try:
898                        thisElXpthEl = el.findall(thisElXpth)
899                       
900                        if len(thisElXpthEl) == 0:
901                                thisElXpthEl = 'None'
902                except:
903                        thisElXpthEl = 'None'
904               
905               
906                return thisElXpthEl
907               
908               
909        '''
910        Method to extract actual value from xml doc - expects a list of namespace qualified xpaths and will return corresponding list of values
911        '''
912        def getXmlVal(self,paths):
913               
914                xmlVals = []
915                               
916                for path in paths:
917                        try:
918                                xmlVals.append(self.root.find(path).text)                       
919                        except:
920                                logging.error("Could not extract value for xpath: " + path)
921                                xmlVals.append('null')
922                               
923                return xmlVals                                 
924               
925        '''
926        Method to hold a dictionary linking namespace prefixes to URLs
927        '''
928        def isoNameSpaces(self):
929               
930                isoNs = {'gmd':'http://www.isotc211.org/2005/gmd','gco':'http://www.isotc211.org/2005/gco',
931                                'gmx':'http://www.isotc211.org/2005/gmx','gml':'http://www.opengis.net/gml/3.2',
932                                'none':'','gts':'http://www.isotc211.org/2005/gts','gsr':'http://www.isotc211.org/2005/gsr',
933                                'gss':'http://www.isotc211.org/2005/gss'
934                                }
935               
936                return isoNs
937       
938       
939        '''
940        Method to return DEFAULT namespace in elementtree format of ISO profile used (i.e. should be gmd)
941        (to deal with those elements with no prefix)
942        '''
943        def defaultIsoNamespace(self):
944                return 'gmd'
945               
946       
947       
948        '''
949        Method to extract root element of XML file specified using elementtree
950        '''     
951        def getIsoXML(self,file):
952               
953                logging.info("Getting xml root element")
954               
955                #parse the xml with elementtree
956                try:
957                        self.etree=ET.parse(file)
958                        root=self.etree.getroot() # should be the "gmd:MD_Metadata" element
959                except:
960                        logging.error("File %s appears to be bad XML!" %file)
961                        return None
962               
963                #avoid ns0 and ns1 etc etc namespaces when rewriting this ET
964                logging.info("Sorting out namespaces so can properly rewrite ISO xml..")
965                for ns in self.isoNameSpaces().keys():         
966                        if ns != 'None':                                               
967                                ET._namespace_map[self.isoNameSpaces()[ns]] = ns
968                               
969                #check root element is an ISO one - use elementtree appended namespace..
970                if root.tag != '{http://www.isotc211.org/2005/gmd}MD_Metadata':
971                        logging.error("XML document does not appear to be ISO (not gmd:MD_Metadata)!!")                 
972                        return None
973       
974                return root
975               
976               
977        '''
978        Method to convert the given xpath list to a namespace abbreviated version so elementtree can handle it (grrr.). 
979        Will return a list of corresponding namespace qualified xpaths - if errors a 'null' will be placed.
980        '''
981        def appendNameSpaceList(self,paths):
982               
983                nameSpaceAppendedPaths = []
984               
985               
986                for path in paths:             
987                       
988                        pathElements = path.split('/')
989                       
990                        #note due to crappy elementtree have to avoid initial "/"
991                        count = 0
992                        for element in pathElements:
993                               
994                                try:                                   
995                                        if ':' in element:                                             
996                                                splitElement = element.split(':')
997                                                nsPrefix,nsElement = splitElement[0],splitElement[1]
998                                        else:
999                                                #use default namespace                                         
1000                                                nsPrefix = self.defaultIsoNamespace()
1001                                                nsElement = element
1002                                       
1003                                        if count == 0:
1004                                               
1005                                                #appendedPath = self.returnNS() + element
1006                                                appendedPath = '{' + self.isoNameSpaces()[nsPrefix] +'}' + nsElement
1007                                        else:
1008                                               
1009                                                appendedPath = appendedPath + '/{' + self.isoNameSpaces()[nsPrefix] +'}' + nsElement
1010                                       
1011                                        count += 1
1012                                except:
1013                                        appendedPath = 'null'
1014                                        logging.error("Could not change to elementtree ns xpath")
1015                                                       
1016                        #clear up any blank namespace prefixes
1017                        appendedPath = appendedPath.replace('{}','')
1018                       
1019                        nameSpaceAppendedPaths.append(appendedPath)
1020
1021                return nameSpaceAppendedPaths
1022       
1023        '''
1024        Method to convert the given xpath to a namespace abbreviated version so elementtree can handle it (grrr.). 
1025        Will return an equivalent namespace qualified xpath - if errors a 'null' will be placed.
1026        '''
1027        def appendNameSpace(self,path):
1028               
1029                nameSpaceAppendedPath = ''
1030               
1031                pathElements = path.split('/')
1032                       
1033                #note due to crappy elementtree have to avoid initial "/"
1034                count = 0
1035                for element in pathElements:
1036                               
1037                        try:                                   
1038                                if ':' in element:                                             
1039                                        splitElement = element.split(':')
1040                                        nsPrefix,nsElement = splitElement[0],splitElement[1]
1041                                else:
1042                                        #use default namespace                                         
1043                                        nsPrefix = self.defaultIsoNamespace()
1044                                        nsElement = element
1045                                       
1046                                if count == 0:
1047                                               
1048                                        #appendedPath = self.returnNS() + element
1049                                        appendedPath = '{' + self.isoNameSpaces()[nsPrefix] +'}' + nsElement
1050                                else:                                           
1051                                        appendedPath = appendedPath + '/{' + self.isoNameSpaces()[nsPrefix] +'}' + nsElement
1052                                       
1053                                count += 1
1054                        except:
1055                                appendedPath = 'null'
1056                                logging.error("Could not change to elementtree ns xpath")
1057                                                       
1058                #clear up any blank namespace prefixes
1059                nameSpaceAppendedPath = appendedPath.replace('{}','')
1060               
1061                return nameSpaceAppendedPath
Note: See TracBrowser for help on using the repository browser.