Index: assembly/src/main/distribution/text/etc/org.ops4j.pax.url.mvn.cfg =================================================================== --- assembly/src/main/distribution/text/etc/org.ops4j.pax.url.mvn.cfg (revision 998721) +++ assembly/src/main/distribution/text/etc/org.ops4j.pax.url.mvn.cfg (working copy) @@ -55,7 +55,8 @@ # # The following property value will add the system folder as a repo. # -org.ops4j.pax.url.mvn.defaultRepositories=file:${karaf.home}/${karaf.default.repository}@snapshots +org.ops4j.pax.url.mvn.defaultRepositories=file:${karaf.home}/${karaf.default.repository}@snapshots, \ + file:${karaf.home}/local-repo@snapshots # # Comma separated list of repositories scanned when resolving an artifact. Index: assembly/src/main/descriptors/windows-bin.xml =================================================================== --- assembly/src/main/descriptors/windows-bin.xml (revision 998721) +++ assembly/src/main/descriptors/windows-bin.xml (working copy) @@ -199,6 +199,7 @@ org.apache.karaf.deployer:org.apache.karaf.deployer.spring org.apache.karaf.deployer:org.apache.karaf.deployer.blueprint org.apache.karaf.deployer:org.apache.karaf.deployer.features + org.apache.karaf.deployer:org.apache.karaf.deployer.kar Index: assembly/src/main/descriptors/unix-bin.xml =================================================================== --- assembly/src/main/descriptors/unix-bin.xml (revision 998721) +++ assembly/src/main/descriptors/unix-bin.xml (working copy) @@ -212,6 +212,7 @@ org.apache.karaf.deployer:org.apache.karaf.deployer.spring org.apache.karaf.deployer:org.apache.karaf.deployer.blueprint org.apache.karaf.deployer:org.apache.karaf.deployer.features + org.apache.karaf.deployer:org.apache.karaf.deployer.kar Index: assembly/src/main/filtered-resources/etc/startup.properties =================================================================== --- assembly/src/main/filtered-resources/etc/startup.properties (revision 998721) +++ assembly/src/main/filtered-resources/etc/startup.properties (working copy) @@ -62,3 +62,4 @@ org/apache/karaf/deployer/org.apache.karaf.deployer.spring/${project.version}/org.apache.karaf.deployer.spring-${project.version}.jar=30 org/apache/karaf/deployer/org.apache.karaf.deployer.blueprint/${project.version}/org.apache.karaf.deployer.blueprint-${project.version}.jar=30 org/apache/karaf/deployer/org.apache.karaf.deployer.features/${project.version}/org.apache.karaf.deployer.features-${project.version}.jar=30 +org/apache/karaf/deployer/org.apache.karaf.deployer.kar/${project.version}/org.apache.karaf.deployer.kar-${project.version}.jar=30 Index: assembly/src/main/filtered-resources/features.xml =================================================================== --- assembly/src/main/filtered-resources/features.xml (revision 998721) +++ assembly/src/main/filtered-resources/features.xml (working copy) @@ -66,6 +66,9 @@ mvn:org.ops4j.pax.url/pax-url-war/${pax.url.version} mvn:org.apache.karaf.deployer/org.apache.karaf.deployer.war/${project.version} + + mvn:org.apache.karaf.deployer/org.apache.karaf.deployer.kar/${project.version} + http Index: assembly/pom.xml =================================================================== --- assembly/pom.xml (revision 998721) +++ assembly/pom.xml (working copy) @@ -74,6 +74,10 @@ org.apache.karaf.deployer + org.apache.karaf.deployer.kar + + + org.apache.karaf.deployer org.apache.karaf.deployer.war Index: deployer/pom.xml =================================================================== --- deployer/pom.xml (revision 998721) +++ deployer/pom.xml (working copy) @@ -37,6 +37,7 @@ blueprint features war + kar Index: deployer/kar/NOTICE =================================================================== --- deployer/kar/NOTICE (revision 0) +++ deployer/kar/NOTICE (revision 0) @@ -0,0 +1,21 @@ +Apache Felix Karaf +Copyright 2010 The Apache Software Foundation + + +I. Included Software + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + + +II. Used Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2010). +Licensed under the Apache License 2.0. + + +III. License Summary +- Apache License 2.0 Index: deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarDeploymentListenerTest.java =================================================================== --- deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarDeploymentListenerTest.java (revision 0) +++ deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarDeploymentListenerTest.java (revision 0) @@ -0,0 +1,56 @@ +package org.apache.karaf.deployer.kar; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URI; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class KarDeploymentListenerTest { + + private KarDeploymentListener karDeploymentListener; + private URI goodKarFile; + private URI zipFileWithKarafManifest; + private URI zipFileWithoutKarafManifest; + private URI badZipFile; + + @Before + public void setUp() throws Exception { + karDeploymentListener = new KarDeploymentListener(); + karDeploymentListener.init(); + + goodKarFile = getClass().getClassLoader().getResource("goodKarFile.kar").toURI(); + zipFileWithKarafManifest = getClass().getClassLoader().getResource("karFileAsZip.zip").toURI(); + zipFileWithoutKarafManifest = getClass().getClassLoader().getResource("karFileAsZipNoManifest.zip").toURI(); + badZipFile = getClass().getClassLoader().getResource("badZipFile.zip").toURI(); + } + + @After + public void destroy() throws Exception { + karDeploymentListener.destroy(); + } + + @Test + public void shouldHandleKarFile() throws Exception { + assertTrue(karDeploymentListener.canHandle(new File(goodKarFile))); + } + + @Test + public void shouldHandleZipFileWithKarafManifest() throws Exception { + assertTrue(karDeploymentListener.canHandle(new File(zipFileWithKarafManifest))); + } + + @Test + public void shouldIgnoreZipFileWithoutKarafManifest() throws Exception { + assertFalse(karDeploymentListener.canHandle(new File(zipFileWithoutKarafManifest))); + } + + @Test + public void shouldIgnoreBadZipFile() throws Exception { + assertFalse(karDeploymentListener.canHandle(new File(badZipFile))); + } +} \ No newline at end of file Index: deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarURLHandlerTest.java =================================================================== --- deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarURLHandlerTest.java (revision 0) +++ deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarURLHandlerTest.java (revision 0) @@ -0,0 +1,88 @@ +package org.apache.karaf.deployer.kar; + +import static org.easymock.EasyMock.createMock; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; + +import org.apache.karaf.features.FeaturesService; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class KarURLHandlerTest { + + private KarURLHandler urlHandler; + private FeaturesService featuresService; + + @Before + public void setUp() throws Exception { + urlHandler = new KarURLHandler(); + featuresService = createMock(FeaturesService.class); + urlHandler.setFeaturesService(featuresService); + urlHandler.setLocalRepoPath("./target/local-repo"); + + urlHandler.init(); + } + + @After + public void tearDown() throws Exception { + urlHandler.destroy(); + } + + @Test (expected = MalformedURLException.class) + public void shouldThrowMalformedURLWhenPathIsEmpty() throws Exception{ + urlHandler.openConnection(new URL("http", null, "")); + } + + @Test + public void shouldRecognizeGoodFeaturesFile() throws Exception + { + URI goodFeaturesXml = getClass().getClassLoader().getResource("goodKarFile/org/foo/goodFeaturesXml.xml").toURI(); + Assert.assertTrue(urlHandler.isFeaturesRepository(new File(goodFeaturesXml))); + } + + @Test + public void shouldRejectBadFeaturesFile() throws Exception + { + URI goodFeaturesXml = getClass().getClassLoader().getResource("badFeaturesXml.xml").toURI(); + Assert.assertFalse((urlHandler.isFeaturesRepository(new File(goodFeaturesXml)))); + } + + @Test + public void shouldExtractAndRegisterFeaturesFromKar() throws Exception { + // Setup expectations on the features service + featuresService.addRepository(EasyMock.anyObject(URI.class)); + EasyMock.replay(featuresService); + + // Test + // + getClass().getClassLoader().getResource("goodKarFile.kar").getFile(); + urlHandler.extractKarafArchive(new URL("file", null, getClass().getClassLoader().getResource("goodKarFile.kar").getFile())); + + // Verify expectations. + // + EasyMock.verify(featuresService); + } + + @Test + public void shouldExtractAndRegisterFeaturesFromZip() throws Exception { + // Setup expectations on the features service + featuresService.addRepository(EasyMock.anyObject(URI.class)); + EasyMock.replay(featuresService); + + // Test + // + getClass().getClassLoader().getResource("karFileAsZip").getFile(); + urlHandler.extractKarafArchive(new URL("file", null, getClass().getClassLoader().getResource("goodKarFile.kar").getFile())); + + // Verify expectations. + // + EasyMock.verify(featuresService); + } + +} Index: deployer/kar/src/test/resources/goodKarFile.kar =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: deployer/kar/src/test/resources/goodKarFile.kar ___________________________________________________________________ Added: svn:mime-type + application/octet-stream Index: deployer/kar/src/test/resources/karFileAsZip/META-INF/KARAF.MF =================================================================== Index: deployer/kar/src/test/resources/karFileAsZip/org/foo/goodFeaturesXml.xml =================================================================== --- deployer/kar/src/test/resources/karFileAsZip/org/foo/goodFeaturesXml.xml (revision 0) +++ deployer/kar/src/test/resources/karFileAsZip/org/foo/goodFeaturesXml.xml (revision 0) @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file Index: deployer/kar/src/test/resources/badFeaturesXml.xml =================================================================== --- deployer/kar/src/test/resources/badFeaturesXml.xml (revision 0) +++ deployer/kar/src/test/resources/badFeaturesXml.xml (revision 0) @@ -0,0 +1 @@ +This is not a features file! \ No newline at end of file Index: deployer/kar/src/test/resources/karFileAsZip.zip =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: deployer/kar/src/test/resources/karFileAsZip.zip ___________________________________________________________________ Added: svn:mime-type + application/octet-stream Index: deployer/kar/src/test/resources/goodKarFile/META-INF/KARAF.MF =================================================================== Index: deployer/kar/src/test/resources/goodKarFile/org/foo/goodFeaturesXml.xml =================================================================== --- deployer/kar/src/test/resources/goodKarFile/org/foo/goodFeaturesXml.xml (revision 0) +++ deployer/kar/src/test/resources/goodKarFile/org/foo/goodFeaturesXml.xml (revision 0) @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file Index: deployer/kar/src/test/resources/goodKarFile/org/bar/hello.txt =================================================================== Index: deployer/kar/src/test/resources/karFileAsZipNoManifest/org/foo/goodFeaturesXml.xml =================================================================== --- deployer/kar/src/test/resources/karFileAsZipNoManifest/org/foo/goodFeaturesXml.xml (revision 0) +++ deployer/kar/src/test/resources/karFileAsZipNoManifest/org/foo/goodFeaturesXml.xml (revision 0) @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file Index: deployer/kar/src/test/resources/karFileAsZipNoManifest.zip =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: deployer/kar/src/test/resources/karFileAsZipNoManifest.zip ___________________________________________________________________ Added: svn:mime-type + application/octet-stream Index: deployer/kar/src/test/resources/badZipFile.zip =================================================================== --- deployer/kar/src/test/resources/badZipFile.zip (revision 0) +++ deployer/kar/src/test/resources/badZipFile.zip (revision 0) @@ -0,0 +1 @@ +This is *not* a well formed .kar file :( \ No newline at end of file Index: deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarDeploymentListener.java =================================================================== --- deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarDeploymentListener.java (revision 0) +++ deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarDeploymentListener.java (revision 0) @@ -0,0 +1,73 @@ +/* + * 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.deployer.kar; + +import java.io.File; +import java.net.URL; +import java.util.zip.ZipFile; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.felix.fileinstall.ArtifactUrlTransformer; + +/** + * A deployment listener able to hot deploy a Karaf archive + */ +public class KarDeploymentListener implements ArtifactUrlTransformer { + + private static final Log LOGGER = LogFactory + .getLog(KarDeploymentListener.class); + + private static final String KAR_PREFIX = ".kar"; + private static final String ZIP_PREFIX = ".zip"; + + public boolean canHandle(File artifact) { + // If the file ends with .kar, then we can handle it! + // + if (artifact.isFile() && artifact.getName().endsWith(KAR_PREFIX)) { + LOGGER.info("Found a .kar file to deploy."); + return true; + } + // Otherwise, check to see if it's a zip file containing a META-INF/KARAF.MF manifest. + // + else if (artifact.isFile() && artifact.getName().endsWith(ZIP_PREFIX)) { + LOGGER.debug("Found a .zip file to deploy; checking contents to see if it's a Karaf archive."); + try { + if (new ZipFile(artifact).getEntry("META-INF/KARAF.MF") != null) { + LOGGER.info("Found a Karaf archive with .zip prefix; will deploy."); + return true; + } + } catch (Exception e) { + LOGGER.warn("Problem extracting zip file '" + artifact.getName() + "'; ignoring.", e); + } + } + + return false; + } + + public URL transform(URL url) throws Exception { + return new URL("kar", null, url.toString()); + } + + public void init() { + LOGGER.debug("Initializing " + KarDeploymentListener.class.getName()); + } + + public void destroy() { + LOGGER.debug("Destroying " + KarDeploymentListener.class.getName()); + } +} Index: deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarURLHandler.java =================================================================== --- deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarURLHandler.java (revision 0) +++ deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarURLHandler.java (revision 0) @@ -0,0 +1,194 @@ +package org.apache.karaf.deployer.kar; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.karaf.features.FeaturesService; +import org.osgi.service.url.AbstractURLStreamHandlerService; +import org.w3c.dom.Document; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class KarURLHandler extends AbstractURLStreamHandlerService { + private static Log logger = LogFactory.getLog(KarURLHandler.class); + + private static String SYNTAX = "kar: kar-uri"; + + private String localRepoPath = "./local-repo"; + + private byte[] buffer = new byte[5 * 1024]; + + private DocumentBuilderFactory dbf; + + private FeaturesService featuresService; + + @Override + public synchronized URLConnection openConnection(URL url) throws IOException { + if (url.getPath() == null || url.getPath().trim().length() == 0) { + throw new MalformedURLException ("Path can not be null or empty. Syntax: " + SYNTAX ); + } + + if (logger.isInfoEnabled()) + logger.info("Extracting Karaf Archive '" + url + "' to " + localRepoPath); + + extractKarafArchive(new URL(url.getPath())); + + return new Connection(url); + } + + protected void extractKarafArchive(URL url) throws IOException { + ZipFile zipFile = null; + try { + zipFile = new ZipFile(url.getFile()); + } catch (IOException e) { + logger.error("Unable to open file; details: " + e.getMessage()); + throw e; + } + Enumeration entries = (Enumeration) zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = (ZipEntry) entries.nextElement(); + + if (! entry.getName().startsWith("META-INF")) { + if (entry.isDirectory()) { + java.io.File directory = new File(localRepoPath + File.separator + entry.getName()); + if (logger.isDebugEnabled()) + logger.debug("Creating directory '" + directory.getName()); + directory.mkdirs(); + } else { + File extract = new File(localRepoPath + File.separator + entry.getName()); + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(extract)); + + int count = 0; + int totalBytes = 0; + InputStream inputStream = zipFile.getInputStream(entry); + while ((count = inputStream.read(buffer)) > 0) + { + bos.write(buffer, 0, count); + totalBytes += count; + } + + if (logger.isDebugEnabled()) + logger.debug("Extracted " + totalBytes + " bytes to " + extract); + + bos.close(); + inputStream.close(); + + if (isFeaturesRepository(extract)) { + addToFeaturesRepositories(extract); + } + } + } + } + + zipFile.close(); + + } + + protected boolean isFeaturesRepository(File artifact) { + try { + if (artifact.isFile() && artifact.getName().endsWith(".xml")) { + Document doc = parse(artifact); + String name = doc.getDocumentElement().getLocalName(); + String uri = doc.getDocumentElement().getNamespaceURI(); + if ("features".equals(name) && (uri == null || "".equals(uri))) { + return true; + } + } + } catch (Exception e) { + if (logger.isDebugEnabled()) + logger.debug("File " + artifact.getName() + " is not a features file.", e); + } + return false; + } + + protected Document parse(File artifact) throws Exception { + DocumentBuilder db = dbf.newDocumentBuilder(); + db.setErrorHandler(new ErrorHandler() { + public void warning(SAXParseException exception) throws SAXException { + } + public void error(SAXParseException exception) throws SAXException { + } + public void fatalError(SAXParseException exception) throws SAXException { + throw exception; + } + }); + return db.parse(artifact); + } + + private void addToFeaturesRepositories(File file) { + try { + featuresService.addRepository(file.toURI()); + if (logger.isInfoEnabled()) + logger.info("Added feature repository '" + file.toURI() + "'."); + } catch (Exception e) { + logger.error("Unable to add repository '" + file.getName() + "'", e); + } + } + + public class Connection extends URLConnection { + public Connection(URL url) { + super(url); + } + + @Override + public void connect() throws IOException { + } + + @Override + public InputStream getInputStream() throws IOException { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + // TODO: Adrian Trenaman 20/09/10 + // + // Not sure if there really is a need to transform the URL into a 'bundle' + // Leaving this in commented-out just in case; can reintroduce later. + // + // KarURLTransformer.transform(url, os); + os.close(); + return new ByteArrayInputStream(os.toByteArray()); + } catch (Exception e) { + logger.error("Error opening kar url", e); + throw (IOException) new IOException("Error opening kar xml url").initCause(e); + } + } + } + + public void init() { + dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + if (logger.isInfoEnabled()) + logger.debug("Initialising the Kar URL Handler; Karaf Archives will be extracted to " + localRepoPath); + } + + public void destroy() { + logger.debug("Destroying the Kar URL Handler"); + } + + public void setFeaturesService(FeaturesService featuresService) { + this.featuresService = featuresService; + } + + public void setLocalRepoPath(String localRepoPath) { + this.localRepoPath = localRepoPath; + } + +} Index: deployer/kar/src/main/resources/OSGI-INF/blueprint/kar-deployer.xml =================================================================== --- deployer/kar/src/main/resources/OSGI-INF/blueprint/kar-deployer.xml (revision 0) +++ deployer/kar/src/main/resources/OSGI-INF/blueprint/kar-deployer.xml (revision 0) @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Index: deployer/kar/pom.xml =================================================================== --- deployer/kar/pom.xml (revision 0) +++ deployer/kar/pom.xml (revision 0) @@ -0,0 +1,96 @@ + + + + + 4.0.0 + + + org.apache.karaf.deployer + deployer + 2.1.99-SNAPSHOT + + + org.apache.karaf.deployer + org.apache.karaf.deployer.kar + bundle + 2.1.99-SNAPSHOT + Apache Karaf :: Karaf Archive (.kar) Deployer + + This deployer can deploy .kar archives on the fly + + + ${basedir}/../../etc/appended-resources + + + + + org.apache.felix + org.osgi.core + provided + + + org.springframework.osgi + spring-osgi-core + + + org.apache.karaf.features + org.apache.karaf.features.core + + + commons-logging + commons-logging + + + org.apache.felix + org.apache.felix.fileinstall + + + org.apache.servicemix.bundles + org.apache.servicemix.bundles.junit + + + org.easymock + easymock + 3.0 + jar + test + + + + + + + org.apache.felix + maven-bundle-plugin + + + + ${project.artifactId};blueprint.graceperiod:=false + ${project.artifactId}*;version=${project.version} + !${project.artifactId}*,* + org.apache.karaf.deployer.features + <_versionpolicy>${bnd.version.policy} + + + + + + + Index: pom.xml =================================================================== --- pom.xml (revision 998721) +++ pom.xml (working copy) @@ -244,6 +244,11 @@ ${project.version} + org.apache.karaf.deployer + org.apache.karaf.deployer.kar + ${project.version} + + org.apache.karaf org.apache.karaf.management ${project.version} Index: features/command/src/main/resources/OSGI-INF/blueprint/features-command.xml =================================================================== --- features/command/src/main/resources/OSGI-INF/blueprint/features-command.xml (revision 998721) +++ features/command/src/main/resources/OSGI-INF/blueprint/features-command.xml (working copy) @@ -36,12 +36,12 @@ - - - - - - + + + + + + @@ -96,7 +96,7 @@ - +