[544075] Add setBasename() and setExtension() methods to CDOResourceNode

https://bugs.eclipse.org/bugs/show_bug.cgi?id=544075
diff --git a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/ResourceTest.java b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/ResourceTest.java
index 6e3e363..ffe071c 100644
--- a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/ResourceTest.java
+++ b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/ResourceTest.java
@@ -2218,6 +2218,88 @@
     }
   }
 
+  public void testNameChecks()
+  {
+    CDOSession session = openSession();
+    CDOTransaction transaction = session.openTransaction();
+    CDOResource resource1 = transaction.createResource(getResourcePath("/my/resource1"));
+
+    assertEquals("resource1", resource1.getName());
+    assertEquals("resource1", resource1.getBasename());
+    assertEquals("", resource1.getExtension());
+
+    try
+    {
+      resource1.setName("/resource1");
+      fail("IllegalArgumentException expected");
+    }
+    catch (IllegalArgumentException expected)
+    {
+      // SUCCESS
+    }
+
+    resource1.setExtension("tar.gz");
+    assertEquals("resource1.tar.gz", resource1.getName());
+    assertEquals("resource1", resource1.getBasename());
+    assertEquals("tar.gz", resource1.getExtension());
+
+    resource1.setBasename("model");
+    assertEquals("model.tar.gz", resource1.getName());
+    assertEquals("model", resource1.getBasename());
+    assertEquals("tar.gz", resource1.getExtension());
+
+    try
+    {
+      resource1.setBasename("model.xyz");
+      fail("IllegalArgumentException expected");
+    }
+    catch (IllegalArgumentException expected)
+    {
+      // SUCCESS
+    }
+
+    resource1.setExtension("");
+    assertEquals("model", resource1.getName());
+    assertEquals("model", resource1.getBasename());
+    assertEquals("", resource1.getExtension());
+
+    resource1.setExtension("ecore");
+    resource1.setBasename("");
+    assertEquals(".ecore", resource1.getName());
+    assertEquals("", resource1.getBasename());
+    assertEquals("ecore", resource1.getExtension());
+
+    try
+    {
+      resource1.setName(null);
+      fail("IllegalArgumentException expected");
+    }
+    catch (IllegalArgumentException expected)
+    {
+      // SUCCESS
+    }
+
+    try
+    {
+      resource1.setName("");
+      fail("IllegalArgumentException expected");
+    }
+    catch (IllegalArgumentException expected)
+    {
+      // SUCCESS
+    }
+
+    try
+    {
+      resource1.setName(".");
+      fail("IllegalArgumentException expected");
+    }
+    catch (IllegalArgumentException expected)
+    {
+      // SUCCESS
+    }
+  }
+
   /**
    * @author Eike Stepper
    */
diff --git a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/bugzilla/Bugzilla_334995_Test.java b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/bugzilla/Bugzilla_334995_Test.java
index 79ea70f..873eddf 100644
--- a/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/bugzilla/Bugzilla_334995_Test.java
+++ b/plugins/org.eclipse.emf.cdo.tests/src/org/eclipse/emf/cdo/tests/bugzilla/Bugzilla_334995_Test.java
@@ -18,6 +18,8 @@
 import org.eclipse.emf.cdo.transaction.CDOTransaction;
 import org.eclipse.emf.cdo.util.CommitException;
 
+import org.eclipse.emf.ecore.resource.Resource;
+
 import java.util.Map.Entry;
 
 /**
@@ -30,37 +32,55 @@
   /**
    * The following test seems obsolete because (as of bug xxxxxx) no local changes can create resource duplicates.
    */
-  public void _test() throws CommitException
+  public void testURIClash() throws CommitException
   {
-    CDOID resourceID = persistResources("/res1")[0];
+    CDOSession session = openSession();
+    CDOTransaction transaction = session.openTransaction();
+    CDOResource resourceA = transaction.createResource(getResourcePath("/resA"));
+    resourceA.getContents().add(getModel1Factory().createCustomer());
+    transaction.commit();
+    System.out.println("Persisted resource: " + resourceA);
 
+    CDOSession session2 = openSession();
+    CDOTransaction transaction2 = session2.openTransaction();
+    CDOResource resourceB2 = transaction2.createResource(getResourcePath("/resB"));
+    resourceB2.getContents().add(getModel1Factory().createSupplier());
+
+    resourceA.setName("resB");
+    commitAndSync(transaction, transaction2);
+
+    System.out.println("newObjects:");
+    for (Entry<CDOID, CDOObject> entry : transaction2.getNewObjects().entrySet())
     {
-      CDOSession session = openSession();
-      CDOTransaction transaction = session.openTransaction();
-
-      CDOResource resource = transaction.createResource(getResourcePath("/res1"));
-      msg("New resource: " + resource);
-      msg("newObjects:");
-
-      for (Entry<CDOID, CDOObject> entry : transaction.getNewObjects().entrySet())
-      {
-        msg(" " + entry + ", state: " + entry.getValue().cdoState());
-        assertNew(entry.getValue(), transaction);
-      }
-
-      // Fetch the persisted resource that has the same URI
-      CDOResource resource1 = (CDOResource)transaction.getObject(resourceID);
-      msg("Persisted resource: " + resource1);
-
-      msg("newObjects:");
-      for (Entry<CDOID, CDOObject> entry : transaction.getNewObjects().entrySet())
-      {
-        msg(" " + entry + ", state: " + entry.getValue().cdoState());
-        assertNew(entry.getValue(), transaction);
-      }
-
-      transaction.commit();
+      System.out.println(" " + entry + ", state: " + entry.getValue().cdoState());
+      assertNew(entry.getValue(), transaction2);
     }
+
+    System.out.println("resources:");
+    for (Resource resource : transaction2.getResourceSet().getResources())
+    {
+      System.out.println(" " + resource);
+    }
+
+    // Fetch the persisted resource that has the same URI
+    CDOResource resourceA2 = (CDOResource)transaction2.getObject(resourceA.cdoID());
+    System.out.println("Remote resource: " + resourceA2);
+    System.out.println("Local resource:  " + resourceB2);
+
+    System.out.println("newObjects:");
+    for (Entry<CDOID, CDOObject> entry : transaction2.getNewObjects().entrySet())
+    {
+      System.out.println(" " + entry + ", state: " + entry.getValue().cdoState());
+      assertNew(entry.getValue(), transaction2);
+    }
+
+    System.out.println("resources:");
+    for (Resource resource : transaction2.getResourceSet().getResources())
+    {
+      System.out.println(" " + resource);
+    }
+
+    transaction2.commit();
   }
 
   public void testRename() throws CommitException
diff --git a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/CDOResourceNode.java b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/CDOResourceNode.java
index bcb9068..b1ce8ba 100644
--- a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/CDOResourceNode.java
+++ b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/CDOResourceNode.java
@@ -144,12 +144,32 @@
 
   /**
    * @ADDED
+   * @since 4.7
+   */
+  public void setExtension(String extension);
+
+  /**
+   * Same as {@link #getBasename()}.
+   *
+   * @ADDED
    * @since 4.4
    */
   public String trimExtension();
 
   /**
    * @ADDED
+   * @since 4.7
+   */
+  public String getBasename();
+
+  /**
+   * @ADDED
+   * @since 4.7
+   */
+  public void setBasename(String basename);
+
+  /**
+   * @ADDED
    */
   public URI getURI();
 
diff --git a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/impl/CDOResourceNodeImpl.java b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/impl/CDOResourceNodeImpl.java
index 40eb6eb..acfaf96 100644
--- a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/impl/CDOResourceNodeImpl.java
+++ b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/cdo/eresource/impl/CDOResourceNodeImpl.java
@@ -24,7 +24,10 @@
 import org.eclipse.emf.internal.cdo.CDOObjectImpl;
 import org.eclipse.emf.internal.cdo.messages.Messages;
 
+import org.eclipse.net4j.util.CheckUtil;
 import org.eclipse.net4j.util.ObjectUtil;
+import org.eclipse.net4j.util.StringUtil;
+import org.eclipse.net4j.util.om.OMPlatform;
 
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EClass;
@@ -52,6 +55,12 @@
  */
 public abstract class CDOResourceNodeImpl extends CDOObjectImpl implements CDOResourceNode
 {
+  private static final boolean disableNameChecks = OMPlatform.INSTANCE.isProperty("org.eclipse.emf.cdo.CDOResourceNode.disableNameChecks");
+
+  private static final boolean singleExtensions = OMPlatform.INSTANCE.isProperty("org.eclipse.emf.cdo.CDOResourceNode.singleExtensions");
+
+  private static final ExtensionFinder EXTENSION_FINDER = singleExtensions ? new ExtensionFinder.Single() : new ExtensionFinder.Multi();
+
   /**
    * <!-- begin-user-doc --> <!-- end-user-doc -->
    * @generated
@@ -171,6 +180,14 @@
    */
   public void basicSetName(String newName, boolean checkDuplicates)
   {
+    if (!disableNameChecks)
+    {
+      CheckUtil.checkArg(newName, "Name is null");
+      CheckUtil.checkArg(newName.length() != 0, "Name is empty");
+      CheckUtil.checkArg(!".".equals(newName), "Name is a dot");
+      CheckUtil.checkArg(newName.indexOf(CDOURIUtil.SEGMENT_SEPARATOR_CHAR) == -1, "Name contains a path separator");
+    }
+
     String oldName = getName();
     if (!ObjectUtil.equals(oldName, newName))
     {
@@ -251,33 +268,141 @@
   public String getExtension()
   {
     String name = getName();
-
-    int lastDot = name.lastIndexOf('.');
-    if (lastDot != -1)
+    if (name != null)
     {
-      return name.substring(lastDot + 1);
+      int dot = EXTENSION_FINDER.findExtension(name);
+      if (dot != -1)
+      {
+        return name.substring(dot + 1);
+      }
     }
 
     return "";
   }
 
   /**
+   * @since 4.7
+   */
+  public void setExtension(String extension)
+  {
+    InternalCDOView view = cdoView();
+    if (view != null)
+    {
+      synchronized (view.getViewMonitor())
+      {
+        view.lockView();
+
+        try
+        {
+          setExtensionSynced(extension);
+        }
+        finally
+        {
+          view.unlockView();
+        }
+      }
+    }
+    else
+    {
+      setExtensionSynced(extension);
+    }
+  }
+
+  private void setExtensionSynced(String extension)
+  {
+    if (StringUtil.isEmpty(extension))
+    {
+      setName(getBasename());
+    }
+    else
+    {
+      if (singleExtensions)
+      {
+        CheckUtil.checkArg(extension.indexOf(ExtensionFinder.DOT) == -1, "Extension contains a dot");
+      }
+
+      setName(getBasename() + ExtensionFinder.DOT + extension);
+    }
+  }
+
+  /**
    * @since 4.4
    */
   public String trimExtension()
   {
-    String name = getName();
+    return getBasename();
+  }
 
-    int lastDot = name.lastIndexOf('.');
-    if (lastDot != -1)
+  /**
+   * @since 4.7
+   */
+  public String getBasename()
+  {
+    String name = getName();
+    if (name != null)
     {
-      return name.substring(0, lastDot);
+      int dot = EXTENSION_FINDER.findExtension(name);
+      if (dot != -1)
+      {
+        return name.substring(0, dot);
+      }
     }
 
     return name;
   }
 
   /**
+   * @since 4.7
+   */
+  public void setBasename(String basename)
+  {
+    InternalCDOView view = cdoView();
+    if (view != null)
+    {
+      synchronized (view.getViewMonitor())
+      {
+        view.lockView();
+
+        try
+        {
+          setBasenameSynced(basename);
+        }
+        finally
+        {
+          view.unlockView();
+        }
+      }
+    }
+    else
+    {
+      setBasenameSynced(basename);
+    }
+  }
+
+  private void setBasenameSynced(String basename)
+  {
+    if (basename == null)
+    {
+      basename = StringUtil.EMPTY;
+    }
+
+    if (!singleExtensions)
+    {
+      CheckUtil.checkArg(basename.indexOf(ExtensionFinder.DOT) == -1, "Basename contains a dot");
+    }
+
+    String extension = getExtension();
+    if (StringUtil.isEmpty(extension))
+    {
+      setName(basename);
+    }
+    else
+    {
+      setName(basename + ExtensionFinder.DOT + extension);
+    }
+  }
+
+  /**
    * @ADDED
    */
   public URI getURI()
@@ -377,4 +502,35 @@
     return string;
   }
 
+  /**
+   * @author Eike Stepper
+   */
+  private interface ExtensionFinder
+  {
+    public static final char DOT = '.';
+
+    public int findExtension(String name);
+
+    /**
+     * @author Eike Stepper
+     */
+    public static final class Single implements ExtensionFinder
+    {
+      public int findExtension(String name)
+      {
+        return name.lastIndexOf(DOT);
+      }
+    }
+
+    /**
+     * @author Eike Stepper
+     */
+    public static final class Multi implements ExtensionFinder
+    {
+      public int findExtension(String name)
+      {
+        return name.indexOf(DOT);
+      }
+    }
+  }
 } // CDOResourceNodeImpl
diff --git a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/transaction/CDOTransactionImpl.java b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/transaction/CDOTransactionImpl.java
index 9532476..df136ab 100644
--- a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/transaction/CDOTransactionImpl.java
+++ b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/transaction/CDOTransactionImpl.java
@@ -3725,6 +3725,24 @@
     }
   }
 
+  @Override
+  protected Map<String, CDOResourceNode> collectNewResourceNodes()
+  {
+    Map<String, CDOResourceNode> result = new HashMap<String, CDOResourceNode>();
+
+    for (CDOObject object : getNewObjects().values())
+    {
+      if (object instanceof CDOResourceNode)
+      {
+        CDOResourceNode node = (CDOResourceNode)object;
+        String path = node.getPath();
+        result.put(path, node);
+      }
+    }
+
+    return result;
+  }
+
   private Set<CDOObject> getObjects(Collection<? extends CDOIdentifiable> identifiables)
   {
     Set<CDOObject> result = new HashSet<CDOObject>();
diff --git a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/AbstractCDOView.java b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/AbstractCDOView.java
index 5869873..7aeb0e3 100644
--- a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/AbstractCDOView.java
+++ b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/AbstractCDOView.java
@@ -152,6 +152,8 @@
 
   private static final String REPOSITORY_NAME_KEY = "cdo.repository.name";
 
+  private static final String SAFE_RENAME = "~renamed";
+
   private static final ThreadLocal<Lock> NEXT_VIEW_LOCK = new ThreadLocal<Lock>();
 
   private final ViewAndState[] viewAndStates = ViewAndState.create(this);
@@ -2055,16 +2057,11 @@
     URI uri = CDOURIUtil.createResourceURI(this, path);
     ResourceSet resourceSet = getResourceSet();
 
-    // Bug 334995: Check if locally there is already a resource with the same URI
+    // Bug 334995: Check if locally there is already a resource with the same URI.
     CDOResource existingResource = (CDOResource)resourceSet.getResource(uri, false);
     if (existingResource != null && !isReadOnly())
     {
-      // We have no other option than to change the name of the local resource
-      String oldName = existingResource.getName();
-      existingResource.setName(oldName + ".renamed");
-
-      OM.LOG.warn("URI clash: resource being instantiated had same URI as a resource already present " + "locally; local resource was renamed from " + oldName
-          + " to " + existingResource.getName());
+      preventURIClash(existingResource);
     }
 
     return getResource(path, true);
@@ -2660,7 +2657,7 @@
   }
 
   /*
-   * Synchronized through InvalidationRunner.run()
+   * Synchronized through CDOViewImpl.ViewInvalidation.run().
    */
   protected Map<CDOObject, Pair<CDORevision, CDORevisionDelta>> invalidate( //
       List<CDORevisionKey> allChangedObjects, //
@@ -2699,6 +2696,7 @@
       }
     }
 
+    Map<String, CDOResourceNode> newResourceNodes = null;
     for (CDORevisionKey key : allChangedObjects)
     {
       CDORevisionDelta delta = null;
@@ -2729,6 +2727,25 @@
         {
           if (delta == null || isResourceNodeContainerOrNameChanged(delta))
           {
+            if (!isReadOnly())
+            {
+              if (newResourceNodes == null)
+              {
+                newResourceNodes = collectNewResourceNodes();
+              }
+
+              CDOResourceNode changedNode = (CDOResourceNode)changedObject;
+              String path = changedNode.getPath();
+
+              CDOResourceNode newResourceNode = newResourceNodes.get(path);
+              if (newResourceNode != null)
+              {
+                preventURIClash(newResourceNode);
+                String oldName = newResourceNode.getBasename();
+                newResourceNode.setBasename(oldName + SAFE_RENAME);
+              }
+            }
+
             ((CDOResourceNodeImpl)changedObject).recacheURIs();
           }
         }
@@ -2764,6 +2781,11 @@
     return false;
   }
 
+  protected Map<String, CDOResourceNode> collectNewResourceNodes()
+  {
+    return Collections.emptyMap();
+  }
+
   /**
    * Overridden by {@link CDOTransactionImpl#handleConflicts(long, Map, List)}.
    */
@@ -3102,6 +3124,18 @@
     return false;
   }
 
+  private static void preventURIClash(CDOResourceNode node)
+  {
+    String oldName = node.getName();
+
+    // We have no other option than to change the name of the local resource.
+    String oldBasename = node.getBasename();
+    node.setBasename(oldBasename + SAFE_RENAME);
+
+    OM.LOG.warn("URI clash: resource being instantiated had same URI as a resource already present locally; local resource was renamed from " //
+        + oldName + " to " + node.getName());
+  }
+
   /**
    * @author Eike Stepper
    */
diff --git a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/CDOViewImpl.java b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/CDOViewImpl.java
index 40cf5f7..1037eb2 100644
--- a/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/CDOViewImpl.java
+++ b/plugins/org.eclipse.emf.cdo/src/org/eclipse/emf/internal/cdo/view/CDOViewImpl.java
@@ -1120,6 +1120,7 @@
   public void prefetchRevisions(CDOID id, int depth)
   {
     checkArg(depth != CDORevision.DEPTH_NONE, "Prefetch depth must not be zero"); //$NON-NLS-1$
+
     synchronized (getViewMonitor())
     {
       lockView();
@@ -1159,7 +1160,7 @@
   /*
    * Must not by synchronized on the view!
    */
-  public /* synchronized */ void invalidate(ViewInvalidationData invalidationData)
+  public void invalidate(ViewInvalidationData invalidationData)
   {
     if (invalidationData.isAsync())
     {