blob: bf603d05b9670e33d422fd75b3eda10de71aa7a9 [file] [log] [blame]
bchilds25637a62008-04-30 19:06:32 +00001/*******************************************************************************
2 * Copyright (c) 2007 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
bchilds68f36fc2007-03-01 22:47:33 +000011package org.eclipse.wst.jsdt.web.ui.internal.hyperlink;
12
13import java.io.File;
14import java.net.URI;
15import java.util.ArrayList;
16import java.util.List;
bchilds68f36fc2007-03-01 22:47:33 +000017
18import org.eclipse.core.resources.IFile;
19import org.eclipse.core.resources.ResourcesPlugin;
20import org.eclipse.core.runtime.IPath;
21import org.eclipse.core.runtime.Path;
22import org.eclipse.jface.text.IDocument;
23import org.eclipse.jface.text.IRegion;
24import org.eclipse.jface.text.ITextViewer;
25import org.eclipse.jface.text.Region;
26import org.eclipse.jface.text.hyperlink.IHyperlink;
27import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
28import org.eclipse.jface.text.hyperlink.URLHyperlink;
29import org.eclipse.wst.common.uriresolver.internal.provisional.URIResolverPlugin;
30import org.eclipse.wst.sse.core.StructuredModelManager;
31import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
32import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
33import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
34import org.eclipse.wst.sse.core.utils.StringUtils;
35import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration;
36import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType;
37import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
38import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
39import org.eclipse.wst.xml.core.internal.contentmodel.util.DOMNamespaceHelper;
40import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
41import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr;
42import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
43import org.w3c.dom.Attr;
44import org.w3c.dom.DocumentType;
45import org.w3c.dom.Element;
46import org.w3c.dom.NamedNodeMap;
47import org.w3c.dom.Node;
48
bchilds133806a2007-03-27 15:52:39 +000049import com.ibm.icu.util.StringTokenizer;
50
bchilds68f36fc2007-03-01 22:47:33 +000051/**
bchilds25637a62008-04-30 19:06:32 +000052*
53
54* Provisional API: This class/interface is part of an interim API that is still under development and expected to
55* change significantly before reaching stability. It is being made available at this early stage to solicit feedback
56* from pioneering adopters on the understanding that any code that uses this API will almost certainly be broken
57* (repeatedly) as the API evolves.
58*/
bchilds68f36fc2007-03-01 22:47:33 +000059public class XMLHyperlinkDetector implements IHyperlinkDetector {
60 // copies of this class exist in:
61 // org.eclipse.wst.xml.ui.internal.hyperlink
62 // org.eclipse.wst.html.ui.internal.hyperlink
63 // org.eclipse.wst.jsdt.web.ui.internal.hyperlink
bchilds68f36fc2007-03-01 22:47:33 +000064 private final String HTTP_PROTOCOL = "http://";//$NON-NLS-1$
65 private final String NO_NAMESPACE_SCHEMA_LOCATION = "noNamespaceSchemaLocation"; //$NON-NLS-1$
66 private final String SCHEMA_LOCATION = "schemaLocation"; //$NON-NLS-1$
67 private final String XMLNS = "xmlns"; //$NON-NLS-1$
68 private final String XSI_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"; //$NON-NLS-1$
bchildsa1181702007-06-14 21:47:04 +000069
bchilds68f36fc2007-03-01 22:47:33 +000070 /**
71 * Create the appropriate hyperlink
72 *
73 * @param uriString
74 * @param hyperlinkRegion
75 * @return IHyperlink
76 */
bchildsa1181702007-06-14 21:47:04 +000077 private IHyperlink createHyperlink(String uriString, IRegion hyperlinkRegion, IDocument document, Node node) {
bchilds68f36fc2007-03-01 22:47:33 +000078 IHyperlink link = null;
bchilds68f36fc2007-03-01 22:47:33 +000079 if (isHttp(uriString)) {
80 link = new URLHyperlink(hyperlinkRegion, uriString);
81 } else {
82 // try to locate the file in the workspace
83 File systemFile = getFileFromUriString(uriString);
84 if (systemFile != null) {
85 String systemPath = systemFile.getPath();
86 IFile file = getFile(systemPath);
87 if (file != null) {
88 // this is a WorkspaceFileHyperlink since file exists in
89 // workspace
90 link = new WorkspaceFileHyperlink(hyperlinkRegion, file);
91 } else {
92 // this is an ExternalFileHyperlink since file does not
93 // exist in workspace
bchildsa1181702007-06-14 21:47:04 +000094 link = new ExternalFileHyperlink(hyperlinkRegion, systemFile);
bchilds68f36fc2007-03-01 22:47:33 +000095 }
96 }
97 }
98 return link;
99 }
bchildsa1181702007-06-14 21:47:04 +0000100
101 public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) {
bchilds68f36fc2007-03-01 22:47:33 +0000102 // for now, only capable of creating 1 hyperlink
103 List hyperlinks = new ArrayList(0);
bchilds68f36fc2007-03-01 22:47:33 +0000104 if (region != null && textViewer != null) {
105 IDocument document = textViewer.getDocument();
106 Node currentNode = getCurrentNode(document, region.getOffset());
107 if (currentNode != null) {
108 String uriString = null;
109 if (currentNode.getNodeType() == Node.DOCUMENT_TYPE_NODE) {
110 // doctype nodes
111 uriString = getURIString(currentNode, document);
112 } else if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
113 // element nodes
bchildsa1181702007-06-14 21:47:04 +0000114 Attr currentAttr = getCurrentAttrNode(currentNode, region.getOffset());
bchilds68f36fc2007-03-01 22:47:33 +0000115 if (currentAttr != null) {
116 // try to find link for current attribute
117 // resolve attribute value
118 uriString = getURIString(currentAttr, document);
119 // verify validity of uri string
120 if (uriString == null || !isValidURI(uriString)) {
121 // reset current attribute
122 currentAttr = null;
123 }
124 }
125 if (currentAttr == null) {
126 // try to find a linkable attribute within element
127 currentAttr = getLinkableAttr((Element) currentNode);
128 if (currentAttr != null) {
129 uriString = getURIString(currentAttr, document);
130 }
131 }
132 currentNode = currentAttr;
133 }
134 // try to create hyperlink from information gathered
bchildsa1181702007-06-14 21:47:04 +0000135 if (uriString != null && currentNode != null && isValidURI(uriString)) {
bchilds68f36fc2007-03-01 22:47:33 +0000136 IRegion hyperlinkRegion = getHyperlinkRegion(currentNode);
bchildsa1181702007-06-14 21:47:04 +0000137 IHyperlink hyperlink = createHyperlink(uriString, hyperlinkRegion, document, currentNode);
bchilds68f36fc2007-03-01 22:47:33 +0000138 if (hyperlink != null) {
139 hyperlinks.add(hyperlink);
140 }
141 }
142 }
143 }
144 if (hyperlinks.size() == 0) {
145 return null;
146 }
147 return (IHyperlink[]) hyperlinks.toArray(new IHyperlink[0]);
148 }
bchildsa1181702007-06-14 21:47:04 +0000149
bchilds68f36fc2007-03-01 22:47:33 +0000150 /**
151 * Get the base location from the current model (local file system)
152 */
153 private String getBaseLocation(IDocument document) {
154 String baseLoc = null;
bchilds68f36fc2007-03-01 22:47:33 +0000155 // get the base location from the current model
156 IStructuredModel sModel = null;
157 try {
bchildsa1181702007-06-14 21:47:04 +0000158 sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
bchilds68f36fc2007-03-01 22:47:33 +0000159 if (sModel != null) {
160 IPath location = new Path(sModel.getBaseLocation());
161 if (location.toFile().exists()) {
162 baseLoc = location.toString();
163 } else {
164 if (location.segmentCount() > 1) {
bchildsa1181702007-06-14 21:47:04 +0000165 baseLoc = ResourcesPlugin.getWorkspace().getRoot().getFile(location).getLocation().toString();
bchilds68f36fc2007-03-01 22:47:33 +0000166 } else {
bchildsa1181702007-06-14 21:47:04 +0000167 baseLoc = ResourcesPlugin.getWorkspace().getRoot().getLocation().append(location).toString();
bchilds68f36fc2007-03-01 22:47:33 +0000168 }
169 }
170 }
171 } finally {
172 if (sModel != null) {
173 sModel.releaseFromRead();
174 }
175 }
176 return baseLoc;
177 }
bchildsa1181702007-06-14 21:47:04 +0000178
bchilds68f36fc2007-03-01 22:47:33 +0000179 /**
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;
bchildsa1181702007-06-14 21:47:04 +0000187 ModelQuery mq = ModelQueryUtil.getModelQuery(element.getOwnerDocument());
bchilds68f36fc2007-03-01 22:47:33 +0000188 if (mq != null) {
189 ed = mq.getCMElementDeclaration(element);
190 }
191 return ed;
192 }
bchildsa1181702007-06-14 21:47:04 +0000193
bchilds68f36fc2007-03-01 22:47:33 +0000194 /**
195 * Returns the attribute node within node at offset
196 *
197 * @param node
198 * @param offset
199 * @return Attr
200 */
201 private Attr getCurrentAttrNode(Node node, int offset) {
bchildsa1181702007-06-14 21:47:04 +0000202 if ((node instanceof IndexedRegion) && ((IndexedRegion) node).contains(offset) && (node.hasAttributes())) {
bchilds68f36fc2007-03-01 22:47:33 +0000203 NamedNodeMap attrs = node.getAttributes();
204 // go through each attribute in node and if attribute contains
205 // offset, return that attribute
206 for (int i = 0; i < attrs.getLength(); ++i) {
207 // assumption that if parent node is of type IndexedRegion,
208 // then its attributes will also be of type IndexedRegion
209 IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
210 if (attRegion.contains(offset)) {
211 return (Attr) attrs.item(i);
212 }
213 }
214 }
215 return null;
216 }
bchildsa1181702007-06-14 21:47:04 +0000217
bchilds68f36fc2007-03-01 22:47:33 +0000218 /**
219 * Returns the node the cursor is currently on in the document. null if no
220 * node is selected
221 *
222 * @param offset
223 * @return Node either element, doctype, text, or null
224 */
225 private Node getCurrentNode(IDocument document, int offset) {
226 // get the current node at the offset (returns either: element,
227 // doctype, text)
228 IndexedRegion inode = null;
229 IStructuredModel sModel = null;
230 try {
bchildsa1181702007-06-14 21:47:04 +0000231 sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document);
bchilds68f36fc2007-03-01 22:47:33 +0000232 inode = sModel.getIndexedRegion(offset);
233 if (inode == null) {
234 inode = sModel.getIndexedRegion(offset - 1);
235 }
236 } finally {
237 if (sModel != null) {
238 sModel.releaseFromRead();
239 }
240 }
bchilds68f36fc2007-03-01 22:47:33 +0000241 if (inode instanceof Node) {
242 return (Node) inode;
243 }
244 return null;
245 }
bchildsa1181702007-06-14 21:47:04 +0000246
bchilds68f36fc2007-03-01 22:47:33 +0000247 /**
248 * Returns an IFile from the given uri if possible, null if cannot find file
249 * from uri.
250 *
251 * @param fileString
252 * file system path
253 * @return returns IFile if fileString exists in the workspace
254 */
255 private IFile getFile(String fileString) {
256 IFile file = null;
bchilds68f36fc2007-03-01 22:47:33 +0000257 if (fileString != null) {
bchildsa1181702007-06-14 21:47:04 +0000258 IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocation(new Path(fileString));
bchilds68f36fc2007-03-01 22:47:33 +0000259 for (int i = 0; i < files.length && file == null; i++) {
260 if (files[i].exists()) {
261 file = files[i];
262 }
263 }
264 }
bchilds68f36fc2007-03-01 22:47:33 +0000265 return file;
266 }
bchildsa1181702007-06-14 21:47:04 +0000267
bchilds68f36fc2007-03-01 22:47:33 +0000268 /**
269 * Create a file from the given uri string
270 *
271 * @param uriString -
272 * assumes uriString is not http://
273 * @return File created from uriString if possible, null otherwise
274 */
275 private File getFileFromUriString(String uriString) {
276 File file = null;
277 try {
278 // first just try to create a file directly from uriString as
279 // default in case create file from uri does not work
280 file = new File(uriString);
bchilds68f36fc2007-03-01 22:47:33 +0000281 // try to create file from uri
282 URI uri = new URI(uriString);
283 file = new File(uri);
284 } catch (Exception e) {
285 // if exception is thrown while trying to create File just ignore
286 // and file will be null
287 }
288 return file;
289 }
bchildsa1181702007-06-14 21:47:04 +0000290
bchilds68f36fc2007-03-01 22:47:33 +0000291 private IRegion getHyperlinkRegion(Node node) {
292 IRegion hyperRegion = null;
bchilds68f36fc2007-03-01 22:47:33 +0000293 if (node != null) {
294 short nodeType = node.getNodeType();
295 if (nodeType == Node.DOCUMENT_TYPE_NODE) {
296 // handle doc type node
297 IDOMNode docNode = (IDOMNode) node;
bchildsa1181702007-06-14 21:47:04 +0000298 hyperRegion = new Region(docNode.getStartOffset(), docNode.getEndOffset() - docNode.getStartOffset());
bchilds68f36fc2007-03-01 22:47:33 +0000299 } else if (nodeType == Node.ATTRIBUTE_NODE) {
300 // handle attribute nodes
301 IDOMAttr att = (IDOMAttr) node;
302 // do not include quotes in attribute value region
303 int regOffset = att.getValueRegionStartOffset();
304 ITextRegion valueRegion = att.getValueRegion();
305 if (valueRegion != null) {
306 int regLength = valueRegion.getTextLength();
307 String attValue = att.getValueRegionText();
308 if (StringUtils.isQuoted(attValue)) {
309 ++regOffset;
310 regLength = regLength - 2;
311 }
312 hyperRegion = new Region(regOffset, regLength);
313 }
314 }
315 }
316 return hyperRegion;
317 }
bchildsa1181702007-06-14 21:47:04 +0000318
bchilds68f36fc2007-03-01 22:47:33 +0000319 /**
320 * Attempts to find an attribute within element that is openable.
321 *
322 * @param element -
323 * cannot be null
324 * @return Attr attribute that can be used for open on, null if no attribute
325 * could be found
326 */
327 private Attr getLinkableAttr(Element element) {
328 CMElementDeclaration ed = getCMElementDeclaration(element);
329 // get the list of attributes for this node
330 NamedNodeMap attrs = element.getAttributes();
331 for (int i = 0; i < attrs.getLength(); ++i) {
332 // check if this attribute is "openOn-able"
333 Attr att = (Attr) attrs.item(i);
334 if (isLinkableAttr(att, ed)) {
335 return att;
336 }
337 }
338 return null;
339 }
bchildsa1181702007-06-14 21:47:04 +0000340
bchilds68f36fc2007-03-01 22:47:33 +0000341 /**
342 * Find the location hint for the given namespaceURI if it exists
343 *
344 * @param elementNode -
345 * cannot be null
346 * @param namespaceURI -
347 * cannot be null
348 * @return location hint (systemId) if it was found, null otherwise
349 */
350 private String getLocationHint(Element elementNode, String namespaceURI) {
bchildsa1181702007-06-14 21:47:04 +0000351 Attr schemaLocNode = elementNode.getAttributeNodeNS(XSI_NAMESPACE_URI, SCHEMA_LOCATION);
bchilds68f36fc2007-03-01 22:47:33 +0000352 if (schemaLocNode != null) {
353 StringTokenizer st = new StringTokenizer(schemaLocNode.getValue());
354 while (st.hasMoreTokens()) {
355 String publicId = st.hasMoreTokens() ? st.nextToken() : null;
356 String systemId = st.hasMoreTokens() ? st.nextToken() : null;
357 // found location hint
358 if (namespaceURI.equalsIgnoreCase(publicId)) {
359 return systemId;
360 }
361 }
362 }
363 return null;
364 }
bchildsa1181702007-06-14 21:47:04 +0000365
bchilds68f36fc2007-03-01 22:47:33 +0000366 /**
367 * Returns the URI string
368 *
369 * @param node -
370 * assumes not null
371 */
372 private String getURIString(Node node, IDocument document) {
373 String resolvedURI = null;
374 // need the base location, publicId, and systemId for URIResolver
375 String baseLoc = null;
376 String publicId = null;
377 String systemId = null;
bchilds68f36fc2007-03-01 22:47:33 +0000378 short nodeType = node.getNodeType();
379 // handle doc type node
380 if (nodeType == Node.DOCUMENT_TYPE_NODE) {
381 baseLoc = getBaseLocation(document);
382 publicId = ((DocumentType) node).getPublicId();
383 systemId = ((DocumentType) node).getSystemId();
384 } else if (nodeType == Node.ATTRIBUTE_NODE) {
385 // handle attribute node
386 Attr attrNode = (Attr) node;
387 String attrName = attrNode.getName();
388 String attrValue = attrNode.getValue();
389 attrValue = StringUtils.strip(attrValue);
390 if (attrValue != null && attrValue.length() > 0) {
391 baseLoc = getBaseLocation(document);
bchilds68f36fc2007-03-01 22:47:33 +0000392 // handle schemaLocation attribute
393 String prefix = DOMNamespaceHelper.getPrefix(attrName);
bchildsa1181702007-06-14 21:47:04 +0000394 String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
bchilds68f36fc2007-03-01 22:47:33 +0000395 if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
396 publicId = attrValue;
bchildsa1181702007-06-14 21:47:04 +0000397 systemId = getLocationHint(attrNode.getOwnerElement(), publicId);
bchilds68f36fc2007-03-01 22:47:33 +0000398 if (systemId == null) {
399 systemId = attrValue;
400 }
bchildsa1181702007-06-14 21:47:04 +0000401 } else if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attrNode))) && (SCHEMA_LOCATION.equals(unprefixedName))) {
bchilds68f36fc2007-03-01 22:47:33 +0000402 // for now just use the first pair
403 // need to look into being more precise
404 StringTokenizer st = new StringTokenizer(attrValue);
405 publicId = st.hasMoreTokens() ? st.nextToken() : null;
406 systemId = st.hasMoreTokens() ? st.nextToken() : null;
407 // else check if xmlns publicId = value
408 } else {
409 systemId = attrValue;
410 }
411 }
412 }
bchilds68f36fc2007-03-01 22:47:33 +0000413 resolvedURI = resolveURI(baseLoc, publicId, systemId);
414 return resolvedURI;
415 }
bchildsa1181702007-06-14 21:47:04 +0000416
bchilds68f36fc2007-03-01 22:47:33 +0000417 /**
418 * Returns true if this uriString is an http string
419 *
420 * @param uriString
421 * @return true if uriString is http string, false otherwise
422 */
423 private boolean isHttp(String uriString) {
424 boolean isHttp = false;
425 if (uriString != null) {
426 String tempString = uriString.toLowerCase();
427 if (tempString.startsWith(HTTP_PROTOCOL)) {
428 isHttp = true;
429 }
430 }
431 return isHttp;
432 }
bchildsa1181702007-06-14 21:47:04 +0000433
bchilds68f36fc2007-03-01 22:47:33 +0000434 /**
435 * Checks to see if the given attribute is openable. Attribute is openable
436 * if it is a namespace declaration attribute or if the attribute value is
437 * of type URI.
438 *
439 * @param attr
440 * cannot be null
441 * @param cmElement
442 * CMElementDeclaration associated with the attribute (can be
443 * null)
444 * @return true if this attribute is "openOn-able" false otherwise
445 */
446 private boolean isLinkableAttr(Attr attr, CMElementDeclaration cmElement) {
447 String attrName = attr.getName();
448 String prefix = DOMNamespaceHelper.getPrefix(attrName);
449 String unprefixedName = DOMNamespaceHelper.getUnprefixedName(attrName);
450 // determine if attribute is namespace declaration
451 if ((XMLNS.equals(prefix)) || (XMLNS.equals(unprefixedName))) {
452 return true;
453 }
bchilds68f36fc2007-03-01 22:47:33 +0000454 // determine if attribute contains schema location
bchildsa1181702007-06-14 21:47:04 +0000455 if ((XSI_NAMESPACE_URI.equals(DOMNamespaceHelper.getNamespaceURI(attr))) && ((SCHEMA_LOCATION.equals(unprefixedName)) || (NO_NAMESPACE_SCHEMA_LOCATION.equals(unprefixedName)))) {
bchilds68f36fc2007-03-01 22:47:33 +0000456 return true;
457 }
bchilds68f36fc2007-03-01 22:47:33 +0000458 // determine if attribute value is of type URI
459 if (cmElement != null) {
bchildsa1181702007-06-14 21:47:04 +0000460 CMAttributeDeclaration attrDecl = (CMAttributeDeclaration) cmElement.getAttributes().getNamedItem(attrName);
461 if ((attrDecl != null) && (attrDecl.getAttrType() != null) && (CMDataType.URI.equals(attrDecl.getAttrType().getDataTypeName()))) {
bchilds68f36fc2007-03-01 22:47:33 +0000462 return true;
463 }
464 }
465 return false;
466 }
bchildsa1181702007-06-14 21:47:04 +0000467
bchilds68f36fc2007-03-01 22:47:33 +0000468 /**
469 * Checks whether the given uriString is really pointing to a file
470 *
471 * @param uriString
472 * @return boolean
473 */
474 private boolean isValidURI(String uriString) {
475 boolean isValid = false;
bchilds68f36fc2007-03-01 22:47:33 +0000476 if (isHttp(uriString)) {
477 isValid = true;
478 } else {
479 File file = getFileFromUriString(uriString);
480 if (file != null) {
481 isValid = file.isFile();
482 }
483 }
484 return isValid;
485 }
bchildsa1181702007-06-14 21:47:04 +0000486
bchilds68f36fc2007-03-01 22:47:33 +0000487 /**
488 * Resolves the given URI information
489 *
490 * @param baseLocation
491 * @param publicId
492 * @param systemId
493 * @return String resolved uri.
494 */
bchildsa1181702007-06-14 21:47:04 +0000495 private String resolveURI(String baseLocation, String publicId, String systemId) {
bchilds68f36fc2007-03-01 22:47:33 +0000496 // dont resolve if there's nothing to resolve
497 if ((baseLocation == null) && (publicId == null) && (systemId == null)) {
498 return null;
499 }
bchildsa1181702007-06-14 21:47:04 +0000500 return URIResolverPlugin.createResolver().resolve(baseLocation, publicId, systemId);
bchilds68f36fc2007-03-01 22:47:33 +0000501 }
502}