pt.ist.maidSyncher.domain.MaidRoot.java Source code

Java tutorial

Introduction

Here is the source code for pt.ist.maidSyncher.domain.MaidRoot.java

Source

/*******************************************************************************
 * Copyright (c) 2013 Instituto Superior Tcnico - Joo Antunes
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     Luis Silva - ACGHSync
 *     Joo Antunes - initial API and implementation
 ******************************************************************************/
package pt.ist.maidSyncher.domain;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pt.ist.bennu.core.domain.Bennu;
import pt.ist.fenixframework.Atomic;
import pt.ist.fenixframework.Atomic.TxMode;
import pt.ist.fenixframework.FenixFramework;
import pt.ist.maidSyncher.api.activeCollab.ACContext;
import pt.ist.maidSyncher.domain.activeCollab.ACInstance;
import pt.ist.maidSyncher.domain.activeCollab.ACTaskCategory;
import pt.ist.maidSyncher.domain.dsi.DSIObject;
import pt.ist.maidSyncher.domain.exceptions.SyncActionError;
import pt.ist.maidSyncher.domain.exceptions.SyncEventIllegalConflict;
import pt.ist.maidSyncher.domain.github.GHOrganization;
import pt.ist.maidSyncher.domain.sync.SyncActionWrapper;
import pt.ist.maidSyncher.domain.sync.SyncEvent;
import pt.ist.maidSyncher.domain.sync.SyncEvent.SyncUniverse;
import pt.ist.maidSyncher.domain.sync.SyncEvent.TypeOfChangeEvent;
import pt.ist.maidSyncher.domain.sync.logs.SyncActionLog;
import pt.ist.maidSyncher.domain.sync.logs.SyncEventConflictLog;
import pt.ist.maidSyncher.domain.sync.logs.SyncLog;
import pt.ist.maidSyncher.domain.sync.logs.SyncWarningLog;
import pt.utl.ist.fenix.tools.util.Strings;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.gag.annotation.remark.ShoutOutTo;

public class MaidRoot extends MaidRoot_Base {

    public static final String AC_SERVER_BASE_URL = "ac.server.baseUrl";

    private static Multimap<DSIObject, SyncEvent> changesBuzz = HashMultimap.create();

    private static GitHubClient gitHubClient;

    private static Properties configurationProperties;

    private static final Logger LOGGER = LoggerFactory.getLogger(MaidRoot.class);

    private static void initProperties() {
        configurationProperties = new Properties();
        InputStream configurationInputStream = MaidRoot.class.getResourceAsStream("/configuration.properties");
        if (configurationInputStream != null) {
            try {
                configurationProperties.load(configurationInputStream);
            } catch (IOException e) {
                throw new Error(e);
            }
        }
    }

    static {
        if (configurationProperties == null || configurationProperties.isEmpty()) {
            initProperties();
        }
        ACContext acContext = ACContext.getInstance();

        if (acContext.getServerBaseUrl() == null || StringUtils.isBlank(acContext.getServerBaseUrl())) {
            acContext.setServerBaseUrl(configurationProperties.getProperty(AC_SERVER_BASE_URL));
            acContext.setToken(configurationProperties.getProperty("ac.server.token"));
        }
        if (getGitHubClient() == null) {
            //let's try to connect to the GH Account

            //let's try to authenticate and get the user and repository list
            setGitHubClient(new GitHubClient());

            String oauth2Token = configurationProperties.getProperty("github.oauth2.token");
            getGitHubClient().setOAuth2Token(oauth2Token);
        }
    }

    @Atomic
    public static void initializeBennu() {
        if (FenixFramework.getDomainRoot() == null || FenixFramework.getDomainRoot().getBennu() == null) {
            Bennu bennu = new Bennu();
        }
    }

    public static MaidRoot getInstance() {
        if (FenixFramework.getDomainRoot() == null || FenixFramework.getDomainRoot().getBennu() == null) {
            //this shouldn't happen, but it does because Bennu2 doesn't correctly
            //implement the same singleton pattern as this class
            initializeBennu();
        }
        if (Bennu.getInstance().getMaidRoot() == null) {
            initialize();
        }
        return Bennu.getInstance().getMaidRoot();
    }

    @Atomic
    private static void initialize() {
        if (Bennu.getInstance().getMaidRoot() == null) {
            Bennu.getInstance().setMaidRoot(new MaidRoot());
            getInstance().init();
        }
    }

    public MaidRoot() {
        super();
        Bennu.getInstance().setMaidRoot(this);
        checkIfIsSingleton();
        setRepositoriesToIgnore(new Strings(Collections.<String>emptyList()));
    }

    public void init() {
        GHOrganization ghOrganization = getGhOrganization();
        if (ghOrganization == null)
            setGhOrganization(new GHOrganization());
        ACInstance acInstance = getAcInstance();
        if (acInstance == null)
            setAcInstance(new ACInstance(this));

    }

    private void checkIfIsSingleton() {
        MaidRoot maidRoot = Bennu.getInstance().getMaidRoot();
        if (maidRoot != null && maidRoot != this) {
            throw new Error("There can only be one! (instance of MyOrg [aka MaidRoot])");
        }
    }

    /**
     * Clears the {@link #changesBuzz}
     * 
     */
    public void resetSyncEvents() {
        this.changesBuzz = HashMultimap.create();

    }

    private void checkChangesBuzzApiObjectsAreEqual() {
        //TODO issue #6
        //        //ok, let's iterate through all of them
        //        for (DSIObject keyDSIObject  : changesBuzz.keySet())
        //        {
        //            List<ACObject> acObjects = new ArrayList<>();
        //            List<Object> ghObjects = new ArrayList<>();
        //            for (SyncEvent syncEvent : changesBuzz.get(keyDSIObject))
        //            {
        //                //let's get the events different api objects
        //                //and check afterwards if they are the same or not
        //                syncEvent.getApiObjectWrapper().getAPIObject()
        //            }
        //        }

    }

    /**
     * Adds the given {@link SyncEvent} to the list of sync events.
     * 
     * <p>
     * It also looks out for possible errors, and throws a {@link SyncEventIllegalConflict} if they are found.
     * </p>
     * 
     * <p>
     * The behavior of the method, is as follows:
     * </p>
     * <p>
     * Checks for the given {@link SyncEvent#getDsiElement()} if there are other events, if there are the behaviour is as follows:
     * </p>
     * <ul>
     * <li>If two {@link TypeOfChangeEvent} events of the type {@link TypeOfChangeEvent#CREATE} are found, with different
     * {@link SyncUniverse}s, an error is thrown;</li>
     * <li>Two {@link TypeOfChangeEvent} of type {@link TypeOfChangeEvent#CREATE}, but with different {@link SyncUniverse}
     * targets, we simply log that, and remove both events (in the end, this means that both objects are already created on both
     * universes)</li>
     * <li>A {@link TypeOfChangeEvent#READ} is neutral (although several reads of the same target, could be squashed to just one)</li>
     * <li>If we get a {@link TypeOfChangeEvent#CREATE} event, and an {@link TypeOfChangeEvent#UPDATE} one, we are conservative
     * and throw an error (as they shouldn't occur for now, because we either create a new object on the other side, or we need to
     * update it) TODO on each case that occurs, see what really happen and change this behaviour</li>
     * <li>If we get an {@value TypeOfChangeEvent#UPDATE} longside with an {@link TypeOfChangeEvent#UPDATE} or
     * {@link TypeOfChangeEvent#DELETE}, we have a conflict solved by date, i.e. the one that wins is the one with the most recent
     * {@link SyncEvent#getDateOfChange()}</li>
     * 
     * 
     * 
     * 
     * 
     * @throws SyncEventIllegalConflict if there are sync errors, see the behavior above
     * @param syncEvent
     */
    protected void addSyncEvent(SyncEvent syncEvent) throws SyncEventIllegalConflict {
        DSIObject dsiElement = syncEvent.getDsiElement();
        boolean addEvent = true;
        List<SyncEvent> syncEventsToDelete = new ArrayList<>();
        if (dsiElement == null) {
            changesBuzz.put(null, syncEvent);
        } else if (changesBuzz.containsKey(dsiElement)) {
            scanExistingEvents: for (SyncEvent syncEventAlreadyPresent : changesBuzz.get(dsiElement)) {
                switch (syncEvent.getTypeOfChangeEvent()) {
                case CREATE:

                    /* CREATE */
                    switch (syncEventAlreadyPresent.getTypeOfChangeEvent()) {
                    case CREATE:
                        if (syncEvent.getTargetSyncUniverse()
                                .equals(syncEventAlreadyPresent.getTargetSyncUniverse())) {
                            throw new SyncEventIllegalConflict(
                                    "Two sync events of the type Create for the DSIObject: "
                                            + dsiElement.getExternalId() + " with class: "
                                            + dsiElement.getClass().getName() + " were detected");
                        } else {
                            //let's remove the one that exists and do not add this one
                            addEvent = false;
                            syncEventsToDelete.add(syncEventAlreadyPresent);
                            break scanExistingEvents;
                        }
                    case READ:
                        break;
                    case UPDATE:
                        throwSyncUpdateAndCreateConflictException(dsiElement);
                    case DELETE:
                        //let's be conservative for now and throw an exception
                        //(this case might be 'legal' and lead to the whoever has the most
                        //recent date to prevail over the other, but let's see this case by case)
                        throw new SyncEventIllegalConflict(
                                "A sync event of Create and Delete over the same DSIObject: "
                                        + dsiElement.getExternalId() + " class: " + dsiElement.getClass().getName()
                                        + " was detected");
                    }
                    break;
                /* end of CREATE */

                case READ:
                    //the READs are pretty much neutral
                    break;
                case UPDATE:
                    switch (syncEventAlreadyPresent.getTypeOfChangeEvent()) {
                    case CREATE:
                        throwSyncUpdateAndCreateConflictException(dsiElement);
                        break;
                    case READ:
                        break;
                    case UPDATE:
                    case DELETE:
                        //let's see the one that prevails, based on the date
                        addEvent = processUpdateAndDeleteOrUpdate(syncEvent, syncEventAlreadyPresent,
                                syncEventsToDelete);
                        break scanExistingEvents;
                    }
                    break;
                case DELETE:
                    switch (syncEventAlreadyPresent.getTypeOfChangeEvent()) {
                    case CREATE:
                        //for now, let's throw an error, but this might be pheasible
                        throw new SyncEventIllegalConflict("a Delete with a Create was detected. dsiElement: "
                                + dsiElement.getExternalId() + " class: " + dsiElement.getClass().getSimpleName());
                    case READ:
                        break;
                    case UPDATE:
                    case DELETE:
                        //let's see the one that prevails, based on the date
                        addEvent = processUpdateAndDeleteOrUpdate(syncEvent, syncEventAlreadyPresent,
                                syncEventsToDelete);
                        break scanExistingEvents;
                    }

                    break;
                }

            }

            //so, let's add if we have to, and delete the ones we should
            if (addEvent) {
                changesBuzz.put(dsiElement, syncEvent);
            }
            for (SyncEvent syncEventToDelete : syncEventsToDelete) {
                changesBuzz.remove(dsiElement, syncEventToDelete);
                syncEventToDelete.delete();
            }

        } else {
            changesBuzz.put(dsiElement, syncEvent);
        }
    }

    private boolean processUpdateAndDeleteOrUpdate(SyncEvent syncEvent, SyncEvent syncEventAlreadyPresent,
            List<SyncEvent> syncEventsToDelete) {
        //let's make sure this is really a conflict

        //if the two syncEvents are to be applied on the same target universe
        if (syncEventAlreadyPresent.getTargetSyncUniverse().equals(syncEvent.getTargetSyncUniverse())) {
            //and both are for the ACTaskCategory, it's not a conflict at all (as long as it's not the same category)
            if (syncEvent.getOriginObject().getClass().equals(ACTaskCategory.class)
                    && syncEventAlreadyPresent.getOriginObject().getClass().equals(ACTaskCategory.class)) {
                if (!ObjectUtils.equals(syncEvent.getOriginObject(), syncEventAlreadyPresent.getOriginObject())) {
                    return true;
                }
            }
        }
        SyncEventConflictLog syncEventConflictLog = new SyncEventConflictLog(syncEvent, syncEventAlreadyPresent);
        MaidRoot.getInstance().getCurrentSyncLog().addSyncConflictLogs(syncEventConflictLog);
        if (syncEvent.getDateOfChange().compareTo(syncEventAlreadyPresent.getDateOfChange()) <= 0) {
            //the already present is more recent
            syncEventConflictLog.markSecondAsWinner();
            return false;
        } else {
            //the update, the new event, is more recent
            //let's delete this already present one
            syncEventsToDelete.add(syncEventAlreadyPresent);
            syncEventConflictLog.markFirstAsWinner();
            return true;
        }
    }

    private void throwSyncUpdateAndCreateConflictException(DSIObject dsiElement) throws SyncEventIllegalConflict {
        throw new SyncEventIllegalConflict("A sync event of the type Create and Update for the DSIObject: "
                + dsiElement.getExternalId() + " class: " + dsiElement.getClass().getName() + " were detected");

    }

    public static Multimap<DSIObject, SyncEvent> getChangesBuzz() {
        return changesBuzz;
    }

    public static GitHubClient getGitHubClient() {
        return gitHubClient;
    }

    //    public void fullySync(User repository) {
    //        checkNotNull(repository, "repository must not be null");
    //        checkArgument(repository.getType().equals(User.TYPE_ORG), "You must provide a repository");
    //
    //    }

    public static void setGitHubClient(GitHubClient gitHubClient) {
        MaidRoot.gitHubClient = gitHubClient;
    }

    private static class SyncWrapper {
        final DSIObject dsiObject;
        private final Set<SyncActionWrapper<? extends SynchableObject>> actionWrappers = new HashSet<>();

        public SyncWrapper(DSIObject dsiObject) {
            this.dsiObject = dsiObject;
        }

        //        @Override
        //        public boolean equals(Object obj) {
        //            if (obj == null) {
        //                return false;
        //            }
        //            if ((obj instanceof SyncWrapper) == false)
        //                return false;
        //            SyncWrapper syncWrapperToCompareWith = (SyncWrapper) obj;
        //            return ObjectUtils.equals(dsiObject, syncWrapperToCompareWith.dsiObject);
        //        }
        //
        //        @Override
        //        public int hashCode() {
        //            return ObjectUtils.hashCode(dsiObject);
        //        }

        public void addSyncAction(SyncActionWrapper<? extends SynchableObject> syncActionWrapper) {
            this.getActionWrappers().add(syncActionWrapper);
        }

        public int getNumberSyncActions() {
            return this.getActionWrappers().size();
        }

        public static int getNumberSyncActions(Collection<SyncWrapper> syncWrappers) {
            int number = 0;
            for (SyncWrapper syncWrapper : syncWrappers) {
                number += syncWrapper.getNumberSyncActions();
            }
            return number;
        }

        public static Set<DSIObject> getDSIObjects(Collection<SyncWrapper> syncWrappers) {
            Set<DSIObject> dsiObjects = new HashSet<>();
            for (SyncWrapper syncWrapper : syncWrappers) {
                dsiObjects.add(syncWrapper.dsiObject);
            }
            return dsiObjects;
        }

        /**
         * Processes the SyncActions contained in the {@link #actionWrappers}. If all are consumed, true is returned
         * 
         * @return true if everything was processed, false otherwise
         * @throws Throwable
         */
        @Atomic(mode = TxMode.READ)
        public boolean process(Set<DSIObject> dsiObjectsToSync) {
            Iterator<SyncActionWrapper<? extends SynchableObject>> actionWrappersIterator = getActionWrappers()
                    .iterator();
            while (actionWrappersIterator.hasNext()) {
                SyncActionWrapper<? extends SynchableObject> syncActionWrapper = actionWrappersIterator.next();
                if (SyncEvent.isAbleToRunNow(syncActionWrapper, dsiObjectsToSync)) {
                    SyncActionLog syncActionLog = logSyncStart(syncActionWrapper);
                    try {
                        Set<SynchableObject> changedObjects = atomicProcessSyncAction(syncActionWrapper);
                        logSyncSuccessAndDeleteSyncEvent(syncActionLog, syncActionWrapper, changedObjects);
                        actionWrappersIterator.remove();
                    } catch (Exception ex) {
                        logSyncFailure(syncActionLog, ex);
                        throw new Error(ex);

                    }
                }
            }
            return getActionWrappers().isEmpty();
        }

        @Atomic(mode = TxMode.WRITE)
        private void logSyncFailure(SyncActionLog syncActionLog, Exception ex) {
            if (ex instanceof SyncActionError) {
                syncActionLog.markExceptionAndEndOfSync((SyncActionError) ex);
            } else {
                syncActionLog.markEndOfSync(false, ExceptionUtils.getFullStackTrace(ex));

            }
        }

        @Atomic(mode = TxMode.WRITE)
        private SyncActionLog logSyncStart(SyncActionWrapper<? extends SynchableObject> syncActionWrapper) {
            SyncLog currentSyncLog = MaidRoot.getInstance().getCurrentSyncLog();
            System.out.println("current " + currentSyncLog == null);
            System.out.println("syncActionWrapper " + syncActionWrapper == null);
            System.out.println("syncActionWrapper event " + syncActionWrapper.getOriginatingSyncEvent() == null);

            SyncActionLog syncActionLog = new SyncActionLog(currentSyncLog,
                    syncActionWrapper.getOriginatingSyncEvent(),
                    syncActionWrapper.getOriginatingSyncEvent().getDsiElement());
            syncActionLog.markStartOfSync();
            return syncActionLog;

        }

        @Atomic(mode = TxMode.WRITE)
        private void logSyncSuccessAndDeleteSyncEvent(SyncActionLog syncActionLog,
                SyncActionWrapper<? extends SynchableObject> syncActionWrapper,
                Set<SynchableObject> changedObjects) {
            MaidRoot.getInstance().getSyncEventsToProcessSet().remove(syncActionWrapper.getOriginatingSyncEvent());
            syncActionLog.markEndOfSync(true);
            syncActionWrapper.getOriginatingSyncEvent().delete();
            syncActionLog.getChangedObjectsSet().addAll(SynchableObject.getLogRepresentation(changedObjects));

        }

        @Atomic(mode = TxMode.WRITE)
        private Set<SynchableObject> atomicProcessSyncAction(SyncActionWrapper syncActionWrapper)
                throws IOException {
            LOGGER.info("Running SyncActionWrapper for event: "
                    + syncActionWrapper.getOriginatingSyncEvent().toString());
            Set<SynchableObject> changedObjects = syncActionWrapper.sync();
            syncActionWrapper.getOriginatingSyncEvent().getDsiElement().setLastSynchedAt(new DateTime());
            return changedObjects;
        }

        public Set<SyncActionWrapper<? extends SynchableObject>> getActionWrappers() {
            return actionWrappers;
        }
    }

    @Atomic(mode = TxMode.READ)
    public void applyChangesBuzz() {
        if (getSyncEventsToProcessSet().isEmpty() == false) {
            throw new IllegalStateException("The maidRoot still has SyncEvents to "
                    + "process, that should be processed before calling this method");
        }
        Map<DSIObject, Collection<SyncEvent>> changesMap = getChangesBuzz().asMap();
        Set<SyncWrapper> syncWrappers = createSyncWrappers(changesMap);

        registerNumberGeneratedSyncActions(syncWrappers);

        //after everything is validated, let's add the SyncEvents to the
        //to process queue of the MaidRoot
        addSyncEventsToProcessQueue(syncWrappers);

        processSyncWrappers(syncWrappers);

        LOGGER.info("Applied all SyncActions");

        getCurrentSyncLog().markAsSuccess();

    }

    private void processSyncWrappers(Set<SyncWrapper> syncWrappers) {
        while (syncWrappers.isEmpty() == false) {
            Iterator<SyncWrapper> syncWrappersIterator = syncWrappers.iterator();
            while (syncWrappersIterator.hasNext()) {
                SyncWrapper syncWrapper = syncWrappersIterator.next();

                if (syncWrapper.process(SyncWrapper.getDSIObjects(syncWrappers))) {
                    //then we can remove it
                    syncWrappersIterator.remove();
                }
            }
        }

    }

    /**
     * Tries to process any remaining {@link SyncEvent} that might still be
     * in {@link #getSyncEventsToProcessSet()}.
     * 
     * @throws Throwable
     * 
     */
    @Atomic(mode = TxMode.READ)
    public void processRemainingInstances() {
        //based on the events we have, let's create the map
        Set<SyncWrapper> syncWrappers = createSyncWrappers(getMultiMapFromSyncEvents(getSyncEventsSet()).asMap());

        registerNumberOfGeneratedSyncActionsForUnprocessedEvents(syncWrappers);

        processSyncWrappers(syncWrappers);

        LOGGER.info("Applied all SyncActions of the unprocessed SyncEvents");

    }

    @Atomic(mode = TxMode.WRITE)
    private void registerNumberOfGeneratedSyncActionsForUnprocessedEvents(Set<SyncWrapper> syncWrappers) {
        int numberSyncActions = SyncWrapper.getNumberSyncActions(syncWrappers);
        getCurrentSyncLog().setNrGeneratedSyncActionsFromRemainingSyncEvents(numberSyncActions);
        LOGGER.info("Number of SyncActions from unprocessed Sync Events Generated: " + numberSyncActions);

    }

    private Set<SyncWrapper> createSyncWrappers(Map<DSIObject, Collection<SyncEvent>> changesMap) {
        Set<SyncWrapper> syncWrappers = new HashSet<>();

        for (DSIObject dsiObject : changesMap.keySet()) {
            SyncWrapper syncWrapper = new SyncWrapper(dsiObject);
            for (SyncEvent syncEvent : changesMap.get(dsiObject)) {
                SyncActionWrapper syncActionWrapper = syncEvent.getOriginObject().sync(syncEvent);
                if (syncActionWrapper != null) {
                    validate(syncActionWrapper, syncEvent);
                    syncWrapper.addSyncAction(syncActionWrapper);
                }
            }
            syncWrappers.add(syncWrapper);
        }
        return syncWrappers;
    }

    private static Multimap<DSIObject, SyncEvent> getMultiMapFromSyncEvents(Collection<SyncEvent> syncEvents) {
        Multimap<DSIObject, SyncEvent> multimap = HashMultimap.create();
        for (SyncEvent syncEvent : syncEvents) {
            multimap.put(syncEvent.getDsiElement(), syncEvent);
        }
        return multimap;
    }

    @Atomic(mode = TxMode.WRITE)
    private void addSyncEventsToProcessQueue(Set<SyncWrapper> syncWrappers) {
        for (SyncWrapper syncWrapper : syncWrappers) {
            for (SyncActionWrapper<? extends SynchableObject> syncActionWrapper : syncWrapper.getActionWrappers()) {
                getSyncEventsToProcessSet().add(syncActionWrapper.getOriginatingSyncEvent());
            }
        }

    }

    @Atomic(mode = TxMode.WRITE)
    private void registerNumberGeneratedSyncActions(Set<SyncWrapper> syncWrappers) {
        int numberSyncActions = SyncWrapper.getNumberSyncActions(syncWrappers);
        getCurrentSyncLog().setNrGeneratedSyncActions(numberSyncActions);
        LOGGER.info("Number of SyncActions Generated: " + numberSyncActions);

    }

    /**
     * 
     * @param syncActionWrapper {@link SyncActionWrapper} to validate
     * @throws Exception if there is any kind of problem with it
     *             It verifies the syncActionWrapper for an originationg sync event;
     *             That the lists are not null, that all of the property descriptors were ticked, etc;
     */
    @ShoutOutTo(value = { "the guy that thought that including a null was a good idea" })
    private void validate(SyncActionWrapper syncActionWrapper, SyncEvent syncEvent) {
        checkNotNull(syncActionWrapper.getOriginatingSyncEvent());
        checkNotNull(syncActionWrapper.getOriginatingSyncEvent().getOriginObject());
        checkNotNull(syncActionWrapper.getOriginatingSyncEvent().getDsiElement());
        checkNotNull(syncActionWrapper.getPropertyDescriptorNamesTicked());
        try {
            checkNotNull(syncActionWrapper.getSyncDependedDSIObjects());
        } catch (NullPointerException ex) {
            //An NPE might occur here, but that doesn't mean that this is invalid
        }
        checkNotNull(syncActionWrapper.getSyncDependedTypesOfDSIObjects());

        if (syncActionWrapper.getOriginatingSyncEvent().equals(syncEvent) == false) {
            throw new IllegalArgumentException("Sync events from wrapper and original event differ");
        }

        Collection<String> propertyDescriptorsTicked = syncActionWrapper.getPropertyDescriptorNamesTicked();
        Collection<String> changedPropertyDescriptors = syncActionWrapper.getOriginatingSyncEvent()
                .getChangedPropertyDescriptorNames().getUnmodifiableList();
        if (!propertyDescriptorsTicked.containsAll(changedPropertyDescriptors)) {
            //let's generate a log
            registerPropertyDescriptorNotConsideredWarning(changedPropertyDescriptors, propertyDescriptorsTicked,
                    syncEvent);
        }

    }

    @SuppressWarnings("unused")
    @Atomic(mode = TxMode.WRITE)
    private static void registerPropertyDescriptorNotConsideredWarning(
            Collection<String> changedPropertyDescriptors, Collection<String> propertyDescriptorsTicked,
            SyncEvent syncEvent) {
        StringBuilder exceptionMessageBuilder = new StringBuilder();
        exceptionMessageBuilder
                .append("One didn't consider a property descriptor change. Property descriptors not ticked: ");
        HashSet<String> copyOfChangedDescriptors = new HashSet<>(changedPropertyDescriptors);
        copyOfChangedDescriptors.removeAll(propertyDescriptorsTicked);
        for (String propertyDescriptor : copyOfChangedDescriptors) {
            exceptionMessageBuilder.append(propertyDescriptor + " ");
        }
        if (syncEvent != null && syncEvent.getOriginObject() != null) {
            exceptionMessageBuilder
                    .append("Class of origin object: " + syncEvent.getOriginObject().getClass().getName());

        } else {
            exceptionMessageBuilder.append("Class of Origin object unknown because syncEvent is null.");
        }

        new SyncWarningLog(MaidRoot.getInstance().getCurrentSyncLog(), exceptionMessageBuilder.toString());

    }

}