Fixed NPE's in resourceless source modules

Change-Id: I3f2b8e65d0596528763e8ab6a61923db2846b589
Signed-off-by: Vasili Gulevich <vasili.gulevich@xored.com>
diff --git a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/AbstractSourceModule.java b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/AbstractSourceModule.java
index c7f997e..178e0f4 100644
--- a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/AbstractSourceModule.java
+++ b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/AbstractSourceModule.java
@@ -16,7 +16,6 @@
 import java.util.List;
 import java.util.Map;
 
-import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -548,17 +547,9 @@
 			printNode(printer);
 			printer.flush();
 		}
-		// update timestamp (might be IResource.NULL_STAMP if original does
-		// not exist)
-		if (underlyingResource == null) {
-			underlyingResource = getResource();
-		}
-		// underlying resource is null in the case of a working copy out of
-		// workspace
-		if (underlyingResource != null) {
-			moduleInfo.timestamp = ((IFile) underlyingResource)
-					.getModificationStamp();
-		}
+		moduleInfo.timestamp = underlyingResource != null
+				? underlyingResource.getLocalTimeStamp()
+				: getOriginTimestamp();
 		// We need to update children contents using model providers
 		// Call for extra model providers
 		final IDLTKLanguageToolkit toolkit = DLTKLanguageManager
@@ -624,7 +615,7 @@
 		// create buffer
 		final BufferManager bufManager = getBufferManager();
 		final boolean isWorkingCopy = isWorkingCopy();
-		IBuffer buffer = isWorkingCopy ? this.owner.createBuffer(this)
+		IBuffer buffer = isWorkingCopy ? createBuffer()
 				: BufferManager.createBuffer(this);
 		if (buffer == null) {
 			return null;
@@ -689,6 +680,10 @@
 		return buffer;
 	}
 
+	protected IBuffer createBuffer() {
+		return this.owner.createBuffer(this);
+	}
+
 	@Override
 	protected void openParent(Object childInfo, HashMap newElements,
 			IProgressMonitor pm) throws ModelException {
@@ -789,4 +784,8 @@
 	public ISourceRange getNameRange() throws ModelException {
 		return null;
 	}
+
+	protected long getOriginTimestamp() {
+		return IResource.NULL_STAMP;
+	}
 }
diff --git a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/BecomeWorkingCopyOperation.java b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/BecomeWorkingCopyOperation.java
index 9e6f5d5..b438430 100644
--- a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/BecomeWorkingCopyOperation.java
+++ b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/BecomeWorkingCopyOperation.java
@@ -5,10 +5,11 @@
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
- 
+
  *******************************************************************************/
 package org.eclipse.dltk.internal.core;
 
+import org.eclipse.core.resources.IResource;
 import org.eclipse.dltk.core.IModelElement;
 import org.eclipse.dltk.core.IModelElementDelta;
 import org.eclipse.dltk.core.IProblemRequestor;
@@ -51,16 +52,19 @@
 			delta.added(workingCopy);
 			addDelta(delta);
 		} else {
-			if (workingCopy.getResource().isAccessible()) {
+			IResource resource = workingCopy.getResource();
+			if (resource != null && resource.isAccessible()) {
 				// report a F_PRIMARY_WORKING_COPY change delta for a primary
 				// working copy
-				ModelElementDelta delta = new ModelElementDelta(this.getModel());
+				ModelElementDelta delta = new ModelElementDelta(
+						this.getModel());
 				delta.changed(workingCopy,
 						IModelElementDelta.F_PRIMARY_WORKING_COPY);
 				addDelta(delta);
 			} else {
 				// report an ADDED delta
-				ModelElementDelta delta = new ModelElementDelta(this.getModel());
+				ModelElementDelta delta = new ModelElementDelta(
+						this.getModel());
 				delta.added(workingCopy,
 						IModelElementDelta.F_PRIMARY_WORKING_COPY);
 				addDelta(delta);
diff --git a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DiscardWorkingCopyOperation.java b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DiscardWorkingCopyOperation.java
index 6b93781..7dbfcda 100644
--- a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DiscardWorkingCopyOperation.java
+++ b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/DiscardWorkingCopyOperation.java
@@ -8,9 +8,10 @@
  *******************************************************************************/
 package org.eclipse.dltk.internal.core;
 
-import org.eclipse.dltk.core.IScriptProject;
+import org.eclipse.core.resources.IResource;
 import org.eclipse.dltk.core.IModelElement;
 import org.eclipse.dltk.core.IModelElementDelta;
+import org.eclipse.dltk.core.IScriptProject;
 import org.eclipse.dltk.core.ModelException;
 
 /**
@@ -37,12 +38,14 @@
 			}
 			if (!workingCopy.isPrimary()) {
 				// report removedscriptdelta for a non-primary working copy
-				ModelElementDelta delta = new ModelElementDelta(this.getModel());
+				ModelElementDelta delta = new ModelElementDelta(
+						this.getModel());
 				delta.removed(workingCopy);
 				addDelta(delta);
 				removeReconcileDelta(workingCopy);
 			} else {
-				if (workingCopy.getResource().isAccessible()) {
+				IResource resource = workingCopy.getResource();
+				if (resource != null && resource.isAccessible()) {
 					// report a F_PRIMARY_WORKING_COPY change delta for a
 					// primary working copy
 					ModelElementDelta delta = new ModelElementDelta(this
diff --git a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/SourceModule.java b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/SourceModule.java
index b375a25..44e748c 100644
--- a/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/SourceModule.java
+++ b/core/plugins/org.eclipse.dltk.core/model/org/eclipse/dltk/internal/core/SourceModule.java
@@ -203,8 +203,18 @@
 			return false;
 		}
 
-		return ((SourceModuleElementInfo) info).timestamp != getResource()
-				.getModificationStamp();
+		return ((SourceModuleElementInfo) info).timestamp != getOriginTimestamp();
+	}
+
+	/**
+	 * Last modification stamp of underlying resource.
+	 *
+	 * @return modification stamp of underlying resource, IResource.NULL_STAMP
+	 *         if deleted
+	 */
+	@Override
+	protected long getOriginTimestamp() {
+		return getResource().getModificationStamp();
 	}
 
 	@Override
@@ -392,8 +402,7 @@
 	protected void updateTimeStamp(SourceModule original)
 			throws ModelException {
 		// XXX: should be an interface method
-		long timeStamp = ((IFile) original.getResource())
-				.getModificationStamp();
+		long timeStamp = original.getOriginTimestamp();
 		if (timeStamp == IResource.NULL_STAMP) {
 			throw new ModelException(
 					new ModelStatus(IModelStatusConstants.INVALID_RESOURCE));
diff --git a/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/ProblemsLabelDecorator.java b/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/ProblemsLabelDecorator.java
index 906c23f..fccde2c 100644
--- a/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/ProblemsLabelDecorator.java
+++ b/core/plugins/org.eclipse.dltk.ui/src/org/eclipse/dltk/ui/ProblemsLabelDecorator.java
@@ -275,11 +275,13 @@
 	private IAnnotationModel isInScriptAnnotationModel(ISourceModule original) {
 		if (original.isWorkingCopy()) {
 			if (original instanceof SourceModule) {
-				FileEditorInput editorInput = new FileEditorInput(
-						(IFile) original.getResource());
-				return DLTKUIPlugin.getDefault()
-						.getSourceModuleDocumentProvider()
-						.getAnnotationModel(editorInput);
+				IFile file = (IFile) original.getResource();
+				if (file != null) {
+					FileEditorInput editorInput = new FileEditorInput(file);
+					return DLTKUIPlugin.getDefault()
+							.getSourceModuleDocumentProvider()
+							.getAnnotationModel(editorInput);
+				}
 			} else if (original instanceof IExternalSourceModule) {
 				ExternalStorageEditorInput editorInput = new ExternalStorageEditorInput(
 						(IExternalSourceModule) original);
diff --git a/core/tests/org.eclipse.dltk.core.tests/src/org/eclipse/dltk/core/tests/model/WorkingCopyTests.java b/core/tests/org.eclipse.dltk.core.tests/src/org/eclipse/dltk/core/tests/model/WorkingCopyTests.java
index 1f7d61c..55d259f 100644
--- a/core/tests/org.eclipse.dltk.core.tests/src/org/eclipse/dltk/core/tests/model/WorkingCopyTests.java
+++ b/core/tests/org.eclipse.dltk.core.tests/src/org/eclipse/dltk/core/tests/model/WorkingCopyTests.java
@@ -8,25 +8,40 @@
  *******************************************************************************/
 package org.eclipse.dltk.core.tests.model;
 
-import junit.framework.Test;
+import java.util.HashMap;
 
 import org.eclipse.core.resources.IProject;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.dltk.compiler.problem.IProblem;
 import org.eclipse.dltk.core.IBuffer;
 import org.eclipse.dltk.core.IModelElement;
+import org.eclipse.dltk.core.IProblemRequestor;
+import org.eclipse.dltk.core.IScriptProject;
 import org.eclipse.dltk.core.ISourceModule;
 import org.eclipse.dltk.core.ModelException;
 import org.eclipse.dltk.core.WorkingCopyOwner;
+import org.eclipse.dltk.internal.core.BufferManager;
+import org.eclipse.dltk.internal.core.DefaultWorkingCopyOwner;
+import org.eclipse.dltk.internal.core.ModelElement;
+import org.eclipse.dltk.internal.core.SourceModule;
 import org.eclipse.team.core.RepositoryProvider;
+import org.junit.Assert;
 
+import junit.framework.Test;
 
 public class WorkingCopyTests extends ModifyingResourceTests {
-	private static final String[] TEST_NATURE = new String[] { "org.eclipse.dltk.core.tests.testnature" };
-	ISourceModule cu = null;
-	ISourceModule copy = null;
+	private static final String[] TEST_NATURE = new String[] {
+			"org.eclipse.dltk.core.tests.testnature" };
+	private ISourceModule cu = null;
+	private ISourceModule copy = null;
+	private IScriptProject scriptProject = null;
+
 	public class TestWorkingCopyOwner extends WorkingCopyOwner {
 		@Override
 		public IBuffer createBuffer(ISourceModule workingCopy) {
@@ -46,16 +61,20 @@
 	protected void setUp() throws Exception {
 		super.setUp();
 		try {
-			
-			this.createScriptProject("P", TEST_NATURE,new String[] {
-				"src"
-			} );
+
+			this.scriptProject = this.createScriptProject("P", TEST_NATURE,
+					new String[] { "src" });
 			this.createFolder("P/src/x/y");
-			this.createFile("P/src/x/y/A.txt", "package x.y;\n" + "import java.io.File;\n" + "public class A {\n"
-					+ "  public class Inner {\n" + "    public class InnerInner {\n" + "    }\n" + "    int innerField;\n"
-					+ "    void innerMethod() {\n" + "    }\n" + "  }\n" + "  static String FIELD;\n" + "  {\n"
-					+ "    FIELD = File.pathSeparator;\n" + "  }\n" + "  int field1;\n" + "  boolean field2;\n" + "  public void foo() {\n"
-					+ "  }\n" + "}");
+			this.createFile("P/src/x/y/A.txt",
+					"package x.y;\n" + "import java.io.File;\n"
+							+ "public class A {\n" + "  public class Inner {\n"
+							+ "    public class InnerInner {\n" + "    }\n"
+							+ "    int innerField;\n"
+							+ "    void innerMethod() {\n" + "    }\n" + "  }\n"
+							+ "  static String FIELD;\n" + "  {\n"
+							+ "    FIELD = File.pathSeparator;\n" + "  }\n"
+							+ "  int field1;\n" + "  boolean field2;\n"
+							+ "  public void foo() {\n" + "  }\n" + "}");
 			this.cu = this.getSourceModule("P/src/x/y/A.txt");
 			this.copy = cu.getWorkingCopy(null);
 		} catch (CoreException e) {
@@ -78,7 +97,8 @@
 	 * delta after method copy-rename)
 	 */
 	public void testCancelMakeConsistent() throws ModelException {
-		String newContents = "package x.y;\n" + "public class A {\n" + "  public void bar() {\n" + "  }\n" + "}";
+		String newContents = "package x.y;\n" + "public class A {\n"
+				+ "  public void bar() {\n" + "  }\n" + "}";
 		this.copy.getBuffer().setContents(newContents);
 		NullProgressMonitor monitor = new NullProgressMonitor();
 		monitor.setCanceled(true);
@@ -93,12 +113,15 @@
 	/**
 	 */
 	public void testChangeContent() throws CoreException {
-		String newContents = "package x.y;\n" + "public class A {\n" + "  public void bar() {\n" + "  }\n" + "}";
+		String newContents = "package x.y;\n" + "public class A {\n"
+				+ "  public void bar() {\n" + "  }\n" + "}";
 		this.copy.getBuffer().setContents(newContents);
 		this.copy.reconcile(false, null, null);
-		assertSourceEquals("Unexpected working copy contents", newContents, this.copy.getBuffer().getContents());
+		assertSourceEquals("Unexpected working copy contents", newContents,
+				this.copy.getBuffer().getContents());
 		this.copy.commitWorkingCopy(true, null);
-		assertSourceEquals("Unexpected original cu contents", newContents, this.cu.getBuffer().getContents());
+		assertSourceEquals("Unexpected original cu contents", newContents,
+				this.cu.getBuffer().getContents());
 	}
 
 	/*
@@ -118,8 +141,10 @@
 		} finally {
 			setReadOnly(resource, readOnlyFlag);
 		}
-		assertTrue("Should have complained about modifying a read-only unit:", didComplain);
-		assertTrue("ReadOnly buffer got modified:", !this.cu.getBuffer().getContents().equals("invalid"));
+		assertTrue("Should have complained about modifying a read-only unit:",
+				didComplain);
+		assertTrue("ReadOnly buffer got modified:",
+				!this.cu.getBuffer().getContents().equals("invalid"));
 	}
 
 	/*
@@ -127,7 +152,8 @@
 	 * cu if a pessimistic repository provider allows it.
 	 */
 	public void testChangeContentOfReadOnlyCU2() throws CoreException {
-		String newContents = "package x.y;\n" + "public class A {\n" + "  public void bar() {\n" + "  }\n" + "}";
+		String newContents = "package x.y;\n" + "public class A {\n"
+				+ "  public void bar() {\n" + "  }\n" + "}";
 		IResource resource = this.cu.getUnderlyingResource();
 		IProject project = resource.getProject();
 		boolean readOnlyFlag = isReadOnly(resource);
@@ -137,19 +163,114 @@
 			setReadOnly(resource, true);
 			this.copy.getBuffer().setContents(newContents);
 			this.copy.commitWorkingCopy(true, null);
-			assertSourceEquals("Unexpected original cu contents", newContents, this.cu.getBuffer().getContents());
+			assertSourceEquals("Unexpected original cu contents", newContents,
+					this.cu.getBuffer().getContents());
 		} finally {
 			TestPessimisticProvider.markWritableOnSave = false;
 			RepositoryProvider.unmap(project);
 			setReadOnly(resource, readOnlyFlag);
 		}
 	}
+
 	/**
 	 * Ensures that the primary cu can be retrieved.
 	 */
 	public void testGetPrimaryCU() {
-		IModelElement primary= this.copy.getPrimaryElement();
-		assertTrue("Element is not a cu", primary instanceof ISourceModule && !((ISourceModule)primary).isWorkingCopy());
+		IModelElement primary = this.copy.getPrimaryElement();
+		assertTrue("Element is not a cu", primary instanceof ISourceModule
+				&& !((ISourceModule) primary).isWorkingCopy());
 		assertTrue("Element should exist", primary.exists());
-	}	
+	}
+
+	private static final class NonResourceSourceModule extends SourceModule {
+		private long originStamp = IResource.NULL_STAMP;
+
+		public NonResourceSourceModule(ModelElement parent,
+				WorkingCopyOwner owner, String name) {
+			super(parent, name, owner);
+		}
+
+		@Override
+		public boolean isReadOnly() {
+			return false;
+		}
+
+		@Override
+		protected IBuffer createBuffer() {
+			return BufferManager.createBuffer(this);
+		}
+
+		@Override
+		protected char[] getBufferContent() throws ModelException {
+			return new char[0];
+		}
+
+		@Override
+		protected void openParent(Object childInfo, HashMap newElements,
+				IProgressMonitor pm) throws ModelException {
+		}
+
+		@Override
+		protected IStatus validateSourceModule(IResource resource) {
+			return Status.OK_STATUS;
+		}
+
+		@Override
+		public IResource getResource() {
+			return null;
+		}
+
+		@Override
+		protected String getNatureId() {
+			return "test_nature";
+		}
+
+		@Override
+		protected long getOriginTimestamp() {
+			return originStamp;
+		}
+	}
+
+	private static final IProblemRequestor DUMMY_REQUESTOR = new IProblemRequestor() {
+
+		@Override
+		public boolean isActive() {
+			return true;
+		}
+
+		@Override
+		public void endReporting() {
+		}
+
+		@Override
+		public void beginReporting() {
+		}
+
+		@Override
+		public void acceptProblem(IProblem problem) {
+		}
+	};
+
+	public void testResourceLessSourceWorkingCopy() throws ModelException {
+		NonResourceSourceModule subject = new NonResourceSourceModule(
+				(ModelElement) scriptProject, DefaultWorkingCopyOwner.PRIMARY,
+				"test.py");
+		Assert.assertFalse(subject.hasResourceChanged());
+		subject.becomeWorkingCopy(DUMMY_REQUESTOR, new NullProgressMonitor());
+		Assert.assertFalse(subject.hasResourceChanged());
+		subject.discardWorkingCopy();
+	}
+
+	public void testResourceLessTimestamps() throws ModelException {
+		NonResourceSourceModule subject = new NonResourceSourceModule(
+				(ModelElement) scriptProject, DefaultWorkingCopyOwner.PRIMARY,
+				"test.py");
+		subject.originStamp = 2;
+		Assert.assertFalse(subject.hasResourceChanged());
+		subject.becomeWorkingCopy(DUMMY_REQUESTOR, new NullProgressMonitor());
+		Assert.assertFalse(subject.hasResourceChanged());
+		subject.originStamp = 3;
+		Assert.assertTrue(subject.hasResourceChanged());
+		subject.discardWorkingCopy();
+	}
 }