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

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

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

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