source: exist/trunk/python/ndgUtils/models/Atom.py @ 4440

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/exist/trunk/python/ndgUtils/models/Atom.py@4440
Revision 4440, 45.0 KB checked in by cbyrom, 12 years ago (diff)

Create new model, Deployment, to hold deployments info - for easy use
with templates. Delete the moles deployments code and implement code
with the new model + add containers in Atom - to allow deployments data
specified in links to be looked up.

Line 
1'''
2 Class representing data in  atom format - allowing moles data to be stored and accessed in a web feed compatible way
3 
4 @author: C Byrom, Tessella Jun 2008
5'''
6try: #python 2.5
7    from xml.etree import cElementTree as ET
8except ImportError:
9    try:
10        # if you've installed it yourself it comes this way
11        import cElementTree as ET
12    except ImportError:
13        # if you've egged it this is the way it comes
14        from ndgUtils.elementtree import cElementTree as ET
15import sys, logging, re, datetime
16from ndgUtils.eXistConnector import eXistConnector
17from ndgUtils.ETxmlView import et2text
18from utilities import getTripleData, escapeSpecialCharacters, \
19    tidyUpParameters, getISO8601Date
20from ndgUtils.vocabtermdata import VocabTermData as VTD
21from ndgUtils.models import MolesEntity as ME
22import csml.parser as CsmlParser
23from ndgUtils.models import Deployment as Deployment
24
25
26class AtomError(Exception):
27    """
28    Exception handling for Atom class.
29    """
30    def __init__(self, msg):
31        logging.error(msg)
32        Exception.__init__(self, msg)
33
34
35class ValidationError(Exception):
36    """
37    Exception handling for validation.
38    """
39    def __init__(self, errorDict):
40        msg = "Data validation error"
41        logging.error(msg)
42        Exception.__init__(self, msg)
43        for val in errorDict.itervalues():
44            logging.error(val)
45        self._errorDict = errorDict
46           
47    def unpack_errors(self):
48        return self._errorDict
49
50
51class Person(object):
52    '''
53    Class representing atom author type data - with name, uri and role attributes
54    '''
55    AUTHOR_TYPE = 0
56    CONTRIBUTOR_TYPE = 1
57    RESPONSIBLE_PARTY_TYPE = 2
58    ELEMENT_NAMES = ["author", "contributor", "responsibleParty"]
59   
60    def __init__(self, personType = AUTHOR_TYPE, namespace = None):
61        self.type = personType
62        self.ns = namespace
63        self.name = ""
64        self.uri = ""
65        self.role = ""
66       
67        # NB, the atom format specifies slightly different data contents
68        self.uriTagName = "email"
69        # NB, responsible party data is always stored in the moles section
70        if self.type == self.RESPONSIBLE_PARTY_TYPE:
71            self.ns = 'moles'
72            self.uriTagName = "uri"
73
74    def __str__(self):
75        if self.name or self.uri or self.role:
76            return self.name + " | " + self.uri + " | " + self.role
77        return ""
78   
79    def fromString(self, personString):
80        (self.name, self.uri, self.role) = getTripleData(personString)
81       
82    def fromETElement(self, personTag):
83        self.name = personTag.findtext('name') or ""
84        self.role = personTag.findtext('role') or ""
85        self.uri = personTag.findtext(self.uriTagName) or ""
86        logging.debug("Added name: '%s', role: '%s', %s: '%s'" \
87                      %(self.name, self.role, self.uriTagName, self.uri))
88
89    def toXML(self):
90        prefix = ""
91        if self.ns:
92            prefix = self.ns + ':'
93
94        author = ET.Element(prefix + self.ELEMENT_NAMES[self.type])
95
96        if self.name:
97            name = ET.SubElement(author, prefix + "name")
98            name.text = self.name
99       
100        if self.uri:
101            uri = ET.SubElement(author, prefix + self.uriTagName)
102            uri.text = self.uri
103       
104        if self.role:
105            role = ET.SubElement(author, prefix + "role")
106            role.text = self.role
107
108        return author
109   
110    def __cmp__(self, person1):
111        '''
112        Override comparison to allow proper object comparison when checking
113        if Person objects are in an array already - i.e. if person in personArray...
114        '''
115        if not person1:
116            return -1
117       
118        if self is person1:
119            return 0
120        elif self.uri == person1.uri and self.name == person1.name and \
121                self.role == person1.role and self.type == person1.type:
122            return 0
123        return 1
124
125
126class Link(object):
127    '''
128    Class representing an atom link - with href, title and rel attributes
129    '''
130
131    def __init__(self):
132        self.href = ""
133        self.title = ""
134        self.rel = ""
135
136    def fromString(self, linkString):
137        (self.href, self.title, self.rel) = getTripleData(linkString)
138       
139    def fromETElement(self, linkTag):
140        self.href = linkTag.attrib.get('href') or ""
141        self.rel = linkTag.attrib.get('rel') or ""
142        self.title = linkTag.attrib.get('title') or ""
143
144    def toXML(self):
145        link = ET.Element("link")
146        link.attrib["href"] = self.href
147        link.attrib["title"] = self.title
148        link.attrib["rel"] = self.rel
149        return link
150
151    def hasValue(self):
152        # NB, just a rel on its own is meaningless - so ignore
153        if self.href or self.title:
154            return True
155        return False
156   
157    def __str__(self):
158        if self.href or self.title or self.rel:
159            return self.href + " | " + self.title + " | " + self.rel
160        return ""
161   
162    def isChildAtom(self):
163        '''
164        Determines whether the link refers to another atom - e.g. a link to
165        a data granule
166        @return True, if so; False otherwise
167        '''
168        if self.rel.endswith(VTD.GRANULE_TERM) or \
169            self.rel.endswith(VTD.DEPLOYMENT_TERM) or \
170            self.rel.endswith(VTD.ACTIVITY_TERM) or \
171            self.rel.endswith(VTD.DPT_TERM) or \
172            self.rel.endswith(VTD.OBS_TERM):
173            return True
174       
175        return False
176   
177    def __cmp__(self, link1):
178        '''
179        Override comparison to allow proper object comparison when checking
180        if Link objects are in an array already - i.e. if link in linkArray...
181        '''
182        if not link1:
183            return -1
184       
185        if self is link1:
186            return 0
187        elif self.href == link1.href and self.title == link1.title and \
188                self.rel == link1.rel:
189            return 0
190        return 1
191
192
193class Category(object):
194    '''
195    Class representing an atom category - with term, scheme and label attributes
196    '''
197    def __init__(self):
198        self.term = ""
199        self.scheme = ""
200        self.label = ""
201
202    def fromString(self, linkString):
203        (self.label, self.scheme, self.term) = getTripleData(linkString)
204       
205    def fromETElement(self, linkTag):
206        self.term = linkTag.attrib.get('term') or ""
207        self.label = linkTag.attrib.get('label') or ""
208        self.scheme = linkTag.attrib.get('scheme') or ""
209
210    def toXML(self):
211        link = ET.Element("category")
212        link.attrib["term"] = self.term
213        link.attrib["scheme"] = self.scheme
214        link.attrib["label"] = self.label
215        return link
216   
217    def hasValue(self):
218        if self.scheme or self.label or self.term:
219            return True
220        return False
221
222
223class Atom(object):
224
225    # labels for use with the atom categories
226    ATOM_TYPE = "ATOM_TYPE"
227    ATOM_SUBTYPE = "ATOM_SUBTYPE"
228
229    # labels for use with the templates to set/extract specific inputs
230    ONLINE_REF_LABEL = "online_ref"
231    PARAMETER_LABEL = "parameter"
232    ATOM_REF_LABEL = "atom_ref"
233    DELIMITER = "---"
234    REMOVE_LABEL = "remove"
235   
236    # format to use for t1-t2 date range
237    YEAR_FORMAT = '%Y-%m-%d'
238
239    def __init__(self, atomType = None, vocabTermData = None, ndgObject = None, \
240                 xmlString = None, state = eXistConnector.WORKING_COLLECTION_PATH, **inputs):
241        '''
242        Constructor - initialise the atom variables
243        '''
244        logging.info("Initialising atom")
245        if atomType:
246            logging.info(" - of type '%s'" %atomType)
247        self.atomTypeID = atomType
248
249        # some data have further subtypes specified
250        self.subtypeID = None # this should be the termID
251        self.subtype = None # and this should be the fully formed vocab URL
252       
253        self.ndgObject = ndgObject
254
255        self.atomName = None
256        self.files = []
257        self.author = None
258        self.contributors = []
259        self.atomAuthors = []
260        self.parameters = []
261        self.spatialData = []
262        self.temporalData = []
263        self.relatedLinks = []
264        self.summary = []
265        self.content = []
266        # NB, this deployments data duplicates other atom data - and is only used for a
267        # convenient way to collect the info (by lookupDeploymentsInfo()) for use in templates
268        self.deployments = []   
269        self.csmlFile = None
270        self.cdmlFile = None
271        # general variable to use for setting the atom content - NB, if a csmlFile is specified
272        # (either directly or via a cdmlFile specification), this will be the content by default
273        # for this purpose
274        self.contentFile = None     
275        self.title = None
276        self.datasetID = None        # NB, the dataset id ends up in the atomName - <path><datasetID>.atom
277        self.atomID = None
278   
279        # boundary box info - to replace spatial/temporalData?
280        self.minX = None
281        self.minY = None
282        self.maxX = None
283        self.maxY = None
284        self.t1 = None
285        self.t2 = None
286
287        self.ME = ME.MolesEntity()
288       
289        # date when the atom was first ingested
290        self.publishedDate = None
291
292        # last update date
293        self.updatedDate = None
294
295        # assume atom in working state by default - this is used to define what collection
296        # in eXist the atom is stored in
297        self.state = state
298       
299        # additional, non standard atom data can be included in the molesExtra element
300        if vocabTermData:
301            self.VTD = vocabTermData
302        else:
303            self.VTD = VTD()
304       
305        if xmlString:
306            self.fromString(xmlString)
307
308        # if inputs passed in as dict, add these now
309        if inputs:
310            logging.info("Adding info to atom from input dict")
311            logging.debug(inputs)
312            self.__dict__.update(inputs)
313           
314            # NB, this doesn't trigger the Summary Property, so do this
315            # explicitly, if need be
316            if inputs.has_key('Summary'):
317                self.Summary = inputs.get('Summary')
318            if inputs.has_key('Content'):
319                self.Content = inputs.get('Content')
320           
321            # also pass any moles data up to the moles entity object
322            if inputs.get('providerID'):
323                self.ME.providerID = inputs.get('providerID')
324               
325            if inputs.get('abbreviation'):
326                self.ME.abbreviation = inputs.get('abbreviation')
327
328        if self.atomTypeID:
329            self.atomTypeName = self.VTD.TERM_DATA[self.atomTypeID].title
330
331        logging.info("Atom initialised")
332
333
334    def addOnlineReferences(self, links):
335        '''
336        Add online reference data associated with the atom
337        - NB, care needs to be taken here since this data is stored in the atom
338        link elements and these are also used for the various atom associations
339        @param links: a Link or array of Links to add to the relatedLinks attribute
340        '''
341        logging.debug("Adding online references")
342        if not links:
343            return
344       
345        if type(links) is not list:
346            links = [links]
347       
348        # firstly clear out any online refs data from the existing related links
349        newLinks = []
350        for link in self.relatedLinks:
351            if link.isChildAtom():
352                newLinks.append(link)
353       
354        newLinks.extend(links)
355        self.relatedLinks = newLinks
356        logging.debug("Online references added")
357
358
359
360    def addUniqueRelatedLinks(self, links):
361        '''
362        Add links to relatedLinks array - if they are not already included
363        @param links: a Link or array of Links to add to the relatedLinks attribute
364        '''
365        logging.debug("Adding new related links")
366        if not links:
367            return
368       
369        if type(links) is not list:
370            links = [links]
371       
372        for link in links:
373            if type(link) is not Link:
374                logging.warning("Link is not of 'Link' object type (type='%s') - skipping" %type(link))
375                continue
376            if link not in self.relatedLinks:
377                logging.debug("- adding unique link")
378                self.relatedLinks.append(link)
379        logging.debug("Finished adding links")
380       
381
382    def removeRelatedLinks(self, linksToDelete):
383        '''
384        Remove any links in the input list from the atom's related links list
385        @param linksToDelete: array of Link objects to remove from atom
386        '''
387        logging.debug("Removing related links from atom")
388        if not linksToDelete:
389            return
390       
391        if type(linksToDelete) is not list:
392            linksToDelete = [linksToDelete]
393       
394        updatedLinks = []
395        for link in self.relatedLinks:
396            if type(link) is not Link:
397                logging.warning("Link is not of 'Link' object type (type='%s') - skipping" %type(link))
398                continue
399            if link in linksToDelete:
400                logging.debug("- found link to remove")
401            else:
402                updatedLinks.append(link)
403
404        self.relatedLinks = updatedLinks
405        logging.debug("Links removed")
406       
407
408    def getDefaultCollectionPath(self):
409        '''
410        Determine the correct collection to use for the atom in eXist
411        '''
412        collectionPath = eXistConnector.BASE_COLLECTION_PATH + self.state
413       
414        if self.atomTypeID == VTD.DE_TERM:
415            collectionPath += eXistConnector.DE_COLLECTION_PATH
416        elif self.atomTypeID == VTD.GRANULE_TERM:
417            collectionPath += eXistConnector.GRANULE_COLLECTION_PATH
418        elif self.atomTypeID == VTD.ACTIVITY_TERM and \
419            self.subtypeID == VTD.DEPLOYMENT_TERM:
420            collectionPath += eXistConnector.DEPLOYMENTS_COLLECTION_PATH
421        else:
422            collectionPath += eXistConnector.DEPLOYMENT_COLLECTION_PATH
423       
424        if not self.ME.providerID:
425            raise AtomError("Error: cannot determine atom collection path because " + \
426                            "the provider ID is not defined")
427           
428        collectionPath += self.ME.providerID + "/"
429        return collectionPath
430
431
432    def __addAtomTypeDataXML(self, root):
433        '''
434        Add the atom type, and subtype data, if available, to atom categories
435        - and lookup and add the appropriate vocab term data
436        '''
437        if self.atomTypeID:
438            logging.info("Adding atom type info to XML output")
439            category = Category()
440            category.label = self.atomTypeID
441            # look up the appropriate vocab term data
442            category.scheme = self.VTD.getTermCurrentVocabURL(self.atomTypeID)
443            category.term = self.ATOM_TYPE
444            root.append(category.toXML())
445
446        if self.subtypeID:
447            logging.info("Adding atom subtype info to XML output")
448            # NB subtypes not all defined, so leave this out for the moment
449            category.label = self.subtypeID
450            # look up the appropriate vocab term data
451            category.scheme = self.VTD.getTermCurrentVocabURL(self.subtypeID)
452            category.term = self.ATOM_SUBTYPE
453            root.append(category.toXML())
454
455
456    def addMolesEntityData(self, abbreviation, provider_id, object_creation_time):
457        '''
458        Add data to include in the moles entity element
459        '''
460        logging.debug('Adding moles entity information')
461        self.ME.abbreviation = abbreviation
462        self.ME.providerID = provider_id
463        self.ME.createdDate = getISO8601Date(object_creation_time)
464        logging.debug('Moles entity information added')
465
466
467    def addAuthors(self, authors):
468        '''
469        Add author data appropriately to the atom
470        NB, these will overwrite any existing authors of the same type
471        @param authors: list of Person objects with the author data
472        '''
473        logging.debug('Adding authors data to Atom')
474        isFirstAuthor = {}
475        authorArray = None
476        for author in authors:
477            # NB, we're only allowed one atom author
478            if author.type == Person.AUTHOR_TYPE:
479                self.author = author
480                if isFirstAuthor.has_key(author.type):
481                    raise AtomError("Error: an atom can only have one author specified")
482                isFirstAuthor[author.type] = 1
483                continue
484            elif author.type == Person.CONTRIBUTOR_TYPE:
485                authorArray = self.contributors
486            elif author.type == Person.RESPONSIBLE_PARTY_TYPE:
487                authorArray = self.ME.responsibleParties
488               
489            # check if this is the first addition - if so, clear out the
490            # array in advance
491            if not isFirstAuthor.has_key(author.type):
492                logging.debug("Clearing out author array")
493                # NB, need to be careful to clear the array, not create a ref
494                # to a new array
495                del authorArray[:]
496                isFirstAuthor[author.type] = 1
497
498            if str(author) != "" and author not in authorArray:
499                logging.debug("Adding author (type:'%s', name:'%s', uri:'%s', role:'%s')" \
500                              %(author.type, author.name, author.uri, author.role))
501                authorArray.append(author)
502
503        logging.debug('Finished adding authors data')
504
505
506    def _isNewParameter(self, param):
507        '''
508        Check if a parameter is already specified in the atom, return False if
509        so, otherwise return True
510        '''
511        for p in self.parameters:
512            if p.term == param.term and \
513                p.scheme == param.scheme and \
514                p.label == param.label:
515                return False
516        return True
517
518
519    def addRelatedLinks(self, linkVals):
520        '''
521        Add related links in string format - converting to Link objects
522        @param linkVals: string of format, 'uri | title | vocabServerURL'
523        '''
524        self.relatedLinks.append(self.objectify(linkVals, 'relatedLinks'))
525
526
527    def addParameters(self, params):
528        '''
529        Add a parameter to list - ensuring it is unique and has been formatted and tidied appropriately
530        @params param: parameter, as string array, to add to atom parameters collection
531        '''
532        # avoid strings being parsed character by character
533        if type(params) is str:
534            params = [params]
535           
536        for param in params:
537            # firstly tidy parameter
538            param = tidyUpParameters(param)
539            category = Category()
540            category.fromString(param)
541
542            # now check for uniqueness
543            if self._isNewParameter(category):
544                logging.debug("Adding new parameter: %s" %param)
545                self.parameters.append(category)
546   
547   
548    def _linksToXML(self, root):
549        '''
550        Add required links to the input element
551        @param root: element to add links to - NB, should be the root element of the atom
552        '''
553        selfLink = ET.SubElement(root, "link")
554        selfLink.attrib["href"] = self.atomBrowseURL
555        selfLink.attrib["rel"] = "self"
556        if self.subtypeID != VTD.DEPLOYMENT_TERM:
557            molesLink = ET.SubElement(root, "link")
558            molesDoc = re.sub('ATOM','NDG-B1', self.atomBrowseURL)
559            molesLink.attrib["href"] = molesDoc
560            molesLink.attrib["rel"] = 'related'
561       
562        for relatedLink in self.relatedLinks:
563            if relatedLink.hasValue():
564                root.append(relatedLink.toXML())
565   
566    def toXML(self):
567        '''
568        Convert the atom into XML representation and return this
569        @return: xml version of atom
570        '''
571        logging.info("Creating formatted XML version of Atom")
572        root = ET.Element("entry")
573        root.attrib["xmlns"] = "http://www.w3.org/2005/Atom"
574        root.attrib["xmlns:moles"] = "http://ndg.nerc.ac.uk/schema/moles2beta"
575        root.attrib["xmlns:georss"] = "http://www.georss.org/georss/10"
576        root.attrib["xmlns:gml"] = "http://www.opengis.net/gml"
577        id = ET.SubElement(root, "id")
578        id.text = self.atomID
579        title = ET.SubElement(root, "title")
580        title.text = self.title
581        self._linksToXML(root)
582
583        # NB, the author tag is mandatory for atoms - so if an explicit
584        # author has not been set, just take the author to be the provider
585        if not self.author:
586            author = Person()
587            author.name = self.ME.providerID
588            author.uri = self.ME.providerID
589            self.author = author
590
591        root.append(self.author.toXML())
592           
593        for contributor in self.contributors:
594            root.append(contributor.toXML())
595
596        # add the moles entity section, if it is required
597        if self.ME:
598            root.append(self.ME.toXML())
599
600        # add parameters data
601        for param in self.parameters:
602            if param.hasValue():
603                root.append(param.toXML())
604
605        # add the type and subtype data
606        self.__addAtomTypeDataXML(root)
607                   
608        summary = ET.SubElement(root, "summary")
609        summary.text = self.Summary
610                   
611        # add link to content, if required - NB, can only have one content element in atom
612        # - and this is mandatory
613        content = ET.SubElement(root, "content")
614        if self.contentFile:
615            content.attrib["type"] = "application/xml"
616            content.attrib["src"] = self.contentFile
617        else:
618            content.text = self.Content
619            content.attrib["type"] = "xhtml"
620       
621        # if there's a published date already defined, assume we're doing an update now
622        # NB, update element is mandatory
623        currentDate = datetime.datetime.today().strftime("%Y-%m-%dT%H:%M:%SZ")
624        if not self.publishedDate:
625            self.publishedDate = currentDate
626
627        updated = ET.SubElement(root, "updated")
628        if not self.updatedDate:
629            self.updatedDate = currentDate
630        updated.text = self.updatedDate
631
632        published = ET.SubElement(root, "published")
633        published.text = self.publishedDate
634
635        # add temporal range data, if available
636        temporalRange = ET.SubElement(root, "moles:temporalRange")
637        if self.t1:
638            temporalRange.text = self.t1
639            if self.t2:
640                temporalRange.text += "/" + self.t2
641
642        # add spatial range data, if available
643        self._addSpatialData(root)
644
645        tree = ET.ElementTree(root)
646        logging.info("XML version of Atom created")
647        return tree
648
649
650    def __getSummary(self):
651        logging.debug("Getting summary data")
652        summaryString = ""
653        for summary_line in self.summary:
654            summaryString += summary_line + "\n"
655
656        return summaryString
657
658    def __setSummary(self, summary):
659        logging.debug("Adding summary data")
660        self.summary = []
661        for summary_line in summary.split('\n'):
662            self.summary.append(escapeSpecialCharacters(summary_line))
663           
664    Summary = property(fset=__setSummary, fget=__getSummary, doc="Atom summary")
665
666
667    def __getContent(self):
668        logging.debug("Getting content data")
669        contentString = ""
670        # NB, there must be content specified in an atom
671        if not self.content:
672            return "Metadata document"
673       
674        for content_line in self.content:
675            contentString += content_line + "\n"
676
677        return contentString
678
679    def __setContent(self, content):
680        logging.debug("Adding content data")
681        self.content = []
682        for content_line in content.split('\n'):
683            self.content.append(escapeSpecialCharacters(content_line))
684           
685    Content = property(fset=__setContent, fget=__getContent, doc="Atom content")
686
687           
688    def fromString(self, xmlString):
689        '''
690        Initialise Atom object using an xmlString
691        @param xmlString: representation of atom as an XML string
692        '''
693        logging.info("Ingesting data from XML string")
694       
695        # firstly, remove any namespaces used - to avoid problems with elementtree
696        logging.debug("Stripping moles namespace from string to allow easy handling with elementtree")
697        xmlString = xmlString.replace('moles:', '')
698        xmlString = xmlString.replace('georss:', '')
699        xmlString = xmlString.replace('gml:', '')
700        xmlString = xmlString.replace('xmlns="http://www.w3.org/2005/Atom"', '')
701
702        # now create elementtree with the XML string
703        logging.debug("Create elementtree instance with XML string")
704        tree = ET.fromstring(xmlString)
705       
706        title = tree.findtext('title')
707        if title:
708            logging.debug("Adding title data")
709            self.title = title
710
711        summary = tree.findtext('summary')
712        if summary:
713            self.Summary = summary
714
715        authorElement = tree.find('author')
716        logging.debug("Adding author data")
717        author = Person()
718        author.fromETElement(authorElement)
719        self.author = author
720
721        contributorElements = tree.findall('contributor')
722        for contributorElement in contributorElements:
723            logging.debug("Adding contributor data")
724            contributor = Person(personType = Person.CONTRIBUTOR_TYPE)
725            contributor.fromETElement(contributorElement)
726            self.contributors.append(contributor)
727
728        molesElement = tree.find('entity')
729        if molesElement:
730            self.ME.fromET(molesElement)
731               
732        self.atomID = tree.findtext('id')
733
734        self._parseCategoryData(tree.findall('category'))
735
736        self._parseLinksData(tree.findall('link'))
737           
738        contentTag = tree.find('content')
739        if contentTag != None:
740            logging.debug("Found content tag - checking for CSML/CDML file data")
741            file = contentTag.attrib.get('src')
742            if file:
743                # NB, the path will reveal more reliably whether we're dealing with CSML and CDML files
744                if file.upper().find('CSML') > -1:
745                    logging.debug("Adding CSML file data")
746                    self.csmlFile = file
747                elif file.upper().find('CDML') > -1:
748                    logging.debug("Adding CDML file data")
749                    self.cdmlFile = file
750                self.contentFile = file
751            else:
752                logging.debug("No file data - adding contents of element instead")
753                self.Content = contentTag.text
754       
755        range = tree.findtext('temporalRange')
756        if range:
757            logging.debug("Adding temporal range data")
758            timeData = range.split('/')
759            self.t1 = timeData[0]
760            if len(timeData) > 1:
761                self.t2 = timeData[1]
762       
763        # NB, this parser won't mind if we're dealing with Envelope or EnvelopeWithTimePeriod
764        minBBox = tree.findall('.//lowerCorner')
765        if minBBox:
766            logging.debug("Adding min spatial range data")
767            minBBox = minBBox[0]
768            spatialData = minBBox.text.split()
769            self.minX = spatialData[0]
770            if len(spatialData) > 1:
771                self.minY = spatialData[1]
772       
773        maxBBox = tree.findall('.//upperCorner')
774        if maxBBox:
775            maxBBox = maxBBox[0]
776            logging.debug("Adding max spatial range data")
777            spatialData = maxBBox.text.split()
778            self.maxX = spatialData[0]
779            if len(spatialData) > 1:
780                self.maxY = spatialData[1]
781               
782        publishedDate = tree.findtext('published')
783        if publishedDate:
784            logging.debug("Adding published date")
785            self.publishedDate = publishedDate
786               
787        updatedDate = tree.findtext('updated')
788        if updatedDate:
789            logging.debug("Adding updated date")
790            self.updatedDate = updatedDate
791           
792        logging.info("Completed data ingest")
793   
794   
795    def _parseCategoryData(self, categories):
796        logging.debug("Adding category/parameters data")
797        for category in categories:
798            cat = Category()
799            cat.fromETElement(category)
800           
801            if cat.term == self.ATOM_TYPE:
802                logging.debug("Found atom type data")
803                self.atomTypeID = cat.label
804                self.atomTypeName = self.VTD.TERM_DATA[cat.label].title
805                continue
806            elif cat.term == self.ATOM_SUBTYPE:
807                logging.debug("Found atom subtype data")
808                self.subtypeID = cat.label
809                self.subtype = cat.scheme
810                continue
811
812            self.parameters.append(cat)
813   
814
815    def setDatasetID(self, datasetID):
816        '''
817        Set the dataset ID for the atom - and generate an appropriate atom name using this
818        @param datasetID: ID to set for the atom
819        '''
820        self.datasetID = datasetID
821        self._generateAtomName(datasetID) 
822        self.atomID = self.createAtomID(datasetID)
823
824
825    def createAtomID(self, datasetID):
826        '''
827        Create a unique ID, conforming to atom standards, for atom
828        NB, see http://diveintomark.org/archives/2004/05/28/howto-atom-id
829        @param datasetID: ID of atom's dataset
830        @return: unique ID
831        '''
832        logging.info("Creating unique ID for atom")
833        if not self.atomBrowseURL:
834            self._generateAtomName(datasetID)
835        urlBit = self.atomBrowseURL.split('://')[1]
836        urlBit = urlBit.replace('#', '')
837        urlBits = urlBit.split('/')
838        dateBit = datetime.datetime.today().strftime("%Y-%m-%d")
839       
840        id = "tag:" + urlBits[0] + "," + dateBit + ":/" + "/".join(urlBits[1:])
841        logging.info("- unique ID created for atom")
842        logging.debug(" - '%s'" %id)
843        return id
844       
845       
846    def _generateAtomName(self, datasetID):
847        '''
848        Generate a consistent name for the atom - with full eXist doc path
849        @param datasetID: ID of atom's dataset
850        '''
851        self.atomName = datasetID + ".atom"
852        self.ndgURI = self.ME.providerID + "__ATOM__" + datasetID
853        self.atomBrowseURL = VTD.BROWSE_ROOT_URL + self.ndgURI
854
855
856    def _parseLinksData(self, links):
857        '''
858        Extract links and atom data from array of link elements in the XML representation of the atom
859        @param links: an array of <link> elements
860        '''
861        # firstly, get all data to start with, so we can properly process it afterwards
862        linkData = {}
863        logging.debug("Getting link data")
864        for linkTag in links:
865            link = Link()
866            link.fromETElement(linkTag)
867
868            if not linkData.has_key(link.rel):
869                linkData[link.rel] = []
870           
871            linkData[link.rel].append(link)
872
873        # there should be one self referencing link - which will provide info on the atom itself
874        if not linkData.has_key('self'):
875            errorMessage = "Atom does not have self referencing link - " + \
876                "cannot ascertain datasetID without this - please fix"
877            logging.error(errorMessage)
878            raise ValueError(errorMessage)
879       
880        # this is the link describing the atom itself
881        self.atomBrowseURL = linkData['self'][0].href
882       
883        self.datasetID = self.atomBrowseURL.split("__ATOM__")[-1]
884        self.atomName = self.datasetID + ".atom"
885        self.ndgURI = self.atomBrowseURL.split(VTD.BROWSE_ROOT_URL)[1]
886       
887        # now remove this value and the associated moles doc link
888        del linkData['self']
889        molesDoc = self.atomBrowseURL.replace('ATOM', 'NDG-B1')
890        if linkData.has_key('related'):
891            relatedLinks = []
892            for link in linkData['related']:
893                if link.href != molesDoc:
894                    relatedLinks.append(link)
895           
896            linkData['related'] = relatedLinks
897               
898        # now add the remaining links to the atom
899        for key in linkData:
900            for link in linkData[key]:
901                logging.debug("Adding link data")
902                self.relatedLinks.append(link)
903       
904
905    def _addSpatialData(self, element):
906        '''
907        Add spatial coverage element to an input element
908        @param element: element to add coverage data to
909        '''
910        logging.info("Adding spatial data to Atom")
911        bbox = ET.SubElement(element, "georss:where")
912        if not self.minX:
913            logging.info("No spatial data specified")
914            return
915       
916        envelope = ET.SubElement(bbox, "gml:Envelope")
917        lc = ET.SubElement(envelope, "gml:lowerCorner")
918        lc.text = self.minX + " " + self.minY
919        uc = ET.SubElement(envelope, "gml:upperCorner")
920        uc.text = self.maxX + " " + self.maxY
921
922       
923    def setAttribute(self, attributeName, attributeValue):
924        '''
925        Set the value of an atom attribute - and do some basic tidying up of the string content
926        - to escape any XML unfriendly characters
927        @param attributeName: name of the attribute whose value to set
928        @param attributeValue: value to set the attribute to 
929        '''
930        logging.debug("Setting attribute, %s, to %s" %(attributeName, attributeValue))
931        origValue = attributeValue
932       
933        # escape any special characters if a value has been specified
934        # NB, need to cope with both single values and arrays
935        if attributeValue:
936            if type(attributeValue) is list:
937                newVals = []
938                for val in attributeValue:
939                    newVals.append(self.objectify(escapeSpecialCharacters(val), attributeName))
940                attributeValue = newVals
941                   
942            else:
943                attributeValue = self.objectify(escapeSpecialCharacters(attributeValue), attributeName)
944
945        # handle the special case of authors; only one author is allowed per atom
946        # - the others should be treated as contributors
947        if attributeName == "authors":
948            setattr(self, "author", attributeValue[0])
949            if len(attributeValue) > 1:
950                setattr(self, "contributors", attributeValue[1:])
951        elif attributeName == "atomAuthors":
952            self.ME.responsibleParties.append(attributeValue)
953        else:
954            setattr(self, attributeName, attributeValue)
955
956
957    def objectify(self, objectVals, attributeName):
958        '''
959        Some inputs are specified as strings but need to be converted into
960        objects - do this here
961        @param objectVals: a '|' delimited string of values
962        @param attributeName: name of attribute the values belong to
963        '''
964        obj = None
965        if type(objectVals) != str:
966            return objectVals
967       
968        if attributeName == "relatedLinks":
969            obj = Link()
970        elif attributeName == "atomAuthors" or attributeName == "authors":
971            obj = Person()
972
973        if obj:
974            obj.fromString(objectVals)
975            return obj
976       
977        return objectVals
978
979
980    def toPrettyXML(self):
981        '''
982        Returns nicely formatted XML as string
983        '''
984        atomXML = self.toXML()
985
986        # create the string
987        logging.debug("Converting the elementtree object into a string")
988        prettyXML = et2text(atomXML.getroot())
989
990        # add XML version tag
991        prettyXML = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + prettyXML
992        logging.info("Created formatted version of XML object")
993        return prettyXML
994
995
996    def getLinksOfType(self, termID):
997        '''
998        Returns links in the atom related links attribute which match the specified
999        term ID
1000        @param termID: the termID to look for in the related links - NB, this is
1001        matched to the end of the link.rel value
1002        @return links: array of Link objects with matching term type
1003        '''
1004        logging.debug("Getting atom links of type, '%s'" %termID)
1005        matchingLinks = []
1006        for link in self.relatedLinks:
1007            # firstly, handle special case where we only want the online ref type links
1008            # returned
1009            if termID == self.ONLINE_REF_LABEL:
1010                if not link.isChildAtom():
1011                    logging.debug("- found link with matching term type")
1012                    matchingLinks.append(link)
1013               
1014            elif link and link.rel and link.rel.lower().endswith(termID.lower()):
1015                logging.debug("- found link with matching term type")
1016                matchingLinks.append(link)
1017               
1018        logging.debug("Returning matched links")
1019        return matchingLinks
1020   
1021   
1022    def validate(self):
1023        '''
1024        Check the various values of the various atom attributes; if an error with any of
1025        these is found, raise a ValueError
1026        @raise ValueError: if any atom attributes have a problem
1027        '''
1028        logging.info("Validating the atom data model")
1029        errors = {}
1030        if not self.title:
1031            errors['title'] = "Title attribute cannot be empty"
1032           
1033        if self.minX or self.maxX or self.minY or self.maxY:
1034            missingVals = False
1035            incorrectFormat = False 
1036            for val in [self.minX, self.maxX, self.minY, self.maxY]:
1037                if val == '':
1038                    missingVals = True
1039                else:
1040                    try:
1041                        float(val)
1042                    except:
1043                        incorrectFormat = True
1044           
1045            if missingVals or incorrectFormat:
1046                errors['spatialcoverage'] = ""
1047            if missingVals:
1048                errors['spatialcoverage'] += "Incomplete spatial coverage data.\n"
1049            if incorrectFormat:
1050                errors['spatialcoverage'] += "Spatial coverage data not in numerical format."
1051
1052        if self.t1 or self.t2:
1053            timeErrors = ''
1054            d1 = None
1055            d2 = None
1056            if self.t1:
1057                try:
1058                    d1 = datetime.datetime.strptime(self.t1, self.YEAR_FORMAT)
1059                except:
1060                    timeErrors += "Incorrect start date format - '%s' - c.f. '2008-04-12. \n'" %self.t1
1061            if self.t2:
1062                try:
1063                    d2 = datetime.datetime.strptime(self.t2, self.YEAR_FORMAT)
1064                except:
1065                    timeErrors += "Incorrect end date format - '%s' - c.f. '2008-04-12. \n'" %self.t2
1066
1067            if d1 and d2:
1068                if d1 > d2 or d2 < d1:
1069                    timeErrors += "Inconsistent date range - '%s' is not before '%s'" \
1070                        %(d1.strftime(self.YEAR_FORMAT), d2.strftime(self.YEAR_FORMAT))
1071
1072            if timeErrors:
1073                errors['temporalrange'] = timeErrors
1074
1075           
1076        # do a quick recursion over all the attributes to look for ascii characters
1077        for key, val in self.__dict__.items():
1078            if val:
1079                if type(val) == str:
1080                    try:
1081                        # NB, the latin coding accepts unicode up to 255
1082                        correctedString = val.decode('latin-1')
1083                    except:
1084                        if not errors.has_key(key):
1085                            errors[key] = ''
1086                        errors[key] += "Illegal unicode found in string: '%s'.\n" %val
1087               
1088        if errors:
1089            logging.warning("Errors found in atom data: %s" %errors)
1090            raise ValidationError(errors)
1091        logging.info("Atom model validated successfully")
1092       
1093       
1094    def getLogos(self):
1095        '''
1096        Return related links that are logos
1097        @return: array of Links containing the logos for the atom
1098        '''
1099        logos = []
1100        for link in self.relatedLinks:
1101            if link.rel.lower().endswith(VTD.LOGO_TERM.lower()):
1102                logos.append(link)
1103               
1104        return logos
1105   
1106   
1107    def isGranule(self):
1108        if self.atomTypeID == VTD.GRANULE_TERM:
1109            return True
1110        return False
1111   
1112   
1113    def isDE(self):
1114        if self.atomTypeID == VTD.DE_TERM:
1115            return True
1116        return False
1117   
1118    def isDeployment(self):
1119        if self.subtypeID and self.subtypeID == VTD.DEPLOYMENT_TERM:
1120            return True
1121        return False
1122
1123       
1124    def addCSMLData(csml, aggregateCoverage=False):
1125        '''
1126        Parse CSML data and add extracted info to the atom
1127        @param csml: csml file contents - or path to csml file
1128        @keyword aggregateCoverage: if set to True, only coverage data that extends the
1129        atom coverage data will be added
1130        '''
1131        logging.info("Creating CSML data model")
1132        csmlDoc = CsmlParser.Dataset(file=self._csmlFile)
1133       
1134        logging.info("Extracting info from CSML file")
1135        logging.debug("Got dataset ID: %s" %csmlDoc.id)
1136        self.setDatasetID(csmlDoc.id)
1137       
1138        title = csmlDoc.name.CONTENT
1139        logging.debug("Got dataset name (title): '%s'" %title)
1140        # NB, if a title is specified (and not as the default value), it automatically is used in
1141        # place of anything in the granulite file
1142        if title and title != "NAME OF DATASET GOES HERE":
1143            logging.info("Title, '%s', extracted from CSML file" %title)
1144            if self.title:
1145                logging.info("- NB, this will override the title specified in the granulite file ('%s')" \
1146                             %self.title)
1147            self.title = title
1148               
1149        bbox1 = csmlDoc.getBoundingBox()
1150        bbox2 = csmlDoc.getCSMLBoundingBox()
1151        time = bbox2.getTimeLimits()
1152   
1153        # now check for other parameters to add to granule
1154        # Firstly, extract the bounding envelope
1155        if bbox1:
1156            w, e = self.moveBox(bbox1[0],bbox1[2])
1157            n, s = (bbox1[3], bbox1[1])
1158   
1159            if not aggregateCoverage or (not self.maxY or float(n) > float(self.maxY)):
1160                self.maxY = n
1161               
1162            if not aggregateCoverage or (not self.minY or float(s) < float(self.minY)):
1163                self.minY = s
1164           
1165            if not aggregateCoverage or (not self.minX or float(w) < float(self.minX)):
1166                self.minX = w
1167   
1168            if not aggregateCoverage or (not self.maxX or float(e) > float(self.maxX)):
1169                self.maxX = e
1170           
1171            logging.debug("Got bounding box data from file: (%s, %s) , (%s, %s)" \
1172                          %(w, s, e, n))
1173           
1174            logging.debug("Updated atom bounding box data: (%s, %s) , (%s, %s)" \
1175                          %(self.minX, self.minY, self.maxX, self.maxY))
1176        else:
1177            logging.debug("No valid bounding box data found")
1178   
1179        if time:
1180            t1 = formatDateYYYYMMDD(time[0])
1181            if not aggregateCoverage or \
1182                (not self.t1 or datetime.datetime.strptime(t1, YEAR_FORMAT) < \
1183                    datetime.datetime.strptime(self.t1, YEAR_FORMAT)):
1184                self.t1 = t1
1185   
1186            t2 = time[1]
1187            if t2 and t2 != 'None':
1188                t2 = formatDateYYYYMMDD(t2)
1189                if not aggregateCoverage or \
1190                    (not self.t2 or datetime.datetime.strptime(t2, YEAR_FORMAT) > \
1191                        datetime.datetime.strptime(self.t2, YEAR_FORMAT)):
1192                    self.t2 = t2
1193           
1194            logging.debug("Got time range: %s -> %s" %(self.t1, self.t2))
1195        else:
1196            logging.debug("No valid time range data found")
1197   
1198        #create parameter summaries:
1199        #set up list to hold the parameters data
1200        parameters = []
1201        for feature in csmlDoc.featureCollection.featureMembers:
1202            if hasattr(feature.parameter, 'href'):
1203                paramTriple = ""
1204                if hasattr(feature, 'description'):
1205                    paramTriple = feature.description.CONTENT
1206                    paramTriple += " | " + feature.parameter.href
1207                   
1208                    term = ""
1209                    if hasattr(feature, 'name'):
1210                        term = feature.name.CONTENT
1211   
1212                    paramTriple += " | " + term
1213                   
1214                    logging.debug("Got parameter info: %s" %paramTriple)
1215                    parameters.append(paramTriple)
1216       
1217        # update the atom with the extracted parameters
1218        logging.info("Adding CSML parameters to granule atom")
1219        self.addParameters(parameters)
1220        logging.info("Finished adding CSML data")
1221
1222
1223    def lookupDeploymentsInfo(self, dr):
1224        '''
1225        Check through the atom links and retrieve any associated deployments
1226        data
1227        @param dr: Instance of DocumentRetrieve object - NB, this requires eXist
1228        config details which are not available to the Atom object
1229        '''
1230        logging.info("Looking up deployments info")
1231        self.deployments = []
1232        self.allActivities = []
1233        self.allObs = []
1234        self.allDpts = []
1235        links = self.getLinksOfType(self.VTD.DEPLOYMENT_TERM)
1236
1237        for link in links:
1238            localID = link.href.split("__ATOM__")[-1]
1239            deploymentAtom = dr.get(self.ME.providerID, 'ATOM', localID, \
1240                                    targetCollection='/db/atoms/')
1241            deployment = Deployment.Deployment(Atom(xmlString=str(deploymentAtom)))
1242            self.deployments.append(deployment)
1243            self.allActivities.extend(deployment.activities)
1244            self.allObs.extend(deployment.obs)
1245            self.allDpts.extend(deployment.dpts)
1246       
1247        logging.info("Finished looking up deployments info")
1248       
Note: See TracBrowser for help on using the repository browser.