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

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

Fixed bug in addOnlineReferences that prevented all references being deleted

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