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

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

Update to fix various bugs and better handling of dodgy characters etc etc in move to production ingest

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        scope = ""
365       
366        for keyword in scopeVals:
367                if keyword in acceptedScopeVals:
368                       
369                        #deal with MEDIN using NDGO0001 as MEDIN scope val
370                        if keyword == 'NDGO0001':
371                                keyword = 'MEDIN'
372                               
373                        if keyword == 'NDGO0003':                               
374                                keyword = 'NERC_DDC'
375                       
376                               
377                        scope += " " + keyword
378               
379        self.scope = re.sub(r'_', 'UNDERSCORE', scope)
380       
381        return self.scope
382     
383       
384    '''
385    Method to parse & check temporal coverage information extracted from the original ISO object
386    '''
387    def parseTemporalInfo(self,timeData):
388       
389        logging.info("Parsing Temporal information from original ISO object")
390       
391               
392        TimeRange = []
393                                       
394        if len(timeData) == 0 or timeData == 'null':
395                        logging.info("No temporal coverage elements found - assuming no temporal data available")
396                        TimeRange.append({'start':'null','end':'null'})
397                        return TimeRange
398        else:           
399                       
400                start = timeData['start']
401                end = timeData['end']
402               
403                if start is None or start == 'None':
404                        startDate = "null"
405                else:
406                        startDate = start
407                       
408                if end is None or end == 'None':
409                        endDate = "null"
410                else:
411                        endDate = end
412                       
413                if (end is None or end == 'None') and (start is None or start == 'None'):
414                        logging.info("No temporal coverage elements found - assuming no temporal data available")
415                else:
416                        TimeRange.append({'start':startDate,'end':endDate})
417                                                       
418                       
419                logging.info("Time range data found; start: " + startDate + " end: " + endDate)
420                               
421                return TimeRange
422                               
423   
424    '''
425    Method to parse & check Spatial coverage information extracted from the original ISO object
426    '''
427    def parseSpatialInfo(self,spatialData):
428       
429        logging.info("Parsing Spatial information from original ISO object")
430       
431        SpatialCoords = []
432        #if len(spatialData) == 0:
433        if spatialData is None:
434                        logging.info("No spatial coverage elements found - assuming no spatial data available")
435                        SpatialCoords.append({'west':'null','east':'null','north':'null','south':'null'})
436                        return SpatialCoords
437       
438        else:
439                cntr = 1
440                for coords in spatialData:
441                                if (coords['north'] is None) or (coords['north'] == 'None'):
442                                        north = "null"
443                                else:
444                                    north = coords['north']
445                                   
446                                if (coords['south'] is None) or (coords['south'] == 'None'):
447                                        south = "null"
448                                else:
449                                    south = coords['south']
450                                   
451                                if (coords['east'] is None) or (coords['east'] == 'None'):
452                                    east = "null"
453                                   
454                                else:
455                                    east = coords['east']
456                                   
457                                if (coords['west'] is None) or (coords['west'] == 'None'):
458                                    west = "null"
459                                   
460                                else:
461                                    west = coords['west']
462                                   
463                                #check the coordinates - NOTE now using MEDIN based coords these need to be ISO defined lon/lats in decimal degrees?
464                                #if not JUST WARN - DIF's might have UKNG coords..
465                                if (north > 90) or (north < -90) or (south > 90) or (south < -90):
466                                        logging.warn("*****************************************************************")
467                                        logging.warn("*** WARNING: latitude coordinates outside of accepted bounds! ***")
468                                        logging.warn("*****************************************************************")
469                                       
470                                if (west > 180) or (west < -180) or (east > 180) or (east < -180):
471                                        logging.warn("*****************************************************************")
472                                        logging.warn("*** WARNING: longitude coordinates outside of accepted bounds! ***")
473                                        logging.warn("*****************************************************************")
474                                       
475                                   
476                                SpatialCoords.append({'west':west,'east':east,'north':north,'south':south})
477                               
478                                logging.info("( " + str(cntr) + ") Spatial Coords found....")
479                                logging.info("................ east: " + east)
480                                logging.info("................ west: " + west)
481                                logging.info("................ north: " + north)
482                                logging.info("................ south: " + south)
483                                cntr += 1
484                               
485        return SpatialCoords
486               
487                       
488       
489       
490       
491       
Note: See TracBrowser for help on using the repository browser.