Bug 569634: Add support for multiple URLs in package description

Change-Id: Ie5316eedc652d26990102f0364b291ec0f3f111f
diff --git a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/SerUtil.java b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/SerUtil.java
index 7d56952..3dc0d2a 100644
--- a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/SerUtil.java
+++ b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/SerUtil.java
@@ -49,6 +49,7 @@
 @NonNullByDefault
 public class SerUtil {
 	
+	// TODO in next ser version: replace urlString by url list
 	
 	public static final int VERSION_12= 12;
 	public static final int VERSION_11= 11;
@@ -447,6 +448,7 @@
 		return pkg;
 	}
 	
+	
 	private void savePkgDescription(final RPkgDescription pkgDescription,
 			final DataStream out) throws IOException {
 		out.writeString(pkgDescription.getName());
@@ -455,7 +457,7 @@
 		out.writeString(pkgDescription.getDescription());
 		out.writeString(pkgDescription.getAuthor());
 		out.writeString(pkgDescription.getMaintainer());
-		out.writeString(pkgDescription.getUrl());
+		out.writeString(joinUrls(pkgDescription.getUrls()));
 		out.writeString(pkgDescription.getBuilt());
 		out.writeString(pkgDescription.getLibLocation().getDirectory());
 	}
@@ -468,17 +470,31 @@
 		final String description= in.readNonNullString();
 		final String author= in.readString();
 		final String maintainer= in.readString();
-		final String url= in.readString();
+		final ImList<String> urls= splitUrls(in.readString());
 		final String built= in.readNonNullString();
 		final RLibLocation libLocation= getLibLocationSafe(rEnvConfig, in.readNonNullString());
 		
 		return new BasicRPkgDescription(name, RNumVersion.create(version),
 				title, description,
 				author, maintainer,
-				url,
+				urls,
 				built, libLocation);
 	}
 	
+	private @Nullable String joinUrls(final List<String> urls) {
+		if (urls.isEmpty()) {
+			return null;
+		}
+		return String.join(",", urls); //$NON-NLS-1$
+	}
+	
+	private ImList<String> splitUrls(final @Nullable String urlString) {
+		if (urlString == null) {
+			return ImCollections.emptyList();
+		}
+		return ImCollections.newList(urlString.split(",")); //$NON-NLS-1$
+	}
+	
 	private RLibLocation getLibLocationSafe(final REnvHelpConfiguration rEnvConfig, final String directory) {
 		RLibLocation libLocation= rEnvConfig.getRLibLocationByDirectory(directory);
 		if (libLocation == null) {
@@ -487,6 +503,7 @@
 		return libLocation;
 	}
 	
+	
 	private void savePage(final RHelpPage page, final DataStream out)
 			throws IOException {
 		if (out.getVersion() >= VERSION_12) {
diff --git a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/index/REnvIndexWriter.java b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/index/REnvIndexWriter.java
index 26f6d55..b0523fb 100644
--- a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/index/REnvIndexWriter.java
+++ b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/internal/rhelp/core/index/REnvIndexWriter.java
@@ -202,18 +202,19 @@
 		private final NameField packageField= new NameField(PACKAGE_FIELD_NAME);
 		private final NameField pageField= new NameField(PAGE_FIELD_NAME);
 		private final TxtField titleTxtField= new TxtField(TITLE_TXT_FIELD_NAME);
-		private final MultiValueFieldList<NameField> aliasFields= MultiValueFieldList.forNameField(
-				ALIAS_FIELD_NAME );
-		private final MultiValueFieldList<TxtField> aliasTxtFields= MultiValueFieldList.forTxtField(
-				ALIAS_TXT_FIELD_NAME );
+		private final MultiValueFieldList<NameField> aliasFields=
+				MultiValueFieldList.forNameField(ALIAS_FIELD_NAME);
+		private final MultiValueFieldList<TxtField> aliasTxtFields=
+				MultiValueFieldList.forTxtField(ALIAS_TXT_FIELD_NAME);
 		private final TxtField descriptionTxtField= new TxtField(DESCRIPTION_TXT_FIELD_NAME);
 		private final TxtField authorsTxtField= new TxtField(AUTHORS_TXT_FIELD_NAME);
 		private final TxtField maintainerTxtField= new TxtField(MAINTAINER_TXT_FIELD_NAME);
-		private final TxtField urlTxtField= new TxtField(URL_TXT_FIELD_NAME);
-		private final MultiValueFieldList<KeywordField> keywordTxtFields= MultiValueFieldList.forKeywordField(
-				KEYWORD_FIELD_NAME );
-		private final MultiValueFieldList<TxtField> conteptTxtFields= MultiValueFieldList.forTxtField(
-				CONCEPT_TXT_FIELD_NAME );
+		private final MultiValueFieldList<TxtField> urlTxtFields=
+				MultiValueFieldList.forTxtField(URL_TXT_FIELD_NAME);
+		private final MultiValueFieldList<KeywordField> keywordTxtFields=
+				MultiValueFieldList.forKeywordField(KEYWORD_FIELD_NAME);
+		private final MultiValueFieldList<TxtField> conteptTxtFields=
+				MultiValueFieldList.forTxtField(CONCEPT_TXT_FIELD_NAME);
 		private final TxtField docTxtField= new TxtField(DOC_TXT_FIELD_NAME);
 		private final TxtField.OmitNorm docHtmlField= new TxtField.OmitNorm(DOC_HTML_FIELD_NAME);
 		private final TxtField examplesTxtField= new TxtField(EXAMPLES_TXT_FIELD_NAME);
@@ -242,9 +243,12 @@
 				this.maintainerTxtField.setStringValue(item.getMaintainer());
 				doc.add(this.maintainerTxtField);
 			}
-			if (item.getUrl() != null) {
-				this.urlTxtField.setStringValue(item.getUrl());
-				doc.add(this.urlTxtField);
+			{	final ImList<String> urls= item.getUrls();
+				for (int i= 0; i < urls.size(); i++) {
+					final TxtField txtField= this.urlTxtFields.get(i);
+					txtField.setStringValue(urls.get(i));
+					doc.add(txtField);
+				}
 			}
 			REnvIndexWriter.this.luceneWriter.addDocument(doc);
 		}
diff --git a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/http/RHelpHttpServlet.java b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/http/RHelpHttpServlet.java
index 10f852f..24fa4ba 100644
--- a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/http/RHelpHttpServlet.java
+++ b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/http/RHelpHttpServlet.java
@@ -31,6 +31,7 @@
 import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -247,8 +248,8 @@
 	@Override
 	protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
 			throws ServletException, IOException {
-		final String path= req.getPathInfo();
 		try {
+			final String path= req.getPathInfo();
 			if (path != null) {
 				if (path.startsWith('/' + IMAGES + '/')) {
 					processImage(path.substring(IMAGES.length() + 2), req, resp);
@@ -512,11 +513,11 @@
 		writer.println("div.toc a { text-decoration: none; color: black; }"); //$NON-NLS-1$
 		writer.println("div.toc a:visited { text-decoration: none; color: black; }"); //$NON-NLS-1$
 		
-		writer.println("a.action { text-decoration: none; }");
-		writer.println("a.action small { padding-left: 1px; padding-right: 1px; }");
-		writer.println("a.action:hover small { background-color: lightgrey; color: black; }");
+		writer.println("a.action { text-decoration: none; }"); //$NON-NLS-1$
+		writer.println("a.action small { padding-left: 1px; padding-right: 1px; }"); //$NON-NLS-1$
+		writer.println("a.action:hover small { background-color: lightgrey; color: black; }"); //$NON-NLS-1$
 		
-		writer.println("img.icon { vertical-align: text-top; padding-top: 1px; padding-right: 2px; margin-right: 2px; }");
+		writer.println("img.icon { vertical-align: text-top; padding-top: 1px; padding-right: 2px; margin-right: 2px; }"); //$NON-NLS-1$
 		
 		customizeCss(writer);
 	}
@@ -585,7 +586,7 @@
 		writer.write("</title>"); //$NON-NLS-1$
 		writer.write("<link rel=\"stylesheet\" type=\"text/css\" href=\""); //$NON-NLS-1$
 		writer.write(getServletPath(req)
-				.append("/R.css")
+				.append("/R.css") //$NON-NLS-1$
 				.toString() );
 		writer.println("\"/>"); //$NON-NLS-1$
 		return writer;
@@ -824,15 +825,28 @@
 				printSaveHtml(writer, pkgDescription.getMaintainer());
 				writer.write("</td>"); //$NON-NLS-1$
 			}
-			if (pkgDescription.getUrl() != null && pkgDescription.getUrl().length() > 0) {
+			final ImList<String> urls= pkgDescription.getUrls();
+			if (!urls.isEmpty()) {
 				writer.write("<tr><td>URL:</td>"); //$NON-NLS-1$
-				writer.write("<td><a href=\""); //$NON-NLS-1$
-				printSaveHtml(writer, pkgDescription.getUrl());
-				writer.write("\"><code>"); //$NON-NLS-1$
-				printSaveHtml(writer, pkgDescription.getUrl());
-				writer.write("</code></a></td>"); //$NON-NLS-1$
+				writer.write("<td>"); //$NON-NLS-1$
+				for (final Iterator<String> iter= urls.iterator();;) {
+					final String url= iter.next();
+					writer.write("<a href=\""); //$NON-NLS-1$
+					printSaveHtml(writer, url);
+					writer.write("\"><code>"); //$NON-NLS-1$
+					printSaveHtml(writer, url);
+					writer.write("</code></a>"); //$NON-NLS-1$
+					if (iter.hasNext()) {
+						writer.write(", "); //$NON-NLS-1$
+						continue;
+					}
+					else {
+						break;
+					}
+				}
+				writer.write("</td>"); //$NON-NLS-1$
 			}
-			writer.write("</table>");
+			writer.write("</table>"); //$NON-NLS-1$
 		}
 		writer.write("<p><a href=\"description\">DESCRIPTION file</a></p>"); //$NON-NLS-1$
 		
diff --git a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/update/REnvIndexUpdater.java b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/update/REnvIndexUpdater.java
index f5f7b99..6767ff0 100644
--- a/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/update/REnvIndexUpdater.java
+++ b/rhelp/org.eclipse.statet.rhelp.core/src/org/eclipse/statet/rhelp/core/update/REnvIndexUpdater.java
@@ -28,7 +28,10 @@
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
+import org.eclipse.statet.jcommons.collections.ImCollections;
+import org.eclipse.statet.jcommons.collections.ImList;
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 import org.eclipse.statet.jcommons.lang.Nullable;
 import org.eclipse.statet.jcommons.status.ErrorStatus;
@@ -95,8 +98,8 @@
 		
 		
 		final String name;
-		final RNumVersion version;
-		final String built;
+		final @Nullable RNumVersion version;
+		final @Nullable String built;
 		final RLibLocation libLocation;
 		final RLibLocationInfo libLocationInfo; // for error messages
 		
@@ -501,44 +504,58 @@
 	
 	protected abstract IndexJob scheduleIndexJob(final String name);
 	
+	
+	private static final Pattern URL_SPLIT_PATTERN= Pattern.compile("(?:,|\\s)+"); //$NON-NLS-1$
+	
 	private RPkgDescription createDescription(final PkgTask task) throws Exception {
 		final RCharacterStore data= RDataUtils.checkLengthEqual(task.rDescr.getData(), PKG_DESCR_LENGTH);
 		
 		final RNumVersion version;
-		final String built;
 		{	final String versionString= RDataUtils.checkValue(data, PKG_DESCR_IDX_VERSION);
-			if (task.version != null) {
-				if (!task.version.toString().equals(versionString)) {
+			final RNumVersion taskVersion= task.version;
+			if (taskVersion != null) {
+				if (!taskVersion.toString().equals(versionString)) {
 					throw new Exception(
 							String.format("Unexpected package version: expected=%1$s, found=%2$s",
-									task.version, versionString ));
+									taskVersion, versionString ));
 				}
-				version= task.version;
+				version= taskVersion;
 			}
 			else {
 				version= RNumVersion.create(versionString);
 			}
 		}
+		final String built;
 		{	final String builtString= RDataUtils.checkValue(data, PKG_DESCR_IDX_BUILT);
-			if (task.built != null) {
-				if (!task.built.equals(builtString)) {
+			final String taskBuilt= task.built;
+			if (taskBuilt != null) {
+				if (!taskBuilt.equals(builtString)) {
 					throw new Exception(
 							String.format("Unexpected package built: expected=%1$s, found=%2$s",
-									task.built, builtString ));
+									taskBuilt, builtString ));
 				}
-				built= task.built;
+				built= taskBuilt;
 			}
 			else {
 				built= builtString;
 			}
 		}
+		final ImList<String> urls;
+		{	String urlString= data.get(PKG_DESCR_IDX_URL);
+			if (urlString != null && !(urlString= urlString.strip()).isEmpty()) {
+				urls= ImCollections.newList(URL_SPLIT_PATTERN.split(urlString));
+			}
+			else {
+				urls= ImCollections.emptyList();
+			}
+		}
 		return new BasicRPkgDescription(task.name,
 				version,
 				RDataUtils.getValue(data, PKG_DESCR_IDX_TITLE, ""), //$NON-NLS-1$
 				RDataUtils.getValue(data, PKG_DESCR_IDX_DESCRIPTION, ""), //$NON-NLS-1$
 				data.get(PKG_DESCR_IDX_AUTHOR),
 				data.get(PKG_DESCR_IDX_MAINTAINER),
-				data.get(PKG_DESCR_IDX_URL),
+				urls,
 				built, task.libLocation );
 	}