Allow ArchiveCommand.registerFormat to be called twice

This should make it possible for the gitiles plugin to register its
archive formats after gerrit has already registered them.

Signed-off-by: Jonathan Nieder <jrn@google.com>
Change-Id: Icb80a446e583961a7278b707d572d6fe456c372c
diff --git a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ArchiveFormats.java b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ArchiveFormats.java
index 65466a9..1be126a 100644
--- a/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ArchiveFormats.java
+++ b/org.eclipse.jgit.archive/src/org/eclipse/jgit/archive/ArchiveFormats.java
@@ -66,7 +66,6 @@ private static final void register(String name, ArchiveCommand.Format<?> fmt) {
 	 * Register all included archive formats so they can be used
 	 * as arguments to the ArchiveCommand.setFormat() method.
 	 *
-	 * Should not be called twice without a call to stop() in between.
 	 * Not thread-safe.
 	 */
 	public static void registerAll() {
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 3bc682b..bb95fa8 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -12,7 +12,7 @@
 anSSHSessionHasBeenAlreadyCreated=An SSH session has been already created
 applyingCommit=Applying {0}
 archiveFormatAlreadyAbsent=Archive format already absent: {0}
-archiveFormatAlreadyRegistered=Archive format already registered: {0}
+archiveFormatAlreadyRegistered=Archive format already registered with different implementation: {0}
 argumentIsNotAValidCommentString=Invalid comment: {0}
 atLeastOnePathIsRequired=At least one path is required.
 atLeastOnePatternIsRequired=At least one pattern is required.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
index 1bafb5e..70ab730 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
@@ -189,65 +189,135 @@ public String getFormat() {
 		}
 	}
 
+	private static class FormatEntry {
+		final Format<?> format;
+		/** Number of times this format has been registered. */
+		final int refcnt;
+
+		public FormatEntry(Format<?> format, int refcnt) {
+			if (format == null)
+				throw new NullPointerException();
+			this.format = format;
+			this.refcnt = refcnt;
+		}
+	};
+
 	/**
 	 * Available archival formats (corresponding to values for
 	 * the --format= option)
 	 */
-	private static final ConcurrentMap<String, Format<?>> formats =
-			new ConcurrentHashMap<String, Format<?>>();
+	private static final ConcurrentMap<String, FormatEntry> formats =
+			new ConcurrentHashMap<String, FormatEntry>();
+
+	/**
+	 * Replaces the entry for a key only if currently mapped to a given
+	 * value.
+	 *
+	 * @param map a map
+	 * @param key key with which the specified value is associated
+	 * @param oldValue expected value for the key (null if should be absent).
+	 * @param newValue value to be associated with the key (null to remove).
+	 * @return true if the value was replaced
+	 */
+	private static <K, V> boolean replace(ConcurrentMap<K, V> map,
+			K key, V oldValue, V newValue) {
+		if (oldValue == null && newValue == null) // Nothing to do.
+			return true;
+
+		if (oldValue == null)
+			return map.putIfAbsent(key, newValue) == null;
+		else if (newValue == null)
+			return map.remove(key, oldValue);
+		else
+			return map.replace(key, oldValue, newValue);
+	}
 
 	/**
 	 * Adds support for an additional archival format.  To avoid
 	 * unnecessary dependencies, ArchiveCommand does not have support
 	 * for any formats built in; use this function to add them.
-	 *
+	 * <p>
 	 * OSGi plugins providing formats should call this function at
 	 * bundle activation time.
+	 * <p>
+	 * It is okay to register the same archive format with the same
+	 * name multiple times, but don't forget to unregister it that
+	 * same number of times, too.
+	 * <p>
+	 * Registering multiple formats with different names and the
+	 * same or overlapping suffixes results in undefined behavior.
+	 * TODO: check that suffixes don't overlap.
 	 *
 	 * @param name name of a format (e.g., "tar" or "zip").
 	 * @param fmt archiver for that format
 	 * @throws JGitInternalException
-	 *              An archival format with that name was already registered.
+	 *              A different archival format with that name was
+	 *              already registered.
 	 */
 	public static void registerFormat(String name, Format<?> fmt) {
-		// TODO(jrn): Check that suffixes don't overlap.
+		if (fmt == null)
+			throw new NullPointerException();
 
-		if (formats.putIfAbsent(name, fmt) != null)
-			throw new JGitInternalException(MessageFormat.format(
-					JGitText.get().archiveFormatAlreadyRegistered,
-					name));
+		FormatEntry old, entry;
+		do {
+			old = formats.get(name);
+			if (old == null) {
+				entry = new FormatEntry(fmt, 1);
+				continue;
+			}
+			if (!old.format.equals(fmt))
+				throw new JGitInternalException(MessageFormat.format(
+						JGitText.get().archiveFormatAlreadyRegistered,
+						name));
+			entry = new FormatEntry(old.format, old.refcnt + 1);
+		} while (!replace(formats, name, old, entry));
 	}
 
 	/**
-	 * Removes support for an archival format so its Format can be
-	 * garbage collected.
+	 * Marks support for an archival format as no longer needed so its
+	 * Format can be garbage collected if no one else is using it either.
+	 * <p>
+	 * In other words, this decrements the reference count for an
+	 * archival format.  If the reference count becomes zero, removes
+	 * support for that format.
 	 *
 	 * @param name name of format (e.g., "tar" or "zip").
 	 * @throws JGitInternalException
 	 *              No such archival format was registered.
 	 */
 	public static void unregisterFormat(String name) {
-		if (formats.remove(name) == null)
-			throw new JGitInternalException(MessageFormat.format(
-					JGitText.get().archiveFormatAlreadyAbsent,
-					name));
+		FormatEntry old, entry;
+		do {
+			old = formats.get(name);
+			if (old == null)
+				throw new JGitInternalException(MessageFormat.format(
+						JGitText.get().archiveFormatAlreadyAbsent,
+						name));
+			if (old.refcnt == 1) {
+				entry = null;
+				continue;
+			}
+			entry = new FormatEntry(old.format, old.refcnt - 1);
+		} while (!replace(formats, name, old, entry));
 	}
 
 	private static Format<?> formatBySuffix(String filenameSuffix)
 			throws UnsupportedFormatException {
 		if (filenameSuffix != null)
-			for (Format<?> fmt : formats.values())
+			for (FormatEntry entry : formats.values()) {
+				Format<?> fmt = entry.format;
 				for (String sfx : fmt.suffixes())
 					if (filenameSuffix.endsWith(sfx))
 						return fmt;
+			}
 		return lookupFormat("tar"); //$NON-NLS-1$
 	}
 
 	private static Format<?> lookupFormat(String formatName) throws UnsupportedFormatException {
-		Format<?> fmt = formats.get(formatName);
-		if (fmt == null)
+		FormatEntry entry = formats.get(formatName);
+		if (entry == null)
 			throw new UnsupportedFormatException(formatName);
-		return fmt;
+		return entry.format;
 	}
 
 	private OutputStream out;