blob: aa6f99cb815d83b6bb5f109d018bace0ab4f8b62 [file] [log] [blame]
# encoding: utf-8
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE.txt file distributed with
# this work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
'''Profiles. A profile is a metadata description of a resource.
Profiles capture:
* Inception metadata. This includes items such as a resouce's title,
description, creators, contributors, publishers, language, and so on. This
set of metdata is based on the Dublin Core. Class `ResourceAttributes`
captures this metadata.
* Composition metadata. This includes data elements that describe what
the resource contains, such as ranges of data values (high and low
temperature, latitude/longitude) or enumerated data values (zoning code,
genus/species, etc.). Subclasses of `ProfileElement` captures this
metadata.
* Profile metadata. This is metadata describing the profile itself, such
as revision notes, ID number, etc. Class `ProfileAttributes` captures this
metadata.
Objects of these classes are unified into a single `Profile` object.
'''
__docformat__ = 'restructuredtext'
import xmlutils
import xml.dom
from xmlutils import DocumentableField
class ProfileAttributes(xmlutils.Documentable):
'''Attributes of a profile. These are attributes not related to the
resource that a profile profiles, but rather the profile itself. In
most cases, simply constructing this object with no initializer
arguments will suffice.
'''
def __init__(self, id='UNKNOWN', version='1.0.0', type='profile', statusID='active', securityType='unclassified',
parentID='UNKNOWN', childIDs=None, regAuthority='UNKNOWN', revNotes=None, node=None):
'''Initialize profile attributes.
- `id` unique ID for the profile
- `version` number
- `type` should always be the string "profile"
- `statusID` should always be the string "active"
- `securityType` tells whether the profile is secret
- `parentID` gives the unique ID for any "parent" profile
- `childIDs` should be a sequnece of any "children" profiles
- `regAuthority` is a unique ID identifying the registration
authority responsible for the profile
- `revNotes` is a sequence of comments detailing historical
changes to the profile
- `node` is an XML DOM node. If the `node` argument is given,
it's used to initialize the object.
'''
self.id = id
self.version = version
self.type = type
self.statusID = statusID
self.securityType = securityType
self.parentID = parentID
self.childIDs = childIDs
self.regAuthority = regAuthority
self.revNotes = revNotes
if self.childIDs is None:
self.childIDs = []
if self.revNotes is None:
self.revNotes = []
if node is not None:
self.parse(node)
def getDocumentElementName(self):
'''Get the XML tag for objects of this class: `profAttributes`.
'''
return 'profAttributes'
def getDocumentableFields(self):
'''Get the attributes that are put into XML.
'''
return (DocumentableField('id', u'profId', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('version', u'profVersion', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('type', u'profType', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('statusID', u'profStatusId', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('securityType', u'profSecurityType', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('parentID', u'profParentId', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('childIDs', u'profChildId', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('regAuthority', u'profRegAuthority', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('revNotes', u'profRevisionNote', DocumentableField.MULTI_VALUED_KIND))
def __repr__(self):
return ('ProfileAttributes(id=%s,version=%s,type=%s,statusID=%s,securityType=%s,parentID=%s,childIDs=%s,' \
'regAuthority=%s,revNotes=%s)') % (self.id, self.version, self.type, self.statusID, self.securityType,
self.parentID, self.childIDs, self.regAuthority, self.revNotes)
def __cmp__(self, other):
return cmp(self.id, other.id)
def __hash__(self):
return hash(self.id)
class ResourceAttributes(xmlutils.Documentable):
'''Attributes of the resource. Objects of this class collect data about the resource's
inception and are based on Dublin Core.
'''
def __init__(self, identifier='UNKNOWN', title='UNKNOWN', formats=None, description='UNKNOWN', creators=None, subjects=None,
publishers=None, contributors=None, dates=None, types=None, sources=None, languages=None, relations=None,
coverages=None, rights=None, contexts=None, aggregation='UNKNOWN', resClass='UNKNOWN', locations=None, node=None):
'''Initialize ResourceAttributes.
The following arguments are required:
- `identifier` is a URI uniquely identifying the resource
- `title` names the resource
- `description` gives a summary or abstract of it
- `aggregation` tells the gross structure of the resource and
should be one of the following values:
- data.granule
- data.dataSet
- data.dataSetCollection
- `resClass` gives the kind of the resource (what to expect
when connecting to one of its locations)
All of the others should be initialized with sequences as the
resource attributes follows the Dublin Core recommendation on
multiplicity.
If `node` is given, it's treated as an XML DOM node and is used to
initialize the resource attributes.
'''
self.identifier = identifier
self.title = title
self.formats = formats
self.description = description
self.creators = creators
self.subjects = subjects
self.publishers = publishers
self.contributors = contributors
self.dates = dates
self.types = types
self.sources = sources
self.languages = languages
self.relations = relations
self.coverages = coverages
self.rights = rights
self.contexts = contexts
self.aggregation = aggregation
self.resClass = resClass
self.locations = locations
for attr in ('formats', 'creators', 'subjects', 'publishers', 'contributors', 'dates', 'sources', 'languages',
'relations', 'coverages', 'rights', 'contexts', 'locations', 'types'):
if getattr(self, attr) is None:
setattr(self, attr, [])
if node is not None:
self.parse(node)
def getDocumentElementName(self):
'''Give the XML tag name: `resAttributes`.
'''
return 'resAttributes'
def __hash__(self):
return hash(self.identifier)
def __cmp__(self, other):
return cmp(self.identifier, other.identifier)
def getDocumentableFields(self):
'''Get the attributes that go into XML.
'''
return (DocumentableField('identifier', u'Identifier', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('title', u'Title', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('formats', u'Format', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('description', u'Description', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('creators', u'Creator', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('subjects', u'Subject', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('publishers', u'Publisher', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('contributors', u'Contributor', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('dates', u'Date', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('types', u'Type', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('sources', u'Source', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('languages', u'Language', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('relations', u'Relation', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('coverages', u'Coverage', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('rights', u'Rights', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('contexts', u'resContext', DocumentableField.MULTI_VALUED_KIND),
DocumentableField('aggregation', u'resAggregation', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('resClass', u'resClass', DocumentableField.SINGLE_VALUE_KIND),
DocumentableField('locations', u'resLocation', DocumentableField.MULTI_VALUED_KIND))
class Profile(object):
'''A profile "profiles" a resource by describing it with metadata.
'''
def __init__(self, profAttr=None, resAttr=None, profElements=None, node=None):
'''Initialize a profile. The profElements should be a dicitonary that maps
from element name to instance of a `ProfileElement`.
'''
self.profAttr, self.resAttr, self.profElements = profAttr, resAttr, profElements
if self.profAttr is None:
self.profAttr = ProfileAttributes()
if self.resAttr is None:
self.resAttr = ResourceAttributes()
if self.profElements is None:
self.profElements = {}
if node is not None:
self.parse(node)
def parse(self, node):
'''Initialize this object from the given XML DOM `node`.
'''
if node.nodeName != 'profile':
raise ValueError('Expected profile element but got "%s"' % node.nodeName)
for child in filter(lambda node: node.nodeType == xml.dom.Node.ELEMENT_NODE, node.childNodes):
name = child.nodeName
if name == u'profAttributes':
self.profAttr = ProfileAttributes()
self.profAttr.parse(child)
elif name == u'resAttributes':
self.resAttr = ResourceAttributes()
self.resAttr.parse(child)
elif name == u'profElement':
elem = _parseProfileElement(child)
self.profElements[elem.name] = elem
def toXML(self, owner):
'''Convert this object into XML owned by the given `owner` document.
'''
root = owner.createElement(u'profile')
root.appendChild(self.profAttr.toXML(owner))
root.appendChild(self.resAttr.toXML(owner))
for elem in self.profElements.itervalues():
root.appendChild(elem.toXML(owner))
return root
def __cmp__(self, other):
profAttr = cmp(self.profAttr, other.profAttr)
if profAttr < 0:
return -1
elif profAttr == 0:
resAttr = cmp(self.resAttr, other.resAttr)
if resAttr < 0:
return -1
elif resAttr == 0:
return cmp(self.profElements, other.profElements)
return 1
def __hash__(self):
return hash(self.profAttr) ^ hash(self.resAttr)
def __repr__(self):
return 'Profile(profAttr=%s,resAttr=%s,profElements=%s)' % (self.profAttr, self.resAttr, self.profElements)
class ProfileElement(object):
'''Abstract profile element.
'''
def __init__(self, name, description, type, units, synonyms, comment):
'''Initialize profile element.
'''
self.name = name
self.description = description
self.type = type
self.units = units
self.synonyms = synonyms
self.comment = comment
def __repr__(self):
return 'ProfileElement(name=%r,description=%r,type=%r,units=%r,synonyms=%r,comment=%r)' % (self.name,
self.description, self.type, self.units, self.synonyms, self.comment)
def __cmp__(self, other):
rc = cmp(self.name, other.name)
if rc < 0:
return -1
elif rc == 0:
rc = cmp(self.description, other.description)
if rc < 0:
return -1
elif rc == 0:
rc = cmp(self.type, other.type)
if rc < 0:
return -1
elif rc == 0:
rc = cmp(self.units, other.units)
if rc < 0:
return -1
elif rc == 0:
rc = cmp(self.synonyms, other.synonyms)
if rc < 0:
return -1
elif rc == 0:
return cmp(self.comment, other.comment)
return 1
def __hash__(self):
return reduce(lambda x, y: hash(x) ^ hash(y), [getattr(self, attr) for attr in ('name', 'description', 'type',
'units', 'synonyms', 'comment')])
def isEnumerated(self):
'''Is this an enumerated element? Enumerated elements include those with
discrete values as well unspecified elements.
'''
return False;
def getValues(self):
'''Get the discrete values of this element, which may be empty.
'''
return []
def getMinValue(self):
'''Get the minimum value of this element, which may be zero.
'''
return 0.0
def getMaxValue(self):
'''Get the maximum value of this element, which may be zero.
'''
return 0.0
def toXML(self, owner):
'''Convert this object into XML owned by the given `owner` document.
'''
root = owner.createElement('profElement')
xmlutils.add(root, u'elemId', self.name)
xmlutils.add(root, u'elemName', self.name)
xmlutils.add(root, u'elemDesc', self.description)
xmlutils.add(root, u'elemType', self.type)
xmlutils.add(root, u'elemUnit', self.units)
if self.isEnumerated():
flag = 'T'
else:
flag = 'F'
xmlutils.add(root, u'elemEnumFlag', flag)
for value in self.getValues():
elem = owner.createElement('elemValue')
root.appendChild(elem)
elem.appendChild(owner.createCDATASection(value))
if not self.isEnumerated():
xmlutils.add(root, u'elemMinValue', str(self.getMinValue()))
xmlutils.add(root, u'elemMaxValue', str(self.getMaxValue()))
xmlutils.add(root, u'elemSynonym', self.synonyms)
xmlutils.add(root, u'elemComment', self.comment)
return root
class UnspecifiedProfileElement(ProfileElement):
'''An unspecified profile element merely documents the existence of a element within a
dataset but says nothing about its actual values.
'''
def __init__(self, name, description, type, units, synonyms, comment):
'''Initialize an "unspecified" profile element.
'''
super(UnspecifiedProfileElement, self).__init__(name, description, type, units, synonyms, comment)
def isEnumerated(self):
'''An unspecified profile element is indeed enumerated. It just has no enumerated values.
'''
return True
class EnumeratedProfileElement(ProfileElement):
'''An enumerated profile element describes set of discrete values.
'''
def __init__(self, name, description, type, units, synonyms, comment, values):
'''Initialize an enumerated profile element.
'''
super(EnumeratedProfileElement, self).__init__(name, description, type, units, synonyms, comment)
self.values = values
def isEnumerated(self):
'''An enumerated profile element is indeed enumerated.
'''
return True
def getValues(self):
'''Return the sequence of values.
'''
return self.values
def __cmp__(self, other):
rc = super(EnumeratedProfileElement, self).__cmp__(other)
if rc < 0:
return -1
elif rc == 0:
return cmp(self.values, other.values)
return 1
class RangedProfileElement(ProfileElement):
'''A ranged profile element describes a value between two numeric ranges.
'''
def __init__(self, name, description, type, units, synonyms, comment, minValue, maxValue):
'''Initialize a ranged profile element.
'''
super(RangedProfileElement, self).__init__(name, description, type, units, synonyms, comment)
self.minValue = minValue
self.maxValue = maxValue
def getMinValue(self):
'''Get the minimum value.
'''
return self.minValue
def getMaxValue(self):
'''Get the maximum value.
'''
return self.maxValue
def __repr__(self):
return 'RangedProfileElement(%r,minValue=%r,maxValue=%r)' % (super(RangedProfileElement, self).__repr__(),
self.minValue, self.maxValue)
def __cmp__(self, other):
rc = super(RangedProfileElement, self).__cmp__(other)
if rc < 0:
return -1
elif rc == 0:
rc = self.minValue - other.minValue
if rc < 0:
return -1
elif rc == 0:
return self.maxValue - other.maxValue
return 1
def _parseProfileElement(node):
'''Construct an appropriate profile element from the given DOM node.
'''
if node.nodeName != u'profElement':
raise ValueError('Expected profElement element but got "%s"' % node.nodeName)
settings = dict(elemId=u'UNKNOWN', elemDesc=u'UNKNOWN', elemType=u'UNKNOWN', elemUnit=u'UNKNOWN', elemEnumFlag=u'F',
elemComment=u'UNKNOWN')
values, syns = [], []
for child in filter(lambda node: node.nodeType == xml.dom.Node.ELEMENT_NODE, node.childNodes):
text = xmlutils.text(child)
if child.nodeName == u'elemValue':
values.append(text)
elif child.nodeName == u'elemSynonym':
syns.append(text)
else:
settings[str(child.nodeName)] = text
if 'elemName' not in settings:
raise ValueError('profElement requires elemName but none specified')
if 'elemEnumFlag' not in settings:
raise ValueError('profElement requires elemEnumFlag but none specified')
# Normally I'd treat only those XML elements where elemEnumFlag as T as possibly producing
# unspecified or enumerated, and F producing *only* ranged elements. But PDS profile
# servers are producing profile elements with F for elemEnumFlag and yet NO elemMinValue
# and elemMaxValue. If they're using the Java profile code, I'd call that a bug in that
# code. If they're not, then I'd say they're producing profiles incorrectly.
if settings['elemEnumFlag'] == 'T':
if len(values) == 0:
return UnspecifiedProfileElement(settings['elemName'], settings['elemDesc'], settings['elemType'],
settings['elemUnit'], syns, settings['elemComment'])
else:
return EnumeratedProfileElement(settings['elemName'], settings['elemDesc'], settings['elemType'],
settings['elemUnit'], syns, settings['elemComment'], values)
else:
if 'elemMinValue' not in settings or 'elemMaxValue' not in settings:
return UnspecifiedProfileElement(settings['elemName'], settings['elemDesc'], settings['elemType'],
settings['elemUnit'], syns, settings['elemComment'])
else:
return RangedProfileElement(settings['elemName'], settings['elemDesc'], settings['elemType'],
settings['elemUnit'], syns, settings['elemComment'], float(settings['elemMinValue']),
float(settings['elemMaxValue']))
# Some sample code:
# if __name__ == '__main__':
# import urllib2, xml.dom.minidom
# x = urllib2.urlopen('http://starbrite.jpl.nasa.gov/q?object=urn%3Aeda%3Armi%3AJPL.PDS.MasterProd&type=profile&keywordQuery=TARGET_NAME+%3D+MARS')
# d = xml.dom.minidom.parse(x)
# x.close()
# profiles = []
# for i in d.documentElement.getElementsByTagName(u'profile'):
# profiles.append(Profile(node=i))
#
# doc = xml.dom.minidom.getDOMImplementation().createDocument(None, None, None)
# print '<?xml version="1.0" encoding="UTF-8"?>'
# print '<!DOCTYPE profiles PUBLIC "-//JPL//DTD Profile 1.1//EN"'
# print ' "http://oodt.jpl.nasa.gov/grid-profile/dtd/prof.dtd">'
# print '<profiles>'
# for profile in profiles:
# print profile.toXML(doc).toxml()
# print '</profiles>'
#