source: mauRepo/HPFos/trunk/src/HPFos/moles3epb.py @ 8354

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/mauRepo/HPFos/trunk/src/HPFos/moles3epb.py@8354
Revision 8354, 19.1 KB checked in by mnagni, 7 years ago (diff)

Implemented the bbox query URL (at least for the CEDA_ObservationCollection)

  • Property svn:mime-type set to text/plain
Line 
1'''
2BSD Licence
3Copyright (c) 2012, Science & Technology Facilities Council (STFC)
4All rights reserved.
5
6Redistribution and use in source and binary forms, with or without modification,
7are permitted provided that the following conditions are met:
8
9    * Redistributions of source code must retain the above copyright notice,
10        this list of conditions and the following disclaimer.
11    * Redistributions in binary form must reproduce the above copyright notice,
12        this list of conditions and the following disclaimer in the documentation
13        and/or other materials provided with the distribution.
14    * Neither the name of the Science & Technology Facilities Council (STFC)
15        nor the names of its contributors may be used to endorse or promote
16        products derived from this software without specific prior written permission.
17
18THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
22BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29Created on 10 Jan 2012
30
31@author: Maurizio Nagni
32'''
33from libs.epb import EPB
34from ea_model.ceda_metadatamodel.ceda_observationcollection.ceda_observationcollection import CEDA_ObservationCollection
35from ea_model.ceda_metadatamodel.ceda_observation.ceda_observation import CEDA_Observation
36from ea_model.iso_19115_2006_metadata_corrigendum.reference_system_information.md_identifier import MD_Identifier
37from ea_model.iso_19115_2006_metadata_corrigendum.citation_and_responsible_party_information.ci_citation import CI_Citation
38from ea_model.moles3_4.observationcollection.mo_observationcollection import MO_ObservationCollection
39from ea_model.moles3_4.observation.mo_observation import MO_Observation
40from ea_model.ceda_metadatamodel.ceda_project.ceda_project import CEDA_Project
41from sqlalchemy import Table, Column, ForeignKey, Integer, String
42from sqlalchemy.orm import mapper
43from ea_model.moles3_4.utilities.mo_responsiblepartyinfo import MO_ResponsiblePartyInfo
44from ea_model.moles3_4.utilities.mo_rolevalue import MO_RoleValue
45from sqlalchemy.orm.collections import InstrumentedList
46from ceda_guid import CedaGUID
47from sqlalchemy.orm.util import identity_key
48from ea_model.iso_19115_2006_metadata_corrigendum.extent_information.ex_geographicboundingbox import EX_GeographicBoundingBox
49
50import logging
51from logging import StreamHandler
52from libs.postgisutil import create_st_setSRID, unifyGeometries,\
53    intersectGeometries
54
55class Moles3EPBFactory(EPB):
56   
57    def __init__(self, dbManager):
58        self._dbManager = dbManager
59        self._initCEDA_Customization()   
60       
61
62    def _initCEDA_Customization(self):
63        self._associateCEDA_GUID()
64        #self._initSearchIndexes()       
65       
66    def _associateCEDA_GUID(self):
67        guid_table = Table('ceda_guid', self._dbManager.metadata, \
68                           Column('id', String, primary_key=True), \
69                           Column('ceda_observationcollection', Integer, ForeignKey('ceda_observationcollection.id')),
70                           Column('ceda_observation', Integer, ForeignKey('ceda_observation.id')))
71        mapper(CedaGUID, guid_table)
72        self._dbManager.metadata.create_all()
73
74    def _initSearchIndexes(self):
75        #To Be Done - CHECK IF THE COLUMN ALREADY EXISTS!
76        # We don't want sqlalchemy to know about this column so we add it externally.
77        try:
78            self._dbManager.engine.execute("alter table md_identifier add column code_search_vector tsvector")                 
79
80            # This indexes the tsvector column
81
82            self._dbManager.engine.execute("create index md_identifier_code_search_index on md_identifier using gin(code_search_vector)")
83
84            # This sets up the trigger that keeps the tsvector column up to date.
85            self._dbManager.engine.execute("create trigger md_identifier_code_search_update before update or insert on md_identifier \
86                for each row execute procedure tsvector_update_trigger('code_search_vector', 'pg_catalog.english', code)")                       
87        except Exception as e:
88            pass
89
90    def _getSession(self):
91        if self._dbManager is not None:
92            return self._dbManager.createDbSession()               
93        return None
94       
95    def createEPB(self):
96        return Moles3EPB(self._getSession())
97
98class Moles3EPB(object):
99
100    def __init__(self, session):
101        self._session = session
102        self.logging = logging.getLogger('Moles3EPB')
103        self.logging.addHandler(StreamHandler())
104        self.logging.setLevel(logging.INFO)
105       
106    def close(self):
107        return self._session.close()       
108       
109    def getObjectsByGE(self, class_type):
110        """
111            Checks if a CEDA_Collection contains a given CEDA_Observation.
112            @param obs_coll_id: the CEDA_ObservationColleciton id
113            @param obs_id: the CEDA_Observation id
114            @return: True if the collection contains the given observation, False otherwise 
115        """
116        if isinstance(class_type, type(CEDA_ObservationCollection)) or isinstance(class_type, type(CEDA_Observation)):
117            return self._session.query(class_type).all()
118        return None
119   
120    def getGeograpicExtentGeometry(self, ge):
121        '''
122            Creates the appropriate postgis geometry from a EX_GeographicExtent
123            @param ge: an EX_GeographicExtent instance
124            @return: a postgix text geometry
125        '''
126        if isinstance(ge, EX_GeographicBoundingBox):
127            return create_st_setSRID(ge.westBoundLongitude, ge.southBoundLatitude, \
128                           ge.eastBoundLongitude, ge.northBoundLatitude)
129        return None
130   
131   
132   
133    def getUnifyObservationCollectionGE(self, collection):
134        """
135            Returns the union of the collections.member'a  GeographicExtension(s)
136            @param collection: an CEDA_ObservationColleciton instance 
137        """
138        bboxes = []
139        for member in collection.member:
140            for ge in member.geographicExtent:
141                bboxes.append(self.getGeograpicExtentGeometry(ge))
142       
143        return unifyGeometries(bboxes, self)                       
144       
145    def getObservationCollections(self, bbox = None):
146        """
147            Returns the stored CEDA_ObservationCollection eventually filtering them against a postgis goemetry
148            @param bbox: a postgis geometry
149            @return: a list of CEDA_ObservationCollections or None if empty 
150        """
151        collections = self._session.query(CEDA_ObservationCollection).all()
152        if bbox == None:
153            return collections
154       
155        res = []
156        for collection in collections:
157            collection_geometry = self.getUnifyObservationCollectionGE(collection)
158            if collection_geometry and intersectGeometries([collection_geometry, bbox], self):
159                res.append(collection)
160        return res
161       
162    def searchEager(self, clazz, inst_id):
163        return EPB.searchEager(clazz, inst_id)     
164     
165    def persistInstance(self, instance):
166        """
167            Adds a new migration object.
168            @param migrationObj: the migration object to add
169            @param session: an SQLAlchemy Session object. If not None the session IS NOT closed at the exit,
170            If None (default) a new Session is created from the underlying EPB and closed at the exit.
171            @return an updated, session independent, object instance reflecting the new persisted object
172        """     
173        EPB.persistInstance(instance, self._session)
174       
175     
176    def updateCedaObject(self, ceda_object, cols_to_update):
177        """
178            Update and eventually commit a CEDA Object in MOLES3 db.
179            NOTE: only the returned instance will reflect the update!
180            @param ceda_object: the CEDA object to persist
181            @param dict: a dictionary containing the columns to update for the given ceda_object
182            @param session: the external session to use. If None a new session will be open to add and commit the object and then closed at the exit. The object is committed
183            @return: the given instance with the updated attributes.
184        """
185        coll = None
186        try:
187            coll = self._session.merge(ceda_object)
188        except Exception as e:
189            print e
190        if coll != None:       
191            for k,v in cols_to_update.items():
192                if hasattr(coll, k):
193                    val = None
194                    try:
195                        val = self._session.merge(v)
196                    except Exception:
197                        val = v
198                    coll_k = getattr(coll, k)                       
199                    if type(coll_k) == list or type(coll_k) == InstrumentedList:
200                        if  type(val) == list or type(val) == InstrumentedList:
201                            coll_k.extend(val)
202                        else:
203                            if val in coll_k:
204                                break
205                            coll_k.append(val)
206                    else:
207                        setattr(coll, k, val)
208        EPB.persistInstance(coll, self._session)                                 
209
210    def getInstanceFromGUID(self, guid):
211        """
212            Returns a CEDA_Observation or a CEDA_ObservationCollection from their GUID
213            @param guid: the object guid
214            @return: the associated instance or None
215        """
216        ceda_guid = self.search(CedaGUID, guid)
217        if ceda_guid:
218            if ceda_guid.ceda_observation:
219                return self.search(CEDA_Observation, ceda_guid.ceda_observation)
220            elif ceda_guid.ceda_observationcollection:
221                return self.search(CEDA_ObservationCollection, ceda_guid.ceda_observationcollection)
222        return None
223
224    def retrieveGUIDFromInstance(self, instance):
225        """
226            Returns the CedaGUID object associated with the given instance.
227            @param instance: an instance of CEDA_Observation os CEDA_ObservationCollection 
228        """
229        if instance is None:
230            return None
231        if type(instance) == CEDA_ObservationCollection:
232            return self._session.query(CedaGUID).filter(CedaGUID.ceda_observationcollection==instance.id).first()
233        elif type(instance) == CEDA_Observation:
234            return self._session.query(CedaGUID).filter(CedaGUID.ceda_observation==instance.id).first()       
235   
236    def getObservationFromObservationCollection(self, obs_coll_id):
237        """
238            Checks if a CEDA_Collection contains a given CEDA_Observation.
239            @param obs_coll_id: the CEDA_ObservationColleciton id
240            @return: the associated CEDA_Observation 
241        """
242        return self._session.query(CEDA_ObservationCollection, CEDA_Observation).filter(CEDA_ObservationCollection.id==obs_coll_id).all()
243
244    def observationAuthor(self, obs_id):
245        """
246            Lists the CEDA_Observation author.
247            @param obs_id: the CEDA_Observation id           
248            @return: True if the collection contains the given observation, False otherwise 
249        """
250        ret = self._session.query(MO_ResponsiblePartyInfo).join(MO_Observation). \
251            filter(MO_ResponsiblePartyInfo.role == MO_RoleValue.cl_author). \
252            filter(MO_Observation.id == obs_id)       
253        return ret
254
255    def extractObservationByTitleKeywords(self, keywords):
256        """
257            Loooks for CEDA_Observation containing a specific title (observation.identifier.code)
258            @param keywords: a space separated terms string
259            @return: a tuple containing a CEDA_Observation satisfying the queryllection.idenfitier element having the title 
260        """               
261        # search_vector is a ts_vector column. To search for terms, you use the
262        # @@ operator. plainto_tsquery turns a string into a query that can be
263        # used with @@. So this adds a where clause like "WHERE search_vector
264        # @@ plaint_tsquery(<search string>)"
265        q = self._session.query(CEDA_Observation). \
266            join(MO_Observation).join(MO_ObservationCollection.identifier). \
267            filter('md_identifier.code_search_vector @@ to_tsquery(:terms)')
268        # This binds the :terms placeholder to the searchterms string. User input
269        # should always be put into queries this way to prevent SQL injection.
270        q = q.params(terms=keywords)
271        return q
272
273
274    def extractCollectionIdentifierByTitle(self, i_title):
275        """
276            Searches for an MD_Identifier from a CEDA_ObservationCollection contains a specific title (observation.identifier.code)
277            @param i_title: the CEDA_ObservationCollection.identifier.title value to search for
278            @return: a tuple containing a CEDA_ObservationCollection and the CEDA_ObservationCollection.idenfitier element having the title 
279        """
280        return self._session.query(CEDA_ObservationCollection, MD_Identifier). \
281            join(MO_ObservationCollection).join(MO_ObservationCollection.identifier). \
282            join(MD_Identifier.authority).filter(CI_Citation.title.like('%' + i_title + '%'))
283
284    def extractObservationsForProject(self, project):
285        """
286            Searches for the CEDA_Observation associated with a CEDA_Project
287            @param project: a CEDA_Project instance
288            @return: a tuple containing the associated CEDA_Observation 
289        """
290        return self._session.query(CEDA_Observation). \
291            join(CEDA_Observation, MO_Observation.inSupportOf).filter(CEDA_Project.id == project.id)
292
293    def extractProjectObservationCollections(self, project):
294        """
295            Searches for the Observation_Collections associated with a CEDA_Project
296            @param project: a CEDA_Project instance
297            @return: a tuple containing the associated CEDA_ObservationCollection 
298        """
299        mo_obs = self._session.query(MO_Observation).join(CEDA_Project).filter(CEDA_Project.id == project.id).subquery()     
300        obsers = self._session.query(CEDA_Observation).join(mo_obs, CEDA_Observation.id == mo_obs.c.id).one()
301        #print "obsers: " + str(intSession.query(CEDA_Observation).join(mo_obs, CEDA_Observation.id == mo_obs.c.id).count())
302       
303        cos = self._session.query(CEDA_ObservationCollection).all()
304        co = self._session.query(MO_ObservationCollection).join(MO_ObservationCollection.member).filter(MO_ObservationCollection.member.contains(obsers))
305       
306        observations = self._session.query(MO_ObservationCollection).join(CEDA_Observation). \
307            filter(obsers.any(CEDA_Observation.id==obsers.c.id))
308        print "observation:" + str(observations.count())
309        return observations
310
311    def search(self, clazz, inst_key):
312        """
313            Searches a required instance by id
314            @param clazz: the class type to search for
315            @param inst_key: the instance id for which the search is done. If None return all the instances of the type
316            @return the required instance(s)
317        """       
318        if inst_key:
319            ret = EPB.search(clazz, inst_key, self._session)
320        else:
321            ret = EPB.search(clazz, None, self._session).all()
322        return ret
323     
324    def _search(self, clazz, inst_id):
325        return EPB.search(clazz, inst_id, self._session)     
326     
327    def searchSelectiveLoadByInstance(self, instance, attributes):
328        id_key = identity_key(instance=instance)
329        if id_key:
330            return self.searchSelectiveLoad(id_key[0], id_key[1], attributes)
331        return None
332     
333    def searchSelectiveLoad(self, clazz, inst_id, attributes):
334        """
335            Searches a required instance by id loading selectively \
336            the specified fields. The parameter "attributes" is a single string or a list of attributes
337            owned by the instance of "clazz". Furthermore such list may contain
338            also the children of the main attributes. For example "attrs" may look
339            like
340            ['resultAccumulation', 'identifier.authority', 'resultTime.position.dateTime8601.month', \
341                      'relatedParty.party', 'result.source.function', 'permission', \
342                      'geographicExtent', 'phenomenonTime', 'keywords', 'description', \
343                      'inSupportOf.abstract', 'dataLineage']
344            the first parameter refers to the main class so is equivalent to
345            clazz.resultAccumulation
346            the second parameter is equivalent to invoke
347            clazz.identifier.authority
348            As single string "attributes" could be as well just 'identifier.authority'
349            @param clazz: the class type to search for
350            @param inst_id: the instance id for which the search is done
351            @param attributes: a single string or a list of attributes to load
352            @param session: a session to use for the query. By default a new one is created automatically at start and closed at the end
353            @return the required instance             
354        """               
355        ret = EPB.searchSelectiveLoad(clazz, inst_id, attributes, self._session)
356        return ret   
357   
358    def loadAttributes(self, instance, attributes):
359        """
360            Returns the attribute of an instance. The parameter "attributes" is a single string or a list of attributes
361            owned by the instance of "clazz". Furthermore such list may contain
362            also the children of the main attributes. For example "attrs" may look
363            like
364            ['resultAccumulation', 'identifier.authority', 'resultTime.position.dateTime8601.month', \
365                      'relatedParty.party', 'result.source.function', 'permission', \
366                      'geographicExtent', 'phenomenonTime', 'keywords', 'description', \
367                      'inSupportOf.abstract', 'dataLineage']
368            the first parameter refers to the main class so is equivalent to
369            clazz.resultAccumulation
370            the second parameter is equivalent to invoke
371            clazz.identifier.authority
372            As single string "attributes" could be as well just 'identifier.authority'
373            @param instance: an instance containing the appropriate id
374            @param attributes: the attribute value required
375            @param session: the session to use for the operation
376            @return: the given instance filled with the required attributes.                     
377        """
378        instance = self._session.merge(instance)
379        EPB.loadAttributes(instance, attributes, self._session)                 
380        return instance
381
382    def executeNative(self, sqlNative):
383        return EPB.executeNative(sqlNative, self._session) 
Note: See TracBrowser for help on using the repository browser.