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

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

Updated keyword handling to allow proper detection and use of vocab list entered keywords. Note that only BODC and CEDA are currently using this so still need to keep old methods for other DCs

Line 
1#!/usr/bin/env python
2'''
3Class representing the a document to be ingested into the postgres DB table
4C Byrom Apr 08
5'''
6from xml.etree import cElementTree
7import os, sys, logging, re, pkg_resources
8from ndg.common.src.models.ndgObject import ndgObject
9from ndg.common.src.lib.ndgresources import ndgResources
10import ndg.common.src.lib.fileutilities as FileUtilities
11import ndg.common.src.lib.utilities as ndgUtilities
12from Utilities import xqueryTransformation,IsoIngestListUtilities
13import keywordAdder
14
15SAXON_JAR_FILE = 'lib/saxon9.jar'
16
17class PostgresRecord:
18    '''
19    Class representing the a document to be ingested into the postgres DB table
20    @param filename: Name of file to use a metadata record
21    @param ndg_dataprovider
22    @param datacentre_groups
23    @param datacentre_namespace
24    @param discovery_id
25    @param xq
26    @param doctype - type of doc to process
27    '''
28    # TODO MDIP transforms do not work very well for lots of files - so currently hiding these
29    #documentTypes = ['DIF', 'DC', 'ISO19139']#, 'MDIP']
30   
31    # vocab server - used for finding scope values in the moles files
32    ndg_data_provider_vocab = "http://vocab.ndg.nerc.ac.uk/term/N010"
33       
34    #def __init__(self, filename, ndg_dataprovider, datacentre_groups, datacentre_namespace, discovery_id, xq, docType):
35    def __init__(self, filename, ndg_dataprovider, datacentre_groups, datacentre_namespace, isoDataModel, xq, docType
36                                , xqExceptions , xqueryConversions, saxonJarFile, xqueryDocTypes, originalXML, currentMedinStandard, stubIso = None, difXML = None, isoXML = None):
37                 
38                 
39        logging.info("Setting up Postgres record for file, " + filename)
40       
41           
42        self.isoDataModel = isoDataModel
43       
44        self.filename = self.isoDataModel.isoFileLocation
45       
46        #note method of extracting info from isoDataModel - nested lists, so if one value then use [0][0]       
47        discovery_id = self.isoDataModel.datasetID[self.isoDataModel.findTheListData(self.isoDataModel.datasetID)][0]
48   
49        # NB, if we're dealing with an NDG data provider, the details are slightly different
50        if ndg_dataprovider:
51            discObj=ndgObject(discovery_id)
52            self._local_id = discObj.localID
53            self._repository_local_id = discObj.repository
54        else:
55            self._local_id = discovery_id
56            self._repository_local_id = datacentre_namespace
57           
58        self._datacentre_groups = datacentre_groups
59        self._repository = datacentre_namespace
60        self.discovery_id = discovery_id # just a single val..
61        self._xq = xq
62       
63        # simplify processing by uppercasing format at initialisation
64        self.docType = docType.upper()   
65       
66        #make sure we escape any special characters in this field... SJD 20/10/09       
67        #self.dataset_name = self.escapeSpecialCharacters(self.isoDataModel.datasetName[0])
68       
69        self.dataset_name = self.escapeSpecialCharacters(self.isoDataModel.datasetName[self.isoDataModel.findTheListData(self.isoDataModel.datasetName)][0])
70       
71        self.dcOb = IsoIngestListUtilities(self.isoDataModel.datacentreName,True)
72        self.datacentre_name = self.dcOb.singleVal
73       
74        #self.dataset_lastEdit = datasetLastEditUpdateDate
75        #self.dataset_lastEdit = self.isoDataModel.revisionDate[0][0]
76        self.dataset_lastEdit = self.isoDataModel.singleRevisionDate
77           
78        #self.datasetStartNom = datasetStartDateNom
79        if self.isoDataModel.boundingDatesRange != 'None':
80                self.datasetStartNom = self.isoDataModel.boundingDatesRange['start'] #dictionary method!
81                self.datasetEndNom = self.isoDataModel.boundingDatesRange['end'] #dictionary method!           
82                self.datasetTemporalData = self.isoDataModel.boundingDatesRange # set whole list of dictionaries for this as may be multiple boxes
83       
84        else:
85                self.datasetStartNom = 'null'
86                self.datasetEndNom = 'null'
87                self.datasetTemporalData = 'null'
88       
89        #self.datasetEndNom = datasetEndDateNom
90       
91        self.datasetSpatialData = self.isoDataModel.boundingBoxCoordinates # set whole list of dictionaries for this as may be multiple boxes
92       
93        self._molesFormat = None    # initialise this, so we can guarantee a value - to avoid using getattr
94        self._allDocs = {}  # array to store all the transformed docs - for easy retrieval by the DAO
95
96        # get the dir of the file - needed by the xquery to use as the target collection
97        tmp = filename.split('/')
98        self._dir = '/'.join(tmp[0:len(tmp)-1])
99        self.shortFilename = tmp[-1]
100       
101       
102        #self.originalFormat = self.isoDataModel.originalFormat[0][0]
103        self.originalFormat = file(self.filename).read()
104       
105        #explicitly pull out original dif file content if original format was dif, & iso if ISO
106        self.difXML = difXML
107        self.isoXML = isoXML
108       
109        # escape any apostrophes
110        self.originalFormat = self.escapeSpecialCharacters(self.originalFormat)
111       
112        self.currentMedinStandard = currentMedinStandard
113               
114        # initialise the various record fields
115        self.db_id = None    # the DB ID of the record, for easy reference when it is created
116        #self.molesFormat = None
117        self.dcFormat = None
118        #self.mdipFormat = None
119        self.iso19139Format = None
120        self.scn = 1    # system change number - keeps track of number of mods to a particular row
121       
122        # spatiotemporal data object
123        self.stData = None
124       
125        # fields to hold author, parameter and scope data
126        self.authors = None
127        self.parameters = None
128        self.scope = None
129       
130        #info needed for performing xquery conversions in the MEDIN ingest stack
131        self._xqueryConversionsExceptions = xqExceptions       
132        self._xqueryConversions = xqueryConversions       
133        self._xqueryDocTypes = xqueryDocTypes       
134        self._saxonJarFile = saxonJarFile
135       
136       
137        self.originalXMLdoc = self.escapeSpecialCharacters(originalXML)
138       
139        #need t define stubISO only if ingesting a dif.
140        if self.originalXMLdoc == "null":
141             self.stubISO = None
142        else:
143                self.stubISO = self.escapeSpecialCharacters(stubIso)
144               
145     
146   
147    def escapeSpecialCharacters(self, inputString):
148        '''
149        Adjust the input string to escape any characters that would interfere with string or DB
150        operations
151        @param inputString: string to correct
152        @return: corrected string
153        '''
154       
155        str = re.sub(r'\'', '\\\'', inputString)                       
156       
157        return str
158
159
160    def unescapeSpecialCharacters(self, inputString):
161        '''
162        Adjust the input string to remove escaped characters that would interfere with string or DB
163        operations
164        @param inputString: string to correct
165        @return: corrected string
166        '''
167        str = re.sub(r'%20', ' ', inputString)
168        return
169       
170   
171    def characterEncoding(self,inputString):       
172                '''
173                Method to use with strings pulled from original xml - try and do a string conversion, catch any unicode errors,
174                report it and return a properly encoded string if so
175                '''
176               
177                convertMsg = ''
178               
179                try:
180                        thisString = str(inputString)
181                       
182                except Exception, error:
183                       
184                        if type(error).__name__ == 'UnicodeEncodeError':
185                               
186                                #get the position of the dodgy characters & message
187                                convertMsg = 'Error with character encoding in this field (%s): %s'%(type(error).__name__,error)
188                                logging.warn(convertMsg)
189                               
190                                #now we've caught this (need to know for future reference rather than do a blanket conversion
191                                logging.warn("Converting bad characters to unicode..")
192                               
193                                thisString = ndgUtilities.encodeIntoHTMLNumericalCodes(inputString)
194                                                               
195                                #Need to escape these now
196                                thisString = re.sub(r'\\u', '\\\\\u', thisString)
197                                thisString = re.sub(r'\\x', '\\\\\\x', thisString)
198                               
199               
200               
201                       
202                return thisString,convertMsg
203   
204   
205    def doRecordTransforms(self):
206        '''
207        Run various transforms on the original doc, to populate the record with
208        the other types of doc used elsewhere
209        '''
210        logging.info("Running transforms for all document types")
211        for docType in self.documentTypes:
212            self.getDocumentFormat(docType)
213           
214        logging.info("Transforms complete")
215
216
217       
218
219    def doTransform(self, xQueryType):
220        '''
221        Transform the record according to the specified XQuery type
222        @param xQueryType: XQuery doc to use to do the transform
223        @return: the metadata record in the required transformed format
224        '''
225        logging.info("Running XQuery transform, " + xQueryType + " to create TRANSFORMED document!!")
226       
227       
228        #takes metadataFileLoc,repositoryName,metadataID,metadataFilename, saxonJar
229        #self.xqueryTransformation = xqueryTransformation(self.discovery_dir,self._repository,self.discovery_id,self._local_id,self._saxonJarFile)
230        self.xqueryTransformation = xqueryTransformation(self.discovery_dir,self._repository,self._local_id,self.discovery_id,self._saxonJarFile)
231        self.transformedXML = self.xqueryTransformation.runXquery(xQueryType)
232                                       
233        return self.transformedXML
234
235
236
237
238    def getDocumentFormat(self, docType):
239        '''
240        Lookup document format; if it is already defined then return it, else do the required XQuery
241        transform.  NB, transforms are ran on the molesFormat document - so ensure this is available
242        @param docType: format of document to return
243        '''
244       
245       
246        logging.info("Retrieving document type, " + docType)
247       
248        # the doc type doesn't exist - so run the xquery
249        transformedDoc = self.doTransform(docType)
250       
251        #not sure if we still need to do this..?
252        setattr(self, docType, transformedDoc)
253       
254       
255        return transformedDoc
256       
257   
258    def getAllDocs(self,transformationDir):
259        '''
260        Return a list of all the available doc types in the record
261        '''
262        # if the stored docs array is the same size as the array of all doc types
263        # assume all transforms have been done - and just return these
264       
265        self.discovery_dir = transformationDir
266       
267             
268        for docType in self._xqueryConversions:
269               
270                #self._allDocs.append([self._xqueryDocTypes[docType], self.escapeSpecialCharacters(self.getDocumentFormat(docType))])
271                self._allDocs[self._xqueryDocTypes[docType]] = self.escapeSpecialCharacters(self.getDocumentFormat(docType))
272               
273       
274        #remember, if a non ISO input format we need to add the stubISO intermediate format too.
275        #(if original format was ISO, this is covered by the self.originalXMLdoc overrider in the insertMetadata method in PostgresDAO.)
276       
277       
278        if self.docType == 'DIF_9.4':
279                logging.info("Transient ISO intermediate format detected; adding to transformed docs!")
280               
281                #we must cast this stubISO as the MEDIN format ISO so can be selected ("provide MEDIN format output from non-MEDIN input")
282                self._allDocs[self.currentMedinStandard] = self.stubISO
283               
284                #must also make sure we use the original dif and use that in here too..
285                self._allDocs[self.docType] = self.escapeSpecialCharacters(self.difXML)
286               
287        else:
288               
289                #if not DIF, put original format ISO in here
290                #TODO - put in version with redirected URLs??
291                #self._allDocs.append([self.docType,self.originalFormat])
292                self._allDocs[self.docType] = self.originalFormat
293               
294       
295        return self._allDocs
296       
297   
298
299    def listify(self, item):
300        '''
301        listify checks if an item is a list, if it isn't it puts it
302        inside a list and returns it. Always returns a list object.
303        @param item: object to check
304        @return: item as a list object
305        '''
306        if type(item) is list:
307            return item
308        else:
309            return [item]
310       
311   
312   
313    def getAuthorsInfo(self):
314        '''
315        Extract authors info from the iso object
316        '''
317       
318        logging.info("Extracting author info")
319       
320        #simple method to generate a space delimited list of authors from isoObject       
321        #13/05/10 - note this now moved to ExtractISO - and the list handling methods in Utilities.py       
322        self.authors, message = self.characterEncoding(self.isoDataModel.authors_text) # Note this is using a unique list of authors so NO duplication
323       
324        #if anything has changed need to escape those utf-8 changed chars
325        self.authors = self.escapeSpecialCharacters(self.authors)
326       
327        if message != '':
328                logging.warn("Authors entry error resolved: %s" %message)
329       
330        return self.authors
331       
332       
333   
334    def getParametersInfo(self):
335        '''
336        Extract parameters info from the ISO file (note here we mean ISO keywords, but conversion done in extractISo.py...)
337        '''
338        logging.info('Retrieving parameters info...')
339       
340        self.params = self.isoDataModel.parameters_text
341               
342        #remember the double list return style..
343        #if self.isoDataModel.parameters[0][0] == 'None':
344        #       logging.info("No parameter information present")
345        #else:
346        #       for param in self.isoDataModel.parameters[0]:
347        #               self.params = self.params + ' ' + param
348       
349        return self.params
350       
351       
352   
353    def getScopeInfo(self):
354        '''
355        Extract scope info from keywords in the input file
356        '''
357        logging.info('Retrieving scope info from moles file')
358       
359        #TODO - put this in configuration file!
360        acceptedScopeVals = ['NERC_DDC','DDDP','MEDIN','NDGO0001','NDGO0003']
361       
362        scopeVals = self.isoDataModel.keywordsList
363       
364       
365        scope = ""
366       
367        for keyword in scopeVals:
368                if keyword in acceptedScopeVals:
369                       
370                        #deal with MEDIN using NDGO0001 as MEDIN scope val
371                        if keyword == 'NDGO0001':
372                                keyword = 'MEDIN'
373                               
374                        if keyword == 'NDGO0003':                               
375                                keyword = 'NERC_DDC'
376                       
377                               
378                        scope += " " + keyword
379               
380        self.scope = re.sub(r'_', 'UNDERSCORE', scope)
381       
382        return self.scope
383     
384       
385    '''
386    Method to parse & check temporal coverage information extracted from the original ISO object
387    '''
388    def parseTemporalInfo(self,timeData):
389       
390        logging.info("Parsing Temporal information from original ISO object")
391       
392               
393        TimeRange = []
394                                       
395        if len(timeData) == 0 or timeData == 'null':
396                        logging.info("No temporal coverage elements found - assuming no temporal data available")
397                        TimeRange.append({'start':'null','end':'null'})
398                        return TimeRange
399        else:           
400                       
401                start = timeData['start']
402                end = timeData['end']
403               
404                if start is None or start == 'None':
405                        startDate = "null"
406                else:
407                        startDate = start
408                       
409                if end is None or end == 'None':
410                        endDate = "null"
411                else:
412                        endDate = end
413                       
414                if (end is None or end == 'None') and (start is None or start == 'None'):
415                        logging.info("No temporal coverage elements found - assuming no temporal data available")
416                else:
417                        TimeRange.append({'start':startDate,'end':endDate})
418                                                       
419                       
420                logging.info("Time range data found; start: " + startDate + " end: " + endDate)
421                               
422                return TimeRange
423                               
424   
425    '''
426    Method to parse & check Spatial coverage information extracted from the original ISO object
427    '''
428    def parseSpatialInfo(self,spatialData):
429       
430        logging.info("Parsing Spatial information from original ISO object")
431       
432        SpatialCoords = []
433        #if len(spatialData) == 0:
434        if spatialData is None:
435                        logging.info("No spatial coverage elements found - assuming no spatial data available")
436                        SpatialCoords.append({'west':'null','east':'null','north':'null','south':'null'})
437                        return SpatialCoords
438       
439        else:
440                cntr = 1
441                for coords in spatialData:
442                                if (coords['north'] is None) or (coords['north'] == 'None'):
443                                        north = "null"
444                                else:
445                                    north = coords['north']
446                                   
447                                if (coords['south'] is None) or (coords['south'] == 'None'):
448                                        south = "null"
449                                else:
450                                    south = coords['south']
451                                   
452                                if (coords['east'] is None) or (coords['east'] == 'None'):
453                                    east = "null"
454                                   
455                                else:
456                                    east = coords['east']
457                                   
458                                if (coords['west'] is None) or (coords['west'] == 'None'):
459                                    west = "null"
460                                   
461                                else:
462                                    west = coords['west']
463                                   
464                                #check the coordinates - NOTE now using MEDIN based coords these need to be ISO defined lon/lats in decimal degrees?
465                                #if not JUST WARN - DIF's might have UKNG coords..
466                                if (north > 90) or (north < -90) or (south > 90) or (south < -90):
467                                        logging.warn("*****************************************************************")
468                                        logging.warn("*** WARNING: latitude coordinates outside of accepted bounds! ***")
469                                        logging.warn("*****************************************************************")
470                                       
471                                if (west > 180) or (west < -180) or (east > 180) or (east < -180):
472                                        logging.warn("*****************************************************************")
473                                        logging.warn("*** WARNING: longitude coordinates outside of accepted bounds! ***")
474                                        logging.warn("*****************************************************************")
475                                       
476                                   
477                                SpatialCoords.append({'west':west,'east':east,'north':north,'south':south})
478                               
479                                logging.info("( " + str(cntr) + ") Spatial Coords found....")
480                                logging.info("................ east: " + east)
481                                logging.info("................ west: " + west)
482                                logging.info("................ north: " + north)
483                                logging.info("................ south: " + south)
484                                cntr += 1
485                               
486        return SpatialCoords
487               
488                       
489       
490       
491       
492       
Note: See TracBrowser for help on using the repository browser.