from Acquisition import aq_base
from Acquisition import aq_inner
from Acquisition import aq_parent
from DateTime import DateTime
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.Five import BrowserView
from ZODB.POSException import ConflictError
from ftw.publisher.core import states, communication
from ftw.publisher.core.interfaces import IDataCollector
from ftw.publisher.receiver import decoder
from ftw.publisher.receiver import getLogger
from ftw.publisher.receiver.events import AfterCreatedEvent, AfterUpdatedEvent
from plone.app.uuid.utils import uuidToObject
from zope import event
from zope.component import getAdapters
from zope.event import notify
from zope.lifecycleevent import ObjectAddedEvent
from zope.publisher.interfaces import Retry
import os.path
import sys
import traceback
import plone.uuid
 
class ReceiveObject(BrowserView):
    """
    The ReceiveObject View is called be ftw.publisher.sender module.
    It expects a "jsondata"-paremeter in the request. It parses the jsondata
    and runs the specified action.
    """
 
    def __call__(self, *args, **kwargs):
        """
        @return:    response string containing a representation  of a
        CommunicationState as string.
        """
        # get a logger instance
        self.logger = getLogger()
        try:
            state = self.handleRequest()
        except Exception, e:
            if isinstance(e, states.CommunicationState):
                # if a CommunicationState is raised, we will use it as response
                state = e
            else:
                # otherwise we encapsulate the exception in a UnexpectedError
                exc = ''.join(traceback.format_exception(*sys.exc_info()))
                state = states.UnexpectedError(exc)
            self.logger.error('request failed: %s' % (state.toString()))
        # get the string representation of the CommunicationState
        resp = communication.createResponse(state)
        try:
            # if a exception was thrown, we add the traceback to the response
            resp += ''.join(traceback.format_exception(*sys.exc_info()))
        except:
            pass
        return resp
 
    def handleRequest(self):
        """
        Handles a request from ftw.publisher.sender. It parses the paremeter
        "jsondata" which should be in the request.
 
        @raise:     CommunicationState
        @return:    CommunicationState-Object
        """
        # get the jsondata
        jsondata = self.request.get('jsondata', None)
        self.logger.info(
            'Receiving request (data length: %i Byte)' % len(jsondata))
        if not jsondata:
            raise states.InvalidRequestError('No jsondata provided')
        # decode the jsondata to a python dictionary
        #XXX we do this now twice (also in core adapters)
        self.decoder = decoder.Decoder(self.context)
        self.data = self.decoder(jsondata)
        # get the action type ..
        action = self.getAction()
        # .. and run the action specific method ..
        if action=='push':
            # extract medata from data, we want to pass metadata
            # separatly to dataCollector adapters
            metadata = self.data['metadata']
            return self.pushAction(metadata, self.data)
        elif action == 'move':
            metadata = self.data['metadata']
            return self.moveAction(metadata, self.data)
        elif action=='delete':
            metadata = self.data['metadata']
            return self.deleteAction(metadata)
        else:
            # ... or raise a UnexpectedError
            raise states.UnknownActionError()
        # should not get here
        raise states.UnexpectedError()
 
    def getAction(self):
        """
        Returns the action name of the current request.
 
        @rtype:     string
        @return:    action name ('push', 'move' or 'delete')
        """
        return self.data['metadata']['action']
 
    def pushAction(self, metadata, data):
        """
        Is called if the action is "push". It creates or updates a object
        (identifiers in metadata) with the given fielddata.
        For more infos about the provided data see
        ftw.publisher.sender.extrator
 
        @param metadata:        metadata dict containing UID, portal_type,
        action, physicalPath and sibling_positions
        @type metadata:         dict
        @param data:            dictionary contains the values collected by
        DataCollectors
        @type data:        dict
        @return:                CommunicationState instance
        @rtype:                 ftw.publisher.core.states.CommunicationState
        """
        # do we have to update or create? does the object already exist?
        # ... try it with the uid
        absPath = self._getAbsolutePath(metadata['physicalPath'])
        is_root = False
 
        object = self._getObjectByUID(metadata['UID'])
        if metadata['portal_type'] == 'Plone Site':
            object = self.context.portal_url.getPortalObject()
            is_root = True
 
        if object:
            # ... is it the right object?
            if '/'.join(object.getPhysicalPath()) != absPath:
                # wrong path -> try to get it by path
                # alias patch because thomas uses to index multiple objects
                # with different paths and the same UID in the reference
                # catalog -.-
                obj1 = object
                object = self._getObjectByPath(absPath)
 
                if not object:
                    # the object is in a wrong place, so lets try to move it
                    # where it belongs
                    current_path = '/'.join(obj1.getPhysicalPath())
                    expected_path = absPath
 
                    # if we can't fix it we will raise the followin exception
                    exception = states.UIDPathMismatchError({
                            'problem': 'path wrong',
                            'found path': current_path,
                            'expected path': expected_path,
                            })
 
                    # is it in the same place? -> rename
                    if current_path.split('/')[:-1] == \
                            expected_path.split('/')[:-1]:
                        putils = obj1.plone_utils
                        success, failure = putils.renameObjectsByPaths(
                            (current_path, ), (expected_path.split('/')[-1], ),
                            (obj1.Title(), ))
                        if failure:
                            raise exception
                        object = self._getObjectByPath(expected_path)
 
                    # .. so its in another place (another parent) -> move
                    else:
                        old_parent = obj1.aq_inner.aq_parent
                        try:
                            new_parent = self.context.restrictedTraverse(
                                '/'.join(expected_path.split('/')[:-1]))
                        except AttributeError:
                            raise exception
                        try:
                            cutted = old_parent.manage_cutObjects(obj1.id)
                            new_parent.manage_pasteObjects(cutted)
                        except (ConflictError, Retry):
                            raise
                        except:
                            raise exception
                        else:
                            object = self._getObjectByPath(expected_path)
 
 
                elif object.UID() != metadata['UID']:
                    raise states.UIDPathMismatchError({
                            'problem': 'UID wrong',
                            'found uid': object.UID(),
                            'expected uid': metadata['UID'],
                            })
 
 
        parent_modified_date = None
 
        # create the object if its not existing ...
        new_object = False
        if not object:
            self.logger.info(
                'Object with UID %s does not existing: creating new object'%(
                    metadata['UID'],
                    )
                )
            # ... find container
            container = self._findContainerObjectByPath(absPath)
            try:
                parent_modified_date = container.modified()
            except AttributeError:
                pass
            if not container:
                raise states.ErrorState('Could not find container of %s' %
                                        absPath)
            self.logger.info('... container: "%s" at %s' % (
                    container.Title(),
                    '/'.join(container.getPhysicalPath()),
                    ))
            # ... create object
            object = container.get(container.invokeFactory(
                    metadata['portal_type'],
                    metadata['id'],
                    ))
 
            if hasattr(aq_base(object), '_setUID'):
                object._setUID(metadata['UID'])
 
            else:
                setattr(object,
                        plone.uuid.interfaces.ATTRIBUTE_NAME,
                        metadata['UID'])
 
                notify(ObjectAddedEvent(object))
 
            #object.processForm()
            new_object = True
        else:
            try:
                parent_modified_date = object.aq_inner.aq_parent.modified()
            except AttributeError:
                pass
 
        # finalize
        if hasattr(aq_base(object), 'processForm'):
            object.processForm()
 
        if not is_root:
            # set review_state
            pm = self.context.portal_membership
            current_user = pm.getAuthenticatedMember().getId()
            wt = self.context.portal_workflow
            wf_ids = wt.getChainFor(object)
            state = metadata['review_state']
            if state and wf_ids:
                wf_id = wf_ids[0]
                comment = 'state set to: %s' % state
                wt.setStatusOf(wf_id, object, {'review_state': state,
                                               'action': state,
                                               'actor': current_user,
                                               'time': DateTime(),
                                               'comments': comment, })
                wf = wt.getWorkflowById(wf_id)
                wf.updateRoleMappingsFor(object)
 
        # updates all data with the registered adapters for IDataCollector
        adapters = getAdapters((object, ), IDataCollector)
        for name, adapter in adapters:
            data = self.decoder.unserializeFields(object, name)
            adapter.setData(data[name], metadata)
 
        # set object position
        if not is_root:
            self.updateObjectPosition(object, metadata)
 
        # reindex
        if not is_root:
            object.reindexObject()
 
        catalog_tool = self.context.portal_catalog
        # re-set the modification date - this must be the last modifying access
        if metadata.get('modified'):
            modifiedDate = DateTime(metadata['modified'])
            object.setModificationDate(modifiedDate)
            if not is_root:
                catalog_tool.catalog_object(object,
                                            '/'.join(object.getPhysicalPath()))
 
        if parent_modified_date:
            parent = object.aq_inner.aq_parent
            parent.setModificationDate(parent_modified_date)
            catalog_tool.catalog_object(object,
                                        '/'.join(object.getPhysicalPath()))
 
        # return the appropriate CommunicationState - notify events
        if new_object:
            event.notify(AfterCreatedEvent(object))
            return states.ObjectCreatedState()
        else:
            event.notify(AfterUpdatedEvent(object))
            return states.ObjectUpdatedState()
 
    def moveAction(self, metadata, data):
        """
        Move or rename object by the given data.
        'move' contains the following keys
        - newName
        - newParent
        - oldName
        - oldParent
        - newTitle
 
        @param metadata:        metadata dictionary containing UID,
        portal_type, action, physicalPath and sibling_positions
        @param data
        @type data              dict
        @type metadata:         dict
        @return:                CommunicationState instance
        @rtype:                 ftw.publisher.core.states.CommunicationState
        """
        # find the object
        object = self._getObjectByUID(metadata['UID'])
        if not object:
            raise states.ObjectNotFoundForMovingWarning()
 
        move_data = 'move' in data and data['move'] or None
        if not move_data:
            return states.PartialError('Missing move part')
 
        obj_path = '/'.join(object.getPhysicalPath())
 
        # check if object has moved by comparing the parents
        if move_data['newParent'] == move_data['oldParent']:
            # rename the object
            putils = object.plone_utils
            paths = [obj_path, ]
            new_ids = [move_data['newName'], ]
            new_titles = [move_data['newTitle'].encode('utf-8'), ]
            success, failure = putils.renameObjectsByPaths(
                paths,
                new_ids,
                new_titles)
            if failure:
                return states.CouldNotMoveError(
                    u'Object on %s could not be renamed/moved (%s)' % (
                        obj_path,
                        failure.get(obj_path).__str__()))
 
        else:
            #object has been moved
            portal_path = '/'.join(
                self.context.portal_url.getPortalObject().getPhysicalPath())
            old_parent_path = portal_path + move_data['oldParent']
            real_old_parent_path = '/'.join(object.aq_parent.getPhysicalPath())
            if old_parent_path == real_old_parent_path:
                # The object is where we expect it
                old_parent = object.restrictedTraverse(old_parent_path)
            else:
                # old parent does not exist
                # try to move the object by using the real_old_parent_path
                old_parent = object.restrictedTraverse(real_old_parent_path)
 
            try:
                new_parent = object.restrictedTraverse(
                    portal_path + move_data['newParent'])
                cutted = old_parent.manage_cutObjects(object.id)
                new_parent.manage_pasteObjects(cutted)
            except (KeyError, AttributeError):
                # The new parent does not exist.
                # Delete the object, because we cannot move it
                # Raise exception anyway...
                old_parent.manage_delObjects([object.id])
                return states.CouldNotMoveError(
                    u'Object on %s could not be renamed/moved (%s)' % (
                        obj_path,
                        'Target parent does not exist, source object deleted'))
 
 
        # return a ObjectMovedState() instance
        return states.ObjectMovedState()
 
    def deleteAction(self, metadata):
        """
        Deletes the object identified by metadata values.
 
        @param metadata:        metadata dict containing UID, portal_type,
        action, physicalPath and sibling_positions
        @type metadata:         dict
        @return:                CommunicationState instance
        @rtype:                 ftw.publisher.core.states.CommunicationState
        """
        # find the object
        object = self._getObjectByUID(metadata['UID'])
        if not object:
            raise states.ObjectNotFoundForDeletingWarning()
        # delete the object
        self.logger.info('Removing object with UID %s' % metadata['UID'])
        container = object.aq_inner.aq_parent
        container.manage_delObjects([object.id])
        # return a ObjectDeletedState() instance
        return states.ObjectDeletedState()
 
    def _getObjectByUID(self, uid):
        """
        Searches a Object by UID in the plone reference_catalog.
        If the Plone-Object could not be found, it will return None.
 
        @param uid:     UID of the object to search for
        @type uid:      string
        @return:        Plone-Object or None
        """
        return uuidToObject(uid)
 
    def _getObjectByPath(self, absolutePath):
        """
        Searches a Object by the absolute path of the object.
        Path example: /data-fs/ploneSite/folder/object
 
        @param absolutePath:    Absolute path to the object to search for
        @type absolutePath:     string
        @return:                Plone-Object or None
        """
        # plone site root is not in catalog ...
        portalObject = self.context.portal_url.getPortalObject()
        portalPath = '/'.join(portalObject.getPhysicalPath())
        if absolutePath==portalPath:
            # plone site root is searched, return it
            return portalObject
        # for any other object we use the catalog tool
        brains = self.context.portal_catalog({
                'path': {
                    'query': absolutePath,
                    'depth': 0,
                    },
                })
        if len(brains)==0:
            return None
        else:
            return brains[0].getObject()
 
    def _findContainerObjectByPath(self, absoluteObjectPath):
        """
        Searches the Container of the currently not existing Plone-Object which
        will be created.
 
        @param absoluteObjectPath:  Absolute path to the object (which does not
        exist yet
        @type absoluteObjectPath:   string
        @return:                    Folderish Plone-Object or None
        """
        # get the path to the "parent" object
        containerPath = os.path.dirname(absoluteObjectPath)
        # search and return the "parent" object
        return self._getObjectByPath(containerPath)
 
    def _getAbsolutePath(self, relativePath):
        """
        Converts a relative path (relative to Plone Site-Root) to a absolute
        Path (containing the full URI from zope-Root object).
        No path validation is done!
        Example:
        relativePath :=     /folder/object
        absolutePath :=     /ploneSite/folder/object
 
        @param relativePath:    Any relative path
        @type relativePath:     string
        @return:                Absolute Path
        @rtype:                 string
        """
        # get the portal path (path to plone site)
        portalObject = self.context.portal_url.getPortalObject()
        portalPath = '/'.join(portalObject.getPhysicalPath())
        # concatinate with portalPath with relativePath
        # handle plone root
        if relativePath == '/':
            return portalPath
        return portalPath + relativePath
 
    def updateObjectPosition(self, object, metadata):
        """
        Updates the position of the object and it siblings (reset position of
        all children of the parent object).
        Objects, which do not exist at the sender instance, are moved to the
        bottom.
 
        @param metadata:        metadata dict containing UID, portal_type,
        action, physicalPath and sibling_positions
        @type metadata:         dict
        @return:                None
        """
        positions = metadata['sibling_positions']
        parent = object.aq_inner.aq_parent
        object_ids = list(parent.objectIds())
 
        # move objects with no position info to the bottom
        for id in object_ids:
            if id not in positions.keys():
                positions[id] = len(positions.keys())
 
        # sort ids by positions
        object_ids.sort(lambda a, b: cmp(positions[a], positions[b]))
 
        # order objects
        parent.moveObjectsByDelta(object_ids, -len(object_ids))
 
 
        # reindex all objects
        for id in object_ids:
            try:
                parent.get(id).reindexObject(
                    idxs=['positionInParent', 'getObjPositionInParent'])
            except:
                pass
 
 
class TestConnection(BrowserView):
    """
    This BrowserView is used by the configlet of the module
    ftw.publisher.sender
    to test a connection to the receiever.
    """
 
    def __call__(self, *args, **kwargs):
        """
        Returns a 'ok' if the context is a Plone Site, otherwise
        it returns 'Not a Plone-Site'.
        @return:        Success message
        @rtype:         string
        """
        if IPloneSiteRoot.providedBy(self.context):
            return 'ok'
        else:
            return 'Not a Plone-Site'