/**
 * File:    WEAboutDialog.java
 * Created: 16.09.2007
 *
 *
/*******************************************************************************
 * Copyright (c) 2005-2011
 * University of Augsburg, Germany <www.ds-lab.org>
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Markus Bauer, Programming distributed Systems Lab, University of Augsburg 
 *    	- initial API and implementation
 *******************************************************************************/

package org.eclipse.jwt.we.misc.dialogs.about;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.StringTokenizer;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.TrayDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jwt.we.Plugin;
import org.eclipse.jwt.we.PluginProperties;
import org.eclipse.jwt.we.misc.util.GeneralHelper;
import org.eclipse.jwt.we.misc.views.Views;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.branding.IProductConstants;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.osgi.framework.Bundle;

/**
 * This class shows an About Dialog with an image, text and a OK button.
 * 
 * If it is run in plugin mode, it reads the image and the text from
 * plugin.properties. If run in rcp mode, it uses the information provided by
 * the product file.
 * 
 * This class is mainly based on:
 * 
 * org.eclipse.ui.internal.dialogs.AboutDialog
 * org.eclipse.ui.internal.dialogs.ProductInfoDialog
 * 
 * @author Christian Saad (csaad@gmx.net), Programming distributed Systems Lab,
 *         University of Augsburg, Germany, www.ds-lab.org
 */
public class WEAboutDialog extends TrayDialog
{

	private final static int MAX_IMAGE_WIDTH_FOR_TEXT = 250;

	private ArrayList images = new ArrayList();

	private StyledText text;

	private WEAboutItem item;

	private Cursor handCursor;

	private Cursor busyCursor;

	private boolean mouseDown = false;

	private boolean dragEvent = false;

	private String aboutText;

	private Image aboutImage;


	/**
	 * Constructor.
	 * 
	 * @param parentShell
	 *            The parent of the dialog.
	 */
	public WEAboutDialog(Shell parentShell)
	{
		super(parentShell);

		// remove context button
		setHelpAvailable(false);
		setDialogHelpAvailable(false);
	}


	protected void configureShell(Shell newShell)
	{
		super.configureShell(newShell);

		// set properties according to the program mode (plugin or rcp)
		if (GeneralHelper.isPluginMode())
		{
			// set title text
			newShell.setText(NLS.bind(PluginProperties.about_title,
					PluginProperties.plugin_Name));
			PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell,
					"org.eclipse.ui.about_dialog_context"); //$NON-NLS-1$

			// get the image
			aboutImage = null;
			ImageDescriptor imageDescriptor = Plugin.getInstance().getFactoryRegistry()
					.getImageFactory(Views.getInstance().getSelectedView())
					.createImageDescriptor(PluginProperties.about_image_file);
			if (imageDescriptor != null)
			{
				aboutImage = Plugin.getDefault().getFactoryRegistry().getImageFactory()
						.getImage(imageDescriptor);
			}

			// get the text
			aboutText = readAboutText(PluginProperties.about_text_file);
		}
		else
		{
			// set title text
			newShell.setText(NLS.bind(PluginProperties.about_title, Platform.getProduct()
					.getName()));
			PlatformUI.getWorkbench().getHelpSystem().setHelp(newShell,
					"org.eclipse.ui.about_dialog_context"); //$NON-NLS-1$

			// get the image
			ImageDescriptor imageDescriptor = null;
			String aboutProperty = Platform.getProduct().getProperty(
					IProductConstants.ABOUT_IMAGE);
			Bundle definingBundle = Platform.getProduct().getDefiningBundle();

			URL url = null;
			try
			{
				if (aboutProperty != null)
				{
					url = new URL(aboutProperty);
				}
			}
			catch (MalformedURLException e)
			{
				if (definingBundle != null)
				{
					url = FileLocator.find(definingBundle, new Path(aboutProperty), null);
				}
			}
			imageDescriptor = url == null ? null : ImageDescriptor.createFromURL(url);

			if (imageDescriptor != null)
			{
				aboutImage = Plugin.getDefault().getFactoryRegistry().getImageFactory()
						.getImage(imageDescriptor);
			}

			// get the text
			String property = Platform.getProduct().getProperty(
					IProductConstants.ABOUT_TEXT);
			if (property == null)
			{
				aboutText = ""; //$NON-NLS-1$
			}
			if (property.indexOf('{') == -1)
			{
				aboutText = property;
			}
		}
	}


	/**
	 * Add buttons to the dialog's button bar.
	 * 
	 * Subclasses should override.
	 * 
	 * @param parent
	 *            the button bar composite
	 */
	protected void createButtonsForButtonBar(Composite parent)
	{
		parent.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		Label l = new Label(parent, SWT.NONE);
		l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		GridLayout layout = (GridLayout) parent.getLayout();
		layout.numColumns++;
		layout.makeColumnsEqualWidth = false;

		Button b = createButton(parent, IDialogConstants.OK_ID,
				IDialogConstants.OK_LABEL, true);
		b.setFocus();
	}


	/**
	 * Creates and returns the contents of the upper part of the dialog (above
	 * the button bar).
	 * 
	 * Subclasses should overide.
	 * 
	 * @param parent
	 *            the parent composite to contain the dialog area
	 * @return the dialog area control
	 */
	protected Control createDialogArea(Composite parent)
	{
		final Cursor hand = new Cursor(parent.getDisplay(), SWT.CURSOR_HAND);
		final Cursor busy = new Cursor(parent.getDisplay(), SWT.CURSOR_WAIT);
		setHandCursor(hand);
		setBusyCursor(busy);
		getShell().addDisposeListener(new DisposeListener()
		{

			public void widgetDisposed(DisposeEvent e)
			{
				setHandCursor(null);
				hand.dispose();
				setBusyCursor(null);
				busy.dispose();
			}
		});

		// if the about image is small enough, then show the text
		if (aboutImage == null
				|| aboutImage.getBounds().width <= MAX_IMAGE_WIDTH_FOR_TEXT)
		{
			if (aboutText != null)
			{
				setItem(scan(aboutText));
			}
		}

		if (aboutImage != null)
		{
			images.add(aboutImage);
		}

		// create a composite which is the parent of the top area and the bottom
		// button bar, this allows there to be a second child of this composite
		// with
		// a banner background on top but not have on the bottom
		Composite workArea = new Composite(parent, SWT.NONE);
		GridLayout workLayout = new GridLayout();
		workLayout.marginHeight = 0;
		workLayout.marginWidth = 0;
		workLayout.verticalSpacing = 0;
		workLayout.horizontalSpacing = 0;
		workArea.setLayout(workLayout);
		workArea.setLayoutData(new GridData(GridData.FILL_BOTH));

		// page group
		Color background = JFaceColors.getBannerBackground(parent.getDisplay());
		Color foreground = JFaceColors.getBannerForeground(parent.getDisplay());
		Composite top = (Composite) super.createDialogArea(workArea);

		// override any layout inherited from createDialogArea
		GridLayout layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		layout.verticalSpacing = 0;
		layout.horizontalSpacing = 0;
		top.setLayout(layout);
		top.setLayoutData(new GridData(GridData.FILL_BOTH));
		top.setBackground(background);
		top.setForeground(foreground);

		// the image & text
		Composite topContainer = new Composite(top, SWT.NONE);
		topContainer.setBackground(background);
		topContainer.setForeground(foreground);

		layout = new GridLayout();
		layout.numColumns = (aboutImage == null || getItem() == null ? 1 : 2);
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		layout.verticalSpacing = 0;
		layout.horizontalSpacing = 0;
		topContainer.setLayout(layout);
		GridData data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		data.grabExcessHorizontalSpace = true;
		topContainer.setLayoutData(data);

		// image on left side of dialog
		if (aboutImage != null)
		{
			Label imageLabel = new Label(topContainer, SWT.NONE);
			imageLabel.setBackground(background);
			imageLabel.setForeground(foreground);

			data = new GridData();
			data.horizontalAlignment = GridData.FILL;
			data.verticalAlignment = GridData.BEGINNING;
			data.grabExcessHorizontalSpace = false;
			imageLabel.setLayoutData(data);
			imageLabel.setImage(aboutImage);
		}

		if (getItem() != null)
		{
			// there is no margins around the image, so insert an extra
			// composite
			// here to provide some margins for the text.
			Composite textContainer = new Composite(topContainer, SWT.NONE);
			textContainer.setBackground(background);
			textContainer.setForeground(foreground);

			layout = new GridLayout();
			layout.numColumns = 1;
			textContainer.setLayout(layout);
			data = new GridData();
			data.horizontalAlignment = GridData.FILL;
			data.verticalAlignment = GridData.BEGINNING;
			data.grabExcessHorizontalSpace = true;
			textContainer.setLayoutData(data);

			// text on the right
			text = new StyledText(textContainer, SWT.MULTI | SWT.READ_ONLY);
			text.setCaret(null);
			text.setFont(parent.getFont());
			data = new GridData();
			data.horizontalAlignment = GridData.FILL;
			data.verticalAlignment = GridData.BEGINNING;
			data.grabExcessHorizontalSpace = true;
			text.setText(getItem().getText());
			text.setLayoutData(data);
			text.setCursor(null);
			text.setBackground(background);
			text.setForeground(foreground);

			setLinkRanges(text, getItem().getLinkRanges());
			addListeners(text);
		}

		// horizontal bar
		Label bar = new Label(workArea, SWT.HORIZONTAL | SWT.SEPARATOR);
		data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		bar.setLayoutData(data);

		// add image buttons for bundle groups that have them
		Composite bottom = (Composite) super.createDialogArea(workArea);
		// override any layout inherited from createDialogArea
		layout = new GridLayout();
		bottom.setLayout(layout);
		bottom.setLayoutData(new GridData(GridData.FILL_BOTH));

		// spacer
		bar = new Label(bottom, SWT.NONE);
		data = new GridData();
		data.horizontalAlignment = GridData.FILL;
		bar.setLayoutData(data);

		return workArea;
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.dialogs.TrayDialog#close()
	 */
	public boolean close()
	{
		// dispose all images
		for (int i = 0; i < images.size(); ++i)
		{
			Image image = (Image) images.get(i);
			image.dispose();
		}

		return super.close();
	}


	/**
	 * Reads the about text from the specified file.
	 * 
	 * @return About text.
	 */
	private String readAboutText(String filename)
	{
		String result = ""; //$NON-NLS-1$
		try
		{
			URL aboutURL = Plugin.getDefault().getBundle().getEntry(filename);

			BufferedReader br = new BufferedReader(new InputStreamReader(aboutURL
					.openStream()));

			String temp = br.readLine();
			while (temp != null && temp.length() != 0)
			{
				result += "\n" + temp; //$NON-NLS-1$
				temp = br.readLine();
			}
		}
		catch (Exception e)
		{
			MessageDialog.openError(GeneralHelper.getActiveShell(), "Error", //$NON-NLS-1$
					"Could not read about text file " //$NON-NLS-1$
							+ PluginProperties.about_text_file + "."); //$NON-NLS-1$
		}

		return result;
	}


	/**
	 * Adds listeners to the given styled text
	 */
	protected void addListeners(StyledText styledText)
	{
		styledText.addMouseListener(new MouseAdapter()
		{

			public void mouseDown(MouseEvent e)
			{
				if (e.button != 1)
				{
					return;
				}
				mouseDown = true;
			}


			public void mouseUp(MouseEvent e)
			{
				mouseDown = false;
				StyledText text = (StyledText) e.widget;
				int offset = text.getCaretOffset();
				if (dragEvent)
				{
					// don't activate a link during a drag/mouse up operation
					dragEvent = false;
					if (item != null && item.isLinkAt(offset))
					{
						text.setCursor(handCursor);
					}
				}
				else if (item != null && item.isLinkAt(offset))
				{
					text.setCursor(busyCursor);
					openLink(item.getLinkAt(offset));
					StyleRange selectionRange = getCurrentRange(text);
					text.setSelectionRange(selectionRange.start, selectionRange.length);
					text.setCursor(null);
				}
			}
		});

		styledText.addMouseMoveListener(new MouseMoveListener()
		{

			public void mouseMove(MouseEvent e)
			{
				// Do not change cursor on drag events
				if (mouseDown)
				{
					if (!dragEvent)
					{
						StyledText text = (StyledText) e.widget;
						text.setCursor(null);
					}
					dragEvent = true;
					return;
				}
				StyledText text = (StyledText) e.widget;
				int offset = -1;
				try
				{
					offset = text.getOffsetAtLocation(new Point(e.x, e.y));
				}
				catch (IllegalArgumentException ex)
				{
					// leave value as -1
				}
				if (offset == -1)
				{
					text.setCursor(null);
				}
				else if (item != null && item.isLinkAt(offset))
				{
					text.setCursor(handCursor);
				}
				else
				{
					text.setCursor(null);
				}
			}
		});

		styledText.addTraverseListener(new TraverseListener()
		{

			public void keyTraversed(TraverseEvent e)
			{
				StyledText text = (StyledText) e.widget;
				switch (e.detail)
				{
					case SWT.TRAVERSE_ESCAPE:
						e.doit = true;
						break;
					case SWT.TRAVERSE_TAB_NEXT:
						// Previously traverse out in the backward direction?
						Point nextSelection = text.getSelection();
						int charCount = text.getCharCount();
						if ((nextSelection.x == charCount)
								&& (nextSelection.y == charCount))
						{
							text.setSelection(0);
						}
						StyleRange nextRange = findNextRange(text);
						if (nextRange == null)
						{
							// Next time in start at beginning, also used by
							// TRAVERSE_TAB_PREVIOUS to indicate we traversed
							// out
							// in the forward direction
							text.setSelection(0);
							e.doit = true;
						}
						else
						{
							text.setSelectionRange(nextRange.start, nextRange.length);
							e.doit = true;
							e.detail = SWT.TRAVERSE_NONE;
						}
						break;
					case SWT.TRAVERSE_TAB_PREVIOUS:
						// Previously traverse out in the forward direction?
						Point previousSelection = text.getSelection();
						if ((previousSelection.x == 0) && (previousSelection.y == 0))
						{
							text.setSelection(text.getCharCount());
						}
						StyleRange previousRange = findPreviousRange(text);
						if (previousRange == null)
						{
							// Next time in start at the end, also used by
							// TRAVERSE_TAB_NEXT to indicate we traversed out
							// in the backward direction
							text.setSelection(text.getCharCount());
							e.doit = true;
						}
						else
						{
							text.setSelectionRange(previousRange.start,
									previousRange.length);
							e.doit = true;
							e.detail = SWT.TRAVERSE_NONE;
						}
						break;
					default:
						break;
				}
			}
		});

		// Listen for Tab and Space to allow keyboard navigation
		styledText.addKeyListener(new KeyAdapter()
		{

			public void keyPressed(KeyEvent event)
			{
				StyledText text = (StyledText) event.widget;
				if (event.character == ' ' || event.character == SWT.CR)
				{
					if (item != null)
					{
						// Be sure we are in the selection
						int offset = text.getSelection().x + 1;

						if (item.isLinkAt(offset))
						{
							text.setCursor(busyCursor);
							openLink(item.getLinkAt(offset));
							StyleRange selectionRange = getCurrentRange(text);
							text.setSelectionRange(selectionRange.start,
									selectionRange.length);
							text.setCursor(null);
						}
					}
					return;
				}
			}
		});
	}


	/**
	 * Gets the busy cursor.
	 * 
	 * @return the busy cursor
	 */
	protected Cursor getBusyCursor()
	{
		return busyCursor;
	}


	/**
	 * Sets the busy cursor.
	 * 
	 * @param busyCursor
	 *            the busy cursor
	 */
	protected void setBusyCursor(Cursor busyCursor)
	{
		this.busyCursor = busyCursor;
	}


	/**
	 * Gets the hand cursor.
	 * 
	 * @return Returns a hand cursor
	 */
	protected Cursor getHandCursor()
	{
		return handCursor;
	}


	/**
	 * Sets the hand cursor.
	 * 
	 * @param handCursor
	 *            The hand cursor to set
	 */
	protected void setHandCursor(Cursor handCursor)
	{
		this.handCursor = handCursor;
	}


	/**
	 * Gets the about item.
	 * 
	 * @return the about item
	 */
	protected WEAboutItem getItem()
	{
		return item;
	}


	/**
	 * Sets the about item.
	 * 
	 * @param item
	 *            about item
	 */
	protected void setItem(WEAboutItem item)
	{
		this.item = item;
	}


	/**
	 * Find the range of the current selection.
	 */
	protected StyleRange getCurrentRange(StyledText text)
	{
		StyleRange[] ranges = text.getStyleRanges();
		int currentSelectionEnd = text.getSelection().y;
		int currentSelectionStart = text.getSelection().x;

		for (int i = 0; i < ranges.length; i++)
		{
			if ((currentSelectionStart >= ranges[i].start)
					&& (currentSelectionEnd <= (ranges[i].start + ranges[i].length)))
			{
				return ranges[i];
			}
		}
		return null;
	}


	/**
	 * Find the next range after the current selection.
	 */
	protected StyleRange findNextRange(StyledText text)
	{
		StyleRange[] ranges = text.getStyleRanges();
		int currentSelectionEnd = text.getSelection().y;

		for (int i = 0; i < ranges.length; i++)
		{
			if (ranges[i].start >= currentSelectionEnd)
			{
				return ranges[i];
			}
		}
		return null;
	}


	/**
	 * Find the previous range before the current selection.
	 */
	protected StyleRange findPreviousRange(StyledText text)
	{
		StyleRange[] ranges = text.getStyleRanges();
		int currentSelectionStart = text.getSelection().x;

		for (int i = ranges.length - 1; i > -1; i--)
		{
			if ((ranges[i].start + ranges[i].length - 1) < currentSelectionStart)
			{
				return ranges[i];
			}
		}
		return null;
	}


	/**
	 * display an error message
	 */
	private void openWebBrowserError(final String href, final Throwable t)
	{
		MessageDialog.openError(GeneralHelper.getActiveShell(), "Error", //$NON-NLS-1$
				"Unable to open web browser for " + href + "."); //$NON-NLS-1$ //$NON-NLS-2$
	}


	/**
	 * Open a link
	 */
	protected void openLink(String href)
	{
		// format the href for an html file (file:///<filename.html>
		// required for Mac only.
		if (href.startsWith("file:")) { //$NON-NLS-1$
			href = href.substring(5);
			while (href.startsWith("/")) { //$NON-NLS-1$
				href = href.substring(1);
			}
			href = "file:///" + href; //$NON-NLS-1$
		}
		IWorkbenchBrowserSupport support = PlatformUI.getWorkbench().getBrowserSupport();
		try
		{
			IWebBrowser browser = support.getExternalBrowser();
			browser.openURL(new URL(urlEncodeForSpaces(href.toCharArray())));
		}
		catch (MalformedURLException e)
		{
			openWebBrowserError(href, e);
		}
		catch (PartInitException e)
		{
			openWebBrowserError(href, e);
		}
	}


	/**
	 * This method encodes the url, removes the spaces from the url and replaces
	 * the same with <code>"%20"</code>. This method is required to fix Bug
	 * 77840.
	 * 
	 * @since 3.0.2
	 */
	private String urlEncodeForSpaces(char[] input)
	{
		StringBuffer retu = new StringBuffer(input.length);
		for (int i = 0; i < input.length; i++)
		{
			if (input[i] == ' ')
			{
				retu.append("%20"); //$NON-NLS-1$
			}
			else
			{
				retu.append(input[i]);
			}
		}
		return retu.toString();
	}


	/**
	 * Open a browser with the argument title on the argument url. If the url
	 * refers to a resource within a bundle, then a temp copy of the file will
	 * be extracted and opened.
	 * 
	 * @see <code>Platform.asLocalUrl</code>
	 * @param url
	 *            The target url to be displayed, null will be safely ignored
	 * @return true if the url was successfully displayed and false otherwise
	 */
	protected boolean openBrowser(URL url)
	{
		if (url != null)
		{
			try
			{
				url = FileLocator.toFileURL(url);
			}
			catch (IOException e)
			{
				return false;
			}
		}
		if (url == null)
		{
			return false;
		}
		openLink(url.toString());
		return true;
	}


	/**
	 * Sets the styled text's bold ranges
	 */
	protected void setBoldRanges(StyledText styledText, int[][] boldRanges)
	{
		for (int i = 0; i < boldRanges.length; i++)
		{
			StyleRange r = new StyleRange(boldRanges[i][0], boldRanges[i][1], null, null,
					SWT.BOLD);
			styledText.setStyleRange(r);
		}
	}


	/**
	 * Sets the styled text's link (blue) ranges
	 */
	protected void setLinkRanges(StyledText styledText, int[][] linkRanges)
	{
		Color fg = JFaceColors.getHyperlinkText(styledText.getShell().getDisplay());
		for (int i = 0; i < linkRanges.length; i++)
		{
			StyleRange r = new StyleRange(linkRanges[i][0], linkRanges[i][1], fg, null);
			styledText.setStyleRange(r);
		}
	}


	/**
	 * Scan the contents of the about text
	 */
	protected WEAboutItem scan(String s)
	{
		ArrayList linkRanges = new ArrayList();
		ArrayList links = new ArrayList();

		// slightly modified version of jface url detection
		// see org.eclipse.jface.text.hyperlink.URLHyperlinkDetector

		int urlSeparatorOffset = s.indexOf("://"); //$NON-NLS-1$
		while (urlSeparatorOffset >= 0)
		{

			boolean startDoubleQuote = false;

			// URL protocol (left to "://")
			int urlOffset = urlSeparatorOffset;
			char ch;
			do
			{
				urlOffset--;
				ch = ' ';
				if (urlOffset > -1)
					ch = s.charAt(urlOffset);
				startDoubleQuote = ch == '"';
			}
			while (Character.isUnicodeIdentifierStart(ch));
			urlOffset++;

			// Right to "://"
			StringTokenizer tokenizer = new StringTokenizer(s
					.substring(urlSeparatorOffset + 3), " \t\n\r\f<>", false); //$NON-NLS-1$
			if (!tokenizer.hasMoreTokens())
				return null;

			int urlLength = tokenizer.nextToken().length() + 3 + urlSeparatorOffset
					- urlOffset;

			if (startDoubleQuote)
			{
				int endOffset = -1;
				int nextDoubleQuote = s.indexOf('"', urlOffset);
				int nextWhitespace = s.indexOf(' ', urlOffset);
				if (nextDoubleQuote != -1 && nextWhitespace != -1)
					endOffset = Math.min(nextDoubleQuote, nextWhitespace);
				else if (nextDoubleQuote != -1)
					endOffset = nextDoubleQuote;
				else if (nextWhitespace != -1)
					endOffset = nextWhitespace;
				if (endOffset != -1)
					urlLength = endOffset - urlOffset;
			}

			linkRanges.add(new int[]
			{ urlOffset, urlLength });
			links.add(s.substring(urlOffset, urlOffset + urlLength));

			urlSeparatorOffset = s.indexOf("://", urlOffset + urlLength + 1); //$NON-NLS-1$
		}
		return new WEAboutItem(s, (int[][]) linkRanges
				.toArray(new int[linkRanges.size()][2]), (String[]) links
				.toArray(new String[links.size()]));
	}

}
