You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2018/04/16 12:07:13 UTC

[sling-ide-tooling] branch feature/SLING-5618 created (now bfb9237)

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a change to branch feature/SLING-5618
in repository https://gitbox.apache.org/repos/asf/sling-ide-tooling.git.


      at bfb9237  SLING-5618 - Make the ResourceChangeCommandFactory independent from Eclipse

This branch includes the following new commits:

     new bfb9237  SLING-5618 - Make the ResourceChangeCommandFactory independent from Eclipse

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


-- 
To stop receiving notification emails like this one, please contact
rombert@apache.org.

[sling-ide-tooling] 01/01: SLING-5618 - Make the ResourceChangeCommandFactory independent from Eclipse

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch feature/SLING-5618
in repository https://gitbox.apache.org/repos/asf/sling-ide-tooling.git

commit bfb9237e07cffb20e05c0e6c9ae32d4e4a2a4ec9
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Apr 13 18:04:38 2018 +0300

    SLING-5618 - Make the ResourceChangeCommandFactory independent from Eclipse
    
    - Introduce abstractions in the API module which support
      working with the ResourceChangeCommandFactory outside of Eclipse.
    - Implement needed abstractions in the eclipse-core module and stop using
      the ResourceChangeCommandFactory internally
    - The ResourceChangeCommandFactory is now left only as a deprecated stub
      until the tests are ported to use the DefaultCommandFactoryImpl.
---
 eclipse/eclipse-core/META-INF/MANIFEST.MF          |   1 +
 .../sling/ide/eclipse/core/EclipseResources.java   |  52 ++
 .../sling/ide/eclipse/core/ResourceUtil.java       |   3 +-
 .../sling/ide/eclipse/core/internal/Activator.java |  10 +
 .../internal/ResourceChangeCommandFactory.java     | 522 +--------------------
 .../core/internal/SlingLaunchpadBehaviour.java     |   2 +-
 .../sync/content/EclipseWorkspaceDirectory.java    |  75 +++
 .../sync/content/EclipseWorkspaceFile.java         |  62 +++
 .../sync/content/EclipseWorkspaceProject.java      |  63 +++
 .../sync/content/EclipseWorkspaceResource.java     | 116 +++++
 .../impl/ResourceChangeCommandFactoryTest.java     |   2 +-
 eclipse/eclipse-ui/META-INF/MANIFEST.MF            |   1 +
 .../sling/ide/eclipse/ui/internal/Activator.java   |  12 +-
 .../ide/eclipse/ui/internal/ExportWizard.java      |  27 +-
 .../ui/internal/ImportRepositoryContentAction.java |   8 +-
 .../ide/serialization/SerializationManager.java    |  13 +
 .../sling/ide/sync/content/SyncCommandFactory.java |  71 +++
 .../sling/ide/sync/content/WorkspaceDirectory.java |  53 +++
 .../content/WorkspaceFile.java}                    |  41 +-
 .../sling/ide/sync/content/WorkspacePath.java      | 126 +++++
 .../sling/ide/sync/content/WorkspacePaths.java     |  37 ++
 .../sling/ide/sync/content/WorkspaceProject.java   |  35 ++
 .../sling/ide/sync/content/WorkspaceResource.java  |  93 ++++
 .../content/impl/DefaultSyncCommandFactory.java    | 447 ++++++++++++++++++
 .../sling/ide/sync/content/package-info.java       |  19 +
 .../sling/ide/transport}/ResourceAndInfo.java      |   5 +-
 .../java/org/apache/sling/ide/util/PathUtil.java   |   3 +
 .../sling/ide/sync/content/WorkspacePathTest.java  | 175 +++++++
 .../sling/ide/sync/content/WorkspacePathsTest.java |  33 ++
 29 files changed, 1548 insertions(+), 559 deletions(-)

diff --git a/eclipse/eclipse-core/META-INF/MANIFEST.MF b/eclipse/eclipse-core/META-INF/MANIFEST.MF
index bd419a5..0d667cd 100644
--- a/eclipse/eclipse-core/META-INF/MANIFEST.MF
+++ b/eclipse/eclipse-core/META-INF/MANIFEST.MF
@@ -24,6 +24,7 @@ Import-Package: org.apache.commons.httpclient;version="3.1.0",
  org.apache.sling.ide.log,
  org.apache.sling.ide.osgi,
  org.apache.sling.ide.serialization,
+ org.apache.sling.ide.sync.content,
  org.apache.sling.ide.transport,
  org.apache.sling.ide.util,
  org.eclipse.core.commands,
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/EclipseResources.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/EclipseResources.java
new file mode 100644
index 0000000..e839076
--- /dev/null
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/EclipseResources.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.eclipse.core;
+
+import java.util.Set;
+
+import org.apache.sling.ide.eclipse.core.internal.Activator;
+import org.apache.sling.ide.eclipse.core.internal.sync.content.EclipseWorkspaceDirectory;
+import org.apache.sling.ide.eclipse.core.internal.sync.content.EclipseWorkspaceFile;
+import org.apache.sling.ide.eclipse.core.internal.sync.content.EclipseWorkspaceProject;
+import org.apache.sling.ide.sync.content.WorkspaceResource;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+
+public abstract class EclipseResources {
+
+    public static WorkspaceResource create(IResource resource) {
+        
+        Set<String> ignoredFileNames = Activator.getDefault().getPreferences().getIgnoredFileNamesForSync();
+        
+        switch ( resource.getType() ) {
+        case IResource.FILE:
+            return new EclipseWorkspaceFile((IFile) resource, ignoredFileNames);
+        case IResource.FOLDER:
+            return new EclipseWorkspaceDirectory((IFolder) resource, ignoredFileNames);
+        case IResource.PROJECT:
+            return new EclipseWorkspaceProject((IProject) resource, ignoredFileNames);
+            default:
+                throw new IllegalArgumentException("Unable to create a local resource for Eclipse IResource.getType() = " + resource.getType() );
+        }
+    }
+    
+    private EclipseResources() {
+        
+    }
+}
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/ResourceUtil.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/ResourceUtil.java
index 641a399..53e35c9 100644
--- a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/ResourceUtil.java
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/ResourceUtil.java
@@ -17,6 +17,7 @@
 package org.apache.sling.ide.eclipse.core;
 
 import org.apache.sling.ide.eclipse.core.internal.Activator;
+import org.apache.sling.ide.sync.content.SyncCommandFactory;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.QualifiedName;
 
@@ -36,7 +37,7 @@ public abstract class ResourceUtil {
      * </p>
      */
     public static final QualifiedName QN_IMPORT_MODIFICATION_TIMESTAMP = new QualifiedName(Activator.PLUGIN_ID,
-            "importModificationTimestamp");
+            SyncCommandFactory.PN_IMPORT_MODIFICATION_TIMESTAMP);
 
     private ResourceUtil() {
 
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/Activator.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/Activator.java
index c6d58c3..48e8ca8 100644
--- a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/Activator.java
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/Activator.java
@@ -28,6 +28,7 @@ import org.apache.sling.ide.filter.FilterLocator;
 import org.apache.sling.ide.log.Logger;
 import org.apache.sling.ide.osgi.OsgiClientFactory;
 import org.apache.sling.ide.serialization.SerializationManager;
+import org.apache.sling.ide.sync.content.SyncCommandFactory;
 import org.apache.sling.ide.transport.BatcherFactory;
 import org.apache.sling.ide.transport.CommandExecutionProperties;
 import org.apache.sling.ide.transport.RepositoryFactory;
@@ -63,6 +64,7 @@ public class Activator extends Plugin {
     private ServiceTracker<Logger, Logger> tracer;
     private ServiceTracker<BatcherFactory, BatcherFactory> batcherFactoryLocator;
     private ServiceTracker<SourceReferenceResolver, Object> sourceReferenceLocator;
+    private ServiceTracker<SyncCommandFactory, SyncCommandFactory> commandFactory;
     
     private ServiceRegistration<Logger> tracerRegistration;
 
@@ -107,6 +109,9 @@ public class Activator extends Plugin {
         
         sourceReferenceLocator = new ServiceTracker<>(context, SourceReferenceResolver.class, null);
         sourceReferenceLocator.open();
+        
+        commandFactory = new ServiceTracker<>(context, SyncCommandFactory.class, null);
+        commandFactory.open();
 	}
 
 	/*
@@ -128,6 +133,7 @@ public class Activator extends Plugin {
         tracer.close();
         batcherFactoryLocator.close();
         sourceReferenceLocator.close();
+        commandFactory.close();
 
         plugin = null;
 		super.stop(context);
@@ -172,6 +178,10 @@ public class Activator extends Plugin {
         return (BatcherFactory) ServiceUtil.getNotNull(batcherFactoryLocator);
     }
     
+    public SyncCommandFactory getCommandFactory() {
+        return ServiceUtil.getNotNull(commandFactory);
+    }
+    
     /**
      * @deprecated This should not be used directly to communicate with the client . There is no direct replacement
      */
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceChangeCommandFactory.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceChangeCommandFactory.java
index 2b3aaa8..4e6a9f8 100644
--- a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceChangeCommandFactory.java
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceChangeCommandFactory.java
@@ -16,555 +16,49 @@
  */
 package org.apache.sling.ide.eclipse.core.internal;
 
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
-import org.apache.sling.ide.eclipse.core.ProjectUtil;
-import org.apache.sling.ide.eclipse.core.ResourceUtil;
-import org.apache.sling.ide.filter.Filter;
-import org.apache.sling.ide.filter.FilterResult;
-import org.apache.sling.ide.log.Logger;
-import org.apache.sling.ide.serialization.SerializationDataBuilder;
-import org.apache.sling.ide.serialization.SerializationException;
-import org.apache.sling.ide.serialization.SerializationKind;
-import org.apache.sling.ide.serialization.SerializationKindManager;
-import org.apache.sling.ide.serialization.SerializationManager;
+import org.apache.sling.ide.eclipse.core.EclipseResources;
 import org.apache.sling.ide.transport.Command;
-import org.apache.sling.ide.transport.CommandContext;
-import org.apache.sling.ide.transport.FileInfo;
 import org.apache.sling.ide.transport.Repository;
-import org.apache.sling.ide.transport.RepositoryException;
-import org.apache.sling.ide.transport.ResourceProxy;
-import org.apache.sling.ide.util.PathUtil;
-import org.eclipse.core.resources.IFile;
-import org.eclipse.core.resources.IFolder;
-import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IResource;
-import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.CoreException;
-import org.eclipse.core.runtime.IPath;
-import org.eclipse.core.runtime.IStatus;
-import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.Status;
-import org.eclipse.ui.statushandlers.StatusManager;
 
 /**
  * The <tt>ResourceChangeCommandFactory</tt> creates new {@link #Command commands} correspoding to resource addition,
  * change, or removal
  *
+ * @deprecated - Use the {@link DefaultCommandFactory} instead. This class is present until the tests are migrated off it
  */
+@Deprecated
 public class ResourceChangeCommandFactory {
 
-    private final Set<String> ignoredFileNames;
-
-    private final SerializationManager serializationManager;
-
-    public ResourceChangeCommandFactory(SerializationManager serializationManager, Set<String> ignoredFileNames) {
-        this.serializationManager = serializationManager;
-        this.ignoredFileNames = ignoredFileNames;
-    }
-
     public Command<?> newCommandForAddedOrUpdated(Repository repository, IResource addedOrUpdated) throws CoreException {
+        
         try {
-            return addFileCommand(repository, addedOrUpdated);
+            return Activator.getDefault().getCommandFactory().newCommandForAddedOrUpdatedResource(repository, EclipseResources.create(addedOrUpdated));
         } catch (IOException e) {
             throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed updating " + addedOrUpdated,
                     e));
         }
     }
 
-    private Command<?> addFileCommand(Repository repository, IResource resource) throws CoreException, IOException {
-
-        ResourceAndInfo rai = buildResourceAndInfo(resource, repository);
-        
-        if (rai == null) {
-            return null;
-        }
-        
-        CommandContext context = new CommandContext(ProjectUtil.loadFilter(resource.getProject()));
-
-        if (rai.isOnlyWhenMissing()) {
-            return repository.newAddOrUpdateNodeCommand(context, rai.getInfo(), rai.getResource(),
-                    Repository.CommandExecutionFlag.CREATE_ONLY_WHEN_MISSING);
-        }
-
-        return repository.newAddOrUpdateNodeCommand(context, rai.getInfo(), rai.getResource());
-    }
-
-    /**
-     * Convenience method which builds a <tt>ResourceAndInfo</tt> info for a specific <tt>IResource</tt>
-     * 
-     * @param resource the resource to process
-     * @param repository the repository, used to extract serialization information for different resource types
-     * @return the build object, or null if one could not be built
-     * @throws CoreException
-     * @throws SerializationException
-     * @throws IOException
-     */
-    public ResourceAndInfo buildResourceAndInfo(IResource resource, Repository repository) throws CoreException,
-            IOException {
-        if (ignoredFileNames.contains(resource.getName())) {
-            return null;
-        }
-
-        Long modificationTimestamp = (Long) resource.getSessionProperty(ResourceUtil.QN_IMPORT_MODIFICATION_TIMESTAMP);
-
-        if (modificationTimestamp != null && modificationTimestamp >= resource.getModificationStamp()) {
-            Activator.getDefault().getPluginLogger()
-                    .trace("Change for resource {0} ignored as the import timestamp {1} >= modification timestamp {2}",
-                            resource, modificationTimestamp, resource.getModificationStamp());
-            return null;
-        }
-
-        if (resource.isTeamPrivateMember(IResource.CHECK_ANCESTORS)) {
-            Activator.getDefault().getPluginLogger().trace("Skipping team-private resource {0}", resource);
-            return null;
-        }
-
-        FileInfo info = createFileInfo(resource);
-        Activator.getDefault().getPluginLogger().trace("For {0} built fileInfo {1}", resource, info);
-
-        File syncDirectoryAsFile = ProjectUtil.getSyncDirectoryFullPath(resource.getProject()).toFile();
-        IFolder syncDirectory = ProjectUtil.getSyncDirectory(resource.getProject());
-
-        Filter filter = ProjectUtil.loadFilter(resource.getProject());
-
-        ResourceProxy resourceProxy = null;
-
-        if (serializationManager.isSerializationFile(resource.getLocation().toOSString())) {
-            IFile file = (IFile) resource;
-            try (InputStream contents = file.getContents()) {
-                String resourceLocation = file.getFullPath().makeRelativeTo(syncDirectory.getFullPath())
-                        .toPortableString();
-                resourceProxy = serializationManager.readSerializationData(resourceLocation, contents);
-                normaliseResourceChildren(file, resourceProxy, syncDirectory, repository);
-
-
-                // TODO - not sure if this 100% correct, but we definitely should not refer to the FileInfo as the
-                // .serialization file, since for nt:file/nt:resource nodes this will overwrite the file contents
-                String primaryType = (String) resourceProxy.getProperties().get(Repository.JCR_PRIMARY_TYPE);
-                if (Repository.NT_FILE.equals(primaryType)) {
-                    // TODO move logic to serializationManager
-                    File locationFile = new File(info.getLocation());
-                    String locationFileParent = locationFile.getParent();
-                    int endIndex = locationFileParent.length() - ".dir".length();
-                    File actualFile = new File(locationFileParent.substring(0, endIndex));
-                    String newLocation = actualFile.getAbsolutePath();
-                    String newName = actualFile.getName();
-                    String newRelativeLocation = actualFile.getAbsolutePath().substring(
-                            syncDirectoryAsFile.getAbsolutePath().length());
-                    info = new FileInfo(newLocation, newRelativeLocation, newName);
-
-                    Activator.getDefault().getPluginLogger()
-                            .trace("Adjusted original location from {0} to {1}", resourceLocation, newLocation);
-
-                }
-
-            } catch (IOException e) {
-                Status s = new Status(Status.WARNING, Activator.PLUGIN_ID, "Failed reading file at "
-                        + resource.getFullPath(), e);
-                StatusManager.getManager().handle(s, StatusManager.LOG | StatusManager.SHOW);
-                return null;
-            }
-        } else {
-
-            // TODO - move logic to serializationManager
-            // possible .dir serialization holder
-            if (resource.getType() == IResource.FOLDER && resource.getName().endsWith(".dir")) {
-                IFolder folder = (IFolder) resource;
-                IResource contentXml = folder.findMember(".content.xml");
-                // .dir serialization holder ; nothing to process here, the .content.xml will trigger the actual work
-                if (contentXml != null && contentXml.exists()
-                        && serializationManager.isSerializationFile(contentXml.getLocation().toOSString())) {
-                    return null;
-                }
-            }
-
-            resourceProxy = buildResourceProxyForPlainFileOrFolder(resource, syncDirectory, repository);
-        }
-
-        FilterResult filterResult = getFilterResult(resource, resourceProxy, filter);
-
-        switch (filterResult) {
-
-            case ALLOW:
-                return new ResourceAndInfo(resourceProxy, info);
-            case PREREQUISITE:
-                // never try to 'create' the root node, we assume it exists
-                if (!resourceProxy.getPath().equals("/")) {
-                    // we don't explicitly set the primary type, which will allow the the repository to choose the best
-                    // suited one ( typically nt:unstructured )
-                    return new ResourceAndInfo(new ResourceProxy(resourceProxy.getPath()), null, true);
-                }
-            case DENY: // falls through
-            default:
-                return null;
-        }
-    }
-
-    private FileInfo createFileInfo(IResource resource) throws CoreException {
-
-        if (resource.getType() != IResource.FILE) {
-            return null;
-        }
-
-        IProject project = resource.getProject();
-
-        IFolder syncFolder = project.getFolder(ProjectUtil.getSyncDirectoryValue(project));
-
-        IPath relativePath = resource.getFullPath().makeRelativeTo(syncFolder.getFullPath());
-
-        FileInfo info = new FileInfo(resource.getLocation().toOSString(), relativePath.toOSString(), resource.getName());
-
-        Activator.getDefault().getPluginLogger().trace("For {0} built fileInfo {1}", resource, info);
-
-        return info;
-    }
-
-    /**
-     * Gets the filter result for a resource/resource proxy combination
-     * 
-     * <p>
-     * The resourceProxy may be null, typically when a resource is already deleted.
-     * 
-     * <p>
-     * In case the filter is {@code null} no resource should be added, i.e. {@link FilterResult#DENY} is returned
-     * 
-     * @param resource the resource to filter for, must not be <code>null</code>
-     * @param resourceProxy the resource proxy to filter for, possibly <code>null</code>
-     * @param filter the filter to use, possibly <tt>null</tt>
-     * @return the filtering result, never <code>null</code>
-     */
-    private FilterResult getFilterResult(IResource resource, ResourceProxy resourceProxy, Filter filter) {
-
-        if (filter == null) {
-            return FilterResult.DENY;
-        }
-
-        File contentSyncRoot = ProjectUtil.getSyncDirectoryFile(resource.getProject());
-
-        String repositoryPath = resourceProxy != null ? resourceProxy.getPath() : getRepositoryPathForDeletedResource(
-                resource, contentSyncRoot);
-
-        FilterResult filterResult = filter.filter(repositoryPath);
-
-        Activator.getDefault().getPluginLogger().trace("Filter result for {0} for {1}", repositoryPath, filterResult);
-
-        return filterResult;
-    }
-
-    private String getRepositoryPathForDeletedResource(IResource resource, File contentSyncRoot) {
-        IFolder syncFolder = ProjectUtil.getSyncDirectory(resource.getProject());
-        IPath relativePath = resource.getFullPath().makeRelativeTo(syncFolder.getFullPath());
-
-        String absFilePath = new File(contentSyncRoot, relativePath.toOSString()).getAbsolutePath();
-        String filePath = serializationManager.getBaseResourcePath(absFilePath);
-
-        IPath osPath = Path.fromOSString(filePath);
-        String repositoryPath = serializationManager.getRepositoryPath(osPath.makeRelativeTo(syncFolder.getLocation())
-                .makeAbsolute().toPortableString());
-
-        Activator.getDefault().getPluginLogger()
-                .trace("Repository path for deleted resource {0} is {1}", resource, repositoryPath);
-
-        return repositoryPath;
-    }
-
-    private ResourceProxy buildResourceProxyForPlainFileOrFolder(IResource changedResource, IFolder syncDirectory,
-            Repository repository)
-            throws CoreException, IOException {
-
-        SerializationKind serializationKind;
-        String fallbackNodeType;
-        if (changedResource.getType() == IResource.FILE) {
-            serializationKind = SerializationKind.FILE;
-            fallbackNodeType = Repository.NT_FILE;
-        } else { // i.e. IResource.FOLDER
-            serializationKind = SerializationKind.FOLDER;
-            fallbackNodeType = Repository.NT_FOLDER;
-        }
-
-        String resourceLocation = '/' + changedResource.getFullPath().makeRelativeTo(syncDirectory.getFullPath())
-                .toPortableString();
-        IPath serializationFilePath = Path.fromOSString(serializationManager.getSerializationFilePath(
-                resourceLocation, serializationKind));
-        IResource serializationResource = syncDirectory.findMember(serializationFilePath);
-
-        if (serializationResource == null && changedResource.getType() == IResource.FOLDER) {
-            ResourceProxy dataFromCoveringParent = findSerializationDataFromCoveringParent(changedResource,
-                    syncDirectory, resourceLocation, serializationFilePath);
-
-            if (dataFromCoveringParent != null) {
-                return dataFromCoveringParent;
-            }
-        }
-        return buildResourceProxy(resourceLocation, serializationResource, syncDirectory, fallbackNodeType, repository);
-    }
-
-    /**
-     * Tries to find serialization data from a resource in a covering parent
-     * 
-     * <p>
-     * If the serialization resource is null, it's valid to look for a serialization resource higher in the filesystem,
-     * given that the found serialization resource covers this resource
-     * 
-     * @param changedResource the resource which has changed
-     * @param syncDirectory the content sync directory for the resource's project
-     * @param resourceLocation the resource location relative to the sync directory
-     * @param serializationFilePath the location
-     * @return a <tt>ResourceProxy</tt> if there is a covering parent, or null is there is not
-     * @throws CoreException
-     * @throws IOException
-     */
-    private ResourceProxy findSerializationDataFromCoveringParent(IResource changedResource, IFolder syncDirectory,
-            String resourceLocation, IPath serializationFilePath) throws CoreException, IOException {
-
-        // TODO - this too should be abstracted in the service layer, rather than in the Eclipse-specific code
-
-        Logger logger = Activator.getDefault().getPluginLogger();
-        logger.trace("Found plain nt:folder candidate at {0}, trying to find a covering resource for it",
-                changedResource.getProjectRelativePath());
-        // don't use isRoot() to prevent infinite loop when the final path is '//'
-        while (serializationFilePath.segmentCount() != 0) {
-            serializationFilePath = serializationFilePath.removeLastSegments(1);
-            IFolder folderWithPossibleSerializationFile = syncDirectory.getFolder(serializationFilePath);
-            if (folderWithPossibleSerializationFile == null) {
-                logger.trace("No folder found at {0}, moving up to the next level", serializationFilePath);
-                continue;
-            }
-
-            // it's safe to use a specific SerializationKind since this scenario is only valid for METADATA_PARTIAL
-            // coverage
-            String possibleSerializationFilePath = serializationManager.getSerializationFilePath(
-                    ((IFolder) folderWithPossibleSerializationFile).getLocation().toOSString(),
-                    SerializationKind.METADATA_PARTIAL);
-
-            logger.trace("Looking for serialization data in {0}", possibleSerializationFilePath);
-
-            if (serializationManager.isSerializationFile(possibleSerializationFilePath)) {
-
-                IPath parentSerializationFilePath = Path.fromOSString(possibleSerializationFilePath).makeRelativeTo(
-                        syncDirectory.getLocation());
-                IFile possibleSerializationFile = syncDirectory.getFile(parentSerializationFilePath);
-                if (!possibleSerializationFile.exists()) {
-                    logger.trace("Potential serialization data file {0} does not exist, moving up to the next level",
-                            possibleSerializationFile.getFullPath());
-                    continue;
-                }
-
-                
-                ResourceProxy serializationData;
-                try (InputStream contents = possibleSerializationFile.getContents()) {
-                    serializationData = serializationManager.readSerializationData(
-                            parentSerializationFilePath.toPortableString(), contents);
-                }
-
-                String repositoryPath = serializationManager.getRepositoryPath(resourceLocation);
-                String potentialPath = serializationData.getPath();
-                boolean covered = serializationData.covers(repositoryPath);
-
-                logger.trace(
-                        "Found possible serialization data at {0}. Resource :{1} ; our resource: {2}. Covered: {3}",
-                        parentSerializationFilePath, potentialPath, repositoryPath, covered);
-                // note what we don't need to normalize the children here since this resource's data is covered by
-                // another resource
-                if (covered) {
-                    return serializationData.getChild(repositoryPath);
-                }
-
-                break;
-            }
-        }
-
-        return null;
-    }
-
-    private ResourceProxy buildResourceProxy(String resourceLocation, IResource serializationResource,
-            IFolder syncDirectory, String fallbackPrimaryType, Repository repository) throws CoreException, IOException {
-        if (serializationResource instanceof IFile) {
-            IFile serializationFile = (IFile) serializationResource;
-            try (InputStream contents = serializationFile.getContents() ) {
-                
-                String serializationFilePath = serializationResource.getFullPath()
-                        .makeRelativeTo(syncDirectory.getFullPath()).toPortableString();
-                ResourceProxy resourceProxy = serializationManager.readSerializationData(serializationFilePath, contents);
-                normaliseResourceChildren(serializationFile, resourceProxy, syncDirectory, repository);
-
-                return resourceProxy;
-            }
-        }
-
-        return new ResourceProxy(serializationManager.getRepositoryPath(resourceLocation), Collections.singletonMap(
-                Repository.JCR_PRIMARY_TYPE, (Object) fallbackPrimaryType));
-    }
-
-    /**
-     * Normalises the of the specified <tt>resourceProxy</tt> by comparing the serialization data and the filesystem
-     * data
-     * 
-     * @param serializationFile the file which contains the serialization data
-     * @param resourceProxy the resource proxy
-     * @param syncDirectory the sync directory
-     * @param repository TODO
-     * @throws CoreException
-     */
-    private void normaliseResourceChildren(IFile serializationFile, ResourceProxy resourceProxy, IFolder syncDirectory,
-            Repository repository) throws CoreException {
-
-        // TODO - this logic should be moved to the serializationManager
-        try {
-            SerializationKindManager skm = new SerializationKindManager();
-            skm.init(repository);
-
-            String primaryType = (String) resourceProxy.getProperties().get(Repository.JCR_PRIMARY_TYPE);
-            List<String> mixinTypesList = getMixinTypes(resourceProxy);
-            SerializationKind serializationKind = skm.getSerializationKind(primaryType, mixinTypesList);
-
-            if (serializationKind == SerializationKind.METADATA_FULL) {
-                return;
-            }
-        } catch (RepositoryException e) {
-            throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed creating a "
-                    + SerializationDataBuilder.class.getName(), e));
-        }
-
-        IPath serializationDirectoryPath = serializationFile.getFullPath().removeLastSegments(1);
-
-        Iterator<ResourceProxy> childIterator = resourceProxy.getChildren().iterator();
-        Map<String, IResource> extraChildResources = new HashMap<>();
-        for (IResource member : serializationFile.getParent().members()) {
-            if (member.equals(serializationFile)) {
-                continue;
-            }
-            extraChildResources.put(member.getName(), member);
-        }
-
-        while (childIterator.hasNext()) {
-            ResourceProxy child = childIterator.next();
-            String childName = PathUtil.getName(child.getPath());
-            String osPath = serializationManager.getOsPath(childName);
-
-            // covered children might have a FS representation, depending on their child nodes, so
-            // accept a directory which maps to their name
-            extraChildResources.remove(osPath);
-
-            // covered children do not need a filesystem representation
-            if (resourceProxy.covers(child.getPath())) {
-                continue;
-            }
-
-            IPath childPath = serializationDirectoryPath.append(osPath);
-
-            IResource childResource = ResourcesPlugin.getWorkspace().getRoot().findMember(childPath);
-            if (childResource == null) {
-
-                Activator.getDefault().getPluginLogger()
-                        .trace("For resource at with serialization data {0} the serialized child resource at {1} does not exist in the filesystem and will be ignored",
-                                serializationFile, childPath);
-                childIterator.remove();
-            }
-        }
-
-        for ( IResource extraChildResource : extraChildResources.values()) {
-            IPath extraChildResourcePath = extraChildResource.getFullPath()
-                    .makeRelativeTo(syncDirectory.getFullPath()).makeAbsolute();
-            resourceProxy.addChild(new ResourceProxy(serializationManager
-                    .getRepositoryPath(extraChildResourcePath.toPortableString())));
-            
-            Activator.getDefault().getPluginLogger()
-                .trace("For resource at with serialization data {0} the found a child resource at {1} which is not listed in the serialized child resources and will be added",
-                            serializationFile, extraChildResource);
-        }
-    }
-
-    private List<String> getMixinTypes(ResourceProxy resourceProxy) {
-
-        Object mixinTypesProp = resourceProxy.getProperties().get(Repository.JCR_MIXIN_TYPES);
-
-        if (mixinTypesProp == null) {
-            return Collections.emptyList();
-        }
-
-        if (mixinTypesProp instanceof String) {
-            return Collections.singletonList((String) mixinTypesProp);
-        }
-
-        return Arrays.asList((String[]) mixinTypesProp);
-    }
-
     public Command<?> newCommandForRemovedResources(Repository repository, IResource removed) throws CoreException {
-        
         try {
-            return removeFileCommand(repository, removed);
+            return  Activator.getDefault().getCommandFactory().newCommandForRemovedResource(repository, EclipseResources.create(removed));
         } catch (IOException e) {
             throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed removing" + removed, e));
         }
     }
 
-    private Command<?> removeFileCommand(Repository repository, IResource resource) throws CoreException, IOException {
-
-        if (resource.isTeamPrivateMember(IResource.CHECK_ANCESTORS)) {
-            Activator.getDefault().getPluginLogger().trace("Skipping team-private resource {0}", resource);
-            return null;
-        }
-
-        if (ignoredFileNames.contains(resource.getName())) {
-            return null;
-        }
-
-        IFolder syncDirectory = ProjectUtil.getSyncDirectory(resource.getProject());
-
-        Filter filter = ProjectUtil.loadFilter(syncDirectory.getProject());
-
-        FilterResult filterResult = getFilterResult(resource, null, filter);
-        if (filterResult == FilterResult.DENY || filterResult == FilterResult.PREREQUISITE) {
-            return null;
-        }
-        
-        String resourceLocation = getRepositoryPathForDeletedResource(resource,
-                ProjectUtil.getSyncDirectoryFile(resource.getProject()));
-        
-        // verify whether a resource being deleted does not signal that the content structure
-        // was rearranged under a covering parent aggregate
-        IPath serializationFilePath = Path.fromOSString(serializationManager.getSerializationFilePath(resourceLocation,
-                SerializationKind.FOLDER));
-
-        ResourceProxy coveringParentData = findSerializationDataFromCoveringParent(resource, syncDirectory,
-                resourceLocation, serializationFilePath);
-        if (coveringParentData != null) {
-            Activator
-                    .getDefault()
-                    .getPluginLogger()
-                    .trace("Found covering resource data ( repository path = {0} ) for resource at {1},  skipping deletion and performing an update instead",
-                            coveringParentData.getPath(), resource.getFullPath());
-            FileInfo info = createFileInfo(resource);
-            return repository.newAddOrUpdateNodeCommand(new CommandContext(filter), info, coveringParentData);
-        }
-        
-        return repository.newDeleteNodeCommand(serializationManager.getRepositoryPath(resourceLocation));
-    }
-
     public Command<Void> newReorderChildNodesCommand(Repository repository, IResource res) throws CoreException {
-
         try {
-            ResourceAndInfo rai = buildResourceAndInfo(res, repository);
-
-            if (rai == null || rai.isOnlyWhenMissing()) {
-                return null;
-            }
-
-            return repository.newReorderChildNodesCommand(rai.getResource());
+            return  Activator.getDefault().getCommandFactory().newReorderChildNodesCommand(repository, EclipseResources.create(res));
         } catch (IOException e) {
             throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID, "Failed reordering child nodes for "
                     + res, e));
         }
     }
+
 }
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/SlingLaunchpadBehaviour.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/SlingLaunchpadBehaviour.java
index c15560d..47aa0fe 100644
--- a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/SlingLaunchpadBehaviour.java
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/SlingLaunchpadBehaviour.java
@@ -196,7 +196,7 @@ public class SlingLaunchpadBehaviour extends ServerBehaviourDelegateWithModulePu
         Logger logger = Activator.getDefault().getPluginLogger();
         
         if (commandFactory == null) {
-            commandFactory = new ResourceChangeCommandFactory(Activator.getDefault().getSerializationManager(), Activator.getDefault().getPreferences().getIgnoredFileNamesForSync());
+            commandFactory = new ResourceChangeCommandFactory();
         }
 
         logger.trace(traceOperation(kind, deltaKind, module));
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceDirectory.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceDirectory.java
new file mode 100644
index 0000000..bac29c3
--- /dev/null
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceDirectory.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.eclipse.core.internal.sync.content;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.sling.ide.eclipse.core.EclipseResources;
+import org.apache.sling.ide.sync.content.WorkspaceDirectory;
+import org.apache.sling.ide.sync.content.WorkspaceFile;
+import org.apache.sling.ide.sync.content.WorkspaceProject;
+import org.apache.sling.ide.sync.content.WorkspaceResource;
+import org.apache.sling.ide.sync.content.WorkspacePath;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.runtime.CoreException;
+
+public class EclipseWorkspaceDirectory extends EclipseWorkspaceResource implements WorkspaceDirectory {
+
+    public EclipseWorkspaceDirectory(IFolder folder, Set<String> ignoredFileNames) {
+        super(folder, ignoredFileNames);
+    }
+    
+    @Override
+    protected IFolder getResource() {
+        return (IFolder) super.getResource();
+    }
+    
+    @Override
+    public WorkspaceProject getProject() {
+        return new EclipseWorkspaceProject(getResource().getProject(), getIgnoredFileNames());
+    }
+    
+    @Override
+    public WorkspacePath getLocalPath() {
+        return new WorkspacePath(getResource().getFullPath().toPortableString());
+    }
+
+    @Override
+    public WorkspaceFile getFile(WorkspacePath relativePath) {
+        return new EclipseWorkspaceFile(getResource().getFile(relativePath.asPortableString()), getIgnoredFileNames());
+    }
+    
+    @Override
+    public WorkspaceDirectory getDirectory(WorkspacePath relativePath) {
+        return new EclipseWorkspaceDirectory(getResource().getFolder(relativePath.asPortableString()), getIgnoredFileNames());
+    }
+
+    @Override
+    public List<WorkspaceResource> getChildren() {
+        try {
+            return Arrays.stream(getResource().members())
+                .map(EclipseResources::create)
+                .collect(Collectors.toList());
+        } catch ( CoreException e ) {
+            // TODO - proper exception handling
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceFile.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceFile.java
new file mode 100644
index 0000000..06b735e
--- /dev/null
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceFile.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.eclipse.core.internal.sync.content;
+
+import java.io.InputStream;
+import java.util.Set;
+
+import org.apache.sling.ide.sync.content.WorkspaceDirectory;
+import org.apache.sling.ide.sync.content.WorkspaceFile;
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.runtime.CoreException;
+
+/**
+ * A {@link WorkspaceFile} implemenation based on Eclipse APIs.
+ *
+ */
+public class EclipseWorkspaceFile extends EclipseWorkspaceResource implements WorkspaceFile {
+
+    public EclipseWorkspaceFile(IFile resource, Set<String> ignoredFileNames) {
+        super(resource, ignoredFileNames);
+    }
+
+    @Override
+    public InputStream getContents() {
+        try {
+            return getResource().getContents();
+        } catch (CoreException e) {
+            // TODO Auto-generated catch block
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public WorkspaceDirectory getParent() {
+        IContainer parent = getResource().getParent();
+        
+        if ( parent instanceof IFolder ) 
+            return new EclipseWorkspaceDirectory((IFolder) parent, getIgnoredFileNames());
+        
+        return null;
+    }
+    @Override
+    protected IFile getResource() {
+        return (IFile) super.getResource();
+    }
+}
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceProject.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceProject.java
new file mode 100644
index 0000000..393c565
--- /dev/null
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceProject.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.eclipse.core.internal.sync.content;
+
+import java.util.Set;
+
+import org.apache.sling.ide.eclipse.core.ProjectUtil;
+import org.apache.sling.ide.filter.Filter;
+import org.apache.sling.ide.sync.content.WorkspaceDirectory;
+import org.apache.sling.ide.sync.content.WorkspacePath;
+import org.apache.sling.ide.sync.content.WorkspaceProject;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+
+public class EclipseWorkspaceProject extends EclipseWorkspaceResource implements WorkspaceProject {
+
+    public EclipseWorkspaceProject(IProject project, Set<String> ignoredFileNames) {
+        super(project, ignoredFileNames);
+    }
+    
+    @Override
+    public WorkspaceDirectory getSyncDirectory() {
+        return new EclipseWorkspaceDirectory(ProjectUtil.getSyncDirectory(getResource()), getIgnoredFileNames());
+    }
+    
+    @Override
+    public Filter getFilter() {
+        try {
+            final Filter filter = ProjectUtil.loadFilter(getResource());
+            if ( filter == null )
+                throw new IllegalStateException("No filter for " + this);
+            return filter;
+        } catch (CoreException e) {
+            // TODO Auto-generated catch block
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public WorkspaceDirectory getDirectory(WorkspacePath path) {
+        return new EclipseWorkspaceDirectory(getResource().getFolder(path.asPortableString()), getIgnoredFileNames());
+    }
+    
+    @Override
+    protected IProject getResource() {
+        return (IProject) super.getResource();
+    }
+
+}
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceResource.java b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceResource.java
new file mode 100644
index 0000000..a307f3a
--- /dev/null
+++ b/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/sync/content/EclipseWorkspaceResource.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.eclipse.core.internal.sync.content;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.sling.ide.eclipse.core.internal.Activator;
+import org.apache.sling.ide.sync.content.WorkspacePath;
+import org.apache.sling.ide.sync.content.WorkspaceProject;
+import org.apache.sling.ide.sync.content.WorkspaceResource;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.QualifiedName;
+
+public abstract class EclipseWorkspaceResource implements WorkspaceResource {
+
+    private final IResource resource;
+    private final Set<String> ignoredFileNames;
+    
+    protected EclipseWorkspaceResource(IResource resource, Set<String> ignoredFileNames) {
+        this.resource = resource;
+        this.ignoredFileNames = ignoredFileNames;
+    }
+
+    @Override
+    public boolean exists() {
+        return resource.exists();
+    }
+
+    @Override
+    public boolean isIgnored() {
+        return ignoredFileNames.contains(getName()) ||
+                resource.isTeamPrivateMember(IResource.CHECK_ANCESTORS);
+    }
+
+    @Override
+    public WorkspacePath getLocalPath() {
+        return new WorkspacePath(resource.getFullPath().toPortableString());
+    }
+
+    @Override
+    public Path getOSPath() {
+        return Paths.get(resource.getLocation().toOSString());
+    }
+
+    @Override
+    public WorkspaceProject getProject() {
+        return new EclipseWorkspaceProject(resource.getProject(), ignoredFileNames);
+    }
+    
+    @Override
+    public long getLastModified() {
+        return resource.getModificationStamp();
+    }
+    
+    @Override
+    public Object getTransientProperty(String propertyName) {
+        try {
+            return resource.getSessionProperty(new QualifiedName(Activator.PLUGIN_ID, propertyName));
+        } catch (CoreException e) {
+            // TODO Auto-generated catch block
+            throw new RuntimeException();
+        }
+    }
+    
+    protected IResource getResource() {
+        return resource;
+    }
+    
+    protected Set<String> getIgnoredFileNames() {
+        return ignoredFileNames;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((resource == null) ? 0 : resource.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if ( !(obj instanceof EclipseWorkspaceResource) ) {
+            return false;
+        }
+        
+        EclipseWorkspaceResource other = (EclipseWorkspaceResource) obj;
+        
+        return Objects.equals(this.resource, other.resource);
+
+    }
+    
+    @Override
+    public String toString() {
+        return resource.toString();
+    }
+    
+}
diff --git a/eclipse/eclipse-test/src/org/apache/sling/ide/test/impl/ResourceChangeCommandFactoryTest.java b/eclipse/eclipse-test/src/org/apache/sling/ide/test/impl/ResourceChangeCommandFactoryTest.java
index 998aa40..c5a328d 100644
--- a/eclipse/eclipse-test/src/org/apache/sling/ide/test/impl/ResourceChangeCommandFactoryTest.java
+++ b/eclipse/eclipse-test/src/org/apache/sling/ide/test/impl/ResourceChangeCommandFactoryTest.java
@@ -72,7 +72,7 @@ public class ResourceChangeCommandFactoryTest {
 
         Set<String> ignoredFileNames = new HashSet<>();
         ignoredFileNames.add(".gitignore");
-        factory = new ResourceChangeCommandFactory(Activator.getDefault().getSerializationManager(), ignoredFileNames);
+        factory = new ResourceChangeCommandFactory();
 
         spyRepo = new SpyRepository();
     }
diff --git a/eclipse/eclipse-ui/META-INF/MANIFEST.MF b/eclipse/eclipse-ui/META-INF/MANIFEST.MF
index ac70d18..b96203a 100644
--- a/eclipse/eclipse-ui/META-INF/MANIFEST.MF
+++ b/eclipse/eclipse-ui/META-INF/MANIFEST.MF
@@ -23,6 +23,7 @@ Import-Package: javax.jcr,
  org.apache.sling.ide.log,
  org.apache.sling.ide.osgi,
  org.apache.sling.ide.serialization,
+ org.apache.sling.ide.sync.content;version="1.0.0",
  org.apache.sling.ide.transport,
  org.apache.sling.ide.util,
  org.eclipse.core.commands,
diff --git a/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/Activator.java b/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/Activator.java
index 28a3810..0e56bdd 100644
--- a/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/Activator.java
+++ b/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/Activator.java
@@ -24,6 +24,7 @@ import org.apache.sling.ide.filter.FilterLocator;
 import org.apache.sling.ide.log.Logger;
 import org.apache.sling.ide.osgi.OsgiClientFactory;
 import org.apache.sling.ide.serialization.SerializationManager;
+import org.apache.sling.ide.sync.content.SyncCommandFactory;
 import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.jface.preference.IPreferenceStore;
 import org.eclipse.ui.plugin.AbstractUIPlugin;
@@ -44,7 +45,8 @@ public class Activator extends AbstractUIPlugin {
     private ServiceTracker<EmbeddedArtifactLocator, EmbeddedArtifactLocator> artifactLocator;
     private ServiceTracker<OsgiClientFactory, OsgiClientFactory> osgiClientFactory;
     private ServiceTracker<Logger, Logger> tracer;
-
+    private ServiceTracker<SyncCommandFactory, SyncCommandFactory> commandFactory;
+    
     private ServiceRegistration<Logger> tracerRegistration;
     private ScopedPreferenceStore preferenceStore;
     
@@ -80,6 +82,9 @@ public class Activator extends AbstractUIPlugin {
 
         tracer = new ServiceTracker<>(context, tracerRegistration.getReference(), null);
         tracer.open();
+        
+        commandFactory = new ServiceTracker<>(context, SyncCommandFactory.class, null);
+        commandFactory.open();        
 
         INSTANCE = this;
     }
@@ -92,6 +97,7 @@ public class Activator extends AbstractUIPlugin {
         eventAdmin.close();
         artifactLocator.close();
         osgiClientFactory.close();
+        commandFactory.close();
 
         super.stop(context);
     }
@@ -120,6 +126,10 @@ public class Activator extends AbstractUIPlugin {
     public Logger getPluginLogger() {
         return (Logger) ServiceUtil.getNotNull(tracer);
     }
+    
+    public SyncCommandFactory getCommandFactory() {
+        return ServiceUtil.getNotNull(commandFactory);
+    }
 
     @Override
     public IPreferenceStore getPreferenceStore() {
diff --git a/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ExportWizard.java b/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ExportWizard.java
index 5b9e1fe..e647e34 100644
--- a/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ExportWizard.java
+++ b/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ExportWizard.java
@@ -16,11 +16,14 @@
  */
 package org.apache.sling.ide.eclipse.ui.internal;
 
+import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 
+import org.apache.sling.ide.eclipse.core.EclipseResources;
 import org.apache.sling.ide.eclipse.core.ServerUtil;
 import org.apache.sling.ide.eclipse.core.internal.ResourceChangeCommandFactory;
 import org.apache.sling.ide.eclipse.ui.WhitelabelSupport;
+import org.apache.sling.ide.sync.content.SyncCommandFactory;
 import org.apache.sling.ide.transport.Command;
 import org.apache.sling.ide.transport.Repository;
 import org.apache.sling.ide.transport.Result;
@@ -61,8 +64,7 @@ public class ExportWizard extends Wizard {
 
                 @Override
                 public void run(final IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
-                    final ResourceChangeCommandFactory factory = new ResourceChangeCommandFactory(Activator
-                            .getDefault().getSerializationManager(), Activator.getDefault().getPreferences().getIgnoredFileNamesForSync());
+                    final SyncCommandFactory factory = Activator.getDefault().getCommandFactory();
 
                     final Repository[] selectedServer = new Repository[1];
                     Display.getDefault().syncExec(new Runnable() {
@@ -82,16 +84,21 @@ public class ExportWizard extends Wizard {
 
                             @Override
                             public boolean visit(IResource resource) throws CoreException {
-                                Command<?> command = factory.newCommandForAddedOrUpdated(selectedServer[0], resource);
-                                if (command == null) {
-                                    return true;
-                                }
-                                Result<?> result = command.execute();
-                                if (!result.isSuccess()) {
+                                try {
+                                    Command<?> command = factory.newCommandForAddedOrUpdatedResource(selectedServer[0],
+                                            EclipseResources.create(resource));
+                                    if (command == null) {
+                                        return true;
+                                    }
+                                    Result<?> result = command.execute();
+                                    if (!result.isSuccess()) {
+                                        throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID,
+                                                "Failed exporting: " + result.toString()));
+                                    }
+                                } catch (IOException e) {
                                     throw new CoreException(new Status(Status.ERROR, Activator.PLUGIN_ID,
-                                            "Failed exporting: " + result.toString()));
+                                            "Failed exporting: " + e.getMessage()));
                                 }
-
                                 return true;
                             }
                         });
diff --git a/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ImportRepositoryContentAction.java b/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ImportRepositoryContentAction.java
index 1df4b11..e5e9196 100644
--- a/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ImportRepositoryContentAction.java
+++ b/eclipse/eclipse-ui/src/org/apache/sling/ide/eclipse/ui/internal/ImportRepositoryContentAction.java
@@ -29,10 +29,10 @@ import java.util.Set;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.jackrabbit.util.Text;
+import org.apache.sling.ide.eclipse.core.EclipseResources;
 import org.apache.sling.ide.eclipse.core.ProjectUtil;
 import org.apache.sling.ide.eclipse.core.ResourceUtil;
 import org.apache.sling.ide.eclipse.core.ServerUtil;
-import org.apache.sling.ide.eclipse.core.internal.ResourceAndInfo;
 import org.apache.sling.ide.eclipse.core.internal.ResourceChangeCommandFactory;
 import org.apache.sling.ide.eclipse.core.progress.ProgressUtils;
 import org.apache.sling.ide.filter.Filter;
@@ -45,9 +45,11 @@ import org.apache.sling.ide.serialization.SerializationException;
 import org.apache.sling.ide.serialization.SerializationKind;
 import org.apache.sling.ide.serialization.SerializationKindManager;
 import org.apache.sling.ide.serialization.SerializationManager;
+import org.apache.sling.ide.sync.content.SyncCommandFactory;
 import org.apache.sling.ide.transport.Command;
 import org.apache.sling.ide.transport.Repository;
 import org.apache.sling.ide.transport.RepositoryException;
+import org.apache.sling.ide.transport.ResourceAndInfo;
 import org.apache.sling.ide.transport.ResourceProxy;
 import org.apache.sling.ide.transport.Result;
 import org.eclipse.core.resources.IContainer;
@@ -185,7 +187,7 @@ public class ImportRepositoryContentAction {
 
     private void recordNotIgnoredResources() throws CoreException {
 
-        final ResourceChangeCommandFactory rccf = new ResourceChangeCommandFactory(serializationManager, Activator.getDefault().getPreferences().getIgnoredFileNamesForSync());
+        final SyncCommandFactory commandFactory = Activator.getDefault().getCommandFactory();
 
         IResource importStartingPoint = contentSyncRootDir.findMember(repositoryImportRoot);
         if (importStartingPoint == null) {
@@ -197,7 +199,7 @@ public class ImportRepositoryContentAction {
             public boolean visit(IResource resource) throws CoreException {
 
                 try {
-                    ResourceAndInfo rai = rccf.buildResourceAndInfo(resource, repository);
+                    ResourceAndInfo rai = commandFactory.buildResourceAndInfo(EclipseResources.create(resource), repository);
 
                     if (rai == null) {
                         // can be a prerequisite
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java b/shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java
index 86639a0..3f56aec 100644
--- a/shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java
@@ -27,10 +27,23 @@ public interface SerializationManager {
 
     void destroy();
 
+    /**
+     * @param filePath the filesystem path
+     * @return
+     */
     boolean isSerializationFile(String filePath);
 
+    /**
+     * @param serializationFilePath the full OS path to the serialization file
+     * @return
+     */
     String getBaseResourcePath(String serializationFilePath);
 
+    /**
+     * @param baseFilePath the filesystem path of the resource
+     * @param serializationKind
+     * @return
+     */
     String getSerializationFilePath(String baseFilePath, SerializationKind serializationKind);
 
     String getRepositoryPath(String osPath);
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/SyncCommandFactory.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/SyncCommandFactory.java
new file mode 100644
index 0000000..e0047b2
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/SyncCommandFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import java.io.IOException;
+
+import org.apache.sling.ide.transport.Command;
+import org.apache.sling.ide.transport.Repository;
+import org.apache.sling.ide.transport.ResourceAndInfo;
+
+/**
+ * Creates commands in response to local resource changes 
+ */
+public interface SyncCommandFactory {
+    
+    String PN_IMPORT_MODIFICATION_TIMESTAMP = "importModificationTimestamp";
+    
+    /**
+     * Creates a command in response to a local resource being deleted
+     * 
+     * @param repository the repository that will create the command
+     * @param resource the resource that was deleted 
+     * @return the commmand to execute
+     * @throws IOException I/O related problems
+     */
+    Command<?> newCommandForRemovedResource(Repository repository, WorkspaceResource resource) throws IOException;
+
+    /**
+     * Creates a command in response to a local resource being added or updated
+     * 
+     * @param repository the repository that will create the command
+     * @param resource the resource that was added or updated
+     * @return the commmand to execute
+     * @throws IOException I/O related problems
+     */
+    Command<?> newCommandForAddedOrUpdatedResource(Repository repository, WorkspaceResource resource) throws IOException;
+    
+    /**
+     * Creates a command which reorders the child nodes of the node corresponding to the specified local resource 
+     * 
+     * @param repository the repository that will create the command
+     * @param resource the resource whose children should be reorderer
+     * @return the command to execut
+     * @throws IOException I/O related problems
+     */
+    Command<Void> newReorderChildNodesCommand(Repository repository, WorkspaceResource resource) throws IOException;
+    
+    /**
+     * Convenience method which builds a <tt>ResourceAndInfo</tt> info for a specific <tt>IResource</tt>
+     * 
+     * @param resource the resource to process
+     * @param repository the repository, used to extract serialization information for different resource types
+     * @return the built object, or null if one could not be built
+     * @throws IOException I/O related problems
+     */
+    ResourceAndInfo buildResourceAndInfo(WorkspaceResource resource, Repository repository) throws IOException;
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceDirectory.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceDirectory.java
new file mode 100644
index 0000000..596af73
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceDirectory.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import java.util.List;
+
+/**
+ * Represents a local directory, <tt>i.e.</tt> present in the local workspace
+ *
+ */
+public interface WorkspaceDirectory extends WorkspaceResource {
+
+    /**
+     * Returns a child file for this directory.
+     * 
+     * <p>The file may not exist, so make sure to check {@link WorkspaceResource#exists()}</p>
+     * 
+     * @param relativePath the relative path to the file
+     * @return the file ( which may not exist )
+     */
+    WorkspaceFile getFile(WorkspacePath relativePath);
+
+    /**
+     * Returns a child directory for this directory.
+     * 
+     * <p>The directory may not exist, so make sure to check {@link WorkspaceResource#exists()}</p>
+     * 
+     * @param relativePath the relative path to the directory
+     * @return the directory ( which may not exist )
+     */        
+    WorkspaceDirectory getDirectory(WorkspacePath relativePath);
+
+    /**
+     * Returns a list of children, guaranteed to exist at the time of the call
+     * 
+     * @return a list of children
+     */
+    List<WorkspaceResource> getChildren();
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceFile.java
similarity index 50%
copy from shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java
copy to shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceFile.java
index 86639a0..e2f59db 100644
--- a/shared/modules/api/src/main/java/org/apache/sling/ide/serialization/SerializationManager.java
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceFile.java
@@ -14,36 +14,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sling.ide.serialization;
+package org.apache.sling.ide.sync.content;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 
-import org.apache.sling.ide.transport.Repository;
-import org.apache.sling.ide.transport.ResourceProxy;
-
-public interface SerializationManager {
-
-    void destroy();
-
-    boolean isSerializationFile(String filePath);
-
-    String getBaseResourcePath(String serializationFilePath);
-
-    String getSerializationFilePath(String baseFilePath, SerializationKind serializationKind);
-
-    String getRepositoryPath(String osPath);
-
-    String getOsPath(String repositoryPath);
+/**
+ * Represents a local file, <tt>i.e.</tt> present in the local workspace
+ */
+public interface WorkspaceFile extends WorkspaceResource {
 
-    SerializationDataBuilder newBuilder(Repository repository, File contentSyncRoot) throws SerializationException;
+    /**
+     * Returns the contents of the file
+     * 
+     * @throws IOException I/O errors
+     * @return the contents of the file
+     */
+    InputStream getContents() throws IOException;
 
     /**
-     * @param filePath The filePath, in repository format
-     * @param source
-     * @return
-     * @throws IOException
+     * Returns the parent of this file
+     * 
+     * @return the parent of this file
      */
-    ResourceProxy readSerializationData(String filePath, InputStream source) throws IOException;
+    WorkspaceDirectory getParent();
+
 }
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspacePath.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspacePath.java
new file mode 100644
index 0000000..eeb450d
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspacePath.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import java.nio.file.Path;
+import java.util.Objects;
+
+import org.apache.sling.ide.util.PathUtil;
+
+/**
+ * A path in the local workspace
+ * 
+ * <p>The workspace path always uses the forward slash ( <tt>/</tt> ) for separating segments.</p>
+ *
+ */
+public class WorkspacePath {
+
+    private final String portablePath;
+    
+    public WorkspacePath(String portablePath) {
+        if ( portablePath == null )
+            throw new IllegalArgumentException("Invalid path :'" + portablePath+"'");
+        
+        if ( portablePath.length() > 1 && portablePath.endsWith("/") )
+            portablePath = portablePath.substring(0, portablePath.length() - 1);
+        
+        this.portablePath = portablePath;
+    }
+    
+    public String getName() {
+        return PathUtil.getName(portablePath);
+    }
+    
+    public String asPortableString() {
+        return portablePath;
+    }
+    
+    /**
+     * Creates relative bath between this path and the one passed as an argument.
+     * 
+     * <p>This method uses the same name uses the same meaning for the parameters` as the {@link Path#relativize(Path)} method.</p>
+     * 
+     * <p>For this comparison the paths are made absolute, but the returned path is relative.</p>
+     * 
+     * @param other the potential parent path
+     * @return a relative path, or <code>null</code> is the paths are unrelated
+     */
+    public WorkspacePath relativize(WorkspacePath other) {
+
+        String ours = absolute().portablePath;
+        String theirs = other.absolute().portablePath;
+        
+        if ( ours.equals(theirs) )
+            return new WorkspacePath("");
+        
+        if ( theirs.startsWith(ours) )
+            return new WorkspacePath(theirs.substring(ours.length() + 1));
+        
+        return null;
+    }
+
+    public boolean isRoot() {
+        return portablePath.equals("/");
+    }
+    
+    public WorkspacePath getParent() {
+        String path = PathUtil.getParent(portablePath);
+        if ( path == null )
+            return null;
+        return new WorkspacePath(path);
+    }
+    
+    public WorkspacePath absolute() {
+        if ( isAbsolute() )
+            return this;
+        
+        return new WorkspacePath('/' + portablePath);
+    }
+
+    private boolean isAbsolute() {
+        return portablePath.charAt(0) == '/';
+    }
+    
+    public WorkspacePath append(String name) {
+        if ( name == null || name.isEmpty() || name.indexOf('/') != -1)
+            throw new IllegalArgumentException("Invalid name: '" + name + "'");
+        
+        return new WorkspacePath(portablePath + '/' + name);
+        
+    }
+    
+    public WorkspacePath append(WorkspacePath other) {
+        
+        if ( other == null )
+            throw new IllegalArgumentException("Unable to append null path");
+        
+        return new WorkspacePath(PathUtil.join(portablePath, other.portablePath));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if ( !(obj instanceof WorkspacePath) )
+            return false;
+        
+        return Objects.equals(portablePath, ((WorkspacePath) obj).portablePath);
+    }
+    
+    @Override
+    public String toString() {
+        return asPortableString();
+    }
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspacePaths.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspacePaths.java
new file mode 100644
index 0000000..1f745f4
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspacePaths.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import java.io.File;
+import java.nio.file.Path;
+
+/**
+ * 
+ * Holds utility methods related to path instances
+ *
+ */
+public abstract class WorkspacePaths {
+
+    public static WorkspacePath fromOsPath(Path path) {
+        
+        return new WorkspacePath(path.toString().replace(File.separatorChar, '/'));
+    }
+    
+    private WorkspacePaths() {
+        
+    }
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceProject.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceProject.java
new file mode 100644
index 0000000..4be9cae
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceProject.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import org.apache.sling.ide.filter.Filter;
+
+/**
+ * Represents a local project, <tt>i.e.</tt> present in the local workspace
+ * 
+ * <p>It also provides easy access to various content sync objects related
+ * to the projects.</p>
+ */
+public interface WorkspaceProject extends WorkspaceResource {
+    
+    WorkspaceDirectory getSyncDirectory();
+
+    Filter getFilter();
+
+    WorkspaceDirectory getDirectory(WorkspacePath path);
+
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceResource.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceResource.java
new file mode 100644
index 0000000..2b29237
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/WorkspaceResource.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import java.nio.file.Path;
+
+/**
+ * Groups common operations on workspace resources.
+ */
+public interface WorkspaceResource {
+    
+    /**
+     * Returns the name of this resource
+     * 
+     * @return the name of the resource
+     */
+    default String getName() {
+        return getLocalPath().getName();
+    }
+    
+    /**
+     * Returns true if the resource exists, false otherwise
+     * 
+     * <p>A resource object not exist if it is constructed for instance to
+     * send a notification for a deleted resource.</p>
+     * 
+     * @return true if the resource exists, false otherwise
+     */
+    boolean exists();
+    
+    /**
+     * Returns true if this resource is ignored for content sync purposes
+     * 
+     * @return true if the resource is ignored, false otherwise
+     */
+    boolean isIgnored();
+    
+    /**
+     * Returns the absolute local path, rooted at the workspace level
+     * 
+     * @return the local path
+     */
+    WorkspacePath getLocalPath();
+    
+    /**
+     * Returns the absolute OS path to this resource
+     * 
+     * @return the OS path
+     */
+    Path getOSPath();
+    
+    /**
+     * @return the project which holds this resource
+     */
+    WorkspaceProject getProject();
+    
+    /**
+     * @return the last modified timestamp
+     */
+    long getLastModified();
+    
+    /**
+     * Returns a value for the specified transient property
+     * 
+     * <p>The properties are not persisted and the lifetime of the duration
+     * is governed by the specific implementation.</p>
+     * 
+     * @param propertyName property name
+     * @return the value for the transient property, or <code>null</code>
+     */
+    Object getTransientProperty(String propertyName);
+    
+    default WorkspacePath getPathRelativeToSyncDir() {
+        final WorkspacePath relativePath = getProject().getSyncDirectory().getLocalPath().relativize(getLocalPath());
+        if ( relativePath == null )
+            throw new RuntimeException("Unable to get relative path between sync dir " + getProject().getSyncDirectory().getLocalPath() + " and " + getLocalPath());
+        return relativePath;
+    }
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/impl/DefaultSyncCommandFactory.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/impl/DefaultSyncCommandFactory.java
new file mode 100644
index 0000000..ec222ad
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/impl/DefaultSyncCommandFactory.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sling.ide.filter.FilterResult;
+import org.apache.sling.ide.log.Logger;
+import org.apache.sling.ide.serialization.SerializationKind;
+import org.apache.sling.ide.serialization.SerializationKindManager;
+import org.apache.sling.ide.serialization.SerializationManager;
+import org.apache.sling.ide.sync.content.SyncCommandFactory;
+import org.apache.sling.ide.sync.content.WorkspaceDirectory;
+import org.apache.sling.ide.sync.content.WorkspaceFile;
+import org.apache.sling.ide.sync.content.WorkspacePath;
+import org.apache.sling.ide.sync.content.WorkspacePaths;
+import org.apache.sling.ide.sync.content.WorkspaceResource;
+import org.apache.sling.ide.transport.Command;
+import org.apache.sling.ide.transport.CommandContext;
+import org.apache.sling.ide.transport.FileInfo;
+import org.apache.sling.ide.transport.Repository;
+import org.apache.sling.ide.transport.RepositoryException;
+import org.apache.sling.ide.transport.ResourceAndInfo;
+import org.apache.sling.ide.transport.ResourceProxy;
+import org.apache.sling.ide.util.PathUtil;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+@Component(service = SyncCommandFactory.class)
+public class DefaultSyncCommandFactory implements SyncCommandFactory {
+    
+    @Reference
+    private SerializationManager serializationManager;
+    
+    @Reference
+    private Logger logger;
+   
+
+    @Override
+    public Command<?> newCommandForRemovedResource(Repository repository, WorkspaceResource resource) throws IOException {
+        
+        if (resource.isIgnored()) {
+            logger.trace("Skipping ignored resource {0}", resource);
+            return null;
+        }
+        
+        String repositoryPath = resource.getPathRelativeToSyncDir().absolute().asPortableString();
+
+        FilterResult filterResult = resource.getProject().getFilter().filter(repositoryPath);
+        
+        logger.trace("Filter result for {0} is {1}", repositoryPath, filterResult);
+        
+        if (filterResult == null || filterResult == FilterResult.DENY || filterResult == FilterResult.PREREQUISITE) {
+            return null;
+        }
+        
+        // TODO - we send different parametrs to findSerializationDataFromCoveringParent 
+        // for added vs removed resources
+        
+        // verify whether a resource being deleted does not signal that the content structure
+        // was rearranged under a covering parent aggregate
+        Path serializationFilePath = Paths.get(serializationManager.getSerializationFilePath(repositoryPath,
+                SerializationKind.FOLDER));
+        
+        WorkspacePath serializationFileLocalPath = resource.getProject().getSyncDirectory().getLocalPath().append(WorkspacePaths.fromOsPath(serializationFilePath));
+
+        ResourceProxy coveringParentData = findSerializationDataFromCoveringParent(resource, repositoryPath, serializationFileLocalPath);
+        if (coveringParentData != null) {
+            logger.trace("Found covering resource data ( repository path = {0} ) for resource at {1},  skipping deletion and performing an update instead",
+                            coveringParentData.getPath(), resource.getLocalPath());
+            FileInfo info = createFileInfo(resource);
+            return repository.newAddOrUpdateNodeCommand(new CommandContext(resource.getProject().getFilter()), info, coveringParentData);
+        }
+        
+        return repository.newDeleteNodeCommand(serializationManager.getRepositoryPath(repositoryPath));
+    }
+    
+    @Override
+    public Command<?> newCommandForAddedOrUpdatedResource(Repository repository, WorkspaceResource resource)
+            throws IOException {
+        ResourceAndInfo rai = buildResourceAndInfo(resource, repository);
+        
+        if (rai == null) {
+            return null;
+        }
+        
+        CommandContext context = new CommandContext(resource.getProject().getFilter());
+
+        if (rai.isOnlyWhenMissing()) {
+            return repository.newAddOrUpdateNodeCommand(context, rai.getInfo(), rai.getResource(),
+                    Repository.CommandExecutionFlag.CREATE_ONLY_WHEN_MISSING);
+        }
+
+        return repository.newAddOrUpdateNodeCommand(context, rai.getInfo(), rai.getResource());
+    }
+
+   private ResourceProxy findSerializationDataFromCoveringParent(WorkspaceResource localFile,
+           String resourceLocation, WorkspacePath serializationFilePath) throws IOException {
+
+       logger.trace("Found plain nt:folder candidate at {0}, trying to find a covering resource for it",
+localFile);
+       
+       WorkspacePath orig = serializationFilePath;
+
+       WorkspacePath syncDirPath = localFile.getProject().getSyncDirectory().getLocalPath();
+       serializationFilePath = localFile.getProject().getLocalPath().relativize(serializationFilePath);
+       if ( serializationFilePath == null )
+           throw new RuntimeException("Unable to get relative path from " + localFile.getProject().getLocalPath() + " to " + orig);
+       serializationFilePath = serializationFilePath.absolute();
+       
+       while (!syncDirPath.equals(serializationFilePath)) {
+           serializationFilePath = serializationFilePath.getParent();
+           // TODO - better check for path going outside the sync directory
+           if ( serializationFilePath.asPortableString().lastIndexOf('/') == 0 ) {
+               break;
+           }
+           WorkspaceDirectory folderWithPossibleSerializationFile = localFile.getProject().getDirectory(serializationFilePath);
+           
+           
+           if (!folderWithPossibleSerializationFile.exists()) {
+               logger.trace("No folder found at {0}, moving up to the next level", serializationFilePath);
+               continue;
+           }
+
+           // it's safe to use a specific SerializationKind since this scenario is only valid for METADATA_PARTIAL
+           // coverage
+           String possibleSerializationFilePath = serializationManager.getSerializationFilePath(
+                   folderWithPossibleSerializationFile.getOSPath().toString(),
+                   SerializationKind.METADATA_PARTIAL);
+
+           logger.trace("Looking for serialization data in {0}", possibleSerializationFilePath);
+           if (serializationManager.isSerializationFile(possibleSerializationFilePath)) {
+               
+               Path parentFileSerializationOSPath = localFile.getProject().getSyncDirectory().getOSPath().
+                       relativize(Paths.get(possibleSerializationFilePath));
+
+               WorkspacePath parentSerializationFilePath = new WorkspacePath(parentFileSerializationOSPath.toString())
+                       .absolute();
+               
+               WorkspaceFile possibleSerializationFile = localFile.getProject().getSyncDirectory().getFile(parentSerializationFilePath);
+               if (!possibleSerializationFile.exists()) {
+                   logger.trace("Potential serialization data file {0} does not exist, moving up to the next level",
+                           possibleSerializationFile.getLocalPath());
+                   continue;
+               }
+               
+               ResourceProxy serializationData;
+               try (InputStream contents = possibleSerializationFile.getContents()) {
+                   serializationData = serializationManager.readSerializationData(
+                           parentSerializationFilePath.asPortableString(), contents);
+               }
+
+               String repositoryPath = serializationManager.getRepositoryPath(resourceLocation);
+               String potentialPath = serializationData.getPath();
+               boolean covered = serializationData.covers(repositoryPath);
+
+               logger.trace(
+                       "Found possible serialization data at {0}. Resource :{1} ; our resource: {2}. Covered: {3}",
+                       parentSerializationFilePath, potentialPath, repositoryPath, covered);
+               // note what we don't need to normalize the children here since this resource's data is covered by
+               // another resource
+               if (covered) {
+                   return serializationData.getChild(repositoryPath);
+               }
+
+               break;
+           }
+       }
+
+       return null;
+   }
+
+   private FileInfo createFileInfo(WorkspaceResource resource) {
+
+       if (!(resource instanceof WorkspaceFile)) {
+           return null;
+       }
+
+       FileInfo info = new FileInfo(resource.getOSPath().toString(), resource.getPathRelativeToSyncDir().asPortableString(), resource.getName());
+
+       logger.trace("For {0} built fileInfo {1}", resource, info);
+
+       return info;
+   }
+   
+   public ResourceAndInfo buildResourceAndInfo(WorkspaceResource resource, Repository repository) throws IOException {
+
+       Long modificationTimestamp = (Long) resource.getTransientProperty(PN_IMPORT_MODIFICATION_TIMESTAMP);
+
+       if (modificationTimestamp != null && modificationTimestamp >= resource.getLastModified()) {
+           logger.trace("Change for resource {0} ignored as the import timestamp {1} >= modification timestamp {2}",
+                           resource, modificationTimestamp, resource.getLastModified());
+           return null;
+       }
+
+       if (resource.isIgnored()) {
+           logger.trace("Skipping team-private resource {0}", resource);
+           return null;
+       }
+
+       FileInfo info = createFileInfo(resource);
+       logger.trace("For {0} built fileInfo {1}", resource, info);
+
+       WorkspaceDirectory syncDirectory = resource.getProject().getSyncDirectory();
+       File syncDirectoryAsFile = syncDirectory.getOSPath().toFile();
+
+       ResourceProxy resourceProxy = null;
+
+       if (serializationManager.isSerializationFile(resource.getOSPath().toString())) {
+           WorkspaceFile file = (WorkspaceFile) resource;
+           try (InputStream contents = file.getContents()) {
+               String resourceLocation = file.getPathRelativeToSyncDir().asPortableString();
+               resourceProxy = serializationManager.readSerializationData(resourceLocation, contents);
+               normaliseResourceChildren(file, resourceProxy, repository);
+
+
+               // TODO - not sure if this 100% correct, but we definitely should not refer to the FileInfo as the
+               // .serialization file, since for nt:file/nt:resource nodes this will overwrite the file contents
+               String primaryType = (String) resourceProxy.getProperties().get(Repository.JCR_PRIMARY_TYPE);
+               if (Repository.NT_FILE.equals(primaryType)) {
+                   // TODO move logic to serializationManager
+                   File locationFile = new File(info.getLocation());
+                   String locationFileParent = locationFile.getParent();
+                   int endIndex = locationFileParent.length() - ".dir".length();
+                   File actualFile = new File(locationFileParent.substring(0, endIndex));
+                   String newLocation = actualFile.getAbsolutePath();
+                   String newName = actualFile.getName();
+                   String newRelativeLocation = actualFile.getAbsolutePath().substring(
+                           syncDirectoryAsFile.getAbsolutePath().length());
+                   info = new FileInfo(newLocation, newRelativeLocation, newName);
+
+                   logger.trace("Adjusted original location from {0} to {1}", resourceLocation, newLocation);
+
+               }
+           }
+       } else {
+
+           // TODO - move logic to serializationManager
+           // possible .dir serialization holder
+           if (resource instanceof WorkspaceDirectory && resource.getName().endsWith(".dir")) {
+               WorkspaceDirectory folder = (WorkspaceDirectory) resource;
+               WorkspaceResource contentXml = folder.getFile(new WorkspacePath(".content.xml"));
+               // .dir serialization holder ; nothing to process here, the .content.xml will trigger the actual work
+               if (contentXml.exists()
+                       && serializationManager.isSerializationFile(contentXml.getOSPath().toString())) {
+                   return null;
+               }
+           }
+
+           resourceProxy = buildResourceProxyForPlainFileOrFolder(resource, repository);
+       }
+       
+       if ( resourceProxy == null )
+           throw new RuntimeException("ResourceProxy is null for resource " + resource);
+
+       FilterResult filterResult = resource.getProject().getFilter().filter(resourceProxy.getPath());
+
+       switch (filterResult) {
+
+           case ALLOW:
+               return new ResourceAndInfo(resourceProxy, info);
+           case PREREQUISITE:
+               // never try to 'create' the root node, we assume it exists
+               if (!resourceProxy.getPath().equals("/")) {
+                   // we don't explicitly set the primary type, which will allow the the repository to choose the best
+                   // suited one ( typically nt:unstructured )
+                   return new ResourceAndInfo(new ResourceProxy(resourceProxy.getPath()), null, true);
+               }
+           case DENY: // falls through
+           default:
+               return null;
+       }
+   }
+   
+   private ResourceProxy buildResourceProxyForPlainFileOrFolder(WorkspaceResource changedResource, Repository repository)
+           throws IOException {
+
+       SerializationKind serializationKind;
+       String fallbackNodeType;
+       if (changedResource instanceof WorkspaceFile) {
+           serializationKind = SerializationKind.FILE;
+           fallbackNodeType = Repository.NT_FILE;
+       } else { // i.e. LocalFolder
+           serializationKind = SerializationKind.FOLDER;
+           fallbackNodeType = Repository.NT_FOLDER;
+       }
+
+       String resourceLocation = changedResource.getPathRelativeToSyncDir().asPortableString();
+       String serializationFilePath = serializationManager.getSerializationFilePath(
+               resourceLocation, serializationKind);
+       WorkspaceFile serializationResource = changedResource.getProject().getSyncDirectory().getFile(new WorkspacePath(serializationFilePath));
+
+       if (!serializationResource.exists() && changedResource instanceof WorkspaceDirectory) {
+           ResourceProxy dataFromCoveringParent = findSerializationDataFromCoveringParent(changedResource,
+                   resourceLocation, serializationResource.getLocalPath());
+
+           if (dataFromCoveringParent != null) {
+               return dataFromCoveringParent;
+           }
+           
+       }
+
+       return buildResourceProxy(resourceLocation, serializationResource, fallbackNodeType, repository);
+   }
+   
+   private ResourceProxy buildResourceProxy(String resourceLocation, WorkspaceFile serializationFile,
+           String fallbackPrimaryType, Repository repository) throws IOException {
+       if (serializationFile.exists()) {
+           try (InputStream contents = serializationFile.getContents() ) {
+               
+               String serializationFilePath = serializationFile.getPathRelativeToSyncDir().asPortableString();
+               ResourceProxy resourceProxy = serializationManager.readSerializationData(serializationFilePath, contents);
+               normaliseResourceChildren(serializationFile, resourceProxy, repository);
+
+               return resourceProxy;
+           }
+       }
+
+       return new ResourceProxy(serializationManager.getRepositoryPath(resourceLocation), Collections.singletonMap(
+               Repository.JCR_PRIMARY_TYPE, (Object) fallbackPrimaryType));
+   }
+   
+   /**
+    * Normalises the of the specified <tt>resourceProxy</tt> by comparing the serialization data and the filesystem
+    * data
+    * 
+    * @param serializationFile the file which contains the serialization data
+    * @param resourceProxy the resource proxy
+    * @param syncDirectory the sync directory
+    * @param repository TODO
+    * @throws CoreException
+    */
+   private void normaliseResourceChildren(WorkspaceFile serializationFile, ResourceProxy resourceProxy,
+           Repository repository) {
+       // TODO - this logic should be moved to the serializationManager
+       try {
+           SerializationKindManager skm = new SerializationKindManager();
+           skm.init(repository);
+
+           String primaryType = (String) resourceProxy.getProperties().get(Repository.JCR_PRIMARY_TYPE);
+           List<String> mixinTypesList = getMixinTypes(resourceProxy);
+           SerializationKind serializationKind = skm.getSerializationKind(primaryType, mixinTypesList);
+
+           if (serializationKind == SerializationKind.METADATA_FULL) {
+               return;
+           }
+       } catch (RepositoryException e) {
+           // TODO proper exception handling
+           throw new RuntimeException(e);
+       }
+
+       WorkspacePath serializationDirectoryPath = serializationFile.getLocalPath().getParent();
+
+       Iterator<ResourceProxy> childIterator = resourceProxy.getChildren().iterator();
+       Map<String, WorkspaceResource> extraChildResources = new HashMap<>();
+       for (WorkspaceResource member : serializationFile.getParent().getChildren()) {
+           if (member.equals(serializationFile)) {
+               continue;
+           }
+           extraChildResources.put(member.getName(), member);
+       }
+
+       while (childIterator.hasNext()) {
+           ResourceProxy child = childIterator.next();
+           String childName = PathUtil.getName(child.getPath());
+           String osChildName = serializationManager.getOsPath(childName);
+
+           // covered children might have a FS representation, depending on their child nodes, so
+           // accept a directory which maps to their name
+           extraChildResources.remove(osChildName);
+
+           // covered children do not need a filesystem representation
+           if (resourceProxy.covers(child.getPath())) {
+               continue;
+           }
+
+           WorkspacePath childPath = serializationDirectoryPath.append(osChildName);
+
+           WorkspaceResource childResource = serializationFile.getParent().getDirectory(new WorkspacePath(osChildName));
+           if (!childResource.exists()) {
+               logger.trace("For resource at with serialization data {0} the serialized child resource at {1} does not exist in the filesystem and will be ignored",
+                               serializationFile, childPath);
+               childIterator.remove();
+           }
+       }
+
+       for ( WorkspaceResource extraChildResource : extraChildResources.values()) {
+           WorkspacePath extraChildResourcePath = extraChildResource.getPathRelativeToSyncDir();
+           resourceProxy.addChild(new ResourceProxy(serializationManager
+                   .getRepositoryPath(extraChildResourcePath.asPortableString())));
+           
+           logger.trace("For resource at with serialization data {0} the found a child resource at {1} which is not listed in the serialized child resources and will be added",
+                           serializationFile, extraChildResource);
+       }
+   }
+   
+   private List<String> getMixinTypes(ResourceProxy resourceProxy) {
+
+       Object mixinTypesProp = resourceProxy.getProperties().get(Repository.JCR_MIXIN_TYPES);
+
+       if (mixinTypesProp == null) {
+           return Collections.emptyList();
+       }
+
+       if (mixinTypesProp instanceof String) {
+           return Collections.singletonList((String) mixinTypesProp);
+       }
+
+       return Arrays.asList((String[]) mixinTypesProp);
+   }
+   
+    @Override
+    public Command<Void> newReorderChildNodesCommand(Repository repository, WorkspaceResource resource) throws IOException {
+        ResourceAndInfo rai = buildResourceAndInfo(resource, repository);
+
+        if (rai == null || rai.isOnlyWhenMissing()) {
+            return null;
+        }
+
+        return repository.newReorderChildNodesCommand(rai.getResource());
+    }
+}
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/package-info.java b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/package-info.java
new file mode 100644
index 0000000..714e15f
--- /dev/null
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/sync/content/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.ide.sync.content;
\ No newline at end of file
diff --git a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceAndInfo.java b/shared/modules/api/src/main/java/org/apache/sling/ide/transport/ResourceAndInfo.java
similarity index 91%
rename from eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceAndInfo.java
rename to shared/modules/api/src/main/java/org/apache/sling/ide/transport/ResourceAndInfo.java
index 7a69614..56bba21 100644
--- a/eclipse/eclipse-core/src/org/apache/sling/ide/eclipse/core/internal/ResourceAndInfo.java
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/transport/ResourceAndInfo.java
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sling.ide.eclipse.core.internal;
-
-import org.apache.sling.ide.transport.FileInfo;
-import org.apache.sling.ide.transport.ResourceProxy;
+package org.apache.sling.ide.transport;
 
 /**
  * The <tt>ResourceAndInfo</tt> is a simple value class allowing both a ResourceProxy and a FileInfo to be passed
diff --git a/shared/modules/api/src/main/java/org/apache/sling/ide/util/PathUtil.java b/shared/modules/api/src/main/java/org/apache/sling/ide/util/PathUtil.java
index 8181d8f..6f146ab 100644
--- a/shared/modules/api/src/main/java/org/apache/sling/ide/util/PathUtil.java
+++ b/shared/modules/api/src/main/java/org/apache/sling/ide/util/PathUtil.java
@@ -34,6 +34,9 @@ public class PathUtil {
     }
 
     public static String getName(String path) {
+        
+        if ( path.length() == 1 && path.charAt(0) == '/')
+            return path;
 
         return path.substring(path.lastIndexOf('/') + 1);
     }
diff --git a/shared/modules/api/src/test/java/org/apache/sling/ide/sync/content/WorkspacePathTest.java b/shared/modules/api/src/test/java/org/apache/sling/ide/sync/content/WorkspacePathTest.java
new file mode 100644
index 0000000..35fcd86
--- /dev/null
+++ b/shared/modules/api/src/test/java/org/apache/sling/ide/sync/content/WorkspacePathTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+public class WorkspacePathTest {
+
+    @Test(expected = IllegalArgumentException.class)
+    public void nullPath() {
+        new WorkspacePath(null);
+    }
+    
+
+    public void emptyPath() {
+        assertThat(new WorkspacePath("").asPortableString(), equalTo(""));
+    }
+    
+    @Test
+    public void removeTrailingSlash() {
+        assertThat(new WorkspacePath("/first/").asPortableString(), equalTo("/first"));
+    }
+    
+    @Test
+    public void getName_root() {
+        assertThat(new WorkspacePath("/").getName(), equalTo("/"));
+    }
+    
+    @Test
+    public void getName_firstLevelChild() {
+        assertThat(new WorkspacePath("/first").getName(), equalTo("first"));
+    }
+    
+    @Test
+    public void getName_secondLevelChild() {
+        assertThat(new WorkspacePath("/first/second").getName(), equalTo("second"));
+    }
+    
+    @Test
+    public void absolute_root() {
+        assertThat(new WorkspacePath("/").absolute().asPortableString(), equalTo("/"));
+    }
+    
+    @Test
+    public void absolute_noop() {
+        assertThat(new WorkspacePath("/first").absolute().asPortableString(), equalTo("/first"));
+    }
+    
+    @Test
+    public void absolute_changed() {
+        assertThat(new WorkspacePath("first").absolute().asPortableString(), equalTo("/first"));
+    }
+    
+    @Test
+    public void makeRelative_parentAndChild() {
+        
+        WorkspacePath parent = new WorkspacePath("first");
+        WorkspacePath child = new WorkspacePath("first/second/third");
+        
+        assertThat(parent.relativize(child).asPortableString(), equalTo("second/third"));
+    }
+    
+    @Test
+    public void makeRelative_same() {
+        
+        WorkspacePath path = new WorkspacePath("first");
+        
+        assertThat(path.relativize(path).asPortableString(), equalTo(""));
+    }
+    
+    @Test
+    public void makeRelative_unrelated() {
+        
+        assertThat(new WorkspacePath("/first").relativize(new WorkspacePath("/second")), nullValue());
+    }
+    
+    @Test
+    public void isRoot_root() {
+        
+        assertThat(new WorkspacePath("/").isRoot(), equalTo(true));
+    }
+    
+    @Test
+    public void isRoot_notRoot() {
+        assertThat(new WorkspacePath("/first").isRoot(), equalTo(false));
+    }
+    
+    @Test
+    public void getParent_root() {
+        assertThat(new WorkspacePath("/").getParent(), nullValue());
+    }
+    
+    @Test
+    public void getParent_firstLevel() {
+        assertThat(new WorkspacePath("/first").getParent().asPortableString(), equalTo("/"));
+    }
+    
+    @Test
+    public void getParent_secondLevel() {
+        assertThat(new WorkspacePath("/first/second").getParent().asPortableString(), equalTo("/first"));
+    }
+    
+    @Test
+    public void equals_same() {
+        assertThat(new WorkspacePath("/first").equals(new WorkspacePath("/first")), equalTo(true));
+    }
+
+    @Test
+    public void equals_different() {
+        assertThat(new WorkspacePath("/first").equals(new WorkspacePath("/second")), equalTo(false));
+    }
+
+    @Test
+    public void equals_relativeVsAbsolute() {
+        assertThat(new WorkspacePath("/first").equals(new WorkspacePath("first")), equalTo(false));
+    }
+    
+    @Test(expected = IllegalArgumentException.class)
+    public void append_notRelative() {
+        new WorkspacePath("/first").append("/second");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void append_null() {
+        new WorkspacePath("/first").append((String) null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void append_empty() {
+        new WorkspacePath("/first").append("");
+    }
+    
+    @Test
+    public void append_ok() {
+        assertThat(new WorkspacePath("/first").append("second").asPortableString(), equalTo("/first/second"));
+    }
+    
+    @Test
+    public void appendPath_absolute() {
+        assertThat(new WorkspacePath("/first").append(new WorkspacePath("/second")), equalTo(new WorkspacePath("/first/second")));
+    }
+    
+    @Test(expected = IllegalArgumentException.class)
+    public void appendPath_null() {
+        new WorkspacePath("/first").append((WorkspacePath) null);
+    }
+    
+    @Test
+    public void appendPath_relative() {
+        assertThat(new WorkspacePath("/first").append(new WorkspacePath("second/third")), equalTo(new WorkspacePath("/first/second/third")));
+    }
+
+    @Test
+    public void appendPath_bothRelative() {
+        assertThat(new WorkspacePath("first").append(new WorkspacePath("second/third")), equalTo(new WorkspacePath("first/second/third")));
+    }
+}
diff --git a/shared/modules/api/src/test/java/org/apache/sling/ide/sync/content/WorkspacePathsTest.java b/shared/modules/api/src/test/java/org/apache/sling/ide/sync/content/WorkspacePathsTest.java
new file mode 100644
index 0000000..9310e66
--- /dev/null
+++ b/shared/modules/api/src/test/java/org/apache/sling/ide/sync/content/WorkspacePathsTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.ide.sync.content;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+import java.nio.file.Paths;
+
+import org.junit.Test;
+
+public class WorkspacePathsTest {
+
+    @Test
+    public void fromLocalPath() {
+        
+        assertThat(WorkspacePaths.fromOsPath(Paths.get("some", "path")), equalTo(new WorkspacePath("some/path")));
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
rombert@apache.org.