blob: 02dcebf6cd7daa2d8d7f6431f36bdd0dd70f8ce1 [file] [log] [blame]
bchilds68f36fc2007-03-01 22:47:33 +00001package org.eclipse.wst.jsdt.web.ui.internal.hyperlink;
2
3import java.io.File;
4import java.net.URI;
5import java.util.ArrayList;
6import java.util.List;
bchilds68f36fc2007-03-01 22:47:33 +00007
8import org.eclipse.core.resources.IFile;
9import org.eclipse.core.resources.ResourcesPlugin;
10import org.eclipse.core.runtime.IPath;
11import org.eclipse.core.runtime.Path;
12import org.eclipse.jface.text.IDocument;
13import org.eclipse.jface.text.IRegion;
14import org.eclipse.jface.text.ITextViewer;
15import org.eclipse.jface.text.Region;
16import org.eclipse.jface.text.hyperlink.IHyperlink;
17import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
18import org.eclipse.jface.text.hyperlink.URLHyperlink;
19import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin;
20import org.eclipse.wst.sse.core.StructuredModelManager;
21import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
22import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
23import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
24import org.eclipse.wst.sse.core.utils.StringUtils;
25import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
26import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType;
27import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
28import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
29import org.eclipse.wst.xml.core.internal.contentmodel.util.DOMNamespaceHelper;
30import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
31import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr;
32import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
33import org.w3c.dom.Attr;
34import org.w3c.dom.DocumentType;
35import org.w3c.dom.Element;
36import org.w3c.dom.NamedNodeMap;
37import org.w3c.dom.Node;
38
bchilds133806a2007-03-27 15:52:39 +000039import com.ibm.icu.util.StringTokenizer;
40
bchilds68f36fc2007-03-01 22:47:33 +000041/**
42 * Detects hyperlinks in XML tags. Includes detection in DOCTYPE and attribute
43 * values. Resolves references to schemas, dtds, etc using the Common URI
44 * Resolver.
45 *
46 */
47public class XMLHyperlinkDetector implements IHyperlinkDetector {
48 // copies of this class exist in:
49 // org.eclipse.wst.xml.ui.internal.hyperlink
50 // org.eclipse.wst.html.ui.internal.hyperlink
51 // org.eclipse.wst.jsdt.web.ui.internal.hyperlink
bchilds68f36fc2007-03-01 22:47:33 +000052 private final String HTTP_PROTOCOL = "http://";//$NON-NLS-1$
53 private final String NO_NAMESPACE_SCHEMA_LOCATION = "noNamespaceSchemaLocation"; //$NON-NLS-1$
54 private final String SCHEMA_LOCATION = "schemaLocation"; //$NON-NLS-1$
55 private final String XMLNS = "xmlns"; //$NON-NLS-1$
56 private final String XSI_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"; //$NON-NLS-1$
bchildsa1181702007-06-14 21:47:04 +000057
bchilds68f36fc2007-03-01 22:47:33 +000058 /**
59 * Create the appropriate hyperlink
60 *
61 * @param uriString
62 * @param hyperlinkRegion
63 * @return IHyperlink
64 */
bchildsa1181702007-06-14 21:47:04 +000065 private IHyperlink createHyperlink(String uriString, IRegion hyperlinkRegion, IDocument document, Node node) {
bchilds68f36fc2007-03-01 22:47:33 +000066 IHyperlink link = null;
bchilds68f36fc2007-03-01 22:47:33 +000067 if (isHttp(uriString)) {
68 link = new URLHyperlink(hyperlinkRegion, uriString);
69 } else {
70 // try to locate the file in the workspace
71 File systemFile = getFileFromUriString(uriString);
72 if (systemFile != null) {
73 String systemPath = systemFile.getPath();
74 IFile file = getFile(systemPath);
75 if (file != null) {
76 // this is a WorkspaceFileHyperlink since file exists in
77 // workspace
78 link = new WorkspaceFileHyperlink(hyperlinkRegion, file);
79 } else {
80 // this is an ExternalFileHyperlink since file does not
81 // exist in workspace
bchildsa1181702007-06-14 21:47:04 +000082 link = new ExternalFileHyperlink(hyperlinkRegion, systemFile);
bchilds68f36fc2007-03-01 22:47:33 +000083 }
84 }
85 }
86 return link;
87 }
bchildsa1181702007-06-14 21:47:04 +000088
89 public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
bchilds68f36fc2007-03-01 22:47:33 +000090 // for now, only capable of creating 1 hyperlink
91 List hyperlinks = new ArrayList(0);
bchilds68f36fc2007-03-01 22:47:33 +000092 if (region != null && textViewer != null) {
93 IDocument document = textViewer.getDocument();
94 Node currentNode = getCurrentNode(document, region.getOffset());
95 if (currentNode != null) {
96 String uriString = null;
97 if (currentNode.getNodeType() == Node.DOCUMENT_TYPE_NODE) {
98 // doctype nodes
99 uriString = getURIString(currentNode, document);
100 } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
101 // element nodes
bchildsa1181702007-06-14 21:47:04 +0000102 Attr currentAttr = getCurrentAttrNode(currentNode, region.getOffset());
bchilds68f36fc2007-03-01 22:47:33 +0000103 if (currentAttr != null) {
104 // try to find link for current attribute
105 // resolve attribute value
106 uriString = getURIString(currentAttr, document);
107 // verify validity of uri string
108 if (uriString == null || !isValidURI(uriString)) {
109 // reset current attribute
110 currentAttr = null;
111 }
112 }
113 if (currentAttr == null) {
114 // try to find a linkable attribute within element
115 currentAttr = getLinkableAttr((Element) currentNode);
116 if (currentAttr != null) {
117 uriString = getURIString(currentAttr, document);
118 }
119 }
120 currentNode = currentAttr;
121 }
122 // try to create hyperlink from information gathered
bchildsa1181702007-06-14 21:47:04 +0000123 if (uriString != null && currentNode != null && isValidURI(uriString)) {
bchilds68f36fc2007-03-01 22:47:33 +0000124 IRegion hyperlinkRegion = getHyperlinkRegion(currentNode);
bchildsa1181702007-06-14 21:47:04 +0000125 IHyperlink hyperlink = createHyperlink(uriString, hyperlinkRegion, document, currentNode);
bchilds68f36fc2007-03-01 22:47:33 +0000126 if (hyperlink != null) {
127 hyperlinks.add(hyperlink);
128 }
129 }
130 }
131 }
132 if (hyperlinks.size() == 0) {
133 return null;
134 }
135 return (IHyperlink[]) hyperlinks.toArray(new IHyperlink[0]);
136 }
bchildsa1181702007-06-14 21:47:04 +0000137
bchilds68f36fc2007-03-01 22:47:33 +0000138 /**
139 * Get the base location from the current model (local file system)
140 */
141 private String getBaseLocation(IDocument document) {
142 String baseLoc = null;
bchilds68f36fc2007-03-01 22:47:33 +0000143 // get the base location from the current model
144 IStructuredModel sModel = null;
145 try {
bchildsa1181702007-06-14 21:47:04 +0000146 sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
bchilds68f36fc2007-03-01 22:47:33 +0000147 if (sModel != null) {
148 IPath location = new Path(sModel.getBaseLocation());
149 if (location.toFile().exists()) {
150 baseLoc = location.toString();
151 } else {
152 if (location.segmentCount() > 1) {
bchildsa1181702007-06-14 21:47:04 +0000153 baseLoc = ResourcesPlugin.getWorkspace().getRoot().getFile(location).getLocation().toString();
bchilds68f36fc2007-03-01 22:47:33 +0000154 } else {
bchildsa1181702007-06-14 21:47:04 +0000155 baseLoc = ResourcesPlugin.getWorkspace().getRoot().getLocation().append(location).toString();
bchilds68f36fc2007-03-01 22:47:33 +0000156 }
157 }
158 }
159 } finally {
160 if (sModel != null) {
161 sModel.releaseFromRead();
162 }
163 }
164 return baseLoc;
165 }
bchildsa1181702007-06-14 21:47:04 +0000166
bchilds68f36fc2007-03-01 22:47:33 +0000167 /**
168 * Get the CMElementDeclaration for an element
169 *
170 * @param element
171 * @return CMElementDeclaration
172 */
173 private CMElementDeclaration getCMElementDeclaration(Element element) {
174 CMElementDeclaration ed = null;
bchildsa1181702007-06-14 21:47:04 +0000175 ModelQuery mq = ModelQueryUtil.getModelQuery(element.getOwnerDocument());
bchilds68f36fc2007-03-01 22:47:33 +0000176 if (mq != null) {
177 ed = mq.getCMElementDeclaration(element);
178 }
179 return ed;
180 }
bchildsa1181702007-06-14 21:47:04 +0000181
bchilds68f36fc2007-03-01 22:47:33 +0000182 /**
183 * Returns the attribute node within node at offset
184 *
185 * @param node
186 * @param offset
187 * @return Attr
188 */
189 private Attr getCurrentAttrNode(Node node, int offset) {
bchildsa1181702007-06-14 21:47:04 +0000190 if ((node instanceof IndexedRegion) && ((IndexedRegion) node).contains(offset) && (node.hasAttributes())) {
bchilds68f36fc2007-03-01 22:47:33 +0000191 NamedNodeMap attrs = node.getAttributes();
192 // go through each attribute in node and if attribute contains
193 // offset, return that attribute
194 for (int i = 0; i < attrs.getLength(); ++i) {
195 // assumption that if parent node is of type IndexedRegion,
196 // then its attributes will also be of type IndexedRegion
197 IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
198 if (attRegion.contains(offset)) {
199 return (Attr) attrs.item(i);
200 }
201 }
202 }
203 return null;
204 }
bchildsa1181702007-06-14 21:47:04 +0000205
bchilds68f36fc2007-03-01 22:47:33 +0000206 /**
207 * Returns the node the cursor is currently on in the document. null if no
208 * node is selected
209 *
210 * @param offset
211 * @return Node either element, doctype, text, or null
212 */
213 private Node getCurrentNode(IDocument document, int offset) {
214 // get the current node at the offset (returns either: element,
215 // doctype, text)
216 IndexedRegion inode = null;
217 IStructuredModel sModel = null;
218 try {
bchildsa1181702007-06-14 21:47:04 +0000219 sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
bchilds68f36fc2007-03-01 22:47:33 +0000220 inode = sModel.getIndexedRegion(offset);
221 if (inode == null) {
222 inode = sModel.getIndexedRegion(offset - 1);
223 }
224 } finally {
225 if (sModel != null) {
226 sModel.releaseFromRead();
227 }
228 }
bchilds68f36fc2007-03-01 22:47:33 +0000229 if (inode instanceof Node) {
230 return (Node) inode;
231 }
232 return null;
233 }
bchildsa1181702007-06-14 21:47:04 +0000234
bchilds68f36fc2007-03-01 22:47:33 +0000235 /**
236 * Returns an IFile from the given uri if possible, null if cannot find file
237 * from uri.
238 *
239 * @param fileString
240 * file system path
241 * @return returns IFile if fileString exists in the workspace
242 */
243 private IFile getFile(String fileString) {
244 IFile file = null;
bchilds68f36fc2007-03-01 22:47:33 +0000245 if (fileString != null) {
bchildsa1181702007-06-14 21:47:04 +0000246 IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocation(new Path(fileString));
bchilds68f36fc2007-03-01 22:47:33 +0000247 for (int i = 0; i < files.length && file == null; i++) {
248 if (files[i].exists()) {
249 file = files[i];
250 }
251 }
252 }
bchilds68f36fc2007-03-01 22:47:33 +0000253 return file;
254 }
bchildsa1181702007-06-14 21:47:04 +0000255
bchilds68f36fc2007-03-01 22:47:33 +0000256 /**
257 * Create a file from the given uri string
258 *
259 * @param uriString -
260 * assumes uriString is not http://
261 * @return File created from uriString if possible, null otherwise
262 */
263 private File getFileFromUriString(String uriString) {
264 File file = null;
265 try {
266 // first just try to create a file directly from uriString as
267 // default in case create file from uri does not work
268 file = new File(uriString);
bchilds68f36fc2007-03-01 22:47:33 +0000269 // try to create file from uri
270 URI uri = new URI(uriString);
271 file = new File(uri);
272 } catch (Exception e) {
273 // if exception is thrown while trying to create File just ignore
274 // and file will be null
275 }
276 return file;
277 }
bchildsa1181702007-06-14 21:47:04 +0000278
bchilds68f36fc2007-03-01 22:47:33 +0000279 private IRegion getHyperlinkRegion(Node node) {
280 IRegion hyperRegion = null;
bchilds68f36fc2007-03-01 22:47:33 +0000281 if (node != null) {
282 short nodeType = node.getNodeType();
283 if (nodeType == Node.DOCUMENT_TYPE_NODE) {
284 // handle doc type node
285 IDOMNode docNode = (IDOMNode) node;
bchildsa1181702007-06-14 21:47:04 +0000286 hyperRegion = new Region(docNode.getStartOffset(), docNode.getEndOffset() - docNode.getStartOffset());
bchilds68f36fc2007-03-01 22:47:33 +0000287 } else if (nodeType == Node.ATTRIBUTE_NODE) {
288 // handle attribute nodes
289 IDOMAttr att = (IDOMAttr) node;
290 // do not include quotes in attribute value region
291 int regOffset = att.getValueRegionStartOffset();
292 ITextRegion valueRegion = att.getValueRegion();
293 if (valueRegion != null) {
294 int regLength = valueRegion.getTextLength();
295 String attValue = att.getValueRegionText();
296 if (StringUtils.isQuoted(attValue)) {
297 ++regOffset;
298 regLength = regLength - 2;
299 }
300 hyperRegion = new Region(regOffset, regLength);
301 }
302 }
303 }
304 return hyperRegion;
305 }
bchildsa1181702007-06-14 21:47:04 +0000306
bchilds68f36fc2007-03-01 22:47:33 +0000307 /**
308 * Attempts to find an attribute within element that is openable.
309 *
310 * @param element -
311 * cannot be null
312 * @return Attr attribute that can be used for open on, null if no attribute
313 * could be found
314 */
315 private Attr getLinkableAttr(Element element) {
316 CMElementDeclaration ed = getCMElementDeclaration(element);
317 // get the list of attributes for this node
318 NamedNodeMap attrs = element.getAttributes();
319 for (int i = 0; i < attrs.getLength(); ++i) {
320 // check if this attribute is "openOn-able"
321 Attr att = (Attr) attrs.item(i);
322 if (isLinkableAttr(att, ed)) {
323 return att;
324 }
325 }
326 return null;
327 }
bchildsa1181702007-06-14 21:47:04 +0000328
bchilds68f36fc2007-03-01 22:47:33 +0000329 /**
330 * Find the location hint for the given namespaceURI if it exists
331 *
332 * @param elementNode -
333 * cannot be null
334 * @param namespaceURI -
335 * cannot be null
336 * @return location hint (systemId) if it was found, null otherwise
337 */
338 private String getLocationHint(Element elementNode, String namespaceURI) {
bchildsa1181702007-06-14 21:47:04 +0000339 Attr schemaLocNode = elementNode.getAttributeNodeNS(XSI_NAMESPACE_URI, SCHEMA_LOCATION);
bchilds68f36fc2007-03-01 22:47:33 +0000340 if (schemaLocNode != null) {
341 StringTokenizer st = new StringTokenizer(schemaLocNode.getValue());
342 while (st.hasMoreTokens()) {
343 String publicId = st.hasMoreTokens() ? st.nextToken() : null;
344 String systemId = st.hasMoreTokens() ? st.nextToken() : null;
345 // found location hint
346 if (namespaceURI.equalsIgnoreCase(publicId)) {
347 return systemId;
348 }
349 }
350 }
351 return null;
352 }
bchildsa1181702007-06-14 21:47:04 +0000353
bchilds68f36fc2007-03-01 22:47:33 +0000354 /**
355 * Returns the URI string
356 *
357 * @param node -
358 * assumes not null
359 */
360 private String getURIString(Node node, IDocument document) {
361 String resolvedURI = null;
362 // need the base location, publicId, and systemId for URIResolver
363 String baseLoc = null;
364 String publicId = null;
365 String systemId = null;
bchilds68f36fc2007-03-01 22:47:33 +0000366 short nodeType = node.getNodeType();
367 // handle doc type node
368 if (nodeType == Node.DOCUMENT_TYPE_NODE) {
369 baseLoc = getBaseLocation(document);
370 publicId = ((DocumentType) node).getPublicId();
371 systemId = ((DocumentType) node).getSystemId();
372 } else if (nodeType == Node.ATTRIBUTE_NODE) {
373 // handle attribute node
374 Attr attrNode = (Attr) node;
375 String attrName = attrNode.getName();
376 String attrValue = attrNode.getValue();
377 attrValue = StringUtils.strip(attrValue);
378 if (attrValue != null && attrValue.length() > 0) {
379 baseLoc = getBaseLocation(document);
bchilds68f36fc2007-03-01 22:47:33 +0000380 // handle schemaLocation attribute
381 String prefix = DOMNamespaceHelper.getPrefix(attrName);
bchildsa1181702007-06-14 21:47:04 +0000382 String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
bchilds68f36fc2007-03-01 22:47:33 +0000383 if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
384 publicId = attrValue;
bchildsa1181702007-06-14 21:47:04 +0000385 systemId = getLocationHint(attrNode.getOwnerElement(), publicId);
bchilds68f36fc2007-03-01 22:47:33 +0000386 if (systemId == null) {
387 systemId = attrValue;
388 }
bchildsa1181702007-06-14 21:47:04 +0000389 } else if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attrNode))) && (SCHEMA_LOCATION.equals(unprefixedName))) {
bchilds68f36fc2007-03-01 22:47:33 +0000390 // for now just use the first pair
391 // need to look into being more precise
392 StringTokenizer st = new StringTokenizer(attrValue);
393 publicId = st.hasMoreTokens() ? st.nextToken() : null;
394 systemId = st.hasMoreTokens() ? st.nextToken() : null;
395 // else check if xmlns publicId = value
396 } else {
397 systemId = attrValue;
398 }
399 }
400 }
bchilds68f36fc2007-03-01 22:47:33 +0000401 resolvedURI = resolveURI(baseLoc, publicId, systemId);
402 return resolvedURI;
403 }
bchildsa1181702007-06-14 21:47:04 +0000404
bchilds68f36fc2007-03-01 22:47:33 +0000405 /**
406 * Returns true if this uriString is an http string
407 *
408 * @param uriString
409 * @return true if uriString is http string, false otherwise
410 */
411 private boolean isHttp(String uriString) {
412 boolean isHttp = false;
413 if (uriString != null) {
414 String tempString = uriString.toLowerCase();
415 if (tempString.startsWith(HTTP_PROTOCOL)) {
416 isHttp = true;
417 }
418 }
419 return isHttp;
420 }
bchildsa1181702007-06-14 21:47:04 +0000421
bchilds68f36fc2007-03-01 22:47:33 +0000422 /**
423 * Checks to see if the given attribute is openable. Attribute is openable
424 * if it is a namespace declaration attribute or if the attribute value is
425 * of type URI.
426 *
427 * @param attr
428 * cannot be null
429 * @param cmElement
430 * CMElementDeclaration associated with the attribute (can be
431 * null)
432 * @return true if this attribute is "openOn-able" false otherwise
433 */
434 private boolean isLinkableAttr(Attr attr, CMElementDeclaration cmElement) {
435 String attrName = attr.getName();
436 String prefix = DOMNamespaceHelper.getPrefix(attrName);
437 String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
438 // determine if attribute is namespace declaration
439 if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
440 return true;
441 }
bchilds68f36fc2007-03-01 22:47:33 +0000442 // determine if attribute contains schema location
bchildsa1181702007-06-14 21:47:04 +0000443 if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attr))) && ((SCHEMA_LOCATION.equals(unprefixedName)) || (NO_NAMESPACE_SCHEMA_LOCATION.equals(unprefixedName)))) {
bchilds68f36fc2007-03-01 22:47:33 +0000444 return true;
445 }
bchilds68f36fc2007-03-01 22:47:33 +0000446 // determine if attribute value is of type URI
447 if (cmElement != null) {
bchildsa1181702007-06-14 21:47:04 +0000448 CMAttributeDeclaration attrDecl = (CMAttributeDeclaration) cmElement.getAttributes().getNamedItem(attrName);
449 if ((attrDecl != null) && (attrDecl.getAttrType() != null) && (CMDataType.URI.equals(attrDecl.getAttrType().getDataTypeName()))) {
bchilds68f36fc2007-03-01 22:47:33 +0000450 return true;
451 }
452 }
453 return false;
454 }
bchildsa1181702007-06-14 21:47:04 +0000455
bchilds68f36fc2007-03-01 22:47:33 +0000456 /**
457 * Checks whether the given uriString is really pointing to a file
458 *
459 * @param uriString
460 * @return boolean
461 */
462 private boolean isValidURI(String uriString) {
463 boolean isValid = false;
bchilds68f36fc2007-03-01 22:47:33 +0000464 if (isHttp(uriString)) {
465 isValid = true;
466 } else {
467 File file = getFileFromUriString(uriString);
468 if (file != null) {
469 isValid = file.isFile();
470 }
471 }
472 return isValid;
473 }
bchildsa1181702007-06-14 21:47:04 +0000474
bchilds68f36fc2007-03-01 22:47:33 +0000475 /**
476 * Resolves the given URI information
477 *
478 * @param baseLocation
479 * @param publicId
480 * @param systemId
481 * @return String resolved uri.
482 */
bchildsa1181702007-06-14 21:47:04 +0000483 private String resolveURI(String baseLocation, String publicId, String systemId) {
bchilds68f36fc2007-03-01 22:47:33 +0000484 // dont resolve if there's nothing to resolve
485 if ((baseLocation == null) && (publicId == null) && (systemId == null)) {
486 return null;
487 }
bchildsa1181702007-06-14 21:47:04 +0000488 return URIResolverPlugin.createResolver().resolve(baseLocation, publicId, systemId);
bchilds68f36fc2007-03-01 22:47:33 +0000489 }
490}