Bug 458201 - Offer new command "Annotate" on ClassFileEditor
Bug 458200 - [null] "Annotate" proposals for adding external null
annotations to library classes

Change-Id: Ibf629c52530df7c06f3d3ccaaf2abb98149c03ac
Signed-off-by: Stephan Herrmann <stephan.herrmann@berlin.de>
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AbstractAnnotateAssistTests.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AbstractAnnotateAssistTests.java
new file mode 100644
index 0000000..7fa0570
--- /dev/null
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AbstractAnnotateAssistTests.java
@@ -0,0 +1,154 @@
+/*******************************************************************************
+ * Copyright (c) 2015 GK Software AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.ui.tests.quickfix;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.testplugin.JavaProjectHelper;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.Path;
+
+import org.eclipse.core.resources.IContainer;
+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.jface.text.TextSelection;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+
+import org.eclipse.jdt.core.IClasspathAttribute;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import org.eclipse.jdt.internal.core.ClasspathEntry;
+import org.eclipse.jdt.internal.corext.util.Strings;
+
+import org.eclipse.jdt.ui.tests.core.ProjectTestSetup;
+
+import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
+import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
+import org.eclipse.jdt.internal.ui.text.correction.ExternalNullAnnotationQuickAssistProcessor;
+import org.eclipse.jdt.internal.ui.text.correction.JavaCorrectionAssistant;
+
+import static org.eclipse.jdt.core.IClasspathAttribute.EXTERNAL_ANNOTATION_PATH;
+
+public abstract class AbstractAnnotateAssistTests extends QuickFixTest {
+
+	protected IJavaProject fJProject1;
+
+	public AbstractAnnotateAssistTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		JavaProjectHelper.clear(fJProject1, ProjectTestSetup.getDefaultClasspath());
+		super.tearDown();
+	}
+
+	protected void ensureExists(IContainer parent) throws CoreException {
+		if (parent.exists()) return;
+		if (parent.getParent() != null)
+			ensureExists(parent.getParent());
+		if (parent instanceof IFolder)
+			((IFolder)parent).create(true, true, null);
+	}
+
+	public List<ICompletionProposal> collectAnnotateProposals(JavaEditor javaEditor, int offset) {
+		JavaSourceViewer viewer= (JavaSourceViewer)javaEditor.getViewer();
+		viewer.setSelection(new TextSelection(offset, 0));
+
+		JavaCorrectionAssistant correctionAssist= new JavaCorrectionAssistant(javaEditor);
+		IQuickAssistProcessor assistProcessor= new ExternalNullAnnotationQuickAssistProcessor(correctionAssist);
+		ICompletionProposal[] proposals= assistProcessor.computeQuickAssistProposals(viewer.getQuickAssistInvocationContext());
+
+		List<ICompletionProposal> list= Arrays.asList(proposals);
+		return list;
+	}
+
+	// === from jdt.core.tests.model: ===
+
+	protected void addLibrary(IJavaProject javaProject, String jarName, String sourceZipName, String[] pathAndContents,
+								String annotationpath, String compliance) throws CoreException, IOException 
+	{	
+		IProject project= createLibrary(javaProject, jarName, sourceZipName, pathAndContents, compliance);
+	
+		String projectPath= '/' + project.getName() + '/';
+		IClasspathEntry entry= JavaCore.newLibraryEntry(
+				new Path(projectPath + jarName),
+				sourceZipName == null ? null : new Path(projectPath + sourceZipName),
+				null,
+				ClasspathEntry.getAccessRules(null, null),
+				new IClasspathAttribute[] { JavaCore.newClasspathAttribute(EXTERNAL_ANNOTATION_PATH, annotationpath)},
+				true);
+		IClasspathEntry[] entries= javaProject.getRawClasspath();
+		int length= entries.length;
+		System.arraycopy(entries, 0, entries= new IClasspathEntry[length + 1], 0, length);
+		entries[length]= entry;
+		javaProject.setRawClasspath(entries, null);
+	}
+
+	protected IProject createLibrary(IJavaProject javaProject, String jarName, String sourceZipName, String[] pathAndContents,
+								String compliance) throws IOException, CoreException
+	{
+		IProject project= javaProject.getProject();
+		String projectLocation= project.getLocation().toOSString();
+		String jarPath= projectLocation + File.separator + jarName;
+		String[] claspath= new String[] { javaProject.getResolvedClasspath(true)[0].getPath().toOSString() };
+		JarUtil.createJar(pathAndContents, null, jarPath, claspath, compliance, null);
+		if (pathAndContents != null && pathAndContents.length != 0) {
+			String sourceZipPath= projectLocation + File.separator + sourceZipName;
+			JarUtil.createSourceZip(pathAndContents, sourceZipPath);
+		}
+		project.refreshLocal(IResource.DEPTH_INFINITE, null);
+		return project;
+	}
+
+	// === from PropertiesFileQuickAssistTest: ===
+
+	protected static void checkContentOfFile(String message, IFile file, String content) throws Exception {
+		InputStream in= file.getContents();
+		try {
+			assertEqualLines(message, content, copyToString(in));
+		} finally {
+			in.close();
+		}
+	}
+
+	protected static String copyToString(InputStream in) throws Exception {
+		ByteArrayOutputStream out= new ByteArrayOutputStream();
+		int read= in.read();
+		while (read != -1) {
+			out.write(read);
+			read= in.read();
+		}
+		out.close();
+		return out.toString();
+	}
+
+	protected static void assertEqualLines(String message, String expected, String actual) {
+		String[] expectedLines= Strings.convertIntoLines(expected);
+		String[] actualLines= Strings.convertIntoLines(actual);
+	
+		String expected2= (expectedLines == null ? null : Strings.concatenate(expectedLines, "\n"));
+		String actual2= (actualLines == null ? null : Strings.concatenate(actualLines, "\n"));
+		assertEquals(message, expected2, actual2);
+	}
+}
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest15.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest15.java
new file mode 100644
index 0000000..3aa6f99
--- /dev/null
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest15.java
@@ -0,0 +1,399 @@
+/*******************************************************************************
+ * Copyright (c) 2015 GK Software AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.ui.tests.quickfix;
+
+import java.io.ByteArrayInputStream;
+import java.util.List;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import org.eclipse.jdt.testplugin.JavaProjectHelper;
+
+import org.eclipse.core.runtime.Path;
+
+import org.eclipse.core.resources.IFile;
+
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.TextSelection;
+import org.eclipse.jface.text.contentassist.ContentAssistEvent;
+import org.eclipse.jface.text.contentassist.ICompletionListener;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.source.SourceViewer;
+
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.tests.core.ProjectTestSetup;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
+import org.eclipse.jdt.internal.ui.javaeditor.JavaSourceViewer;
+
+public class AnnotateAssistTest15 extends AbstractAnnotateAssistTests {
+
+	protected static final String ANNOTATION_PATH= "annots";
+	
+	protected static final Class<?> THIS= AnnotateAssistTest15.class;
+	
+	public static Test suite() {
+		return setUpTest(new TestSuite(THIS));
+	}
+
+	public static Test setUpTest(Test test) {
+		return new ProjectTestSetup(test);
+	}
+	
+	public AnnotateAssistTest15(String name) {
+		super(name);
+	}
+
+	protected void setUp() throws Exception {
+		fJProject1= ProjectTestSetup.getProject();
+		fJProject1.getProject().getFolder(ANNOTATION_PATH).create(true, true, null);
+		fJProject1.setOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, JavaCore.ENABLED);
+	}
+	
+	// === Tests ===
+
+	/**
+	 * Assert that the "Annotate" command can be invoked on a ClassFileEditor
+	 * @throws Exception 
+	 */
+	public void testAnnotateReturn() throws Exception {
+		
+		String MY_MAP_PATH= "pack/age/MyMap";
+		String[] pathAndContents= new String[] { 
+					MY_MAP_PATH+".java", 
+					"package pack.age;\n" +
+					"public interface MyMap<K,V> {\n" +
+					"    public V get(K key);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_5);
+		IType type= fJProject1.findType(MY_MAP_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			SourceViewer viewer= (SourceViewer)javaEditor.getViewer();
+
+			// invoke the full command and asynchronously collect the result:
+			final ICompletionProposal[] proposalBox= new ICompletionProposal[1];
+			viewer.getQuickAssistAssistant().addCompletionListener(new ICompletionListener() {
+				public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) {
+					proposalBox[0]= proposal;
+				}				
+				public void assistSessionStarted(ContentAssistEvent event) { /* nop */ }
+				public void assistSessionEnded(ContentAssistEvent event) { /* nop */ }
+			});
+
+			int offset= pathAndContents[1].indexOf("V get");		
+			viewer.setSelection(new TextSelection(offset, 0));
+			viewer.doOperation(JavaSourceViewer.ANNOTATE_CLASS_FILE);
+
+			int count = 10;
+			while (proposalBox[0] == null && count-- > 0)
+				Thread.sleep(200);
+			ICompletionProposal proposal= proposalBox[0];
+			assertNotNull("should have a proposal", proposal);
+			
+			viewer.getQuickAssistAssistant().uninstall();
+			JavaProjectHelper.emptyDisplayLoop();
+
+			assertEquals("expect proposal", "Annotate as '@NonNull V'", proposal.getDisplayString());
+			String expectedInfo=
+					"<dl><dt>get</dt>" +
+					"<dd>(TK;)TV;</dd>" +
+					"<dd>(TK;)T<b>1</b>V;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append("pack/age/MyMap.eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/MyMap\n" +
+					"get\n" +
+					" (TK;)TV;\n" +
+					" (TK;)T1V;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on a simple return type (type variable).
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	public void testAnnotateReturn2() throws Exception {
+		
+		String MY_MAP_PATH= "pack/age/MyMap";
+		String[] pathAndContents= new String[] { 
+					MY_MAP_PATH+".java",
+					"package pack.age;\n" +
+					"public interface MyMap<K,V> {\n" +
+					"    public V get(K key);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_5);
+		IType type= fJProject1.findType(MY_MAP_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("V get");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull V'", list);
+			String expectedInfo=
+					"<dl><dt>get</dt>" +
+					"<dd>(TK;)TV;</dd>" +
+					"<dd>(TK;)T<b>1</b>V;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as '@Nullable V'", list);
+			expectedInfo=
+					"<dl><dt>get</dt>" +
+					"<dd>(TK;)TV;</dd>" +
+					"<dd>(TK;)T<b>0</b>V;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(MY_MAP_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/MyMap\n" +
+					"get\n" +
+					" (TK;)TV;\n" +
+					" (TK;)T0V;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "Remove") if annotation file already says "@Nullable".
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	public void testAnnotateRemove() throws Exception {
+		
+		String MY_MAP_PATH= "pack/age/MyMap";
+
+		String[] pathAndContents= new String[] { 
+				MY_MAP_PATH+".java", 
+				"package pack.age;\n" +
+				"public interface MyMap<K,V> {\n" +
+				"    public V get(K key);\n" +
+				"}\n"
+			};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_5);
+		IType type= fJProject1.findType(MY_MAP_PATH.replace('/', '.'));
+		IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(MY_MAP_PATH+".eea"));
+		String initialContent=
+				"class pack/age/MyMap\n" +
+				"get\n" +
+				" (TK;)TV;\n" +
+				" (TK;)T0V;\n";
+		ensureExists(annotationFile.getParent());
+		annotationFile.create(new ByteArrayInputStream(initialContent.getBytes("UTF-8")), 0, null);
+		
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("V get");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull V'", list);
+			String expectedInfo=
+					"<dl><dt>get</dt>" +
+					"<dd>(TK;)TV;</dd>" +
+					"<dd>(TK;)T<b>1</b>V;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Remove nullness annotation from type 'V'", list);
+			expectedInfo=
+					"<dl><dt>get</dt>" +
+					"<dd>(TK;)TV;</dd>" +
+					"<dd>(TK;)T<del>0</del>V;</dd>" + // <= <strike>0</strike>
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			assertTrue("Annotation file should still exist", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/MyMap\n" +
+					"get\n" +
+					" (TK;)TV;\n" +
+					" (TK;)TV;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+	
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on an (outer) array type (in parameter position).
+	 * The method already has a 2-line entry (i.e., not yet annotated).
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	public void testAnnotateParameter_Array1() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public String test(int[][] ints, List<String> list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+		
+		IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+		String initialContent=
+				"class pack/age/X\n" +
+				"test\n" +
+				" ([[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n";
+		ensureExists(annotationFile.getParent());
+		annotationFile.create(new ByteArrayInputStream(initialContent.getBytes("UTF-8")), 0, null);
+
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("[][] ints");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as 'int @NonNull [][]'", list);
+			String expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([<b>1</b>[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as 'int @Nullable [][]'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([<b>0</b>[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/X\n" +
+					"test\n" +
+					" ([[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n" +
+					" ([0[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on a simple field type (type variable).
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	// FIXME(stephan): enable once implemented
+	public void _testAnnotateField1() throws Exception {
+		
+		String NODE_PATH= "pack/age/Node";
+		String[] pathAndContents= new String[] { 
+					NODE_PATH+".java",
+					"package pack.age;\n" +
+					"public class Node<V> {\n" +
+					"    V value;\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_5);
+		IType type= fJProject1.findType(NODE_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("V value");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull V'", list);
+			String expectedInfo=
+					"<dl><dt>value</dt>" +
+					"<dd>TV;</dd>" +
+					"<dd>T<b>1</b>V;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as '@Nullable V'", list);
+			expectedInfo=
+					"<dl><dt>value</dt>" +
+					"<dd>TV;</dd>" +
+					"<dd>T<b>0</b>V;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(NODE_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/Node\n" +
+					"value\n" +
+					" TV;\n" +
+					" T0V;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+}
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java
new file mode 100644
index 0000000..02c1703
--- /dev/null
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java
@@ -0,0 +1,505 @@
+/*******************************************************************************
+ * Copyright (c) 2015 GK Software AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.ui.tests.quickfix;
+
+import java.io.ByteArrayInputStream;
+import java.util.List;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import org.eclipse.core.runtime.Path;
+
+import org.eclipse.core.resources.IFile;
+
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.tests.core.Java18ProjectTestSetup;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
+
+public class AnnotateAssistTest18 extends AbstractAnnotateAssistTests {
+
+	protected static final String ANNOTATION_PATH= "annots";
+
+	protected static final Class<?> THIS= AnnotateAssistTest18.class;
+	
+	public static Test suite() {
+		return setUpTest(new TestSuite(THIS));
+	}
+
+	public static Test setUpTest(Test test) {
+		return new Java18ProjectTestSetup(test);
+	}
+	public AnnotateAssistTest18(String name) {
+		super(name);
+	}
+
+	protected void setUp() throws Exception {
+		fJProject1= Java18ProjectTestSetup.getProject();
+		fJProject1.getProject().getFolder(ANNOTATION_PATH).create(true, true, null);
+		fJProject1.setOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, JavaCore.ENABLED);
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on a type argument of a parameter.
+	 * The parameterized type already has a @NonNull annotation.
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	public void testAnnotateParameter_TypeArgument() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public String test(int[] ints, List<String> list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+		
+		IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+		String initialContent=
+				"class pack/age/X\n" +
+				"test\n" +
+				" ([ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n" +
+				" ([IL1java/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n";
+		ensureExists(annotationFile.getParent());
+		annotationFile.create(new ByteArrayInputStream(initialContent.getBytes("UTF-8")), 0, null);
+
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("String> list");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull String'", list);
+			String expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([IL1java/util/List&lt;L<b>1</b>java/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as '@Nullable String'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([IL1java/util/List&lt;L<b>0</b>java/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/X\n" +
+					"test\n" +
+					" ([ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n" +
+					" ([IL1java/util/List<L0java/lang/String;>;)Ljava/lang/String;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert NO proposals on the primitive leaf type of an array type.
+	 * @throws Exception
+	 */
+	public void testAnnotateParameter_ArrayOfPrimitive() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public String test(int[] ints, List<String> list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("int[]");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertNumberOfProposals(list, 0);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on a wildcard bound inside a parameter type
+	 * The parameterized type and the wildcard already has a @NonNull annotation.
+	 * Annotation entry already exists, with @NonNull on the wildcard itself.
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	public void testAnnotateParameter_WildcardBound() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public String test(Object[] objects, List<? extends Number> list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+		
+		IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+		String initialContent=
+				"class pack/age/X\n" +
+				"test\n" +
+				" ([Ljava/lang/Object;Ljava/util/List<+Ljava/lang/Number;>;)Ljava/lang/String;\n" +
+				" ([Ljava/lang/Object;L1java/util/List<+1Ljava/lang/Number;>;)Ljava/lang/String;\n";
+		ensureExists(annotationFile.getParent());
+		annotationFile.create(new ByteArrayInputStream(initialContent.getBytes("UTF-8")), 0, null);
+
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("Number> list");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull Number'", list);
+			String expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([Ljava/lang/Object;Ljava/util/List&lt;+Ljava/lang/Number;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([Ljava/lang/Object;L1java/util/List&lt;+1L<b>1</b>java/lang/Number;&gt;;)Ljava/lang/String;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as '@Nullable Number'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([Ljava/lang/Object;Ljava/util/List&lt;+Ljava/lang/Number;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([Ljava/lang/Object;L1java/util/List&lt;+1L<b>0</b>java/lang/Number;&gt;;)Ljava/lang/String;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/X\n" +
+					"test\n" +
+					" ([Ljava/lang/Object;Ljava/util/List<+Ljava/lang/Number;>;)Ljava/lang/String;\n" +
+					" ([Ljava/lang/Object;L1java/util/List<+1L0java/lang/Number;>;)Ljava/lang/String;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on an inner array type (in parameter position).
+	 * A single line entry using this selector exists and will be amended.
+	 * Apply the second proposal and check the effect.
+	 * 
+	 * Cf. {@link AnnotateAssistTest15#testAnnotateParameter_Array1()}
+	 * @throws Exception
+	 */
+	public void testAnnotateParameter_Array2() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public String test(int[][] ints, List<String> list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+		
+		IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+		String initialContent=
+				"class pack/age/X\n" +
+				"test\n";
+		ensureExists(annotationFile.getParent());
+		annotationFile.create(new ByteArrayInputStream(initialContent.getBytes("UTF-8")), 0, null);
+
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("[] ints");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as 'int[] @NonNull []'", list);
+			String expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([[<b>1</b>ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as 'int[] @Nullable []'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([[<b>0</b>ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/X\n" +
+					"test\n" +
+					" ([[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n" +
+					" ([[0ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on an inner array type (in parameter position).
+	 * An entry with annotation on the outer array already exists and will be amended.
+	 * Apply the second proposal and check the effect.
+	 * 
+	 * Cf. {@link AnnotateAssistTest15#testAnnotateParameter_Array1()}
+	 * @throws Exception
+	 */
+	public void testAnnotateParameter_Array3() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public String test(int[][] ints, List<String> list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+		
+		IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+		String initialContent=
+				"class pack/age/X\n" +
+				"test\n" +
+				" ([[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n" +
+				" ([1[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n";
+		ensureExists(annotationFile.getParent());
+		annotationFile.create(new ByteArrayInputStream(initialContent.getBytes("UTF-8")), 0, null);
+
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("[] ints");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as 'int[] @NonNull []'", list);
+			String expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([1[<b>1</b>ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as 'int[] @Nullable []'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>([[ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" +
+					"<dd>([1[<b>0</b>ILjava/util/List&lt;Ljava/lang/String;&gt;;)Ljava/lang/String;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/X\n" +
+					"test\n" +
+					" ([[ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n" +
+					" ([1[0ILjava/util/List<Ljava/lang/String;>;)Ljava/lang/String;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on a method's type parameter
+	 * The parameterized type and the wildcard already has a @NonNull annotation.
+	 * Annotation entry already exists, with @NonNull on the wildcard itself.
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	// FIXME(stephan): enable once implemented
+	public void _testAnnotateParameter_TypeParameter() throws Exception {
+		
+		String X_PATH= "pack/age/X";
+		String[] pathAndContents= new String[] { 
+					X_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public interface X {\n" +
+					"    public <X, T extends List<X>> X test(T list);\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8);
+
+		IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("T extends");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull T'", list);
+			String expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>&lt;X:Ljava/lang/Object;T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" +
+					"<dd>&lt;X:Ljava/lang/Object;1T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as '@Nullable Number'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>&lt;X:Ljava/lang/Object;T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" +
+					"<dd>&lt;X:Ljava/lang/Object;0T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/X\n" +
+					"test\n" +
+					" <X:Ljava/lang/Object;T:Ljava/util/List<;TX;>>(TT;)TX;\n" +
+					" <X:Ljava/lang/Object;0T:Ljava/util/List<;TX;>>(TT;)TX;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+
+	/**
+	 * Assert two proposals ("@NonNull" and "@Nullable") on a complex field type (list of array)
+	 * Apply the second proposal and check the effect.
+	 * @throws Exception
+	 */
+	// FIXME(stephan): enable once implemented
+	public void _testAnnotateField2() throws Exception {
+		
+		String NODE_PATH= "pack/age/Node";
+		String[] pathAndContents= new String[] { 
+					NODE_PATH+".java",
+					"package pack.age;\n" +
+					"import java.util.List;\n" +
+					"public class Node {\n" +
+					"    List<Object[]> value;\n" +
+					"}\n"
+				};
+		addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_5);
+		IType type= fJProject1.findType(NODE_PATH.replace('/', '.'));
+		JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+		try {
+			int offset= pathAndContents[1].indexOf("[]> value");
+
+			List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+			
+			assertCorrectLabels(list);
+			assertNumberOfProposals(list, 2);
+			
+			ICompletionProposal proposal= findProposalByName("Annotate as 'Object @NonNull []'", list);
+			String expectedInfo=
+					"<dl><dt>value</dt>" +
+					"<dd>Ljava/util/List&lt;[Ljava/lang/Object;&gt;;</dd>" +
+					"<dd>Ljava/util/List&lt;[<b>1</b>Ljava/lang/Object;&gt;;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as 'Object @Nullable []'", list);
+			expectedInfo=
+					"<dl><dt>value</dt>" +
+					"<dd>Ljava/util/List&lt;[Ljava/lang/Object;&gt;;</dd>" +
+					"<dd>Ljava/util/List&lt;[<b>0</b>Ljava/lang/Object;&gt;;</dd>" + // <= 0
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+			proposal.apply(document);
+			
+			IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(NODE_PATH+".eea"));
+			assertTrue("Annotation file should have been created", annotationFile.exists());
+
+			String expectedContent=
+					"class pack/age/Node\n" +
+					"value\n" +
+					" Ljava/util/List<[Ljava/lang/Object;>;\n" +
+					" Ljava/util/List<[0Ljava/lang/Object;>;\n";
+			checkContentOfFile("annotation file content", annotationFile, expectedContent);
+		} finally {
+			JavaPlugin.getActivePage().closeAllEditors(false);
+		}
+	}
+}
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/JarUtil.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/JarUtil.java
new file mode 100644
index 0000000..275f2b2
--- /dev/null
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/JarUtil.java
@@ -0,0 +1,717 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2015 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     IBM Corporation - initial API and implementation
+ *     Nina Rinskaya
+ *     		Fix for https://bugs.eclipse.org/bugs/show_bug.cgi?id=172820.
+ *     Stephan Herrmann - Contribution for
+ *								[null] "Annotate" proposals for adding external null annotations to library classes - https://bugs.eclipse.org/458200
+ *******************************************************************************/
+package org.eclipse.jdt.ui.tests.quickfix;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.*;
+
+import org.eclipse.jdt.internal.compiler.ClassFile;
+import org.eclipse.jdt.internal.compiler.CompilationResult;
+import org.eclipse.jdt.internal.compiler.Compiler;
+import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
+import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
+import org.eclipse.jdt.internal.compiler.IProblemFactory;
+import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
+import org.eclipse.jdt.internal.compiler.batch.FileSystem;
+import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
+import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
+import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
+
+/**
+ * This is a reduced and marginally adjusted copy from org.eclipse.jdt.core.tests.util.Util
+ */
+@SuppressWarnings("javadoc")
+public class JarUtil {
+
+	// Trace for delete operation
+    /*
+     * Maximum time wasted repeating delete operations while running JDT/Core tests.
+     */
+    private static int DELETE_MAX_TIME = 0;
+    /**
+     * Trace deletion operations while running JDT/Core tests.
+     */
+    public static boolean DELETE_DEBUG = false;
+    /**
+     * Maximum of time in ms to wait in deletion operation while running JDT/Core tests.
+     * Default is 10 seconds. This number cannot exceed 1 minute (ie. 60000).
+     * <br>
+     * To avoid too many loops while waiting, the ten first ones are done waiting
+     * 10ms before repeating, the ten loops after are done waiting 100ms and
+     * the other loops are done waiting 1s...
+     */
+    public static int DELETE_MAX_WAIT = 10000;
+
+    /**
+     * Initially, output directory was located in System.getProperty("java.io.tmpdir")+"\comptest".
+     * To allow user to run several compiler tests at the same time, main output directory
+     * is now located in a sub-directory of "comptest" which name is "run."+<code>System.currentMilliseconds</code>.
+     *
+     * @see #DELAY_BEFORE_CLEAN_PREVIOUS
+     */
+    private final static String OUTPUT_DIRECTORY;
+    /**
+     * Let user specify the delay in hours before output directories are removed from file system
+     * while starting a new test run. Default value is 2 hours.
+     * <p>
+     * Note that this value may be a float and so have time less than one hour.
+     * If value is 0 or negative, then all previous run directories will be removed...
+     *
+     * @see #OUTPUT_DIRECTORY
+     */
+    private final static String DELAY_BEFORE_CLEAN_PREVIOUS = System.getProperty("delay");
+    /*
+     * Static initializer to clean directories created while running previous test suites.
+     */
+    static {
+        // Get delay for cleaning sub-directories
+        long millisecondsPerHour = 1000L * 3600L;
+        long delay = millisecondsPerHour * 2; // default is to keep previous run directories for 2 hours
+        try {
+            if (DELAY_BEFORE_CLEAN_PREVIOUS != null) {
+                float hours = Float.parseFloat(DELAY_BEFORE_CLEAN_PREVIOUS);
+                delay = (int) (millisecondsPerHour * hours);
+            }
+        }
+        catch (NumberFormatException nfe) {
+            // use default
+        }
+
+        // Get output directory root from system properties
+        String container = System.getProperty("jdt.test.output_directory");
+        if (container == null){
+            container = System.getProperty("java.io.tmpdir");
+        }
+        if (container == null) {
+            container = "."; // use current directory
+        }
+
+        // Get file for root directory
+        if (Character.isLowerCase(container.charAt(0)) && container.charAt(1) == ':') {
+            container = Character.toUpperCase(container.charAt(0)) + container.substring(1);
+        }
+        File dir = new File(new File(container), "comptest");
+
+        // If root directory already exists, clean it
+        if (dir.exists()) {
+            long now = System.currentTimeMillis();
+            if ((now - dir.lastModified()) > delay) {
+                // remove all directory content
+                flushDirectoryContent(dir);
+            } else {
+                // remove only old sub-dirs
+                File[] testDirs = dir.listFiles();
+                for (int i=0,l=testDirs.length; i<l; i++) {
+                    if (testDirs[i].isDirectory()) {
+                        if ((now - testDirs[i].lastModified()) > delay) {
+                            delete(testDirs[i]);
+                        }
+                    }
+                }
+            }
+        }
+
+        // Computed test run directory name based on current time
+        File dateDir = new File(dir, "run."+System.currentTimeMillis());
+        String pathDir = null;
+        try {
+        	pathDir = dateDir.getCanonicalPath();
+		} catch (IOException e) {
+			pathDir = dateDir.getAbsolutePath();
+		}
+		OUTPUT_DIRECTORY = pathDir;
+   }
+
+
+public static CompilationUnit[] compilationUnits(String[] testFiles) {
+    int length = testFiles.length / 2;
+    CompilationUnit[] result = new CompilationUnit[length];
+    int index = 0;
+    for (int i = 0; i < length; i++) {
+        result[i] = new CompilationUnit(testFiles[index + 1].toCharArray(), testFiles[index], null);
+        index += 2;
+    }
+    return result;
+}
+/* inlined and simplified for JDT/UI */
+private static class Requestor implements ICompilerRequestor {
+	public boolean hasErrors = false;
+	public String outputPath;
+	public String problemLog = "";
+
+	public void acceptResult(CompilationResult compilationResult) {
+		this.hasErrors |= compilationResult.hasErrors();
+		this.problemLog += compilationResult.toString();
+		outputClassFiles(compilationResult);
+	}
+	protected void outputClassFiles(CompilationResult unitResult) {
+		if ((unitResult != null) && !unitResult.hasErrors()) {
+			ClassFile[] classFiles = unitResult.getClassFiles();
+			if (this.outputPath != null) {
+				for (int i = 0, fileCount = classFiles.length; i < fileCount; i++) {
+					// retrieve the key and the corresponding classfile
+					ClassFile classFile = classFiles[i];
+					String relativeName =
+						new String(classFile.fileName()).replace('/', File.separatorChar) + ".class";
+					try {
+						org.eclipse.jdt.internal.compiler.util.Util.writeToDisk(true, this.outputPath, relativeName, classFile);
+					} catch(IOException e) {
+						e.printStackTrace();
+					}
+				}
+			}
+		}
+	}
+}
+public static void compile(String[] pathsAndContents, Map options, String[] classpath, String outputPath) {
+        IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault());
+        Requestor requestor = new Requestor();
+        requestor.outputPath = outputPath.endsWith(File.separator) ? outputPath : outputPath + File.separator;
+
+        String[] classLibs = getJavaClassLibs();
+        if (classpath != null) {
+        	int length = classpath.length;
+        	int classLibsLength = classLibs.length;
+        	System.arraycopy(classpath, 0, classpath = new String[length + classLibsLength], 0, length);
+        	System.arraycopy(classLibs, 0, classpath, length, classLibsLength);
+        } else {
+        	classpath = classLibs;
+        }
+        
+        INameEnvironment nameEnvironment = new FileSystem(classpath, new String[] {}, null);
+        IErrorHandlingPolicy errorHandlingPolicy =
+            new IErrorHandlingPolicy() {
+                public boolean proceedOnErrors() {
+                    return true;
+                }
+                public boolean stopOnFirstError() {
+                    return false;
+                }
+				public boolean ignoreAllErrors() {
+					return false;
+				}
+            };
+        CompilerOptions compilerOptions = new CompilerOptions(options);
+        compilerOptions.performMethodsFullRecovery = false;
+        compilerOptions.performStatementsRecovery = false;
+        Compiler batchCompiler =
+            new Compiler(
+                nameEnvironment,
+                errorHandlingPolicy,
+                compilerOptions,
+                requestor,
+                problemFactory);
+        batchCompiler.options.produceReferenceInfo = true;
+        batchCompiler.compile(compilationUnits(pathsAndContents)); // compile all files together
+        // cleanup
+    	nameEnvironment.cleanup();
+        if (requestor.hasErrors)
+	        System.err.print(requestor.problemLog); // problem log empty if no problems
+}
+public static void createFile(String path, String contents) throws IOException {
+    FileOutputStream output = new FileOutputStream(path);
+    try {
+        output.write(contents.getBytes());
+    } finally {
+        output.close();
+    }
+}
+public static void createJar(String[] pathsAndContents, String[] extraPathsAndContents, Map options, String[] classpath, String jarPath) throws IOException {
+    String classesPath = getOutputDirectory() + File.separator + "classes";
+    File classesDir = new File(classesPath);
+    flushDirectoryContent(classesDir);
+	if (pathsAndContents != null) {
+		compile(pathsAndContents, options, classpath, classesPath);
+	}
+	if (extraPathsAndContents != null) {
+		for (int i = 0, l = extraPathsAndContents.length; i < l; /* inc in loop */) {
+			File  outputFile = new File(classesPath, extraPathsAndContents[i++]);
+			outputFile.getParentFile().mkdirs();
+			JarUtil.writeToFile(extraPathsAndContents[i++], outputFile.getAbsolutePath());
+		}
+	}
+    zip(classesDir, jarPath);
+}
+public static void createJar(String[] javaPathsAndContents, String[] extraPathsAndContents, String jarPath, String[] classpath, String compliance, Map options) throws IOException {
+	Map compileOptions = getCompileOptions(compliance);
+	if (options != null) {
+		compileOptions.putAll(options);
+	}
+	createJar(javaPathsAndContents, extraPathsAndContents, compileOptions, classpath, jarPath);
+}
+public static void createSourceZip(String[] pathsAndContents, String zipPath) throws IOException {
+    String sourcesPath = getOutputDirectory() + File.separator + "sources";
+    createSourceDir(pathsAndContents, sourcesPath);
+    zip(new File(sourcesPath), zipPath);
+}
+
+public static void createSourceDir(String[] pathsAndContents, String sourcesPath) throws IOException {
+	flushDirectoryContent(new File(sourcesPath));
+    for (int i = 0, length = pathsAndContents.length; i < length; i+=2) {
+        String sourcePath = sourcesPath + File.separator + pathsAndContents[i];
+        File sourceFile = new File(sourcePath);
+        sourceFile.getParentFile().mkdirs();
+        createFile(sourcePath, pathsAndContents[i+1]);
+    }
+}
+/**
+ * Delete a file or directory and insure that the file is no longer present
+ * on file system. In case of directory, delete all the hierarchy underneath.
+ *
+ * @param file The file or directory to delete
+ * @return true iff the file was really delete, false otherwise
+ */
+public static boolean delete(File file) {
+	// flush all directory content
+	if (file.isDirectory()) {
+		flushDirectoryContent(file);
+	}
+	// remove file
+	file.delete();
+	if (isFileDeleted(file)) {
+		return true;
+	}
+	return waitUntilFileDeleted(file);
+}
+/**
+ * Flush content of a given directory (leaving it empty),
+ * no-op if not a directory.
+ */
+public static void flushDirectoryContent(File dir) {
+    File[] files = dir.listFiles();
+    if (files == null) return;
+    for (int i = 0, max = files.length; i < max; i++) {
+        delete(files[i]);
+    }
+}
+private static Map getCompileOptions(String compliance) {
+    Map options = new HashMap();
+    options.put(CompilerOptions.OPTION_Compliance, compliance);
+    options.put(CompilerOptions.OPTION_Source, compliance);
+    options.put(CompilerOptions.OPTION_TargetPlatform, compliance);
+    return options;
+}
+/**
+ * Search the user hard-drive for a Java class library.
+ * Returns null if none could be found.
+*/
+public static String[] getJavaClassLibs() {
+	// check bootclasspath properties for Sun, JRockit and Harmony VMs
+	String bootclasspathProperty = System.getProperty("sun.boot.class.path"); //$NON-NLS-1$
+	if ((bootclasspathProperty == null) || (bootclasspathProperty.length() == 0)) {
+		// IBM J9 VMs
+		bootclasspathProperty = System.getProperty("vm.boot.class.path"); //$NON-NLS-1$
+		if ((bootclasspathProperty == null) || (bootclasspathProperty.length() == 0)) {
+			// Harmony using IBM VME
+			bootclasspathProperty = System.getProperty("org.apache.harmony.boot.class.path"); //$NON-NLS-1$
+		}
+	}
+	String[] jars = null;
+	if ((bootclasspathProperty != null) && (bootclasspathProperty.length() != 0)) {
+		StringTokenizer tokenizer = new StringTokenizer(bootclasspathProperty, File.pathSeparator);
+		final int size = tokenizer.countTokens();
+		jars = new String[size];
+		int i = 0;
+		while (tokenizer.hasMoreTokens()) {
+			final String fileName = toNativePath(tokenizer.nextToken());
+			if (new File(fileName).exists()) {
+				jars[i] = fileName;
+				i++;
+			}
+		}
+		if (size != i) {
+			// resize
+			System.arraycopy(jars, 0, (jars = new String[i]), 0, i);
+		}
+	} else {
+		String jreDir = getJREDirectory();
+		final String osName = System.getProperty("os.name");
+		if (jreDir == null) {
+			return new String[] {};
+		}
+		if (osName.startsWith("Mac")) {
+			return new String[] {
+					toNativePath(jreDir + "/../Classes/classes.jar")
+			};
+		}
+		final String vmName = System.getProperty("java.vm.name");
+		if ("J9".equals(vmName)) {
+			return new String[] {
+					toNativePath(jreDir + "/lib/jclMax/classes.zip")
+			};
+		}
+		String[] jarsNames = null;
+		ArrayList paths = new ArrayList();
+		if ("DRLVM".equals(vmName)) {
+			FilenameFilter jarFilter = new FilenameFilter() {
+				public boolean accept(File dir, String name) {
+					return name.endsWith(".jar") & !name.endsWith("-src.jar");
+				}
+			};
+			jarsNames = new File(jreDir + "/lib/boot/").list(jarFilter);
+			addJarEntries(jreDir + "/lib/boot/", jarsNames, paths);
+		} else {
+			jarsNames = new String[] {
+					"/lib/vm.jar",
+					"/lib/rt.jar",
+					"/lib/core.jar",
+					"/lib/security.jar",
+					"/lib/xml.jar",
+					"/lib/graphics.jar"
+			};
+			addJarEntries(jreDir, jarsNames, paths);
+		}
+		jars = new String[paths.size()];
+		paths.toArray(jars);
+	}
+	return jars;
+}
+private static void addJarEntries(String jreDir, String[] jarNames, ArrayList paths) {
+	for (int i = 0, max = jarNames.length; i < max; i++) {
+		final String currentName = jreDir + jarNames[i];
+		File f = new File(currentName);
+		if (f.exists()) {
+			paths.add(toNativePath(currentName));
+		}
+	}
+}
+/**
+ * Returns the JRE directory this tests are running on.
+ * Returns null if none could be found.
+ *
+ * Example of use: [org.eclipse.jdt.core.tests.util.Util.getJREDirectory()]
+ */
+public static String getJREDirectory() {
+    return System.getProperty("java.home");
+}
+/**
+ * Search the user hard-drive for a possible output directory.
+ * Returns null if none could be found.
+ *
+ * Example of use: [org.eclipse.jdt.core.tests.util.Util.getOutputDirectory()]
+ */
+public static String getOutputDirectory() {
+    return OUTPUT_DIRECTORY;
+}
+/**
+ * Returns the parent's child file matching the given file or null if not found.
+ *
+ * @param file The searched file in parent
+ * @return The parent's child matching the given file or null if not found.
+ */
+private static File getParentChildFile(File file) {
+    File parent = file.getParentFile();
+    if (parent == null || !parent.exists()) return null;
+    File[] files = parent.listFiles();
+    int length = files==null ? 0 : files.length;
+    if (length > 0) {
+        for (int i=0; i<length; i++) {
+            if (files[i] == file) {
+                return files[i];
+            } else if (files[i].equals(file)) {
+                return files[i];
+            } else if (files[i].getPath().equals(file.getPath())) {
+                return files[i];
+            }
+        }
+    }
+    return null;
+}
+/**
+ * Returns the test name from stack elements info.
+ *
+ * @return The name of the test currently running
+ */
+private static String getTestName() {
+    StackTraceElement[] elements = new Exception().getStackTrace();
+    int idx = 0, length=elements.length;
+    while (idx<length && !elements[idx++].getClassName().startsWith("org.eclipse.jdt")) {
+        // loop until JDT/Core class appears in the stack
+    }
+    if (idx<length) {
+        StackTraceElement testElement = null;
+        while (idx<length && elements[idx].getClassName().startsWith("org.eclipse.jdt")) {
+            testElement = elements[idx++];
+        }
+        if (testElement != null) {
+            return testElement.getClassName() + " - " + testElement.getMethodName();
+        }
+    }
+    return "?";
+}
+/**
+ * Returns whether a file is really deleted or not.
+ * Does not only rely on {@link File#exists()} method but also
+ * look if it's not in its parent children {@link #getParentChildFile(File)}.
+ *
+ * @param file The file to test if deleted
+ * @return true if the file does not exist and was not found in its parent children.
+ */
+public static boolean isFileDeleted(File file) {
+    return !file.exists() && getParentChildFile(file) == null;
+}
+/**
+ * Print given file information with specified indentation.
+ * These information are:<ul>
+ * 	<li>read {@link File#canRead()}</li>
+ * 	<li>write {@link File#canWrite()}</li>
+ * 	<li>exists {@link File#exists()}</li>
+ * 	<li>is file {@link File#isFile()}</li>
+ * 	<li>is directory {@link File#isDirectory()}</li>
+ * 	<li>is hidden {@link File#isHidden()}</li>
+ * </ul>
+ * May recurse several level in parents hierarchy.
+ * May also display children, but then will not recusre in parent
+ * hierarchy to avoid infinite loop...
+ *
+ * @param file The file to display information
+ * @param indent Number of tab to print before the information
+ * @param recurse Display also information on <code>recurse</code>th parents in hierarchy.
+ * 	If negative then display children information instead.
+ */
+private static void printFileInfo(File file, int indent, int recurse) {
+    String tab = "";
+    for (int i=0; i<indent; i++) tab+="\t";
+    System.out.print(tab+"- "+file.getName()+" file info: ");
+    String sep = "";
+    if (file.canRead()) {
+        System.out.print("read");
+        sep = ", ";
+    }
+    if (file.canWrite()) {
+        System.out.print(sep+"write");
+        sep = ", ";
+    }
+    if (file.exists()) {
+        System.out.print(sep+"exist");
+        sep = ", ";
+    }
+    if (file.isDirectory()) {
+        System.out.print(sep+"dir");
+        sep = ", ";
+    }
+    if (file.isFile()) {
+        System.out.print(sep+"file");
+        sep = ", ";
+    }
+    if (file.isHidden()) {
+        System.out.print(sep+"hidden");
+        sep = ", ";
+    }
+    System.out.println();
+    File[] files = file.listFiles();
+    int length = files==null ? 0 : files.length;
+    if (length > 0) {
+        boolean children = recurse < 0;
+        System.out.print(tab+"	+ children: ");
+        if (children) System.out.println();
+        for (int i=0; i<length; i++) {
+            if (children) { // display children
+                printFileInfo(files[i], indent+2, -1);
+            } else {
+                if (i>0) System.out.print(", ");
+                System.out.print(files[i].getName());
+                if (files[i].isDirectory()) System.out.print("[dir]");
+                else if (files[i].isFile()) System.out.print("[file]");
+                else System.out.print("[?]");
+            }
+        }
+        if (!children) System.out.println();
+    }
+    if (recurse > 0) {
+        File parent = file.getParentFile();
+        if (parent != null) printFileInfo(parent, indent+1, recurse-1);
+    }
+}
+/**
+ * Print stack trace with only JDT elements.
+ *
+ * @param exception Exception of the stack trace. May be null, then a fake exception is used.
+ * @param indent Number of tab to display before the stack elements to display.
+ */
+private static void printJdtStackTrace(Exception exception, int indent) {
+    String tab = "";
+    for (int i=0; i<indent; i++) tab+="\t";
+    StackTraceElement[] elements = (exception==null?new Exception():exception).getStackTrace();
+    int idx = 0, length=elements.length;
+    while (idx<length && !elements[idx++].getClassName().startsWith("org.eclipse.jdt")) {
+        // loop until JDT/Core class appears in the stack
+    }
+    if (idx<length) {
+        System.out.print(tab+"- stack trace");
+        if (exception == null)
+            System.out.println(":");
+        else
+            System.out.println(" for exception "+exception+":");
+        while (idx<length && elements[idx].getClassName().startsWith("org.eclipse.jdt")) {
+            StackTraceElement testElement = elements[idx++];
+            System.out.println(tab+"	-> "+testElement);
+        }
+    } else {
+        exception.printStackTrace(System.out);
+    }
+}
+/**
+ * Makes the given path a path using native path separators as returned by File.getPath()
+ * and trimming any extra slash.
+ */
+public static String toNativePath(String path) {
+    String nativePath = path.replace('\\', File.separatorChar).replace('/', File.separatorChar);
+    return
+        nativePath.endsWith("/") || nativePath.endsWith("\\") ?
+            nativePath.substring(0, nativePath.length() - 1) :
+            nativePath;
+}
+public static void waitAtLeast(int time) {
+	long start = System.currentTimeMillis();
+	do {
+		try {
+			Thread.sleep(time);
+		} catch (InterruptedException e) {
+		}
+	} while ((System.currentTimeMillis() - start) < time);
+}
+
+/**
+ * Wait until the file is _really_ deleted on file system.
+ *
+ * @param file Deleted file
+ * @return true if the file was finally deleted, false otherwise
+ */
+private static boolean waitUntilFileDeleted(File file) {
+    if (DELETE_DEBUG) {
+        System.out.println();
+        System.out.println("WARNING in test: "+getTestName());
+        System.out.println("	- problems occured while deleting "+file);
+        printJdtStackTrace(null, 1);
+        printFileInfo(file.getParentFile(), 1, -1); // display parent with its children
+        System.out.print("	- wait for ("+DELETE_MAX_WAIT+"ms max): ");
+    }
+    int count = 0;
+    int delay = 10; // ms
+    int maxRetry = DELETE_MAX_WAIT / delay;
+    int time = 0;
+    while (count < maxRetry) {
+        try {
+            count++;
+            Thread.sleep(delay);
+            time += delay;
+            if (time > DELETE_MAX_TIME) DELETE_MAX_TIME = time;
+            if (DELETE_DEBUG) System.out.print('.');
+            if (file.exists()) {
+                if (file.delete()) {
+                    // SUCCESS
+                    if (DELETE_DEBUG) {
+                        System.out.println();
+                        System.out.println("	=> file really removed after "+time+"ms (max="+DELETE_MAX_TIME+"ms)");
+                        System.out.println();
+                    }
+                    return true;
+                }
+            }
+            if (isFileDeleted(file)) {
+                // SUCCESS
+                if (DELETE_DEBUG) {
+                    System.out.println();
+                    System.out.println("	=> file disappeared after "+time+"ms (max="+DELETE_MAX_TIME+"ms)");
+                    System.out.println();
+                }
+                return true;
+            }
+            // Increment waiting delay exponentially
+            if (count >= 10 && delay <= 100) {
+                count = 1;
+                delay *= 10;
+                maxRetry = DELETE_MAX_WAIT / delay;
+                if ((DELETE_MAX_WAIT%delay) != 0) {
+                    maxRetry++;
+                }
+            }
+        }
+        catch (InterruptedException ie) {
+            break; // end loop
+        }
+    }
+    if (!DELETE_DEBUG) {
+        System.out.println();
+        System.out.println("WARNING in test: "+getTestName());
+        System.out.println("	- problems occured while deleting "+file);
+        printJdtStackTrace(null, 1);
+        printFileInfo(file.getParentFile(), 1, -1); // display parent with its children
+    }
+    System.out.println();
+    System.out.println("	!!! ERROR: "+file+" was never deleted even after having waited "+DELETE_MAX_TIME+"ms!!!");
+    System.out.println();
+    return false;
+}
+public static void writeToFile(String contents, String destinationFilePath) {
+    File destFile = new File(destinationFilePath);
+    FileOutputStream output = null;
+    PrintWriter writer = null;
+    try {
+        output = new FileOutputStream(destFile);
+        writer = new PrintWriter(output);
+        writer.print(contents);
+        writer.flush();
+    } catch (IOException e) {
+        e.printStackTrace();
+        return;
+    } finally {
+        if (writer != null) {
+        	writer.close();
+        }
+    }
+}
+public static void zip(File rootDir, String zipPath) throws IOException {
+    ZipOutputStream zip = null;
+    try {
+        File zipFile = new File(zipPath);
+        if (zipFile.exists()) {
+        	if (!delete(zipFile))
+	        	throw new IOException("Could not delete " + zipPath);
+        	 // ensure the new zip file has a different timestamp than the previous one
+        	int timeToWait = 1000; // some platform (like Linux) have a 1s granularity)
+            waitAtLeast(timeToWait);
+        } else {
+        	zipFile.getParentFile().mkdirs();
+        }
+        zip = new ZipOutputStream(new FileOutputStream(zipFile));
+        zip(rootDir, zip, rootDir.getPath().length()+1); // 1 for last slash
+    } finally {
+        if (zip != null) {
+            zip.close();
+        }
+    }
+}
+private static void zip(File dir, ZipOutputStream zip, int rootPathLength) throws IOException {
+    File[] files = dir.listFiles();
+    if (files != null) {
+        for (int i = 0, length = files.length; i < length; i++) {
+            File file = files[i];
+            if (file.isFile()) {
+                String path = file.getPath();
+                path = path.substring(rootPathLength);
+                ZipEntry entry = new ZipEntry(path.replace('\\', '/'));
+                zip.putNextEntry(entry);
+                zip.write(org.eclipse.jdt.internal.compiler.util.Util.getFileByteContent(file));
+                zip.closeEntry();
+            } else {
+                zip(file, zip, rootPathLength);
+            }
+        }
+    }
+}
+}
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest.java
index fe4fe90..e05a3f4 100644
--- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest.java
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/QuickFixTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2014 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -104,6 +104,8 @@
 		suite.addTest(TypeParameterMismatchTest.suite());
 		suite.addTest(PropertiesFileQuickAssistTest.suite());
 		suite.addTest(NullAnnotationsQuickFixTest.suite());
+		suite.addTest(AnnotateAssistTest15.suite());
+		suite.addTest(AnnotateAssistTest18.suite());
 
 		return new ProjectTestSetup(suite);
 	}
diff --git a/org.eclipse.jdt.ui/META-INF/MANIFEST.MF b/org.eclipse.jdt.ui/META-INF/MANIFEST.MF
index a7da5b5..b5d2725 100644
--- a/org.eclipse.jdt.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.jdt.ui/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.jdt.ui; singleton:=true
-Bundle-Version: 3.10.100.qualifier
+Bundle-Version: 3.11.0.qualifier
 Bundle-Activator: org.eclipse.jdt.internal.ui.JavaPlugin
 Bundle-ActivationPolicy: lazy
 Bundle-Vendor: %providerName
diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnoatationChangeProposals.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnoatationChangeProposals.java
new file mode 100644
index 0000000..63af60c
--- /dev/null
+++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnoatationChangeProposals.java
@@ -0,0 +1,616 @@
+/*******************************************************************************
+ * Copyright (c) 2015 GK Software AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.corext.fix;
+
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.NONNULL;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.NO_ANNOTATION;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.NULLABLE;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.annotateMethodParameterType;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.annotateMethodReturnType;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.extractGenericSignature;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.getAnnotationFile;
+import static org.eclipse.jdt.internal.ui.text.spelling.WordCorrectionProposal.getHtmlRepresentation;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRoot;
+
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+
+import org.eclipse.jdt.core.IClasspathAttribute;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.ArrayType;
+import org.eclipse.jdt.core.dom.Dimension;
+import org.eclipse.jdt.core.dom.IMethodBinding;
+import org.eclipse.jdt.core.dom.ITypeBinding;
+import org.eclipse.jdt.core.dom.MethodDeclaration;
+import org.eclipse.jdt.core.dom.ParameterizedType;
+import org.eclipse.jdt.core.dom.PrimitiveType;
+import org.eclipse.jdt.core.dom.SimpleType;
+import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
+import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
+import org.eclipse.jdt.core.dom.Type;
+import org.eclipse.jdt.core.dom.TypeParameter;
+import org.eclipse.jdt.core.dom.WildcardType;
+import org.eclipse.jdt.core.util.ExternalAnnotationUtil;
+import org.eclipse.jdt.core.util.ExternalAnnotationUtil.MergeStrategy;
+
+import org.eclipse.jdt.internal.corext.dom.ASTNodes;
+import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
+import org.eclipse.jdt.internal.corext.util.Messages;
+
+import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
+import org.eclipse.jdt.ui.text.java.correction.ICommandAccess;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.JavaPluginImages;
+import org.eclipse.jdt.internal.ui.text.correction.ExternalNullAnnotationQuickAssistProcessor;
+import org.eclipse.jdt.internal.ui.text.correction.IProposalRelevance;
+
+/**
+ * Proposals for null annotations that modify external annotations, rather than Java source files.
+ * 
+ * @see <a href="https://bugs.eclipse.org/458200">[null] "Annotate" proposals for adding external
+ *      null annotations to library classes</a>
+ * @since 3.11
+ */
+public class ExternalNullAnnoatationChangeProposals {
+
+	static abstract class SignatureAnnotationChangeProposal implements IJavaCompletionProposal, ICommandAccess {
+
+		protected String fLabel;
+
+		protected ICompilationUnit fCU; // cu where the assist was invoked
+
+		protected String fAffectedTypeName;
+
+		protected IFile fAnnotationFile;
+
+		protected String fSelector;
+
+		protected String fSignature;
+
+		protected String fCurrentAnnotated;
+
+		protected String fAnnotatedSignature;
+
+		protected MergeStrategy fMergeStrategy;
+
+		protected String[] fDryRun; // result from a dry-run signature update; structure: { prefix, old-type, new-type, postfix }
+
+
+		/* return true if the operation is available. */
+		protected boolean initializeOperation(ICompilationUnit cu, ITypeBinding declaringClass, String selector,
+				String plainSignature, String annotatedSignature, String label, MergeStrategy mergeStrategy) {
+			IJavaProject project= (IJavaProject) cu.getAncestor(IJavaElement.JAVA_PROJECT);
+			IFile file= null;
+			try {
+				file= getAnnotationFile(project, declaringClass, new NullProgressMonitor());
+			} catch (CoreException e) {
+				return false;
+			}
+			if (file == null)
+				return false;
+
+			fCU= cu;
+			fAffectedTypeName= declaringClass.getErasure().getBinaryName().replace('.', '/');
+			fAnnotationFile= file;
+			fSelector= selector;
+			fAnnotatedSignature= annotatedSignature;
+			fSignature= plainSignature;
+
+			fLabel= label;
+			fMergeStrategy= mergeStrategy;
+
+			fCurrentAnnotated= ExternalAnnotationUtil.getAnnotatedSignature(fAffectedTypeName, file, fSelector, fSignature);
+			if (fCurrentAnnotated == null)
+				fCurrentAnnotated= fSignature;
+			dryRun();
+			return fDryRun != null && !fDryRun[1].equals(fDryRun[2]);
+		}
+
+		/**
+		 * Perform a dry-run annotation update, to check if we have any update, indeed. If
+		 * successful, the result should be available in {@link #fDryRun}.
+		 */
+		protected abstract void dryRun();
+
+		public Point getSelection(IDocument document) {
+			return null; // nothing to reveal in the current editor.
+		}
+
+		public String getDisplayString() {
+			return fLabel;
+		}
+
+		public Image getImage() {
+			return JavaPluginImages.get(JavaPluginImages.IMG_OBJS_ANNOTATION);
+		}
+
+		public IContextInformation getContextInformation() {
+			return null;
+		}
+
+		public void apply(IDocument document) {
+			try {
+				doAnnotateMember(new NullProgressMonitor());
+			} catch (CoreException e) {
+				JavaPlugin.log(e);
+			} catch (IOException e) {
+				JavaPlugin.log(e);
+			}
+		}
+
+		public int getRelevance() {
+			return IProposalRelevance.CHANGE_METHOD;
+		}
+
+		public String getCommandId() {
+			return ExternalNullAnnotationQuickAssistProcessor.ANNOTATE_MEMBER_ID;
+		}
+
+		public String getAdditionalProposalInfo() {
+			StringBuffer buffer= new StringBuffer();
+			buffer.append("<dl>"); //$NON-NLS-1$
+			buffer.append("<dt>").append(fSelector).append("</dt>"); //$NON-NLS-1$ //$NON-NLS-2$
+			buffer.append("<dd>").append(getHtmlRepresentation(fSignature)).append("</dd>"); //$NON-NLS-1$ //$NON-NLS-2$
+			buffer.append("<dd>").append(getFullAnnotatedSignatureHTML()).append("</dd>"); //$NON-NLS-1$ //$NON-NLS-2$
+			buffer.append("</dl>"); //$NON-NLS-1$
+			return buffer.toString();
+		}
+
+		protected String getFullAnnotatedSignatureHTML() {
+			String[] parts= fDryRun;
+
+			// search the difference:
+			int pos= 0;
+			while (pos < parts[1].length() && pos < parts[2].length()) {
+				if (parts[1].charAt(pos) != parts[2].charAt(pos))
+					break;
+				pos++;
+			}
+
+			// prefix up-to the difference:
+			StringBuilder buf= new StringBuilder();
+			buf.append(getHtmlRepresentation(parts[0]));
+			buf.append(getHtmlRepresentation(parts[2].substring(0, pos)));
+
+			// highlight the difference:
+			switch (parts[2].charAt(pos)) {
+				case NULLABLE:
+				case NONNULL:
+					// added annotation in parts[2]: bold:
+					buf.append("<b>").append(parts[2].charAt(pos)).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+					break;
+				default:
+					// removed annotation in parts[1]: strike:
+					buf.append("<del>").append(parts[1].charAt(pos)).append("</del>"); //$NON-NLS-1$ //$NON-NLS-2$
+					pos--; // char in parts[2] is not yet consumed
+			}
+
+			// everything else:
+			buf.append(getHtmlRepresentation(parts[2].substring(pos + 1)));
+			buf.append(getHtmlRepresentation(parts[3]));
+			return buf.toString();
+		}
+
+		protected abstract void doAnnotateMember(IProgressMonitor monitor) throws CoreException, UnsupportedEncodingException, IOException;
+	}
+
+	static class ReturnAnnotationRewriteProposal extends SignatureAnnotationChangeProposal {
+
+		@Override
+		protected void dryRun() {
+			fDryRun= ExternalAnnotationUtil.annotateReturnType(fCurrentAnnotated, fAnnotatedSignature, fMergeStrategy);
+		}
+
+		@Override
+		protected void doAnnotateMember(IProgressMonitor monitor) throws CoreException, IOException {
+			annotateMethodReturnType(fAffectedTypeName, fAnnotationFile, fSelector, fSignature, fAnnotatedSignature, fMergeStrategy, monitor);
+		}
+	}
+
+	static class ParameterAnnotationRewriteProposal extends SignatureAnnotationChangeProposal {
+
+		int fParamIdx;
+
+		ParameterAnnotationRewriteProposal(int paramIdx) {
+			fParamIdx= paramIdx;
+		}
+
+		@Override
+		protected void dryRun() {
+			fDryRun= ExternalAnnotationUtil.annotateParameterType(fCurrentAnnotated, fAnnotatedSignature, fParamIdx, fMergeStrategy);
+		}
+
+		@Override
+		protected void doAnnotateMember(IProgressMonitor monitor) throws CoreException, IOException {
+			annotateMethodParameterType(fAffectedTypeName, fAnnotationFile, fSelector, fSignature, fAnnotatedSignature, fParamIdx, fMergeStrategy, monitor);
+		}
+	}
+
+	/* Quick assist on class file, propose changes an any type detail. */
+	public static void collectExternalAnnotationProposals(ICompilationUnit cu, ASTNode coveringNode, int offset, ArrayList<IJavaCompletionProposal> resultingCollection) {
+
+		IJavaProject javaProject= cu.getJavaProject();
+		if (JavaCore.DISABLED.equals(javaProject.getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true)))
+			return;
+
+		if (!hasAnnotationPathInWorkspace(javaProject, cu)) // refuse to update files outside the workspace
+			return;
+
+		ASTNode inner= null; // the innermost type or type parameter node
+		while (true) {
+			if (coveringNode instanceof Type || coveringNode instanceof TypeParameter) {
+				inner= coveringNode;
+				break;
+			}
+			coveringNode= coveringNode.getParent();
+			if (coveringNode == null)
+				return;
+		}
+		if (inner.getNodeType() == ASTNode.PRIMITIVE_TYPE)
+			return; // cannot be annotated
+
+		// prepare three renderers for three proposals:
+		TypeRenderer rendererNonNull= new TypeRenderer(inner, offset, NONNULL);
+		TypeRenderer rendererNullable= new TypeRenderer(inner, offset, NULLABLE);
+		TypeRenderer rendererRemove= new TypeRenderer(inner, offset, NO_ANNOTATION);
+		ASTNode outer= inner; // will become the outermost type or type parameter node
+		{
+			ASTNode next;
+			while (((next= outer.getParent()) instanceof Type) || (next instanceof TypeParameter))
+				outer= next;
+		}
+		boolean useJava8= JavaModelUtil.is18OrHigher(javaProject.getOption(JavaCore.COMPILER_SOURCE, true));
+		if (!useJava8 && outer != inner) {
+			return; // below 1.8 we can only annotate the top type (not type parameter)
+		}
+		if (outer instanceof Type) {
+			ITypeBinding typeBinding= ((Type) outer).resolveBinding();
+			if (typeBinding != null && typeBinding.isPrimitive())
+				return;
+			outer.accept(rendererNonNull);
+			outer.accept(rendererNullable);
+			outer.accept(rendererRemove);
+		} else {
+			List<?> siblingList= (List<?>) outer.getParent().getStructuralProperty(outer.getLocationInParent());
+			rendererNonNull.visitTypeParameters(siblingList);
+			rendererNullable.visitTypeParameters(siblingList);
+			rendererRemove.visitTypeParameters(siblingList);
+		}
+
+		StructuralPropertyDescriptor locationInParent= outer.getLocationInParent();
+		ProposalCreator creator= null;
+		if (locationInParent == MethodDeclaration.RETURN_TYPE2_PROPERTY) {
+			MethodDeclaration method= (MethodDeclaration) ASTNodes.getParent(coveringNode, MethodDeclaration.class);
+			creator= new ReturnProposalCreator(cu, method.resolveBinding());
+		} else if (locationInParent == SingleVariableDeclaration.TYPE_PROPERTY) {
+			ASTNode param= outer.getParent();
+			if (param.getLocationInParent() == MethodDeclaration.PARAMETERS_PROPERTY) {
+				MethodDeclaration method= (MethodDeclaration) ASTNodes.getParent(coveringNode, MethodDeclaration.class);
+				int paramIdx= method.parameters().indexOf(param);
+				if (paramIdx != -1)
+					creator= new ParameterProposalCreator(cu, method.resolveBinding(), paramIdx);
+			}
+		}
+		if (creator != null) {
+			createProposalsForType(cu, inner, offset, rendererNonNull, rendererNullable, rendererRemove, creator, resultingCollection);
+		}
+	}
+
+	static boolean hasAnnotationPathInWorkspace(IJavaProject javaProject, ICompilationUnit cu) {
+		IPackageFragmentRoot root= (IPackageFragmentRoot) cu.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
+		if (root != null) {
+			try {
+				IClasspathEntry resolvedClasspathEntry= root.getResolvedClasspathEntry();
+				for (IClasspathAttribute cpa : resolvedClasspathEntry.getExtraAttributes()) {
+					if (IClasspathAttribute.EXTERNAL_ANNOTATION_PATH.equals(cpa.getName())) {
+						Path annotationPath= new Path(cpa.getValue());
+						IProject project= javaProject.getProject();
+						if (project.exists(annotationPath))
+							return true;
+						IWorkspaceRoot wsRoot= project.getWorkspace().getRoot();
+						return wsRoot.exists(annotationPath);
+					}
+				}
+			} catch (JavaModelException jme) {
+				return false;
+			}
+		}
+		return false;
+	}
+
+	private static abstract class ProposalCreator {
+
+		ICompilationUnit fCU;
+
+		ITypeBinding fDeclaringClass;
+
+		String fSelector;
+
+		String fSignature;
+
+		MergeStrategy fMergeStrategy= MergeStrategy.OVERWRITE_ANNOTATIONS;
+
+		ProposalCreator(ICompilationUnit cu, ITypeBinding declaringClass, String selector, String signature) {
+			fCU= cu;
+			fDeclaringClass= declaringClass;
+			fSelector= selector;
+			fSignature= signature;
+		}
+
+		SignatureAnnotationChangeProposal create(String annotatedSignature, String label) {
+			SignatureAnnotationChangeProposal operation= doCreate(annotatedSignature, label);
+			if (!operation.initializeOperation(fCU, fDeclaringClass, fSelector, fSignature, annotatedSignature, label, fMergeStrategy))
+				return null;
+			return operation;
+		}
+
+		abstract SignatureAnnotationChangeProposal doCreate(String annotatedSignature, String label);
+	}
+
+	private static class ReturnProposalCreator extends ProposalCreator {
+
+		ReturnProposalCreator(ICompilationUnit cu, IMethodBinding methodBinding) {
+			super(cu, methodBinding.getDeclaringClass(), methodBinding.getName(), extractGenericSignature(methodBinding));
+		}
+
+		@Override
+		SignatureAnnotationChangeProposal doCreate(String annotatedSignature, String label) {
+			return new ReturnAnnotationRewriteProposal();
+		}
+	}
+
+	private static class ParameterProposalCreator extends ProposalCreator {
+		int fParamIdx;
+
+		ParameterProposalCreator(ICompilationUnit cu, IMethodBinding methodBinding, int paramIdx) {
+			super(cu, methodBinding.getDeclaringClass(), methodBinding.getName(), extractGenericSignature(methodBinding));
+			fParamIdx= paramIdx;
+		}
+
+		@Override
+		SignatureAnnotationChangeProposal doCreate(String annotatedSignature, String label) {
+			return new ParameterAnnotationRewriteProposal(fParamIdx);
+		}
+	}
+
+	/* Create one proposal from each of the three given renderers. */
+	static void createProposalsForType(ICompilationUnit cu, ASTNode type, int offset,
+			TypeRenderer rendererNonNull, TypeRenderer rendererNullable, TypeRenderer rendererRemove,
+			ProposalCreator creator, ArrayList<IJavaCompletionProposal> resultingCollection) {
+		SignatureAnnotationChangeProposal operation;
+		String label;
+		// propose adding @NonNull:
+		label= getAddAnnotationLabel(NullAnnotationsFix.getNonNullAnnotationName(cu, true), type, offset);
+		operation= creator.create(rendererNonNull.getResult(), label);
+		if (operation != null)
+			resultingCollection.add(operation);
+
+		// propose adding @Nullable:
+		label= getAddAnnotationLabel(NullAnnotationsFix.getNullableAnnotationName(cu, true), type, offset);
+		operation= creator.create(rendererNullable.getResult(), label);
+		if (operation != null)
+			resultingCollection.add(operation);
+
+		// propose removing annotation:
+		label= Messages.format(FixMessages.ExternalNullAnnotationChangeProposals_remove_nullness_annotation,
+				new String[] { type2String(type, offset) });
+		operation= creator.create(rendererRemove.getResult(), label);
+		if (operation != null)
+			resultingCollection.add(operation);
+	}
+
+	static String getAddAnnotationLabel(String annotationName, ASTNode type, int offset) {
+		if (type.getNodeType() == ASTNode.ARRAY_TYPE) {
+			// need to assemble special format with annotation attached to the selected dimension:
+			ArrayType arrayType= (ArrayType) type;
+			StringBuilder left= new StringBuilder(arrayType.getElementType().toString());
+			StringBuilder dimsRight= new StringBuilder();
+			@SuppressWarnings("rawtypes")
+			List dimensions= arrayType.dimensions();
+			for (int i= 0; i < dimensions.size(); i++) {
+				Dimension dimension= (Dimension) dimensions.get(i);
+				if (dimension.getStartPosition() + dimension.getLength() <= offset)
+					left.append("[]"); //$NON-NLS-1$
+				else
+					dimsRight.append("[]"); //$NON-NLS-1$
+			}
+			return Messages.format(FixMessages.ExternalNullAnnotationChangeProposals_add_nullness_array_annotation,
+					new String[] { left.toString(), annotationName, dimsRight.toString() });
+		}
+		return Messages.format(FixMessages.ExternalNullAnnotationChangeProposals_add_nullness_annotation,
+				new String[] { annotationName, type.toString() });
+	}
+
+	static String type2String(ASTNode type, int offset) {
+		if (type.getNodeType() == ASTNode.ARRAY_TYPE) {
+			ArrayType arrayType= (ArrayType) type;
+			StringBuilder buf= new StringBuilder(arrayType.getElementType().toString());
+			@SuppressWarnings("rawtypes")
+			List dimensions= arrayType.dimensions();
+			for (int i= 0; i < dimensions.size(); i++) {
+				Dimension dimension= (Dimension) dimensions.get(i);
+				if (dimension.getStartPosition() + dimension.getLength() > offset)
+					buf.append("[]"); //$NON-NLS-1$
+			}
+			return buf.toString();
+		}
+		return type.toString();
+	}
+
+	/**
+	 * A visitor that renders an AST snippet representing a type or type parameter. For rendering
+	 * the Eclipse External Annotation format is used, i.e., class file signatures with additions
+	 * for null annotations.
+	 * <p>
+	 * In particular a given null annotation is inserted for the given focusType.
+	 * </p>
+	 */
+	static class TypeRenderer extends ASTVisitor {
+
+		StringBuffer fBuffer;
+
+		ASTNode fFocusType; // Type or TypeParameter
+
+		int fOffset;
+
+		char fAnnotation;
+
+		public TypeRenderer(ASTNode focusType, int offset, char annotation) {
+			fBuffer= new StringBuffer();
+			fFocusType= focusType;
+			fOffset= offset;
+			fAnnotation= annotation;
+		}
+
+		public String getResult() {
+			return fBuffer.toString();
+		}
+
+		/* Renders a type parameter list in angle brackets. */
+		public void visitTypeParameters(@SuppressWarnings("rawtypes") List parameters) {
+			fBuffer.append('<');
+			for (Object p : parameters)
+				((TypeParameter) p).accept(this);
+			fBuffer.append('>');
+		}
+
+		@Override
+		public boolean visit(ParameterizedType type) {
+			fBuffer.append('L');
+			if (type == fFocusType || type.getType() == fFocusType)
+				fBuffer.append(fAnnotation);
+			fBuffer.append(binaryName(type.resolveBinding()));
+			fBuffer.append('<');
+			for (Object arg : type.typeArguments())
+				((Type) arg).accept(this);
+			fBuffer.append('>');
+			fBuffer.append(';');
+			return false;
+		}
+
+		@Override
+		public boolean visit(WildcardType wildcard) {
+			Type bound= wildcard.getBound();
+			if (bound == null) {
+				fBuffer.append('*');
+			} else if (wildcard.isUpperBound()) {
+				fBuffer.append('+');
+			} else {
+				fBuffer.append('-');
+			}
+			if (wildcard == fFocusType)
+				fBuffer.append(fAnnotation);
+			if (bound != null)
+				bound.accept(this);
+			return false;
+		}
+
+		@Override
+		public boolean visit(ArrayType array) {
+			@SuppressWarnings("rawtypes")
+			List dimensions= array.dimensions();
+			boolean annotated= false;
+			for (int i= 0; i < dimensions.size(); i++) {
+				fBuffer.append('[');
+				Dimension dimension= (Dimension) dimensions.get(i);
+				if (!annotated && array == fFocusType && dimension.getStartPosition() + dimension.getLength() > fOffset) {
+					fBuffer.append(fAnnotation);
+					annotated= true;
+				}
+			}
+			array.getElementType().accept(this);
+			return false;
+		}
+
+		@Override
+		public boolean visit(TypeParameter parameter) {
+			if (parameter == fFocusType)
+				fBuffer.append(fAnnotation);
+			fBuffer.append(parameter.getName().getIdentifier());
+			Type classBound= null;
+			for (Object bound : parameter.typeBounds()) {
+				Type typeBound= (Type) bound;
+				if (typeBound.resolveBinding().isClass()) {
+					classBound= typeBound;
+					break;
+				}
+			}
+			if (classBound != null) {
+				fBuffer.append(':');
+				classBound.accept(this);
+			} else {
+				ITypeBinding typeBinding= parameter.resolveBinding();
+				fBuffer.append(":L").append(binaryName(typeBinding.getSuperclass())).append(';'); //$NON-NLS-1$
+			}
+			for (Object bound : parameter.typeBounds()) {
+				if (bound == classBound)
+					continue;
+				Type typeBound= (Type) bound;
+				fBuffer.append(':');
+				typeBound.accept(this);
+			}
+			return false;
+		}
+
+		@Override
+		public boolean visit(SimpleType type) {
+			ITypeBinding typeBinding= type.resolveBinding();
+			if (typeBinding.isTypeVariable()) {
+				fBuffer.append('T');
+				if (fFocusType == type)
+					fBuffer.append(fAnnotation);
+				fBuffer.append(typeBinding.getName()).append(';');
+			} else {
+				fBuffer.append('L');
+				if (fFocusType == type)
+					fBuffer.append(fAnnotation);
+				fBuffer.append(binaryName(typeBinding)).append(';');
+			}
+			return false;
+		}
+
+		@Override
+		public boolean visit(PrimitiveType node) {
+			// not a legal focus type, but could be array element type
+			fBuffer.append(node.resolveBinding().getBinaryName());
+			return false;
+		}
+
+		String binaryName(ITypeBinding type) {
+			return type.getBinaryName().replace('.', '/');
+		}
+	}
+}
diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java
index b4a62fd..3153d10 100644
--- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java
+++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2014 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -10,6 +10,7 @@
  *     Stephan Herrmann - Contributions for
  *								[quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977
  *								[quick fix] The fix change parameter type to @Nonnull generated a null change - https://bugs.eclipse.org/400668
+ *								[null] "Annotate" proposals for adding external null annotations to library classes - https://bugs.eclipse.org/458200
  *******************************************************************************/
 package org.eclipse.jdt.internal.corext.fix;
 
@@ -130,6 +131,10 @@
 	public static String NullAnnotationsRewriteOperations_change_overridden_return_nullness;
 	public static String NullAnnotationsRewriteOperations_remove_redundant_nullness_annotation;
 
+	public static String ExternalNullAnnotationChangeProposals_add_nullness_annotation;
+	public static String ExternalNullAnnotationChangeProposals_add_nullness_array_annotation;
+	public static String ExternalNullAnnotationChangeProposals_remove_nullness_annotation;
+
 	public static String ExtractToNullCheckedLocalProposal_extractCheckedLocal_editName;
 	public static String ExtractToNullCheckedLocalProposal_extractToCheckedLocal_proposalName;
 	public static String ExtractToNullCheckedLocalProposal_todoHandleNullDescription;
diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties
index e1c4e3d..703f175 100644
--- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties
+++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties
@@ -1,5 +1,5 @@
 ###############################################################################
-# Copyright (c) 2005, 2014 IBM Corporation and others.
+# Copyright (c) 2005, 2015 IBM Corporation and others.
 # All rights reserved. This program and the accompanying materials
 # are made available under the terms of the Eclipse Public License v1.0
 # which accompanies this distribution, and is available at
@@ -9,7 +9,8 @@
 #     IBM Corporation - initial API and implementation
 #     Stephan Herrmann - Contributions for
 #								[quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977
-#								[quick fix] The fix change parameter type to @Nonnull generated a null change - https://bugs.eclipse.org/400668 
+#								[quick fix] The fix change parameter type to @Nonnull generated a null change - https://bugs.eclipse.org/400668
+#								[null] "Annotate" proposals for adding external null annotations to library classes - https://bugs.eclipse.org/458200
 ###############################################################################
 CleanUpRefactoring_Refactoring_name=Clean Up
 CleanUpRefactoring_Initialize_message=Checking preconditions for project ''{0}''
@@ -117,6 +118,10 @@
 
 NullAnnotationsRewriteOperations_remove_redundant_nullness_annotation=Remove redundant nullness annotation
 
+ExternalNullAnnotationChangeProposals_add_nullness_annotation=Annotate as ''@{0} {1}''
+ExternalNullAnnotationChangeProposals_add_nullness_array_annotation=Annotate as ''{0} @{1} {2}''
+ExternalNullAnnotationChangeProposals_remove_nullness_annotation=Remove nullness annotation from type ''{0}''
+
 ExtractToNullCheckedLocalProposal_extractCheckedLocal_editName=Extract checked local
 ExtractToNullCheckedLocalProposal_extractToCheckedLocal_proposalName=Extract to checked local variable
 ExtractToNullCheckedLocalProposal_todoHandleNullDescription=handle null value
diff --git a/org.eclipse.jdt.ui/plugin.properties b/org.eclipse.jdt.ui/plugin.properties
index 5ba320d..b506a09 100644
--- a/org.eclipse.jdt.ui/plugin.properties
+++ b/org.eclipse.jdt.ui/plugin.properties
@@ -480,6 +480,9 @@
 
 IndentAction.label= Correct &Indentation
 
+annotateClassFile.label= Annotate Class File
+annotateClassFile.description= Externally add Annotations to a Class File.
+
 ##########################################################################
 # Javadoc Support
 ##########################################################################
@@ -611,6 +614,9 @@
 context.editingJavaSource.name= Editing Java Source
 context.editingJavaSource.description= Editing Java Source Context
 
+context.browsingJavaSource.name= Browsing attached Java Source
+context.browsingJavaSource.description= Browsing attached Java Source Context
+
 context.editingPropertiesSource.name= Editing Properties Files
 context.editingPropertiesSource.description= Editing Properties Files Context
 
diff --git a/org.eclipse.jdt.ui/plugin.xml b/org.eclipse.jdt.ui/plugin.xml
index dd19b6f..79c074d 100644
--- a/org.eclipse.jdt.ui/plugin.xml
+++ b/org.eclipse.jdt.ui/plugin.xml
@@ -3360,6 +3360,12 @@
             id="org.eclipse.jdt.ui.javaEditorScope">
       </context>
       <context
+            name="%context.browsingJavaSource.name"
+            description="%context.browsingJavaSource.description"
+            parentId="org.eclipse.ui.textEditorScope"
+            id="org.eclipse.jdt.ui.classFileEditorScope">
+      </context>
+      <context
             name="%context.editingPropertiesSource.name"
             description="%context.editingPropertiesSource.description"
             parentId="org.eclipse.ui.textEditorScope"
@@ -4186,6 +4192,13 @@
             categoryId="org.eclipse.search.ui.category.search"
             id="org.eclipse.jdt.ui.edit.text.java.search.implement.occurrences">
       </command>
+<!-- annotate class file: -->
+	  <command
+	        name="%annotateClassFile.label"
+	        description="%annotateClassFile.description"
+	        categoryId="org.eclipse.jdt.ui.category.source"
+	        id="org.eclipse.jdt.ui.edit.text.java.annotate.classFile">
+	  </command>
    </extension>
    
    <extension
@@ -4793,6 +4806,12 @@
 			value="org.eclipse.jdt.ui.JavadocView">
 		</parameter>
 	</key>
+	<key
+       commandId="org.eclipse.jdt.ui.edit.text.java.annotate.classFile"
+       contextId="org.eclipse.jdt.ui.classFileEditorScope"
+       schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+       sequence="M1+1">
+	</key>
 	</extension>
    
    
diff --git a/org.eclipse.jdt.ui/pom.xml b/org.eclipse.jdt.ui/pom.xml
index b063295..dc2ad3d 100644
--- a/org.eclipse.jdt.ui/pom.xml
+++ b/org.eclipse.jdt.ui/pom.xml
@@ -18,6 +18,6 @@
   </parent>
   <groupId>org.eclipse.jdt</groupId>
   <artifactId>org.eclipse.jdt.ui</artifactId>
-  <version>3.10.100-SNAPSHOT</version>
+  <version>3.11.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/AnnotateClassFileAction.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/AnnotateClassFileAction.java
new file mode 100644
index 0000000..34e5862
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/AnnotateClassFileAction.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2015 GK Software AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.javaeditor;
+
+import org.eclipse.jface.action.Action;
+
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.SourceViewer;
+
+/**
+ * Action to add external null annotations from a ClassFileEditor.
+ * 
+ * @see <a href="https://bugs.eclipse.org/458201">[null] Offer new command "Annotate" on
+ *      ClassFileEditor</a>
+ * @since 3.11
+ */
+public class AnnotateClassFileAction extends Action {
+
+	private final ClassFileEditor fEditor;
+
+	protected AnnotateClassFileAction(ClassFileEditor editor) {
+		super(JavaEditorMessages.AnnotateClassFile_label);
+		fEditor= editor;
+	}
+
+	@Override
+	public void run() {
+		ISourceViewer viewer= fEditor.getViewer();
+		if (viewer instanceof SourceViewer) {
+			SourceViewer sourceViewer= (SourceViewer) viewer;
+			if (sourceViewer.canDoOperation(JavaSourceViewer.ANNOTATE_CLASS_FILE))
+				sourceViewer.doOperation(JavaSourceViewer.ANNOTATE_CLASS_FILE);
+		}
+	}
+}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java
index eccd345..47ac163 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2012 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Stephan Herrmann - Contribution for Bug 458201 - Offer new command "Annotate" on ClassFileEditor
  *******************************************************************************/
 package org.eclipse.jdt.internal.ui.javaeditor;
 
@@ -54,6 +55,7 @@
 import org.eclipse.jface.util.PropertyChangeEvent;
 
 import org.eclipse.jface.text.IWidgetTokenKeeper;
+import org.eclipse.jface.text.source.IOverviewRuler;
 import org.eclipse.jface.text.source.ISourceViewer;
 import org.eclipse.jface.text.source.IVerticalRuler;
 
@@ -64,6 +66,7 @@
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.actions.ActionContext;
 import org.eclipse.ui.actions.ActionGroup;
+import org.eclipse.ui.navigator.ICommonMenuConstants;
 
 import org.eclipse.ui.texteditor.IDocumentProvider;
 import org.eclipse.ui.texteditor.ITextEditorActionConstants;
@@ -89,6 +92,7 @@
 
 import org.eclipse.jdt.ui.JavaElementLabels;
 import org.eclipse.jdt.ui.SharedASTProvider;
+import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds;
 import org.eclipse.jdt.ui.actions.RefactorActionGroup;
 import org.eclipse.jdt.ui.wizards.BuildPathDialogAccess;
 
@@ -547,6 +551,10 @@
 		fActionGroups.addGroup(group);
 		fContextMenuGroup= new CompositeActionGroup(new ActionGroup[] {group});
 
+		Action action= new AnnotateClassFileAction(this);
+		action.setActionDefinitionId(IJavaEditorActionDefinitionIds.ANNOTATE_CLASS_FILE);
+		setAction(IJavaEditorActionDefinitionIds.ANNOTATE_CLASS_FILE, action);
+
 		/*
 		 * 1GF82PL: ITPJUI:ALL - Need to be able to add bookmark to class file
 		 *
@@ -566,6 +574,9 @@
 	public void editorContextMenuAboutToShow(IMenuManager menu) {
 		super.editorContextMenuAboutToShow(menu);
 
+		IAction action = getAction(IJavaEditorActionDefinitionIds.ANNOTATE_CLASS_FILE);
+		menu.appendToGroup(ICommonMenuConstants.GROUP_EDIT, action);
+
 		ActionContext context= new ActionContext(getSelectionProvider().getSelection());
 		fContextMenuGroup.setContext(context);
 		fContextMenuGroup.fillContextMenu(menu);
@@ -573,6 +584,14 @@
 	}
 
 	/*
+	 * @see org.eclipse.ui.texteditor.AbstractDecoratedTextEditor#initializeKeyBindingScopes()
+	 */
+	@Override
+	protected void initializeKeyBindingScopes() {
+		setKeyBindingScopes(new String[] { "org.eclipse.jdt.ui.javaEditorScope", "org.eclipse.jdt.ui.classFileEditorScope" });  //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	/*
 	 * @see JavaEditor#getElementAt(int)
 	 */
 	@Override
@@ -882,8 +901,9 @@
 	/*
 	 * @see JavaEditor#createJavaSourceViewer(Composite, IVerticalRuler, int)
 	 */
-	protected ISourceViewer createJavaSourceViewer(Composite parent, IVerticalRuler ruler, int styles, IPreferenceStore store) {
-		return new JavaSourceViewer(parent, ruler, null, false, styles, store) {
+	@Override
+	protected ISourceViewer createJavaSourceViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, boolean isOverviewRulerVisible, int styles, IPreferenceStore store) {
+		return new JavaSourceViewer(parent, ruler, overviewRuler, isOverviewRulerVisible, styles, store) {
 
 			@Override
 			public boolean requestWidgetToken(IWidgetTokenKeeper requester) {
@@ -898,6 +918,25 @@
 					return false;
 				return super.requestWidgetToken(requester, priority);
 			}
+			
+			@Override
+			public boolean canDoOperation(int operation) {
+				if (operation == JavaSourceViewer.ANNOTATE_CLASS_FILE)
+					return true;
+				return super.canDoOperation(operation);
+			}
+
+			@Override
+			public void doOperation(int operation) {
+				if (operation == JavaSourceViewer.ANNOTATE_CLASS_FILE) {
+					fQuickAssistAssistant.setStatusLineVisible(true);
+					fQuickAssistAssistant.setStatusMessage(JavaEditorMessages.ClassFileEditor_changeExternalAnnotations_caption + ' ');
+					String msg= fQuickAssistAssistant.showPossibleQuickAssists();
+					setStatusLineErrorMessage(msg);
+					return;
+				}
+				super.doOperation(operation);
+			}
 		};
 	}
 
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.java
index 36a54f6..02df16f 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2013 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -10,6 +10,7 @@
  *     Andre Soereng <andreis@fast.no> - [syntax highlighting] highlight numbers - https://bugs.eclipse.org/bugs/show_bug.cgi?id=63573
  *     Björn Michael <b.michael@gmx.de> - [syntax highlighting] Syntax coloring for abstract classes - https://bugs.eclipse.org/331311
  *     Björn Michael <b.michael@gmx.de> - [syntax highlighting] Add highlight for inherited fields - https://bugs.eclipse.org/348368
+ *     Stephan Herrmann - Offer new command "Annotate" on ClassFileEditor - https://bugs.eclipse.org/458201
  *******************************************************************************/
 package org.eclipse.jdt.internal.ui.javaeditor;
 
@@ -48,6 +49,7 @@
 	public static String AddImportOnSelection_error_title;
 	public static String AddImportOnSelection_dialog_title;
 	public static String AddImportOnSelection_dialog_message;
+	public static String ClassFileEditor_changeExternalAnnotations_caption;
 	public static String ClassFileEditor_error_classfile_not_on_classpath;
 	public static String ClassFileEditor_error_invalid_input_message;
 	public static String ClassFileEditor_error_title;
@@ -165,6 +167,7 @@
 	public static String JavaElementReturnTypeHyperlink_error_msg;
 	public static String JavaElementSuperImplementationHyperlink_hyperlinkText;
 	public static String JavaElementSuperImplementationHyperlink_hyperlinkText_qualified;
+	public static String AnnotateClassFile_label;
 
 	static {
 		NLS.initializeMessages(BUNDLE_NAME, JavaEditorMessages.class);
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.properties b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.properties
index 49a40f1..235b327 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.properties
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaEditorMessages.properties
@@ -10,6 +10,7 @@
 #     Andre Soereng <andreis@fast.no> - [syntax highlighting] highlight numbers - https://bugs.eclipse.org/bugs/show_bug.cgi?id=63573
 #     Bjoern Michael <b.michael@gmx.de> - [syntax highlighting] Syntax coloring for abstract classes - https://bugs.eclipse.org/331311
 #     Bjoern Michael <b.michael@gmx.de> - [syntax highlighting] Add highlight for inherited fields - https://bugs.eclipse.org/348368
+#     Stephan Herrmann - Offer new command "Annotate" on ClassFileEditor - https://bugs.eclipse.org/458201
 ###############################################################################
 
 AddImportOnSelection_label=A&dd Import
@@ -21,6 +22,7 @@
 AddImportOnSelection_dialog_title=Add Import
 AddImportOnSelection_dialog_message=&Choose element to generate an import statement for:
 
+ClassFileEditor_changeExternalAnnotations_caption=Change external annotations
 ClassFileEditor_error_classfile_not_on_classpath=The class file is not on the classpath
 ClassFileEditor_SourceAttachmentForm_cannotconfigure=The JAR of this class file belongs to container ''{0}'' can not be configured.
 ClassFileEditor_SourceAttachmentForm_notsupported=The JAR of this class file belongs to container ''{0}'' which does not support the attachment of sources to its entries.
@@ -157,3 +159,5 @@
 Editor_OpenPropertiesFile_hyperlinkText= Open in ''{0}''
 
 Editor_MoveLines_IllegalMove_status= Move not possible - Uncheck "Show Source of Selected Element Only" to see the entire document
+
+AnnotateClassFile_label=&Annotate
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaSourceViewer.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaSourceViewer.java
index ba418b6..9c8bf2d 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaSourceViewer.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/JavaSourceViewer.java
@@ -83,6 +83,11 @@
 	 */
 	public static final int SHOW_HIERARCHY= 53;
 
+	/**
+	 * Operation code for the annotate class file action.
+	 */
+	public static final int ANNOTATE_CLASS_FILE= 54;
+
 	private IInformationPresenter fOutlinePresenter;
 	private IInformationPresenter fStructurePresenter;
 	private IInformationPresenter fHierarchyPresenter;
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/ExternalNullAnnotationQuickAssistProcessor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/ExternalNullAnnotationQuickAssistProcessor.java
new file mode 100644
index 0000000..bf40680
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/ExternalNullAnnotationQuickAssistProcessor.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Copyright (c) 2015 GK Software AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Stephan Herrmann - Initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.ui.text.correction;
+
+import java.util.ArrayList;
+
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.ISourceViewer;
+
+import org.eclipse.ui.IEditorPart;
+
+import org.eclipse.jdt.core.IClassFile;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.WorkingCopyOwner;
+
+import org.eclipse.jdt.internal.corext.fix.ExternalNullAnnoatationChangeProposals;
+
+import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
+
+import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.javaeditor.IClassFileEditorInput;
+
+
+/**
+ * Alternate, stripped down quick assist processor for the annotate class file action.
+ * 
+ * @see <a href="https://bugs.eclipse.org/458200">[null] "Annotate" proposals for adding external
+ *      null annotations to library classes</a>
+ * @since 3.11
+ */
+public class ExternalNullAnnotationQuickAssistProcessor implements IQuickAssistProcessor {
+
+	public static final String ANNOTATE_MEMBER_ID= "org.eclipse.jdt.ui.annotate.nullAnnotateMember.assist"; //$NON-NLS-1$
+
+	private JavaCorrectionAssistant fAssistant;
+
+	private String fErrorMessage;
+
+	public ExternalNullAnnotationQuickAssistProcessor(JavaCorrectionAssistant javaCorrectionAssistant) {
+		fAssistant= javaCorrectionAssistant;
+	}
+
+	public String getErrorMessage() {
+		return fErrorMessage;
+	}
+
+	public boolean canFix(Annotation annotation) {
+		return false; // irrelevant on class files
+	}
+
+	public boolean canAssist(IQuickAssistInvocationContext invocationContext) {
+		IEditorPart part= fAssistant.getEditor();
+		IClassFile classFile= ((IClassFileEditorInput) part.getEditorInput()).getClassFile();
+		return classFile.getJavaProject().getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true).equals(JavaCore.ENABLED);
+	}
+
+	public ICompletionProposal[] computeQuickAssistProposals(IQuickAssistInvocationContext quickAssistContext) {
+		ICompilationUnit cu= null;
+		try {
+			// preparation similar to JavaCorrectionProcessor:
+			IEditorPart part= fAssistant.getEditor();
+
+			IClassFile classFile= ((IClassFileEditorInput) part.getEditorInput()).getClassFile();
+			cu= classFile.getWorkingCopy((WorkingCopyOwner) null, null);
+			if (cu == null || classFile.getSource() == null)
+				return null;
+
+			ISourceViewer viewer= quickAssistContext.getSourceViewer();
+			int documentOffset= quickAssistContext.getOffset();
+
+			AssistContext context= null;
+			int length= viewer != null ? viewer.getSelectedRange().y : 0;
+			context= new AssistContext(cu, viewer, part, documentOffset, length);
+
+			ArrayList<IJavaCompletionProposal> proposals= new ArrayList<IJavaCompletionProposal>();
+
+			// shortcut (we only have one processor):
+			ExternalNullAnnoatationChangeProposals.collectExternalAnnotationProposals(context.getCompilationUnit(),
+					context.getCoveringNode(), documentOffset, proposals);
+
+			return proposals.toArray(new IJavaCompletionProposal[proposals.size()]);
+
+		} catch (JavaModelException e) {
+			fErrorMessage= e.getMessage();
+			JavaPlugin.log(e);
+			return null;
+		} finally {
+			try {
+				if (cu != null)
+					cu.discardWorkingCopy();
+			} catch (JavaModelException e) {
+				// ignore
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/JavaCorrectionAssistant.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/JavaCorrectionAssistant.java
index 10ce6e0..4376cc2 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/JavaCorrectionAssistant.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/JavaCorrectionAssistant.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2014 IBM Corporation and others.
+ * Copyright (c) 2000, 2015 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Stephan Herrmann - Offer new command "Annotate" on ClassFileEditor - https://bugs.eclipse.org/458201
  *******************************************************************************/
 package org.eclipse.jdt.internal.ui.text.correction;
 
@@ -30,6 +31,7 @@
 import org.eclipse.jface.text.contentassist.ICompletionListener;
 import org.eclipse.jface.text.contentassist.ICompletionProposal;
 import org.eclipse.jface.text.quickassist.IQuickAssistAssistant;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
 import org.eclipse.jface.text.quickassist.QuickAssistAssistant;
 import org.eclipse.jface.text.source.Annotation;
 import org.eclipse.jface.text.source.IAnnotationModel;
@@ -49,6 +51,7 @@
 import org.eclipse.jdt.ui.SharedASTProvider;
 
 import org.eclipse.jdt.internal.ui.JavaPlugin;
+import org.eclipse.jdt.internal.ui.javaeditor.ClassFileEditor;
 
 
 public class JavaCorrectionAssistant extends QuickAssistAssistant {
@@ -72,7 +75,11 @@
 		Assert.isNotNull(editor);
 		fEditor= editor;
 
-		JavaCorrectionProcessor processor= new JavaCorrectionProcessor(this);
+		IQuickAssistProcessor processor;
+		if (editor instanceof ClassFileEditor)
+			processor= new ExternalNullAnnotationQuickAssistProcessor(this);
+		else
+			processor= new JavaCorrectionProcessor(this);
 
 		setQuickAssistProcessor(processor);
 		enableColoredLabels(PlatformUI.getPreferenceStore().getBoolean(IWorkbenchPreferenceConstants.USE_COLORED_LABELS));
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/ui/actions/IJavaEditorActionDefinitionIds.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/ui/actions/IJavaEditorActionDefinitionIds.java
index 997ed24..d2ae3cb 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/ui/actions/IJavaEditorActionDefinitionIds.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/ui/actions/IJavaEditorActionDefinitionIds.java
@@ -759,4 +759,11 @@
 	 * @since 3.2
 	 */
 	public static final String CLEAN_UP= "org.eclipse.jdt.ui.edit.text.java.clean.up"; //$NON-NLS-1$
+
+	/**
+	 * Action definition id of the annotate class file action
+	 * (value: <code>"org.eclipse.jdt.ui.edit.text.java.annotate.classFile"</code>).
+	 * @since 3.11
+	 */
+	public static final String ANNOTATE_CLASS_FILE= "org.eclipse.jdt.ui.edit.text.java.annotate.classFile"; //$NON-NLS-1$
 }