source: TI01-discovery-Ingest/trunk/v4.3.0/ingestAutomation-upgrade/OAIBatch/ExtractISO.py @ 7456

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

various updates and tweaks so can support other DC difs

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