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

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

Updates to allow proper handling of NERC DMS format and uprated dif2iso conversion

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