Merge pull request #20 from oliverlietz/SM-4383

SM-4383 activation-api spec modification to CommandMap leads to inval…
diff --git a/activation-api-2.0.1/pom.xml b/activation-api-2.0.1/pom.xml
index 0094028..5deddf0 100644
--- a/activation-api-2.0.1/pom.xml
+++ b/activation-api-2.0.1/pom.xml
@@ -57,6 +57,18 @@
             <version>1.4.0</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.13.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.9.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
@@ -65,9 +77,6 @@
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <inherited>true</inherited>
-              <configuration>
-                <compilerArgument>-Xbootclasspath/p:${settings.localRepository}/org/apache/geronimo/specs/geronimo-activation_1.1_spec/1.1/geronimo-activation_1.1_spec-1.1.jar</compilerArgument>
-              </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.felix</groupId>
@@ -77,7 +86,6 @@
                         <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
                         <Bundle-Description>${project.description}</Bundle-Description>
                         <Export-Package>
-                          javax.activation*;version=2.0.1;-split-package:=merge-first;-noimport:=true,
                           jakarta.activation*;version=2.0.1;-split-package:=merge-first;-noimport:=true
                         </Export-Package>
                         <Import-Package>
@@ -116,7 +124,7 @@
                                 <filter>
                                     <artifact>jakarta.activation:jakarta.activation-api</artifact>
                                     <excludes>
-                                        <exclude>javax/**</exclude>
+                                        <exclude>jakarta/**</exclude>
                                     </excludes>
                                 </filter>
                                 <filter>
@@ -133,6 +141,22 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>3.0.0-M5</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                        <configuration>
+                            <argLine>-Djava.endorsed.dirs=${project.build.directory},${maven.dependency.org.osgi.core.jar.path}</argLine>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/activation-api-2.0.1/src/main/java/javax/activation/MailcapCommandMap.java b/activation-api-2.0.1/src/main/java/jakarta/activation/MailcapCommandMap.java
similarity index 97%
rename from activation-api-2.0.1/src/main/java/javax/activation/MailcapCommandMap.java
rename to activation-api-2.0.1/src/main/java/jakarta/activation/MailcapCommandMap.java
index 536083f..1c2dea6 100644
--- a/activation-api-2.0.1/src/main/java/javax/activation/MailcapCommandMap.java
+++ b/activation-api-2.0.1/src/main/java/jakarta/activation/MailcapCommandMap.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package javax.activation;
+package jakarta.activation;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -28,7 +28,6 @@
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.URL;
-import java.security.Security;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
@@ -281,11 +280,21 @@
                     cmdList = new ArrayList();
                     allCommands.put(mimeType, cmdList);
                 }
-                cmdList.add(info);
+                addUnique(cmdList, info);
             }
         }
     }
 
+    private void addUnique(List commands, CommandInfo newCommand) {
+        for (Iterator i = commands.iterator(); i.hasNext();) {
+            CommandInfo info = (CommandInfo)i.next();
+            if (info.getCommandName().equals(newCommand.getCommandName())
+                && info.getCommandClass().equals(newCommand.getCommandClass())) {
+                return;
+            }
+        }
+        commands.add(newCommand);
+    }
 
     /**
      * Add a command to a target command list (preferred or fallback).
diff --git a/activation-api-2.0.1/src/main/java/javax/activation/MimetypesFileTypeMap.java b/activation-api-2.0.1/src/main/java/jakarta/activation/MimetypesFileTypeMap.java
similarity index 99%
rename from activation-api-2.0.1/src/main/java/javax/activation/MimetypesFileTypeMap.java
rename to activation-api-2.0.1/src/main/java/jakarta/activation/MimetypesFileTypeMap.java
index bf8836a..a3e5459 100644
--- a/activation-api-2.0.1/src/main/java/javax/activation/MimetypesFileTypeMap.java
+++ b/activation-api-2.0.1/src/main/java/jakarta/activation/MimetypesFileTypeMap.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package javax.activation;
+package jakarta.activation;
 
 import java.io.File;
 import java.io.IOException;
diff --git a/activation-api-2.0.1/src/main/java/javax/activation/DataHandler.java b/activation-api-2.0.1/src/main/java/javax/activation/DataHandler.java
deleted file mode 100644
index b4b4bcf..0000000
--- a/activation-api-2.0.1/src/main/java/javax/activation/DataHandler.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *  http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package javax.activation;
-
-import java.awt.datatransfer.DataFlavor;
-import java.awt.datatransfer.Transferable;
-import java.awt.datatransfer.UnsupportedFlavorException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-import java.net.URL;
-
-public class DataHandler implements Transferable {
-    private final DataSource ds;
-    private final DataFlavor flavor;
-
-    private CommandMap commandMap;
-    private DataContentHandler dch;
-
-    public DataHandler(DataSource ds) {
-        this.ds = ds;
-        this.flavor = new ActivationDataFlavor(ds.getContentType(), null);
-    }
-
-    public DataHandler(Object data, String type) {
-        this.ds = new ObjectDataSource(data, type);
-        this.flavor = new ActivationDataFlavor(data.getClass(), null);
-    }
-
-    public DataHandler(URL url) {
-        this.ds = new URLDataSource(url);
-        this.flavor = new ActivationDataFlavor(ds.getContentType(), null);
-    }
-
-    public DataSource getDataSource() {
-        return ds;
-    }
-
-    public String getName() {
-        return ds.getName();
-    }
-
-    public String getContentType() {
-        return ds.getContentType();
-    }
-
-    public InputStream getInputStream() throws IOException {
-        return ds.getInputStream();
-    }
-
-    public void writeTo(OutputStream os) throws IOException {
-        if (ds instanceof ObjectDataSource) {
-            ObjectDataSource ods = (ObjectDataSource) ds;
-            DataContentHandler dch = getDataContentHandler();
-            if (dch == null) {
-                throw new UnsupportedDataTypeException(ods.mimeType);
-            }
-            dch.writeTo(ods.data, ods.mimeType, os);
-        } else {
-            byte[] buffer = new byte[1024];
-            InputStream is = getInputStream();
-            try {
-                int count;
-                while ((count = is.read(buffer)) != -1) {
-                    os.write(buffer, 0, count);
-                }
-            } finally {
-                is.close();
-            }
-        }
-    }
-
-    public OutputStream getOutputStream() throws IOException {
-        return ds.getOutputStream();
-    }
-
-    public synchronized DataFlavor[] getTransferDataFlavors() {
-        return getDataContentHandler().getTransferDataFlavors();
-    }
-
-    public boolean isDataFlavorSupported(DataFlavor flavor) {
-        DataFlavor[] flavors = getTransferDataFlavors();
-        for (int i = 0; i < flavors.length; i++) {
-            DataFlavor dataFlavor = flavors[i];
-            if (dataFlavor.equals(flavor)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
-        DataContentHandler dch = getDataContentHandler();
-        if (dch != null) {
-            return dch.getTransferData(flavor, ds);
-        } else if (this.flavor.match(flavor)) {
-            if (ds instanceof ObjectDataSource) {
-                return ((ObjectDataSource) ds).data;
-            } else {
-                return ds.getInputStream();
-            }
-        } else {
-            throw new UnsupportedFlavorException(flavor);
-        }
-    }
-
-    public CommandInfo[] getPreferredCommands() {
-        return getCommandMap().getPreferredCommands(ds.getContentType());
-    }
-
-    public CommandInfo[] getAllCommands() {
-        return getCommandMap().getAllCommands(ds.getContentType());
-    }
-
-    public CommandInfo getCommand(String cmdName) {
-        return getCommandMap().getCommand(ds.getContentType(), cmdName);
-    }
-
-    public Object getContent() throws IOException {
-        if (ds instanceof ObjectDataSource) {
-            return ((ObjectDataSource) ds).data;
-        } else {
-            DataContentHandler dch = getDataContentHandler();
-            if (dch != null) {
-                return dch.getContent(ds);
-            } else {
-                return ds.getInputStream();
-            }
-        }
-    }
-
-    public Object getBean(CommandInfo cmdinfo) {
-        try {
-            return cmdinfo.getCommandObject(this, this.getClass().getClassLoader());
-        } catch (IOException e) {
-            return null;
-        } catch (ClassNotFoundException e) {
-            return null;
-        }
-    }
-
-    /**
-     * A local implementation of DataSouce used to wrap an Object and mime-type.
-     */
-    private class ObjectDataSource implements DataSource {
-        private final Object data;
-        private final String mimeType;
-
-        public ObjectDataSource(Object data, String mimeType) {
-            this.data = data;
-            this.mimeType = mimeType;
-        }
-
-        public String getName() {
-            return null;
-        }
-
-        public String getContentType() {
-            return mimeType;
-        }
-
-        public InputStream getInputStream() throws IOException {
-            final DataContentHandler dch = getDataContentHandler();
-            if (dch == null) {
-                throw new UnsupportedDataTypeException(mimeType);
-            }
-            final PipedInputStream is = new PipedInputStream();
-            final PipedOutputStream os = new PipedOutputStream(is);
-            Thread thread = new Thread("DataHandler Pipe Pump") {
-                public void run() {
-                    try {
-                        try {
-                            dch.writeTo(data, mimeType, os);
-                        } finally {
-                            os.close();
-                        }
-                    } catch (IOException e) {
-                        // ignore, per spec - doh!
-                    }
-                }
-            };
-            thread.start();
-            return is;
-        }
-
-        public OutputStream getOutputStream() throws IOException {
-            return null;
-        }
-    }
-
-    public synchronized void setCommandMap(CommandMap commandMap) {
-        this.commandMap = commandMap;
-        this.dch = null;
-    }
-
-    private synchronized CommandMap getCommandMap() {
-        return commandMap != null ? commandMap : CommandMap.getDefaultCommandMap();
-    }
-
-    /**
-     * Search for a DataContentHandler for our mime type.
-     * The search is performed by first checking if a global factory has been set using
-     * {@link #setDataContentHandlerFactory(DataContentHandlerFactory)};
-     * if found then it is called to attempt to create a handler.
-     * If this attempt fails, we then call the command map set using {@link #setCommandMap(CommandMap)}
-     * (or if that has not been set, the default map returned by {@link CommandMap#getDefaultCommandMap()})
-     * to create the handler.
-     *
-     * The resulting handler is cached until the global factory is changed.
-     *
-     * @return
-     */
-    private synchronized DataContentHandler getDataContentHandler() {
-        DataContentHandlerFactory localFactory;
-        synchronized (DataHandler.class) {
-            if (factory != originalFactory) {
-                // setDCHF was called - clear our cached copy of the DCH and DCHF
-                dch = null;
-                originalFactory = factory;
-            }
-            localFactory = originalFactory;
-        }
-        if (dch == null) {
-            // get the main mime-type portion of the content.
-            String mimeType = getMimeType(ds.getContentType());
-            if (localFactory != null) {
-                dch = localFactory.createDataContentHandler(mimeType);
-            }
-            if (dch == null) {
-                dch = getCommandMap().createDataContentHandler(mimeType);
-            }
-        }
-        return dch;
-    }
-
-    /**
-     * Retrieve the base MIME type from a content type.  This parses
-     * the type into its base components, stripping off any parameter
-     * information.
-     *
-     * @param contentType
-     *               The content type string.
-     *
-     * @return The MIME type identifier portion of the content type.
-     */
-    private String getMimeType(String contentType) {
-        try {
-            MimeType mimeType = new MimeType(contentType);
-            return mimeType.getBaseType();
-        } catch (MimeTypeParseException e) {
-        }
-        return contentType;
-    }
-
-    /**
-     * This is used to check if the DataContentHandlerFactory has been changed.
-     * This is not specified behaviour but this check is required to make this work like the RI.
-     */
-    private DataContentHandlerFactory originalFactory;
-
-    {
-        synchronized (DataHandler.class) {
-            originalFactory = factory;
-        }
-    }
-
-    private static DataContentHandlerFactory factory;
-
-    /**
-     * Set the DataContentHandlerFactory to use.
-     * If this method has already been called then an Error is raised.
-     *
-     * @param newFactory the new factory
-     * @throws SecurityException if the caller does not have "SetFactory" RuntimePermission
-     */
-    public static synchronized void setDataContentHandlerFactory(DataContentHandlerFactory newFactory) {
-        if (factory != null) {
-            throw new Error("javax.activation.DataHandler.setDataContentHandlerFactory has already been defined");
-        }
-        SecurityManager sm = System.getSecurityManager();
-        if (sm != null) {
-            sm.checkSetFactory();
-        }
-        factory = newFactory;
-    }
-}
diff --git a/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/Activator.java b/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/Activator.java
index fc2643f..4d306ed 100644
--- a/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/Activator.java
+++ b/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/Activator.java
@@ -28,8 +28,8 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
-import javax.activation.CommandMap;
-import javax.activation.DataContentHandler;
+import jakarta.activation.CommandMap;
+import jakarta.activation.DataContentHandler;
 
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -61,7 +61,7 @@
 
             try {
                 final Class<?> clazz = bundle
-                        .loadClass("javax.activation.DataContentHandler");
+                        .loadClass("jakarta.activation.DataContentHandler");
                 if (!clazz.isAssignableFrom(DataContentHandler.class)) {
                     debugPrintln("incompatible DataContentHandler class in bundle "
                             + bundle.getBundleId());
@@ -99,6 +99,7 @@
                     commandMap.addMailcap(line, mailcap.bundle);
                 }
             }
+            commandMap.addMailcap("", null);
             CommandMap.setDefaultCommandMap(commandMap);
         } finally {
             Thread.currentThread().setContextClassLoader(tccl);
diff --git a/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/OsgiMailcapCommandMap.java b/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/OsgiMailcapCommandMap.java
index a914247..df79b68 100644
--- a/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/OsgiMailcapCommandMap.java
+++ b/activation-api-2.0.1/src/main/java/org/apache/servicemix/specs/activation/OsgiMailcapCommandMap.java
@@ -21,9 +21,9 @@
 import java.util.Map;
 import java.util.HashMap;
 
-import javax.activation.MailcapCommandMap;
-import javax.activation.CommandInfo;
-import javax.activation.DataContentHandler;
+import jakarta.activation.MailcapCommandMap;
+import jakarta.activation.CommandInfo;
+import jakarta.activation.DataContentHandler;
 
 import org.osgi.framework.Bundle;
 
diff --git a/activation-api-2.0.1/src/test/java/jakarta/activation/ITBugDuplicatedMaincapRegistrations.java b/activation-api-2.0.1/src/test/java/jakarta/activation/ITBugDuplicatedMaincapRegistrations.java
new file mode 100644
index 0000000..58f1b13
--- /dev/null
+++ b/activation-api-2.0.1/src/test/java/jakarta/activation/ITBugDuplicatedMaincapRegistrations.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package jakarta.activation;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+
+/**
+ * SM-4383. Filter out duplicate registrations of a command.
+ */
+public class ITBugDuplicatedMaincapRegistrations {
+
+    private static final String MIMETYPE = "example/sm-4383";
+
+    private static final String MAILCAP = MIMETYPE+";; x-java-content-handler=some.value";
+
+    @Test
+    public void testFilterDuplicates() {
+        MailcapCommandMap mc = new MailcapCommandMap();
+
+        assertEquals(0, mc.getAllCommands(MIMETYPE).length);
+
+        mc.addMailcap(MAILCAP);
+        mc.addMailcap(MAILCAP);
+        assertEquals(1, mc.getAllCommands(MIMETYPE).length);
+
+        mc.addMailcap(MAILCAP+".alt");
+        assertEquals(2, mc.getAllCommands(MIMETYPE).length);
+	}
+
+}
diff --git a/activation-api-2.0.1/src/test/java/org/apache/servicemix/specs/activation/ITClearCurrentBundle.java b/activation-api-2.0.1/src/test/java/org/apache/servicemix/specs/activation/ITClearCurrentBundle.java
new file mode 100644
index 0000000..ab5774d
--- /dev/null
+++ b/activation-api-2.0.1/src/test/java/org/apache/servicemix/specs/activation/ITClearCurrentBundle.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.servicemix.specs.activation;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Field;
+import java.net.URL;
+
+import jakarta.activation.CommandMap;
+import jakarta.activation.DataContentHandler;
+
+import org.junit.After;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+
+/**
+ * After Activator.rebuildCommandMap the currentBundle of OsgiMailcapCommandMap should be null, so that calls
+ * to addMailcap do not register with the last started bundle.
+ */
+public class ITClearCurrentBundle {
+
+	@After
+	public void tearDown() {
+		CommandMap.setDefaultCommandMap(null);
+	}
+
+	@Test
+	public void testClearCurrentBundle() throws Exception {
+		Activator activator = new Activator();
+		Bundle bundle = createBundle(ITClearCurrentBundle.class.getResource("/mailcap.example"));
+		
+		activator.register(bundle);
+		
+		assertTrue(CommandMap.getDefaultCommandMap() instanceof OsgiMailcapCommandMap);
+		assertNullCurrentBundle((OsgiMailcapCommandMap) CommandMap.getDefaultCommandMap());
+	}
+
+	private void assertNullCurrentBundle(OsgiMailcapCommandMap commandMap) throws Exception {
+		Field field = OsgiMailcapCommandMap.class.getDeclaredField("currentBundle");
+		boolean oldAccessible = field.isAccessible();
+		try {
+			field.setAccessible(true);
+			assertNull(field.get(commandMap));
+		}
+		finally {
+			field.setAccessible(oldAccessible);
+		}
+	}
+
+	private Bundle createBundle(URL mailcapResource) throws Exception {
+		Bundle result = Mockito.mock(Bundle.class);
+		Mockito.when(result.loadClass("jakarta.activation.DataContentHandler")).thenReturn(DataContentHandler.class);
+		Mockito.when(result.getResource("/META-INF/mailcap")).thenReturn(mailcapResource);
+		return result;
+	}
+}
diff --git a/activation-api-2.0.1/src/test/resources/mailcap.example b/activation-api-2.0.1/src/test/resources/mailcap.example
new file mode 100644
index 0000000..3ec1b0d
--- /dev/null
+++ b/activation-api-2.0.1/src/test/resources/mailcap.example
@@ -0,0 +1,2 @@
+# Example mailcap
+example/sm-4383;; x-java-content-handler=some.value