| package org.eclipse.wst.xml.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.internal.provisional.URIResolverPlugin; |
| import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.StructuredModelManager; |
| import org.eclipse.wst.sse.core.internal.util.StringUtils; |
| 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.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; |
| 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. |
| * |
| */ |
| public 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 |
| */ |
| private 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; |
| } |
| } |