blob: d2e65bfc28a5210420a7096805705a7efe8a2055 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.javadoc;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Map;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.core.resources.IResource;
import org.eclipse.ui.PlatformUI;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModelStatusConstants;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IModularClassFile;
import org.eclipse.jdt.core.IModuleDescription;
import org.eclipse.jdt.core.IOpenable;
import org.eclipse.jdt.core.IOrdinaryClassFile;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.internal.corext.CorextMessages;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.wizards.buildpaths.BuildPathSupport;
import org.eclipse.jdt.internal.ui.wizards.buildpaths.CPListElement;
public class JavaDocLocations {
private static final String JAR_PROTOCOL= "jar"; //$NON-NLS-1$
public static final String ARCHIVE_PREFIX= "jar:"; //$NON-NLS-1$
private static final QualifiedName PROJECT_JAVADOC= new QualifiedName(JavaUI.ID_PLUGIN, "project_javadoc_location"); //$NON-NLS-1$
private static IClasspathEntry getConvertedEntry(IClasspathEntry entry, IJavaProject project, Map<IPath, String> oldLocationMap) {
IPath path= null;
switch (entry.getEntryKind()) {
case IClasspathEntry.CPE_SOURCE:
case IClasspathEntry.CPE_PROJECT:
return null;
case IClasspathEntry.CPE_CONTAINER:
convertContainer(entry, project, oldLocationMap);
return null;
case IClasspathEntry.CPE_LIBRARY:
path= entry.getPath();
break;
case IClasspathEntry.CPE_VARIABLE:
path= JavaCore.getResolvedVariablePath(entry.getPath());
break;
default:
return null;
}
if (path == null) {
return null;
}
IClasspathAttribute[] extraAttributes= entry.getExtraAttributes();
for (int i= 0; i < extraAttributes.length; i++) {
if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME.equals(extraAttributes[i].getName())) {
return null;
}
}
String libraryJavadocLocation= oldLocationMap.get(path);
if (libraryJavadocLocation != null) {
CPListElement element= CPListElement.createFromExisting(entry, project);
element.setAttribute(CPListElement.JAVADOC, libraryJavadocLocation);
return element.getClasspathEntry();
}
return null;
}
private static void convertContainer(IClasspathEntry entry, IJavaProject project, Map<IPath, String> oldLocationMap) {
try {
IClasspathContainer container= JavaCore.getClasspathContainer(entry.getPath(), project);
if (container == null) {
return;
}
IClasspathEntry[] entries= container.getClasspathEntries();
boolean hasChange= false;
for (int i= 0; i < entries.length; i++) {
IClasspathEntry curr= entries[i];
IClasspathEntry updatedEntry= getConvertedEntry(curr, project, oldLocationMap);
if (updatedEntry != null) {
entries[i]= updatedEntry;
hasChange= true;
}
}
if (hasChange) {
BuildPathSupport.requestContainerUpdate(project, container, entries);
}
} catch (CoreException e) {
// ignore
}
}
/**
* Sets the Javadoc location for an archive with the given path.
* @param project the Java project
* @param url the Javadoc location
*/
public static void setProjectJavadocLocation(IJavaProject project, URL url) {
try {
String location= url != null ? url.toExternalForm() : null;
setProjectJavadocLocation(project, location);
} catch (CoreException e) {
JavaPlugin.log(e);
}
}
private static void setProjectJavadocLocation(IJavaProject project, String url) throws CoreException {
project.getProject().setPersistentProperty(PROJECT_JAVADOC, url);
}
public static URL getProjectJavadocLocation(IJavaProject project) {
if (!project.getProject().isAccessible()) {
return null;
}
try {
String prop= project.getProject().getPersistentProperty(PROJECT_JAVADOC);
if (prop == null) {
return null;
}
return parseURL(prop);
} catch (CoreException e) {
JavaPlugin.log(e);
}
return null;
}
public static URL getLibraryJavadocLocation(IClasspathEntry entry) {
if (entry == null) {
throw new IllegalArgumentException("Entry must not be null"); //$NON-NLS-1$
}
int kind= entry.getEntryKind();
if (kind != IClasspathEntry.CPE_LIBRARY && kind != IClasspathEntry.CPE_VARIABLE) {
throw new IllegalArgumentException("Entry must be of kind CPE_LIBRARY or CPE_VARIABLE"); //$NON-NLS-1$
}
IClasspathAttribute[] extraAttributes= entry.getExtraAttributes();
for (int i= 0; i < extraAttributes.length; i++) {
IClasspathAttribute attrib= extraAttributes[i];
if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME.equals(attrib.getName())) {
return parseURL(attrib.getValue());
}
}
return null;
}
public static URL getJavadocBaseLocation(IJavaElement element) throws JavaModelException {
if (element.getElementType() == IJavaElement.JAVA_PROJECT) {
return getProjectJavadocLocation((IJavaProject) element);
}
IPackageFragmentRoot root= JavaModelUtil.getPackageFragmentRoot(element);
if (root == null) {
return null;
}
if (root.getKind() == IPackageFragmentRoot.K_BINARY) {
IClasspathEntry entry= root.getResolvedClasspathEntry();
URL javadocLocation= getLibraryJavadocLocation(entry);
if (javadocLocation != null) {
return getLibraryJavadocLocation(entry);
}
entry= root.getRawClasspathEntry();
switch (entry.getEntryKind()) {
case IClasspathEntry.CPE_LIBRARY:
case IClasspathEntry.CPE_VARIABLE:
return getLibraryJavadocLocation(entry);
default:
return null;
}
} else {
return getProjectJavadocLocation(root.getJavaProject());
}
}
public static URL getJavadocLocation(IJavaElement element, boolean includeMemberReference) throws JavaModelException {
URL baseLocation= getJavadocBaseLocation(element);
if (baseLocation == null) {
return null;
}
String urlString= baseLocation.toExternalForm();
StringBuffer urlBuffer= new StringBuffer(urlString);
if (!urlString.endsWith("/")) { //$NON-NLS-1$
urlBuffer.append('/');
}
StringBuffer pathBuffer= new StringBuffer();
StringBuffer fragmentBuffer= new StringBuffer();
switch (element.getElementType()) {
case IJavaElement.PACKAGE_FRAGMENT:
appendPackageSummaryPath((IPackageFragment) element, pathBuffer);
break;
case IJavaElement.JAVA_PROJECT:
case IJavaElement.PACKAGE_FRAGMENT_ROOT :
appendIndexPath(pathBuffer);
break;
case IJavaElement.IMPORT_CONTAINER :
element= element.getParent();
//$FALL-THROUGH$
case IJavaElement.COMPILATION_UNIT :
IType mainType= ((ICompilationUnit) element).findPrimaryType();
if (mainType == null) {
return null;
}
appendTypePath(mainType, pathBuffer);
break;
case IJavaElement.CLASS_FILE :
if (element instanceof IModularClassFile) {
try {
appendModuleSummaryPath(((IModularClassFile) element).getModule(), pathBuffer);
} catch (JavaModelException e) {
return null;
}
} else {
appendTypePath(((IOrdinaryClassFile) element).getType(), pathBuffer);
}
break;
case IJavaElement.TYPE :
appendTypePath((IType) element, pathBuffer);
break;
case IJavaElement.FIELD :
IField field= (IField) element;
appendTypePath(field.getDeclaringType(), pathBuffer);
if (includeMemberReference) {
appendFieldReference(field, fragmentBuffer);
}
break;
case IJavaElement.METHOD :
IMethod method= (IMethod) element;
appendTypePath(method.getDeclaringType(), pathBuffer);
if (includeMemberReference) {
appendMethodReference(method, fragmentBuffer);
}
break;
case IJavaElement.INITIALIZER :
appendTypePath(((IMember) element).getDeclaringType(), pathBuffer);
break;
case IJavaElement.IMPORT_DECLARATION :
IImportDeclaration decl= (IImportDeclaration) element;
if (decl.isOnDemand()) {
IJavaElement cont= JavaModelUtil.findTypeContainer(element.getJavaProject(), Signature.getQualifier(decl.getElementName()));
if (cont instanceof IType) {
appendTypePath((IType) cont, pathBuffer);
} else if (cont instanceof IPackageFragment) {
appendPackageSummaryPath((IPackageFragment) cont, pathBuffer);
}
} else {
IType imp= element.getJavaProject().findType(decl.getElementName());
appendTypePath(imp, pathBuffer);
}
break;
case IJavaElement.PACKAGE_DECLARATION :
IJavaElement pack= element.getAncestor(IJavaElement.PACKAGE_FRAGMENT);
if (pack != null) {
appendPackageSummaryPath((IPackageFragment) pack, pathBuffer);
} else {
return null;
}
break;
case IJavaElement.JAVA_MODULE:
IModuleDescription module= (IModuleDescription) element;
appendModuleSummaryPath(module, pathBuffer);
break;
default :
return null;
}
return getURL(urlBuffer, pathBuffer, fragmentBuffer);
}
private static URL getURL(StringBuffer urlBuffer, StringBuffer pathBuffer, StringBuffer fragmentBuffer) {
try {
String fragment= fragmentBuffer.length() == 0 ? null : fragmentBuffer.toString();
try {
URI relativeURI= new URI(null, null, pathBuffer.toString(), fragment);
urlBuffer.append(relativeURI.toString());
return new URL(urlBuffer.toString());
} catch (URISyntaxException e) {
JavaPlugin.log(e);
return new URL(urlBuffer.append(pathBuffer).toString());
}
} catch (MalformedURLException e) {
JavaPlugin.log(e);
}
return null;
}
private static void appendPackageSummaryPath(IPackageFragment pack, StringBuffer buf) {
appendModulePath(pack, buf);
String packPath= pack.getElementName().replace('.', '/');
buf.append(packPath);
buf.append("/package-summary.html"); //$NON-NLS-1$
}
private static void appendModuleSummaryPath(IModuleDescription module, StringBuffer buf) {
String moduleName= module.getElementName();
buf.append(moduleName);
buf.append("/module-summary.html"); //$NON-NLS-1$
}
private static void appendIndexPath(StringBuffer buf) {
buf.append("index.html"); //$NON-NLS-1$
}
private static void appendTypePath(IType type, StringBuffer buf) {
IPackageFragment pack= type.getPackageFragment();
appendModulePath(pack, buf);
String packPath= pack.getElementName().replace('.', '/');
String typePath= type.getTypeQualifiedName('.');
if (packPath.length() > 0) {
buf.append(packPath);
buf.append('/');
}
buf.append(typePath);
buf.append(".html"); //$NON-NLS-1$
}
private static void appendModulePath(IPackageFragment pack, StringBuffer buf) {
IModuleDescription moduleDescription= getModuleDescription(pack);
if (moduleDescription != null) {
String moduleName= moduleDescription.getElementName();
if (moduleName != null && moduleName.length() > 0) {
buf.append(moduleName);
buf.append('/');
}
}
}
private static IModuleDescription getModuleDescription(IPackageFragment pack) {
if (pack == null) {
return null;
}
IModuleDescription moduleDescription= null;
/*
* The Javadoc tool for Java SE 11 uses module name in the created URL.
* We can't know what format is required, so we just guess by the project's compiler compliance.
*/
IJavaProject javaProject= pack.getJavaProject();
if (javaProject != null && JavaModelUtil.is11OrHigher(javaProject)) {
if (pack.isReadOnly()) {
IPackageFragmentRoot root= (IPackageFragmentRoot) pack.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
if (root != null) {
moduleDescription= root.getModuleDescription();
}
} else {
try {
moduleDescription= javaProject.getModuleDescription();
} catch (JavaModelException e) {
// do nothing
}
}
}
return moduleDescription;
}
private static void appendFieldReference(IField field, StringBuffer buf) {
buf.append(field.getElementName());
}
private static void appendMethodReference(IMethod meth, StringBuffer buf) throws JavaModelException {
buf.append(meth.getElementName());
/*
* The Javadoc tool for Java SE 8 changed the anchor syntax and now tries to avoid "strange" characters in URLs.
* This breaks all clients that directly create such URLs.
* We can't know what format is required, so we just guess by the project's compiler compliance.
*/
boolean is18OrHigher= JavaModelUtil.is18OrHigher(meth.getJavaProject());
buf.append(is18OrHigher ? '-' : '(');
String[] params= meth.getParameterTypes();
IType declaringType= meth.getDeclaringType();
boolean isVararg= Flags.isVarargs(meth.getFlags());
int lastParam= params.length - 1;
for (int i= 0; i <= lastParam; i++) {
if (i != 0) {
buf.append(is18OrHigher ? "-" : ", "); //$NON-NLS-1$ //$NON-NLS-2$
}
String curr= Signature.getTypeErasure(params[i]);
String fullName= JavaModelUtil.getResolvedTypeName(curr, declaringType);
if (fullName == null) { // e.g. a type parameter "QE;"
fullName= Signature.toString(Signature.getElementType(curr));
}
if (fullName != null) {
buf.append(fullName);
int dim= Signature.getArrayCount(curr);
if (i == lastParam && isVararg) {
dim--;
}
while (dim > 0) {
buf.append(is18OrHigher ? ":A" : "[]"); //$NON-NLS-1$ //$NON-NLS-2$
dim--;
}
if (i == lastParam && isVararg) {
buf.append("..."); //$NON-NLS-1$
}
}
}
buf.append(is18OrHigher ? '-' : ')');
}
/**
* Returns the location of the Javadoc.
*
* @param element whose Javadoc location has to be found
* @param isBinary <code>true</code> if the Java element is from a binary container
* @return the location URL of the Javadoc or <code>null</code> if the location cannot be found
* @throws JavaModelException thrown when the Java element cannot be accessed
* @since 3.9
*/
public static String getBaseURL(IJavaElement element, boolean isBinary) throws JavaModelException {
if (isBinary) {
// Source attachment usually does not include Javadoc resources
// => Always use the Javadoc location as base:
URL baseURL= JavaUI.getJavadocLocation(element, false);
if (baseURL != null) {
if (baseURL.getProtocol().equals(JAR_PROTOCOL)) {
// It's a JarURLConnection, which is not known to the browser widget.
// Let's start the help web server:
URL baseURL2= PlatformUI.getWorkbench().getHelpSystem().resolve(baseURL.toExternalForm(), true);
if (baseURL2 != null) { // can be null if org.eclipse.help.ui is not available
baseURL= baseURL2;
}
}
return baseURL.toExternalForm();
}
} else {
IResource resource= element.getResource();
if (resource != null) {
/*
* Too bad: Browser widget knows nothing about EFS and custom URL handlers,
* so IResource#getLocationURI() does not work in all cases.
* We only support the local file system for now.
* A solution could be https://bugs.eclipse.org/bugs/show_bug.cgi?id=149022 .
*/
IPath location= resource.getLocation();
if (location != null)
return location.toFile().toURI().toString();
}
}
return null;
}
/**
* Returns the reason for why the Javadoc of the Java element could not be retrieved.
*
* @param element whose Javadoc could not be retrieved
* @param root the root of the Java element
* @return the String message for why the Javadoc could not be retrieved for the Java element or
* <code>null</code> if the Java element is from a source container
* @since 3.9
*/
public static String getExplanationForMissingJavadoc(IJavaElement element, IPackageFragmentRoot root) {
String message= null;
try {
boolean isBinary= (root.exists() && root.getKind() == IPackageFragmentRoot.K_BINARY);
if (isBinary) {
boolean hasAttachedJavadoc= JavaDocLocations.getJavadocBaseLocation(element) != null;
boolean hasAttachedSource= root.getSourceAttachmentPath() != null;
IOpenable openable= element.getOpenable();
boolean hasSource= openable.getBuffer() != null;
// Provide hint why there's no Java doc
if (!hasAttachedSource && !hasAttachedJavadoc)
message= CorextMessages.JavaDocLocations_noAttachments;
else if (!hasAttachedJavadoc && !hasSource)
message= CorextMessages.JavaDocLocations_noAttachedJavadoc;
else if (!hasAttachedSource)
message= CorextMessages.JavaDocLocations_noAttachedSource;
else if (!hasSource)
message= CorextMessages.JavaDocLocations_noInformation;
}
} catch (JavaModelException e) {
message= CorextMessages.JavaDocLocations_error_gettingJavadoc;
JavaPlugin.log(e);
}
return message;
}
/**
* Handles the exception thrown from JDT Core when the attached Javadoc
* cannot be retrieved due to accessibility issues or location URL issue. This exception is not
* logged but the exceptions occurred due to other reasons are logged.
*
* @param e the exception thrown when retrieving the Javadoc fails
* @return the String message for why the Javadoc could not be retrieved
* @since 3.9
*/
public static String handleFailedJavadocFetch(CoreException e) {
IStatus status= e.getStatus();
if (JavaCore.PLUGIN_ID.equals(status.getPlugin())) {
Throwable cause= e.getCause();
int code= status.getCode();
// See bug 120559, bug 400060 and bug 400062
if (code == IJavaModelStatusConstants.CANNOT_RETRIEVE_ATTACHED_JAVADOC_TIMEOUT
|| (code == IJavaModelStatusConstants.CANNOT_RETRIEVE_ATTACHED_JAVADOC && (cause instanceof FileNotFoundException || cause instanceof SocketException
|| cause instanceof UnknownHostException
|| cause instanceof ProtocolException)))
return CorextMessages.JavaDocLocations_error_gettingAttachedJavadoc;
}
JavaPlugin.log(e);
return CorextMessages.JavaDocLocations_error_gettingJavadoc;
}
/**
* Parse a URL from a String. This method first tries to treat <code>url</code> as a valid, encoded URL.
* If that didn't work, it tries to recover from bad URLs, e.g. the unencoded form we used to use in persistent storage.
*
* @param url a URL
* @return the parsed URL or <code>null</code> if the URL couldn't be parsed
* @since 3.9
*/
public static URL parseURL(String url) {
try {
try {
return new URI(url).toURL();
} catch (URISyntaxException e) {
try {
// don't log, since we used to store bad (unencoded) URLs
if (url.startsWith("file:/")) { //$NON-NLS-1$
// workaround for a bug in the 3-arg URI constructor for paths that contain '[' or ']':
return new URI("file", null, url.substring(5), null).toURL(); //$NON-NLS-1$
} else {
return URIUtil.fromString(url).toURL();
}
} catch (URISyntaxException e1) {
// last try, not expected to happen
JavaPlugin.log(e);
return new URL(url);
}
}
} catch (MalformedURLException e) {
JavaPlugin.log(e);
return null;
}
}
/**
* Returns the {@link File} of a <code>file:</code> URL. This method tries to recover from bad URLs,
* e.g. the unencoded form we used to use in persistent storage.
*
* @param url a <code>file:</code> URL
* @return the file
* @since 3.9
*/
public static File toFile(URL url) {
try {
return URIUtil.toFile(url.toURI());
} catch (URISyntaxException e) {
JavaPlugin.log(e);
return new File(url.getFile());
}
}
}