495085: connector discovery should tolerate servers that return 403 for
non-existent resources

Discovery looks for any of "content.jar", "content.xml",
"compositeContent.jar", "compositeContent.xml", "site.xml" to determine
if an update site is valid. It assumes it will get a 404 for resources
that don't exist. But it is common for servers (e.g. AWS) to use 403 in
that case. It should succeed if any of the resources exist, regardless
of the error that occurs accessing the others.

Change-Id: I2c4e0a2000e59672071ce8f30822ad54110cb763
Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=495085
diff --git a/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/model/ConnectorDiscovery.java b/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/model/ConnectorDiscovery.java
index 1c92fdd..2e73ddf 100644
--- a/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/model/ConnectorDiscovery.java
+++ b/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/model/ConnectorDiscovery.java
@@ -4,13 +4,15 @@
  * 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:
  *     Tasktop Technologies - initial API and implementation
  *******************************************************************************/
 package org.eclipse.mylyn.internal.discovery.core.model;
 
+import java.net.MalformedURLException;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -53,7 +55,7 @@
 
 /**
  * A means of discovering connectors.
- * 
+ *
  * @author David Green
  */
 public class ConnectorDiscovery {
@@ -87,7 +89,7 @@
 	/**
 	 * Initialize this by performing discovery. Discovery may take a long time as it involves network access.
 	 * PRECONDITION: must add at least one {@link #getDiscoveryStrategies() discovery strategy} prior to calling.
-	 * 
+	 *
 	 * @return
 	 */
 	public IStatus performDiscovery(IProgressMonitor monitor) {
@@ -111,12 +113,13 @@
 				discoveryStrategy.setConnectors(connectors);
 				discoveryStrategy.setCertifications(certifications);
 				try {
-					discoveryStrategy.performDiscovery(new SubProgressMonitor(monitor, discoveryTicks
-							/ discoveryStrategies.size()));
+					discoveryStrategy.performDiscovery(
+							new SubProgressMonitor(monitor, discoveryTicks / discoveryStrategies.size()));
 				} catch (CoreException e) {
-					status.add(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
-							Messages.ConnectorDiscovery_Strategy_failed_Error, discoveryStrategy.getClass()
-									.getSimpleName()), e));
+					status.add(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
+							NLS.bind(Messages.ConnectorDiscovery_Strategy_failed_Error,
+									discoveryStrategy.getClass().getSimpleName()),
+							e));
 				}
 			}
 
@@ -134,7 +137,7 @@
 
 	/**
 	 * get the top-level categories
-	 * 
+	 *
 	 * @return the categories, or an empty list if there are none.
 	 */
 	public List<DiscoveryCategory> getCategories() {
@@ -143,7 +146,7 @@
 
 	/**
 	 * get the connectors that were discovered and not filtered
-	 * 
+	 *
 	 * @return the connectors, or an empty list if there are none.
 	 */
 	public List<DiscoveryConnector> getConnectors() {
@@ -152,7 +155,7 @@
 
 	/**
 	 * get the connectors that were discovered but filtered
-	 * 
+	 *
 	 * @return the filtered connectors, or an empty list if there were none.
 	 */
 	public List<DiscoveryConnector> getFilteredConnectors() {
@@ -180,7 +183,7 @@
 
 	/**
 	 * indicate if update site availability should be verified. The default is false.
-	 * 
+	 *
 	 * @see DiscoveryConnector#getAvailable()
 	 * @see #verifySiteAvailability(IProgressMonitor)
 	 */
@@ -190,7 +193,7 @@
 
 	/**
 	 * indicate if update site availability should be verified. The default is false.
-	 * 
+	 *
 	 * @see DiscoveryConnector#getAvailable()
 	 * @see #verifySiteAvailability(IProgressMonitor)
 	 */
@@ -219,10 +222,10 @@
 		for (DiscoveryCertification certification : certifications) {
 			DiscoveryCertification previous = idToCertification.put(certification.getId(), certification);
 			if (previous != null) {
-				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
-						"Duplicate certification id ''{0}'': declaring sources: {1}, {2}", //$NON-NLS-1$
-						new Object[] { certification.getId(), certification.getSource().getId(),
-								previous.getSource().getId() })));
+				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
+						NLS.bind("Duplicate certification id ''{0}'': declaring sources: {1}, {2}", //$NON-NLS-1$
+								new Object[] { certification.getId(), certification.getSource().getId(),
+										previous.getSource().getId() })));
 			}
 		}
 
@@ -232,9 +235,10 @@
 				if (certification != null) {
 					connector.setCertification(certification);
 				} else {
-					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
-							"Unknown category ''{0}'' referenced by connector ''{1}'' declared in {2}", new Object[] { //$NON-NLS-1$
-							connector.getCertificationId(), connector.getId(), connector.getSource().getId() })));
+					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
+							NLS.bind("Unknown category ''{0}'' referenced by connector ''{1}'' declared in {2}", //$NON-NLS-1$
+									new Object[] { connector.getCertificationId(), connector.getId(),
+											connector.getSource().getId() })));
 				}
 			}
 		}
@@ -245,8 +249,8 @@
 		for (DiscoveryCategory category : categories) {
 			DiscoveryCategory previous = idToCategory.put(category.getId(), category);
 			if (previous != null) {
-				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
-						Messages.ConnectorDiscovery_duplicate_category_id, new Object[] { category.getId(),
+				StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
+						NLS.bind(Messages.ConnectorDiscovery_duplicate_category_id, new Object[] { category.getId(),
 								category.getSource().getId(), previous.getSource().getId() })));
 			}
 		}
@@ -276,10 +280,10 @@
 					Filter filter = FrameworkUtil.createFilter(connector.getPlatformFilter());
 					match = filter.match((Dictionary) environment);
 				} catch (InvalidSyntaxException e) {
-					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, NLS.bind(
-							Messages.ConnectorDiscovery_illegal_filter_syntax,
-							new Object[] { connector.getPlatformFilter(), connector.getId(),
-									connector.getSource().getId() })));
+					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
+							NLS.bind(Messages.ConnectorDiscovery_illegal_filter_syntax,
+									new Object[] { connector.getPlatformFilter(), connector.getId(),
+											connector.getSource().getId() })));
 				}
 				if (!match) {
 					connectors.remove(connector);
@@ -379,13 +383,8 @@
 								monitor.setCanceled(true);
 								return;
 							}
-							IStatus status;
-							if (e.getCause() instanceof CoreException) {
-								status = ((CoreException) e.getCause()).getStatus();
-							} else {
-								status = new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
-										Messages.ConnectorDiscovery_unexpected_exception, e.getCause());
-							}
+							IStatus status = new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
+									Messages.ConnectorDiscovery_unexpected_exception, e.getCause());
 							StatusHandler.log(status);
 						}
 						monitor.worked(1);
@@ -409,11 +408,11 @@
 			this.url = url;
 		}
 
-		public VerifyUpdateSiteJob call() throws Exception {
+		public VerifyUpdateSiteJob call() throws MalformedURLException, URISyntaxException {
 			URL baseUrl = new URL(url);
 			List<URI> locations = new ArrayList<URI>();
-			for (String location : new String[] {
-					"content.jar", "content.xml", "compositeContent.jar", "compositeContent.xml", "site.xml" }) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
+			for (String location : new String[] { "content.jar", "content.xml", "compositeContent.jar", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
+					"compositeContent.xml", "site.xml" }) { //$NON-NLS-1$ //$NON-NLS-2$
 				locations.add(new URL(baseUrl, location).toURI());
 			}
 			ok = WebUtil.verifyAvailability(locations, true, new NullProgressMonitor());
@@ -432,7 +431,8 @@
 
 				public void handleException(Throwable exception) {
 					StatusHandler.log(new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN,
-							Messages.ConnectorDiscovery_exception_disposing + strategy.getClass().getName(), exception));
+							Messages.ConnectorDiscovery_exception_disposing + strategy.getClass().getName(),
+							exception));
 				}
 			});
 		}
diff --git a/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/util/WebUtil.java b/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/util/WebUtil.java
index 088d78a..c05845d 100644
--- a/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/util/WebUtil.java
+++ b/org.eclipse.mylyn.discovery.core/src/org/eclipse/mylyn/internal/discovery/core/util/WebUtil.java
@@ -1,178 +1,197 @@
-/*******************************************************************************
- * Copyright (c) 2009, 2013 Tasktop Technologies 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:
- *     Tasktop Technologies - initial API and implementation
- *******************************************************************************/
-package org.eclipse.mylyn.internal.discovery.core.util;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.net.URI;
-import java.util.List;
-
-import org.eclipse.core.runtime.CoreException;
-import org.eclipse.core.runtime.IProgressMonitor;
-import org.eclipse.core.runtime.IStatus;
-import org.eclipse.core.runtime.Platform;
-import org.eclipse.mylyn.commons.net.AbstractWebLocation;
-
-/**
- * A utility for accessing web resources
- * 
- * @author David Green
- */
-public class WebUtil {
-	/**
-	 * implementors are capable of processing character content
-	 * 
-	 * @see WebUtil#readResource(AbstractWebLocation, TextContentProcessor, IProgressMonitor)
-	 */
-	public interface TextContentProcessor {
-		public void process(Reader reader) throws IOException;
-	}
-
-	private static ITransportService transport;
-
-	/**
-	 * Download an HTTP-based resource
-	 * 
-	 * @param target
-	 *            the target file to which the content is saved
-	 * @param location
-	 *            the web location of the content
-	 * @param monitor
-	 *            the monitor
-	 * @return
-	 * @throws IOException
-	 *             if a network or IO problem occurs
-	 */
-	public static IStatus download(URI uri, File target, IProgressMonitor monitor) throws IOException {
-		IStatus result;
-		OutputStream out = new BufferedOutputStream(new FileOutputStream(target));
-		try {
-			result = download(uri, out, monitor);
-		} finally {
-			out.close();
-		}
-		if (!result.isOK()) {
-			target.delete();
-			if (result.getException() instanceof IOException) {
-				throw (IOException) result.getException();
-			}
-			throw new IOWithCauseException(result.getException());
-		}
-		return result;
-	}
-
-	/**
-	 * Read a web-based resource at the specified location using the given processor.
-	 * 
-	 * @param location
-	 *            the web location of the content
-	 * @param processor
-	 *            the processor that will handle content
-	 * @param monitor
-	 *            the monitor
-	 * @throws IOException
-	 *             if a network or IO problem occurs
-	 * @throws CoreException
-	 */
-	public static void readResource(URI uri, TextContentProcessor processor, IProgressMonitor monitor)
-			throws IOException, CoreException {
-		InputStream in = stream(uri, monitor);
-		try {
-			// FIXME how can the charset be determined?
-			BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); //$NON-NLS-1$
-			processor.process(reader);
-		} finally {
-			in.close();
-		}
-	}
-
-	/**
-	 * Verify availability of resources at the given web locations. Normally this would be done using an HTTP HEAD.
-	 * 
-	 * @param locations
-	 *            the locations of the resource to verify
-	 * @param one
-	 *            indicate if only one of the resources must exist
-	 * @param monitor
-	 *            the monitor
-	 * @return true if the resource exists
-	 * @throws CoreException
-	 */
-	public static boolean verifyAvailability(List<? extends URI> locations, boolean one, IProgressMonitor monitor)
-			throws IOException, CoreException {
-		if (locations.isEmpty() || locations.size() > 5) {
-			throw new IllegalArgumentException();
-		}
-		int countFound = 0;
-		for (URI location : locations) {
-			try {
-				getLastModified(location, monitor);
-				if (one) {
-					return true;
-				}
-				++countFound;
-			} catch (FileNotFoundException e) {
-				if (!one) {
-					return false;
-				}
-				continue;
-			}
-		}
-		return countFound == locations.size();
-	}
-
-	public static synchronized ITransportService getTransport() {
-		if (transport == null) {
-			if (Platform.isRunning()) {
-				try {
-					transport = new P2TransportService();
-				} catch (ClassNotFoundException e) {
-					// fall back to HttpClientTransport
-				}
-			}
-			if (transport == null) {
-				transport = new HttpClientTransportService();
-			}
-		}
-		return transport;
-	}
-
-	public static IStatus download(URI uri, OutputStream out, IProgressMonitor monitor) {
-		return getTransport().download(uri, out, monitor);
-	}
-
-	public static InputStream stream(URI uri, IProgressMonitor monitor) throws IOException, CoreException {
-		return getTransport().stream(uri, monitor);
-	}
-
-	private static long getLastModified(URI location, IProgressMonitor monitor) throws CoreException, IOException {
-		return getTransport().getLastModified(location, monitor);
-	}
-
-	public static String getFileNameFor(String bundleUrl) throws IOException {
-		if (bundleUrl.charAt(bundleUrl.length() - 1) == '/') {
-			bundleUrl = bundleUrl.substring(0, bundleUrl.length() - 1);
-		}
-		if (bundleUrl.lastIndexOf('/') != -1) {
-			bundleUrl = bundleUrl.substring(bundleUrl.lastIndexOf('/') + 1);
-		}
-		return bundleUrl.replaceAll("[^a-zA-Z0-9_\\.]", "_"); //$NON-NLS-1$ //$NON-NLS-2$
-	}
-
-}
+/*******************************************************************************

+ * Copyright (c) 2009, 2013 Tasktop Technologies 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:

+ *     Tasktop Technologies - initial API and implementation

+ *******************************************************************************/

+package org.eclipse.mylyn.internal.discovery.core.util;

+

+import java.io.BufferedOutputStream;

+import java.io.BufferedReader;

+import java.io.File;

+import java.io.FileOutputStream;

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.InputStreamReader;

+import java.io.OutputStream;

+import java.io.Reader;

+import java.net.URI;

+import java.util.List;

+

+import org.eclipse.core.runtime.CoreException;

+import org.eclipse.core.runtime.IProgressMonitor;

+import org.eclipse.core.runtime.IStatus;

+import org.eclipse.core.runtime.MultiStatus;

+import org.eclipse.core.runtime.Platform;

+import org.eclipse.core.runtime.Status;

+import org.eclipse.mylyn.commons.core.StatusHandler;

+import org.eclipse.mylyn.commons.net.AbstractWebLocation;

+import org.eclipse.mylyn.internal.discovery.core.DiscoveryCore;

+

+/**

+ * A utility for accessing web resources

+ *

+ * @author David Green

+ */

+public class WebUtil {

+	/**

+	 * implementors are capable of processing character content

+	 *

+	 * @see WebUtil#readResource(AbstractWebLocation, TextContentProcessor, IProgressMonitor)

+	 */

+	public interface TextContentProcessor {

+		public void process(Reader reader) throws IOException;

+	}

+

+	private static ITransportService transport;

+

+	/**

+	 * Download an HTTP-based resource

+	 *

+	 * @param target

+	 *            the target file to which the content is saved

+	 * @param location

+	 *            the web location of the content

+	 * @param monitor

+	 *            the monitor

+	 * @return

+	 * @throws IOException

+	 *             if a network or IO problem occurs

+	 */

+	public static IStatus download(URI uri, File target, IProgressMonitor monitor) throws IOException {

+		IStatus result;

+		OutputStream out = new BufferedOutputStream(new FileOutputStream(target));

+		try {

+			result = download(uri, out, monitor);

+		} finally {

+			out.close();

+		}

+		if (!result.isOK()) {

+			target.delete();

+			if (result.getException() instanceof IOException) {

+				throw (IOException) result.getException();

+			}

+			throw new IOWithCauseException(result.getException());

+		}

+		return result;

+	}

+

+	/**

+	 * Read a web-based resource at the specified location using the given processor.

+	 *

+	 * @param location

+	 *            the web location of the content

+	 * @param processor

+	 *            the processor that will handle content

+	 * @param monitor

+	 *            the monitor

+	 * @throws IOException

+	 *             if a network or IO problem occurs

+	 * @throws CoreException

+	 */

+	public static void readResource(URI uri, TextContentProcessor processor, IProgressMonitor monitor)

+			throws IOException, CoreException {

+		InputStream in = stream(uri, monitor);

+		try {

+			// FIXME how can the charset be determined?

+			BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); //$NON-NLS-1$

+			processor.process(reader);

+		} finally {

+			in.close();

+		}

+	}

+

+	/**

+	 * Verify availability of resources at the given web locations. Normally this would be done using an HTTP HEAD.

+	 *

+	 * @param locations

+	 *            the locations of the resource to verify

+	 * @param one

+	 *            indicate if only one of the resources must exist

+	 * @param monitor

+	 *            the monitor

+	 * @return true if the resource exists

+	 * @throws CoreException

+	 */

+	public static boolean verifyAvailability(List<? extends URI> locations, boolean one, IProgressMonitor monitor) {

+		if (locations.isEmpty() || locations.size() > 5) {

+			throw new IllegalArgumentException();

+		}

+		int countFound = 0;

+		MultiStatus status = new MultiStatus(DiscoveryCore.ID_PLUGIN, 0, "Verifying resource availability failed", //$NON-NLS-1$

+				new Exception());

+		try {

+			for (URI location : locations) {

+				try {

+					getLastModified(location, monitor);

+					if (one) {

+						return true;

+					}

+					++countFound;

+				} catch (IOException | CoreException e) {

+					status.add(getStatus(e));

+					if (!one) {

+						return false;

+					}

+					continue;

+				}

+			}

+		} finally {

+			if (!status.isOK()) {

+				StatusHandler.log(status);

+			}

+

+		}

+		return countFound == locations.size();

+	}

+

+	private static IStatus getStatus(Exception e) {

+		if (e instanceof CoreException) {

+			return ((CoreException) e).getStatus();

+		}

+		return new Status(IStatus.ERROR, DiscoveryCore.ID_PLUGIN, e.getMessage(), e);

+	}

+

+	public static synchronized ITransportService getTransport() {

+		if (transport == null) {

+			if (Platform.isRunning()) {

+				try {

+					transport = new P2TransportService();

+				} catch (ClassNotFoundException e) {

+					// fall back to HttpClientTransport

+				}

+			}

+			if (transport == null) {

+				transport = new HttpClientTransportService();

+			}

+		}

+		return transport;

+	}

+

+	public static IStatus download(URI uri, OutputStream out, IProgressMonitor monitor) {

+		return getTransport().download(uri, out, monitor);

+	}

+

+	public static InputStream stream(URI uri, IProgressMonitor monitor) throws IOException, CoreException {

+		return getTransport().stream(uri, monitor);

+	}

+

+	private static long getLastModified(URI location, IProgressMonitor monitor) throws CoreException, IOException {

+		return getTransport().getLastModified(location, monitor);

+	}

+

+	public static String getFileNameFor(String bundleUrl) throws IOException {

+		if (bundleUrl.charAt(bundleUrl.length() - 1) == '/') {

+			bundleUrl = bundleUrl.substring(0, bundleUrl.length() - 1);

+		}

+		if (bundleUrl.lastIndexOf('/') != -1) {

+			bundleUrl = bundleUrl.substring(bundleUrl.lastIndexOf('/') + 1);

+		}

+		return bundleUrl.replaceAll("[^a-zA-Z0-9_\\.]", "_"); //$NON-NLS-1$ //$NON-NLS-2$

+	}

+

+}