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

Java tutorial

Introduction

Here is the source code for pt.ist.maidSyncher.domain.SynchableObject.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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.beans.IndexedPropertyDescriptor;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ObjectUtils;
import org.eclipse.egit.github.core.client.GitHubRequest;
import org.joda.time.DateTime;
import org.joda.time.LocalTime;
import org.json.simple.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pt.ist.fenixframework.Atomic;
import pt.ist.fenixframework.Atomic.TxMode;
import pt.ist.fenixframework.DomainObject;
import pt.ist.fenixframework.core.WriteOnReadError;
import pt.ist.maidSyncher.api.activeCollab.ACContext;
import pt.ist.maidSyncher.api.activeCollab.ACObject;
import pt.ist.maidSyncher.api.activeCollab.interfaces.RequestProcessor;
import pt.ist.maidSyncher.domain.activeCollab.exceptions.TaskNotVisibleException;
import pt.ist.maidSyncher.domain.dsi.DSIObject;
import pt.ist.maidSyncher.domain.exceptions.SyncEventIllegalConflict;
import pt.ist.maidSyncher.domain.exceptions.SyncEventOriginObjectChanged;
import pt.ist.maidSyncher.domain.github.GHObject;
import pt.ist.maidSyncher.domain.sync.APIObjectWrapper;
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.ChangedObjectLog;
import pt.ist.maidSyncher.utils.MiscUtils;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;

public abstract class SynchableObject extends SynchableObject_Base {

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

    //The fields for operations with PropertyDescriptor s
    public static final String DSC_LAST_SYNC_TIME = "lastSynchTime"; //not used
    public static final String DSC_ID = "id";
    public static final String DSC_URL = "url";

    public SynchableObject() {
        super();
    }

    public abstract DSIObject getDSIObject();

    public abstract DSIObject findOrCreateDSIObject();

    static void checkProccessPreconditions(Class clazz, Object object) {
        checkNotNull(object);
        checkArgument(clazz.isAssignableFrom(object.getClass()),
                "Object's class must be a class (or superclass) of " + clazz.getName());
    }

    @Atomic(mode = TxMode.WRITE)
    public static Set<ChangedObjectLog> getLogRepresentation(Set<SynchableObject> synchableObjects) {
        Set<ChangedObjectLog> changedObjectLogs = new HashSet<>();
        for (SynchableObject synchableObject : synchableObjects) {
            changedObjectLogs.add(new ChangedObjectLog(synchableObject));
        }
        return changedObjectLogs;
    }

    public static enum ObjectFindStrategy {
        FIND_BY_ID {
            @Override
            public Optional<SynchableObject> find(Object object, Class<? extends SynchableObject> clazz,
                    Iterable iterable) {
                long id;
                try {
                    id = Long.valueOf(object.getClass().getMethod("getId").invoke(object).toString());
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
                        | NoSuchMethodException | SecurityException e1) {
                    throw new UnsupportedOperationException(
                            "Class: " + object.getClass().getName() + " has no usable getter for an id");
                }

                return Iterables.tryFind(iterable, new PredicateFindGHObjectByClassAndId(clazz, id));
            }
        },
        FIND_BY_URL {
            @Override
            public Optional<SynchableObject> find(Object object, Class<? extends SynchableObject> clazz,
                    Iterable iterable) {

                String url;
                try {
                    url = (String) object.getClass().getMethod("getUrl").invoke(object);
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
                        | NoSuchMethodException | SecurityException e1) {
                    throw new UnsupportedOperationException(
                            "Class: " + object.getClass().getName() + " has no usable getter for an url");
                }

                try {
                    return Iterables.tryFind(iterable, new PredicateFindGHObjectByClassAndUrl(clazz, url));
                } catch (MalformedURLException e) {
                    LOGGER.error("trying to find an object by url with a malformed url on the origin object"
                            + " origin object class: " + object.getClass().getName() + " url: " + url, e);
                    throw new IllegalArgumentException(e);
                }

            }
        };

        public static class PredicateFindGHObjectByClassAndId implements Predicate<SynchableObject> {

            private final Class<? extends SynchableObject> clazz;
            private final long id;

            public PredicateFindGHObjectByClassAndId(Class<? extends SynchableObject> clazz, long id) {
                checkNotNull(clazz);
                checkArgument(id > 0, "The id must be > 0");
                this.clazz = clazz;
                this.id = id;
            }

            @Override
            public boolean apply(SynchableObject input) {
                if (input == null)
                    return false;
                if (this.clazz.isAssignableFrom(input.getClass()) && input.getId() == id)
                    return true;
                return false;
            }

        }

        public static class PredicateFindGHObjectByClassAndUrl implements Predicate<SynchableObject> {

            private final Class<? extends SynchableObject> clazz;
            private final URL url;

            public PredicateFindGHObjectByClassAndUrl(Class<? extends SynchableObject> clazz, String url)
                    throws MalformedURLException {
                checkNotNull(clazz);
                this.url = new URL(url);
                this.clazz = clazz;
            }

            @Override
            public boolean apply(SynchableObject input) {
                if (input == null)
                    return false;
                URL urlOfInput = null;
                try {
                    urlOfInput = new URL(input.getUrl());
                } catch (MalformedURLException ex) {
                    LOGGER.warn("Found a persisted GHObject with a malformed URL. Object: " + input.getExternalId(),
                            ex);
                    return false;
                }
                if (this.clazz.isAssignableFrom(input.getClass()) && this.url.equals(urlOfInput))
                    return true;
                return false;
            }

        }

        public abstract Optional<SynchableObject> find(Object object, Class<? extends SynchableObject> clazz,
                Iterable iterable);

    }

    protected static SynchableObject findOrCreateAndProccess(Object object, Class<? extends SynchableObject> clazz,
            Iterable iterable, boolean skipGenerateSyncEvent, ObjectFindStrategy... optionalObjectFindStrategy)
            throws SyncEventIllegalConflict {
        SynchableObject toProccessAndReturn = null;
        checkNotNull(object);
        checkNotNull(iterable);
        //        checkArgument(clazz.isAssignableFrom(object.getClass()),
        //                "Object's class must be a class (or superclass) of " + clazz.getName());

        ObjectFindStrategy optionalObjectFindStrategyToBeUsed = null;
        if (optionalObjectFindStrategy == null || optionalObjectFindStrategy.length == 0) {
            optionalObjectFindStrategyToBeUsed = ObjectFindStrategy.FIND_BY_ID;
        } else if (optionalObjectFindStrategy.length >= 1) {
            optionalObjectFindStrategyToBeUsed = optionalObjectFindStrategy[0];
        }

        Optional<SynchableObject> synchableObject = optionalObjectFindStrategyToBeUsed.find(object, clazz,
                iterable);
        if (synchableObject.isPresent()) {
            toProccessAndReturn = synchableObject.get();
        } else {
            //let's create it
            try {
                toProccessAndReturn = clazz.getConstructor().newInstance();
            } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException | NoSuchMethodException | SecurityException e) {
                if (e.getCause() instanceof WriteOnReadError)
                    throw (WriteOnReadError) e.getCause();
                throw new UnsupportedOperationException(
                        "Class: " + clazz.getName() + " has no usable, simple, e.g. Constructor() constructor", e);
            }
        }

        //let's copy the values
        HashSet<String> changedDescriptors = new HashSet<>();
        try {
            changedDescriptors.addAll(toProccessAndReturn.copyPropertiesFrom(object));
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            if (e.getCause() instanceof WriteOnReadError)
                throw ((WriteOnReadError) e.getCause());
            throw new IllegalArgumentException("There were problems copying properties from object of class "
                    + object.getClass().getName() + " to " + toProccessAndReturn.getClass().getName() + " oid: "
                    + toProccessAndReturn.getExternalId(), e);
        }

        if (!skipGenerateSyncEvent && changedDescriptors.isEmpty() == false) {
            //we changed something, let's create and add the syncEvent to the ChangesBuzz
            generateSyncEvent(toProccessAndReturn, changedDescriptors, object);
        } else if (skipGenerateSyncEvent == true && toProccessAndReturn.getDSIObject() == null) {
            //we should create the DSIObject on this file
            try {
                toProccessAndReturn.findOrCreateDSIObject();
            } catch (UnsupportedOperationException ex) {
                //continue silently
            }

        }

        return toProccessAndReturn;

    }

    /**
     * 
     * @param object the object from the API containing all the data
     * @param clazz the class of the object to find
     * @param iterable the iterable where to find it
     * @return
     * @throws SyncEventIllegalConflict
     * @throws TaskNotVisibleException
     */
    protected static SynchableObject findOrCreateAndProccess(Object object, Class<? extends SynchableObject> clazz,
            Iterable iterable, ObjectFindStrategy... optionalObjectFindStrategy) throws SyncEventIllegalConflict {
        return findOrCreateAndProccess(object, clazz, iterable, false, optionalObjectFindStrategy);

    }

    private static RequestProcessor acRequestProcessor = ACContext.getInstance();

    private static void generateSyncEvent(final SynchableObject toProccessAndReturn,
            final Collection<String> changedDescriptors, final Object apiObject) throws SyncEventIllegalConflict {
        final SynchableObject originObject = toProccessAndReturn;
        SyncEvent.TypeOfChangeEvent typeOfChange = null;
        DSIObject dsiObject = toProccessAndReturn.getDSIObject();
        if (dsiObject == null || dsiObject.getLastSynchedAt() == null) {
            typeOfChange = TypeOfChangeEvent.CREATE;
        }
        try {
            dsiObject = toProccessAndReturn.findOrCreateDSIObject();

        } catch (UnsupportedOperationException ex) {
            LOGGER.debug(toProccessAndReturn.getClass().getName() + " doesn't support Synch");
        }
        if (dsiObject != null && dsiObject.getLastSynchedAt() != null) {
            typeOfChange = TypeOfChangeEvent.UPDATE;
        }
        if (dsiObject != null) {
            if (typeOfChange == null) {
                typeOfChange = TypeOfChangeEvent.UPDATE;
            }

            SyncUniverse syncUniverse = null;
            if (originObject instanceof pt.ist.maidSyncher.domain.activeCollab.ACObject) {
                syncUniverse = SyncUniverse.GITHUB;
            } else if (originObject instanceof GHObject) {
                syncUniverse = SyncUniverse.ACTIVE_COLLAB;
            }
            SyncEvent syncEvent = new SyncEvent(toProccessAndReturn.getUpdatedAtDate(), typeOfChange,
                    changedDescriptors, dsiObject, new APIObjectWrapper() {

                        @Override
                        public void validateAPIObject() throws SyncEventOriginObjectChanged {
                            try {

                                Object currentAPIObject = null;

                                if (apiObject instanceof ACObject) {
                                    //let's get the object from the repository
                                    ACObject acObject = (ACObject) apiObject;
                                    Constructor<? extends Object> constructor = null;
                                    JSONObject jsonObject = null;

                                    jsonObject = (JSONObject) acRequestProcessor.processGet(acObject.getUrl());
                                    constructor = apiObject.getClass().getConstructor(JSONObject.class);
                                    currentAPIObject = constructor.newInstance(jsonObject);

                                } else {
                                    //if it's not an active collab one, let's assume it's a GH
                                    GitHubRequest gitHubRequest = new GitHubRequest();
                                    getPropertyDescriptorNameAndCheckItExists(apiObject, "url");
                                    String uri = null;
                                    uri = (String) PropertyUtils.getSimpleProperty(apiObject, "url");
                                    gitHubRequest.setUri(uri);
                                    currentAPIObject = MaidRoot.getGitHubClient().get(gitHubRequest).getBody();

                                }

                                Collection<PropertyDescriptor> changedProperties;
                                changedProperties = propertiesEqual(apiObject, currentAPIObject);
                                if (changedProperties.isEmpty() == false) {
                                    String changedDescriptors = "";
                                    for (PropertyDescriptor changedDescriptor : changedProperties)
                                        changedDescriptors += " " + changedDescriptor.getName();

                                    throw new SyncEventOriginObjectChanged(
                                            "Origin object: " + originObject.getClass().getSimpleName()
                                                    + " unequal descriptors:" + changedDescriptors);

                                }
                            } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                                    | InvocationTargetException | IOException | NoSuchMethodException
                                    | SecurityException e) {
                                throw new SyncEventOriginObjectChanged(
                                        "Could not assert if Origin object changed. Origin obj: "
                                                + apiObject.getClass().getSimpleName(),
                                        e);
                            }

                        }

                        @Override
                        public Object getAPIObject() {
                            return apiObject;
                        }
                    }, syncUniverse, originObject);
            addSyncEvent(syncEvent);

        }
    }

    public static void addSyncEvent(SyncEvent syncEvent) {
        MaidRoot.getInstance().addSyncEvent(syncEvent);
        logSync(syncEvent);
    }

    private static void logSync(SyncEvent syncEvent) {
        SynchableObject originObject = syncEvent.getOriginObject();
        Collection<String> changedPropertyDescriptors = syncEvent.getChangedPropertyDescriptorNames()
                .getUnmodifiableList();
        String logContent = "Added Sync event with origin object: " + originObject.getExternalId() + " class: "
                + originObject.getClass().getName() + " changed properties: ";
        for (String descriptorName : changedPropertyDescriptors) {
            logContent += " " + descriptorName;
        }
        logContent += " type: " + syncEvent.getTypeOfChangeEvent();
        LOGGER.debug(logContent);

    }

    public abstract DateTime getUpdatedAtDate();

    /**
     * 
     * @param syncEvent the syncEvent to process
     * @return the {@link SyncActionWrapper} with the PropertyDescriptors 'ticked' i.e. acted upon - used to trace which ones we
     *         weren't able to 'reach' due to
     *         coding errors - and a method that will do the event
     */
    public abstract SyncActionWrapper sync(SyncEvent syncEvent);

    /**
     * 
     * @param orig
     * @return a collection with the {@link PropertyDescriptor} s that changed
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws TaskNotVisibleException
     */
    public Collection<String> copyPropertiesFrom(Object orig) throws IllegalAccessException,
            InvocationTargetException, NoSuchMethodException, TaskNotVisibleException {
        Set<PropertyDescriptor> propertyDescriptorsThatChanged = new HashSet<PropertyDescriptor>();

        Object dest = this;

        if (orig == null) {
            throw new IllegalArgumentException("No origin bean specified");
        }
        PropertyDescriptor origDescriptors[] = PropertyUtils.getPropertyDescriptors(orig);
        for (PropertyDescriptor origDescriptor : origDescriptors) {
            String name = origDescriptor.getName();
            if (PropertyUtils.isReadable(orig, name)) {
                if (PropertyUtils.isWriteable(dest, name)) {
                    Object valueDest = PropertyUtils.getSimpleProperty(dest, name);
                    Object valueOrigin = PropertyUtils.getSimpleProperty(orig, name);
                    if (valueOrigin != null && valueOrigin.getClass().getPackage().getName()
                            .equalsIgnoreCase("org.eclipse.egit.github.core"))
                        continue; //let's skip the properties with egit core objects (they shall be copied from a custom overriden version of this method)
                    if (valueOrigin instanceof Date)
                        valueOrigin = new DateTime(valueOrigin);
                    if (Objects.equal(valueDest, valueOrigin) == false)
                        propertyDescriptorsThatChanged.add(origDescriptor);
                    try {
                        //let's see if this is actually a Date, if so, let's convert it
                        PropertyUtils.setSimpleProperty(dest, name, valueOrigin);
                    } catch (IllegalArgumentException ex) {
                        throw new Error("setSimpleProperty returned an exception, dest: "
                                + dest.getClass().getName() + " oid: " + ((DomainObject) dest).getExternalId()
                                + " name : " + name + " valueOrig: " + valueOrigin, ex);
                    }
                    LOGGER.trace("Copied property " + name + " from " + orig.getClass().getName() + " object to a "
                            + dest.getClass().getName() + " oid: " + getExternalId());
                }
            }
        }

        return Collections2.transform(propertyDescriptorsThatChanged, new Function<PropertyDescriptor, String>() {
            @Override
            public String apply(PropertyDescriptor propertyDescriptor) {
                if (propertyDescriptor == null) {
                    return null;
                }
                return propertyDescriptor.getName();

            }
        });
    }

    public void copyPropertiesTo(Object dest) throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException, TaskNotVisibleException {
        Set<PropertyDescriptor> propertyDescriptorsThatChanged = new HashSet<PropertyDescriptor>();

        Object orig = this;
        checkNotNull(dest);

        PropertyDescriptor origDescriptors[] = PropertyUtils.getPropertyDescriptors(orig);
        for (PropertyDescriptor origDescriptor : origDescriptors) {
            String name = origDescriptor.getName();
            if (PropertyUtils.isReadable(orig, name)) {
                PropertyDescriptor destDescriptor = PropertyUtils.getPropertyDescriptor(dest,
                        origDescriptor.getName());
                if (PropertyUtils.isWriteable(dest, name) || (destDescriptor != null
                        && MiscUtils.getWriteMethodIncludingFlowStyle(destDescriptor, dest.getClass()) != null)) {
                    Object valueDest = PropertyUtils.getSimpleProperty(dest, name);
                    Object valueOrigin = PropertyUtils.getSimpleProperty(orig, name);

                    LOGGER.debug("OrigDescriptor PropertyType: " + origDescriptor.getPropertyType().getName());
                    //                    System.out.println("OrigDescriptor PropertyType: " + origDescriptor.getPropertyType().getName());
                    //let's ignore the properties were the values are our domain packages
                    if (valueOrigin != null && (SynchableObject.class.isAssignableFrom(valueOrigin.getClass()))) {
                        //                        System.out.println("Skipping");
                        continue; //let's skip these properties
                    }
                    if (SynchableObject.class.isAssignableFrom(origDescriptor.getPropertyType())) {
                        //                        System.out.println("Skipping");
                        continue;
                    }
                    if (origDescriptor instanceof IndexedPropertyDescriptor) {
                        IndexedPropertyDescriptor indexedPropertyDescriptor = (IndexedPropertyDescriptor) origDescriptor;
                        //                        System.out.println("OrigDescriptor IndexedPropertyDescriptor: " + indexedPropertyDescriptor.getName());
                        if (SynchableObject.class
                                .isAssignableFrom(indexedPropertyDescriptor.getIndexedPropertyType())) {
                            //                            System.out.println("Skipping");
                            continue;
                        }

                    }

                    //let's ignore all of the dates - as they should be filled by
                    //the system
                    if (valueOrigin instanceof LocalTime)
                        continue;
                    if (Objects.equal(valueDest, valueOrigin) == false)
                        propertyDescriptorsThatChanged.add(origDescriptor);
                    try {
                        if (PropertyUtils.isWriteable(dest, name) == false) {
                            //let's use the flow version
                            Class<?> origPropertyType = origDescriptor.getPropertyType();
                            Method writeMethodIncludingFlowStyle = MiscUtils
                                    .getWriteMethodIncludingFlowStyle(destDescriptor, dest.getClass());
                            if (Arrays.asList(writeMethodIncludingFlowStyle.getParameterTypes())
                                    .contains(origPropertyType)) {
                                writeMethodIncludingFlowStyle.invoke(dest, valueOrigin);
                            } else {
                                continue;
                            }

                        } else {
                            PropertyUtils.setSimpleProperty(dest, name, valueOrigin);

                        }
                    } catch (IllegalArgumentException ex) {
                        throw new Error("setSimpleProperty returned an exception, dest: "
                                + dest.getClass().getName() + " name : " + name + " valueOrig: " + valueOrigin, ex);
                    }
                    LOGGER.trace("Copied property " + name + " from " + orig.getClass().getName() + " object to a "
                            + dest.getClass().getName() + " oid: " + getExternalId());
                }
                //                System.out.println("--");
            }
        }

    }

    /**
     * 
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws TaskNotVisibleException
     * @throws IllegalArgumentException if the property descriptors of the arguments differ
     * 
     * @return a collection of {@link PropertyDescriptor} that changed, or an empty collection
     */
    public static Collection<PropertyDescriptor> propertiesEqual(Object object1, Object object2)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException,
            TaskNotVisibleException, IllegalArgumentException {

        List<PropertyDescriptor> changedDescriptors = new ArrayList<>();

        if (object1 == null || object2 == null) {
            throw new IllegalArgumentException("Both objects must be non null");
        }
        PropertyDescriptor object1Descriptors[] = PropertyUtils.getPropertyDescriptors(object1);
        PropertyDescriptor object2Descriptors[] = PropertyUtils.getPropertyDescriptors(object2);

        //let's make sure that they match
        checkArgument(ObjectUtils.equals(object1Descriptors, object2Descriptors),
                "Error, object1 : " + object1.getClass().getSimpleName() + " and object2 : "
                        + object2.getClass().getSimpleName() + " don't match");

        for (PropertyDescriptor object1Descriptor : object1Descriptors) {
            String name = object1Descriptor.getName();
            if (PropertyUtils.isReadable(object1, name) && PropertyUtils.isReadable(object2, name)) {

                //if both are readable, let's check on the values
                Object valueObject1 = PropertyUtils.getSimpleProperty(object1, name);
                Object valueObject2 = PropertyUtils.getSimpleProperty(object2, name);
                //                if (isGitHubObject(valueObject1) || isGitHubObject(valueObject2))
                //                    continue; //let's skip the GitHub properties, we won't be able to compare that

                if (!ObjectUtils.equals(valueObject1, valueObject2))
                    changedDescriptors.add(object1Descriptor);
            }
        }
        return changedDescriptors;
    }

    private static boolean isGitHubObject(Object valueObject1) {
        return valueObject1 != null
                && valueObject1.getClass().getPackage().getName().equalsIgnoreCase("org.eclipse.egit.github.core");
    }

    protected static String getPropertyDescriptorNameAndCheckItExists(Object bean, String propertyName) {
        PropertyDescriptor propertyDescriptor;
        try {
            propertyDescriptor = PropertyUtils.getPropertyDescriptor(bean, propertyName);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new Error(e);
        }
        checkNotNull(propertyDescriptor);
        return propertyDescriptor.getName();
    }
}