Move karaf 4 maven-proxy servlet to cave, use the HttpService instead of the 
war deployer for the server/http module

Project: http://git-wip-us.apache.org/repos/asf/karaf-cave/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf-cave/commit/e63f19b1
Tree: http://git-wip-us.apache.org/repos/asf/karaf-cave/tree/e63f19b1
Diff: http://git-wip-us.apache.org/repos/asf/karaf-cave/diff/e63f19b1

Branch: refs/heads/master
Commit: e63f19b10a130e297489636b1678d2fdca0c4eca
Parents: d36371b
Author: Guillaume Nodet <[email protected]>
Authored: Tue May 5 14:14:29 2015 +0200
Committer: Guillaume Nodet <[email protected]>
Committed: Tue May 5 14:14:29 2015 +0200

----------------------------------------------------------------------
 assembly/src/main/resources/features.xml        |   9 +-
 pom.xml                                         |  90 ++-
 server/command/pom.xml                          |   1 -
 server/http/pom.xml                             |  28 +-
 .../karaf/cave/server/http/Activator.java       |  70 ++
 .../karaf/cave/server/http/WrapperServlet.java  |   6 +-
 server/http/src/main/resources/WEB-INF/web.xml  |  19 -
 server/management/pom.xml                       |   2 -
 server/maven/pom.xml                            | 110 +++
 .../karaf/cave/server/maven/Activator.java      |  87 +++
 .../karaf/cave/server/maven/DefaultFuture.java  | 358 +++++++++
 .../karaf/cave/server/maven/FutureListener.java |  35 +
 .../maven/InvalidMavenArtifactRequest.java      |  35 +
 .../cave/server/maven/MavenProxyServlet.java    | 675 +++++++++++++++++
 .../karaf/cave/server/maven/ThreadFactory.java  |  43 ++
 .../server/maven/MavenProxyServletTest.java     | 743 +++++++++++++++++++
 server/pom.xml                                  |   1 +
 server/rest/pom.xml                             |   1 -
 server/storage/pom.xml                          |   3 -
 19 files changed, 2263 insertions(+), 53 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/assembly/src/main/resources/features.xml
----------------------------------------------------------------------
diff --git a/assembly/src/main/resources/features.xml 
b/assembly/src/main/resources/features.xml
index abce0f1..fa53995 100644
--- a/assembly/src/main/resources/features.xml
+++ b/assembly/src/main/resources/features.xml
@@ -46,8 +46,8 @@
     </feature>
 
     <feature name="cave-http" version="${project.version}">
-        <feature>war</feature>
-        <feature>cave-server</feature>
+        <feature>http</feature>
+        <feature>cave-storage</feature>
         
<bundle>mvn:org.apache.karaf.cave.server/org.apache.karaf.cave.server.http/${project.version}</bundle>
     </feature>
 
@@ -61,4 +61,9 @@
         
<bundle>mvn:org.apache.karaf.cave.server/org.apache.karaf.cave.server.rest/${project.version}</bundle>
     </feature>
 
+    <feature name="cave-maven" version="${project.version}">
+        <feature>http</feature>
+        
<bundle>mvn:org.apache.karaf.cave.server/org.apache.karaf.cave.server.maven/${project.version}</bundle>
+    </feature>
+
 </features>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 0b5f608..854c952 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,12 @@
         <karaf.version>4.0.0-SNAPSHOT</karaf.version>
         <osgi.version>5.0.0</osgi.version>
         <wagon.version>1.0</wagon.version>
+
+        <servlet.spec.groupId>javax.servlet</servlet.spec.groupId>
+        <servlet.spec.artifactId>javax.servlet-api</servlet.spec.artifactId>
+        <servlet.spec.version>3.1.0</servlet.spec.version>
+
+        
<bnd.version.policy>[$(version;==;$(@)),$(version;+;$(@)))</bnd.version.policy>
     </properties>
 
     <modules>
@@ -59,9 +65,39 @@
     <dependencyManagement>
         <dependencies>
             <dependency>
-                <groupId>commons-io</groupId>
-                <artifactId>commons-io</artifactId>
-                <version>${commons-io.version}</version>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                <artifactId>org.apache.karaf.cave.server.api</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                <artifactId>org.apache.karaf.cave.server.storage</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                <artifactId>org.apache.karaf.cave.server.command</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                
<artifactId>org.apache.karaf.cave.server.management</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                <artifactId>org.apache.karaf.cave.server.maven</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                <artifactId>org.apache.karaf.cave.server.rest</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.karaf.cave.server</groupId>
+                <artifactId>org.apache.karaf.cave.server.http</artifactId>
+                <version>${project.version}</version>
             </dependency>
             <dependency>
                 <groupId>org.apache.cxf</groupId>
@@ -69,11 +105,6 @@
                 <version>${cxf.version}</version>
             </dependency>
             <dependency>
-                <groupId>org.apache.httpcomponents</groupId>
-                <artifactId>httpclient-osgi</artifactId>
-                <version>${httpclient.version}</version>
-            </dependency>
-            <dependency>
                 <groupId>org.apache.karaf.shell</groupId>
                 <artifactId>org.apache.karaf.shell.core</artifactId>
                 <version>${karaf.version}</version>
@@ -89,6 +120,11 @@
                 <version>${karaf.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.apache.karaf</groupId>
+                <artifactId>org.apache.karaf.util</artifactId>
+                <version>${karaf.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.jsoup</groupId>
                 <artifactId>jsoup</artifactId>
                 <version>${jsoup.version}</version>
@@ -99,10 +135,40 @@
                 <version>${osgi.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.osgi</groupId>
+                <artifactId>org.osgi.compendium</artifactId>
+                <version>${osgi.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.slf4j</groupId>
                 <artifactId>slf4j-api</artifactId>
                 <version>1.7.5</version>
             </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-log4j12</artifactId>
+                <version>1.7.5</version>
+            </dependency>
+            <dependency>
+                <groupId>${servlet.spec.groupId}</groupId>
+                <artifactId>${servlet.spec.artifactId}</artifactId>
+                <version>${servlet.spec.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.ops4j.pax.url</groupId>
+                <artifactId>pax-url-aether</artifactId>
+                <version>2.4.1</version>
+            </dependency>
+            <dependency>
+                <groupId>org.ops4j.pax.web</groupId>
+                <artifactId>pax-web-jetty-bundle</artifactId>
+                <version>4.1.2</version>
+            </dependency>
+            <dependency>
+                <groupId>org.easymock</groupId>
+                <artifactId>easymock</artifactId>
+                <version>3.2</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
     <dependencies>
@@ -152,8 +218,14 @@
                 <plugin>
                     <groupId>org.apache.felix</groupId>
                     <artifactId>maven-bundle-plugin</artifactId>
-                    <version>2.5.3</version>
+                    <version>2.5.4</version>
                     <extensions>true</extensions>
+                    <configuration>
+                        <instructions>
+                            
<_removeheaders>Private-Package,Include-Resource,Embed-Dependency,Created-By,Bnd-LastModified,Built-By,Tool</_removeheaders>
+                            
<_versionpolicy>${bnd.version.policy}</_versionpolicy>
+                        </instructions>
+                    </configuration>
                 </plugin>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/command/pom.xml
----------------------------------------------------------------------
diff --git a/server/command/pom.xml b/server/command/pom.xml
index c185ea5..b34b229 100644
--- a/server/command/pom.xml
+++ b/server/command/pom.xml
@@ -45,7 +45,6 @@
         <dependency>
             <groupId>org.apache.karaf.cave.server</groupId>
             <artifactId>org.apache.karaf.cave.server.api</artifactId>
-            <version>${project.version}</version>
         </dependency>
     </dependencies>
 

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/http/pom.xml
----------------------------------------------------------------------
diff --git a/server/http/pom.xml b/server/http/pom.xml
index cf15f87..112ecda 100644
--- a/server/http/pom.xml
+++ b/server/http/pom.xml
@@ -35,48 +35,50 @@
 
     <dependencies>
         <dependency>
-            <groupId>javax.servlet</groupId>
-            <artifactId>servlet-api</artifactId>
-            <version>2.4</version>
+            <groupId>${servlet.spec.groupId}</groupId>
+            <artifactId>${servlet.spec.artifactId}</artifactId>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.karaf.cave.server</groupId>
             <artifactId>org.apache.karaf.cave.server.api</artifactId>
-            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.core</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.apache.karaf</groupId>
             <artifactId>org.apache.karaf.util</artifactId>
-            <version>${karaf.version}</version>
         </dependency>
     </dependencies>
 
     <build>
         <plugins>
             <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-services-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
                 <configuration>
                     <instructions>
-                        
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
-                        <Export-Package>
-                            
org.apache.karaf.cave.server.http*;version=${project.version}
-                        </Export-Package>
                         <Import-Package>
-                            
org.apache.karaf.cave.server.api*;version=${project.version},
-                            org.osgi.framework;version="[1,4)",
+                            javax.servlet.*;version="[3.0,4)",
                             *
                         </Import-Package>
+                        <Export-Package>
+                            !*
+                        </Export-Package>
                         <Private-Package>
+                            org.apache.karaf.cave.server.http,
                             org.apache.karaf.util
                         </Private-Package>
-                        <Webapp-Context>/cave</Webapp-Context>
-                        <Web-ContextPath>/cave</Web-ContextPath>
                     </instructions>
                 </configuration>
             </plugin>

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/http/src/main/java/org/apache/karaf/cave/server/http/Activator.java
----------------------------------------------------------------------
diff --git 
a/server/http/src/main/java/org/apache/karaf/cave/server/http/Activator.java 
b/server/http/src/main/java/org/apache/karaf/cave/server/http/Activator.java
new file mode 100644
index 0000000..f7d874a
--- /dev/null
+++ b/server/http/src/main/java/org/apache/karaf/cave/server/http/Activator.java
@@ -0,0 +1,70 @@
+/*
+ * 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.karaf.cave.server.http;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+import org.apache.karaf.util.tracker.BaseActivator;
+import org.apache.karaf.util.tracker.annotation.Managed;
+import org.apache.karaf.util.tracker.annotation.RequireService;
+import org.apache.karaf.util.tracker.annotation.Services;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.http.HttpService;
+
+@Services(
+        requires = @RequireService(HttpService.class)
+)
+@Managed("org.apache.karaf.cave.http")
+public class Activator extends BaseActivator implements ManagedService {
+
+    private HttpService httpService;
+    private String alias;
+    private WrapperServlet servlet;
+
+    @Override
+    protected void doStart() throws Exception {
+        httpService = getTrackedService(HttpService.class);
+        if (httpService == null) {
+            return;
+        }
+
+        String alias = getString("cave.http.alias", "/cave/http");
+        Hashtable<String, String> config = new Hashtable<>();
+        if (getConfiguration() != null) {
+            for (Enumeration<String> e = getConfiguration().keys(); 
e.hasMoreElements();) {
+                String key = e.nextElement();
+                String val = getConfiguration().get(key).toString();
+                config.put(key, val);
+            }
+        }
+        this.alias = alias;
+        this.servlet = new WrapperServlet();
+        this.httpService.registerServlet(this.alias, this.servlet, config, 
null);
+    }
+
+    @Override
+    protected void doStop() {
+        super.doStop();
+        if (httpService != null) {
+            httpService.unregister(alias);
+        }
+        if (this.servlet != null) {
+            this.servlet.destroy();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/http/src/main/java/org/apache/karaf/cave/server/http/WrapperServlet.java
----------------------------------------------------------------------
diff --git 
a/server/http/src/main/java/org/apache/karaf/cave/server/http/WrapperServlet.java
 
b/server/http/src/main/java/org/apache/karaf/cave/server/http/WrapperServlet.java
index b219a4b..e2f360e 100644
--- 
a/server/http/src/main/java/org/apache/karaf/cave/server/http/WrapperServlet.java
+++ 
b/server/http/src/main/java/org/apache/karaf/cave/server/http/WrapperServlet.java
@@ -60,7 +60,7 @@ public class WrapperServlet extends HttpServlet {
     public void init(ServletConfig servletConfig) throws ServletException {
         ServletContext context = servletConfig.getServletContext();
         bundleContext = (BundleContext) 
context.getAttribute("osgi-bundlecontext");
-        tracker = new ServiceTracker<CaveRepositoryService, 
CaveRepositoryService>(bundleContext, CaveRepositoryService.class, null);
+        tracker = new ServiceTracker<>(bundleContext, 
CaveRepositoryService.class, null);
         tracker.open();
     }
 
@@ -188,8 +188,8 @@ public class WrapperServlet extends HttpServlet {
     }
 
     private boolean acceptsGZipEncoding(HttpServletRequest httpRequest) {
-        String acceptEncoding = httpRequest.getHeader("Accept-Encoding");
-        return acceptEncoding != null && acceptEncoding.contains("gzip");
+        String acceptEncoding = httpRequest.getHeader(HEADER_ACCEPT_ENCODING);
+        return acceptEncoding != null && acceptEncoding.contains(GZIP);
     }
 
     private void resolveRelativeUrls(URL url, String baseUri, OutputStream os) 
throws IOException, XMLStreamException, SAXException, 
ParserConfigurationException, TransformerException {

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/http/src/main/resources/WEB-INF/web.xml
----------------------------------------------------------------------
diff --git a/server/http/src/main/resources/WEB-INF/web.xml 
b/server/http/src/main/resources/WEB-INF/web.xml
deleted file mode 100644
index c8688e0..0000000
--- a/server/http/src/main/resources/WEB-INF/web.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<webapp version="2.4"
-        xmlns="http://java.sun.com/xml/ns/j2ee";
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd";>
-
-    <display-name>Karaf Cave HTTP wrapper</display-name>
-
-    <servlet>
-        <servlet-name>WrapperServlet</servlet-name>
-        
<servlet-class>org.apache.karaf.cave.server.http.WrapperServlet</servlet-class>
-    </servlet>
-
-    <servlet-mapping>
-        <servlet-name>WrapperServlet</servlet-name>
-        <url-pattern>/*</url-pattern>
-    </servlet-mapping>
-
-</webapp>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/management/pom.xml
----------------------------------------------------------------------
diff --git a/server/management/pom.xml b/server/management/pom.xml
index 4ccb8e4..98ca1ad 100644
--- a/server/management/pom.xml
+++ b/server/management/pom.xml
@@ -37,12 +37,10 @@
         <dependency>
             <groupId>org.apache.karaf.cave.server</groupId>
             <artifactId>org.apache.karaf.cave.server.api</artifactId>
-            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.karaf</groupId>
             <artifactId>org.apache.karaf.util</artifactId>
-            <version>${karaf.version}</version>
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/pom.xml
----------------------------------------------------------------------
diff --git a/server/maven/pom.xml b/server/maven/pom.xml
new file mode 100644
index 0000000..3a6c865
--- /dev/null
+++ b/server/maven/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf.cave</groupId>
+        <artifactId>org.apache.karaf.cave.server</artifactId>
+        <version>4.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cave.server</groupId>
+    <artifactId>org.apache.karaf.cave.server.maven</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Karaf :: Cave :: Server :: Maven Proxy</name>
+    <description>Maven Proxy Service</description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-services-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            javax.servlet.*;version="[3.0,4)",
+                            !shaded.*,
+                            *
+                        </Import-Package>
+                        <Export-Package>
+                            !*
+                        </Export-Package>
+                        <Private-Package>
+                            org.apache.karaf.cave.server.maven,
+                            org.apache.karaf.util,
+                            org.apache.karaf.util.base64
+                        </Private-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>${servlet.spec.groupId}</groupId>
+            <artifactId>${servlet.spec.artifactId}</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.url</groupId>
+            <artifactId>pax-url-aether</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>org.apache.karaf.util</artifactId>
+        </dependency>
+
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.ops4j.pax.web</groupId>
+            <artifactId>pax-web-jetty-bundle</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.easymock</groupId>
+            <artifactId>easymock</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/src/main/java/org/apache/karaf/cave/server/maven/Activator.java
----------------------------------------------------------------------
diff --git 
a/server/maven/src/main/java/org/apache/karaf/cave/server/maven/Activator.java 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/Activator.java
new file mode 100644
index 0000000..f62516b
--- /dev/null
+++ 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/Activator.java
@@ -0,0 +1,87 @@
+/*
+ * 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.karaf.cave.server.maven;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+import org.apache.karaf.util.tracker.BaseActivator;
+import org.apache.karaf.util.tracker.annotation.Managed;
+import org.apache.karaf.util.tracker.annotation.RequireService;
+import org.apache.karaf.util.tracker.annotation.Services;
+import org.ops4j.pax.url.mvn.MavenResolver;
+import org.ops4j.pax.url.mvn.MavenResolvers;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.http.HttpService;
+
+@Services(
+        requires = @RequireService(HttpService.class)
+)
+@Managed("org.apache.karaf.cave.maven")
+public class Activator extends BaseActivator implements ManagedService {
+
+    private HttpService httpService;
+    private String alias;
+    private MavenResolver resolver;
+    private MavenProxyServlet servlet;
+
+    @Override
+    protected void doStart() throws Exception {
+        httpService = getTrackedService(HttpService.class);
+        if (httpService == null) {
+            return;
+        }
+
+        String pid = getString("cave.maven.pid", "org.ops4j.pax.url.mvn");
+        String alias = getString("cave.maven.alias", "/cave/maven");
+        String realm = getString("cave.maven.realm", "karaf");
+        String downloadRole = getString("cave.maven.downloadRole", null);
+        String uploadRole = getString("cave.maven.uploadRole", "karaf");
+        int poolSize = getInt("cave.maven.poolSize", 8);
+        Hashtable<String, String> config = new Hashtable<>();
+        if (getConfiguration() != null) {
+            for (Enumeration<String> e = getConfiguration().keys(); 
e.hasMoreElements();) {
+                String key = e.nextElement();
+                String val = getConfiguration().get(key).toString();
+                config.put(key, val);
+            }
+        }
+        this.resolver = MavenResolvers.createMavenResolver(null, config, pid);
+        this.alias = alias;
+        this.servlet = new MavenProxyServlet(this.resolver, poolSize, realm, 
downloadRole, uploadRole);
+        this.httpService.registerServlet(this.alias, this.servlet, config, 
null);
+    }
+
+    @Override
+    protected void doStop() {
+        super.doStop();
+        if (httpService != null) {
+            httpService.unregister(alias);
+        }
+        if (this.servlet != null) {
+            this.servlet.destroy();
+        }
+        if (resolver != null) {
+            try {
+                resolver.close();
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/src/main/java/org/apache/karaf/cave/server/maven/DefaultFuture.java
----------------------------------------------------------------------
diff --git 
a/server/maven/src/main/java/org/apache/karaf/cave/server/maven/DefaultFuture.java
 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/DefaultFuture.java
new file mode 100644
index 0000000..26cf235
--- /dev/null
+++ 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/DefaultFuture.java
@@ -0,0 +1,358 @@
+/*
+ * 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.karaf.cave.server.maven;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A simple future
+ */
+public class DefaultFuture<T extends DefaultFuture> {
+
+    /**
+     * A default value to indicate the future has been canceled
+     */
+    private static final Object CANCELED = new Object();
+
+    /**
+     * A number of seconds to wait between two deadlock controls ( 5 seconds )
+     */
+    private static final long DEAD_LOCK_CHECK_INTERVAL = 5000L;
+
+    /**
+     * A lock used by the wait() method
+     */
+    private final Object lock;
+    private FutureListener<T> firstListener;
+    private List<FutureListener<T>> otherListeners;
+    private Object result;
+    private boolean ready;
+    private int waiters;
+
+
+    public DefaultFuture() {
+        this(null);
+    }
+
+    /**
+     * Creates a new instance.
+     */
+    public DefaultFuture(Object lock) {
+        this.lock = lock != null ? lock : this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @SuppressWarnings("unchecked")
+    public T await() throws InterruptedException {
+        synchronized (lock) {
+            while (!ready) {
+                waiters++;
+                try {
+                    // Wait for a notify, or if no notify is called,
+                    // assume that we have a deadlock and exit the
+                    // loop to check for a potential deadlock.
+                    lock.wait(DEAD_LOCK_CHECK_INTERVAL);
+                } finally {
+                    waiters--;
+                    if (!ready) {
+                        checkDeadLock();
+                    }
+                }
+            }
+        }
+        return (T) this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean await(long timeout, TimeUnit unit) throws 
InterruptedException {
+        return await(unit.toMillis(timeout));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean await(long timeoutMillis) throws InterruptedException {
+        return await0(timeoutMillis, true);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @SuppressWarnings("unchecked")
+    public T awaitUninterruptibly() {
+        try {
+            await0(Long.MAX_VALUE, false);
+        } catch (InterruptedException ie) {
+            // Do nothing : this catch is just mandatory by contract
+        }
+
+        return (T) this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean awaitUninterruptibly(long timeout, TimeUnit unit) {
+        return awaitUninterruptibly(unit.toMillis(timeout));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean awaitUninterruptibly(long timeoutMillis) {
+        try {
+            return await0(timeoutMillis, false);
+        } catch (InterruptedException e) {
+            throw new InternalError();
+        }
+    }
+
+    /**
+     * Wait for the Future to be ready. If the requested delay is 0 or
+     * negative, this method immediately returns the value of the
+     * 'ready' flag.
+     * Every 5 second, the wait will be suspended to be able to check if
+     * there is a deadlock or not.
+     *
+     * @param timeoutMillis The delay we will wait for the Future to be ready
+     * @param interruptable Tells if the wait can be interrupted or not
+     * @return <code>true</code> if the Future is ready
+     * @throws InterruptedException If the thread has been interrupted
+     *                              when it's not allowed.
+     */
+    private boolean await0(long timeoutMillis, boolean interruptable) throws 
InterruptedException {
+        long endTime = System.currentTimeMillis() + timeoutMillis;
+
+        synchronized (lock) {
+            if (ready) {
+                return true;
+            } else if (timeoutMillis <= 0) {
+                return false;
+            }
+
+            waiters++;
+            try {
+                for (; ;) {
+                    try {
+                        long timeOut = Math.min(timeoutMillis, 
DEAD_LOCK_CHECK_INTERVAL);
+                        lock.wait(timeOut);
+                    } catch (InterruptedException e) {
+                        if (interruptable) {
+                            throw e;
+                        }
+                    }
+
+                    if (ready) {
+                        return true;
+                    } else if (endTime < System.currentTimeMillis()) {
+                        return false;
+                    }
+                }
+            } finally {
+                waiters--;
+                if (!ready) {
+                    checkDeadLock();
+                }
+            }
+        }
+    }
+
+
+    /**
+     * TODO checkDeadLock.
+     */
+    private void checkDeadLock() {
+//        // Only read / write / connect / write future can cause dead lock.
+//        if (!(this instanceof CloseFuture || this instanceof WriteFuture ||
+//              this instanceof ReadFuture || this instanceof ConnectFuture)) {
+//            return;
+//        }
+//
+//        // Get the current thread stackTrace.
+//        // Using Thread.currentThread().getStackTrace() is the best solution,
+//        // even if slightly less efficient than doing a new 
Exception().getStackTrace(),
+//        // as internally, it does exactly the same thing. The advantage of 
using
+//        // this solution is that we may benefit some improvement with some
+//        // future versions of Java.
+//        StackTraceElement[] stackTrace = 
Thread.currentThread().getStackTrace();
+//
+//        // Simple and quick check.
+//        for (StackTraceElement s: stackTrace) {
+//            if 
(AbstractPollingIoProcessor.class.getName().equals(s.getClassName())) {
+//                IllegalStateException e = new IllegalStateException( "t" );
+//                e.getStackTrace();
+//                throw new IllegalStateException(
+//                    "DEAD LOCK: " + IoFuture.class.getSimpleName() +
+//                    ".await() was invoked from an I/O processor thread.  " +
+//                    "Please use " + IoFutureListener.class.getSimpleName() +
+//                    " or configure a proper thread model alternatively.");
+//            }
+//        }
+//
+//        // And then more precisely.
+//        for (StackTraceElement s: stackTrace) {
+//            try {
+//                Class<?> cls = 
DefaultSshFuture.class.getClassLoader().loadClass(s.getClassName());
+//                if (IoProcessor.class.isAssignableFrom(cls)) {
+//                    throw new IllegalStateException(
+//                        "DEAD LOCK: " + IoFuture.class.getSimpleName() +
+//                        ".await() was invoked from an I/O processor thread.  
" +
+//                        "Please use " + 
IoFutureListener.class.getSimpleName() +
+//                        " or configure a proper thread model 
alternatively.");
+//                }
+//            } catch (Exception cnfe) {
+//                // Ignore
+//            }
+//        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isDone() {
+        synchronized (lock) {
+            return ready;
+        }
+    }
+
+    /**
+     * Sets the result of the asynchronous operation, and mark it as finished.
+     */
+    public void setValue(Object newValue) {
+        synchronized (lock) {
+            // Allow only once.
+            if (ready) {
+                return;
+            }
+
+            result = newValue;
+            ready = true;
+            if (waiters > 0) {
+                lock.notifyAll();
+            }
+        }
+
+        notifyListeners();
+    }
+
+    /**
+     * Returns the result of the asynchronous operation.
+     */
+    protected Object getValue() {
+        synchronized (lock) {
+            return result;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @SuppressWarnings("unchecked")
+    public T addListener(FutureListener<T> listener) {
+        if (listener == null) {
+            throw new NullPointerException("listener");
+        }
+
+        boolean notifyNow = false;
+        synchronized (lock) {
+            if (ready) {
+                notifyNow = true;
+            } else {
+                if (firstListener == null) {
+                    firstListener = listener;
+                } else {
+                    if (otherListeners == null) {
+                        otherListeners = new ArrayList<>(1);
+                    }
+                    otherListeners.add(listener);
+                }
+            }
+        }
+
+        if (notifyNow) {
+            notifyListener(listener);
+        }
+        return (T) this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @SuppressWarnings("unchecked")
+    public T removeListener(FutureListener<T> listener) {
+        if (listener == null) {
+            throw new NullPointerException("listener");
+        }
+
+        synchronized (lock) {
+            if (!ready) {
+                if (listener == firstListener) {
+                    if (otherListeners != null && !otherListeners.isEmpty()) {
+                        firstListener = otherListeners.remove(0);
+                    } else {
+                        firstListener = null;
+                    }
+                } else if (otherListeners != null) {
+                    otherListeners.remove(listener);
+                }
+            }
+        }
+
+        return (T) this;
+    }
+
+    private void notifyListeners() {
+        // There won't be any visibility problem or concurrent modification
+        // because 'ready' flag will be checked against both addListener and
+        // removeListener calls.
+        if (firstListener != null) {
+            notifyListener(firstListener);
+            firstListener = null;
+
+            if (otherListeners != null) {
+                for (FutureListener<T> l : otherListeners) {
+                    notifyListener(l);
+                }
+                otherListeners = null;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void notifyListener(FutureListener<T> l) {
+        try {
+            l.operationComplete((T) this);
+        } catch (Throwable t) {
+            // TODO
+            t.printStackTrace();
+        }
+    }
+
+    public boolean isCanceled() {
+        return getValue() == CANCELED;
+    }
+
+    public void cancel() {
+        setValue(CANCELED);
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/src/main/java/org/apache/karaf/cave/server/maven/FutureListener.java
----------------------------------------------------------------------
diff --git 
a/server/maven/src/main/java/org/apache/karaf/cave/server/maven/FutureListener.java
 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/FutureListener.java
new file mode 100644
index 0000000..0cd7ec5
--- /dev/null
+++ 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/FutureListener.java
@@ -0,0 +1,35 @@
+/*
+ * 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.karaf.cave.server.maven;
+
+/**
+ * Something interested in being notified when the completion
+ * of an asynchronous download operation : {@link DefaultFuture}.
+ *
+ */
+public interface FutureListener<T extends DefaultFuture> {
+
+    /**
+     * Invoked when the operation associated with the {@link DefaultFuture}
+     * has been completed even if you add the listener after the completion.
+     *
+     * @param future The source {@link DefaultFuture} which called this
+     *               callback.
+     */
+    void operationComplete(T future);
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/src/main/java/org/apache/karaf/cave/server/maven/InvalidMavenArtifactRequest.java
----------------------------------------------------------------------
diff --git 
a/server/maven/src/main/java/org/apache/karaf/cave/server/maven/InvalidMavenArtifactRequest.java
 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/InvalidMavenArtifactRequest.java
new file mode 100644
index 0000000..b108edd
--- /dev/null
+++ 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/InvalidMavenArtifactRequest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.karaf.cave.server.maven;
+
+public class InvalidMavenArtifactRequest extends Exception {
+
+    public InvalidMavenArtifactRequest() {
+    }
+
+    public InvalidMavenArtifactRequest(String s) {
+        super(s);
+    }
+
+    public InvalidMavenArtifactRequest(String s, Throwable throwable) {
+        super(s, throwable);
+    }
+
+    public InvalidMavenArtifactRequest(Throwable throwable) {
+        super(throwable);
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/src/main/java/org/apache/karaf/cave/server/maven/MavenProxyServlet.java
----------------------------------------------------------------------
diff --git 
a/server/maven/src/main/java/org/apache/karaf/cave/server/maven/MavenProxyServlet.java
 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/MavenProxyServlet.java
new file mode 100644
index 0000000..21fcbd1
--- /dev/null
+++ 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/MavenProxyServlet.java
@@ -0,0 +1,675 @@
+/*
+ * 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.karaf.cave.server.maven;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AccountException;
+import javax.security.auth.login.FailedLoginException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.bind.DatatypeConverter;
+
+import org.apache.karaf.util.StreamUtils;
+import org.ops4j.pax.url.mvn.MavenResolver;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.http.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MavenProxyServlet extends HttpServlet {
+
+    public static Logger LOGGER = 
LoggerFactory.getLogger(MavenProxyServlet.class);
+
+    public static final Pattern REPOSITORY_ID_REGEX = Pattern.compile("[^ 
]*(@id=([^@ ]+))+[^ ]*");
+
+    private static final String SNAPSHOT_TIMESTAMP_REGEX = 
"^([0-9]{8}.[0-9]{6}-[0-9]+).*";
+    private static final Pattern SNAPSHOT_TIMESTAMP_PATTERN = 
Pattern.compile(SNAPSHOT_TIMESTAMP_REGEX);
+
+    //The pattern below matches a path to the following:
+    //1: groupId
+    //2: artifactId
+    //3: version
+    //4: artifact filename
+    public static final Pattern ARTIFACT_REQUEST_URL_REGEX = 
Pattern.compile("([^ ]+)/([^/ ]+)/([^/ ]+)/([^/ ]+)");
+
+    //The pattern bellow matches the path to the following:
+    //1: groupId
+    //2: artifactId
+    //3: version
+    //4: maven-metadata xml filename
+    //7: repository id.
+    //9: type
+    public static final Pattern ARTIFACT_METADATA_URL_REGEX = 
Pattern.compile("([^ ]+)/([^/ ]+)/([^/ ]+)/(maven-metadata([-]([^ 
.]+))?.xml)([.]([^ ]+))?");
+
+    private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+    private static final String HEADER_AUTHORIZATION = "Authorization";
+    private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";
+
+    protected static final String LOCATION_HEADER = "X-Location";
+
+    private final ConcurrentMap<String, ArtifactDownloadFuture> requestMap = 
new ConcurrentHashMap<>();
+    private final int threadMaximumPoolSize;
+    private final String realm;
+    private final String downloadRole;
+    private final String uploadRole;
+    private ThreadPoolExecutor executorService;
+
+    protected File tmpFolder = new File(System.getProperty("karaf.data") + 
File.separator + "maven" + File.separator + "proxy" + File.separator + "tmp");
+
+    final MavenResolver resolver;
+
+    public MavenProxyServlet(MavenResolver resolver, int 
threadMaximumPoolSize, String realm, String downloadRole, String uploadRole) {
+        this.resolver = resolver;
+        this.threadMaximumPoolSize = threadMaximumPoolSize;
+        this.realm = realm;
+        this.downloadRole = downloadRole;
+        this.uploadRole = uploadRole;
+    }
+
+
+    //
+    //  Lifecycle
+    //
+
+    @Override
+    public void init() throws ServletException {
+        if (!tmpFolder.exists() && !tmpFolder.mkdirs()) {
+            throw new ServletException("Failed to create temporary artifact 
folder");
+        }
+        // Create a thread pool with the given maxmimum number of threads
+        // All threads will time out after 60 seconds
+        int nbThreads = threadMaximumPoolSize > 0 ? threadMaximumPoolSize : 8;
+        executorService = new ThreadPoolExecutor(0, nbThreads, 60, 
TimeUnit.SECONDS,
+                new LinkedBlockingQueue<Runnable>(), new 
ThreadFactory("MavenDownloadProxyServlet"));
+    }
+
+    @Override
+    public void destroy() {
+        if (executorService != null) {
+            executorService.shutdown();
+            try {
+                executorService.awaitTermination(5, TimeUnit.MINUTES);
+            } catch (InterruptedException e) {
+                executorService.shutdownNow();
+            }
+        }
+    }
+
+
+
+    //
+    // Security
+    //
+
+    protected boolean authorize(HttpServletRequest request, 
HttpServletResponse response, String role) throws IOException {
+        if (role == null) {
+            return true;
+        }
+        // Return immediately if the header is missing
+        String authHeader = request.getHeader(HEADER_AUTHORIZATION);
+        if (authHeader != null && authHeader.length() > 0) {
+
+            // Get the authType (Basic, Digest) and authInfo (user/password)
+            // from the header
+            authHeader = authHeader.trim();
+            int blank = authHeader.indexOf(' ');
+            if (blank > 0) {
+                String authType = authHeader.substring(0, blank);
+                String authInfo = authHeader.substring(blank).trim();
+
+                // Check whether authorization type matches
+                if (authType.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
+                    try {
+                        String srcString = base64Decode(authInfo);
+                        int i = srcString.indexOf(':');
+                        String username = srcString.substring(0, i);
+                        String password = srcString.substring(i + 1);
+
+                        // authenticate
+                        Subject subject = doAuthenticate(username, password, 
role);
+                        if (subject != null) {
+                            // as per the spec, set attributes
+                            
request.setAttribute(HttpContext.AUTHENTICATION_TYPE, 
HttpServletRequest.BASIC_AUTH);
+                            request.setAttribute(HttpContext.REMOTE_USER, 
username);
+                            // succeed
+                            return true;
+                        }
+                    } catch (Exception e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+
+        // request authentication
+        try {
+            response.setHeader(HEADER_WWW_AUTHENTICATE, 
AUTHENTICATION_SCHEME_BASIC + " realm=\"" + this.realm + "\"");
+            // must response with status and flush as Jetty may report 
org.eclipse.jetty.server.Response Committed before 401 null
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.setContentLength(0);
+            response.flushBuffer();
+        } catch (IOException ioe) {
+            // failed sending the response ... cannot do anything about it
+        }
+
+        // inform HttpService that authentication failed
+        return false;
+    }
+
+    private static String base64Decode(String srcString) {
+        byte[] transformed = DatatypeConverter.parseBase64Binary(srcString);
+        return new String(transformed, StandardCharsets.ISO_8859_1);
+    }
+
+    public Subject doAuthenticate(final String username, final String 
password, final String role) {
+        try {
+            Subject subject = new Subject();
+            LoginContext loginContext = new LoginContext(realm, subject, new 
CallbackHandler() {
+                public void handle(Callback[] callbacks) throws IOException, 
UnsupportedCallbackException {
+                    for (Callback callback : callbacks) {
+                        if (callback instanceof NameCallback) {
+                            ((NameCallback) callback).setName(username);
+                        } else if (callback instanceof PasswordCallback) {
+                            ((PasswordCallback) 
callback).setPassword(password.toCharArray());
+                        } else {
+                            throw new UnsupportedCallbackException(callback);
+                        }
+                    }
+                }
+            });
+            loginContext.login();
+            if (role != null && role.length() > 0) {
+                String clazz = 
"org.apache.karaf.jaas.boot.principal.RolePrincipal";
+                String name = role;
+                int idx = role.indexOf(':');
+                if (idx > 0) {
+                    clazz = role.substring(0, idx);
+                    name = role.substring(idx + 1);
+                }
+                boolean found = false;
+                for (Principal p : subject.getPrincipals()) {
+                    if (p.getClass().getName().equals(clazz)
+                            && p.getName().equals(name)) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found) {
+                    throw new FailedLoginException("User does not have the 
required role " + role);
+                }
+            }
+            return subject;
+        } catch (AccountException e) {
+            LOGGER.warn("Account failure", e);
+            return null;
+        } catch (LoginException e) {
+            LOGGER.debug("Login failed", e);
+            return null;
+        }
+    }
+
+
+
+
+
+    //
+    // Download
+    //
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final 
HttpServletResponse resp) throws ServletException, IOException {
+        if (!authorize(req, resp, downloadRole)) {
+            return;
+        }
+        String tpath = req.getPathInfo();
+        if (tpath == null) {
+            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            return;
+        }
+        if (tpath.startsWith("/")) {
+            tpath = tpath.substring(1);
+        }
+        final String path = tpath;
+
+        final AsyncContext asyncContext = req.startAsync();
+        asyncContext.setTimeout(TimeUnit.MINUTES.toMillis(5));
+        final ArtifactDownloadFuture future = new ArtifactDownloadFuture(path);
+        ArtifactDownloadFuture masterFuture = requestMap.putIfAbsent(path, 
future);
+        if (masterFuture == null) {
+            masterFuture = future;
+            masterFuture.lock();
+            executorService.execute(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        File file = download(path);
+                        future.setValue(file);
+                    } catch (Throwable t) {
+                        future.setValue(t);
+                    }
+                }
+            });
+        } else {
+            masterFuture.lock();
+        }
+        masterFuture.addListener(new FutureListener<ArtifactDownloadFuture>() {
+            @Override
+            public void operationComplete(ArtifactDownloadFuture future) {
+                Object value = future.getValue();
+                if (value instanceof Throwable) {
+                    LOGGER.warn("Error while downloading artifact: {}", 
((Throwable) value).getMessage(), value);
+                    
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                } else if (value instanceof File) {
+                    File artifactFile = (File) value;
+                    try (InputStream is = new FileInputStream(artifactFile)) {
+                        LOGGER.info("Writing response for file : {}", path);
+                        resp.setStatus(HttpServletResponse.SC_OK);
+                        resp.setContentType("application/octet-stream");
+                        resp.setDateHeader("Date", System.currentTimeMillis());
+                        resp.setHeader("Connection", "close");
+                        resp.setContentLength(is.available());
+                        Bundle bundle = FrameworkUtil.getBundle(getClass());
+                        if (bundle != null) {
+                            resp.setHeader("Server", bundle.getSymbolicName() 
+ "/" + bundle.getVersion());
+                        } else {
+                            resp.setHeader("Server", "Karaf Maven Proxy");
+                        }
+                        StreamUtils.copy(is, resp.getOutputStream());
+                    } catch (Exception e) {
+                        LOGGER.warn("Error while sending artifact: {}", 
e.getMessage(), e);
+                        
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                    }
+                } else {
+                    resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+                future.release();
+                try {
+                    asyncContext.complete();
+                } catch (IllegalStateException e) {
+                    // Ignore, the response must have already been sent with 
an error
+                }
+            }
+        });
+    }
+
+    public File download(String path) throws InvalidMavenArtifactRequest {
+        if (path == null) {
+            throw new InvalidMavenArtifactRequest();
+        }
+
+        Matcher artifactMatcher = ARTIFACT_REQUEST_URL_REGEX.matcher(path);
+        Matcher metadataMatcher = ARTIFACT_METADATA_URL_REGEX.matcher(path);
+
+        if (metadataMatcher.matches()) {
+            LOGGER.info("Received request for maven metadata : {}", path);
+            try {
+                MavenCoord coord = convertMetadataPathToCoord(path);
+                return resolver.resolveMetadata(coord.groupId, 
coord.artifactId, coord.type, coord.version);
+            } catch (Exception e) {
+                LOGGER.warn(String.format("Could not find metadata : %s due to 
%s", path, e.getMessage()), e);
+                return null;
+            }
+        } else if (artifactMatcher.matches()) {
+            LOGGER.info("Received request for maven artifact : {}", path);
+            try {
+                MavenCoord artifact = convertArtifactPathToCoord(path);
+                Path download = resolver.resolve(artifact.groupId, 
artifact.artifactId, artifact.classifier, artifact.type, 
artifact.version).toPath();
+                Path tmpFile = Files.createTempFile("mvn-", ".tmp");
+                Files.copy(download, tmpFile, 
StandardCopyOption.REPLACE_EXISTING);
+                return tmpFile.toFile();
+            } catch (Exception e) {
+                LOGGER.warn(String.format("Could not find artifact : %s due to 
%s", path, e.getMessage()), e);
+                return null;
+            }
+        }
+        return null;
+    }
+
+    private class ArtifactDownloadFuture extends 
DefaultFuture<ArtifactDownloadFuture> {
+
+        private final AtomicInteger participants = new AtomicInteger();
+        private final String path;
+
+        private ArtifactDownloadFuture(String path) {
+            this.path = path;
+        }
+
+        public void lock() {
+            participants.incrementAndGet();
+        }
+
+        public void release() {
+            if (participants.decrementAndGet() == 0) {
+                requestMap.remove(path);
+                Object v = getValue();
+                if (v instanceof File) {
+                    ((File) v).delete();
+                }
+            }
+        }
+
+    }
+
+
+
+    //
+    // Upload
+    //
+
+    @Override
+    protected void doPut(HttpServletRequest request, HttpServletResponse 
response) throws ServletException, IOException {
+        if (!authorize(request, response, uploadRole)) {
+            return;
+        }
+        try {
+            String path = request.getPathInfo();
+            //Make sure path is valid
+            if (path != null) {
+                if (path.startsWith("/")) {
+                    path = path.substring(1);
+                }
+            }
+            if (path == null || path.isEmpty()) {
+                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+                return;
+            }
+
+            boolean result;
+            // handle move
+            String location = request.getHeader(LOCATION_HEADER);
+            if (location != null) {
+                result = upload(new File(location), path, response);
+            } else {
+                Path dir = 
tmpFolder.toPath().resolve(UUID.randomUUID().toString());
+                Path temp = dir.resolve(Paths.get(path).getFileName());
+                Files.createDirectories(dir);
+                try (OutputStream os = Files.newOutputStream(temp)) {
+                    StreamUtils.copy(request.getInputStream(), os);
+                }
+                result = upload(temp.toFile(), path, response);
+            }
+
+            response.setStatus(result ? HttpServletResponse.SC_ACCEPTED : 
HttpServletResponse.SC_NOT_ACCEPTABLE);
+
+        } catch (InvalidMavenArtifactRequest ex) {
+            // must response with status and flush as Jetty may report 
org.eclipse.jetty.server.Response Committed before 401 null
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            response.setContentLength(0);
+            response.flushBuffer();
+        } catch (Exception ex) {
+            // must response with status and flush as Jetty may report 
org.eclipse.jetty.server.Response Committed before 401 null
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            response.setContentLength(0);
+            response.flushBuffer();
+        }
+
+    }
+
+    protected boolean upload(File input, String path, HttpServletResponse 
response) throws InvalidMavenArtifactRequest, NoSuchFileException {
+        if (!input.isFile()) {
+            throw new NoSuchFileException(input.toString());
+        }
+        if (path == null) {
+            throw new InvalidMavenArtifactRequest();
+        }
+        // root path, try reading mvn coords
+        if (path.indexOf('/') < 0) {
+            try {
+                String mvnCoordsPath = readMvnCoordsPath(input);
+                if (mvnCoordsPath != null) {
+                    return install(input, mvnCoordsPath);
+                } else {
+                    response.addHeader(LOCATION_HEADER, input.toString()); // 
we need manual mvn coords input
+                    return true;
+                }
+            } catch (Exception e) {
+                LOGGER.warn(String.format("Failed to deploy artifact : %s due 
to %s", path, e.getMessage()), e);
+                return false;
+            }
+        }
+
+        return install(input, path);
+
+    }
+
+    private boolean install(File file, String path) {
+        Matcher artifactMatcher = ARTIFACT_REQUEST_URL_REGEX.matcher(path);
+        Matcher metadataMatcher = ARTIFACT_METADATA_URL_REGEX.matcher(path);
+
+        if (metadataMatcher.matches()) {
+            LOGGER.info("Received upload request for maven metadata : {}", 
path);
+            try {
+                MavenCoord coord = convertMetadataPathToCoord(path);
+                resolver.uploadMetadata(coord.groupId, coord.artifactId, 
coord.type, coord.version, file);
+                LOGGER.info("Maven metadata installed: {}", coord.toString());
+            } catch (Exception e) {
+                LOGGER.warn(String.format("Failed to upload metadata: %s due 
to %s", path, e.getMessage()), e);
+                return false;
+            }
+            //If no matching metadata found return nothing
+        } else if (artifactMatcher.matches()) {
+            LOGGER.info("Received upload request for maven artifact : {}", 
path);
+            try {
+                MavenCoord coord = convertArtifactPathToCoord(path);
+                resolver.upload(coord.groupId, coord.artifactId, 
coord.classifier, coord.type, coord.version, file);
+                LOGGER.info("Artifact installed: {}", coord.toString());
+            } catch (Exception e) {
+                LOGGER.warn(String.format("Failed to upload artifact : %s due 
to %s", path, e.getMessage()), e);
+                return false;
+            }
+        }
+        return false;
+    }
+
+    protected static String readMvnCoordsPath(File file) throws Exception {
+        try (JarFile jarFile = new JarFile(file)) {
+            String previous = null;
+            String match = null;
+
+            Enumeration<JarEntry> entries = jarFile.entries();
+            while (entries.hasMoreElements()) {
+                JarEntry entry = entries.nextElement();
+                String name = entry.getName();
+                if (name.startsWith("META-INF/maven/") && 
name.endsWith("pom.properties")) {
+                    if (previous != null) {
+                        throw new 
IllegalStateException(String.format("Duplicate pom.properties found: %s != %s", 
name, previous));
+                    }
+
+                    previous = name; // check for dups
+
+                    Properties props = new Properties();
+                    try (InputStream stream = jarFile.getInputStream(entry)) {
+                        props.load(stream);
+                    }
+                    String groupId = props.getProperty("groupId");
+                    String artifactId = props.getProperty("artifactId");
+                    String version = props.getProperty("version");
+                    String type = getFileExtension(file);
+                    match = String.format("%s/%s/%s/%s-%s.%s", groupId, 
artifactId, version, artifactId, version, type != null ? type : "jar");
+                }
+            }
+
+            return match;
+        }
+    }
+
+    private static String getFileExtension(File file) {
+        String fileName = file.getName();
+        int idx = fileName.lastIndexOf('.');
+        if (idx > 1) {
+            String answer = fileName.substring(idx + 1);
+            if (answer.length() > 0) {
+                return answer;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Converts the path of the request to maven coords.
+     *
+     * @param path The request path, following the format: {@code 
<groupId>/<artifactId>/<version>/<artifactId>-<version>-[<classifier>].extension}
+     * @return A {@link MavenCoord}
+     * @throws InvalidMavenArtifactRequest
+     */
+    protected MavenCoord convertArtifactPathToCoord(String path) throws 
InvalidMavenArtifactRequest {
+        if (path == null) {
+            throw new InvalidMavenArtifactRequest("Cannot match request path 
to maven url, request path is empty.");
+        }
+        Matcher pathMatcher = ARTIFACT_REQUEST_URL_REGEX.matcher(path);
+        if (pathMatcher.matches()) {
+            String groupId = pathMatcher.group(1).replaceAll("/", ".");
+            String artifactId = pathMatcher.group(2);
+            String version = pathMatcher.group(3);
+            String filename = pathMatcher.group(4);
+            String extension;
+            String classifier = "";
+            String filePerfix = artifactId + "-" + version;
+            String stripedFileName;
+
+            if (version.endsWith("SNAPSHOT")) {
+                String baseVersion = version.replaceAll("-SNAPSHOT", "");
+                String timestampedFileName = 
filename.substring(artifactId.length() + baseVersion.length() + 2);
+                //Check if snapshot is timestamped and override the version. 
@{link Artifact} will still treat it as a SNAPSHOT.
+                //and also in case of artifact installation the proper 
filename will be used.
+                Matcher ts = 
SNAPSHOT_TIMESTAMP_PATTERN.matcher(timestampedFileName);
+                if (ts.matches()) {
+                    version = baseVersion + "-" + ts.group(1);
+                    filePerfix = artifactId + "-" + version;
+                }
+                stripedFileName = 
filename.replaceAll(SNAPSHOT_TIMESTAMP_REGEX, "SNAPSHOT");
+                stripedFileName = 
stripedFileName.substring(filePerfix.length());
+            } else {
+                stripedFileName = filename.substring(filePerfix.length());
+            }
+
+            if (stripedFileName.startsWith("-") && 
stripedFileName.contains(".")) {
+                classifier = stripedFileName.substring(1, 
stripedFileName.indexOf('.'));
+            }
+            extension = stripedFileName.substring(stripedFileName.indexOf('.') 
+ 1);
+
+            MavenCoord coord = new MavenCoord();
+            coord.groupId = groupId;
+            coord.artifactId = artifactId;
+            coord.type = extension;
+            coord.classifier = classifier;
+            coord.version = version;
+            return coord;
+        }
+        return null;
+    }
+
+    /**
+     * Converts the path of the request to {@link MavenCoord}.
+     *
+     * @param path The request path, following the format: {@code 
<groupId>/<artifactId>/<version>/<artifactId>-<version>-[<classifier>].extension}
+     * @return A {@link MavenCoord}
+     * @throws InvalidMavenArtifactRequest
+     */
+    protected MavenCoord convertMetadataPathToCoord(String path) throws 
InvalidMavenArtifactRequest {
+        if (path == null) {
+            throw new InvalidMavenArtifactRequest("Cannot match request path 
to maven url, request path is empty.");
+        }
+        Matcher pathMatcher = ARTIFACT_METADATA_URL_REGEX.matcher(path);
+        if (pathMatcher.matches()) {
+            MavenCoord coord = new MavenCoord();
+            coord.groupId = pathMatcher.group(1).replaceAll("/", ".");
+            coord.artifactId = pathMatcher.group(2);
+            coord.version = pathMatcher.group(3);
+            String type = pathMatcher.group(8);
+            coord.type = type == null ? "maven-metadata.xml" : 
"maven-metadata.xml." + type;
+            return coord;
+        }
+        return null;
+    }
+
+    static class MavenCoord {
+        String groupId;
+        String artifactId;
+        String type;
+        String classifier;
+        String version;
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(groupId).append(":").append(artifactId).append(":");
+            sb.append(type).append(":");
+            if (classifier != null && !classifier.isEmpty()) {
+                sb.append(classifier).append(":");
+            }
+            sb.append(version);
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Reads a {@link java.io.File} from the {@link java.io.InputStream} then 
saves it under a temp location and returns the file.
+     *
+     * @param is           The source input stream.
+     * @param tempLocation The temporary location to save the content of the 
stream.
+     * @param name         The name of the file.
+     * @return
+     * @throws java.io.FileNotFoundException
+     */
+    protected File copyFile(InputStream is, File tempLocation, String name) 
throws IOException {
+        Path tmpFile = tempLocation.toPath().resolve(name);
+        Files.deleteIfExists(tmpFile);
+        try (OutputStream os = Files.newOutputStream(tmpFile)) {
+            StreamUtils.copy(is, os);
+        }
+        return tmpFile.toFile();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf-cave/blob/e63f19b1/server/maven/src/main/java/org/apache/karaf/cave/server/maven/ThreadFactory.java
----------------------------------------------------------------------
diff --git 
a/server/maven/src/main/java/org/apache/karaf/cave/server/maven/ThreadFactory.java
 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/ThreadFactory.java
new file mode 100644
index 0000000..485b439
--- /dev/null
+++ 
b/server/maven/src/main/java/org/apache/karaf/cave/server/maven/ThreadFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.karaf.cave.server.maven;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link ThreadFactory} which sets the thread name to an unique name.
+ * <p/>
+ * The thread name uses the following syntax <tt>name #counter</tt>, where 
counter in an unique counter, starting from 1.
+ */
+public final class ThreadFactory implements java.util.concurrent.ThreadFactory 
{
+
+    private static final AtomicInteger counter = new AtomicInteger();
+
+    private final String name;
+
+    /**
+     * Prefix of the thread name
+     */
+    public ThreadFactory(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Thread newThread(Runnable r) {
+        return new Thread(r, name + " #" + counter.incrementAndGet());
+    }
+}

Reply via email to