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()); + } +}
