blob: e6e9bcebfc114effd400839301c6133a52e95880 [file] [log] [blame]
package org.eclipse.wst.html.ui.internal.hyperlink;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.URLHyperlink;
import org.eclipse.wst.common.uriresolver.URIResolverPlugin;
import org.eclipse.wst.sse.core.IStructuredModel;
import org.eclipse.wst.sse.core.IndexedRegion;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.util.StringUtils;
import org.eclipse.wst.xml.core.document.IDOMAttr;
import org.eclipse.wst.xml.core.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.contentmodel.util.DOMNamespaceHelper;
import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
import org.w3c.dom.Attr;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/**
* Detects hyperlinks in XML tags. Includes detection in DOCTYPE and attribute
* values. Resolves references to schemas, dtds, etc using the Common URI
* Resolver.
*
*/
class XMLHyperlinkDetector implements IHyperlinkDetector {
// copies of this class exist in:
// org.eclipse.wst.xml.ui.internal.hyperlink
// org.eclipse.wst.html.ui.internal.hyperlink
// org.eclipse.jst.jsp.ui.internal.hyperlink
private final String NO_NAMESPACE_SCHEMA_LOCATION = "noNamespaceSchemaLocation"; //$NON-NLS-1$
private final String SCHEMA_LOCATION = "schemaLocation"; //$NON-NLS-1$
private final String XMLNS = "xmlns"; //$NON-NLS-1$
private final String XSI_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"; //$NON-NLS-1$
private final String HTTP_PROTOCOL = "http://";//$NON-NLS-1$
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
// for now, only capable of creating 1 hyperlink
List hyperlinks = new ArrayList(0);
if (region != null && textViewer != null) {
IDocument document = textViewer.getDocument();
Node currentNode = getCurrentNode(document, region.getOffset());
if (currentNode != null) {
String uriString = null;
if (currentNode.getNodeType() == Node.DOCUMENT_TYPE_NODE) {
// doctype nodes
uriString = getURIString(currentNode, document);
}
else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
// element nodes
Attr currentAttr = getCurrentAttrNode(currentNode, region.getOffset());
if (currentAttr != null) {
// try to find link for current attribute
// resolve attribute value
uriString = getURIString(currentAttr, document);
// verify validity of uri string
if (!isValidURI(uriString))
// reset current attribute
currentAttr = null;
}
if (currentAttr == null) {
// try to find a linkable attribute within element
currentAttr = getLinkableAttr((Element) currentNode);
if (currentAttr != null) {
uriString = getURIString(currentAttr, document);
}
}
currentNode = currentAttr;
}
// try to create hyperlink from information gathered
if (uriString != null && currentNode != null && isValidURI(uriString)) {
IRegion hyperlinkRegion = getHyperlinkRegion(currentNode);
IHyperlink hyperlink = createHyperlink(uriString, hyperlinkRegion, document, currentNode);
if (hyperlink != null) {
hyperlinks.add(hyperlink);
}
}
}
}
if (hyperlinks.size() == 0)
return null;
return (IHyperlink[]) hyperlinks.toArray(new IHyperlink[0]);
}
/**
* Create the appropriate hyperlink
*
* @param uriString
* @param hyperlinkRegion
* @return IHyperlink
*/
private IHyperlink createHyperlink(String uriString, IRegion hyperlinkRegion, IDocument document, Node node) {
IHyperlink link = null;
if (uriString != null) {
String temp = uriString.toLowerCase();
if (temp.startsWith(HTTP_PROTOCOL)) {
// this is a URLHyperlink since this is a web address
link = new URLHyperlink(hyperlinkRegion, uriString);
}
else {
// try to locate the file in the workspace
IFile file = getFile(uriString);
if (file != null && file.exists()) {
// this is a WorkspaceFileHyperlink since file exists in
// workspace
link = new WorkspaceFileHyperlink(hyperlinkRegion, file);
}
else {
// this is an ExternalFileHyperlink since file does not
// exist
// in workspace
File externalFile = new File(uriString);
link = new ExternalFileHyperlink(hyperlinkRegion, externalFile);
}
}
}
return link;
}
private IRegion getHyperlinkRegion(Node node) {
IRegion hyperRegion = null;
if (node != null) {
short nodeType = node.getNodeType();
if (nodeType == Node.DOCUMENT_TYPE_NODE) {
// handle doc type node
IDOMNode docNode = (IDOMNode) node;
hyperRegion = new Region(docNode.getStartOffset(), docNode.getEndOffset() - docNode.getStartOffset());
}
else if (nodeType == Node.ATTRIBUTE_NODE) {
// handle attribute nodes
IDOMAttr att = (IDOMAttr) node;
// do not include quotes in attribute value region
int regOffset = att.getValueRegionStartOffset();
int regLength = att.getValueRegion().getTextLength();
String attValue = att.getValueRegionText();
if (StringUtils.isQuoted(attValue)) {
regOffset = ++regOffset;
regLength = regLength - 2;
}
hyperRegion = new Region(regOffset, regLength);
}
}
return hyperRegion;
}
/**
* Returns the URI string
*
* @param node -
* assumes not null
*/
protected String getURIString(Node node, IDocument document) {
String resolvedURI = null;
// need the base location, publicId, and systemId for URIResolver
String baseLoc = null;
String publicId = null;
String systemId = null;
short nodeType = node.getNodeType();
// handle doc type node
if (nodeType == Node.DOCUMENT_TYPE_NODE) {
baseLoc = getBaseLocation(document);
publicId = ((DocumentType) node).getPublicId();
systemId = ((DocumentType) node).getSystemId();
}
else if (nodeType == Node.ATTRIBUTE_NODE) {
// handle attribute node
Attr attrNode = (Attr) node;
baseLoc = getBaseLocation(document);
String attrName = attrNode.getName();
String attrValue = attrNode.getValue();
attrValue = StringUtils.strip(attrValue);
// handle schemaLocation attribute
String prefix = DOMNamespaceHelper.getPrefix(attrName);
String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
publicId = attrValue;
systemId = getLocationHint(attrNode.getOwnerElement(), publicId);
}
else if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attrNode))) && (SCHEMA_LOCATION.equals(unprefixedName))) {
// for now just use the first pair
// need to look into being more precise
StringTokenizer st = new StringTokenizer(attrValue);
publicId = st.hasMoreTokens() ? st.nextToken() : null;
systemId = st.hasMoreTokens() ? st.nextToken() : null;
// else check if xmlns publicId = value
}
else {
systemId = attrValue;
}
}
resolvedURI = resolveURI(baseLoc, publicId, systemId);
return resolvedURI;
}
/**
* Returns an IFile from the given uri if possible, null if cannot find
* file from uri.
*
* @param fileString
* file system path
* @return returns IFile if fileString exists in the workspace
*/
private IFile getFile(String fileString) {
IFile file = null;
if (fileString != null) {
IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocation(new Path(fileString));
for (int i = 0; i < files.length && file == null; i++)
if (files[i].exists())
file = files[i];
}
return file;
}
/**
* Checks to see if the given attribute is openable. Attribute is openable
* if it is a namespace declaration attribute or if the attribute value is
* of type URI.
*
* @param attr
* cannot be null
* @param cmElement
* CMElementDeclaration associated with the attribute (can be
* null)
* @return true if this attribute is "openOn-able" false otherwise
*/
private boolean isLinkableAttr(Attr attr, CMElementDeclaration cmElement) {
String attrName = attr.getName();
String prefix = DOMNamespaceHelper.getPrefix(attrName);
String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
// determine if attribute is namespace declaration
if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName)))
return true;
// determine if attribute contains schema location
if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attr))) && ((SCHEMA_LOCATION.equals(unprefixedName)) || (NO_NAMESPACE_SCHEMA_LOCATION.equals(unprefixedName))))
return true;
// determine if attribute value is of type URI
if (cmElement != null) {
CMAttributeDeclaration attrDecl = (CMAttributeDeclaration) cmElement.getAttributes().getNamedItem(attrName);
if ((attrDecl != null) && (attrDecl.getAttrType() != null) && (CMDataType.URI.equals(attrDecl.getAttrType().getDataTypeName()))) {
return true;
}
}
return false;
}
/**
* Attempts to find an attribute within element that is openable.
*
* @param element -
* cannot be null
* @return Attr attribute that can be used for open on, null if no
* attribute could be found
*/
private Attr getLinkableAttr(Element element) {
CMElementDeclaration ed = getCMElementDeclaration(element);
// get the list of attributes for this node
NamedNodeMap attrs = element.getAttributes();
for (int i = 0; i < attrs.getLength(); ++i) {
// check if this attribute is "openOn-able"
Attr att = (Attr) attrs.item(i);
if (isLinkableAttr(att, ed)) {
return att;
}
}
return null;
}
/**
* Get the CMElementDeclaration for an element
*
* @param element
* @return CMElementDeclaration
*/
private CMElementDeclaration getCMElementDeclaration(Element element) {
CMElementDeclaration ed = null;
ModelQuery mq = ModelQueryUtil.getModelQuery(element.getOwnerDocument());
if (mq != null) {
ed = mq.getCMElementDeclaration(element);
}
return ed;
}
/**
* Returns the attribute node within node at offset
*
* @param node
* @param offset
* @return Attr
*/
private Attr getCurrentAttrNode(Node node, int offset) {
if ((node instanceof IndexedRegion) && ((IndexedRegion) node).contains(offset) && (node.hasAttributes())) {
NamedNodeMap attrs = node.getAttributes();
// go through each attribute in node and if attribute contains
// offset, return that attribute
for (int i = 0; i < attrs.getLength(); ++i) {
// assumption that if parent node is of type IndexedRegion,
// then its attributes will also be of type IndexedRegion
IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
if (attRegion.contains(offset)) {
return (Attr) attrs.item(i);
}
}
}
return null;
}
/**
* Returns the node the cursor is currently on in the document. null if no
* node is selected
*
* @param offset
* @return Node either element, doctype, text, or null
*/
private Node getCurrentNode(IDocument document, int offset) {
// get the current node at the offset (returns either: element,
// doctype, text)
IndexedRegion inode = null;
IStructuredModel sModel = null;
try {
sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
inode = sModel.getIndexedRegion(offset);
if (inode == null)
inode = sModel.getIndexedRegion(offset - 1);
}
finally {
if (sModel != null)
sModel.releaseFromRead();
}
if (inode instanceof Node) {
return (Node) inode;
}
return null;
}
/**
* Get the base location from the current model (local file system)
*/
private String getBaseLocation(IDocument document) {
String baseLoc = null;
// get the base location from the current model
IStructuredModel sModel = null;
try {
sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
if (sModel != null) {
IPath location = new Path(sModel.getBaseLocation());
if (location.toFile().exists()) {
baseLoc = location.toString();
}
else {
IPath basePath = new Path(sModel.getBaseLocation());
if(basePath.segmentCount() > 1)
baseLoc = ResourcesPlugin.getWorkspace().getRoot().getFile(basePath).getLocation().toString();
else
baseLoc = ResourcesPlugin.getWorkspace().getRoot().getLocation().append(basePath).toString();
}
}
}
finally {
if (sModel != null) {
sModel.releaseFromRead();
}
}
return baseLoc;
}
/**
* Checks whether the given uriString is really pointing to a file
*
* @param uriString
* @return boolean
*/
private boolean isValidURI(String uriString) {
boolean isValid = false;
if (uriString != null) {
// first do a quick check to see if this is some sort of http://
String tempString = uriString.toLowerCase();
if (tempString.startsWith(HTTP_PROTOCOL))
isValid = true;
else {
File file = new File(uriString);
try {
URI uri = new URI(uriString);
file = new File(uri);
}
catch (URISyntaxException e) {
// it is okay that a uri could not be created out of
// uriString
}
catch (IllegalArgumentException e) {
// it is okay that file could not be created out of uri
}
isValid = file.exists();
}
}
return isValid;
}
/**
* Resolves the given URI information
*
* @param baseLocation
* @param publicId
* @param systemId
* @return String resolved uri.
*/
private String resolveURI(String baseLocation, String publicId, String systemId) {
// dont resolve if there's nothing to resolve
if ((baseLocation == null) && (publicId == null) && (systemId == null))
return null;
return URIResolverPlugin.createResolver().resolve(baseLocation, publicId, systemId);
}
/**
* Find the location hint for the given namespaceURI if it exists
*
* @param elementNode -
* cannot be null
* @param namespaceURI -
* cannot be null
* @return location hint (systemId) if it was found, null otherwise
*/
private String getLocationHint(Element elementNode, String namespaceURI) {
Attr schemaLocNode = elementNode.getAttributeNodeNS(XSI_NAMESPACE_URI, SCHEMA_LOCATION);
if (schemaLocNode != null) {
StringTokenizer st = new StringTokenizer(schemaLocNode.getValue());
while (st.hasMoreTokens()) {
String publicId = st.hasMoreTokens() ? st.nextToken() : null;
String systemId = st.hasMoreTokens() ? st.nextToken() : null;
// found location hint
if (namespaceURI.equalsIgnoreCase(publicId))
return systemId;
}
}
return null;
}
}