/*******************************************************************************
* Copyright (c) 2008, 2010 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
*******************************************************************************/
package org.eclipse.php.internal.ui.documentation;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.dltk.core.*;
import org.eclipse.php.internal.core.typeinference.PHPModelUtils;
import org.eclipse.php.internal.ui.PHPUiPlugin;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationAdapter;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.widgets.Display;
/**
* Links inside PHPDoc hovers.
*
* @since 3.4
*/
public class PHPElementLinks {
/**
* A handler is asked to handle links to targets.
*
* @see PHPElementLinks#createLocationListener(PHPElementLinks.ILinkHandler)
*/
public interface ILinkHandler {
/**
* Handle normal kind of link to given target.
*
* @param target
* the target to show
*/
void handleInlineLink(IModelElement target);
/**
* Handle link to given target to open its declaration
*
* @param target
* the target to show
*/
void handleDeclarationLink(IModelElement target);
/**
* Handle link to given URL to open in browser.
*
* @param url
* the url to show
* @param display
* the current display
* @return <code>true</code> if the handler could open the link
* <code>false</code> if the browser should follow the link
*/
boolean handleExternalLink(URL url, Display display);
/**
* Informs the handler that the text of the browser was set.
*/
void handleTextSet();
}
public static final String OPEN_LINK_SCHEME = "eclipse-open"; //$NON-NLS-1$
public static final String PHPDOC_SCHEME = "eclipse-phpdoc"; //$NON-NLS-1$
private static final char LINK_BRACKET_REPLACEMENT = '\u2603';
/**
* The link is composed of a number of segments, separated by
* LINK_SEPARATOR:
* <p>
* segments[0]: ""<br>
* segments[1]: baseElementHandle<br>
* segments[2]: typeName<br>
* segments[3]: memberName<br>
* segments[4...]: parameterTypeName (optional)
*/
private static final char LINK_SEPARATOR = '\u2602';
private PHPElementLinks() {
// static only
}
/**
* Creates a location listener which uses the given handler to handle java
* element links.
*
* The location listener can be attached to a {@link Browser}
*
* @param handler
* the handler to use to handle links
* @return a new {@link LocationListener}
*/
public static LocationListener createLocationListener(final ILinkHandler handler) {
return new LocationAdapter() {
@Override
public void changing(LocationEvent event) {
String loc = event.location;
if ("about:blank".equals(loc)) { //$NON-NLS-1$
/*
* Using the Browser.setText API triggers a location change
* to "about:blank". XXX: remove this code once
* https://bugs.eclipse.org/bugs/show_bug.cgi?id=130314 is
* fixed
*/
// input set with setText
handler.handleTextSet();
return;
}
event.doit = false;
if (loc.startsWith("about:")) { //$NON-NLS-1$
// Relative links should be handled via head > base tag.
// If no base is available, links just won't work.
return;
}
URI uri;
try {
uri = new URI(loc);
} catch (URISyntaxException e) {
// try it with a file (workaround for
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=237903 ):
File file = new File(loc);
if (!file.exists()) {
PHPUiPlugin.log(e);
return;
}
uri = file.toURI();
loc = uri.toASCIIString();
}
String scheme = uri.getScheme();
if (PHPElementLinks.PHPDOC_SCHEME.equals(scheme)) {
IModelElement linkTarget = PHPElementLinks.parseURI(uri);
if (linkTarget == null)
return;
handler.handleInlineLink(linkTarget);
} else if (PHPElementLinks.OPEN_LINK_SCHEME.equals(scheme)) {
IModelElement linkTarget = PHPElementLinks.parseURI(uri);
if (linkTarget == null)
return;
handler.handleDeclarationLink(linkTarget);
} else {
try {
if (handler.handleExternalLink(new URL(loc), event.display))
return;
event.doit = true;
} catch (MalformedURLException e) {
PHPUiPlugin.log(e);
}
}
}
};
}
protected static IModelElement parseURI(URI uri) {
String ssp = uri.getSchemeSpecificPart();
String[] segments = ssp.split(String.valueOf(LINK_SEPARATOR), -1);
// replace '[' manually, since URI confuses it for an IPv6 address as
// per RFC 2732:
IModelElement element = DLTKCore.create(segments[1].replace(LINK_BRACKET_REPLACEMENT, '['));
if ((segments.length > 2) && (element instanceof IMember)) {
IMember member = (IMember) element;
String refTypeName = segments[2];
IType[] types = null;
try {
int offset = member.getSourceRange() != null ? member.getSourceRange().getOffset() : 0;
types = PHPModelUtils.getTypes(refTypeName, member.getSourceModule(), offset,
new NullProgressMonitor());
} catch (ModelException e) {
PHPUiPlugin.log(e);
}
if ((types != null) && (types.length > 0)) {
IType type = types[0]; // take first one
if (segments.length > 3) {
String refMemberName = segments[3];
IMethod method = type.getMethod(refMemberName);
if (method != null && method.exists()) {
return method;
} else {
// if (refMemberName.startsWith("$")) {
// refMemberName = refMemberName.substring(0);
// }
return type.getField(refMemberName);
}
}
return type;
// String refParamterTypes = segments[4..];
}
}
return element;
}
/**
* Creates an {@link URI} with the given scheme for the given element.
*
* @param scheme
* the scheme
* @param element
* the element
* @return an {@link URI}, encoded as {@link URI#toASCIIString() ASCII}
* string, ready to be used as <code>href</code> attribute in an
* <code><a></code> tag
* @throws URISyntaxException
* if the arguments were invalid
*/
public static String createURI(String scheme, IModelElement element) throws URISyntaxException {
return createURI(scheme, element, null, null, null);
}
/**
* Creates an {@link URI} with the given scheme based on the given element.
* The additional arguments specify a member referenced from the given
* element.
*
* @param scheme
* a scheme
* @param element
* the declaring element
* @param refTypeName
* a (possibly qualified) type name, can be <code>null</code>
* @param refMemberName
* a member name, can be <code>null</code>
* @param refParameterTypes
* a (possibly empty) array of (possibly qualified) parameter
* type names, can be <code>null</code>
* @return an {@link URI}, encoded as {@link URI#toASCIIString() ASCII}
* string, ready to be used as <code>href</code> attribute in an
* <code><a></code> tag
* @throws URISyntaxException
* if the arguments were invalid
*/
public static String createURI(String scheme, IModelElement element, String refTypeName, String refMemberName,
String[] refParameterTypes) throws URISyntaxException {
/*
* We use an opaque URI, not ssp and fragments (to work around Safari
* bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=212527 (wrongly
* encodes #)).
*/
StringBuilder ssp = new StringBuilder(60);
ssp.append(LINK_SEPARATOR); // make sure first character is not a /
// (would be hierarchical URI)
// replace '[' manually, since URI confuses it for an IPv6 address as
// per RFC 2732:
ssp.append(element.getHandleIdentifier().replace('[', LINK_BRACKET_REPLACEMENT)); // segments[1]
if (refTypeName != null) {
ssp.append(LINK_SEPARATOR);
ssp.append(refTypeName); // segments[2]
if (refMemberName != null) {
ssp.append(LINK_SEPARATOR);
ssp.append(refMemberName); // segments[3]
if (refParameterTypes != null) {
ssp.append(LINK_SEPARATOR);
for (int i = 0; i < refParameterTypes.length; i++) {
ssp.append(refParameterTypes[i]); // segments[4|5|..]
if (i != refParameterTypes.length - 1) {
ssp.append(LINK_SEPARATOR);
}
}
}
}
}
return new URI(scheme, ssp.toString(), null).toASCIIString();
}
}