com.funambol.server.engine.Sync4jStrategy.java Source code

Java tutorial

Introduction

Here is the source code for com.funambol.server.engine.Sync4jStrategy.java

Source

/*
 * Funambol is a mobile platform developed by Funambol, Inc.
 * Copyright (C) 2003 - 2007 Funambol, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT  OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
 * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by Funambol" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by Funambol".
 */
package com.funambol.server.engine;

import java.security.Principal;
import java.sql.Timestamp;
import java.util.*;
import java.util.Map;

import com.funambol.framework.core.*;
import com.funambol.framework.engine.*;
import com.funambol.framework.engine.Util;
import com.funambol.framework.engine.source.*;
import com.funambol.framework.logging.FunambolLogger;
import com.funambol.framework.logging.FunambolLoggerFactory;
import com.funambol.framework.server.ClientMapping;
import com.funambol.framework.server.ClientMappingEntry;

import org.apache.commons.collections.ListUtils;

/**
 * This class represents a synchronization process.
 *
 * The base concrete implementation of a synchronization strategy. It implements
 * the <i>SyncStrategy</i> partecipant of the Strategy Pattern.
 * <p>
 * The synchronization process is implemented in this class as follows:
 * <p>
 * Given a set of sources A, B, C, D, etc, the synchronization process takes place
 * between two sources at a time: A is first synchronzed with B, then AB with
 * C, then ABC with D and so on. <br>
 * The synchronization process is divided in three phases: preparation, synchronization,
 * finalization.
 * <br>
 * prepareSync returns an array of SyncOperation, in which each element represents
 * a particular synchronization action, ie. create an item in the source A,
 * delete the item X from the source B, etc. Sometime it is not possible decide
 * what to do, thus a SyncConflict operation is used. A conflict must be solved
 * by something external the synchronization process, for instance by a user
 * action. Below is a table of all possible situations.
 * <pre>
 *
 * | -------- | --- | --- | --- | --- | --- |
 * | Source A |     |     |     |     |     |
 * |    /     |  N  |  D  |  U  |  S  |  X  |     N : item new
 * | Source B |     |     |     |     |     |     D : item deleted
 * | -------- | --- | --- | --- | --- | --- |     U : item updated
 * |       N  |  O  |  O  |  O  |  O  |  B  |     S : item synchronized/unchanged
 * | -------- | --- | --- | --- | --- | --- |     X : item not existing
 * |       D  |  O  |  X  |  O  |  X  |  X  |     O : conflict
 * | -------- | --- | --- | --- | --- | --- |     A : item A replaces item B
 * |       U  |  O  |  O  |  O  |  B  |  B  |     B : item B replaces item A
 * | -------- | --- | --- | --- | --- | --- |
 * |       S  |  O  |  X  |  A  |  =  |  B  |
 * | -------- | --- | --- | --- | --- | --- |
 * |       X  |  A  |  X  |  A  |  A  |  X  |
 * | -------- | --- | --- | --- | --- | --- |
 *
 * </pre>
 * When a conflict is detected, it's resolved using one between this methods:
 * <ui>
 * <li><code>CONFLICT_RESOLUTION_SERVER_WINS</code></li>
 * <li><code>CONFLICT_RESOLUTION_CLIENT_WINS</code></li>
 * <li><code>CONFLICT_RESOLUTION_MERGE_DATA</code>: this is applicable only with a
 * <code>MergeableSyncSource</code></li>
 * </ui>.
 * <br/>
 * If a syncsource is a <code>ClientWinsSyncSource</code> the conflict
 * resolution used is <code>CONFLICT_RESOLUTION_CLIENT_WINS</code>.
 * <br/>
 * If a syncsource is a <code>ServerWinsSyncSource</code> the conflict resolution
 * used is <code>CONFLICT_RESOLUTION_SERVER_WINS</code>.
 * <br/>
 * If a syncsource is a <code>MergeableSyncSource</code> the conflict resolution
 * used is <code>CONFLICT_RESOLUTION_MERGE_DATA</code>.
 * <p/>
 * In the other cases, it's possible to set a different conflict resolution setting
 * the property <code>sourceUriConflictsResolution</code>. This object contains
 * for each syncsource the conflict resolution to use. If for a syncSource, no
 * conflict resolution is set, the default value (<code>defaultConflictResolution</code>)
 * is used. By default, <code>defaultConflictResolution</code> is
 * <code>CONFLICT_RESOLUTION_CLIENT_WINS</code>.
 *
 * @version  $Id: Sync4jStrategy.java,v 1.2 2008-06-18 08:23:43 nichele Exp $
 */
public class Sync4jStrategy implements SyncStrategy, java.io.Serializable {

    // --------------------------------------------------------------- Constants

    public static final String LOG_NAME = "funambol.engine.strategy";

    protected transient static FunambolLogger log = FunambolLoggerFactory.getLogger(LOG_NAME);

    // -------------------------------------------------------------- Properties

    /**
     * Contains a map of source uri and the resolution to use if a conflict is detected
     */
    protected Map sourceUriConflictsResolution = null;

    public void setSourceUriConflictsResolution(Map conflictsResolution) {
        this.sourceUriConflictsResolution = conflictsResolution;
    }

    /**
     *  Gets the maps with the resolution defined in Sync4jStrategy
     */
    public Map getSourceUriConflictsResolution() {
        return this.sourceUriConflictsResolution;
    }

    /** The default conflict resolution  */
    protected int defaultConflictResolution = CONFLICT_RESOLUTION_SERVER_WINS;

    /**
     * Sets the default conflict resolution.
     * @param conflictResolution the resolution to set as default
     */
    public void setDefaultConflictResolution(int conflictResolution) {
        this.defaultConflictResolution = conflictResolution;
    }

    /**
     * Gets the value of the default conflict resolution
     * @return the value of the default conflict resolution
     */
    public int getDefaultConflictResolution() {
        return defaultConflictResolution;
    }

    /**
     * The process' name
     */
    public String getName() {
        return name;
    }

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    // ------------------------------------------------------------ Private data

    //
    // NOTE: server modification MUST be detected and stored at the beginning of
    // the sync process (i.e. in the first modification message), otherwise in
    // the case of multi message, likely the server will detect items modified
    // by the current sync as server modifications.
    // Reading server modifications at the synchronization start does not
    // loose items since. Let's suppose we have the following events axis:
    //
    //  s1            e1    s2              e2             s3          e3
    //  |-------------|....|---------------|...............|-----------|
    //
    // where s<x> is the beginning of a sync and e<x> represents its end.
    // In s2 we collect all modified items since s1; if any other process
    // modifies an item after s2, it won't be detected during the second
    // synchronization. However, it will be detected at s3, where we collect all
    // modified items since s2.
    //
    protected Map newBAll;
    protected Map updatedBAll;
    protected Map deletedBAll;
    protected Map BmAll; //used into Slow Sync

    /**
     * Collects statistics like the number of added/updated/deleted items
     */
    protected Map<String, SyncStatistic> syncStatistics;

    public SyncStatistic getSyncStatistics(String uri) {
        return syncStatistics.get(uri);
    }

    // ------------------------------------------------------------ Constructors

    public Sync4jStrategy() {
        newBAll = new HashMap();
        updatedBAll = new HashMap();
        deletedBAll = new HashMap();
        BmAll = new HashMap();
        sourceUriConflictsResolution = new HashMap();
        syncStatistics = new HashMap();
    }

    // ---------------------------------------------------------- Public methods

    /**
     * Preparation for the synchronization. If <i>sources</i> is not null,
     * the preparation works on the given sources. Otherwise it works on the
     * sources as they were set in the constructor.
     * Note that in the case of slow sync, the sync analysis to determine the
     * new items on the server that must be returned to the client, we need to
     * wait for the last call to this method. This is used for protocols that
     * supports a multi message session. At the contrary, client side
     * modifications can be processed immediately.
     *
     *
     * @param sources the sources to be synchronized
     * @param nextSync the timestamp of the beginning of this synchronization
     * @param principal the entity for which the synchronization is required
     * @param clientMapping the current mapping
     * @param syncFilterType the type of the filter used
     * @param last is this the last call to prepareSlowSync ?
     *
     * @return an array of SyncOperation, one for each SyncItem that must be
     *         created/updated/deleted or in conflict.
     *
     * @throws SyncException
     * @see com.funambol.framework.engine.SyncStrategy
     */
    public SyncOperation[] prepareSlowSync(SyncSource[] sources, Principal principal, ClientMapping clientMapping,
            int syncFilterType, Timestamp nextSync, boolean last) throws SyncException {

        SyncItem[] Am, Bm;
        List syncOperations = new ArrayList();

        FunambolLogger log = FunambolLoggerFactory.getLogger(LOG_NAME);

        if (log.isInfoEnabled()) {
            log.info("Preparing slow synchronization");
        }

        if (log.isTraceEnabled()) {
            if (last) {
                log.trace("Last message in the package");
            } else {
                log.trace("Not the last message in the package");
            }
        }

        //
        // First process the items provided by the client
        //
        Am = ((MemorySyncSource) sources[1]).getUpdatedSyncItems();

        String uri = sources[0].getSourceURI();
        if (BmAll.get(uri) == null) {
            SyncItemKey[] allKeys = sources[0].getAllSyncItemKeys();

            if (log.isTraceEnabled()) {
                log.trace("allItemsKeys: " + Util.arrayToString(allKeys));
            }

            Bm = EngineHelper.createSyncItems(allKeys, SyncItemState.NEW, sources[0], null, null);
            BmAll.put(uri, Bm);
        } else {
            Bm = (SyncItem[]) BmAll.get(uri);
        }

        //
        // Just in case...
        //
        if (Am == null) {
            Am = new SyncItem[0];
        }

        if (Bm == null) {
            Bm = new SyncItem[0];
        }

        //
        // If any item from the client has not a corresponding mapping, the
        // server source must be queried for the item, since the client item
        // could be the same of an existing server item. In this case, the old
        // unmapped item is replaced in Map by the newly mapped item.
        //
        List newlyMappedItems = new ArrayList();

        //
        // The validMapping is used by the fixMappedItems to know the mapping. It has
        // to know that because in the twins search we need to handle the parentKey
        // and moreover, knowing the mapping, the fixMappedItems is able to ignore
        // the twins already mapped. We can't use the object mapping (of type ClientMapping)
        // because the fixMappedItems has to handle also the mapping created by itself
        //
        Map validMapping = null;
        if (clientMapping != null) {
            validMapping = clientMapping.getValidMapping();
        } else {
            validMapping = new HashMap();
        }

        fixMappedItems(newlyMappedItems, Am, sources[0], validMapping, // in a slow sync, we haven't valid mapping
                principal);

        //
        // Because it is a slow sync, items state must be reset to N
        //
        EngineHelper.setState(Am, SyncItemState.NEW);
        EngineHelper.setState(Bm, SyncItemState.NEW);

        for (int i = 0; i < Am.length; ++i) {
            if (!((AbstractSyncItem) Am[i]).isMapped()) {
                syncOperations.add(checkSyncOperation(principal, nextSync, Am[i], null));
            }
        }

        Iterator j = newlyMappedItems.iterator();
        SyncItemMapping mapping = null;

        while (j.hasNext()) {
            mapping = (SyncItemMapping) j.next();

            syncOperations
                    .add(checkSyncOperation(principal, nextSync, mapping.getSyncItemA(), mapping.getSyncItemB()));
        }

        //
        // We have to remove from Bm the items already used in some operations because if
        // we have an item there, it's already handled
        //
        Bm = EngineHelper.removeItemsInOperations(Bm, syncOperations, false);
        BmAll.put(uri, Bm);

        //
        // If it is the last call, we have also to consider what is in source
        // B but not in A
        //
        if (last) {

            for (int i = 0; i < Bm.length; ++i) {

                syncOperations.add(checkSyncOperation(principal, nextSync, null, Bm[i]));

            }

        }

        if (log.isTraceEnabled()) {
            log.trace("operations: " + syncOperations);
        }

        if (log.isInfoEnabled()) {
            if (last) {
                log.info("Preparation completed (last message in the package)");
            } else {
                log.info("Preparation completed (not last message in the package)");
            }
        }

        return (SyncOperation[]) syncOperations.toArray(new SyncOperationImpl[] {});
    }

    /**
     * Preparation for fast synchronization. If <i>sources</i> is not null,
     * the preparation operates on the given sources. Otherwise it works on the
     * sources as they were set in the constructor.
     * <p>
     * Refer to the architecture document for details about the algoritm applied.
     *
     * @param sources the sources to be synchronized
     * @param principal the entity for which the synchronization is required
     * @param mapping the current mapping
     * @param syncFilterType the type of the filter used
     * @param lastSync timestamp of the last synchronization
     * @param nextSync timestamp of the current synchronization
     * @param lastAnchor the last anchor to use in the mapping handling. This is
     *        required in order to check the item already sent with the same
     *         lastAnchor (Suspende and Resume).
     * @param last is this the last call to prepareFastSync ?
     *
     * @return an array of SyncOperation, one for each SyncItem that must be
     *         created/updated/deleted or in conflict.
     *
     * @see com.funambol.framework.engine.SyncStrategy
     */
    public SyncOperation[] prepareFastSync(SyncSource[] sources, Principal principal, ClientMapping mapping,
            int syncFilterType, Timestamp lastSync, Timestamp nextSync, String lastAnchor, boolean last)
            throws SyncException {

        // ---------------------------------------------------------------------

        List Am, // items modified in A
                Bm, // items modified in B
                AmBm, // Am intersect Bm
                AAmBm, // items unmodified in A, but modified in B
                AmBBm; // items unmodified in B, but modified in A

        List syncOperations = null;

        SyncItem[] newA = null, newB = null, updatedA = null, updatedB = null, deletedA = null, deletedB = null;

        if (log.isInfoEnabled()) {
            log.info("Preparing fast synchronization since " + lastSync);
        }

        if (log.isTraceEnabled()) {
            if (last) {
                log.trace("Last message in the package");
            } else {
                log.trace("Not the last message in the package");
            }
        }

        // ---------------------------------------------------------------------

        //
        // NOTE: simplified version - only two sources, the second one of which
        // is the client
        //
        newA = ((MemorySyncSource) sources[1]).getNewSyncItems();
        updatedA = ((MemorySyncSource) sources[1]).getUpdatedSyncItems();
        deletedA = ((MemorySyncSource) sources[1]).getDeletedSyncItems();

        if (log.isTraceEnabled()) {
            log.trace("newA: " + Util.arrayToString(newA));
            log.trace("updatedA: " + Util.arrayToString(updatedA));
            log.trace("deletedA: " + Util.arrayToString(deletedA));
        }

        //
        // If there are some updated items not mapped, we have to handle them
        // as New item.
        // If there are some new items mapped, we have to handle them
        // as Updated item
        //
        EngineHelper.checkItemsState(newA);
        EngineHelper.checkItemsState(updatedA);

        String uri = sources[0].getSourceURI();
        if (newBAll.get(uri) == null) {

            if (log.isTraceEnabled()) {
                log.trace("Detecting server changes...");
            }

            SyncItemKey[] newBItemKeys = sources[0].getNewSyncItemKeys(lastSync, nextSync);
            SyncItemKey[] updateBItemKeys = sources[0].getUpdatedSyncItemKeys(lastSync, nextSync);
            SyncItemKey[] deleteBItemKeys = sources[0].getDeletedSyncItemKeys(lastSync, nextSync);

            if (newBItemKeys == null) {
                newBItemKeys = new SyncItemKey[0];
            }
            if (updateBItemKeys == null) {
                updateBItemKeys = new SyncItemKey[0];
            }
            if (deleteBItemKeys == null) {
                deleteBItemKeys = new SyncItemKey[0];
            }

            if (log.isTraceEnabled()) {
                log.trace("newBItemKeys: " + Util.arrayToString(newBItemKeys));
                log.trace("updateBItemKeys: " + Util.arrayToString(updateBItemKeys));
                log.trace("deleteBItemKeys: " + Util.arrayToString(deleteBItemKeys));
            }

            newB = EngineHelper.createSyncItems(newBItemKeys, SyncItemState.NEW, sources[0], mapping, lastAnchor);

            updatedB = EngineHelper.createSyncItems(updateBItemKeys, SyncItemState.UPDATED, sources[0], mapping,
                    lastAnchor);

            deletedB = EngineHelper.createSyncItems(deleteBItemKeys, SyncItemState.DELETED, sources[0], mapping,
                    lastAnchor);

            //
            // The SyncSource should return empty arrays... but just in case...
            //
            if (newB == null) {
                newB = new InMemorySyncItem[0];
            }
            if (updatedB == null) {
                updatedB = new InMemorySyncItem[0];
            }
            if (deletedB == null) {
                deletedB = new InMemorySyncItem[0];
            }

            if (log.isTraceEnabled()) {
                log.trace("newB: " + Util.arrayToString(newB));
                log.trace("updatedB: " + Util.arrayToString(updatedB));
                log.trace("deletedB: " + Util.arrayToString(deletedB));
            }

            newBAll.put(uri, newB);
            updatedBAll.put(uri, updatedB);
            deletedBAll.put(uri, deletedB);
        } else {
            newB = (SyncItem[]) newBAll.get(uri);
            updatedB = (SyncItem[]) updatedBAll.get(uri);
            deletedB = (SyncItem[]) deletedBAll.get(uri);
        }

        //
        // If any item from the client has not a corresponding mapping, the
        // server source must be queried for the item, since the client item
        // could be the same of an existing server item. In this case,  the old
        // unmapped item is replaced by the newly mapped item.
        //
        List newlyMappedItems = new ArrayList();

        //
        // The validMapping is used by the fixMappedItems to know the mapping. It has
        // to know that because in the twins search we need to handle the parentKey
        // and moreover, knowing the mapping, the fixMappedItems is able to ignore
        // the twins already mapped. We can't use the object mapping (of type ClientMapping)
        // because the fixMappedItems has to handle also the mapping created by itself
        //
        // prepareFastSync doesn't use the validMapping.
        //
        Map validMapping = null;
        if (mapping != null) {
            validMapping = mapping.getValidMapping();
        } else {
            validMapping = new HashMap();
        }

        fixMappedItems(newlyMappedItems, newA, sources[0], validMapping, principal);
        fixMappedItems(newlyMappedItems, updatedA, sources[0], validMapping, principal);
        fixMappedItems(newlyMappedItems, deletedA, sources[0], validMapping, principal);

        if (log.isTraceEnabled()) {
            log.trace("Newly mapped items: " + newlyMappedItems);
        }

        Am = new ArrayList();
        Bm = new ArrayList();
        Am.addAll(Arrays.asList(newA));
        Am.addAll(Arrays.asList(updatedA));
        Am.addAll(Arrays.asList(deletedA));
        Bm.addAll(Arrays.asList(newB));
        Bm.addAll(Arrays.asList(updatedB));
        Bm.addAll(Arrays.asList(deletedB));

        if (log.isTraceEnabled()) {
            log.trace("Am: " + Am);
            log.trace("Bm: " + Bm);
            log.trace("Am-Bm: " + ListUtils.subtract(Am, Bm));
            log.trace("Bm-Am: " + ListUtils.subtract(Bm, Am));
        }

        //
        // Now calculate subsets: AmBm, AmBBm, AAmBm.
        // Note that Bm and AAmBm must be calculated only when we got the
        // last message. For the others, it must be empty.
        //
        AmBm = EngineHelper.intersect(Am, Bm);

        //
        // We have to remove from AmBm the newlyMappedItems and then re-add them.
        // This is done because otherwise we can have the same mapping in AmBm and
        // in newlyMappedItems. However, we have to keep the mapping in the
        // newlyMappedItems replacing the one in the AmBm
        // because here we could have a merged item.
        //
        // Example: Conflict NEW-NEW: the server has to merge the items but in Bm
        // we have the item with the original data (before the merge operation)
        //
        //
        // The remove is based on SyncItemMapping.equals method.
        //
        AmBm.removeAll(newlyMappedItems);

        AmBm.addAll(newlyMappedItems);

        AmBBm = EngineHelper.buildAmBBm(ListUtils.subtract(Am, Bm), sources[0], principal);

        //
        // When a conflict is detected finding a twin, AmBm contains the mapping
        // with client item and server item. The server item is set with state 'U'
        // because otherwise we don't handle this situation as a conflict
        // (see fixMappedItems) also if indeed the server item is not updated (it
        // isn't in Bm).
        // But in this way, we have a wrong mapping in AmBBm because in Am with have
        // the client item with state 'U' and the server item isn't changed (it isn't
        // in Bm so the method buildAmBBm considers it as 'S').
        // So, from AmBBM we remove the mapping already in AmBm
        //
        AmBBm.removeAll(AmBm);

        AAmBm = new ArrayList();
        if (last) {

            //
            // If the server source is a FilterableSyncSource, we have to check
            // if there are some items outside the filter criteria to delete on
            // the client (if the filter is exclusive) or if there are some
            // server item to add on the client (maybe because the filter is changed)
            // For example, if the client syncs just the contacts with CompanyName
            // equals to "MyCompany" and then it changes the filter to CompanyName
            // equals to "MyCompany" or "YourCompany", maybe the server has to add
            // some items.
            //
            if (sources[0] instanceof FilterableSyncSource) {
                SyncItemKey[] allItemsKeys = sources[0].getAllSyncItemKeys();

                if (log.isTraceEnabled()) {
                    log.trace("allItemsKeys: " + Util.arrayToString(allItemsKeys));
                }

                checkForItemsToDelete(sources, syncFilterType, allItemsKeys, mapping, Bm);

                checkForItemsToAdd(sources, allItemsKeys, mapping, Bm);
            }

            AAmBm = EngineHelper.buildAAmBm(ListUtils.subtract(Bm, Am), sources[1], principal);
        } else {
            Bm.clear();
        }

        if (log.isTraceEnabled()) {
            log.trace("AmBm:  " + AmBm);
            log.trace("AmBBm: " + AmBBm);
            log.trace("AAmBm: " + AAmBm);
        }

        //
        // Ready for conflict detection!
        //
        syncOperations = checkSyncOperations(principal, nextSync, Am, Bm, AmBm, AmBBm, AAmBm);

        if (log.isTraceEnabled()) {
            log.trace("operations: " + syncOperations);
        }

        //
        // We have to remove from xxxB the items in newlyMappedItems because if
        // we have an item here, it's already handled
        //
        newB = EngineHelper.removeItemsInOperations(newB, syncOperations, false);
        updatedB = EngineHelper.removeItemsInOperations(updatedB, syncOperations, false);
        deletedB = EngineHelper.removeItemsInOperations(deletedB, syncOperations, false);

        newBAll.put(uri, newB);
        updatedBAll.put(uri, updatedB);
        deletedBAll.put(uri, deletedB);

        if (log.isInfoEnabled()) {
            if (last) {
                log.info("Preparation completed (last message in the package)");
            } else {
                log.info("Preparation completed (not last message in the package)");
            }
        }

        return (SyncOperation[]) syncOperations.toArray(new SyncOperationImpl[] {});
    }

    /**
     * Preparation for Smart-One-Way-From-Client synchronization.
     * <p>
     * Refer to the architecture document for details about the algoritm applied.
     *
     * @param sources the sources to be synchronized
     * @param principal the entity for which the synchronization is required
     * @param mapping the current mapping
     * @param syncFilterType the type of the filter used
     * @param nextSync timestamp of the current synchronization
     * @param last is this the last call to prepareFastSync ?
     *
     * @return an array of SyncOperation, one for each SyncItem that must be
     *         created/updated/deleted or in conflict.
     *
     * @see com.funambol.framework.engine.SyncStrategy
     */
    public SyncOperation[] prepareSmartOneWayFromClient(SyncSource[] sources, Principal principal,
            ClientMapping mapping, int syncFilterType, Timestamp nextSync, boolean last) throws SyncException {

        // ---------------------------------------------------------------------
        MemorySyncSource clientSynSource = (MemorySyncSource) sources[1];
        SyncSource serverSyncSource = sources[0];

        if (log.isInfoEnabled()) {
            log.info("Preparing smart one way from client synchronization");
        }

        if (log.isTraceEnabled()) {
            if (last) {
                log.trace("Last message in the package");
            } else {
                log.trace("Not the last message in the package");
            }
        }

        // ---------------------------------------------------------------------

        // getClientNewItems()
        SyncItem[] newA = clientSynSource.getNewSyncItems();
        if (log.isTraceEnabled()) {
            log.trace("newA: " + Util.arrayToString(newA));
        }
        // If there are some new items mapped, we have to handle them
        // as Updated item
        EngineHelper.checkItemsState(newA);

        // getClientUpdatedItems()
        SyncItem[] updatedA = clientSynSource.getUpdatedSyncItems();
        if (log.isTraceEnabled()) {
            log.trace("updatedA: " + Util.arrayToString(updatedA));
        }
        // If there are some updated items not mapped, we have to handle them
        // as New item.
        EngineHelper.checkItemsState(updatedA);

        // getClientDeletedItems()
        SyncItem[] deletedA = clientSynSource.getDeletedSyncItems();
        if (log.isTraceEnabled()) {
            log.trace("deletedA: " + Util.arrayToString(deletedA));
        }

        String uri = serverSyncSource.getSourceURI();

        if (newBAll.get(uri) == null) {
            if (log.isTraceEnabled()) {
                log.trace("Detecting server changes...");
            }
        }

        // get Server New Items
        SyncItem[] newB = null;
        if (newBAll.get(uri) == null) {
            newB = new InMemorySyncItem[0];
            newBAll.put(uri, newB);
        } else {
            newB = (SyncItem[]) newBAll.get(uri);
        }

        // get Server Updated Items
        SyncItem[] updatedB = null;
        if (updatedBAll.get(uri) == null) {
            updatedB = new InMemorySyncItem[0];
            updatedBAll.put(uri, updatedB);
        } else {
            updatedB = (SyncItem[]) updatedBAll.get(uri);
        }

        // get Server Deleted Items
        SyncItem[] deletedB = null;
        if (deletedBAll.get(uri) == null) {
            deletedB = new InMemorySyncItem[0];
            deletedBAll.put(uri, deletedB);
        } else {
            deletedB = (SyncItem[]) deletedBAll.get(uri);
        }

        //
        // calculate Am
        //
        List Am = new ArrayList();
        Am.addAll(Arrays.asList(newA));
        Am.addAll(Arrays.asList(updatedA));
        Am.addAll(Arrays.asList(deletedA));
        if (log.isTraceEnabled()) {
            log.trace("Am: " + Am);
        }

        //
        // calculate Bm
        //
        List Bm = new ArrayList();

        //
        // Calculate AmBm
        //
        List AmBm;
        {
            AmBm = new ArrayList();
            //
            // If any item from the client has not a corresponding mapping, the
            // server source must be queried for the item, since the client item
            // could be the same of an existing server item. In this case,  the old
            // unmapped item is replaced by the newly mapped item.
            //
            // List of SyncItemMapping
            //
            List newlyMappedItems;
            {
                newlyMappedItems = new ArrayList();
                //
                // The validMapping is used by the fixMappedItems to know the mapping. It has
                // to know that because in the twins search we need to handle the parentKey
                // and moreover, knowing the mapping, the fixMappedItems is able to ignore
                // the twins already mapped. We can't use the object mapping (of type ClientMapping)
                // because the fixMappedItems has to handle also the mapping created by itself
                //
                Map validMapping = null;
                if (mapping != null) {
                    validMapping = mapping.getValidMapping();
                } else {
                    validMapping = new HashMap();
                }

                fixMappedItems(newlyMappedItems, newA, serverSyncSource, validMapping, principal);
                fixMappedItems(newlyMappedItems, updatedA, serverSyncSource, validMapping, principal);
                fixMappedItems(newlyMappedItems, deletedA, serverSyncSource, validMapping, principal);

                if (log.isTraceEnabled()) {
                    log.trace("Newly mapped items: " + newlyMappedItems);
                }
            }

            // All the newly mapped items are considered modified (are set to
            // UPDATED in fixMappedItems) and hince have to be added to AmBm
            //
            // We have to remove from AmBm the newlyMappedItems and then re-add them.
            // This is done because otherwise we can have the same mapping in AmBm and
            // in newlyMappedItems. However, we have to keep the mapping in the
            // newlyMappedItems replacing the one in the AmBm
            // because here we could have a merged item.
            //
            // Example: Conflict NEW-NEW: the server has to merge the items but in Bm
            // we have the item with the original data (before the merge operation)
            //
            //
            // The remove is based on SyncItemMapping.equals method.
            //
            AmBm.removeAll(newlyMappedItems);
            AmBm.addAll(newlyMappedItems);
            if (log.isTraceEnabled()) {
                log.trace("AmBm:  " + AmBm);
            }
        }

        //
        // calculate AmBBm
        // items modified in A (client) but not modified in B (server)
        //
        List AmBBm = EngineHelper.buildAmBBm(ListUtils.subtract(Am, Bm), serverSyncSource, principal);
        //
        // When a conflict is detected finding a twin, AmBm contains the mapping
        // with client item and server item. The server item is set with state 'U'
        // because otherwise we don't handle this situation as a conflict
        // (see fixMappedItems) also if indeed the server item is not updated (it
        // isn't in Bm).
        // But in this way, we have a wrong mapping in AmBBm because in Am with have
        // the client item with state 'U' and the server item isn't changed (it isn't
        // in Bm so the method buildAmBBm considers it as 'S').
        // So, from AmBBM we remove the mapping already in AmBm
        //
        AmBBm.removeAll(AmBm);
        if (log.isTraceEnabled()) {
            log.trace("AmBBm: " + AmBBm);
        }

        //
        // calculate AAmBm
        // items not modified in A (client) but modified in B (server)
        // Note. AAmBm must be calculated only when we got the
        // last message. For the other messages it must be empty.
        List AAmBm = new ArrayList();

        //
        // Ready for conflict detection!
        //
        List syncOperations = checkSyncOperations(principal, nextSync, Am, Bm, AmBm, AmBBm, AAmBm);

        if (log.isTraceEnabled()) {
            log.trace("operations: " + syncOperations);
        }

        //
        // We have to remove from xxxB the items in newlyMappedItems because if
        // we have an item here, it's already handled
        //
        newB = EngineHelper.removeItemsInOperations(newB, syncOperations, false);
        updatedB = EngineHelper.removeItemsInOperations(updatedB, syncOperations, false);
        deletedB = EngineHelper.removeItemsInOperations(deletedB, syncOperations, false);

        newBAll.put(uri, newB);
        updatedBAll.put(uri, updatedB);
        deletedBAll.put(uri, deletedB);

        if (log.isInfoEnabled()) {
            if (last) {
                log.info("Preparation completed (last message in the package)");
            } else {
                log.info("Preparation completed (not last message in the package)");
            }
        }

        return (SyncOperation[]) syncOperations.toArray(new SyncOperationImpl[] {});
    }

    /**
     * Implements Synchronizable.sync
     */
    public SyncOperationStatus[] sync(SyncSource[] sources, boolean slowSync, ClientMapping mapping,
            String lastAnchor, SyncOperation[] syncOperations, int syncFilterType) {

        if (log.isInfoEnabled()) {
            log.info("Synchronizing...");
        }

        if ((syncOperations == null) || (syncOperations.length == 0)) {
            return new SyncOperationStatus[0];
        }

        List status = new ArrayList();

        SyncOperationStatus[] operationStatus = null;
        for (int i = 0; i < syncOperations.length; ++i) {
            if (log.isTraceEnabled()) {
                log.trace("Executing " + syncOperations[i]);
            }

            //
            // execSyncOperation can return more than one status for one
            // operation when more than one source are involved
            //
            operationStatus = execSyncOperation(sources, slowSync, mapping, lastAnchor,
                    (SyncOperationImpl) syncOperations[i], syncFilterType);

            for (int j = 0; j < operationStatus.length; ++j) {
                status.add(operationStatus[j]);
                if (log.isTraceEnabled()) {
                    log.trace("status: " + operationStatus[j]);
                }
            } // next j
        } // next i

        return (SyncOperationStatus[]) status.toArray(new SyncOperationStatus[0]);
    }

    /**
     * Implements Synchronizable.endSync
     */
    public void endSync() throws SyncException {
        // nothing to do
    }

    /**
     * Returns the conflict resolution to apply on the given source.
     * <p>
     * If the source is a <code>ServerWinsSyncSource</code>,
     * CONFLICT_RESOLUTION_SERVER_WINS is returned.
     * <br/>
     * If the source is a <code>ClientWinsSyncSource</code>,
     * CONFLICT_RESOLUTION_CLIENT_WINS is returned.
     * <br/>
     * If the source is a <code>MergeableSyncSource</code>,
     * CONFLICT_RESOLUTION_MERGE_DATA is returned.
     * <br/>
     * If the source is "just" a <code>SyncSource</code>, the configured conflict
     * resolution is returned. If the configured value is CONFLICT_RESOLUTION_MERGE_DATA,
     * the default conflict resolution is returned (CONFLICT_RESOLUTION_MERGE_DATA is
     * applicable jsut with MergeableSyncSource).
     * <br/>
     * <p>
     * @param sourceUri the source uri
     * @return the conflict resolution to apply.
     */
    public int getConflictResolution(SyncSource serverSource) {

        String sourceUri = serverSource.getSourceURI();

        if (serverSource instanceof MergeableSyncSource) {
            if (log.isTraceEnabled()) {
                log.trace("The syncsource '" + sourceUri + "' is a MergeableSyncSource, so "
                        + getConflictResolutionDescription(CONFLICT_RESOLUTION_MERGE_DATA) + " is used");
            }
            return CONFLICT_RESOLUTION_MERGE_DATA;
        } else if (serverSource instanceof ServerWinsSyncSource) {
            if (log.isTraceEnabled()) {
                log.trace("The syncsource '" + sourceUri + "' is a ServerWinsSyncSource, so "
                        + getConflictResolutionDescription(CONFLICT_RESOLUTION_SERVER_WINS) + " is used");
            }
            return CONFLICT_RESOLUTION_SERVER_WINS;
        } else if (serverSource instanceof ClientWinsSyncSource) {
            if (log.isTraceEnabled()) {
                log.trace("The syncsource '" + sourceUri + "' is a ClientWinsSyncSource, so "
                        + getConflictResolutionDescription(CONFLICT_RESOLUTION_CLIENT_WINS) + " is used");
            }
            return CONFLICT_RESOLUTION_CLIENT_WINS;
        }

        Integer conflictResolution = (Integer) sourceUriConflictsResolution.get(sourceUri);

        int value = -2; // we use -2 because -1 means DEFAULT_VALUE

        if (conflictResolution != null) {

            value = conflictResolution.intValue();

            if (value != CONFLICT_RESOLUTION_CLIENT_WINS && value != CONFLICT_RESOLUTION_SERVER_WINS
                    && value != CONFLICT_RESOLUTION_MERGE_DATA && value != CONFLICT_RESOLUTION_DEFAULT) {

                value = getDefaultConflictResolution();

                if (log.isTraceEnabled()) {
                    log.trace("The configured value (" + value + ") is not valid, so the default value '"
                            + getConflictResolutionDescription(value) + "' is used");
                }

                return value;

            } else if (value == CONFLICT_RESOLUTION_MERGE_DATA && !(serverSource instanceof MergeableSyncSource)) {

                value = getDefaultConflictResolution();

                if (log.isTraceEnabled()) {
                    log.trace("The " + getConflictResolutionDescription(value)
                            + " is applicable only with a MergeableSyncSource and '" + sourceUri
                            + "' is not MergeableSyncSource. The default value "
                            + getConflictResolutionDescription(value) + "' is used");
                }

                return value;

            } else if (value == CONFLICT_RESOLUTION_DEFAULT) {

                value = getDefaultConflictResolution();
                if (log.isTraceEnabled()) {
                    log.trace("Default conflict resolution configured for '" + sourceUri
                            + "'. The default value is " + getConflictResolutionDescription(value));
                }

                return value;

            } else {

                if (log.isTraceEnabled()) {
                    log.trace("Configured conflict resolution for '" + sourceUri + "': "
                            + getConflictResolutionDescription(value));
                }

                return value;

            }
        } // conflictResolution != null

        value = getDefaultConflictResolution();
        if (log.isTraceEnabled()) {
            log.trace("Conflict resolution not configured for '" + sourceUri + "'. The default value is used ("
                    + getConflictResolutionDescription(value) + ")");
        }

        return value;
    }

    // -------------------------------------------------------- Protected methds

    /**
     * Checks the given SyncItem lists and creates the needed SyncOperations
     * following the rules described in the class description and in the
     * architecture document.
     *
     * @param principal who has requested the synchronization
     * @param nextSync timestamp of the current synchronization
     * @param Am the Am set
     * @param Bm the Bm set
     * @param AmBm the AmBm
     * @param AmBBm the AmBBm
     * @param AAmBm the AAmBm
     *
     * @return an List containing all the collected sync operations
     */
    protected List checkSyncOperations(Principal principal, Timestamp nextSync, List Am, List Bm, List AmBm,
            List AmBBm, List AAmBm) {
        SyncItemMapping mapping = null;
        SyncItem syncItemA = null, syncItemB = null;

        List all = new ArrayList();
        List operations = new ArrayList();

        // ---------------------------------------------------------------------

        all.addAll(AmBm);
        all.addAll(AmBBm);
        all.addAll(AAmBm);

        //
        // 1st check: items in both sources
        //
        Iterator i = all.iterator();
        while (i.hasNext()) {
            mapping = (SyncItemMapping) i.next();

            syncItemA = mapping.getSyncItemA();
            syncItemB = mapping.getSyncItemB();

            operations.add(checkSyncOperation(principal, nextSync, syncItemA, syncItemB));
            Am.remove(syncItemA);
            Bm.remove(syncItemB);
        }

        //
        // 2nd check: items in source A and not in source B
        //
        i = Am.iterator();
        while (i.hasNext()) {
            syncItemA = (SyncItem) i.next();

            operations.add(checkSyncOperation(principal, nextSync, syncItemA, null));
        } // next i

        //
        // 3rd check: items in source B and not in source A
        //
        i = Bm.iterator();
        while (i.hasNext()) {
            syncItemB = (SyncItem) i.next();

            operations.add(checkSyncOperation(principal, nextSync, null, syncItemB));
        } // next i

        return operations;
    }

    /**
     * Create a SyncOperation based on the state of the given SyncItem couple.
     *
     * @param principal     the entity that wnats to do the operation
     * @param nextSync      timestamp of the current synchronization (used for "B" operation)
     * @param syncItemA     the SyncItem of the source A - NULL means <i>not existing</i/
     * @param syncItemB     the SyncItem of the source B - NULL means <i>not existing</i/
     *
     * @return the SyncOperation object
     */
    protected SyncOperation checkSyncOperation(Principal principal, Timestamp nextSync, SyncItem syncItemA,
            SyncItem syncItemB) {
        if (log.isTraceEnabled()) {
            log.trace("check: syncItemA: " + syncItemA + " syncItemB: " + syncItemB);
        }

        if (syncItemA == null) {
            syncItemA = InMemorySyncItem.getNotExistingSyncItem(null);
        }
        if (syncItemB == null) {
            syncItemB = InMemorySyncItem.getNotExistingSyncItem(null);
        }

        switch (syncItemA.getState()) {
        //
        // NEW
        //
        case SyncItemState.NEW:
            switch (syncItemB.getState()) {
            case SyncItemState.NEW:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_NEW_NEW);
            case SyncItemState.UPDATED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_NEW_UPDATED);
            case SyncItemState.DELETED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_NEW_DELETED);

            case SyncItemState.SYNCHRONIZED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_NEW_SYNCHRONIZED);

            case SyncItemState.NOT_EXISTING:
                syncItemA.setTimestamp(nextSync);
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NEW, false, true);
            } // end inner switch

            //
            // DELETED
            //
        case SyncItemState.DELETED:
            switch (syncItemB.getState()) {
            case SyncItemState.NEW:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_DELETED_NEW);

            case SyncItemState.UPDATED:
                syncItemB.setState(SyncItemState.NEW);
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_DELETED_UPDATED);

            case SyncItemState.SYNCHRONIZED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.DELETE, false, true);

            case SyncItemState.DELETED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_DELETED_DELETED);

            case SyncItemState.NOT_EXISTING:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NOP, false, false);
            } // end inner switch

            //
            // UPDATED
            //
        case SyncItemState.UPDATED:
            switch (syncItemB.getState()) {
            case SyncItemState.NEW:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_UPDATED_NEW);

            case SyncItemState.UPDATED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_UPDATED_UPDATED);

            case SyncItemState.DELETED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_UPDATED_DELETED);
            case SyncItemState.SYNCHRONIZED:
                syncItemA.setTimestamp(nextSync);
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.UPDATE, false, true);
            case SyncItemState.NOT_EXISTING:
                syncItemA.setTimestamp(nextSync);
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NEW, false, true);
            } // end inner switch

            //
            // SYNCHRONIZED
            //
        case SyncItemState.SYNCHRONIZED:
            switch (syncItemB.getState()) {
            case SyncItemState.NEW:
            case SyncItemState.UPDATED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.UPDATE, true, false);
            case SyncItemState.DELETED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.DELETE, true, false);
            case SyncItemState.SYNCHRONIZED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NOP, false, false);
            case SyncItemState.NOT_EXISTING:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NEW, false, true);
            } // end inner switch

            //
            // NOTEXISITNG
            //
        case SyncItemState.NOT_EXISTING:
            switch (syncItemB.getState()) {
            case SyncItemState.NEW:
            case SyncItemState.UPDATED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NEW, true, false);
            case SyncItemState.SYNCHRONIZED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NEW, true, false);
            case SyncItemState.NOT_EXISTING:
            case SyncItemState.DELETED:
                return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NOP, false, false);
            } // end inner switch

            //
            // PARTIAL
            //
        case SyncItemState.PARTIAL:
            switch (syncItemB.getState()) {
            case SyncItemState.NEW:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_UPDATED_NEW);

            case SyncItemState.UPDATED:
                return new SyncOperationImpl(principal, syncItemA, null, SyncOperation.ACCEPT_CHUNK, false, true);

            case SyncItemState.DELETED:
                return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_UPDATED_DELETED);

            case SyncItemState.SYNCHRONIZED:
                return new SyncOperationImpl(principal, syncItemA, null, SyncOperation.ACCEPT_CHUNK, false, true);
            case SyncItemState.NOT_EXISTING:
                return new SyncOperationImpl(principal, syncItemA, null, SyncOperation.ACCEPT_CHUNK, false, true);
            } // end inner switch

            //
            // CONFLICT
            // In this case itemA has a mapped twin and itemB not existing
            //
        case SyncItemState.CONFLICT:
            return new SyncConflict(principal, syncItemA, syncItemB, SyncConflict.STATE_CONFLICT_NONE);
        } // end switch

        return new SyncOperationImpl(principal, syncItemA, syncItemB, SyncOperation.NOP, false, false);
    }

    /**
     * Executes the given SyncOperation. Note that conflicts are ignored!
     * <p>
     * Note also that the number of status returned is equal to the number of
     * sources affected by the operation (0 for NOP, 1 for NEW and UPDATE and
     * 1 or 2 for DELETE).
     *
     * @param sources the syncsources relative to the operation
     * @param slowSync is this sync a slow sync ?
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation the SyncOperation to execute
     * @param syncFilterType the type of the filter used in the sync
     *
     * @return an array of <i>SyncOperationStatus</i> objects representing the
     *         status of the executed operation. For instance, in case of error,
     *         <i>SyncOperation.error</i> will be set to the catched exception.
     */
    protected SyncOperationStatus[] execSyncOperation(SyncSource[] sources, boolean slowSync, ClientMapping mapping,
            String lastAnchor, SyncOperationImpl operation, int syncFilterType) {

        SyncOperationStatus[] status = null;

        if (operation.getOperation() != SyncOperation.ACCEPT_CHUNK) {
            //
            // Converts the parent luid in parent guid if the parent is mapped
            //
            fixParent(mapping, operation);
        }

        switch (operation.getOperation()) {
        case SyncOperation.NEW:

            status = execNewOperation(sources, mapping, lastAnchor, operation, syncFilterType);
            break;

        case SyncOperation.UPDATE:

            status = execUpdateOperation(sources, mapping, lastAnchor, operation, syncFilterType);

            break;

        case SyncOperation.DELETE:

            status = execDeleteOperation(sources, mapping, lastAnchor, operation, syncFilterType);

            break;

        case SyncOperation.NOP:

            status = execNOPOperation(sources, mapping, lastAnchor, operation, syncFilterType);

            break;

        case SyncOperation.ACCEPT_CHUNK:

            status = execAcceptChunkOperation(sources, mapping, lastAnchor, operation, syncFilterType);

            break;

        case SyncOperation.CONFLICT:

            status = execConflict(sources, mapping, lastAnchor, operation, syncFilterType);

            break;

        } // end switch

        updateMapping(slowSync, mapping, lastAnchor, operation);

        return status;
    }

    // --------------------------------------------------------- Private methods

    /**
     * If any item from the client has not a corresponding mapping, the
     * server source must be queried for the item, since the client item
     * could be the same of an existing server item. In this case, the old
     * unmapped item is replaced in Am by the newly mapped item.
     *
     * BTW, we have to know the valid client mapping because in the twins search
     * we need to handle the parentKey and moreover, knowing the mapping,
     * we are able to ignore the twins already mapped.
     *
     *
     * @param newlyMappedItems the collection that will contain the newly mapped
     *        items. If null, it won't be used.
     * @param syncItems the items to be searched if not mapped. If an item is
     * newly mapped then the old unmapped item is replaced by the newly mapped
     * item.
     * @param source the source that has to be queried for twins
     * @param validClientMapping the valid client mapping. The newly mapped items
     * found are added to the validClientMapping
     *
     * @throws SyncSourceException if an error occurs
     */
    private void fixMappedItems(Collection newlyMappedItems, SyncItem[] syncItems, SyncSource source,
            Map validClientMapping, Principal principal) throws SyncSourceException {

        if (validClientMapping == null) {
            validClientMapping = new HashMap();
        }

        SyncItem itemB = null;

        SyncItemKey[] twinsKey = null;
        for (int i = 0; ((syncItems != null) && (i < syncItems.length)); ++i) {
            itemB = null;
            twinsKey = null;
            //
            // We have to exclude the item already mapped, the item not complete
            // and the item deleted (this items don't contain data)
            //
            if ((!((AbstractSyncItem) syncItems[i]).isMapped())
                    && (SyncItemState.PARTIAL != syncItems[i].getState())
                    && (SyncItemState.DELETED != syncItems[i].getState())) {

                try {

                    //
                    // In order to check the twins, we have to fix the parentKey.
                    // In order to avoid to change the client item directly, we
                    // create a cloneItem of the item. In this way the original client
                    // item doesn't change.
                    //
                    AbstractSyncItem clone = (AbstractSyncItem) (syncItems[i].cloneItem());
                    fixParent(validClientMapping, clone);
                    twinsKey = source.getSyncItemKeysFromTwin(clone);
                    if (twinsKey == null || twinsKey.length == 0) {
                        //
                        // No twin found
                        //
                        continue;
                    } else {
                        //
                        // Search for an item not mapped yet.
                        //
                        SyncItemKey twinKey = findNotMappedItem(validClientMapping, twinsKey);

                        if (twinKey == null) {
                            //
                            // No twin found
                            //
                            continue;
                        }

                        itemB = new InMemorySyncItem(source, twinKey.getKeyAsString());
                    }

                } catch (Exception e) {
                    if (log.isWarningEnabled()) {
                        String msg = "Error retrieving the twin item of " + syncItems[i].getKey() + " from source "
                                + source;

                        log.warn(msg, e);
                    }
                    continue;
                }

                if (log.isTraceEnabled()) {
                    log.trace("Hey, client item " + syncItems[i].getKey().getKeyAsString()
                            + " is the same of server item " + itemB.getKey().getKeyAsString()
                            + "! We have to resolve the conflict");
                }

                //
                // We add the mapping in the validClientMapping because we are in
                // a "for" cycle and in this way we are able to handle the mapping found
                // previously.
                //
                validClientMapping.put(itemB.getKey().getKeyAsString(), syncItems[i].getKey().getKeyAsString());

                InMemorySyncItem newItemB = new InMemorySyncItem(itemB.getSyncSource(),
                        itemB.getKey().getKeyAsString(), null, syncItems[i].getKey().getKeyAsString(),
                        syncItems[i].getState());

                //
                // We set the newItemB's state to UPDATED because in this way we
                // have a conflict (and so we can resolve the conflict). If
                // we set the state to 'S', we have an U-S and this isn't a conflict
                // and the client data replaces the server data (execUpdateOperation).
                //
                newItemB.setState(SyncItemState.UPDATED);

                SyncItemMapping mapping = new SyncItemMapping(newItemB.getKey());
                syncItems[i] = SyncItemHelper.newMappedSyncItem(newItemB.getKey(), (AbstractSyncItem) syncItems[i]);

                mapping.setMapping(syncItems[i], newItemB);
                if (newlyMappedItems != null) {
                    newlyMappedItems.add(mapping);
                }
            }
        } // next i
    }

    /**
     * Verifies that the given item the size of its content metches the
     * specified size, if specified. In the case of a mismatch, a
     * <code>SyncSourceException</code> is thrown with status code
     * OBJECT_SIZE_MISMATCH (424).
     *
     * @param item item to check
     *
     * @throws ObjectSizeMismatchException if the content size does not match
           the declared size (if declared)
     */
    private void checkSize(SyncItem item) throws ObjectSizeMismatchException {

        if (log.isTraceEnabled()) {
            log.trace("Check item size");
        }

        Long size = (Long) ((AbstractSyncItem) item).getPropertyValue(AbstractSyncItem.PROPERTY_DECLARED_SIZE);

        if (size == null) {
            if (log.isTraceEnabled()) {
                log.trace("Declared size is null...skip the check");
            }

            return;
        }

        if (log.isTraceEnabled()) {
            log.trace("Declared size: " + size);
        }

        Long contentSize = (Long) ((AbstractSyncItem) item)
                .getPropertyValue(AbstractSyncItem.PROPERTY_CONTENT_SIZE);

        if (contentSize == null) {
            contentSize = new Long(item.getContent().length);
        }

        if (log.isTraceEnabled()) {
            log.trace("Content size: " + contentSize);
        }

        //
        // We cannot check the content lenght (content.lenght) because the items
        // are already handled by the DataTransformationManager in the Sync4jEngine
        // See Sync4jEngine.applyDataTransformationOnIngoingItems
        //
        if (contentSize == null || !size.equals(contentSize)) {

            if (log.isTraceEnabled()) {
                log.trace("Size mismatch. The size of the received object (" + contentSize
                        + ") does not match the declared size (" + size + ")");
            }

            throw new ObjectSizeMismatchException("The size of the received object does not match the given size");
        }
    }

    /**
     * Executes a conflict and returns the right status.
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     *
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflict(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = null;

        try {
            SyncConflict sc = (SyncConflict) operation;

            //
            //Type CX means that itemA is in state CONFLICT and itemB is in state NOT_EXISTING
            //This situation happens when itemA has already a twin mapped
            //
            if (SyncConflict.STATE_CONFLICT_NONE.equals(sc.getType())) {

                status = execConflictCX(sources, mapping, lastAnchor, operation, syncFilterType);

            } else if (SyncConflict.STATE_DELETED_DELETED.equals(sc.getType())) {

                status = execConflictDD(sources, mapping, lastAnchor, operation, syncFilterType);

            } else if (SyncConflict.STATE_NEW_NEW.equals(sc.getType())
                    || SyncConflict.STATE_NEW_UPDATED.equals(sc.getType())) {

                status = execConflictNU(sources, mapping, lastAnchor, operation, syncFilterType);

            } else if (SyncConflict.STATE_UPDATED_UPDATED.equals(sc.getType())
                    || SyncConflict.STATE_UPDATED_NEW.equals(sc.getType())) {

                status = execConflictUU(sources, mapping, lastAnchor, operation, syncFilterType);

            } else if (SyncConflict.STATE_UPDATED_DELETED.equals(sc.getType())) {

                status = execConflictUD(sources, mapping, lastAnchor, operation, syncFilterType);

            } else if (SyncConflict.STATE_NEW_SYNCHRONIZED.equals(sc.getType())) {

                status = execConflictNS(sources, mapping, lastAnchor, operation, syncFilterType);

            } else if (SyncConflict.STATE_DELETED_NEW.equals(sc.getType())
                    || SyncConflict.STATE_DELETED_UPDATED.equals(sc.getType())) {

                status = execConflictDU(sources, mapping, lastAnchor, operation, syncFilterType);

            } else {

                status = execConflictNotSpecified(sources, mapping, lastAnchor, operation, syncFilterType);

            }

        } catch (SyncException e) {
            log.error("Error executing sync operation", e);

            status[0] = new Sync4jOperationStatusError(operation, sources[1], cmd, e);
            operation.setAOperation(false);
            operation.setBOperation(false);
        }

        return status;
    }

    /**
     * Executes a conflict DU and returns the right status
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictDU(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        SyncItemKey luid = ((AbstractSyncItem) syncItemA).getMappedKey();
        if (luid == null) {
            throw new SyncException(
                    "Item A with key " + syncItemA.getKey() + " should have a mapping, but the mapped key is null");
        }

        int conflictResolution = getConflictResolution(syncItemB.getSyncSource());

        if (conflictResolution == CONFLICT_RESOLUTION_CLIENT_WINS) {
            //
            // We have to delete the item on the server because the client has
            // deleted the item. The changes on the server item will be lose
            //
            sources[0].removeSyncItem(syncItemB.getKey(), syncItemA.getTimestamp(), false // softDelete ?
            );

            status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, StatusCode.OK);

            operation.setAOperation(false);
            operation.setBOperation(true);
            incrementDeletedItemsServer(sources[0].getSourceURI());
            return status;
        }

        //
        // If the conflict resolution is not CONFLICT_RESOLUTION_CLIENT_WINS,
        // the server has to re-add the item on the client
        // If we are using some filter, the server has to re-add the item ONLY if
        // it is in the filter criteria
        //
        boolean isItemInFilter = true;

        if (sources[0] instanceof FilterableSyncSource) {
            if (syncFilterType != SYNC_WITHOUT_FILTER) {
                //
                // We have to use the key because we don't have the content
                //
                isItemInFilter = ((FilterableSyncSource) sources[0]).isSyncItemInFilterClause(syncItemB.getKey());

                if (!isItemInFilter) {

                    status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd,
                            StatusCode.ITEM_NOT_DELETED);

                    operation.setAOperation(false);
                    operation.setBOperation(false);

                    return status;

                }
            }
        }

        //
        // In all others cases we re-add the item on the client
        // (with status code 419)
        //
        AbstractSyncItem resolvingItem = SyncItemHelper.newResolvingClientItem((AbstractSyncItem) syncItemB,
                syncItemA);

        if (resolvingItem.isMapped()) {
            syncItemA = (AbstractSyncItem) (sources[1].updateSyncItem(resolvingItem));
            incrementUpdatedItemsClient(sources[1].getSourceURI());
        } else {
            syncItemA = (AbstractSyncItem) (sources[1].addSyncItem(resolvingItem));
            incrementAddedItemsOnClient(sources[1].getSourceURI());
        }

        operation.setSyncItemA(syncItemA);
        operation.setAOperation(true);

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd,
                StatusCode.CONFLICT_RESOLVED_WITH_SERVER_DATA);

        return status;
    }

    /**
     * Executes a conflict DD and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictDD(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, StatusCode.ITEM_NOT_DELETED);

        return status;
    }

    /**
     * Executes a conflict CX and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictCX(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        sources[1].removeSyncItem(syncItemA.getMappedKey(), syncItemA.getTimestamp(), false); // it isn't a softDelete

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd,
                StatusCode.CONFLICT_RESOLVED_WITH_SERVER_DATA);
        incrementDeletedItemsClient(sources[1].getSourceURI());

        return status;
    }

    /**
     * Executes a conflict NS and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictNS(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        //
        // This happens when the Client sent an item that have a twin
        // synchronized on server.
        //

        SyncItemKey luid = ((AbstractSyncItem) syncItemA).getMappedKey();
        if (luid == null) {
            throw new SyncException(
                    "Item A with key " + syncItemA.getKey() + " should have a mapping, but the mapped key is null");
        }

        if (sources[0] instanceof FilterableSyncSource) {
            if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER
                    && !((FilterableSyncSource) sources[0]).isSyncItemInFilterClause(syncItemA)) {

                sources[1].removeSyncItem(luid, syncItemB.getTimestamp(), false); // it isn't a softDelete

                operation.setDeleteForced(true);
                operation.setAOperation(true);
                incrementDeletedItemsClient(sources[1].getSourceURI());

            } else {
                operation.setAOperation(false);
            }
        } else {
            operation.setAOperation(false);
        }

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, StatusCode.ALREADY_EXISTS);

        return status;
    }

    /**
     * Executes a conflict not handle by the others cases
     * (this is the default conflict handling)
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictNotSpecified(SyncSource[] sources, ClientMapping mapping,
            String lastAnchor, SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        AbstractSyncItem resolvingItem = null;

        SyncItemKey luid = ((AbstractSyncItem) syncItemA).getMappedKey();
        if (luid == null) {
            throw new SyncException(
                    "Item A with key " + syncItemA.getKey() + " should have a mapping, but the mapped key is null");
        }

        if (sources[0] instanceof FilterableSyncSource && syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER
                && !((FilterableSyncSource) sources[0]).isSyncItemInFilterClause(syncItemA)) {

            sources[1].removeSyncItem(luid, syncItemB.getTimestamp(), false); // it isn't a softDelete

            operation.setDeleteForced(true);
            operation.setAOperation(true);
            incrementDeletedItemsClient(sources[1].getSourceURI());

        } else {

            resolvingItem = SyncItemHelper.newResolvingClientItem((AbstractSyncItem) syncItemB, syncItemA);

            if (resolvingItem.isMapped()) {
                syncItemA = (AbstractSyncItem) (sources[1].updateSyncItem(resolvingItem));
                incrementUpdatedItemsClient(sources[1].getSourceURI());
            } else {
                syncItemA = (AbstractSyncItem) (sources[1].addSyncItem(resolvingItem));
                incrementAddedItemsOnClient(sources[1].getSourceURI());
            }

            operation.setSyncItemA(syncItemA);
            operation.setAOperation(true);

        }

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd,
                StatusCode.CONFLICT_RESOLVED_WITH_SERVER_DATA);

        return status;
    }

    /**
     * Executes a conflict NU and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictNU(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncItemKey luid = syncItemA.getMappedKey();
        if (luid == null) {
            throw new SyncException(
                    "Item A with key " + syncItemA.getKey() + " should have a mapping, but the mapped key is null");
        }

        AbstractSyncItem resolvingItem = new InMemorySyncItem(syncItemB.getSyncSource(),
                syncItemA.getKey().getKeyAsString());

        boolean clientItemChanged = resolveConflict(syncItemB.getSyncSource(), syncItemB.getKey(), syncItemA,
                resolvingItem);

        operation.setSyncItemB(resolvingItem);

        // set statusCode
        int statusCode = StatusCode.OK;
        if (clientItemChanged) {
            int conflictResolution = getConflictResolution(syncItemB.getSyncSource());
            switch (conflictResolution) {
            //
            // We haven't to check CONFLICT_RESOLUTION_CLIENT_WINS because
            // in this case the client item doesn't change
            //
            case CONFLICT_RESOLUTION_MERGE_DATA:
                statusCode = StatusCode.CONFLICT_RESOLVED_WITH_MERGE;
                break;
            case CONFLICT_RESOLUTION_SERVER_WINS:
                statusCode = StatusCode.CONFLICT_RESOLVED_WITH_SERVER_DATA;
                break;
            default:
            }
        } else {
            //
            // Or the conflit resolution is CLIENT_WINS or is MERGE_DATA and
            // the client content isn't changed. BTW, the item on the server
            // already exists, so an ALREADY_EXISTS (418) is returned
            //
            statusCode = StatusCode.ALREADY_EXISTS;
        }
        //
        // In all cases we have to check if it's in the filter
        // criteria or not.
        //
        boolean isItemInFilter = true;

        if (sources[0] instanceof FilterableSyncSource && syncFilterType != SYNC_WITHOUT_FILTER) {
            isItemInFilter = ((FilterableSyncSource) sources[0]).isSyncItemInFilterClause(resolvingItem.getKey());
        }

        if (isItemInFilter && clientItemChanged) {
            //
            // The client item is in the filter criteria and his content is
            // changed resolving the conflict. The new contect has to be sent
            // to the client.
            //

            if (resolvingItem.isMapped()) {
                syncItemA = (AbstractSyncItem) (sources[1].updateSyncItem(resolvingItem));
                incrementUpdatedItemsClient(sources[1].getSourceURI());
            } else {
                syncItemA = (AbstractSyncItem) (sources[1].addSyncItem(resolvingItem));
                incrementAddedItemsOnClient(sources[1].getSourceURI());
            }

            operation.setSyncItemA(syncItemA);
            operation.setAOperation(true);

        } else if (isItemInFilter && !clientItemChanged) {
            //
            // The client item is in the filter criteria but his content isn't
            // changed resolving the conflict. So nothing is required
            //
            operation.setAOperation(false);
        } else {
            if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER) {
                //
                // The client item is out the filter so the server has to delete
                // it on the client
                //
                operation.setDeleteForced(true);
                incrementDeletedItemsClient(sources[1].getSourceURI());

            } else if (syncFilterType == SYNC_WITH_INCLUSIVE_FILTER) {
                //
                // The client item is out the filter so the server mustn't
                // send it to the client
                //
                operation.setAOperation(false);
            }
        }

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, statusCode);

        return status;
    }

    /**
     * Executes a conflict UU and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictUU(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        SyncItemKey luid = syncItemA.getMappedKey();
        if (luid == null) {
            throw new SyncException(
                    "Item A with key " + syncItemA.getKey() + " should have a mapping, but the mapped key is null");
        }

        AbstractSyncItem resolvingItem = new InMemorySyncItem(syncItemB.getSyncSource(),
                syncItemA.getKey().getKeyAsString());

        boolean clientItemChanged = resolveConflict(syncItemB.getSyncSource(), syncItemB.getKey(), syncItemA,
                resolvingItem);

        if (clientItemChanged) {

            int statusCode = StatusCode.OK;
            int conflictResolution = getConflictResolution(syncItemB.getSyncSource());
            switch (conflictResolution) {
            //
            // We haven't to check CONFLICT_RESOLUTION_CLIENT_WINS because
            // in this case the client item doesn't change
            //
            case CONFLICT_RESOLUTION_MERGE_DATA:
                statusCode = StatusCode.CONFLICT_RESOLVED_WITH_MERGE;
                break;
            case CONFLICT_RESOLUTION_SERVER_WINS:
                statusCode = StatusCode.CONFLICT_RESOLVED_WITH_SERVER_DATA;
                break;
            default:
            }

            //
            // Or the conflit resolution is SERVER_WINS or is MERGE_DATA and
            // the client content is changed.
            //
            // The client content is changed, we have to check if it's in the filter
            // criteria or not.
            //
            boolean isItemInFilter = true;

            if (sources[0] instanceof FilterableSyncSource && syncFilterType != SYNC_WITHOUT_FILTER) {
                isItemInFilter = ((FilterableSyncSource) sources[0]).isSyncItemInFilterClause(syncItemB.getKey());
            }

            if (isItemInFilter) {

                if (resolvingItem.isMapped()) {
                    syncItemA = (AbstractSyncItem) (sources[1].updateSyncItem(resolvingItem));
                    incrementUpdatedItemsClient(sources[1].getSourceURI());
                } else {
                    syncItemA = (AbstractSyncItem) (sources[1].addSyncItem(resolvingItem));
                    incrementAddedItemsOnClient(sources[1].getSourceURI());
                }

                operation.setSyncItemA(syncItemA);
                operation.setAOperation(true);

                status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, statusCode);
            } else {
                if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER) {
                    //
                    // The resolved item is out the filter so the server has to delete
                    // it on the client
                    //
                    operation.setDeleteForced(true);
                    status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, statusCode);
                    incrementDeletedItemsClient(sources[1].getSourceURI());
                } else if (syncFilterType == SYNC_WITH_INCLUSIVE_FILTER) {
                    //
                    // The resolved item is out the filter so the server mustn't
                    // send it to the client
                    //
                    operation.setAOperation(false);
                    status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, statusCode);
                }
            }
        } else {

            //
            // Or the conflit resolution is CLIENT_WINS or is MERGE_DATA and
            // the client content isn't changed.
            //
            status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd, StatusCode.OK);
        }

        return status;
    }

    /**
     * Executes a conflict UD and returns the status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execConflictUD(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) throws SyncException {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        SyncItemKey luid = syncItemA.getMappedKey();
        if (luid == null) {
            throw new SyncException(
                    "Item A with key " + syncItemA.getKey() + " should have a mapping, but the mapped key is null");
        }

        int conflictResolution = getConflictResolution(syncItemB.getSyncSource());

        if (conflictResolution == CONFLICT_RESOLUTION_CLIENT_WINS
                || conflictResolution == CONFLICT_RESOLUTION_MERGE_DATA) {
            //
            // In this case the item on the server has been deleted
            // so we have to call the addSyncItem in order to re-add it.
            //
            try {
                syncItemB = sources[0].addSyncItem(syncItemA);
                operation.setSyncItemB(syncItemB);
                operation.setBOperation(true);
                incrementAddedItemsOnServer(sources[0].getSourceURI());

                status[0] = new Sync4jOperationStatusConflict(operation, sources[1], // TODO should not be 0?
                        cmd, StatusCode.OK);
            } catch (RefusedItemException ex) {
                operation.setDeleteForced(true);
                status[0] = new Sync4jOperationStatusConflict(operation, sources[0], cmd, StatusCode.OK);
            }
            return status;
        }

        //
        // With CONFLICT_RESOLUTION_SERVER_WINS the item has to be deleted
        // on the client
        //
        sources[1].removeSyncItem(syncItemA.getKey(), syncItemA.getTimestamp(), false // is softDelete ?
        );

        operation.setAOperation(true);
        operation.setBOperation(true);
        incrementDeletedItemsClient(sources[1].getSourceURI());

        status[0] = new Sync4jOperationStatusConflict(operation, sources[1], cmd,
                StatusCode.CONFLICT_RESOLVED_WITH_SERVER_DATA);

        return status;

    }

    /**
     * Executes an ACCEPT_CHUNK operation and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execAcceptChunkOperation(SyncSource[] sources, ClientMapping mapping,
            String lastAnchor, SyncOperationImpl operation, int syncFilterType) {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        cmd = (ModificationCommand) syncItemA.getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        status[0] = new Sync4jOperationStatusOK(operation, sources[0], cmd, StatusCode.CHUNKED_ITEM_ACCEPTED);

        return status;
    }

    /**
     * Executes a NEW operation and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     */
    private SyncOperationStatus[] execNewOperation(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];

        if (operation.isAOperation()) {

            try {

                if (((AbstractSyncItem) syncItemB).isMapped()) {
                    syncItemA = (AbstractSyncItem) sources[1].updateSyncItem(syncItemB);
                } else {
                    syncItemA = (AbstractSyncItem) sources[1].addSyncItem(syncItemB);
                }
                incrementAddedItemsOnClient(sources[1].getSourceURI());

                operation.setSyncItemA(syncItemA);

                status[0] = new Sync4jOperationStatusOK(operation, sources[1], cmd, StatusCode.ITEM_ADDED);

            } catch (SyncException e) {
                if (!e.isSilent()) {
                    log.error("Error executing sync operation", e);
                }
                status[0] = new Sync4jOperationStatusError(operation, sources[1], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            }
        } else if (operation.isBOperation()) {

            syncItemB.setTimestamp(syncItemA.getTimestamp()); // this contains the
                                                              // current sync
            try {
                checkSize(syncItemA);

                if (syncItemA.isMapped()) {
                    syncItemB = sources[0].updateSyncItem(syncItemA);
                    incrementUpdatedItemsServer(sources[0].getSourceURI());
                } else {
                    syncItemB = sources[0].addSyncItem(syncItemA);
                    incrementAddedItemsOnServer(sources[0].getSourceURI());
                }

                operation.setSyncItemB(syncItemB);

                if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER) {
                    //
                    // We check the syncsource type, but if sources[0] isn't a
                    // FilterableSyncSource the syncFilterType is SYNC_WITHOUT_FILTER
                    //
                    if (sources[0] instanceof FilterableSyncSource) {
                        boolean itemInFilter = ((FilterableSyncSource) sources[0])
                                .isSyncItemInFilterClause(syncItemB);

                        if (!itemInFilter) {
                            operation.setDeleteForced(true);
                        }
                    } else {
                        //
                        // This cases should never happen
                        //
                        throw new SyncSourceException("The syncFilterType is different "
                                + "from SYNC_WITHOUT_FILTER but the SyncSource isn't "
                                + "a FilterableSyncSource. This means the engine hasn't "
                                + "handled correctly the filter type");
                    }
                }

                if (status[0] == null) {
                    status[0] = new Sync4jOperationStatusOK(operation, sources[1], // TODO should not be 0?
                            cmd, StatusCode.ITEM_ADDED);
                }
            } catch (ObjectSizeMismatchException e) {
                log.info(e.getMessage());
                status[0] = new Sync4jOperationStatusError(operation, sources[0], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            } catch (RefusedItemException ex) {
                operation.setDeleteForced(true);
                status[0] = new Sync4jOperationStatusOK(operation, sources[0], cmd, StatusCode.OK);
            } catch (SyncException e) {
                if (!e.isSilent()) {
                    log.error("Error executing sync operation", e);
                }
                status[0] = new Sync4jOperationStatusError(operation, sources[0], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            }
        }

        return status;
    }

    /**
     * Executes an UPDATE operation and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execUpdateOperation(SyncSource[] sources, ClientMapping mapping,
            String lastAnchor, SyncOperationImpl operation, int syncFilterType) {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = new SyncOperationStatus[1];
        if (operation.isAOperation()) {
            try {

                if (((AbstractSyncItem) syncItemB).isMapped()) {
                    syncItemA = (AbstractSyncItem) (sources[1].updateSyncItem(syncItemB));
                } else {
                    syncItemA = (AbstractSyncItem) (sources[1].addSyncItem(syncItemB));
                }
                incrementUpdatedItemsClient(sources[1].getSourceURI());

                operation.setSyncItemA(syncItemA);
                status[0] = new Sync4jOperationStatusOK(operation, sources[1], cmd);
            } catch (SyncException e) {
                if (!e.isSilent()) {
                    log.error("Error executing sync operation", e);
                }
                status[0] = new Sync4jOperationStatusError(operation, sources[1], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            }
        } else if (operation.isBOperation()) {

            syncItemB.setTimestamp(syncItemA.getTimestamp()); // this contains the
                                                              // current sync
            try {
                checkSize(syncItemA);

                if (((AbstractSyncItem) syncItemA).isMapped()) {
                    syncItemB = sources[0].updateSyncItem(syncItemA);
                    incrementUpdatedItemsServer(sources[0].getSourceURI());
                } else {
                    syncItemB = sources[0].addSyncItem(syncItemA);
                    incrementAddedItemsOnServer(sources[0].getSourceURI());
                }

                operation.setSyncItemB(syncItemB);

                if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER) {
                    if (sources[0] instanceof FilterableSyncSource) {
                        //
                        // We check the syncsource type, but if sources[0] isn't a
                        // FilterableSyncSource the syncFilterType is SYNC_WITHOUT_FILTER
                        //
                        boolean itemInFilter = ((FilterableSyncSource) sources[0])
                                .isSyncItemInFilterClause(syncItemB);

                        if (!itemInFilter) {
                            operation.setDeleteForced(true);
                        }
                    } else {
                        //
                        // This cases should never happen
                        //
                        throw new SyncSourceException("The syncFilterType is different "
                                + "from SYNC_WITHOUT_FILTER but the SyncSource isn't "
                                + "a FilterableSyncSource. This means the engine hasn't "
                                + "handled correctly the filter type");
                    }

                }
                if (status[0] == null) {
                    status[0] = new Sync4jOperationStatusOK(operation, sources[0], cmd);
                }
            } catch (ObjectSizeMismatchException e) {
                log.info(e.getMessage());
                status[0] = new Sync4jOperationStatusError(operation, sources[0], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            } catch (RefusedItemException ex) {
                operation.setDeleteForced(true);
                status[0] = new Sync4jOperationStatusOK(operation, sources[0], cmd, StatusCode.OK);
            } catch (SyncException e) {
                if (!e.isSilent()) {
                    log.error("Error executing sync operation", e);
                }
                status[0] = new Sync4jOperationStatusError(operation, sources[0], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            }
        }
        return status;
    }

    /**
     * Executes a DELETE operation and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execDeleteOperation(SyncSource[] sources, ClientMapping mapping,
            String lastAnchor, SyncOperationImpl operation, int syncFilterType) {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = null;

        //
        // How many status we need? One if we have to delete the item
        // from only one source, two if we have to delete it from both
        // sources
        //
        int size = 1;
        if (operation.isAOperation() && operation.isBOperation()) {
            size = 2;
        }
        status = new SyncOperationStatus[size];

        int s = 0;
        if (operation.isBOperation()) {
            syncItemB.setTimestamp(syncItemA.getTimestamp()); // this contains the
                                                              // timestamp of the
                                                              // current sync

            boolean softDelete = false;

            if (cmd instanceof Delete) {
                softDelete = ((Delete) cmd).isSftDel();
            }
            try {
                sources[0].removeSyncItem(syncItemB.getKey(), syncItemB.getTimestamp(), softDelete);
                status[s++] = new Sync4jOperationStatusOK(operation, sources[0], cmd);
                incrementDeletedItemsServer(sources[0].getSourceURI());
            } catch (SyncException e) {
                log.error("Error executing sync operation", e);

                status[s++] = new Sync4jOperationStatusError(operation, sources[0], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            }
        }
        if (operation.isAOperation()) {

            try {
                sources[1].removeSyncItem(syncItemA.getKey(), syncItemA.getTimestamp(), false); // it isn't a softDelete

                status[s++] = new Sync4jOperationStatusOK(operation, sources[1], cmd);
                incrementDeletedItemsClient(sources[1].getSourceURI());
            } catch (SyncException e) {
                log.error("Error executing sync operation", e);
                status[s++] = new Sync4jOperationStatusError(operation, sources[1], cmd, e);
                operation.setAOperation(false);
                operation.setBOperation(false);
            }
        }

        return status;
    }

    /**
     * Executes a NOP operation and returns the right status
     *
     * @param sources the syncsources relative to the operation
     * @param mapping the mapping
     * @param lastAnchor the lastAnchor of this sync
     * @param operation SyncOperationImpl
     * @param syncFilterType int
     * @return SyncOperationStatus
     * @throws SyncException
     */
    private SyncOperationStatus[] execNOPOperation(SyncSource[] sources, ClientMapping mapping, String lastAnchor,
            SyncOperationImpl operation, int syncFilterType) {

        AbstractSyncItem syncItemA = (AbstractSyncItem) operation.getSyncItemA();
        SyncItem syncItemB = operation.getSyncItemB();

        ModificationCommand cmd = (ModificationCommand) syncItemA
                .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);

        SyncOperationStatus[] status = null;

        //
        // A NOP operation can be due by one of the following conditions
        // (see checkOperation(...)):
        //
        // 1. both items are flagged as "Deleted"
        // 2. both items are flagged "Synchronized"
        // 3. itemA is "Deleted" and itemB is "Not existing" or "Deleted"
        //
        // In case 1. we want a status is returned, thought no real
        // action is performed.
        //
        int s = 0;
        int size = 0;

        if (operation.isAOperation()) {
            ++size;
        }
        if (operation.isBOperation()) {
            ++size;
        }

        status = new SyncOperationStatus[size];

        if (operation.isBOperation()) {
            status[s++] = new Sync4jOperationStatusOK(operation, sources[0], cmd);
        }

        if (operation.isAOperation()) {
            status[s++] = new Sync4jOperationStatusOK(operation, sources[1], cmd);
        }

        if (!operation.isAOperation() && !operation.isBOperation()) {
            char stateItemA = syncItemA.getState();
            char stateItemB = syncItemB.getState();

            switch (stateItemA) {
            case SyncItemState.SYNCHRONIZED:
                if (stateItemB == SyncItemState.SYNCHRONIZED) {
                    status = new SyncOperationStatus[1];

                    if (cmd instanceof Add) {
                        status[0] = new Sync4jOperationStatusOK(operation, sources[1], cmd,
                                StatusCode.ALREADY_EXISTS);
                    } else {
                        status[0] = new Sync4jOperationStatusOK(operation, sources[1], cmd);
                    }

                    try {
                        if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER) {
                            if (sources[0] instanceof FilterableSyncSource) {
                                //
                                // We check the syncsource type, but if sources[0] isn't a
                                // FilterableSyncSource the syncFilterType is SYNC_WITHOUT_FILTER
                                //
                                boolean itemInFilter = ((FilterableSyncSource) sources[0])
                                        .isSyncItemInFilterClause(syncItemA);
                                if (!itemInFilter) {
                                    operation.setDeleteForced(true);
                                }
                            } else {
                                //
                                // This cases should never happen
                                //
                                throw new SyncSourceException("The syncFilterType is different from "
                                        + "SYNC_WITHOUT_FILTER but the SyncSource "
                                        + "isn't a FilterableSyncSource. This means "
                                        + "the engine hasn't handled correctly " + "the filter type");
                            }
                        }

                    } catch (SyncSourceException e) {
                        log.error("Error executing sync operation", e);

                        status[0] = new Sync4jOperationStatusError(operation, sources[0], cmd, e);
                        operation.setAOperation(false);
                        operation.setBOperation(false);
                    }
                    break;
                }
            case SyncItemState.DELETED:
                if (stateItemB == SyncItemState.NOT_EXISTING) {
                    //
                    //client sent Delete item that not exist on server
                    //
                    operation.setSyncItemA(syncItemA);

                    status = new SyncOperationStatus[1];
                    status[0] = new Sync4jOperationStatusOK(operation, sources[1], cmd,
                            StatusCode.ITEM_NOT_DELETED);
                    break;

                } else if (stateItemB == SyncItemState.DELETED) {
                    //
                    //client sent Delete item that is deleted on the server
                    //
                    status = new SyncOperationStatus[1];
                    cmd = (ModificationCommand) ((AbstractSyncItem) syncItemA)
                            .getPropertyValue(AbstractSyncItem.PROPERTY_COMMAND);
                    status[0] = new Sync4jOperationStatusOK(operation, sources[1], cmd);
                    break;
                }
            }
        }

        return status;
    }

    /**
     * Check if there are some items to delete on the client.
     * If the client specifies a exclusive filter, the server has to delete
     * on the client all items that don't satisfy the filter. The server
     * knows the item on the client using the mapping. In order to do this,
     * we call the method getAllSyncItemKeys to retrieve all items in the filter
     * criteria. Then, we check all items in the mapping with this set; if an
     * item isn't in the set, we have to delete it on the client
     *
     * @param sources the sources in sync
     * @param syncFilterType the filter type
     * @param allItemsKeys all items on the server inside the filter criteria
     * @param mapping the mapping
     * @param Bm the list of the changed items on the server. If an item not
     * satisfying the filter has to be deleted, it is added to the list.
     */
    private void checkForItemsToDelete(SyncSource[] sources, int syncFilterType, SyncItemKey[] allItemsKeys,
            ClientMapping mapping, List Bm) {

        List guidluid = mapping.getMapping();

        if (syncFilterType == SYNC_WITH_EXCLUSIVE_FILTER) {

            if (log.isTraceEnabled()) {
                log.trace("Check for items on the client (seeing the mapping) "
                        + "in order to discovery the items to delete");
            }

            List keysList = Arrays.asList(allItemsKeys);
            SyncItemKey tmp = new SyncItemKey("tmp");

            Iterator it = guidluid.iterator();
            String guid = null;
            ClientMappingEntry mappingEntry = null;

            while (it.hasNext()) {
                mappingEntry = ((ClientMappingEntry) it.next());
                if (!mappingEntry.isValid()) {
                    continue;
                }
                guid = mappingEntry.getGuid();

                tmp.setKeyValue(guid);

                if (keysList.contains(tmp)) {
                    //
                    // The item satisfies the filter
                    //
                    continue;
                } else {

                    //
                    // We simulate the item on the server has been deleted
                    //
                    AbstractSyncItem dummyItem = new InMemorySyncItem(sources[0], guid, SyncItemState.DELETED);

                    //
                    // If there is already an item with the same key in Bm,
                    // the add isn't required (the item is already handled)
                    //
                    if (!Bm.contains(dummyItem)) {
                        if (log.isTraceEnabled()) {
                            log.trace("The item '" + guid + "' is in the mapping but "
                                    + "it does not match the filter...deleting it");
                        }
                        Bm.add(dummyItem);
                    } else {
                        if (log.isTraceEnabled()) {
                            log.trace("The item '" + guid + "' is in the mapping but "
                                    + " it does not match the filter, should be deleted"
                                    + " but it is already in the Bm list");
                        }
                    }
                }
            }
        }
    }

    /**
     * Check if there are some items to add to the client because if the filter
     * changes some server have to added to the client.
     * So, an item could be not updated but the server has to send an Add command.
     * As borderline case, the client could not have specific any filter.
     *
     * @param sources the syncsources in sync
     * @param allItemsKeys all items on the server inside the filter criteria
     * @param mapping the mapping
     * @param Bm the list of the changed items on the server
     */
    private void checkForItemsToAdd(SyncSource[] sources, SyncItemKey[] allItemsKeys, ClientMapping mapping,
            List Bm) {

        List guidluid = mapping.getMapping();

        if (log.isTraceEnabled()) {
            log.trace("Check the items that satisfy the filter criteria in"
                    + " order to discovery the items to add on the client");
        }

        Iterator itMapping = null;
        String guid = null;
        boolean itemFound = false;
        ClientMappingEntry mappingEntry = null;

        for (int i = 0; i < allItemsKeys.length; i++) {

            itMapping = guidluid.iterator();

            while (itMapping.hasNext()) {
                itemFound = false;
                mappingEntry = (ClientMappingEntry) itMapping.next();
                if (!mappingEntry.isValid()) {
                    continue;
                }
                guid = mappingEntry.getGuid();
                if (guid.equals(allItemsKeys[i].getKeyAsString())) {
                    //
                    // The item is already in the mapping (so it's already
                    // on the client)
                    //
                    if (log.isTraceEnabled()) {
                        log.trace("The item '" + guid + "' is already on the client");
                    }
                    itemFound = true;
                    break;
                }
            }
            if (!itemFound) {
                //
                // The item is on the server and it satisfies the filter
                // but it isn't in the mapping (so it isn't on the client).
                // The server has to send the item with an Add command.
                // In order to do this, we create a Dummy new item
                //

                AbstractSyncItem dummyItem = new InMemorySyncItem(sources[0], allItemsKeys[i].getKeyValue(),
                        SyncItemState.NEW);

                //
                // If there is already an item with the same key in Bm,
                // the addition isn't required (the item is already handled)
                //
                if (!Bm.contains(dummyItem)) {
                    if (log.isTraceEnabled()) {
                        log.trace("The item '" + allItemsKeys[i].getKeyAsString()
                                + "' isn't on the client...adding it");
                    }

                    Bm.add(dummyItem);
                } else {
                    if (log.isTraceEnabled()) {
                        log.trace("The item '" + allItemsKeys[i].getKeyAsString() + "' isn't on the client"
                                + " but it's already in the Bm list");
                    }
                }
            }
        }
    }

    /**
     * Resolves the conflict between the server item identifies by the given
     * serverKey and the given clientItem. In the given newServerItem is stored the
     * new server item content (for example if the conflict is resolved
     * with the client data - CONFLICT_RESOLUTION_CLIENT_WINS).
     * If the conflict is resolved using CONFLICT_RESOLUTION_SERVER_WINS or
     * with CONFLICT_RESOLUTION_MERGE_DATA, and the content of the client item is
     * changed, true is returned.
     *
     * @param serverSource the server syncsource
     * @param serverKey the key of the server's item
     * @param clientItem the client item
     * @param resolvingItem the new resolving item
     * @return true is the content of the client item is changed
     * @throws SyncSourceException if an error occurs
     */
    private boolean resolveConflict(SyncSource serverSource, SyncItemKey serverKey, AbstractSyncItem clientItem,
            AbstractSyncItem resolvingItem) throws SyncSourceException {

        if (log.isTraceEnabled()) {
            log.trace("Resolve conflict between server item '" + serverKey.getKeyAsString() + "' and client item '"
                    + ((AbstractSyncItem) clientItem).getMappedKey().getKeyAsString() + "' for the source '"
                    + serverSource.getSourceURI() + "'");
        }

        int conflictResolution = getConflictResolution(serverSource);

        if (conflictResolution == CONFLICT_RESOLUTION_MERGE_DATA
                && !(serverSource instanceof MergeableSyncSource)) {
            //
            // The strategy is configured to merge the items, but
            // this source doesn't support the merge
            //
            if (log.isWarningEnabled()) {
                log.warn("The strategy is configured to merge the items, but the source "
                        + serverSource.getSourceURI() + " is not a MergeableSyncSource "
                        + " so the default conflict resolution is used");
            }

            conflictResolution = defaultConflictResolution;
        }

        if (log.isTraceEnabled()) {
            log.trace("Conflict resolution: " + getConflictResolutionDescription(conflictResolution));
        }

        switch (conflictResolution) {

        case CONFLICT_RESOLUTION_CLIENT_WINS:

            SyncItem updatedItem = serverSource.updateSyncItem(clientItem);
            incrementUpdatedItemsServer(serverSource.getSourceURI());

            //
            // We update the newServerItem with the information returned by
            // the updateSyncItem method
            // and we set set the key and mapped key
            //
            resolvingItem.copy(updatedItem);
            resolvingItem.setKey(new SyncItemKey(clientItem.getKey().getKeyAsString()));
            resolvingItem.setMappedKey(
                    clientItem.getMappedKey() != null ? new SyncItemKey(clientItem.getMappedKey().getKeyAsString())
                            : null);
            resolvingItem.setState(SyncItemState.SYNCHRONIZED);

            //
            // The client data isn't changed
            //
            return false;

        case CONFLICT_RESOLUTION_SERVER_WINS:
            SyncItem serverItem = null;
            try {
                serverItem = serverSource.getSyncItemFromId(serverKey);
            } catch (SyncSourceException e) {
                String msg = "Error getting the server item '" + serverKey + "' from source " + serverSource + ": "
                        + e.getMessage();

                log.error(msg, e);

                throw e;
            }

            resolvingItem.copy(serverItem);
            resolvingItem.setKey(new SyncItemKey(clientItem.getKey().getKeyAsString()));
            resolvingItem.setMappedKey(
                    clientItem.getMappedKey() != null ? new SyncItemKey(clientItem.getMappedKey().getKeyAsString())
                            : null);
            resolvingItem.setState(SyncItemState.UPDATED);

            //
            // The client data is changed
            //
            return true;

        case CONFLICT_RESOLUTION_MERGE_DATA:

            //
            // We keep the state of the clientItem because the ss
            // can change it (but we don't want it!!!)
            //
            char state = clientItem.getState();

            boolean itemChanged = false;

            try {
                itemChanged = ((MergeableSyncSource) serverSource).mergeSyncItems(serverKey, clientItem);
            } catch (SyncSourceException e) {
                String msg = "Error merging the client item " + clientItem.getKey() + " with server item "
                        + serverKey.getKeyAsString() + " from source " + serverSource + ": " + e.getMessage();

                log.error(msg, e);

                throw e;
            }

            //
            // We restore the state of the item
            //
            clientItem.setState(state);

            // the clientItem is copied into the resolvingItem because
            // the mergeSyncItems method changes the clientItem that acually
            // represent the resolvingItem
            resolvingItem.copy(clientItem);

            // we don't count it as an update because in a NN conflict, as
            // often occurs in slow syncs, the items are actually not
            // updated, so we prefer to not count updated instead of
            // counting updates that are not actually updates
            // incrementUpdatedItemsServer(serverSource.getSourceURI());

            if (itemChanged) {
                //
                // We set the newServerItem state at 'U'
                // so his content is sent to the client
                //
                resolvingItem.setState(SyncItemState.UPDATED);
            } else {
                resolvingItem.setState(SyncItemState.SYNCHRONIZED);
            }

            return itemChanged;

        default:

        }

        return false;
    }

    /**
     * Returns a description of the given conflictResolution
     * @param conflictResolution the conflictResolution
     * @return a description of the given conflictResolution
     */
    protected String getConflictResolutionDescription(int conflictResolution) {
        switch (conflictResolution) {
        case CONFLICT_RESOLUTION_CLIENT_WINS:
            return "CONFLICT_RESOLUTION_CLIENT_WINS";
        case CONFLICT_RESOLUTION_SERVER_WINS:
            return "CONFLICT_RESOLUTION_SERVER_WINS";
        case CONFLICT_RESOLUTION_MERGE_DATA:
            return "CONFLICT_RESOLUTION_MERGE_DATA";
        default:
            return "Unknown conflict resolution (" + conflictResolution + ")";
        }
    }

    /**
     * Updates the mapping dispatching the real update to the right method
     * according with the operation type.
     * @param slowSync boolean
     * @param clientMapping ClientMapping
     * @param lastAnchor String
     * @param operation SyncOperation
     */
    protected void updateMapping(boolean slowSync, ClientMapping clientMapping, String lastAnchor,
            SyncOperation operation) {

        char op = operation.getOperation();

        if (operation.isDeleteForced()) {

            String guid = null;
            SyncItem itemB = operation.getSyncItemB();

            guid = itemB.getKey().getKeyAsString();
            clientMapping.removeMappedValuesForGuid(guid, true);

        } else if (slowSync && (op != SyncOperation.DELETE) && (op != SyncOperation.NEW)) {
            //
            //this is the case of slow sync and without mappings into db
            //
            String guid = null;
            String luid = null;

            AbstractSyncItem itemA = ((AbstractSyncItem) operation.getSyncItemA());
            SyncItem itemB = operation.getSyncItemB();

            if (itemA != null && itemB != null) {

                guid = itemB.getKey().getKeyAsString();

                SyncItemKey mappedKeyA = itemA.getMappedKey();
                if (mappedKeyA != null) {
                    luid = mappedKeyA.getKeyAsString();
                } else {
                    luid = itemA.getKey().getKeyAsString();
                }

                if (operation.isAOperation()) {
                    clientMapping.updateMapping(guid, luid, lastAnchor);
                } else if (operation.isBOperation()) {
                    clientMapping.updateMapping(guid, luid, lastAnchor);
                } else {
                    //
                    // This case happens when during a slow sync the client sends
                    // a item twin of a server item but the merging doesn't produce
                    // a new content. So, no operations are required on A (client)
                    // and no operations are required on B (server)
                    //
                    clientMapping.updateMapping(guid, luid, lastAnchor);
                }

                return;
            }

        } else if (op == SyncOperation.DELETE) {

            updateMappingDelete(clientMapping, lastAnchor, operation);

        } else if (op == SyncOperation.NEW) {

            updateMappingNew(clientMapping, lastAnchor, operation);

        } else if (op == SyncOperation.UPDATE) {

            updateMappingUpdate(clientMapping, lastAnchor, operation);

        } else if (op == SyncOperation.CONFLICT) {

            updateMappingConflict(clientMapping, lastAnchor, operation);

        }
    }

    /**
     * Updates the mapping for a 'New' operation
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    protected void updateMappingNew(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        if (operation.isBOperation()) {
            luid = itemA.getKey().getKeyAsString();
            guid = itemB.getKey().getKeyAsString();
            clientMapping.updateMapping(guid, luid, lastAnchor);
        }
        if (operation.isAOperation()) {
            guid = itemB.getKey().getKeyAsString();
            clientMapping.updateMapping(guid, guid, null);
        }
    }

    /**
     * Updates the mapping for a 'UPDATE' operation. This is required because an
     * update operation on the server can change the item's key (GUID)
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    protected void updateMappingUpdate(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        if (operation.isBOperation()) {
            luid = itemA.getMappedKey().getKeyAsString();
            guid = itemB.getKey().getKeyAsString();

            String oldGuid = clientMapping.getMappedValueForLuid(luid);
            if (oldGuid != null && !oldGuid.equals(guid)) {
                clientMapping.removeMappedValuesForGuid(oldGuid, false);
                clientMapping.updateMapping(guid, luid, lastAnchor);
            }
        }
    }

    /**
     * Updates the mapping for a 'Delete' operation
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    protected void updateMappingDelete(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemB = null;

        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String guid = null;

        guid = itemB.getKey().getKeyAsString();
        if (operation.isAOperation()) {
            clientMapping.removeMappedValuesForGuid(guid, true);
        } else {
            clientMapping.removeMappedValuesForGuid(guid, false);
        }
    }

    /**
     * Updates the mapping for a 'Conflict' operation dispatching the
     * real update to the right method according with the conflict type.
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    protected void updateMappingConflict(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        String conflictType = ((SyncConflict) operation).getType();

        if (SyncConflict.STATE_DELETED_UPDATED.equals(conflictType)
                || SyncConflict.STATE_DELETED_NEW.equals(conflictType)) {

            updateMappingConflictDU(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_UPDATED_DELETED.equals(conflictType)) {

            updateMappingConflictUD(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_UPDATED_UPDATED.equals(conflictType)) {

            updateMappingConflictUU(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_NEW_NEW.equals(conflictType)) {

            updateMappingConflictNN(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_NEW_UPDATED.equals(conflictType)) {

            updateMappingConflictNU(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_UPDATED_NEW.equals(conflictType)) {

            updateMappingConflictUN(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_NEW_SYNCHRONIZED.equals(conflictType)) {

            updateMappingConflictNS(clientMapping, lastAnchor, operation);

        } else if (SyncConflict.STATE_DELETED_DELETED.equals(conflictType)) {

            updateMappingConflictDD(clientMapping, lastAnchor, operation);

        } else {

            //
            // These cases shuold never happen
            //
            assert (!SyncConflict.STATE_DELETED_CONFLICT.equals(conflictType));
            assert (!SyncConflict.STATE_UPDATED_NEW.equals(conflictType));
            assert (!SyncConflict.STATE_UPDATED_CONFLICT.equals(conflictType));
            assert (!SyncConflict.STATE_NEW_DELETED.equals(conflictType));
            assert (!SyncConflict.STATE_NEW_CONFLICT.equals(conflictType));
            assert (!SyncConflict.STATE_NONE_CONFLICT.equals(conflictType));
            assert (!SyncConflict.STATE_CONFLICT_DELETED.equals(conflictType));
            assert (!SyncConflict.STATE_CONFLICT_UPDATED.equals(conflictType));
            assert (!SyncConflict.STATE_CONFLICT_NEW.equals(conflictType));
            assert (!SyncConflict.STATE_CONFLICT_CONFLICT.equals(conflictType));
        }

    }

    /**
     * Updates the mapping for a 'DU' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictDU(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemB = null;

        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String guid = null;

        if (!operation.isAOperation() && !operation.isBOperation()) {
            //
            // This happen when there is a conflict DU and we are
            // using some filters so we have to remove the mapping
            // (see Sync4jStragegy, method execConflictDU)
            //
            guid = itemB.getKey().getKeyAsString();
            clientMapping.removeMappedValuesForGuid(guid, false);
        } else if (operation.isBOperation()) {
            //
            // This happen when the conflict resolution is CLIENT_WINS
            // (see Sync4jStragegy, method execConflictDU)
            //
            guid = itemB.getKey().getKeyAsString();
            clientMapping.removeMappedValuesForGuid(guid, false);
        } else {
            //
            // This happen when the conflict resolution is SERVER_WINS or
            // MERGE_DATA and the item is inside the filter criteria
            // (see Sync4jStragegy, method execConflictDU)
            //
            guid = itemB.getKey().getKeyAsString();
            clientMapping.updateMapping(guid, guid, null);
        }

    }

    /**
     * Updates the mapping for a 'UD' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictUD(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        SyncItemKey mappedKey = itemA.getMappedKey();
        if (mappedKey != null) {
            luid = mappedKey.getKeyAsString();
        } else {
            luid = itemA.getKey().getKeyAsString();
        }

        String oldGuid = itemA.getKey().getKeyAsString();

        if (operation.isAOperation()) {
            //
            // We are in this case if the strategy is configurated with
            // CONFLICT_RESOLUTION_SERVER_WINS: in this case we have deleted
            // the item on the client
            // (see Sync4jStragegy, method execConflictUD)
            //
            clientMapping.removeMappedValuesForGuid(oldGuid, true);
            return;
        }

        //
        // With CONFLICT_RESOLUTION_CLIENT_WINS or CONFLICT_RESOLUTION_MERGE_DATA,
        // the server calls the method addSyncItem on the server ss.
        // This method can create a new item with a new GUID.
        // But in the mapping we have LUID-Old GUID.
        // So, we have to remove it before to insert the new mapping
        //
        clientMapping.removeMappedValuesForGuid(oldGuid, false);

        //
        // Insert the new mapping
        //
        guid = itemB.getKey().getKeyAsString();
        clientMapping.updateMapping(guid, luid, lastAnchor);
    }

    /**
     * Updates the mapping for a 'UU' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictUU(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        SyncItemKey mappedKey = itemA.getMappedKey();
        if (mappedKey != null) {
            luid = mappedKey.getKeyAsString();
        } else {
            luid = itemA.getKey().getKeyAsString();
        }
        guid = itemB.getKey().getKeyAsString();
        if (operation.isAOperation()) {
            clientMapping.updateMapping(guid, luid, null);
        } else {
            clientMapping.updateMapping(guid, luid, lastAnchor);
        }
    }

    /**
     * Updates the mapping for a 'NN' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictNN(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        luid = itemA.getKey().getKeyAsString();
        guid = itemB.getKey().getKeyAsString();
        if (operation.isAOperation()) {
            clientMapping.updateMapping(guid, luid, null);
        } else {
            clientMapping.updateMapping(guid, luid, lastAnchor);
        }
    }

    /**
     * Updates the mapping for a 'NU' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictNU(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        luid = itemA.getMappedKey().getKeyAsString();
        guid = itemB.getKey().getKeyAsString();

        clientMapping.updateMapping(guid, luid, lastAnchor);

    }

    /**
     * Updates the mapping for a 'NN' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictUN(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        SyncItemKey mappedKey = itemA.getMappedKey();
        if (mappedKey != null) {
            luid = mappedKey.getKeyAsString();
        } else {
            luid = itemA.getKey().getKeyAsString();
        }
        guid = itemB.getKey().getKeyAsString();
        if (operation.isAOperation()) {
            clientMapping.updateMapping(guid, luid, null);
        } else {
            clientMapping.updateMapping(guid, luid, lastAnchor);
        }
    }

    /**
     * Updates the mapping for a 'NS' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictNS(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        AbstractSyncItem itemA = null;
        AbstractSyncItem itemB = null;

        itemA = (AbstractSyncItem) operation.getSyncItemA();
        itemB = (AbstractSyncItem) operation.getSyncItemB();

        String luid = null;
        String guid = null;

        SyncItemKey mappedKey = itemA.getMappedKey();
        if (mappedKey != null) {
            luid = mappedKey.getKeyAsString();
        } else {
            luid = itemA.getKey().getKeyAsString();
        }
        guid = itemB.getKey().getKeyAsString();
        if (operation.isAOperation()) {
            clientMapping.updateMapping(guid, luid, null);
        } else {
            clientMapping.updateMapping(guid, luid, lastAnchor);
        }
    }

    /**
     * Updates the mapping for a 'DD' conflict
     * @param clientMapping the current mapping
     * @param lastAnchor the last anchor of this sync
     * @param operation the operation
     */
    private void updateMappingConflictDD(ClientMapping clientMapping, String lastAnchor, SyncOperation operation) {

        SyncItem itemB = null;

        itemB = operation.getSyncItemB();

        String guid = null;

        guid = itemB.getKey().getKeyAsString();
        clientMapping.removeMappedValuesForGuid(guid, false);
    }

    /**
     * Converts the parentKey of the given item from LUID to GUID
     * (the server syncsource needs the GUID) using the given mapping
     * @param mapping the mapping
     * @param item the item
     */
    protected void fixParent(Map mapping, SyncItem item) {

        SyncItemKey parentKey = item.getParentKey();
        if (mapping == null) {
            return;
        }
        if (parentKey != null) {
            String sParentKey = parentKey.getKeyAsString();
            //
            // We have to transform the LUID in GUID
            //
            if (mapping.containsValue(sParentKey)) {

                Iterator itMap = mapping.keySet().iterator();
                String key = null;
                String value = null;
                while (itMap.hasNext()) {
                    key = (String) itMap.next(); // GUID
                    value = (String) mapping.get(key); // LUID
                    if (value.equals(sParentKey)) {
                        parentKey.setKeyValue(key);
                    }
                }
            }
        }
    }

    /**
     * Converts the parentKey of the itemA from LUID to GUID
     * (the server syncsource needs the GUID) using the given mapping
     * @param mapping the mapping
     * @param operation the operation
     */
    private void fixParent(ClientMapping mapping, SyncOperation operation) {

        SyncItem item = operation.getSyncItemA();

        SyncItemKey parentKey = item.getParentKey();
        if (parentKey != null) {
            //
            // We have to transform the LUID in GUID
            //
            String guid = mapping.getMappedValueForLuid(parentKey.getKeyAsString());
            if (guid != null) {
                parentKey.setKeyValue(guid);
            }
        }
    }

    /**
     * Returns the first key found in the given keys not in the given
     * mapping (as GUID).
     * @param mapping Map
     * @param keys SyncItemKey[]
     * @return the first key found in the given keys not in the given
     *         mapping as GUID. If no mapped key is found, <code>null</code> is
     *         returned.
     */
    protected SyncItemKey findNotMappedItem(Map mapping, SyncItemKey[] keys) {
        if (keys == null || keys.length == 0) {
            return null;
        }
        if (mapping == null) {
            if (keys.length > 0) {
                return keys[0];
            }
        }
        int numKeys = keys.length;
        String luid = null;
        for (int i = 0; i < numKeys; i++) {
            luid = (String) mapping.get(keys[i].getKeyAsString());
            if (luid == null) {
                return keys[i];
            }
        }
        return null;
    }

    private void incrementAddedItemsOnClient(String uri) {
        SyncStatistic syncStatistic = syncStatistics.get(uri);
        if (syncStatistic == null) {
            syncStatistic = new SyncStatistic();
            syncStatistics.put(uri, syncStatistic);
        }
        syncStatistic.incrementAddedItemsOnClient();
    }

    private void incrementAddedItemsOnServer(String uri) {
        SyncStatistic syncStatistic = syncStatistics.get(uri);
        if (syncStatistic == null) {
            syncStatistic = new SyncStatistic();
            syncStatistics.put(uri, syncStatistic);
        }
        syncStatistic.incrementAddedItemsOnServer();
    }

    private void incrementDeletedItemsServer(String uri) {
        SyncStatistic syncStatistic = syncStatistics.get(uri);
        if (syncStatistic == null) {
            syncStatistic = new SyncStatistic();
            syncStatistics.put(uri, syncStatistic);
        }
        syncStatistic.incrementDeletedItemsServer();
    }

    private void incrementDeletedItemsClient(String uri) {
        SyncStatistic syncStatistic = syncStatistics.get(uri);
        if (syncStatistic == null) {
            syncStatistic = new SyncStatistic();
            syncStatistics.put(uri, syncStatistic);
        }
        syncStatistic.incrementDeletedItemsClient();
    }

    private void incrementUpdatedItemsClient(String uri) {
        SyncStatistic syncStatistic = syncStatistics.get(uri);
        if (syncStatistic == null) {
            syncStatistic = new SyncStatistic();
            syncStatistics.put(uri, syncStatistic);
        }
        syncStatistic.incrementUpdatedItemsClient();
    }

    private void incrementUpdatedItemsServer(String uri) {
        SyncStatistic syncStatistic = syncStatistics.get(uri);
        if (syncStatistic == null) {
            syncStatistic = new SyncStatistic();
            syncStatistics.put(uri, syncStatistic);
        }
        syncStatistic.incrementUpdatedItemsServer();
    }
}