# -*- coding: utf-8 -*- ############################################################################## # # Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved. # Sebastien Robin <seb@nexedi.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## from AccessControl import ClassSecurityInfo from Acquisition import aq_base from Products.ERP5Type.XMLObject import XMLObject from Products.ERP5Type import Permissions, PropertySheet from Products.ERP5Type.Utils import deprecated from DateTime import DateTime from zLOG import LOG, DEBUG, INFO, WARNING from base64 import b16encode, b16decode from Products.ERP5SyncML.XMLSyncUtils import getConduitByName from Products.ERP5SyncML.SyncMLConstant import MAX_OBJECTS, ACTIVITY_PRIORITY from warnings import warn _MARKER = [] class SyncMLSubscription(XMLObject): """ Subscription hold the definition of a master ODB from/to which a selection of objects will be synchronized Subscription defined by:: publication_url -- a URI to a publication subscription_url -- URL of ourselves destination_path -- the place where objects are stored query -- a query which defines a local set of documents which are going to be synchronized xml_mapping -- a PageTemplate to map documents to XML gpg_key -- the name of a gpg key to use Subscription also holds private data to manage the synchronization. We choose to keep an MD5 value for all documents which belong to the synchronization process:: signatures -- a dictionnary which contains the signature of documents at the time they were synchronized session_id -- it defines the id of the session with the server. last_anchor - it defines the id of the last synchronization next_anchor - it defines the id of the current synchronization Subscription inherit of File because the Signature use method _read_data which have the need of a __r_jar not None. During the initialization of a Signature this __p_jar is None """ meta_type = 'ERP5 Subscription' portal_type = 'SyncML Subscription' # may be useful in the future... security = ClassSecurityInfo() security.declareObjectProtected(Permissions.AccessContentsInformation) # Declarative properties property_sheets = ( PropertySheet.Base , PropertySheet.XMLObject , PropertySheet.CategoryCore , PropertySheet.DublinCore , PropertySheet.Reference , PropertySheet.Login , PropertySheet.Url , PropertySheet.Gpg , PropertySheet.Data , PropertySheet.SyncMLSubscription , PropertySheet.SyncMLSubscriptionConstraint ) security.declareProtected(Permissions.AccessContentsInformation, 'isOneWayFromServer') def isOneWayFromServer(self): return self.getPortalType() == 'SyncML Subscription' and \ self.getSyncmlAlertCode() == 'one_way_from_server' security.declareProtected(Permissions.AccessContentsInformation, 'isOneWayFromClient') def isOneWayFromClient(self): return self.getParentValue().getPortalType() == 'SyncML Publication' and \ self.getSyncmlAlertCode() == 'one_way_from_client' security.declareProtected(Permissions.AccessContentsInformation, 'getSynchronizationType') def getSynchronizationType(self, default=_MARKER): """Deprecated alias of getSyncmlAlertCode """ warn('Use getSyncmlAlertCode instead', DeprecationWarning) if default is _MARKER: code = self.getSyncmlAlertCode() else: code = self.getSyncmlAlertCode(default=default) return code security.declarePrivate('checkCorrectRemoteSessionId') def checkCorrectRemoteSessionId(self, session_id): """ We will see if the last session id was the same wich means that the same message was sent again return True if the session id was not seen, False if already seen """ if self.getLastSessionId() == session_id: return False self.setLastSessionId(session_id) return True security.declarePrivate('checkCorrectRemoteMessageId') def checkCorrectRemoteMessageId(self, message_id): """ We will see if the last message id was the same wich means that the same message was sent again return True if the message id was not seen, False if already seen """ if self.getLastMessageId() == message_id: return False self.setLastMessageId(message_id) return True security.declareProtected(Permissions.ModifyPortalContent, 'initLastMessageId') def initLastMessageId(self, last_message_id=0): """ set the last message id to 0 """ self.setLastMessageId(last_message_id) security.declareProtected(Permissions.AccessContentsInformation, 'getXmlBindingGeneratorMethodId') def getXmlBindingGeneratorMethodId(self, default=_MARKER, force=False): """ return the xml mapping """ if self.isOneWayFromServer() and not force: return None if default is _MARKER: return self._baseGetXmlBindingGeneratorMethodId() else: return self._baseGetXmlBindingGeneratorMethodId(default=default) security.declareProtected(Permissions.AccessContentsInformation, 'getGidFromObject') def getGidFromObject(self, object, encoded=True): """ Returns the object gid """ o_base = aq_base(object) gid = None # first try with new method gid_generator = self.getGidGeneratorMethodId("") if gid_generator not in ("", None) and getattr(self, gid_generator, None): raw_gid = getattr(self, gid_generator)(object) else: # old way using the conduit conduit_name = self.getConduitModuleId() conduit = getConduitByName(conduit_name) raw_gid = conduit.getGidFromObject(object) if isinstance(raw_gid, unicode): raw_gid = raw_gid.encode('ascii', 'ignore') if encoded: gid = b16encode(raw_gid) else: gid = raw_gid return gid security.declareProtected(Permissions.AccessContentsInformation, 'getObjectFromGid') def getObjectFromGid(self, gid, use_list_method=True): """ This tries to get the object with the given gid This uses the query if it exist and use_list_method is True """ if len(gid)%2 != 0: #something encode in base 16 is always a even number of number #if not, b16decode will failed return None signature = self.getSignatureFromGid(gid) # First look if we do already have the mapping between # the id and the gid destination = self.getSourceValue() if signature is not None and signature.getReference(): document_path = signature.getReference() document = self.getPortalObject().unrestrictedTraverse(document_path, None) if document is not None: return document #LOG('entering in the slow loop of getObjectFromGid !!!', WARNING, #self.getPath()) if use_list_method: object_list = self.getObjectList(gid=b16decode(gid)) for document in object_list: document_gid = self.getGidFromObject(document) if document_gid == gid: return document #LOG('getObjectFromGid', DEBUG, 'returning None') return None security.declareProtected(Permissions.AccessContentsInformation, 'getObjectFromId') def getObjectFromId(self, id): """ return the object corresponding to the id """ object_list = self.getObjectList(id=id) o = None for object in object_list: if object.getId() == id: o = object break return o security.declareProtected(Permissions.AccessContentsInformation, 'getObjectList') def getObjectList(self, **kw): """ This returns the list of sub-object corresponding to the query """ folder = self.getSourceValue() list_method_id = self.getListMethodId() if list_method_id is not None and isinstance(list_method_id, str): result_list = [] query_method = folder.unrestrictedTraverse(list_method_id, None) if query_method is not None: result_list = query_method(context_document=self, **kw) else: raise KeyError, 'This Subscriber %s provide no list method:%r'\ % (self.getPath(), list_method_id) else: raise KeyError, 'This Subscriber %s provide no list method with id:%r'\ % (self.getPath(), list_method_id) # XXX Access all objects is very costly return [x for x in result_list if not getattr(x, '_conflict_resolution', False)] security.declarePrivate('generateNewIdWithGenerator') def generateNewIdWithGenerator(self, object=None, gid=None): """ This tries to generate a new Id """ warn('Only container is allowed to compute new ID', DeprecationWarning) id_generator = self.getSynchronizationIdGeneratorMethodId() if id_generator is not None: o_base = aq_base(object) new_id = None if callable(id_generator): new_id = id_generator(object, gid=gid) elif getattr(o_base, id_generator, None) is not None: generator = getattr(object, id_generator) new_id = generator() else: # This is probably a python script generator = getattr(object, id_generator) new_id = generator(object=object, gid=gid) #LOG('generateNewIdWithGenerator, new_id: ', DEBUG, new_id) return new_id return None security.declareProtected(Permissions.ModifyPortalContent, 'incrementSessionId') def incrementSessionId(self): """ increment and return the session id """ session_id = self.getSessionId() session_id += 1 self._setSessionId(session_id) self.resetMessageId() # for a new session, the message Id must be reset return session_id security.declareProtected(Permissions.ModifyPortalContent, 'incrementMessageId') def incrementMessageId(self): """ return the message id """ message_id = self.getMessageId(0) message_id += 1 self._setMessageId(message_id) return message_id security.declareProtected(Permissions.ModifyPortalContent, 'resetMessageId') def resetMessageId(self): """ set the message id to 0 """ self._setMessageId(0) security.declareProtected(Permissions.ModifyPortalContent, 'createNewAnchor') def createNewAnchor(self): """ set a new anchor """ self.setLastAnchor(self.getNextAnchor()) self.setNextAnchor(DateTime()) security.declareProtected(Permissions.ModifyPortalContent, 'resetAnchorList') def resetAnchorList(self): """ reset both last and next anchors """ self.setLastAnchor(None) self.setNextAnchor(None) security.declareProtected(Permissions.AccessContentsInformation, 'getSignatureFromObjectId') def getSignatureFromObjectId(self, id): """ return the signature corresponding to the id ### Use a reverse dictionary will be usefull to handle changes of GIDs """ document = None # XXX very slow for signature in self.objectValues(): document = signature.getSourceValue() if document is not None: if id == document.getId(): document = signature break return document security.declareProtected(Permissions.AccessContentsInformation, 'getSignatureFromGid') def getSignatureFromGid(self, gid): """ return the signature corresponding to the gid """ return self._getOb(gid, None) security.declareProtected(Permissions.AccessContentsInformation, 'getGidList') def getGidList(self): """ Returns the list of gids from signature """ return [id for id in self.getObjectIds()] security.declareProtected(Permissions.AccessContentsInformation, 'getSignatureList') @deprecated def getSignatureList(self): """ Returns the list of Signatures """ return self.contentValues(portal_type='SyncML Signature') security.declareProtected(Permissions.AccessContentsInformation, 'hasSignature') def hasSignature(self, gid): """ Check if there's a signature with this uid """ return self.getSignatureFromGid(gid) is not None security.declareProtected(Permissions.ModifyPortalContent, 'resetSignatureList') def resetSignatureList(self): """ Reset all signatures in activities """ object_id_list = [id for id in self.getObjectIds()] object_list_len = len(object_id_list) for i in xrange(0, object_list_len, MAX_OBJECTS): current_id_list = object_id_list[i:i+MAX_OBJECTS] self.activate(activity='SQLQueue', tag=self.getId(), priority=ACTIVITY_PRIORITY).manage_delObjects(current_id_list) security.declareProtected(Permissions.AccessContentsInformation, 'getConflictList') def getConflictList(self, *args, **kw): """ Return the list of all conflicts from all signatures """ conflict_list = [] for signature in self.objectValues(): conflict_list.extend(signature.getConflictList()) return conflict_list security.declareProtected(Permissions.ModifyPortalContent, 'removeRemainingObjectPath') def removeRemainingObjectPath(self, object_path): """ We should now wich objects should still synchronize """ remaining_object_list = self.getProperty('remaining_object_path_list') if remaining_object_list is None: # it is important to let remaining_object_path_list to None # it means it has not beeing initialised yet return new_list = [] new_list.extend(remaining_object_list) while object_path in new_list: new_list.remove(object_path) self._edit(remaining_object_path_list=new_list) security.declareProtected(Permissions.ModifyPortalContent, 'initialiseSynchronization') def initialiseSynchronization(self): """ Set the status of every object as not_synchronized XXX Improve method to not fetch many objects in unique transaction """ LOG('Subscription.initialiseSynchronization()', 0, self.getPath()) for signature in self.contentValues(portal_type='SyncML Signature'): # Change the status only if we are not in a conflict mode if signature.getValidationState() not in ('conflict', 'conflict_resolved_with_merge', 'conflict_resolved_with_client_command_winning'): if self.getIsActivityEnabled(): signature.activate(tag=self.getId(), activity='SQLQueue', priority=ACTIVITY_PRIORITY).reset() else: signature.reset() self._edit(remaining_object_path_list=None)