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 999885) +++ 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 999885) +++ 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 999885) +++ 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 999885) +++ 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 999885) +++ 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 999885) +++ 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 999885) +++ 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/KarArtifactInstallerTest.java =================================================================== --- deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarArtifactInstallerTest.java (revision 0) +++ deployer/kar/src/test/java/org/apache/karaf/deployer/kar/KarArtifactInstallerTest.java (revision 0) @@ -0,0 +1,183 @@ +package org.apache.karaf.deployer.kar; + +import static org.easymock.EasyMock.createMock; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URI; + +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 KarArtifactInstallerTest { + private KarArtifactInstaller karArtifactInstaller; + private FeaturesService featuresService; + + private URI goodKarFile; + private URI zipFileWithKarafManifest; + private URI zipFileWithoutKarafManifest; + private URI badZipFile; + + @Before + public void setUp() throws Exception { + featuresService = createMock(FeaturesService.class); + + karArtifactInstaller = new KarArtifactInstaller(); + + karArtifactInstaller.setFeaturesService(featuresService); + karArtifactInstaller.setLocalRepoPath("./target/local-repo"); + + karArtifactInstaller.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 { + karArtifactInstaller.destroy(); + karArtifactInstaller.deleteLocalRepository(); + } + + + + @Test + public void shouldHandleKarFile() throws Exception { + assertTrue(karArtifactInstaller.canHandle(new File(goodKarFile))); + } + + @Test + public void shouldHandleZipFileWithKarafManifest() throws Exception { + assertTrue(karArtifactInstaller.canHandle(new File(zipFileWithKarafManifest))); + } + + @Test + public void shouldIgnoreZipFileWithoutKarafManifest() throws Exception { + assertFalse(karArtifactInstaller.canHandle(new File(zipFileWithoutKarafManifest))); + } + + @Test + public void shouldIgnoreBadZipFile() throws Exception { + assertFalse(karArtifactInstaller.canHandle(new File(badZipFile))); + } + + @Test + public void shouldRecognizeGoodFeaturesFile() throws Exception + { + File goodFeaturesXml = new File(getClass().getClassLoader().getResource("goodKarFile/org/foo/goodFeaturesXml.xml").getFile()); + Assert.assertTrue(karArtifactInstaller.isFeaturesRepository(goodFeaturesXml)); + } + + @Test + public void shouldRejectNonFeaturesXMLFile() throws Exception + { + File goodFeaturesXml = new File(getClass().getClassLoader().getResource("badFeaturesXml.xml").toURI()); + Assert.assertFalse(karArtifactInstaller.isFeaturesRepository(goodFeaturesXml)); + } + + + @Test + public void shouldRejectMalformedXMLFile() throws Exception + { + File malformedXml = new File(getClass().getClassLoader().getResource("malformedXml.xml").toURI()); + Assert.assertFalse(karArtifactInstaller.isFeaturesRepository(malformedXml)); + } + + @Test + public void shouldExtractAndRegisterFeaturesFromKar() throws Exception { + // Setup expectations on the features service + featuresService.addRepository(EasyMock.anyObject(URI.class)); + EasyMock.replay(featuresService); + + // Test + // + File goodKarFile = new File(getClass().getClassLoader().getResource("goodKarFile.kar").getFile()); + karArtifactInstaller.install(goodKarFile); + + // Verify expectations. + // + EasyMock.verify(featuresService); + } + + @Test + public void shouldLogAndNotThrowExceptionIfCannotAddToFeaturesRepository() throws Exception { + // Setup expectations on the features service + featuresService.addRepository(EasyMock.anyObject(URI.class)); + EasyMock.expectLastCall().andThrow(new Exception("Unable to add to repository.")); + EasyMock.replay(featuresService); + + // Test + // + File goodKarFile = new File(getClass().getClassLoader().getResource("goodKarFile.kar").getFile()); + karArtifactInstaller.install(goodKarFile); + + // Verify expectations. + // + EasyMock.verify(featuresService); + } + + @Test + public void shouldIgnoreUpdateIfFileHasNotChanged() throws Exception { + // Setup expectations on the features service: the addRepository + // should only be added once, as the update command should be ignored! + // + featuresService.addRepository(EasyMock.anyObject(URI.class)); + EasyMock.replay(featuresService); + + // Test + // + File goodKarFile = new File(getClass().getClassLoader().getResource("goodKarFile.kar").getFile()); + karArtifactInstaller.install(goodKarFile); + karArtifactInstaller.update(goodKarFile); + + // 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 + // + File karFileAsZip = new File(getClass().getClassLoader().getResource("karFileAsZip.zip").getFile()); + karArtifactInstaller.install(karFileAsZip); + + // Verify expectations. + // + EasyMock.verify(featuresService); + } + + @Test (expected = java.io.IOException.class) + public void shouldThrowExceptionIfFileDoesNotExist() throws Exception + { + File nonExistantFile = new File("DoesNotExist"); + karArtifactInstaller.install(nonExistantFile); + } + + @Test + public void uninstallShouldDoNothing() throws Exception + { + EasyMock.replay(featuresService); + + // Test + // + File karFileAsZip = new File(getClass().getClassLoader().getResource("karFileAsZip.zip").getFile()); + karArtifactInstaller.uninstall(karFileAsZip); + + // 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/malformedXml.xml =================================================================== --- deployer/kar/src/test/resources/malformedXml.xml (revision 0) +++ deployer/kar/src/test/resources/malformedXml.xml (revision 0) @@ -0,0 +1 @@ + + + + + \ 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/test/resources/log4j.properties =================================================================== --- deployer/kar/src/test/resources/log4j.properties (revision 0) +++ deployer/kar/src/test/resources/log4j.properties (revision 0) @@ -0,0 +1,8 @@ +log4j.rootLogger=DEBUG, stdout + +#The logging properties used during tests.. +# CONSOLE appender not used by default +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%-5p %-30.30c{1} - %m%n +log4j.appender.stdout.threshold=DEBUG Index: deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarArtifactInstaller.java =================================================================== --- deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarArtifactInstaller.java (revision 0) +++ deployer/kar/src/main/java/org/apache/karaf/deployer/kar/KarArtifactInstaller.java (revision 0) @@ -0,0 +1,246 @@ +package org.apache.karaf.deployer.kar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +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.felix.fileinstall.ArtifactInstaller; +import org.apache.karaf.features.FeaturesService; +import org.w3c.dom.Document; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +public class KarArtifactInstaller implements ArtifactInstaller { + + private static Log logger = LogFactory.getLog(KarArtifactInstaller.class); + + private static final String KAR_PREFIX = ".kar"; + private static final String ZIP_PREFIX = ".zip"; + + private String localRepoPath = "./local-repo"; + + private String timestampPath; + + private byte[] buffer = new byte[5 * 1024]; + + private DocumentBuilderFactory dbf; + + private FeaturesService featuresService; + + public void init() { + dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + + timestampPath = localRepoPath + File.separator + ".timestamps"; + if (new File(timestampPath).mkdirs()) { + logger.warn("Unable to create directory for Karaf Archive timestamps. Results may vary..."); + } + + if (logger.isInfoEnabled()) { + logger.info("Karaf archives will be extracted to " + localRepoPath); + logger.info("Timestamps for Karaf archives will be extracted to " + timestampPath); + + } + } + + public void destroy() { + logger.info("Karaf archive installer destroyed."); + } + + + public void install(File file) throws Exception { + // Check to see if this file has already been extracted. For example, on restart of Karaf, + // we don't necessarily want to re-extract all the Karaf Archives! + // + if (alreadyExtracted(file)) { + logger.info("Ignoring '" + file + "'; timestamp indicates it's already been deployed."); + return; + } + + if (logger.isInfoEnabled()) + logger.info("Installing " + file); + + ZipFile zipFile = new ZipFile(file); + + 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(); + + updateTimestamp(file); + } + + public void uninstall(File file) throws Exception { + logger.warn("Karaf archive '" + file + "' has been removed; however, it's feature URLs have not been deregistered, and it's bundles are still available in '" + localRepoPath + "'."); + } + + public void update(File file) throws Exception { + logger.warn("Karaf archive " + file + " has been updated; redeploying."); + install(file); + } + + protected void updateTimestamp(File karafArchive) throws Exception { + File timestamp = getArchiveTimestampFile(karafArchive); + + if (timestamp.exists()) { + if (logger.isDebugEnabled()) + logger.debug("Deleting old timestamp file '" + timestamp + ""); + + if (!timestamp.delete()) { + throw new Exception("Unable to delete archive timestamp '" + timestamp + "'"); + } + } + + logger.debug("Creating timestamp file '" + timestamp + "'"); + timestamp.createNewFile(); + } + + protected boolean alreadyExtracted(File karafArchive) { + File timestamp = getArchiveTimestampFile(karafArchive); + if (timestamp.exists()) { + return timestamp.lastModified() >= karafArchive.lastModified(); + } + return false; + } + + protected File getArchiveTimestampFile(File karafArchive) { + return new File(localRepoPath + File.separator + ".timestamps" + File.separator + karafArchive.getName()); + } + + 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 boolean canHandle(File file) { + // If the file ends with .kar, then we can handle it! + // + if (file.isFile() && file.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 (file.isFile() && file.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(file).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 '" + file.getName() + "'; ignoring.", e); + } + } + + return false; + } + + public boolean deleteLocalRepository() { + return deleteDirectory(new File(localRepoPath)); + } + + private boolean deleteDirectory(File path) { + if (path.exists()) { + File[] files = path.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteDirectory(files[i]); + } else { + files[i].delete(); + } + } + } + return (path.delete()); + } + + public void setLocalRepoPath(String localRepoPath) { + this.localRepoPath = localRepoPath; + } + + public void setFeaturesService(FeaturesService featuresService) { + this.featuresService = featuresService; + } + +} 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,36 @@ + + + + + + + + + + + + + + + + Index: deployer/kar/pom.xml =================================================================== --- deployer/kar/pom.xml (revision 0) +++ deployer/kar/pom.xml (revision 0) @@ -0,0 +1,102 @@ + + + + + 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 + + + log4j + log4j + 1.2.16 + 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 999885) +++ 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 999885) +++ features/command/src/main/resources/OSGI-INF/blueprint/features-command.xml (working copy) @@ -36,12 +36,12 @@ - - - - - - + + + + + + @@ -96,7 +96,7 @@ - +