blob: d68b183b48b0541c7046cea6ef4f9a4bff69ac9c [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;
7import com.ibm.icu.util.StringTokenizer;
8
9import org.eclipse.core.resources.IFile;
10import org.eclipse.core.resources.ResourcesPlugin;
11import org.eclipse.core.runtime.IPath;
12import org.eclipse.core.runtime.Path;
13import org.eclipse.jface.text.IDocument;
14import org.eclipse.jface.text.IRegion;
15import org.eclipse.jface.text.ITextViewer;
16import org.eclipse.jface.text.Region;
17import org.eclipse.jface.text.hyperlink.IHyperlink;
18import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
19import org.eclipse.jface.text.hyperlink.URLHyperlink;
20import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin;
21import org.eclipse.wst.sse.core.StructuredModelManager;
22import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
23import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
24import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
25import org.eclipse.wst.sse.core.utils.StringUtils;
26import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
27import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType;
28import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
29import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
30import org.eclipse.wst.xml.core.internal.contentmodel.util.DOMNamespaceHelper;
31import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
32import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr;
33import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
34import org.w3c.dom.Attr;
35import org.w3c.dom.DocumentType;
36import org.w3c.dom.Element;
37import org.w3c.dom.NamedNodeMap;
38import org.w3c.dom.Node;
39
40/**
41 * Detects hyperlinks in XML tags. Includes detection in DOCTYPE and attribute
42 * values. Resolves references to schemas, dtds, etc using the Common URI
43 * Resolver.
44 *
45 */
46public class XMLHyperlinkDetector implements IHyperlinkDetector {
47 // copies of this class exist in:
48 // org.eclipse.wst.xml.ui.internal.hyperlink
49 // org.eclipse.wst.html.ui.internal.hyperlink
50 // org.eclipse.wst.jsdt.web.ui.internal.hyperlink
51
52 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$
57
58 /**
59 * Create the appropriate hyperlink
60 *
61 * @param uriString
62 * @param hyperlinkRegion
63 * @return IHyperlink
64 */
65 private IHyperlink createHyperlink(String uriString,
66 IRegion hyperlinkRegion, IDocument document, Node node) {
67 IHyperlink link = null;
68
69 if (isHttp(uriString)) {
70 link = new URLHyperlink(hyperlinkRegion, uriString);
71 } else {
72 // try to locate the file in the workspace
73 File systemFile = getFileFromUriString(uriString);
74 if (systemFile != null) {
75 String systemPath = systemFile.getPath();
76 IFile file = getFile(systemPath);
77 if (file != null) {
78 // this is a WorkspaceFileHyperlink since file exists in
79 // workspace
80 link = new WorkspaceFileHyperlink(hyperlinkRegion, file);
81 } else {
82 // this is an ExternalFileHyperlink since file does not
83 // exist in workspace
84 link = new ExternalFileHyperlink(hyperlinkRegion,
85 systemFile);
86 }
87 }
88 }
89 return link;
90 }
91
92 public IHyperlink[] detectHyperlinks(ITextViewer textViewer,
93 IRegion region, boolean canShowMultipleHyperlinks) {
94 // for now, only capable of creating 1 hyperlink
95 List hyperlinks = new ArrayList(0);
96
97 if (region != null && textViewer != null) {
98 IDocument document = textViewer.getDocument();
99 Node currentNode = getCurrentNode(document, region.getOffset());
100 if (currentNode != null) {
101 String uriString = null;
102 if (currentNode.getNodeType() == Node.DOCUMENT_TYPE_NODE) {
103 // doctype nodes
104 uriString = getURIString(currentNode, document);
105 } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
106 // element nodes
107 Attr currentAttr = getCurrentAttrNode(currentNode, region
108 .getOffset());
109 if (currentAttr != null) {
110 // try to find link for current attribute
111 // resolve attribute value
112 uriString = getURIString(currentAttr, document);
113 // verify validity of uri string
114 if (uriString == null || !isValidURI(uriString)) {
115 // reset current attribute
116 currentAttr = null;
117 }
118 }
119 if (currentAttr == null) {
120 // try to find a linkable attribute within element
121 currentAttr = getLinkableAttr((Element) currentNode);
122 if (currentAttr != null) {
123 uriString = getURIString(currentAttr, document);
124 }
125 }
126 currentNode = currentAttr;
127 }
128 // try to create hyperlink from information gathered
129 if (uriString != null && currentNode != null
130 && isValidURI(uriString)) {
131 IRegion hyperlinkRegion = getHyperlinkRegion(currentNode);
132 IHyperlink hyperlink = createHyperlink(uriString,
133 hyperlinkRegion, document, currentNode);
134 if (hyperlink != null) {
135 hyperlinks.add(hyperlink);
136 }
137 }
138 }
139 }
140 if (hyperlinks.size() == 0) {
141 return null;
142 }
143 return (IHyperlink[]) hyperlinks.toArray(new IHyperlink[0]);
144 }
145
146 /**
147 * Get the base location from the current model (local file system)
148 */
149 private String getBaseLocation(IDocument document) {
150 String baseLoc = null;
151
152 // get the base location from the current model
153 IStructuredModel sModel = null;
154 try {
155 sModel = StructuredModelManager.getModelManager()
156 .getExistingModelForRead(document);
157 if (sModel != null) {
158 IPath location = new Path(sModel.getBaseLocation());
159 if (location.toFile().exists()) {
160 baseLoc = location.toString();
161 } else {
162 if (location.segmentCount() > 1) {
163 baseLoc = ResourcesPlugin.getWorkspace().getRoot()
164 .getFile(location).getLocation().toString();
165 } else {
166 baseLoc = ResourcesPlugin.getWorkspace().getRoot()
167 .getLocation().append(location).toString();
168 }
169 }
170 }
171 } finally {
172 if (sModel != null) {
173 sModel.releaseFromRead();
174 }
175 }
176 return baseLoc;
177 }
178
179 /**
180 * Get the CMElementDeclaration for an element
181 *
182 * @param element
183 * @return CMElementDeclaration
184 */
185 private CMElementDeclaration getCMElementDeclaration(Element element) {
186 CMElementDeclaration ed = null;
187
188 ModelQuery mq = ModelQueryUtil
189 .getModelQuery(element.getOwnerDocument());
190 if (mq != null) {
191 ed = mq.getCMElementDeclaration(element);
192 }
193 return ed;
194 }
195
196 /**
197 * Returns the attribute node within node at offset
198 *
199 * @param node
200 * @param offset
201 * @return Attr
202 */
203 private Attr getCurrentAttrNode(Node node, int offset) {
204 if ((node instanceof IndexedRegion)
205 && ((IndexedRegion) node).contains(offset)
206 && (node.hasAttributes())) {
207 NamedNodeMap attrs = node.getAttributes();
208 // go through each attribute in node and if attribute contains
209 // offset, return that attribute
210 for (int i = 0; i < attrs.getLength(); ++i) {
211 // assumption that if parent node is of type IndexedRegion,
212 // then its attributes will also be of type IndexedRegion
213 IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
214 if (attRegion.contains(offset)) {
215 return (Attr) attrs.item(i);
216 }
217 }
218 }
219 return null;
220 }
221
222 /**
223 * Returns the node the cursor is currently on in the document. null if no
224 * node is selected
225 *
226 * @param offset
227 * @return Node either element, doctype, text, or null
228 */
229 private Node getCurrentNode(IDocument document, int offset) {
230 // get the current node at the offset (returns either: element,
231 // doctype, text)
232 IndexedRegion inode = null;
233 IStructuredModel sModel = null;
234 try {
235 sModel = StructuredModelManager.getModelManager()
236 .getExistingModelForRead(document);
237 inode = sModel.getIndexedRegion(offset);
238 if (inode == null) {
239 inode = sModel.getIndexedRegion(offset - 1);
240 }
241 } finally {
242 if (sModel != null) {
243 sModel.releaseFromRead();
244 }
245 }
246
247 if (inode instanceof Node) {
248 return (Node) inode;
249 }
250 return null;
251 }
252
253 /**
254 * Returns an IFile from the given uri if possible, null if cannot find file
255 * from uri.
256 *
257 * @param fileString
258 * file system path
259 * @return returns IFile if fileString exists in the workspace
260 */
261 private IFile getFile(String fileString) {
262 IFile file = null;
263
264 if (fileString != null) {
265 IFile[] files = ResourcesPlugin.getWorkspace().getRoot()
266 .findFilesForLocation(new Path(fileString));
267 for (int i = 0; i < files.length && file == null; i++) {
268 if (files[i].exists()) {
269 file = files[i];
270 }
271 }
272 }
273
274 return file;
275 }
276
277 /**
278 * Create a file from the given uri string
279 *
280 * @param uriString -
281 * assumes uriString is not http://
282 * @return File created from uriString if possible, null otherwise
283 */
284 private File getFileFromUriString(String uriString) {
285 File file = null;
286 try {
287 // first just try to create a file directly from uriString as
288 // default in case create file from uri does not work
289 file = new File(uriString);
290
291 // try to create file from uri
292 URI uri = new URI(uriString);
293 file = new File(uri);
294 } catch (Exception e) {
295 // if exception is thrown while trying to create File just ignore
296 // and file will be null
297 }
298 return file;
299 }
300
301 private IRegion getHyperlinkRegion(Node node) {
302 IRegion hyperRegion = null;
303
304 if (node != null) {
305 short nodeType = node.getNodeType();
306 if (nodeType == Node.DOCUMENT_TYPE_NODE) {
307 // handle doc type node
308 IDOMNode docNode = (IDOMNode) node;
309 hyperRegion = new Region(docNode.getStartOffset(), docNode
310 .getEndOffset()
311 - docNode.getStartOffset());
312 } else if (nodeType == Node.ATTRIBUTE_NODE) {
313 // handle attribute nodes
314 IDOMAttr att = (IDOMAttr) node;
315 // do not include quotes in attribute value region
316 int regOffset = att.getValueRegionStartOffset();
317 ITextRegion valueRegion = att.getValueRegion();
318 if (valueRegion != null) {
319 int regLength = valueRegion.getTextLength();
320 String attValue = att.getValueRegionText();
321 if (StringUtils.isQuoted(attValue)) {
322 ++regOffset;
323 regLength = regLength - 2;
324 }
325 hyperRegion = new Region(regOffset, regLength);
326 }
327 }
328 }
329 return hyperRegion;
330 }
331
332 /**
333 * Attempts to find an attribute within element that is openable.
334 *
335 * @param element -
336 * cannot be null
337 * @return Attr attribute that can be used for open on, null if no attribute
338 * could be found
339 */
340 private Attr getLinkableAttr(Element element) {
341 CMElementDeclaration ed = getCMElementDeclaration(element);
342 // get the list of attributes for this node
343 NamedNodeMap attrs = element.getAttributes();
344 for (int i = 0; i < attrs.getLength(); ++i) {
345 // check if this attribute is "openOn-able"
346 Attr att = (Attr) attrs.item(i);
347 if (isLinkableAttr(att, ed)) {
348 return att;
349 }
350 }
351 return null;
352 }
353
354 /**
355 * Find the location hint for the given namespaceURI if it exists
356 *
357 * @param elementNode -
358 * cannot be null
359 * @param namespaceURI -
360 * cannot be null
361 * @return location hint (systemId) if it was found, null otherwise
362 */
363 private String getLocationHint(Element elementNode, String namespaceURI) {
364 Attr schemaLocNode = elementNode.getAttributeNodeNS(XSI_NAMESPACE_URI,
365 SCHEMA_LOCATION);
366 if (schemaLocNode != null) {
367 StringTokenizer st = new StringTokenizer(schemaLocNode.getValue());
368 while (st.hasMoreTokens()) {
369 String publicId = st.hasMoreTokens() ? st.nextToken() : null;
370 String systemId = st.hasMoreTokens() ? st.nextToken() : null;
371 // found location hint
372 if (namespaceURI.equalsIgnoreCase(publicId)) {
373 return systemId;
374 }
375 }
376 }
377 return null;
378 }
379
380 /**
381 * Returns the URI string
382 *
383 * @param node -
384 * assumes not null
385 */
386 private String getURIString(Node node, IDocument document) {
387 String resolvedURI = null;
388 // need the base location, publicId, and systemId for URIResolver
389 String baseLoc = null;
390 String publicId = null;
391 String systemId = null;
392
393 short nodeType = node.getNodeType();
394 // handle doc type node
395 if (nodeType == Node.DOCUMENT_TYPE_NODE) {
396 baseLoc = getBaseLocation(document);
397 publicId = ((DocumentType) node).getPublicId();
398 systemId = ((DocumentType) node).getSystemId();
399 } else if (nodeType == Node.ATTRIBUTE_NODE) {
400 // handle attribute node
401 Attr attrNode = (Attr) node;
402 String attrName = attrNode.getName();
403 String attrValue = attrNode.getValue();
404 attrValue = StringUtils.strip(attrValue);
405 if (attrValue != null && attrValue.length() > 0) {
406 baseLoc = getBaseLocation(document);
407
408 // handle schemaLocation attribute
409 String prefix = DOMNamespaceHelper.getPrefix(attrName);
410 String unprefixedName = DOMNamespaceHelper
411 .getUnprefixedName(attrName);
412 if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
413 publicId = attrValue;
414 systemId = getLocationHint(attrNode.getOwnerElement(),
415 publicId);
416 if (systemId == null) {
417 systemId = attrValue;
418 }
419 } else if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper
420 .getNamespaceURI(attrNode)))
421 && (SCHEMA_LOCATION.equals(unprefixedName))) {
422 // for now just use the first pair
423 // need to look into being more precise
424 StringTokenizer st = new StringTokenizer(attrValue);
425 publicId = st.hasMoreTokens() ? st.nextToken() : null;
426 systemId = st.hasMoreTokens() ? st.nextToken() : null;
427 // else check if xmlns publicId = value
428 } else {
429 systemId = attrValue;
430 }
431 }
432 }
433
434 resolvedURI = resolveURI(baseLoc, publicId, systemId);
435 return resolvedURI;
436 }
437
438 /**
439 * Returns true if this uriString is an http string
440 *
441 * @param uriString
442 * @return true if uriString is http string, false otherwise
443 */
444 private boolean isHttp(String uriString) {
445 boolean isHttp = false;
446 if (uriString != null) {
447 String tempString = uriString.toLowerCase();
448 if (tempString.startsWith(HTTP_PROTOCOL)) {
449 isHttp = true;
450 }
451 }
452 return isHttp;
453 }
454
455 /**
456 * Checks to see if the given attribute is openable. Attribute is openable
457 * if it is a namespace declaration attribute or if the attribute value is
458 * of type URI.
459 *
460 * @param attr
461 * cannot be null
462 * @param cmElement
463 * CMElementDeclaration associated with the attribute (can be
464 * null)
465 * @return true if this attribute is "openOn-able" false otherwise
466 */
467 private boolean isLinkableAttr(Attr attr, CMElementDeclaration cmElement) {
468 String attrName = attr.getName();
469 String prefix = DOMNamespaceHelper.getPrefix(attrName);
470 String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
471 // determine if attribute is namespace declaration
472 if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
473 return true;
474 }
475
476 // determine if attribute contains schema location
477 if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attr)))
478 && ((SCHEMA_LOCATION.equals(unprefixedName)) || (NO_NAMESPACE_SCHEMA_LOCATION
479 .equals(unprefixedName)))) {
480 return true;
481 }
482
483 // determine if attribute value is of type URI
484 if (cmElement != null) {
485 CMAttributeDeclaration attrDecl = (CMAttributeDeclaration) cmElement
486 .getAttributes().getNamedItem(attrName);
487 if ((attrDecl != null)
488 && (attrDecl.getAttrType() != null)
489 && (CMDataType.URI.equals(attrDecl.getAttrType()
490 .getDataTypeName()))) {
491 return true;
492 }
493 }
494 return false;
495 }
496
497 /**
498 * Checks whether the given uriString is really pointing to a file
499 *
500 * @param uriString
501 * @return boolean
502 */
503 private boolean isValidURI(String uriString) {
504 boolean isValid = false;
505
506 if (isHttp(uriString)) {
507 isValid = true;
508 } else {
509 File file = getFileFromUriString(uriString);
510 if (file != null) {
511 isValid = file.isFile();
512 }
513 }
514 return isValid;
515 }
516
517 /**
518 * Resolves the given URI information
519 *
520 * @param baseLocation
521 * @param publicId
522 * @param systemId
523 * @return String resolved uri.
524 */
525 private String resolveURI(String baseLocation, String publicId,
526 String systemId) {
527 // dont resolve if there's nothing to resolve
528 if ((baseLocation == null) && (publicId == null) && (systemId == null)) {
529 return null;
530 }
531 return URIResolverPlugin.createResolver().resolve(baseLocation,
532 publicId, systemId);
533 }
534}