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

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

Added validation checking for dataOriginator and ability to handle multiple originators

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