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

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

fixed bug in originator handling and error reporting

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