source: ndgCommon/trunk/ndg/common/src/models/Atom.py @ 5106

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/ndgCommon/trunk/ndg/common/src/models/Atom.py@5106
Revision 5106, 52.6 KB checked in by cbyrom, 11 years ago (diff)

Fix parsing of atom content + avoid adding newline characters for
summary + adjust test data.

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