PageRenderTime 206ms CodeModel.GetById 168ms app.highlight 32ms RepoModel.GetById 1ms app.codeStats 1ms

/bundles/plugins-trunk/XML/xml/hyperlinks/XMLHyperlinkSource.java

#
Java | 436 lines | 278 code | 28 blank | 130 comment | 70 complexity | 80f605ea88402e3a018c778c5013b7b5 MD5 | raw file
  1/*
  2 * XMLHyperlinkSource.java - Hyperlink source from the XML plugin
  3 * :tabSize=8:indentSize=8:noTabs=false:
  4 * :folding=explicit:collapseFolds=1:
  5 *
  6 * Copyright (C) 2011 Eric Le Lay
  7 *
  8 * This program is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU General Public License
 10 * as published by the Free Software Foundation; either version 2
 11 * of the License, or any later version.
 12 * This program is distributed in the hope that it will be useful,
 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15 * GNU General Public License for more details.
 16 *
 17 * You should have received a copy of the GNU General Public License
 18 * along with this program; if not, write to the Free Software
 19 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 20 */
 21package xml.hyperlinks;
 22
 23import java.io.Reader;
 24import java.net.URI;
 25import java.util.Arrays;
 26import java.util.regex.Pattern;
 27import java.util.regex.Matcher;
 28
 29import org.gjt.sp.jedit.View;
 30import org.gjt.sp.jedit.jEdit;
 31import org.gjt.sp.jedit.Buffer;
 32import org.gjt.sp.util.Log;
 33
 34import sidekick.SideKickParsedData;
 35import sidekick.IAsset;
 36import sidekick.util.ElementUtil;
 37
 38import gatchan.jedit.hyperlinks.*;
 39
 40import static xml.Debug.*;
 41import xml.Resolver;
 42import xml.CharSequenceReader;
 43import xml.XmlParsedData;
 44import xml.parser.javacc.*;
 45import xml.parser.XmlTag;
 46import xml.completion.ElementDecl;
 47import xml.completion.IDDecl;
 48
 49/**
 50 * Provides hyperlinks from XML attributes.
 51 * Supported hyperlinks are :
 52 * <ul>
 53 * 	<li>XInclude (&lt;xi:include href="..."/&gt;). Fragments are not supported:
 54 *	      the buffer is opened and that's all.
 55 *	</li>
 56 *	<li>simple XLinks (&lt;myelt xlink:href="..."/&gt;). Only simple links are supported:
 57 *	      the buffer is opened and that's all.
 58 *	</li>
 59 *	<li>IDREF attributes, when the buffer has been parsed and the attribute has the
 60 *	      IDREF datatype. This includes docbook 4.x links (&lt;xref linkend="id"/&gt;).
 61 *	</li>
 62 *	<li>XML Schema (XSD) schema location (&lt;myelt xsi:schemaLocation="ns1 url1..."&gt;)
 63 *	      and no namespace schema location (&lt;myelt xsi:noNamespaceSchemaLocation="url"&gt;).
 64 *	</li>
 65 *	<li>attributes with datatype anyURI
 66 *	      eg. XSD include and import (&lt;xs:include schemaLocation="url"/&gt;).
 67 *	</li>
 68 *	<li>DocBook ulink (&lt;ulink url="..."&gt;)</li>
 69 * </ul>
 70 *
 71 * @todo	HTML attributes
 72 * @todo	HTML BASE tag (beware: it's in HTML/HEAD)
 73 *
 74 * @author Eric Le Lay
 75 * @version $Id: XMLHyperlinkSource.java 21198 2012-02-24 11:19:21Z kerik-sf $
 76 */
 77public class XMLHyperlinkSource implements HyperlinkSource
 78{
 79	/**
 80	 * Returns the hyperlink for the given offset.
 81	 * returns an hyperlink as soon as pointer enters the attribute's value
 82	 *
 83	 * @param buffer the buffer
 84	 * @param offset the offset
 85	 * @return the hyperlink (or null if there is no hyperlink)
 86	 */
 87	public Hyperlink getHyperlink(Buffer buffer, int offset){
 88		View view = jEdit.getActiveView();
 89		XmlParsedData data = XmlParsedData.getParsedData(view, false);
 90		if(data==null)return null;
 91		
 92		IAsset asset = data.getAssetAtOffset(offset);
 93		if(asset == null){
 94			Log.log(Log.DEBUG, XMLHyperlinkSource.class,"no Sidekick asset here");
 95			return null;
 96		} else {
 97			int wantedLine = buffer.getLineOfOffset(offset);
 98			int wantedLineOffset = buffer.getVirtualWidth(wantedLine, offset - buffer.getLineStartOffset(wantedLine));
 99
100
101			/* use the XML javacc parser to parse the start tag.*/
102			int max = buffer.getLength();
103			int sA = asset.getStart().getOffset();
104			int lA = asset.getEnd().getOffset()-sA;
105			if(sA < 0 || sA > max){
106				return null;
107			}
108			if(lA < 0 || sA+lA > max){
109				return null;
110			}
111			CharSequence toParse = buffer.getSegment(sA,lA);
112			int line = buffer.getLineOfOffset(sA);
113			int col = buffer.getVirtualWidth(line, sA-buffer.getLineStartOffset(line)+1);
114			Reader r = new CharSequenceReader(toParse);
115			XmlParser parser = new XmlParser(r, line+1, col);
116			parser.setTabSize( buffer.getTabSize() );
117			try{
118				XmlDocument.XmlElement startTag = parser.Tag();
119				int start = ElementUtil.createStartPosition(buffer,startTag).getOffset();
120				int end= ElementUtil.createEndPosition(buffer,startTag).getOffset();
121				/* if the offset is inside start tag */
122				if(offset <= end)
123				{
124					XmlDocument.AttributeList al = ((XmlDocument.Tag)startTag).attributeList;
125					if(al == null){
126						if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,XMLHyperlinkSource.class,"no attribute in this element");
127						return null;
128					}else{
129						for(XmlDocument.Attribute att: al.attributes){
130							// offset is inside attribute's value
131							if(  (    att.getValueStartLocation().line < wantedLine+1
132							       || (att.getValueStartLocation().line == wantedLine+1
133						                   && att.getValueStartLocation().column <= wantedLineOffset)
134						              )
135						              &&
136						              (    att.getEndLocation().line > wantedLine+1
137							       || (att.getEndLocation().line == wantedLine+1
138						                   && att.getEndLocation().column > wantedLineOffset)
139						              )
140						          )
141						        {
142						        	if(asset instanceof XmlTag) {
143						        		return getHyperlinkForAttribute(buffer, offset, data, asset, att);
144						        	}
145						        }
146						}
147						if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,XMLHyperlinkSource.class,"not inside attributes");
148						return null;
149					}
150				}else{
151					return null;
152				}
153			}catch(ParseException pe){
154				if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG, XMLHyperlinkSource.class, "error parsing element", pe);
155				return null;
156			}
157		}
158	}
159	
160	/**
161	 * get an hyperlink for an identified XML attribute
162	 * @param	buffer	current buffer
163	 * @param	offset	offset where an hyperlink is required in current buffer
164	 * @param	data		sidekick tree for current buffer
165	 * @param	asset		element containing offset
166	 * @param	att		parsed attribute
167	 */
168	public Hyperlink getHyperlinkForAttribute(
169		Buffer buffer, int offset, XmlParsedData data, 
170		IAsset asset, XmlDocument.Attribute att)
171	{
172		XmlTag sideKickTag = (XmlTag)asset;
173		
174		if(sideKickTag.attributes == null){
175			if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,XMLHyperlinkSource.class,"Sidekick version of this element doesn't have attributes");
176			return null;
177		}
178		
179		int attIndex = sideKickTag.attributes.getIndex(att.name);
180		if(attIndex < 0){
181			if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,XMLHyperlinkSource.class,"Sidekick version of this element doesn't have this attribute: "+att.name);
182			return null;
183		}
184		
185		String tagNS = sideKickTag.namespace;
186		String tagLocalName = sideKickTag.getLocalName();
187		
188		String ns = sideKickTag.attributes.getURI(attIndex);
189		String localName = att.name.contains(":")? sideKickTag.attributes.getLocalName(attIndex) : att.name;
190		String value = sideKickTag.attributes.getValue(attIndex);
191		
192		Hyperlink h = getHyperlinkForAttribute(buffer, offset,
193			tagNS,tagLocalName, ns, localName, value,
194			data, sideKickTag, att);
195		
196		if(h == null) {
197		
198			ElementDecl eltDecl  = data.getElementDecl(sideKickTag.getName(),offset);
199			if(eltDecl == null){
200				if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,XMLHyperlinkSource.class,"no element declaration for "+tagLocalName);
201			}else{
202				ElementDecl.AttributeDecl attDecl = eltDecl.attributeHash.get(localName);
203				if(attDecl == null){
204					if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,XMLHyperlinkSource.class,"no attribute declaration for "+localName);
205					return null;
206				}else{
207					if("IDREF".equals(attDecl.type)){
208						return getHyperlinkForIDREF(buffer, data, value, att);
209					}else if("anyURI".equals(attDecl.type)){
210						// FIXME: shall it use xml:base ?
211						String href =  resolve(value, buffer, offset, data, sideKickTag, false);
212						if(href!=null){
213							return newJEditOpenFileHyperlink(buffer, att, href);
214						}
215					}else if("IDREFS".equals(attDecl.type)){
216						return getHyperlinkForIDREFS(buffer, offset, data, value, att);
217					}
218					return null;
219				}
220			}
221			
222		} else {
223			return h;
224		}
225		return null;
226	}
227	
228	/**
229	 * creates an hyperlink to the location of the element with id (in same or another buffer).
230	 * @param	buffer	current buffer
231	 * @param	data		sidekick tree
232	 * @param	id		id we are looking for
233	 * @param	att		parsed attribute (for hyperlink boundaries)
234	 */
235	public Hyperlink getHyperlinkForIDREF(Buffer buffer,
236		XmlParsedData data, String id, XmlDocument.Attribute att)
237	{
238		IDDecl idDecl = data.getIDDecl(id);
239		if(idDecl == null){
240			return null;
241		} else{
242			return newJEditOpenFileAndGotoHyperlink(buffer, att,
243			idDecl.uri, idDecl.line, idDecl.column);
244		}
245	}
246	
247	// {{{ getHyperlinkForIDREFS() method
248	private static final Pattern noWSPattern = Pattern.compile("[^\\s]+");
249	/**
250	 * creates an hyperlink to the location of the element with id (in same or another buffer)
251	 * @param	buffer	current buffer
252	 * @param	offset	offset of required hyperlink
253	 * @param	data		sidekick tree
254	 * @param	attValue		ids in the attribute
255	 * @param	att		parsed attribute (for hyperlink boundaries)
256	 */
257	public Hyperlink getHyperlinkForIDREFS(Buffer buffer, int offset,
258		XmlParsedData data, String attValue, XmlDocument.Attribute att)
259	{
260		int attStart = xml.ElementUtil.createOffset(buffer, att.getValueStartLocation());
261		// +1 for the quote around the attribute value
262		attStart++;
263		
264		Matcher m = noWSPattern.matcher(attValue);
265		while(m.find()){
266			int st = m.start(0);
267			int nd = m.end(0);
268			if(attStart + st <= offset && attStart + nd >= offset){
269				IDDecl idDecl = data.getIDDecl(m.group(0));
270				if(idDecl==null)return null;
271				int start = attStart + st;
272				int end= attStart + nd;
273				int line = buffer.getLineOfOffset(start);
274				return new jEditOpenFileAndGotoHyperlink(start, end, line, idDecl.uri, idDecl.line, idDecl.column);
275			}
276		}
277		return null;
278	}//}}}
279	
280	//{{{ getHyperlinkForAttribute(tagNS,tagName,attNS,attName) method
281	private static final Pattern nsURIPairsPattern = Pattern.compile("[^\\s]+\\s+([^\\s]+)");
282	/**
283	 * recognize hyperlink attributes by their parent element's
284	 * namespace:localname and/or their namespace:localname
285	 */
286	public Hyperlink getHyperlinkForAttribute(Buffer buffer, int offset,
287		String tagNS, String tagLocalName,
288		String attNS, String attLocalName, String attValue,
289		XmlParsedData data, XmlTag tag, XmlDocument.Attribute att)
290	{
291		String href = null; 
292		if("http://www.w3.org/2001/XInclude".equals(tagNS)
293			&& "include".equals(tagLocalName)
294			&& "href".equals(attLocalName))
295		{
296			href = resolve(attValue, buffer, offset, data, tag, true);
297		} else if("http://www.w3.org/1999/xlink".equals(attNS)
298			&& "href".equals(attLocalName))
299		{
300			href = resolve(attValue, buffer, offset, data, tag, true);
301		} else if("http://www.w3.org/2001/XMLSchema-instance".equals(attNS)
302			&& "noNamespaceSchemaLocation".equals(attLocalName))
303		{
304			href = resolve(attValue, buffer, offset, data, tag, false);
305		} else if("".equals(tagNS) && "ulink".equals(tagLocalName)
306				&& "url".equals(attLocalName))
307		{
308			href = resolve(attValue, buffer, offset, data, tag, false);
309		} else if("http://www.w3.org/2001/XMLSchema-instance".equals(attNS)
310			&& "schemaLocation".equals(attLocalName))
311		{
312			
313			// +1 for the quote around the attribute value
314			int attStart = xml.ElementUtil.createOffset(buffer, att.getValueStartLocation()) +1;
315			
316			Matcher m = nsURIPairsPattern.matcher(attValue);
317			// find will accept unbalanced pairs of ns->uri
318			while(m.find()){
319				int st = m.start(1);
320				int nd = m.end(1);
321				if(attStart + st <= offset && attStart + nd >= offset){
322					href = resolve(m.group(1), buffer, offset, data, tag, false);
323					if(href==null)href=m.group(1);
324					int start = attStart + st;
325					int end= attStart + nd;
326					int line = buffer.getLineOfOffset(start);
327					return new jEditOpenFileHyperlink(start, end, line, href);
328				}
329			}
330		}
331		if(href==null){
332			return null;
333		}else{
334			return newJEditOpenFileHyperlink(
335				buffer, att, href);
336		}
337	}//}}}
338	
339	/**
340	 * resolve a potentially relative uri using xml:base attributes,
341	 * the buffer's URL, xml.Resolver.
342	 * Has the effect of opening the cached document if it's in cache (eg. docbook XSD if not in catalog).
343	 * Maybe this is not desirable, because if there is a relative link in this document, it won't work
344	 * because the document will be .jedit/dtds/cachexxxxx.xml and not the real url.
345	 *
346	 * @param	uri		text of uri to reach
347	 * @param	buffer	current buffer
348	 * @param	offset	offset in current buffer where an hyperlink is required
349	 * @param	data		SideKick parsed data
350	 * @param	tag		SideKick asset
351	 * @param	useXmlBase	should xml:base attribute be used to resolve uri (only for XML!)
352	 *
353	 * @return	resolved URL
354	 */
355	public String resolve(String uri, Buffer buffer, int offset, XmlParsedData data, XmlTag tag,
356		boolean useXmlBase)
357	{
358		String href = null;
359		String base = xml.PathUtilities.pathToURL(buffer.getPath());
360		if(useXmlBase){
361			try {
362				URI baseURI = URI.create(base);
363				Object[] pathObjs = data.getObjectsTo(offset);
364				// go down the tree, resolving existing xml:base uri if they exist
365				for(int i=1; i<pathObjs.length;i++){  //first object (i==0) is a SourceAsset for the file
366					XmlTag t = (XmlTag)pathObjs[i];
367					String newBase = t.attributes.getValue("xml:base"); // xml is a reserved prefix
368					if(newBase!=null){
369						baseURI = baseURI.resolve(newBase);
370					}
371				}
372				if(!base.equals(baseURI.toString())){
373					// add a dummy component, otherwise xml.Resolver
374					// removes the last part of the xml:base
375					// FIXME: review xml.Resolver : it should only be used with URLs
376					// for current, now, so could use URI.resolve() instead of removing
377					// last part of the path to get the parent...
378					baseURI = baseURI.resolve("dummy");
379					base = baseURI.toString();
380				}
381			}catch(IllegalArgumentException e){
382				Log.log(Log.WARNING, XMLHyperlinkSource.class, "error resolving uri", e);
383			}
384		}
385		try{
386			href = Resolver.instance().resolveEntityToPath(
387				"", /*name*/
388				"", /*publicId*/
389				base, /*current, augmented by xml:base */
390				uri);
391		}catch(java.io.IOException ioe){
392			Log.log(Log.ERROR,XMLHyperlinkSource.class,"error resolving href="+uri,ioe);
393		}
394		return href;
395	}
396	
397	/**
398	 * create an hyperlink for attribute att.
399	 * the hyperlink will span whole attribute value
400	 * @param	buffer	current buffer
401	 * @param	att		parsed attribute
402	 * @param	href		uri to open
403	 */
404	public Hyperlink newJEditOpenFileHyperlink(
405		Buffer buffer, XmlDocument.Attribute att, String href)
406	{
407		int start = xml.ElementUtil.createOffset(buffer,att.getValueStartLocation());
408		int end= xml.ElementUtil.createOffset(buffer,att.getEndLocation());
409        	int line = buffer.getLineOfOffset(start);
410        	return new jEditOpenFileHyperlink(start, end, line, href);
411	}
412
413	/**
414	 * create an hyperlink for attribute att.
415	 * the hyperlink will span whole attribute value
416	 * @param	buffer	current buffer
417	 * @param	att		parsed attribute
418	 * @param	href		uri to open
419	 * @param	gotoLine	target line in buffer
420	 * @param	gotoCol	target column in buffer
421	 */
422	public Hyperlink newJEditOpenFileAndGotoHyperlink(
423		Buffer buffer, XmlDocument.Attribute att, String href, int gotoLine, int gotoCol)
424	{
425		int start = xml.ElementUtil.createOffset(buffer,att.getValueStartLocation());
426		int end= xml.ElementUtil.createOffset(buffer,att.getEndLocation());
427        	int line = buffer.getLineOfOffset(start);
428        	return new jEditOpenFileAndGotoHyperlink(start, end, line, href, gotoLine, gotoCol);
429	}
430
431	public static HyperlinkSource create(){
432		return new FallbackHyperlinkSource(
433			Arrays.asList(new XMLHyperlinkSource(),
434				new gatchan.jedit.hyperlinks.url.URLHyperlinkSource()));
435	}
436}