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/17 07:39:35 UTC

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

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

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

commit 551caad37a43bcda40e38d41aa2a3e7adb80d850
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.