com.nextep.datadesigner.vcs.impl.Merger.java Source code

Java tutorial

Introduction

Here is the source code for com.nextep.datadesigner.vcs.impl.Merger.java

Source

/*******************************************************************************
 * Copyright (c) 2011 neXtep Software and contributors.
 * All rights reserved.
 *
 * This file is part of neXtep designer.
 *
 * NeXtep designer 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 3 of the License, or any later version.
 *
 * NeXtep designer 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 Foobar.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contributors:
 *     neXtep Softwares - initial API and implementation
 *******************************************************************************/
/**
 *
 */
package com.nextep.datadesigner.vcs.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.runtime.IProgressMonitor;
import org.hibernate.classic.Session;

import com.nextep.datadesigner.Designer;
import com.nextep.datadesigner.exception.ErrorException;
import com.nextep.datadesigner.hibernate.HibernateUtil;
import com.nextep.datadesigner.impl.Observable;
import com.nextep.datadesigner.impl.StringAttribute;
import com.nextep.datadesigner.model.INamedObject;
import com.nextep.datadesigner.model.IReference;
import com.nextep.datadesigner.model.IReferenceable;
import com.nextep.datadesigner.model.ITypedObject;
import com.nextep.datadesigner.model.IdentifiedObject;
import com.nextep.datadesigner.vcs.services.VersionHelper;
import com.nextep.designer.core.CorePlugin;
import com.nextep.designer.core.model.IReferenceManager;
import com.nextep.designer.vcs.factories.VersionFactory;
import com.nextep.designer.vcs.model.ComparisonScope;
import com.nextep.designer.vcs.model.IActivity;
import com.nextep.designer.vcs.model.IComparisonItem;
import com.nextep.designer.vcs.model.IMergeStrategy;
import com.nextep.designer.vcs.model.IMerger;
import com.nextep.designer.vcs.model.IVersionInfo;
import com.nextep.designer.vcs.model.IVersionable;
import com.nextep.designer.vcs.model.MergeInfo;
import com.nextep.designer.vcs.model.MergeStatus;
import com.nextep.designer.vcs.model.VersionableFactory;
import com.nextep.designer.vcs.model.impl.Activity;

/**
 * The abstract root class of all mergers. All mergers should extend this class
 * and must not implement <code>IMerger</code>
 * 
 * @author Christophe Fondacci
 */
public abstract class Merger<T> extends Observable implements IMerger {

    private static final Log log = LogFactory.getLog(Merger.class);
    public static final String ATTR_NAME = "Name";
    public static final String ATTR_DESC = "Description";
    private static boolean refreshWorkEnabled = true;
    private IMergeStrategy mergeStrategy = new MergeStrategyRepository();

    /**
     * TODO: Once the
     * {@link MergerFactory#getMerger(com.nextep.datadesigner.model.IElementType)}
     * will be removed, simply set the provided strategy
     * 
     * @see com.nextep.designer.vcs.model.IMerger#setMergeStrategy(com.nextep.designer.vcs.model.IMergeStrategy)
     */
    @Override
    public void setMergeStrategy(IMergeStrategy mergeStrategy) {
        this.mergeStrategy = MergeStrategy.create(mergeStrategy.getComparisonScope());
    }

    /**
     * @see com.nextep.designer.vcs.model.IMerger#getMergeStrategy()
     */
    @Override
    public IMergeStrategy getMergeStrategy() {
        return mergeStrategy;
    }

    /**
     * @see com.nextep.designer.vcs.model.IMerger#compare(com.nextep.designer.vcs.model.VersionReference,
     *      com.nextep.designer.vcs.model.IVersionInfo,
     *      com.nextep.designer.vcs.model.IVersionInfo)
     */
    @SuppressWarnings("unchecked")
    @Override
    public final IComparisonItem compare(IReference ref, IVersionInfo sourceVersion,
            IVersionInfo destinationVersion, boolean sandBoxSession) {
        IReferenceable source = null;
        // A flag to clear the session even if source version is null
        final Session session = (sandBoxSession ? HibernateUtil.getInstance().getSandBoxSession()
                : HibernateUtil.getInstance().getSession());
        if (sourceVersion != null) {
            CorePlugin.getService(IReferenceManager.class).flushVolatiles(session);
            IVersionable<IReferenceable> sourceItem = (IVersionable<IReferenceable>) CorePlugin.getIdentifiableDao()
                    .load(IVersionable.class, sourceVersion.getUID(), session, sandBoxSession); // If sandbox, we clear the session
            if (sourceItem != null) {
                source = sourceItem.getVersionnedObject().getModel();
            } else {
                log.warn("Unable to load source item for merge/compare operations.");
            }
        }
        IReferenceable target = null;
        if (destinationVersion != null) {
            CorePlugin.getService(IReferenceManager.class).flushVolatiles(session);
            IVersionable<IReferenceable> targetItem = (IVersionable<IReferenceable>) CorePlugin.getIdentifiableDao()
                    .load(IVersionable.class, destinationVersion.getUID(), session, sandBoxSession); // If sandboxed we clear
            // the session
            if (targetItem != null) {
                target = targetItem.getVersionnedObject().getModel();
            } else {
                log.warn("Unable to load target item for merge/compare operations.");
            }
        }

        if (source instanceof ITypedObject) {
            VersionHelper.relink((ITypedObject) source);
        }
        if (target instanceof ITypedObject) {
            VersionHelper.relink((ITypedObject) target);
        }
        // Preserving result
        refreshProgressLabel(source, target);
        IComparisonItem item = compare(source, target);
        refreshWork();
        return item;
    }

    @Override
    public final IComparisonItem compare(IReferenceable source, IReferenceable target) {
        // Handling MISSING_SOURCE / MISSING_TARGET without any memory usage
        if (getMergeStrategy().getComparisonScope() != ComparisonScope.DB_TO_REPOSITORY) {
            if ((source == null || target == null || source == target) && !copyWhenUnchanged()) {
                final IComparisonItem result = new ComparisonResult(source, target,
                        getMergeStrategy().getComparisonScope());
                return result;
            } else {
                // If we have 2 versionables, we only need to compare differing
                // versions
                if (source instanceof IVersionable<?> && target instanceof IVersionable<?>) {
                    final IVersionInfo srcVersion = ((IVersionable<?>) source).getVersion();
                    final IVersionInfo tgtVersion = ((IVersionable<?>) target).getVersion();
                    if (srcVersion.equals(tgtVersion)) {
                        final IComparisonItem result = new ComparisonResult(source, target,
                                getMergeStrategy().getComparisonScope());
                        return result;
                    }
                }
            }
        }

        // Generic comparison
        final IComparisonItem result = doCompare(source, target);

        // Specific comparison, using some abstract / inheritance tweaks which
        // are not good. We know that but that's the way mergers are built at
        // the moment
        // and we could not refactor everything at the moment.
        if (isClass(getSpecificClass(), source, target)) {
            fillSpecificComparison(result, (T) source, (T) target);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    @Override
    public IComparisonItem compare(IReferenceable source, IVersionInfo otherVersion) {
        IReferenceable target = null;
        final Session session = HibernateUtil.getInstance().getSandBoxSession();
        if (otherVersion != null) {
            CorePlugin.getService(IReferenceManager.class).flushVolatiles(session);
            IVersionable<IReferenceable> targetItem = (IVersionable<IReferenceable>) CorePlugin.getIdentifiableDao()
                    .load(IVersionable.class, otherVersion.getUID(), session, true);
            VersionHelper.relink(targetItem, CorePlugin.getService(IReferenceManager.class)
                    .getReverseDependenciesMapFor(targetItem.getReference()));
            if (targetItem != null) {
                target = targetItem.getVersionnedObject().getModel();
            } else {
                log.warn("Unable to load target item for merge/compare operations.");
            }
        }
        IComparisonItem item = compare(source, target);
        return item;
    }

    /**
     * Makes the comparison between the 2 specified referenceable. This is an
     * inner protected method as the comparison is *proxied* by the
     * {@link Merger#compare(IReferenceable, IReferenceable)} method to only
     * perform the comparison when there is no other way to identify
     * differences.
     * 
     * @param source
     *            source element of the comparison
     * @param target
     *            target element of the comparison
     * @return a {@link IComparisonItem} containing the comparison information
     */
    protected abstract IComparisonItem doCompare(IReferenceable source, IReferenceable target);

    protected void refreshProgressLabel(IReferenceable src, IReferenceable tgt) {
        // IReferenceable r = (src == null ? tgt : src);
        if (src == null && tgt == null)
            return;
        if (Designer.getProgressMonitor() != null) {
            // Designer.getProgressMonitor().subTask("Comparing " +
            // Designer.getInstance().getQualifiedName(r) + "...");
        }
    }

    protected void refreshWork() {
        if (Designer.getProgressMonitor() != null && refreshWorkEnabled) {
            try {
                Designer.getProgressMonitor().worked(1);
            } catch (RuntimeException e) {
                log.debug(e);
            }
        }
    }

    public static void setRefreshWorkEnabled(boolean refreshWorkEnabled) {
        Merger.refreshWorkEnabled = refreshWorkEnabled;
    }

    /**
     * Compares 2 <code>INamedObject</code> and adds the result to the provided
     * comparison item.
     * 
     * @param result
     *            the current comparison result
     * @param source
     *            source of the comparison
     * @param target
     *            target of the comparison
     */
    protected void compareName(IComparisonItem result, INamedObject source, INamedObject target) {
        result.addSubItem(new ComparisonNameAttribute(ATTR_NAME, source == null ? null : notNull(source.getName()),
                target == null ? null : notNull(target.getName()), MergeStrategy.isCaseSensitive()));
        result.addSubItem(
                new ComparisonAttribute(ATTR_DESC, source == null ? null : notNull(source.getDescription()),
                        target == null ? null : notNull(target.getDescription()), ComparisonScope.REPOSITORY));
    }

    protected String notNull(String str) {
        return str == null ? "" : str;
    }

    protected void fillName(IComparisonItem result, INamedObject target) {
        target.setName(getStringProposal(ATTR_NAME, result));
        target.setDescription(getStringProposal(ATTR_DESC, result));
    }

    public static String getStringProposalValue(String attributeName, IComparisonItem result) {
        return getStringProposal(attributeName, result, null);
    }

    public String getStringProposal(String attributeName, IComparisonItem result) {
        return getStringProposal(attributeName, result, getMergeStrategy().getComparisonScope());
    }

    public static String getStringProposal(String attributeName, IComparisonItem result, ComparisonScope scope) {
        ComparisonAttribute attr = (ComparisonAttribute) result.getAttribute(attributeName);
        if (attr == null) {
            // Special use-case for comparison result, we try to return out of
            // scope information
            // rather than nothing. It allows to preserve descriptions, short
            // names, etc. from the
            // repository
            // while reverse synchronizing
            if (result instanceof ComparisonResult) {
                attr = (ComparisonAttribute) ((ComparisonResult) result).getOutOfScopeAttribute(attributeName);
            }
            if (attr == null) {
                return "";
            }
        }
        if (scope != null && attr.getScope() != scope && scope != ComparisonScope.ALL
                && attr.getScope() != ComparisonScope.ALL) {
            if (scope == ComparisonScope.DB_TO_REPOSITORY && attr.getScope() == ComparisonScope.REPOSITORY) {
                attr.getMergeInfo().setMergeProposal(attr.getTarget());
            } else {
                return "";
            }
        }
        StringAttribute attrVal = (StringAttribute) attr.getMergeInfo().getMergeProposal();
        if (attrVal == null) {
            return null;
        }
        return attrVal.getValue();
    }

    /**
     * @see com.nextep.designer.vcs.model.IMerger#merge(com.nextep.designer.vcs.model.IComparisonItem,
     *      com.nextep.designer.vcs.model.IComparisonItem,
     *      com.nextep.designer.vcs.model.IComparisonItem)
     */
    @Override
    public MergeInfo merge(IComparisonItem result, IComparisonItem sourceRootDiff, IComparisonItem targetRootDiff) {
        // Notifying listeners
        final IProgressMonitor m = Designer.getProgressMonitor();
        if (m != null) {
            try {
                m.subTask("Merging " + result.getType().getName() + " "
                        + (result.getReference() != null ? result.getReference().getArbitraryName() : ""));
            } catch (RuntimeException e) {
                log.debug(e);
            }
        }
        // Delegating to our strategy
        final MergeInfo info = mergeStrategy.merge(this, result, sourceRootDiff, targetRootDiff);
        if (m != null) {
            try {
                m.worked(1);
            } catch (RuntimeException e) {
                log.debug(e);
            }
        }
        return info;
    }

    /**
     * Specifies if the merger should copy a resolved already-existing items.
     * The default is <code>false</code> so a resolved existing item will be
     * used in place during merge. If set to <code>true</code> the merger will
     * be invoked to create and fill a brand new objects by calling
     * {@link Merger#createTargetObject(IComparisonItem, IActivity)} and
     * {@link Merger#fillObject(Object, IComparisonItem, IActivity)} methods.
     * Mergers implementation should override this method if they want to hard
     * copy a resolved item instead of using it in place.
     * 
     * @return a flag indicating if the merger should copy an unchanged item or
     *         use it in place
     */
    protected boolean copyWhenUnchanged() {
        return false;
    }

    /**
     * Indicates if the current merger should use the comparison information
     * from its parent or if it should generate new comparison information.<br>
     * Default implementation will return false and generate comparison
     * information, mergers should override this method (mainly for
     * unversionable items)
     * 
     * @return <code>true</code> to use parent comparison, else
     *         <code>false</code>
     */
    @Override
    public boolean isVersionable() {
        return true;
    }

    /**
     * Computes the merge status of the comparison sub elements.
     * 
     * @param result
     *            comparison information
     * @return a <code>MergeStatus</code> computed from child comparison items
     */
    public static MergeStatus getSubStatus(IComparisonItem result) {
        MergeStatus status = result.getMergeInfo().getMergeProposal() == null ? MergeStatus.NOT_MERGED
                : MergeStatus.MERGE_RESOLVED;

        for (IComparisonItem item : result.getSubItems()) {
            if (item.getMergeInfo().getMergeProposal() == null
                    && item.getMergeInfo().getStatus() != MergeStatus.MERGE_RESOLVED) {
                status = status.compute(getSubStatus(item));
            } else {
                status = status.compute(item.getMergeInfo().getStatus());
            }
        }
        // Handling case of no subitem
        if (status == MergeStatus.NOT_MERGED) {
            status = MergeStatus.MERGE_UNRESOLVED;
        }
        // log.debug("END Status: [" + status.name() + "] on item " +
        // result.toString());
        return status;
    }

    /**
     * @see com.nextep.designer.vcs.model.IMerger#buildMergedObject(com.nextep.designer.vcs.model.IComparisonItem,
     *      com.nextep.designer.vcs.model.IActivity)
     */
    @Override
    public final Object buildMergedObject(IComparisonItem result, IActivity mergeActivity) {
        if (result == null) {
            return null;
        }
        MergeInfo mergeInfo = result.getMergeInfo();
        // If merge is resolved by the current target, the merged
        // object is this target and we have nothing to do.
        // ONLY IF THE MERGER DOES NOT WANT A COPY (copyWhenUnchanged check)
        if (mergeInfo.getStatus() == MergeStatus.MERGE_RESOLVED
                && (mergeInfo.getMergeProposal() == result.getTarget()
                        || mergeInfo.getMergeProposal() == result.getSource())
                && !copyWhenUnchanged()) {
            // Checking if sub items are all checked for either source or
            // target, but not BOTH
            // otherwise we need to merge
            boolean targetSelection = (mergeInfo.getMergeProposal() == result.getTarget());
            if (checkChildTargetSelection(result, targetSelection)) {
                return mergeInfo.getMergeProposal();
            }
        }
        // Checking merge status and raise if any unresolved item
        if (mergeInfo.getStatus() != MergeStatus.MERGE_RESOLVED) {
            // || (mergeInfo.getStatus() == MergeStatus.MERGE_RESOLVED
            // && mergeInfo.getMergeProposal()==null
            // && getSubStatus(result)!=MergeStatus.MERGE_RESOLVED)) {
            throw new ErrorException("There are unresolved items, aborting merge process.");
        }
        // We are ready to merge
        Object mergedObject = createTargetObject(result, mergeActivity);
        if (mergedObject instanceof IReferenceable) {
            // Reloading reference since the session may have been cleared
            // VersionReference r =
            // (VersionReference)IdentifiableDAO.getInstance().load(VersionReference.class,
            // result.getReference().getReferenceId());
            ((IReferenceable) mergedObject).setReference(result.getReference());
        }
        // Returning our merged object
        return fillObject(mergedObject, result, mergeActivity);

    }

    /**
     * This method checks is all sub items of the specified comparison items
     * have a same target selection flag as its parent.<br>
     * We need this to detect if a parent item is selected but a different child
     * selection is made. In this case we will need to merge whereas we would
     * otherwise take the parent proposal in place.
     * 
     * @param item
     * @param targetSelection
     * @return
     */
    private boolean checkChildTargetSelection(IComparisonItem item, boolean targetSelection) {
        for (IComparisonItem child : item.getSubItems()) {
            if (targetSelection != (child.getMergeInfo().getMergeProposal() == child.getTarget())) {
                return false;
            }
            checkChildTargetSelection(child, targetSelection);
        }
        return true;
    }

    /**
     * Fills the object with its data obtained from the comparison result item.
     * Implementors should COMPLETELY fill the object as the given target object
     * might be empty.<br>
     * It is the responsibility of implementors to return <code>null</code> to
     * indicate that the object has been removed.<br>
     * Note that if a merger need to save the object which is being filled, they
     * <b>MUST</b> do so by calling the {@link Merger#save(IdentifiedObject)}
     * method. Saving a merge objects directly will result in unexpected
     * behaviours.
     * 
     * @param target
     *            target merged object
     * @param result
     *            comparison result containing resolved merge information
     * @param activity
     *            activity to use for any versioning operation
     * @return the merged object, or <code>null</code>.
     */
    protected abstract Object fillObject(Object target, IComparisonItem result, IActivity activity);

    /**
     * Dedicated for external calls which needs to fill information from a
     * comparison item to refill an existing object
     * 
     * @param target
     *            object to fill
     * @param result
     *            comparison information with fully resolved merge information (
     *            no check will be made)
     */
    public void fill(Object target, IComparisonItem result) {
        fillObject(target, result, Activity.getDefaultActivity());
    }

    /**
     * Creates the merge target object. The default implementation will handle
     * <code>IVersionable</code> by checking out the target merge item. Non
     * versionable mergers should extend this method to create the target merge
     * object.
     * 
     * @param result
     *            comparison result containing merge information
     * @param mergeActivity
     *            activity to use for versioning operations
     * @return the object in which the merge will be applied
     */
    protected Object createTargetObject(IComparisonItem result, IActivity mergeActivity) {
        IVersionable<?> mergedObject = null;
        // In any other case we checkout our target version
        IVersionable<?> v = VersionHelper
                .getVersionable(result.getTarget() != null ? result.getTarget() : result.getSource());
        if (v != null) {
            mergedObject = VersionableFactory.createVersionable(result.getType().getInterface());
            // If we are building the object in a repository merge, we setup
            // proper version
            // information
            // Otherwise this will be a temporary object
            if (getMergeStrategy().getComparisonScope() == ComparisonScope.REPOSITORY) {
                IVersionInfo newVersion = VersionFactory.buildNextVersionInfo(v.getVersion(), mergeActivity);
                VersionHelper.incrementRelease(newVersion, VersionHelper.PATCH);
                while (!VersionHelper.isVersionAvailable(newVersion)) {
                    VersionHelper.incrementRelease(newVersion, VersionHelper.PATCH);
                }
                // IdentifiableDAO.getInstance().save(newVersion);
                IVersionable<?> srcVersion = VersionHelper.getVersionable(result.getSource());
                if (srcVersion != null) {
                    // IVersionInfo mergeSrcVersion =
                    // (IVersionInfo)IdentifiableDAO.getInstance().load(VersionInfo.class,
                    // srcVersion.getUID());
                    newVersion.setMergedFromVersion(srcVersion.getVersion());
                }
                mergedObject.setVersion(newVersion);
            }
            // mergedObject = VersionHelper.checkOut(v, mergeActivity);
        } else {
            throw new ErrorException("Unable to create the merged target object.");
        }
        return mergedObject;
    }

    /**
     * A method which should be used by mergers when they build objects through
     * {@link Merger#fillObject(Object, IComparisonItem, IActivity)} to
     * temporarily save their intermediate objects.
     * 
     * @param o
     *            object to save
     */
    public void save(IdentifiedObject o) {
        // We only save the object within a repository scope
        if (getMergeStrategy().getComparisonScope() == ComparisonScope.REPOSITORY) {
            CorePlugin.getIdentifiableDao().save(o, true, HibernateUtil.getInstance().getSandBoxSession(), true);
        } else {
            log.debug("Skipping merge of non-REPOSITORY scope: " + this.getClass().getName());
        }
    }

    protected String strVal(Object o) {
        if (o == null) {
            return "";
        } else {
            return o.toString();
        }
    }

    protected boolean isClass(Class<?> clazz, Object src, Object tgt) {
        return ((src == null || (clazz.isInstance(src))) && (tgt == null || (clazz.isInstance(tgt))));
    }

    protected void fillSpecificComparison(IComparisonItem result, T src, T tgt) {
        // Default empty implementation, to override
    }

    protected Class<? extends IReferenceable> getSpecificClass() {
        return IReferenceable.class;
    }
}