FELIX-1107, FELIX-1109, FELIX-1110: refactor the deployer into its own module, add a blueprint deployer and extract the features deployer in its own module
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@770120 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/deployer/blueprint/pom.xml b/karaf/deployer/blueprint/pom.xml
new file mode 100644
index 0000000..2b0bcf5
--- /dev/null
+++ b/karaf/deployer/blueprint/pom.xml
@@ -0,0 +1,74 @@
+<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/maven-v4_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.servicemix.kernel.deployer</groupId>
+ <artifactId>deployer</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.blueprint</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache ServiceMix Kernel :: Blueprint Deployer</name>
+
+ <description>This deployer transforms a plain blueprint xml file to a deployable bundle</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.osgi</groupId>
+ <artifactId>spring-osgi-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.filemonitor</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.junit</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
+ <Private-Package>org.apache.servicemix.kernel.deployer.blueprint</Private-Package>
+ <Spring-Context>*;publish-context:=false;create-asynchronously:=false</Spring-Context>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintDeploymentListener.java b/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintDeploymentListener.java
new file mode 100644
index 0000000..13ef072
--- /dev/null
+++ b/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintDeploymentListener.java
@@ -0,0 +1,81 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.blueprint;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener;
+
+/**
+ * A deployment listener that listens for spring xml applications
+ * and creates bundles for these.
+ */
+public class BlueprintDeploymentListener implements DeploymentListener {
+
+
+ private static final Log LOGGER = LogFactory.getLog(BlueprintDeploymentListener.class);
+
+ private DocumentBuilderFactory dbf;
+
+ public boolean canHandle(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 ("blueprint".equals(name) && "http://www.osgi.org/xmlns/blueprint/v1.0.0".equals(uri)) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("Unable to parse deployed file " + artifact.getAbsolutePath(), e);
+ }
+ return false;
+ }
+
+ public File handle(File artifact, File tmpDir) {
+ try {
+ File destFile = new File(tmpDir, artifact.getName() + ".jar");
+ FileOutputStream os = new FileOutputStream(destFile);
+ BlueprintTransformer.transform(artifact.toURL(), os);
+ os.close();
+ return destFile;
+ } catch (Exception e) {
+ LOGGER.error("Unable to build blueprint application bundle", e);
+ return null;
+ }
+ }
+
+ protected Document parse(File artifact) throws Exception {
+ if (dbf == null) {
+ dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ }
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ return db.parse(artifact);
+ }
+
+}
diff --git a/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintTransformer.java b/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintTransformer.java
new file mode 100644
index 0000000..f466527
--- /dev/null
+++ b/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintTransformer.java
@@ -0,0 +1,248 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.blueprint;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import org.osgi.framework.Constants;
+
+public class BlueprintTransformer {
+
+ static Transformer transformer;
+ static DocumentBuilderFactory dbf;
+ static TransformerFactory tf;
+
+
+ public static void transform(URL url, OutputStream os) throws Exception {
+ // Build dom document
+ Document doc = parse(url);
+ // Heuristicly retrieve name and version
+ String name = url.getPath();
+ int idx = name.lastIndexOf('/');
+ if (idx >= 0) {
+ name = name.substring(idx + 1);
+ }
+ String[] str = extractNameVersionType(name);
+ // Create manifest
+ Manifest m = new Manifest();
+ m.getMainAttributes().putValue("Manifest-Version", "2");
+ m.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
+ m.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, str[0]);
+ m.getMainAttributes().putValue(Constants.BUNDLE_VERSION, str[1]);
+ String importPkgs = getImportPackages(analyze(new DOMSource(doc)));
+ if (importPkgs != null && importPkgs.length() > 0) {
+ m.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, importPkgs);
+ }
+ m.getMainAttributes().putValue(Constants.DYNAMICIMPORT_PACKAGE, "*");
+ // Extract manifest entries from the DOM
+ NodeList l = doc.getElementsByTagName("manifest");
+ if (l != null) {
+ for (int i = 0; i < l.getLength(); i++) {
+ Element e = (Element) l.item(i);
+ String text = e.getTextContent();
+ Properties props = new Properties();
+ props.load(new ByteArrayInputStream(text.trim().getBytes()));
+ Enumeration en = props.propertyNames();
+ while (en.hasMoreElements()) {
+ String k = (String) en.nextElement();
+ String v = props.getProperty(k);
+ m.getMainAttributes().putValue(k, v);
+ }
+ e.getParentNode().removeChild(e);
+ }
+ }
+
+ JarOutputStream out = new JarOutputStream(os);
+ ZipEntry e = new ZipEntry(JarFile.MANIFEST_NAME);
+ out.putNextEntry(e);
+ m.write(out);
+ out.closeEntry();
+ e = new ZipEntry("OSGI-INF/");
+ out.putNextEntry(e);
+ e = new ZipEntry("OSGI-INF/blueprint/");
+ out.putNextEntry(e);
+ out.closeEntry();
+ e = new ZipEntry("OSGI-INF/blueprint/" + name);
+ out.putNextEntry(e);
+ // Copy the new DOM
+ if (tf == null) {
+ tf = TransformerFactory.newInstance();
+ }
+ tf.newTransformer().transform(new DOMSource(doc), new StreamResult(out));
+ out.closeEntry();
+ out.close();
+ }
+
+ private static final String DEFAULT_VERSION = "0.0.0";
+
+ private static final Pattern ARTIFACT_MATCHER = Pattern.compile("(.+)(?:-(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?(?:[^a-zA-Z0-9](.*))?)(?:\\.([^\\.]+))", Pattern.DOTALL);
+ private static final Pattern FUZZY_MODIFIDER = Pattern.compile("(?:\\d+[.-])*(.*)", Pattern.DOTALL);
+
+ public static String[] extractNameVersionType(String url) {
+ Matcher m = ARTIFACT_MATCHER.matcher(url);
+ if (!m.matches()) {
+ return new String[] { url, DEFAULT_VERSION };
+ }
+ else {
+ //System.err.println(m.groupCount());
+ //for (int i = 1; i <= m.groupCount(); i++) {
+ // System.err.println("Group " + i + ": " + m.group(i));
+ //}
+
+ StringBuffer v = new StringBuffer();
+ String d1 = m.group(1);
+ String d2 = m.group(2);
+ String d3 = m.group(3);
+ String d4 = m.group(4);
+ String d5 = m.group(5);
+ String d6 = m.group(6);
+ if (d2 != null) {
+ v.append(d2);
+ if (d3 != null) {
+ v.append('.');
+ v.append(d3);
+ if (d4 != null) {
+ v.append('.');
+ v.append(d4);
+ if (d5 != null) {
+ v.append(".");
+ cleanupModifier(v, d5);
+ }
+ } else if (d5 != null) {
+ v.append(".0.");
+ cleanupModifier(v, d5);
+ }
+ } else if (d5 != null) {
+ v.append(".0.0.");
+ cleanupModifier(v, d5);
+ }
+ }
+ return new String[] { d1, v.toString(), d6 };
+ }
+ }
+
+ private static void cleanupModifier(StringBuffer result, String modifier) {
+ Matcher m = FUZZY_MODIFIDER.matcher(modifier);
+ if (m.matches()) {
+ modifier = m.group(1);
+ }
+ for (int i = 0; i < modifier.length(); i++) {
+ char c = modifier.charAt(i);
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-') {
+ result.append(c);
+ }
+ }
+ }
+
+ public static Set<String> analyze(Source source) throws Exception {
+ if (transformer == null) {
+ if (tf == null) {
+ tf = TransformerFactory.newInstance();
+ }
+ Source s = new StreamSource(BlueprintTransformer.class.getResourceAsStream("extract.xsl"));
+ transformer = tf.newTransformer(s);
+ }
+
+ Set<String> refers = new TreeSet<String>();
+
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ Result r = new StreamResult(bout);
+ transformer.transform(source, r);
+
+ ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
+ bout.close();
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(bin));
+
+ String line = br.readLine();
+ while (line != null) {
+ line = line.trim();
+ if (line.length() > 0) {
+ String parts[] = line.split("\\s*,\\s*");
+ for (int i = 0; i < parts.length; i++) {
+ int n = parts[i].lastIndexOf('.');
+ if (n > 0) {
+ refers.add(parts[i].substring(0, n));
+ }
+ }
+ }
+ line = br.readLine();
+ }
+ br.close();
+ return refers;
+ }
+
+ protected static String getImportPackages(Set<String> packages) {
+ StringBuilder sb = new StringBuilder();
+ for (String pkg : packages) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(pkg);
+ }
+ return sb.toString();
+ }
+
+ protected static Document parse(URL url) throws Exception {
+ if (dbf == null) {
+ dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ }
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ return db.parse(url.toString());
+ }
+
+ protected static void copyInputStream(InputStream in, OutputStream out) throws Exception {
+ byte[] buffer = new byte[4096];
+ int len = in.read(buffer);
+ while (len >= 0) {
+ out.write(buffer, 0, len);
+ len = in.read(buffer);
+ }
+ }
+}
diff --git a/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintURLHandler.java b/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintURLHandler.java
new file mode 100644
index 0000000..a03ecae
--- /dev/null
+++ b/karaf/deployer/blueprint/src/main/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintURLHandler.java
@@ -0,0 +1,97 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.blueprint;
+
+import java.io.File;
+import java.io.FileInputStream;
+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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.service.url.AbstractURLStreamHandlerService;
+
+/**
+ * A URL handler that will transform a JBI artifact to an OSGi bundle
+ * on the fly. Needs to be registered in the OSGi registry.
+ */
+public class BlueprintURLHandler extends AbstractURLStreamHandlerService {
+
+ private static Log logger = LogFactory.getLog(BlueprintURLHandler.class);
+
+ private static String SYNTAX = "blueprint: bp-xml-uri";
+
+ private URL blueprintXmlURL;
+
+ /**
+ * Open the connection for the given URL.
+ *
+ * @param url the url from which to open a connection.
+ * @return a connection on the specified URL.
+ * @throws IOException if an error occurs or if the URL is malformed.
+ */
+ @Override
+ public 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 );
+ }
+ blueprintXmlURL = new URL(url.getPath());
+
+ logger.debug("Spring xml URL is: [" + blueprintXmlURL + "]");
+ return new Connection(url);
+ }
+
+ public URL getBlueprintXmlURL() {
+ return blueprintXmlURL;
+ }
+
+ public class Connection extends URLConnection {
+
+ public Connection(URL url) {
+ super(url);
+ }
+
+ @Override
+ public void connect() throws IOException {
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ try {
+ final File f = File.createTempFile("smx", "xml");
+ FileOutputStream os = new FileOutputStream(f);
+ BlueprintTransformer.transform(blueprintXmlURL, os);
+ os.close();
+ return new FileInputStream(f) {
+ public void close() throws IOException {
+ super.close();
+ f.delete();
+ }
+ };
+ } catch (Exception e) {
+ logger.error("Error opening spring xml url", e);
+ throw (IOException) new IOException("Error opening spring xml url").initCause(e);
+ }
+ }
+ }
+
+}
diff --git a/karaf/deployer/blueprint/src/main/resources/META-INF/spring/blueprint-deployer.xml b/karaf/deployer/blueprint/src/main/resources/META-INF/spring/blueprint-deployer.xml
new file mode 100644
index 0000000..bb3e430
--- /dev/null
+++ b/karaf/deployer/blueprint/src/main/resources/META-INF/spring/blueprint-deployer.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:osgi="http://www.springframework.org/schema/osgi"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/util
+ http://www.springframework.org/schema/util/spring-util.xsd
+ http://www.springframework.org/schema/osgi
+ http://www.springframework.org/schema/osgi/spring-osgi.xsd">
+
+ <bean id="blueprintDeploymentListener" class="org.apache.servicemix.kernel.deployer.blueprint.BlueprintDeploymentListener">
+
+ </bean>
+
+ <osgi:service ref="blueprintDeploymentListener">
+ <osgi:interfaces>
+ <value>org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener</value>
+ </osgi:interfaces>
+ </osgi:service>
+
+ <bean id="blueprintHandler" class="org.apache.servicemix.kernel.deployer.blueprint.BlueprintURLHandler" />
+
+ <osgi:service ref="blueprintHandler" interface="org.osgi.service.url.URLStreamHandlerService">
+ <osgi:service-properties>
+ <entry key="url.handler.protocol" value="blueprint"/>
+ </osgi:service-properties>
+ </osgi:service>
+
+</beans>
\ No newline at end of file
diff --git a/karaf/deployer/blueprint/src/main/resources/org/apache/servicemix/kernel/deployer/blueprint/extract.xsl b/karaf/deployer/blueprint/src/main/resources/org/apache/servicemix/kernel/deployer/blueprint/extract.xsl
new file mode 100644
index 0000000..2c8c291
--- /dev/null
+++ b/karaf/deployer/blueprint/src/main/resources/org/apache/servicemix/kernel/deployer/blueprint/extract.xsl
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:bp="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+ <xsl:output method="text" />
+
+ <xsl:template match="/">
+
+ <!-- Match all attributes that holds a class or a comma delimited
+ list of classes and print them -->
+
+ <xsl:for-each select="
+ //bp:*/@class
+ | //bp:*/@type
+ | //bp:*/@value-type
+ | //bp:*/@key-type
+ | //bp:*/bp:interfaces/bp:value/text()
+ ">
+ <xsl:value-of select="." />
+ <xsl:text>
+ </xsl:text>
+ </xsl:for-each>
+
+ <!-- This seems some magic to get extra imports? -->
+
+ </xsl:template>
+
+
+</xsl:stylesheet>
+
diff --git a/karaf/deployer/blueprint/src/test/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintDeploymentListenerTest.java b/karaf/deployer/blueprint/src/test/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintDeploymentListenerTest.java
new file mode 100644
index 0000000..6ab2c33
--- /dev/null
+++ b/karaf/deployer/blueprint/src/test/java/org/apache/servicemix/kernel/deployer/blueprint/BlueprintDeploymentListenerTest.java
@@ -0,0 +1,84 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.blueprint;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.jar.JarInputStream;
+
+import javax.xml.transform.dom.DOMSource;
+
+import junit.framework.TestCase;
+
+public class BlueprintDeploymentListenerTest extends TestCase {
+
+ public void testPackagesExtraction() throws Exception {
+ BlueprintDeploymentListener l = new BlueprintDeploymentListener();
+ File f = new File(getClass().getClassLoader().getResource("test.xml").toURI());
+ Set<String> pkgs = BlueprintTransformer.analyze(new DOMSource(BlueprintTransformer.parse(f.toURL())));
+ assertNotNull(pkgs);
+ assertEquals(2, pkgs.size());
+ Iterator<String> it = pkgs.iterator();
+ assertEquals("java.lang", it.next());
+ assertEquals("org.apache.geronimo.blueprint.sample", it.next());
+ }
+
+ public void testCustomManifest() throws Exception {
+ File f = File.createTempFile("smx", ".jar");
+ try {
+ OutputStream os = new FileOutputStream(f);
+ BlueprintTransformer.transform(getClass().getClassLoader().getResource("test.xml"), os);
+ os.close();
+ InputStream is = new FileInputStream(f);
+ JarInputStream jar = new JarInputStream(is);
+ jar.getManifest().write(System.err);
+ is.close();
+ } finally {
+ f.delete();
+ }
+ }
+
+ public void testVersions() {
+ assertVersion("org.apache.servicemix.bundles.ant-1.7.0-1.0-m3-SNAPSHOT.jar",
+ "org.apache.servicemix.bundles.ant-1.7.0", "1.0.0.m3-SNAPSHOT", "jar");
+ assertVersion("org.apache.activemq.core-1.0-SNAPSHOT.xml",
+ "org.apache.activemq.core", "1.0.0.SNAPSHOT", "xml");
+ assertVersion("org.apache.activemq.core-1.0.0-SNAPSHOT.xml",
+ "org.apache.activemq.core", "1.0.0.SNAPSHOT", "xml");
+ assertVersion("org.apache.activemq.core-1.0.0.xml",
+ "org.apache.activemq.core", "1.0.0", "xml");
+ assertVersion("geronimo-servlet_2.5_spec-1.1.2.jar",
+ "geronimo-servlet_2.5_spec", "1.1.2", "jar");
+ assertVersion("spring-aop-2.5.1.jar",
+ "spring-aop", "2.5.1", "jar");
+ }
+
+ private void assertVersion(String s, String... expectedParts) {
+ String[] parts = BlueprintTransformer.extractNameVersionType(s);
+ assertEquals(expectedParts.length, parts.length);
+ for (int i = 0; i < expectedParts.length; i++) {
+ assertEquals(expectedParts[i], parts[i]);
+ }
+ }
+
+}
diff --git a/karaf/deployer/blueprint/src/test/resources/test.xml b/karaf/deployer/blueprint/src/test/resources/test.xml
new file mode 100644
index 0000000..3411d48
--- /dev/null
+++ b/karaf/deployer/blueprint/src/test/resources/test.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+ xmlns:cm="http://www.osgi.org/xmlns/blueprint-cm/v1.0.0"
+ default-availability="optional">
+
+ <type-converters>
+ <bean id="converter1" class="org.apache.geronimo.blueprint.sample.DateTypeConverter">
+ <property name="format" value="yyyy.MM.dd"/>
+ </bean>
+ <bean id="converter2" class="org.apache.geronimo.blueprint.sample.CurrencyTypeConverter"/>
+
+ <cm:property-placeholder id="property-placeholder" persistent-id="blueprint-sample">
+ <cm:default-properties>
+ <cm:property name="key.b" value="-1"/>
+ </cm:default-properties>
+ </cm:property-placeholder>
+ </type-converters>
+
+ <bean id="foo" class="org.apache.geronimo.blueprint.sample.Foo" init-method="init" destroy-method="destroy">
+ <property name="a" value="5" />
+ <property name="b" value="${key.b}" />
+ <property name="bar" ref="bar" />
+ <property name="currency">
+ <value>PLN</value>
+ </property>
+ <property name="date">
+ <value>2009.04.17</value>
+ </property>
+ </bean>
+
+ <bean id="bar" class="org.apache.geronimo.blueprint.sample.Bar">
+ <property name="value"><value>Hello FooBar</value></property>
+ <property name="context" ref="bundleContext"/>
+ <property name="list">
+ <list>
+ <value>a list element</value>
+ <value type = "java.lang.Integer">5</value>
+ </list>
+ </property>
+ </bean>
+
+ <service ref="foo" auto-export="all-classes">
+ <service-properties>
+ <entry key="key" value="value"/>
+ </service-properties>
+ <registration-listener ref="fooRegistrationListener"
+ registration-method="serviceRegistered"
+ unregistration-method="serviceUnregistered"/>
+ </service>
+
+ <bean id="fooRegistrationListener" class="org.apache.geronimo.blueprint.sample.FooRegistrationListener"/>
+
+ <reference id="ref" interface="org.apache.geronimo.blueprint.sample.Foo">
+ </reference>
+
+ <reference id="ref2" interface="org.apache.geronimo.blueprint.sample.InterfaceA" timeout="100">
+ <listener bind-method="bind" unbind-method="unbind" ref="bindingListener" />
+ </reference>
+
+ <bean id="bindingListener" class="org.apache.geronimo.blueprint.sample.BindingListener"/>
+
+ <ref-list id="ref-list" interface="org.apache.geronimo.blueprint.sample.InterfaceA">
+ <listener bind-method="bind" unbind-method="unbind" ref="listBindingListener" />
+ </ref-list>
+
+ <bean id="listBindingListener" class="org.apache.geronimo.blueprint.sample.BindingListener"/>
+
+</blueprint>
+
diff --git a/karaf/deployer/features/pom.xml b/karaf/deployer/features/pom.xml
new file mode 100644
index 0000000..738126e
--- /dev/null
+++ b/karaf/deployer/features/pom.xml
@@ -0,0 +1,78 @@
+<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/maven-v4_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.servicemix.kernel.deployer</groupId>
+ <artifactId>deployer</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.features</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache ServiceMix Kernel :: Features Deployer</name>
+
+ <description>This deployer can deploy features descriptor on the fly</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.osgi</groupId>
+ <artifactId>spring-osgi-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.kernel.gshell</groupId>
+ <artifactId>org.apache.servicemix.kernel.gshell.features</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.filemonitor</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.junit</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
+ <Private-Package>org.apache.servicemix.kernel.deployer.blueprint</Private-Package>
+ <Spring-Context>*;publish-context:=false;create-asynchronously:=false</Spring-Context>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/karaf/deployer/features/src/main/java/org/apache/servicemix/kernel/deployer/features/FeatureDeploymentListener.java b/karaf/deployer/features/src/main/java/org/apache/servicemix/kernel/deployer/features/FeatureDeploymentListener.java
new file mode 100644
index 0000000..b26d548
--- /dev/null
+++ b/karaf/deployer/features/src/main/java/org/apache/servicemix/kernel/deployer/features/FeatureDeploymentListener.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.features;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.servicemix.kernel.gshell.features.Feature;
+import org.apache.servicemix.kernel.gshell.features.FeaturesService;
+import org.apache.servicemix.kernel.gshell.features.Repository;
+import org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.Constants;
+import org.osgi.framework.SynchronousBundleListener;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.osgi.context.BundleContextAware;
+
+/**
+ * A deployment listener able to hot deploy a feature descriptor
+ */
+public class FeatureDeploymentListener implements DeploymentListener, SynchronousBundleListener, BundleContextAware,
+ InitializingBean, DisposableBean {
+
+ public static final String FEATURE_PATH = "org.apache.servicemix.kernel.gshell.features";
+
+ private static final Log LOGGER = LogFactory.getLog(FeatureDeploymentListener.class);
+
+ private DocumentBuilderFactory dbf;
+ private FeaturesService featuresService;
+ private BundleContext bundleContext;
+
+ public void setFeaturesService(FeaturesService featuresService) {
+ this.featuresService = featuresService;
+ }
+
+ public FeaturesService getFeaturesService() {
+ return featuresService;
+ }
+
+ public BundleContext getBundleContext() {
+ return bundleContext;
+ }
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public void afterPropertiesSet() throws Exception {
+ bundleContext.addBundleListener(this);
+ for (Bundle bundle : bundleContext.getBundles()) {
+ bundleChanged(new BundleEvent(BundleEvent.INSTALLED, bundle));
+ }
+ }
+
+ public void destroy() throws Exception {
+ bundleContext.removeBundleListener(this);
+ }
+
+ public boolean canHandle(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) {
+ LOGGER.error("Unable to parse deployed file " + artifact.getAbsolutePath(), e);
+ }
+ return false;
+ }
+
+ public File handle(File artifact, File tmpDir) {
+ // We can't really install the feature right now and just return nothing.
+ // We would not be aware of the fact that the bundle has been uninstalled
+ // and therefore require the feature to be uninstalled.
+ // So instead, create a fake bundle with the file inside, which will be listened by
+ // this deployer: installation / uninstallation of the feature will be done
+ // while the bundle is installed / uninstalled.
+ try {
+ File destFile = new File(tmpDir, artifact.getName() + ".jar");
+ OutputStream os = new BufferedOutputStream(new FileOutputStream(destFile));
+
+ String name = artifact.getCanonicalPath();
+ int idx = name.lastIndexOf('/');
+ if (idx >= 0) {
+ name = name.substring(idx + 1);
+ }
+ String[] str = extractNameVersionType(name);
+ // Create manifest
+ Manifest m = new Manifest();
+ m.getMainAttributes().putValue("Manifest-Version", "2");
+ m.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
+ m.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, str[0]);
+ m.getMainAttributes().putValue(Constants.BUNDLE_VERSION, str[1]);
+ // Put content
+ JarOutputStream out = new JarOutputStream(os);
+ ZipEntry e = new ZipEntry(JarFile.MANIFEST_NAME);
+ out.putNextEntry(e);
+ m.write(out);
+ out.closeEntry();
+ e = new ZipEntry("META-INF/");
+ out.putNextEntry(e);
+ e = new ZipEntry("META-INF/" + FEATURE_PATH + "/");
+ out.putNextEntry(e);
+ out.closeEntry();
+ e = new ZipEntry("META-INF/" + FEATURE_PATH + "/" + name);
+ out.putNextEntry(e);
+ InputStream fis = new BufferedInputStream(new FileInputStream(artifact));
+ copyInputStream(fis, out);
+ fis.close();
+ out.closeEntry();
+ out.close();
+ os.close();
+ return destFile;
+ } catch (Exception e) {
+ LOGGER.error("Unable to build spring application bundle", e);
+ return null;
+ }
+ }
+
+ public void bundleChanged(BundleEvent bundleEvent) {
+ try {
+ Bundle bundle = bundleEvent.getBundle();
+ if (bundleEvent.getType() == BundleEvent.INSTALLED) {
+ Enumeration featuresUrlEnumeration = bundle.findEntries("/META-INF/" + FEATURE_PATH + "/", "*.xml", false);
+ while (featuresUrlEnumeration != null && featuresUrlEnumeration.hasMoreElements()) {
+ URL url = (URL) featuresUrlEnumeration.nextElement();
+ featuresService.addRepository(url.toURI());
+ for (Repository repo : featuresService.listRepositories()) {
+ if (repo.getURI().equals(url.toURI())) {
+ for (Feature f : repo.getFeatures()) {
+ featuresService.installFeature(f.getName(), f.getVersion());
+ }
+ }
+ }
+ featuresService.removeRepository(url.toURI());
+ }
+ } else if (bundleEvent.getType() == BundleEvent.UNINSTALLED) {
+ Enumeration featuresUrlEnumeration = bundle.findEntries("/META-INF/" + FEATURE_PATH + "/", "*.xml", false);
+ while (featuresUrlEnumeration != null && featuresUrlEnumeration.hasMoreElements()) {
+ URL url = (URL) featuresUrlEnumeration.nextElement();
+ featuresService.addRepository(url.toURI());
+ for (Repository repo : featuresService.listRepositories()) {
+ if (repo.getURI().equals(url.toURI())) {
+ for (Feature f : repo.getFeatures()) {
+ featuresService.uninstallFeature(f.getName(), f.getVersion());
+ }
+ }
+ }
+ featuresService.removeRepository(url.toURI());
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("Unable to install / uninstall feature", e);
+ }
+ }
+
+ protected Document parse(File artifact) throws Exception {
+ if (dbf == null) {
+ dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ }
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ return db.parse(artifact);
+ }
+
+ private static void copyInputStream(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[8192];
+ int len = in.read(buffer);
+ while (len >= 0) {
+ out.write(buffer, 0, len);
+ len = in.read(buffer);
+ }
+ }
+ private static final String DEFAULT_VERSION = "0.0.0";
+
+ private static final Pattern ARTIFACT_MATCHER = Pattern.compile("(.+)(?:-(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?(?:[^a-zA-Z0-9](.*))?)(?:\\.([^\\.]+))", Pattern.DOTALL);
+ private static final Pattern FUZZY_MODIFIDER = Pattern.compile("(?:\\d+[.-])*(.*)", Pattern.DOTALL);
+
+ public static String[] extractNameVersionType(String url) {
+ Matcher m = ARTIFACT_MATCHER.matcher(url);
+ if (!m.matches()) {
+ return new String[] { url, DEFAULT_VERSION };
+ }
+ else {
+ //System.err.println(m.groupCount());
+ //for (int i = 1; i <= m.groupCount(); i++) {
+ // System.err.println("Group " + i + ": " + m.group(i));
+ //}
+
+ StringBuffer v = new StringBuffer();
+ String d1 = m.group(1);
+ String d2 = m.group(2);
+ String d3 = m.group(3);
+ String d4 = m.group(4);
+ String d5 = m.group(5);
+ String d6 = m.group(6);
+ if (d2 != null) {
+ v.append(d2);
+ if (d3 != null) {
+ v.append('.');
+ v.append(d3);
+ if (d4 != null) {
+ v.append('.');
+ v.append(d4);
+ if (d5 != null) {
+ v.append(".");
+ cleanupModifier(v, d5);
+ }
+ } else if (d5 != null) {
+ v.append(".0.");
+ cleanupModifier(v, d5);
+ }
+ } else if (d5 != null) {
+ v.append(".0.0.");
+ cleanupModifier(v, d5);
+ }
+ }
+ return new String[] { d1, v.toString(), d6 };
+ }
+ }
+
+ private static void cleanupModifier(StringBuffer result, String modifier) {
+ Matcher m = FUZZY_MODIFIDER.matcher(modifier);
+ if (m.matches()) {
+ modifier = m.group(1);
+ }
+ for (int i = 0; i < modifier.length(); i++) {
+ char c = modifier.charAt(i);
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-') {
+ result.append(c);
+ }
+ }
+ }
+
+}
diff --git a/karaf/deployer/features/src/main/resources/META-INF/spring/features-deployer.xml b/karaf/deployer/features/src/main/resources/META-INF/spring/features-deployer.xml
new file mode 100644
index 0000000..54b1913
--- /dev/null
+++ b/karaf/deployer/features/src/main/resources/META-INF/spring/features-deployer.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:ctx="http://www.springframework.org/schema/context"
+ xmlns:osgi="http://www.springframework.org/schema/osgi"
+ xmlns:osgix="http://www.springframework.org/schema/osgi-compendium"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:gshell="http://servicemix.apache.org/schema/servicemix-gshell"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context
+ http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/osgi
+ http://www.springframework.org/schema/osgi/spring-osgi.xsd
+ http://www.springframework.org/schema/osgi-compendium
+ http://www.springframework.org/schema/osgi-compendium/spring-osgi-compendium.xsd
+ http://www.springframework.org/schema/util
+ http://www.springframework.org/schema/util/spring-util.xsd
+ http://servicemix.apache.org/schema/servicemix-gshell
+ http://servicemix.apache.org/schema/servicemix-gshell/servicemix-gshell.xsd">
+
+ <osgi:reference id="featuresService" interface="org.apache.servicemix.kernel.gshell.features.FeaturesService" cardinality="1..1"/>
+
+ <bean id="featureDeploymentListener" class="org.apache.servicemix.kernel.deployer.features.FeatureDeploymentListener">
+ <property name="featuresService" ref="featuresService" />
+ </bean>
+
+ <osgi:service ref="featureDeploymentListener" interface="org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener" />
+
+</beans>
\ No newline at end of file
diff --git a/karaf/deployer/filemonitor/pom.xml b/karaf/deployer/filemonitor/pom.xml
new file mode 100644
index 0000000..7817bc2
--- /dev/null
+++ b/karaf/deployer/filemonitor/pom.xml
@@ -0,0 +1,86 @@
+<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/maven-v4_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.servicemix.kernel.deployer</groupId>
+ <artifactId>deployer</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.filemonitor</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache ServiceMix Kernel :: File Monitor</name>
+
+ <description>This bundle monitors the deploy directory for new OSGi bundles in jar form, expanded form or for
+ configuration file changes and undeploys/redeploys them
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-logging</groupId>
+ <artifactId>commons-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ </dependency>
+
+ <!-- testing -->
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.commons-io</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
+ <Export-Package>${pom.artifactId}</Export-Package>
+ <Bundle-Activator>org.apache.servicemix.kernel.deployer.filemonitor.FileMonitorActivator
+ </Bundle-Activator>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/DeploymentListener.java b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/DeploymentListener.java
new file mode 100644
index 0000000..18486c9
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/DeploymentListener.java
@@ -0,0 +1,36 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import java.io.File;
+
+
+public interface DeploymentListener {
+
+ /**
+ * Returns true if the listener can process the given file
+ */
+ boolean canHandle(File artifact);
+
+ /**
+ * Process the given file (canHandle returned true previously)
+ * Can return <null> or a pointer to a transformed file.
+ */
+ File handle(File artifact, File tmpDir);
+
+}
diff --git a/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitor.java b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitor.java
new file mode 100644
index 0000000..2a08990
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitor.java
@@ -0,0 +1,767 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.osgi.service.prefs.Preferences;
+import org.osgi.service.prefs.PreferencesService;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Watches a deploy directory for files that are added, updated or removed then
+ * processing them. Currently we support OSGi bundles, OSGi configuration files
+ * and expanded directories of OSGi bundles.
+ *
+ * @version $Revision: 1.1 $
+ */
+public class FileMonitor {
+
+ public final static String CONFIG_DIR = "org.apache.servicemix.filemonitor.configDir";
+ public final static String DEPLOY_DIR = "org.apache.servicemix.filemonitor.monitorDir";
+ public final static String GENERATED_JAR_DIR = "org.apache.servicemix.filemonitor.generatedJarDir";
+ public final static String SCAN_INTERVAL = "org.apache.servicemix.filemonitor.scanInterval";
+ public final static String PREFERENCE_KEY = "FileMonitor";
+
+ protected static final String ALIAS_KEY = "_alias_factory_pid";
+
+ private static final Log LOGGER = LogFactory.getLog(FileMonitor.class);
+
+ private FileMonitorActivator activator;
+ private File configDir;
+ private File deployDir;
+ private File generateDir;
+ private Scanner scanner = new Scanner();
+ private long scanInterval = 500L;
+ private boolean loggedConfigAdminWarning;
+ private List<Bundle> bundlesToStart = new ArrayList<Bundle>();
+ private List<Bundle> bundlesToUpdate = new ArrayList<Bundle>();
+ private Map<String, String> artifactToBundle = new HashMap<String, String>();
+ private final Set<String> pendingTransformationArtifacts = new HashSet<String>();
+ private final Set<Bundle> pendingStartBundles = new HashSet<Bundle>();
+ private ServiceListener deployerListener;
+ private BundleListener bundleListener;
+ private Executor executor;
+ private String pid;
+
+ public FileMonitor() {
+ String base = System.getProperty("servicemix.base", ".");
+ configDir = new File(base, "etc");
+ deployDir = new File(base, "deploy");
+ generateDir = new File(base, "data/generated-bundles");
+ }
+
+ @SuppressWarnings("unchecked")
+ public FileMonitor(FileMonitorActivator activator, Dictionary properties, String pid) {
+ this();
+
+ this.activator = activator;
+ this.pid = pid;
+
+ File value = getFileValue(properties, CONFIG_DIR);
+ if (value != null) {
+ configDir = value;
+ }
+ value = getFileValue(properties, DEPLOY_DIR);
+ if (value != null) {
+ deployDir = value;
+ }
+ value = getFileValue(properties, GENERATED_JAR_DIR);
+ if (value != null) {
+ generateDir = value;
+ }
+ Long i = getLongValue(properties, SCAN_INTERVAL);
+ if (i != null) {
+ scanInterval = i;
+ }
+ }
+
+ public void start() {
+ executor = Executors.newSingleThreadExecutor();
+ if (configDir != null) {
+ configDir.mkdirs();
+ }
+ deployDir.mkdirs();
+ generateDir.mkdirs();
+
+ List<File> dirs = new ArrayList<File>();
+ if (configDir != null) {
+ dirs.add(configDir);
+ }
+ dirs.add(deployDir);
+ scanner.setScanDirs(dirs);
+ scanner.setScanInterval(scanInterval);
+ // load the scan results for this monitor
+ loadScannerState();
+ scanner.addListener(new Scanner.BulkListener() {
+ public void filesChanged(List<String> filenames) throws Exception {
+ onFilesChanged(filenames);
+ }
+ });
+
+ LOGGER.info("Starting to monitor the deploy directory: " + deployDir + " every " + scanInterval
+ + " millis");
+ if (configDir != null) {
+ LOGGER.info("Config directory is at: " + configDir);
+ }
+ LOGGER.info("Will generate bundles from expanded source directories to: " + generateDir);
+
+ executor.execute(new Runnable() {
+ public void run() {
+ scanner.start();
+ }
+ });
+ }
+
+ public void stop() {
+ // first stop the scanner
+ scanner.stop();
+ // load the scan results for this monitor
+ saveScannerState();
+ }
+
+ // Properties
+ // -------------------------------------------------------------------------
+
+ public BundleContext getContext() {
+ return activator.getContext();
+ }
+
+ public FileMonitorActivator getActivator() {
+ return activator;
+ }
+
+ public void setActivator(FileMonitorActivator activator) {
+ this.activator = activator;
+ }
+
+ public File getConfigDir() {
+ return configDir;
+ }
+
+ public void setConfigDir(File configDir) {
+ this.configDir = configDir;
+ }
+
+ public File getDeployDir() {
+ return deployDir;
+ }
+
+ public void setDeployDir(File deployDir) {
+ this.deployDir = deployDir;
+ }
+
+ public File getGenerateDir() {
+ return generateDir;
+ }
+
+ public void setGenerateDir(File generateDir) {
+ this.generateDir = generateDir;
+ }
+
+ public long getScanInterval() {
+ return scanInterval;
+ }
+
+ public void setScanInterval(long scanInterval) {
+ this.scanInterval = scanInterval;
+ }
+
+ // Implementation methods
+ // -------------------------------------------------------------------------
+
+ protected synchronized void onFilesChanged(Collection<String> filenames) {
+ bundlesToStart.clear();
+ bundlesToUpdate.clear();
+ Set<File> bundleJarsCreated = new HashSet<File>();
+
+ for (String filename : filenames) {
+ File file = new File(filename);
+ try {
+ LOGGER.debug("File changed: " + filename);
+
+ // Handle config files
+ if (isValidConfigFile(file)) {
+ if (file.exists()) {
+ updateConfiguration(file);
+ } else {
+ deleteConfiguration(file);
+ }
+ continue;
+ }
+
+ // Handle exploded artifacts removal
+ if (!file.exists() && file.getName().equals("MANIFEST.MF")) {
+ File parentFile = file.getParentFile();
+ if (parentFile.getName().equals("META-INF")) {
+ File bundleDir = parentFile.getParentFile();
+ if (isValidBundleSourceDirectory(bundleDir)) {
+ undeployBundle(bundleDir);
+ continue;
+ }
+ }
+ }
+
+ // Handle exploded artifacts add
+ File jardir = getExpandedBundleRootDirectory(file);
+ if (jardir != null) {
+ if (bundleJarsCreated.contains(jardir)) {
+ continue;
+ }
+ bundleJarsCreated.add(jardir);
+ file = createBundleJar(jardir);
+ }
+
+ // Transformation step
+ if (file.exists()) {
+ File f = transformArtifact(file);
+ if (f == null) {
+ LOGGER.warn("Unsupported deployment: " + filename);
+ rescheduleTransformation(file);
+ continue;
+ }
+ file = f;
+ } else {
+ String transformedFile = artifactToBundle.get(filename);
+ if (transformedFile != null) {
+ file = new File(transformedFile);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+ }
+
+ // Handle final bundles
+ if (isValidArtifactFile(file)) {
+ if (file.exists()) {
+ deployBundle(file);
+ } else {
+ undeployBundle(file);
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.warn("Failed to process: " + file + ". Reason: " + e, e);
+ }
+ }
+ refreshPackagesAndStartOrUpdateBundles();
+ }
+
+ private void rescheduleTransformation(File file) {
+ synchronized (pendingTransformationArtifacts) {
+ pendingTransformationArtifacts.add(file.getAbsolutePath());
+ }
+ if (deployerListener == null) {
+ try {
+ String filter = "(" + Constants.OBJECTCLASS + "=" + DeploymentListener.class.getName() + ")";
+ deployerListener = new ServiceListener() {
+ public void serviceChanged(ServiceEvent event) {
+ executor.execute(new Runnable() {
+ public void run() {
+ Set<String> files;
+ synchronized (pendingTransformationArtifacts) {
+ files = new HashSet<String>(pendingTransformationArtifacts);
+ pendingTransformationArtifacts.clear();
+ }
+ onFilesChanged(files);
+ }
+ });
+ }
+ };
+ getContext().addServiceListener(deployerListener, filter);
+ } catch (InvalidSyntaxException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private File transformArtifact(File file) throws Exception {
+ // Check registered deployers
+ ServiceReference[] srvRefs = getContext().getAllServiceReferences(DeploymentListener.class.getName(),
+ null);
+ if (srvRefs != null) {
+ for (ServiceReference sr : srvRefs) {
+ try {
+ DeploymentListener deploymentListener = (DeploymentListener)getContext().getService(sr);
+ if (deploymentListener.canHandle(file)) {
+ File transformedFile = deploymentListener.handle(file, getGenerateDir());
+ artifactToBundle.put(file.getAbsolutePath(), transformedFile.getAbsolutePath());
+ return transformedFile;
+ }
+ } finally {
+ getContext().ungetService(sr);
+ }
+ }
+ }
+ JarFile jar = null;
+ try {
+ // Handle OSGi bundles with the default deployer
+ if (file.getName().endsWith("txt") || file.getName().endsWith("xml")
+ || file.getName().endsWith("properties")) {
+ // that's file type which is not supported as bundle and avoid
+ // exception in the log
+ return null;
+ }
+ jar = new JarFile(file);
+ Manifest m = jar.getManifest();
+ if (m.getMainAttributes().getValue(new Attributes.Name("Bundle-SymbolicName")) != null
+ && m.getMainAttributes().getValue(new Attributes.Name("Bundle-Version")) != null) {
+ return file;
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Error transforming artifact " + file.getName(), e);
+ } finally {
+ if (jar != null) {
+ jar.close();
+ }
+ }
+ return null;
+ }
+
+ protected void deployBundle(File file) throws IOException, BundleException {
+ LOGGER.info("Deploying: " + file.getCanonicalPath());
+
+ InputStream in = new FileInputStream(file);
+
+ try {
+ Bundle bundle = getBundleForJarFile(file);
+ if (bundle != null) {
+ bundlesToUpdate.add(bundle);
+ } else {
+ bundle = getContext().installBundle(file.getCanonicalFile().toURI().toString(), in);
+ if (!isBundleFragment(bundle)) {
+ bundlesToStart.add(bundle);
+ }
+ }
+ } finally {
+ closeQuietly(in);
+ }
+ }
+
+ protected void undeployBundle(File file) throws BundleException, IOException {
+ LOGGER.info("Undeploying: " + file.getCanonicalPath());
+ Bundle bundle = getBundleForJarFile(file);
+
+ if (bundle == null) {
+ LOGGER.warn("Could not find Bundle for file: " + file.getCanonicalPath());
+ } else {
+ if (!isBundleFragment(bundle)) {
+ bundle.stop();
+ }
+ bundle.uninstall();
+ }
+ }
+
+ protected Bundle getBundleForJarFile(File file) throws IOException {
+ String absoluteFilePath = file.getAbsoluteFile().toURI().toString();
+ Bundle bundles[] = getContext().getBundles();
+ for (Bundle bundle : bundles) {
+ String location = bundle.getLocation();
+ if (filePathsMatch(absoluteFilePath, location)) {
+ return bundle;
+ }
+ }
+ return null;
+ }
+
+ protected static boolean filePathsMatch(String p1, String p2) {
+ p1 = normalizeFilePath(p1);
+ p2 = normalizeFilePath(p2);
+ return (p1 != null && p1.equalsIgnoreCase(p2));
+ }
+
+ protected static String normalizeFilePath(String path) {
+ if (path != null) {
+ path = path.replaceFirst("file:/*", "");
+ path = path.replaceAll("[\\\\/]+", "/");
+ }
+ return path;
+ }
+
+ protected void updateConfiguration(File file) throws IOException, InvalidSyntaxException {
+ ConfigurationAdmin configurationAdmin = activator.getConfigurationAdmin();
+ if (configurationAdmin == null) {
+ if (!loggedConfigAdminWarning) {
+ LOGGER.warn("No ConfigurationAdmin so cannot deploy configurations");
+ loggedConfigAdminWarning = true;
+ }
+ } else {
+ Properties properties = new Properties();
+ InputStream in = new FileInputStream(file);
+ try {
+ properties.load(in);
+ interpolation(properties);
+ closeQuietly(in);
+ String[] pid = parsePid(file);
+ Hashtable<Object, Object> hashtable = new Hashtable<Object, Object>();
+ hashtable.putAll(properties);
+ if (pid[1] != null) {
+ hashtable.put(ALIAS_KEY, pid[1]);
+ }
+
+ Configuration config = getConfiguration(pid[0], pid[1]);
+ if (config.getBundleLocation() != null) {
+ config.setBundleLocation(null);
+ }
+ config.update(hashtable);
+ } finally {
+ closeQuietly(in);
+ }
+ }
+ }
+
+ protected void interpolation(Properties properties) {
+ for (Enumeration e = properties.propertyNames(); e.hasMoreElements();) {
+ String key = (String)e.nextElement();
+ String val = properties.getProperty(key);
+ Matcher matcher = Pattern.compile("\\$\\{([^}]+)\\}").matcher(val);
+ while (matcher.find()) {
+ String rep = System.getProperty(matcher.group(1));
+ if (rep != null) {
+ val = val.replace(matcher.group(0), rep);
+ matcher.reset(val);
+ }
+ }
+ properties.put(key, val);
+ }
+ }
+
+ protected void deleteConfiguration(File file) throws IOException, InvalidSyntaxException {
+ String[] pid = parsePid(file);
+ Configuration config = getConfiguration(pid[0], pid[1]);
+ config.delete();
+ }
+
+ protected Configuration getConfiguration(String pid, String factoryPid) throws IOException,
+ InvalidSyntaxException {
+ ConfigurationAdmin configurationAdmin = activator.getConfigurationAdmin();
+ if (factoryPid != null) {
+ Configuration[] configs = configurationAdmin.listConfigurations("(|(" + ALIAS_KEY + "=" + pid
+ + ")(.alias_factory_pid="
+ + factoryPid + "))");
+ if (configs == null || configs.length == 0) {
+ return configurationAdmin.createFactoryConfiguration(pid, null);
+ } else {
+ return configs[0];
+ }
+ } else {
+ return configurationAdmin.getConfiguration(pid, null);
+ }
+ }
+
+ protected String[] parsePid(File file) {
+ String path = file.getName();
+ String pid = path.substring(0, path.length() - 4);
+ int n = pid.indexOf('-');
+ if (n > 0) {
+ String factoryPid = pid.substring(n + 1);
+ pid = pid.substring(0, n);
+ return new String[] {pid, factoryPid};
+ } else {
+ return new String[] {pid, null};
+ }
+ }
+
+ protected PackageAdmin getPackageAdmin() {
+ ServiceTracker packageAdminTracker = activator.getPackageAdminTracker();
+ if (packageAdminTracker != null) {
+ try {
+ return (PackageAdmin)packageAdminTracker.waitForService(5000L);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+
+ protected boolean isBundleFragment(Bundle bundle) {
+ PackageAdmin packageAdmin = getPackageAdmin();
+ if (packageAdmin != null) {
+ return packageAdmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
+ }
+ return false;
+ }
+
+ protected void refreshPackagesAndStartOrUpdateBundles() {
+ for (Bundle bundle : bundlesToUpdate) {
+ try {
+ bundle.update();
+ LOGGER.info("Updated: " + bundle);
+ } catch (BundleException e) {
+ LOGGER.warn("Failed to update bundle: " + bundle + ". Reason: " + e, e);
+ }
+ }
+
+ for (Bundle bundle : bundlesToStart) {
+ try {
+ bundle.start();
+ LOGGER.info("Started: " + bundle);
+ } catch (BundleException e) {
+ LOGGER.warn("Failed to start bundle: " + bundle + ". Reason: " + e, e);
+ rescheduleStart(bundle);
+ }
+ }
+
+ PackageAdmin packageAdmin = getPackageAdmin();
+ if (packageAdmin != null) {
+ packageAdmin.refreshPackages(null);
+ }
+ }
+
+ private void rescheduleStart(Bundle bundle) {
+ synchronized (pendingStartBundles) {
+ pendingStartBundles.add(bundle);
+ if (bundleListener == null) {
+ bundleListener = new BundleListener() {
+ public void bundleChanged(BundleEvent event) {
+ if (event.getType() == BundleEvent.RESOLVED) {
+ executor.execute(new Runnable() {
+ public void run() {
+ retryPendingStartBundles();
+ }
+ });
+ } else if (event.getType() == BundleEvent.UNINSTALLED) {
+ // bundle was uninstalled meanwhile, so remove
+ // it from the list of pending bundles
+ pendingStartBundles.remove(event.getBundle());
+ }
+ }
+ };
+ getContext().addBundleListener(bundleListener);
+ }
+ }
+ }
+
+ protected void retryPendingStartBundles() {
+ synchronized (pendingStartBundles) {
+ Set<Bundle> bundles = new HashSet<Bundle>();
+ bundles.addAll(pendingStartBundles);
+ pendingStartBundles.clear();
+ boolean success = false;
+ for (Bundle bundle : bundles) {
+ try {
+ bundle.start();
+ LOGGER.info("Pending bundle started: " + bundle);
+ success = true;
+ } catch (BundleException e) {
+ LOGGER.info("Pending bundle not started: " + bundle);
+ pendingStartBundles.add(bundle);
+ }
+ }
+ if (success) {
+ PackageAdmin packageAdmin = getPackageAdmin();
+ if (packageAdmin != null) {
+ packageAdmin.refreshPackages(null);
+ }
+ }
+ }
+ }
+
+ protected File createBundleJar(File dir) throws BundleException, IOException {
+ File destFile = new File(generateDir, dir.getName() + ".jar");
+ if (destFile.exists()) {
+ undeployBundle(destFile);
+ destFile.delete();
+ }
+ JarUtil.jarDir(dir.getPath(), destFile.getPath());
+ return destFile;
+ }
+
+ /**
+ * Returns the root directory of the expanded OSGi bundle if the file is
+ * part of an expanded OSGi bundle or null if it is not
+ */
+ protected File getExpandedBundleRootDirectory(File file) throws IOException {
+ File parent = file.getParentFile();
+ if (file.isDirectory()) {
+ String rootPath = deployDir.getCanonicalPath();
+ if (file.getCanonicalPath().equals(rootPath)) {
+ return null;
+ }
+ if (containsManifest(file)) {
+ return file;
+ }
+ }
+ if (isValidBundleSourceDirectory(parent)) {
+ return getExpandedBundleRootDirectory(parent);
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the given directory is a valid child directory within the
+ * {@link #deployDir}
+ */
+ protected boolean isValidBundleSourceDirectory(File dir) throws IOException {
+ if (dir != null) {
+ String parentPath = dir.getCanonicalPath();
+ String rootPath = deployDir.getCanonicalPath();
+ return !parentPath.equals(rootPath) && parentPath.startsWith(rootPath);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if the given directory is a valid child file within the
+ * {@link #deployDir} or a child of {@link #generateDir}
+ */
+ protected boolean isValidArtifactFile(File file) throws IOException {
+ if (file != null) {
+ String filePath = file.getParentFile().getCanonicalPath();
+ String deployPath = deployDir.getCanonicalPath();
+ String generatePath = generateDir.getCanonicalPath();
+ return filePath.equals(deployPath) || filePath.startsWith(generatePath);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if the given directory is a valid child file within the
+ * {@link #configDir}
+ */
+ protected boolean isValidConfigFile(File file) throws IOException {
+ if (file != null && file.getName().endsWith(".cfg")) {
+ String filePath = file.getParentFile().getCanonicalPath();
+ String configPath = configDir.getCanonicalPath();
+ return filePath.equals(configPath);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if the given directory contains a valid manifest file
+ */
+ protected boolean containsManifest(File dir) {
+ File metaInfDir = new File(dir, "META-INF");
+ if (metaInfDir.exists() && metaInfDir.isDirectory()) {
+ File manifest = new File(metaInfDir, "MANIFEST.MF");
+ return manifest.exists() && !manifest.isDirectory();
+ }
+ return false;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected File getFileValue(Dictionary properties, String key) {
+ Object value = properties.get(key);
+ if (value instanceof File) {
+ return (File)value;
+ } else if (value != null) {
+ return new File(value.toString());
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected Long getLongValue(Dictionary properties, String key) {
+ Object value = properties.get(key);
+ if (value instanceof Long) {
+ return (Long)value;
+ } else if (value != null) {
+ return Long.parseLong(value.toString());
+ }
+ return null;
+ }
+
+ protected void closeQuietly(Closeable in) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ LOGGER.warn("Failed to close stream. " + e, e);
+ }
+ }
+
+ protected PreferencesService getPreferenceService() {
+ if (activator.getPreferenceServiceTracker() != null) {
+ try {
+ return (PreferencesService)activator.getPreferenceServiceTracker().waitForService(5000L);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ return null;
+ }
+
+ protected void saveScannerState() {
+ try {
+ Map<String, Long> results = scanner.getLastScanResults();
+ Preferences prefs = getPreferenceService().getUserPreferences(PREFERENCE_KEY).node(pid);
+ Iterator<String> it = results.keySet().iterator();
+ while (it.hasNext()) {
+ String key = it.next();
+ Long value = results.get(key);
+ prefs.putLong(key, value);
+ }
+ prefs.flush();
+ } catch (Exception e) {
+ LOGGER.error("Error persisting FileMonitor state", e);
+ }
+ }
+
+ protected void loadScannerState() {
+ try {
+ Preferences prefs = getPreferenceService().getUserPreferences(PREFERENCE_KEY).node(pid);
+ Map<String, Long> lastResult = new HashMap<String, Long>();
+ for (String key : prefs.keys()) {
+ lastResult.put(key, prefs.getLong(key, -1));
+ }
+ scanner.setLastScanResults(lastResult);
+ } catch (Exception e) {
+ LOGGER.error("Error loading FileMonitor state", e);
+ }
+ }
+}
diff --git a/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitorActivator.java b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitorActivator.java
new file mode 100644
index 0000000..a700523
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitorActivator.java
@@ -0,0 +1,146 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedServiceFactory;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.osgi.service.prefs.PreferencesService;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Inspired by <a href="http://www.aqute.biz/Code/FileInstall">FileInstall</a>
+ * by Peter Kriens.
+ *
+ * @version $Revision: 1.1 $
+ */
+public class FileMonitorActivator implements BundleActivator, ManagedServiceFactory {
+ private static final Log LOGGER = LogFactory.getLog(FileMonitorActivator.class);
+
+ private BundleContext context;
+ private ServiceTracker packageAdminTracker;
+ private ServiceTracker configurationAdminTracker;
+ private ServiceTracker preferenceServiceTracker;
+ private Map<String, FileMonitor> fileMonitors = new HashMap<String, FileMonitor>();
+
+ // BundleActivator interface
+ // -------------------------------------------------------------------------
+ public void start(BundleContext context) throws Exception {
+ this.context = context;
+
+ Hashtable properties = new Hashtable();
+ properties.put(Constants.SERVICE_PID, getName());
+ context.registerService(ManagedServiceFactory.class.getName(), this, properties);
+
+ packageAdminTracker = new ServiceTracker(context, PackageAdmin.class.getName(), null);
+ packageAdminTracker.open();
+
+ configurationAdminTracker = new ServiceTracker(context, ConfigurationAdmin.class.getName(), null);
+ configurationAdminTracker.open();
+
+ preferenceServiceTracker = new ServiceTracker(context, PreferencesService.class.getName(), null);
+ preferenceServiceTracker.open();
+
+ Hashtable initialProperties = new Hashtable();
+ setPropertiesFromContext(initialProperties, FileMonitor.CONFIG_DIR, FileMonitor.DEPLOY_DIR,
+ FileMonitor.GENERATED_JAR_DIR, FileMonitor.SCAN_INTERVAL);
+ updated("initialPid", initialProperties);
+ }
+
+ public void stop(BundleContext context) throws Exception {
+ Collection<FileMonitor> fileMonitors = this.fileMonitors.values();
+ for (FileMonitor monitor : fileMonitors) {
+ try {
+ // stop the monitor
+ monitor.stop();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ this.fileMonitors.clear();
+
+ preferenceServiceTracker.close();
+ configurationAdminTracker.close();
+ packageAdminTracker.close();
+ }
+
+ // ManagedServiceFactory interface
+ // -------------------------------------------------------------------------
+ public String getName() {
+ return "org.apache.servicemix.kernel.deployer.filemonitor.FileMonitor";
+ }
+
+ public void updated(String pid, Dictionary properties) throws ConfigurationException {
+ deleted(pid);
+ FileMonitor monitor = new FileMonitor(this, properties, pid);
+ fileMonitors.put(pid, monitor);
+ monitor.start();
+ }
+
+ public void deleted(String pid) {
+ FileMonitor monitor = fileMonitors.remove(pid);
+ if (monitor != null) {
+ monitor.stop();
+ }
+ }
+
+ // Properties
+ // -------------------------------------------------------------------------
+ public BundleContext getContext() {
+ return context;
+ }
+
+ public ServiceTracker getConfigurationAdminTracker() {
+ return configurationAdminTracker;
+ }
+
+ public ServiceTracker getPackageAdminTracker() {
+ return packageAdminTracker;
+ }
+
+ public ConfigurationAdmin getConfigurationAdmin() {
+ return (ConfigurationAdmin)getConfigurationAdminTracker().getService();
+ }
+
+ public ServiceTracker getPreferenceServiceTracker() {
+ return preferenceServiceTracker;
+ }
+
+ // Implementation methods
+ // -------------------------------------------------------------------------
+ protected void setPropertiesFromContext(Hashtable properties, String... keys) {
+ for (String key : keys) {
+ Object value = context.getProperty(key);
+ if (value != null) {
+ properties.put(key, value);
+ }
+ }
+ }
+}
diff --git a/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/JarUtil.java b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/JarUtil.java
new file mode 100644
index 0000000..dff9e5c
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/JarUtil.java
@@ -0,0 +1,122 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class JarUtil {
+
+ /**
+ * Zip up a directory
+ *
+ * @param directory
+ * @param zipName
+ * @throws IOException
+ */
+ public static void zipDir(String directory, String zipName) throws IOException {
+ // create a ZipOutputStream to zip the data to
+ ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipName)));
+ String path = "";
+ zipDir(directory, zos, path, Collections.<String>emptySet());
+ // close the stream
+ zos.close();
+ }
+
+ /**
+ * Jar up a directory
+ *
+ * @param directory
+ * @param zipName
+ * @throws IOException
+ */
+ public static void jarDir(String directory, String zipName) throws IOException {
+ // create a ZipOutputStream to zip the data to
+ JarOutputStream zos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(zipName)));
+ String path = "";
+ File manFile = new File(directory, JarFile.MANIFEST_NAME);
+ if (manFile.exists()) {
+ byte[] readBuffer = new byte[8192];
+ FileInputStream fis = new FileInputStream(manFile);
+ try {
+ ZipEntry anEntry = new ZipEntry(JarFile.MANIFEST_NAME);
+ zos.putNextEntry(anEntry);
+ int bytesIn = fis.read(readBuffer);
+ while (bytesIn != -1) {
+ zos.write(readBuffer, 0, bytesIn);
+ bytesIn = fis.read(readBuffer);
+ }
+ } finally {
+ fis.close();
+ }
+ zos.closeEntry();
+ }
+ zipDir(directory, zos, path, Collections.singleton(JarFile.MANIFEST_NAME));
+ // close the stream
+ zos.close();
+ }
+
+ /**
+ * Zip up a directory path
+ * @param directory
+ * @param zos
+ * @param path
+ * @throws IOException
+ */
+ public static void zipDir(String directory, ZipOutputStream zos, String path, Set<String> exclusions) throws IOException {
+ File zipDir = new File(directory);
+ // get a listing of the directory content
+ String[] dirList = zipDir.list();
+ byte[] readBuffer = new byte[8192];
+ int bytesIn = 0;
+ // loop through dirList, and zip the files
+ for (int i = 0; i < dirList.length; i++) {
+ File f = new File(zipDir, dirList[i]);
+ if (f.isDirectory()) {
+ String filePath = f.getPath();
+ zipDir(filePath, zos, path + f.getName() + "/", exclusions);
+ continue;
+ }
+ String entry = path + f.getName();
+ if (!exclusions.contains(entry)) {
+ FileInputStream fis = new FileInputStream(f);
+ try {
+ ZipEntry anEntry = new ZipEntry(entry);
+ zos.putNextEntry(anEntry);
+ bytesIn = fis.read(readBuffer);
+ while (bytesIn != -1) {
+ zos.write(readBuffer, 0, bytesIn);
+ bytesIn = fis.read(readBuffer);
+ }
+ } finally {
+ fis.close();
+ }
+ }
+ }
+ }
+
+}
diff --git a/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/Scanner.java b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/Scanner.java
new file mode 100644
index 0000000..239a1a8
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/main/java/org/apache/servicemix/kernel/deployer/filemonitor/Scanner.java
@@ -0,0 +1,455 @@
+//========================================================================
+//$Id: Scanner.java 2180 2007-10-31 04:01:26Z janb $
+//Copyright 2006 Mort Bay Consulting Pty. Ltd.
+//------------------------------------------------------------------------
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//http://www.apache.org/licenses/LICENSE-2.0
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+//========================================================================
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Scanner
+ * <p/>
+ * Utility for scanning a directory for added, removed and changed files and
+ * reporting these events via registered Listeners.
+ * <p/>
+ * From the <a href="http://jetty.codehaus.org/">Jetty Util project</a>
+ *
+ * @version $Revision: 1.1 $
+ */
+public class Scanner {
+
+ private static Log logger = LogFactory.getLog(Scanner.class);
+ private long _scanInterval;
+ private List<Listener> _listeners = Collections.synchronizedList(new ArrayList<Listener>());
+ private Map<String, Long> _prevScan = new HashMap<String, Long>();
+ private FilenameFilter _filter;
+ private List<File> _scanDirs;
+ private volatile boolean _running = false;
+ private boolean _reportExisting = true;
+ private Timer _timer;
+ private TimerTask _task;
+
+ /**
+ * Listener
+ * <p/>
+ * Marker for notifications re file changes.
+ */
+ public interface Listener {
+ }
+
+ public interface DiscreteListener extends Listener {
+ public void fileChanged(String filename) throws Exception;
+
+ public void fileAdded(String filename) throws Exception;
+
+ public void fileRemoved(String filename) throws Exception;
+ }
+
+ public interface BulkListener extends Listener {
+ public void filesChanged(List<String> filenames) throws Exception;
+ }
+
+ /**
+ *
+ */
+ public Scanner() {
+ }
+
+ /**
+ * Get the scan interval
+ *
+ * @return interval between scans in millis
+ */
+ public long getScanInterval() {
+ return _scanInterval;
+ }
+
+ /**
+ * Set the scan interval
+ *
+ * @param scanInterval pause between scans in millis
+ */
+ public synchronized void setScanInterval(long scanInterval) {
+ this._scanInterval = scanInterval;
+
+ if (_running) {
+ stop();
+
+ _timer = newTimer();
+ _task = newTimerTask();
+
+ schedule(_timer, _task);
+ _running = true;
+ }
+ }
+
+ /**
+ * Set the location of the directory to scan.
+ *
+ * @param dir
+ * @deprecated use setScanDirs(List dirs) instead
+ */
+ public void setScanDir(File dir) {
+ _scanDirs = new ArrayList<File>();
+ _scanDirs.add(dir);
+ }
+
+ /**
+ * Get the location of the directory to scan
+ *
+ * @return
+ * @deprecated use getScanDirs() instead
+ */
+ public File getScanDir() {
+ return (_scanDirs == null ? null : (File)_scanDirs.get(0));
+ }
+
+ public void setScanDirs(List<File> dirs) {
+ _scanDirs = dirs;
+ }
+
+ public List<File> getScanDirs() {
+ return _scanDirs;
+ }
+
+ /**
+ * Apply a filter to files found in the scan directory. Only files matching
+ * the filter will be reported as added/changed/removed.
+ *
+ * @param filter
+ */
+ public void setFilenameFilter(FilenameFilter filter) {
+ this._filter = filter;
+ }
+
+ /**
+ * Get any filter applied to files in the scan dir.
+ *
+ * @return
+ */
+ public FilenameFilter getFilenameFilter() {
+ return _filter;
+ }
+
+ /**
+ * Whether or not an initial scan will report all files as being added.
+ *
+ * @param reportExisting if true, all files found on initial scan will be
+ * reported as being added, otherwise not
+ */
+ public void setReportExistingFilesOnStartup(boolean reportExisting) {
+ this._reportExisting = reportExisting;
+ }
+
+ /**
+ * Add an added/removed/changed listener
+ *
+ * @param listener
+ */
+ public synchronized void addListener(Listener listener) {
+ if (listener == null) {
+ return;
+ }
+
+ _listeners.add(listener);
+ }
+
+ /**
+ * Remove a registered listener
+ *
+ * @param listener the Listener to be removed
+ */
+ public synchronized void removeListener(Listener listener) {
+ if (listener == null) {
+ return;
+ }
+ _listeners.remove(listener);
+ }
+
+ /**
+ * Start the scanning action.
+ */
+ public synchronized void start() {
+ if (_running) {
+ return;
+ }
+
+ _running = true;
+
+ if (_reportExisting) {
+ // if files exist at startup, report them
+ scan();
+ } else {
+ // just register the list of existing files and only report changes
+ _prevScan = scanFiles();
+ }
+
+ _timer = newTimer();
+ _task = newTimerTask();
+
+ schedule(_timer, _task);
+ }
+
+ public TimerTask newTimerTask() {
+ return new TimerTask() {
+ public void run() {
+ scan();
+ }
+ };
+ }
+
+ public Timer newTimer() {
+ return new Timer(true);
+ }
+
+ public void schedule(Timer timer, TimerTask task) {
+ if (timer == null) {
+ throw new IllegalArgumentException("Timer is null");
+ }
+ if (task == null) {
+ throw new IllegalArgumentException("TimerTask is null");
+ }
+
+ if (getScanInterval() > 0) {
+ timer.scheduleAtFixedRate(task, getScanInterval(), getScanInterval());
+ }
+ }
+
+ /**
+ * Stop the scanning.
+ */
+ public synchronized void stop() {
+ if (_running) {
+ _running = false;
+ _timer.cancel();
+ _task.cancel();
+ _task = null;
+ _timer = null;
+ }
+ }
+
+ /**
+ * Perform a pass of the scanner and report changes
+ */
+ public void scan() {
+ Map<String, Long> currentScan = scanFiles();
+ reportDifferences(currentScan, _prevScan);
+ _prevScan = currentScan;
+ }
+
+ /**
+ * Recursively scan all files in the designated directories.
+ *
+ * @return Map of name of file to last modified time
+ */
+ public Map<String, Long> scanFiles() {
+ if (_scanDirs == null) {
+ return Collections.emptyMap();
+ }
+
+ HashMap<String, Long> scanInfo = new HashMap<String, Long>();
+ Iterator<File> itor = _scanDirs.iterator();
+ while (itor.hasNext()) {
+ File dir = (File)itor.next();
+
+ if ((dir != null) && (dir.exists())) {
+ scanFile(dir, scanInfo);
+ }
+ }
+
+ return scanInfo;
+ }
+
+ /**
+ * Report the adds/changes/removes to the registered listeners
+ *
+ * @param currentScan the info from the most recent pass
+ * @param oldScan info from the previous pass
+ */
+ public void reportDifferences(Map<String, Long> currentScan, Map<String, Long> oldScan) {
+ List<String> bulkChanges = new ArrayList<String>();
+
+ Set<String> oldScanKeys = new HashSet<String>(oldScan.keySet());
+ Iterator<Map.Entry<String, Long>> itor = currentScan.entrySet().iterator();
+ while (itor.hasNext()) {
+ Map.Entry<String, Long> entry = itor.next();
+ if (!oldScanKeys.contains(entry.getKey())) {
+ logger.debug("File added: " + entry.getKey());
+ reportAddition(entry.getKey());
+ bulkChanges.add(entry.getKey());
+ } else if (!oldScan.get(entry.getKey()).equals(entry.getValue())) {
+ logger.debug("File changed: " + entry.getKey());
+ reportChange(entry.getKey());
+ oldScanKeys.remove(entry.getKey());
+ bulkChanges.add(entry.getKey());
+ } else {
+ oldScanKeys.remove(entry.getKey());
+ }
+ }
+
+ if (!oldScanKeys.isEmpty()) {
+
+ Iterator<String> keyItor = oldScanKeys.iterator();
+ while (keyItor.hasNext()) {
+ String filename = keyItor.next();
+ logger.debug("File removed: " + filename);
+ reportRemoval(filename);
+ bulkChanges.add(filename);
+ }
+ }
+
+ if (!bulkChanges.isEmpty()) {
+ reportBulkChanges(bulkChanges);
+ }
+ }
+
+ /**
+ * Get last modified time on a single file or recurse if the file is a
+ * directory.
+ *
+ * @param f file or directory
+ * @param scanInfoMap map of filenames to last modified times
+ */
+ private void scanFile(File f, Map<String, Long> scanInfoMap) {
+ try {
+ if (!f.exists()) {
+ return;
+ }
+
+ if (f.isFile()) {
+ if ((_filter == null)
+ || ((_filter != null) && _filter.accept(f.getParentFile(), f.getName()))) {
+ String name = f.getCanonicalPath();
+ long lastModified = f.lastModified();
+ scanInfoMap.put(name, new Long(lastModified));
+ }
+ } else if (f.isDirectory()) {
+ File[] files = f.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ scanFile(files[i], scanInfoMap);
+ }
+ }
+ } catch (IOException e) {
+ logger.warn("Error scanning watched files", e);
+ }
+ }
+
+ /**
+ * Report a file addition to the registered FileAddedListeners
+ *
+ * @param filename
+ */
+ private void reportAddition(String filename) {
+ Iterator<Listener> itor = _listeners.iterator();
+ while (itor.hasNext()) {
+ try {
+ Listener l = itor.next();
+ if (l instanceof DiscreteListener) {
+ ((DiscreteListener)l).fileAdded(filename);
+ }
+ } catch (Exception e) {
+ logger.warn(e);
+ } catch (Error e) {
+ logger.warn(e);
+ }
+ }
+ }
+
+ /**
+ * Report a file removal to the FileRemovedListeners
+ *
+ * @param filename
+ */
+ private void reportRemoval(String filename) {
+ Iterator<Listener> itor = _listeners.iterator();
+ while (itor.hasNext()) {
+ try {
+ Listener l = itor.next();
+ if (l instanceof DiscreteListener) {
+ ((DiscreteListener)l).fileRemoved(filename);
+ }
+ } catch (Exception e) {
+ logger.warn(e);
+ } catch (Error e) {
+ logger.warn(e);
+ }
+ }
+ }
+
+ /**
+ * Report a file change to the FileChangedListeners
+ *
+ * @param filename
+ */
+ private void reportChange(String filename) {
+ Iterator<Listener> itor = _listeners.iterator();
+ while (itor.hasNext()) {
+ try {
+ Listener l = itor.next();
+ if (l instanceof DiscreteListener) {
+ ((DiscreteListener)l).fileChanged(filename);
+ }
+ } catch (Exception e) {
+ logger.warn(e);
+ } catch (Error e) {
+ logger.warn(e);
+ }
+ }
+ }
+
+ private void reportBulkChanges(List<String> filenames) {
+ Iterator<Listener> itor = _listeners.iterator();
+ while (itor.hasNext()) {
+ try {
+ Listener l = itor.next();
+ if (l instanceof BulkListener) {
+ ((BulkListener)l).filesChanged(filenames);
+ }
+ } catch (Exception e) {
+ logger.warn(e);
+ } catch (Error e) {
+ logger.warn(e);
+ }
+ }
+ }
+
+ protected Map<String, Long> getLastScanResults() {
+ Map<String, Long> result = new HashMap<String, Long>();
+ synchronized (_prevScan) {
+ result.putAll(_prevScan);
+ }
+ return result;
+ }
+
+ protected void setLastScanResults(Map<String, Long> results) {
+ synchronized (_prevScan) {
+ _prevScan.clear();
+ _prevScan.putAll(results);
+ }
+ }
+}
diff --git a/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/LICENSE.txt b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/LICENSE.txt
new file mode 100644
index 0000000..6b0b127
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/LICENSE.txt
@@ -0,0 +1,203 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
+
diff --git a/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/MANIFEST.MF b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..15e6aae
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/MANIFEST.MF
@@ -0,0 +1,24 @@
+Manifest-Version: 1.0
+Created-By: 1.5.0_07 (Apple Computer, Inc.)
+Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt
+Import-Package: org.apache.camel.builder,org.apache.camel.example.spri
+ ng;version=1.3.0.SNAPSHOT,org.apache.camel.model,org.apache.camel.spr
+ ing
+Include-Resource: src/main/resources
+Bnd-LastModified: 1195835480265
+Export-Package: org.apache.camel.example.spring;uses:="org.apache.came
+ l.model,org.apache.camel.builder,org.apache.camel.spring";version=1.3
+ .0.SNAPSHOT
+Bundle-Version: 1.3.0.SNAPSHOT
+Bundle-Name: camel-example-spring
+Bundle-Description: An example showing how to work with Camel and Spri
+ ng
+Private-Package: META-INF.spring
+Bundle-DocURL: http://www.apache.org/
+Bundle-Vendor: Apache Software Foundation
+Bundle-ManifestVersion: 2
+Implementation-Title: Apache Camel
+Bundle-SymbolicName: org.apache.camel.camel-example-spring
+Tool: Bnd-0.0.160
+Implementation-Version: 1.3-SNAPSHOT
+
diff --git a/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/NOTICE.txt b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/NOTICE.txt
new file mode 100644
index 0000000..2e215bf
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/NOTICE.txt
@@ -0,0 +1,11 @@
+ =========================================================================
+ == NOTICE file corresponding to the section 4 d of ==
+ == the Apache License, Version 2.0, ==
+ == in this case for the Apache Camel distribution. ==
+ =========================================================================
+
+ This product includes software developed by
+ The Apache Software Foundation (http://www.apache.org/).
+
+ Please read the different LICENSE files present in the licenses directory of
+ this distribution.
diff --git a/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/spring/camel-context.xml b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/spring/camel-context.xml
new file mode 100644
index 0000000..eb2c42a
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/META-INF/spring/camel-context.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<!--
+
+ The default Application Context used by the org.apache.camel.spring.Main if there
+ is no /META-INF/sprint.xml
+
+ -->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
+ http://activemq.apache.org/camel/schema/spring http://activemq.apache.org/camel/schema/spring/camel-spring.xsd">
+
+ <camelContext id="camel" useJmx="true" mbeanServer="mbeanServer"
+ xmlns="http://activemq.apache.org/camel/schema/spring">
+ <package>org.apache.camel.example.spring</package>
+ </camelContext>
+
+ <!-- lets configure the default ActiveMQ broker URL -->
+ <bean id="jms" class="org.apache.camel.component.jms.JmsComponent">
+ <property name="connectionFactory">
+ <bean class="org.apache.activemq.ActiveMQConnectionFactory">
+ <property name="brokerURL" value="vm://localhost?broker.persistent=false&broker.useJmx=false"/>
+ </bean>
+ </property>
+ </bean>
+
+ <!-- Use JMX -->
+ <bean id="agent" class="org.apache.camel.spring.SpringInstrumentationAgent">
+ <property name="MBeanServer" ref="mbeanServer"/>
+ </bean>
+
+ <!--
+ this bean needs to be eagerly pre-instantiated in order for the exporting to occur;
+ this means that it must not be marked as lazily initialized
+ -->
+ <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
+ <property name="defaultDomain" value="org.apache.camel.test"/>
+ </bean>
+ <bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
+ <property name="port" value="1099"/>
+ </bean>
+ <bean id="serverConnector"
+ class="org.springframework.jmx.support.ConnectorServerFactoryBean" depends-on="registry">
+ <property name="objectName" value="connector:name=rmi"/>
+ <property name="serviceUrl"
+ value="service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi"/>
+ <property name="threaded" value="true"/>
+ <property name="daemon" value="true"/>
+ </bean>
+
+</beans>
diff --git a/karaf/deployer/filemonitor/src/test/data/camel-example-spring/org/apache/camel/example/spring/MyRouteBuilder$SomeBean.class b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/org/apache/camel/example/spring/MyRouteBuilder$SomeBean.class
new file mode 100644
index 0000000..158c9da
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/org/apache/camel/example/spring/MyRouteBuilder$SomeBean.class
Binary files differ
diff --git a/karaf/deployer/filemonitor/src/test/data/camel-example-spring/org/apache/camel/example/spring/MyRouteBuilder.class b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/org/apache/camel/example/spring/MyRouteBuilder.class
new file mode 100644
index 0000000..79a433e
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/camel-example-spring/org/apache/camel/example/spring/MyRouteBuilder.class
Binary files differ
diff --git a/karaf/deployer/filemonitor/src/test/data/foo/META-INF/MANIFEST.MF b/karaf/deployer/filemonitor/src/test/data/foo/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..001dd22
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/foo/META-INF/MANIFEST.MF
@@ -0,0 +1,17 @@
+Manifest-Version: 1.0
+License-00:
+License-01: Licensed to the Apache Software Foundation (ASF) under one or more
+License-02: contributor license agreements. See the NOTICE file distributed with
+License-03: this work for additional information regarding copyright ownership.
+License-04: The ASF licenses this file to You under the Apache License, Version 2.0
+License-05: (the "License"); you may not use this file except in compliance with
+License-06: the License. You may obtain a copy of the License at
+License-07:
+License-08: http://www.apache.org/licenses/LICENSE-2.0
+License-09:
+License-10: Unless required by applicable law or agreed to in writing, software
+License-11: distributed under the License is distributed on an "AS IS" BASIS,
+License-12: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+License-13: See the License for the specific language governing permissions and
+License-14: limitations under the License.
+License-15:
diff --git a/karaf/deployer/filemonitor/src/test/data/foo/foo.txt b/karaf/deployer/filemonitor/src/test/data/foo/foo.txt
new file mode 100644
index 0000000..c094a79
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/data/foo/foo.txt
@@ -0,0 +1,18 @@
+/**
+ *
+ * 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.
+ */
+Hey!
\ No newline at end of file
diff --git a/karaf/deployer/filemonitor/src/test/java/org/apache/servicemix/kernel/deployer/filemonitor/BundlePackerTest.java b/karaf/deployer/filemonitor/src/test/java/org/apache/servicemix/kernel/deployer/filemonitor/BundlePackerTest.java
new file mode 100644
index 0000000..83109ed
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/java/org/apache/servicemix/kernel/deployer/filemonitor/BundlePackerTest.java
@@ -0,0 +1,45 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+import org.apache.commons.io.FileUtils;
+
+/**
+ * @version $Revision: 1.1 $
+ */
+public class BundlePackerTest extends TestCase {
+ public void testFiles() throws Exception {
+ File bundleDir = new File("target/deploy");
+ File unpackDir = new File("target/data");
+ FileUtils.deleteDirectory(unpackDir);
+
+ bundleDir.mkdirs();
+ unpackDir.mkdirs();
+
+ FileUtils.copyDirectory(new File("src/test/data"), unpackDir);
+
+/*
+ FileMonitor packer = new FileMonitor(bundleDir, unpackDir);
+ packer.scan();
+*/
+ }
+
+}
diff --git a/karaf/deployer/filemonitor/src/test/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitorTest.java b/karaf/deployer/filemonitor/src/test/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitorTest.java
new file mode 100644
index 0000000..998ca84
--- /dev/null
+++ b/karaf/deployer/filemonitor/src/test/java/org/apache/servicemix/kernel/deployer/filemonitor/FileMonitorTest.java
@@ -0,0 +1,100 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.filemonitor;
+
+import junit.framework.TestCase;
+import org.apache.servicemix.kernel.deployer.filemonitor.FileMonitor;
+
+/**
+ * FileMonitor Tester.
+ *
+ * @author <Authors name>
+ * @version 1.0
+ * @since <pre>07/02/2008</pre>
+ */
+public class FileMonitorTest extends TestCase {
+
+ public FileMonitorTest(String name) {
+ super(name);
+ }
+
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testFilePathsMatch() throws Exception {
+ assertTrue(FileMonitor.filePathsMatch(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt"));
+
+ assertTrue(FileMonitor.filePathsMatch(
+ "C:\\Apps\\apache-servicemix-kernel-1.0.0-rc1\\src\\README.txt",
+ "C:\\Apps\\apache-servicemix-kernel-1.0.0-rc1\\src\\README.txt"));
+
+ assertTrue(FileMonitor.filePathsMatch(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ "C:\\Apps\\apache-servicemix-kernel-1.0.0-rc1\\src\\README.txt"));
+
+ assertTrue(FileMonitor.filePathsMatch(
+ "file:/C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ "C:\\Apps\\apache-servicemix-kernel-1.0.0-rc1\\src\\README.txt"));
+
+ assertTrue(FileMonitor.filePathsMatch(
+ "file://C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ "C:\\Apps\\apache-servicemix-kernel-1.0.0-rc1\\src\\README.txt"));
+ }
+
+ public void testNormalizeFilePath() throws Exception {
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("C:/Apps//apache-servicemix-kernel-1.0.0-rc1////src/////README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("C:\\Apps\\apache-servicemix-kernel-1.0.0-rc1\\src\\README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("C:\\\\Apps\\\\apache-servicemix-kernel-1.0.0-rc1\\\\src\\\\README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("C:\\Apps\\//apache-servicemix-kernel-1.0.0-rc1\\/\\src///\\\\README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("file:C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("file:/C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt"));
+
+ assertEquals(
+ "C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt",
+ FileMonitor.normalizeFilePath("file://C:/Apps/apache-servicemix-kernel-1.0.0-rc1/src/README.txt"));
+ }
+
+}
diff --git a/karaf/deployer/pom.xml b/karaf/deployer/pom.xml
new file mode 100644
index 0000000..7e2810f
--- /dev/null
+++ b/karaf/deployer/pom.xml
@@ -0,0 +1,43 @@
+<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/maven-v4_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.servicemix.kernel</groupId>
+ <artifactId>kernel</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>deployer</artifactId>
+ <packaging>pom</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache ServiceMix Kernel :: Deployer</name>
+
+ <modules>
+ <module>filemonitor</module>
+ <module>spring</module>
+ <module>blueprint</module>
+ <module>features</module>
+ </modules>
+
+</project>
\ No newline at end of file
diff --git a/karaf/deployer/spring/pom.xml b/karaf/deployer/spring/pom.xml
new file mode 100644
index 0000000..31778a9
--- /dev/null
+++ b/karaf/deployer/spring/pom.xml
@@ -0,0 +1,74 @@
+<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/maven-v4_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.servicemix.kernel</groupId>
+ <artifactId>kernel</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.spring</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache ServiceMix Kernel :: Spring Deployer</name>
+
+ <description>This deployer transforms a plain spring xml file to a deployable bundle</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.osgi</groupId>
+ <artifactId>spring-osgi-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.kernel.deployer</groupId>
+ <artifactId>org.apache.servicemix.kernel.deployer.filemonitor</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.junit</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
+ <Private-Package>org.apache.servicemix.kernel.spring</Private-Package>
+ <Spring-Context>*;publish-context:=false;create-asynchronously:=false</Spring-Context>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringDeploymentListener.java b/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringDeploymentListener.java
new file mode 100644
index 0000000..11c394f
--- /dev/null
+++ b/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringDeploymentListener.java
@@ -0,0 +1,80 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.spring;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener;
+
+/**
+ * A deployment listener that listens for spring xml applications
+ * and creates bundles for these.
+ */
+public class SpringDeploymentListener implements DeploymentListener {
+
+ private static final Log LOGGER = LogFactory.getLog(SpringDeploymentListener.class);
+
+ private DocumentBuilderFactory dbf;
+
+ public boolean canHandle(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 ("beans".equals(name) && "http://www.springframework.org/schema/beans".equals(uri)) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("Unable to parse deployed file " + artifact.getAbsolutePath(), e);
+ }
+ return false;
+ }
+
+ public File handle(File artifact, File tmpDir) {
+ try {
+ File destFile = new File(tmpDir, artifact.getName() + ".jar");
+ FileOutputStream os = new FileOutputStream(destFile);
+ SpringTransformer.transform(artifact.toURL(), os);
+ os.close();
+ return destFile;
+ } catch (Exception e) {
+ LOGGER.error("Unable to build spring application bundle", e);
+ return null;
+ }
+ }
+
+ protected Document parse(File artifact) throws Exception {
+ if (dbf == null) {
+ dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ }
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ return db.parse(artifact);
+ }
+
+}
diff --git a/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringTransformer.java b/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringTransformer.java
new file mode 100644
index 0000000..91d8b2a
--- /dev/null
+++ b/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringTransformer.java
@@ -0,0 +1,249 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.spring;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import org.osgi.framework.Constants;
+
+public class SpringTransformer {
+
+ static Transformer transformer;
+ static DocumentBuilderFactory dbf;
+ static TransformerFactory tf;
+
+
+ public static void transform(URL url, OutputStream os) throws Exception {
+ // Build dom document
+ Document doc = parse(url);
+ // Heuristicly retrieve name and version
+ String name = url.getPath();
+ int idx = name.lastIndexOf('/');
+ if (idx >= 0) {
+ name = name.substring(idx + 1);
+ }
+ String[] str = extractNameVersionType(name);
+ // Create manifest
+ Manifest m = new Manifest();
+ m.getMainAttributes().putValue("Manifest-Version", "2");
+ m.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2");
+ m.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, str[0]);
+ m.getMainAttributes().putValue(Constants.BUNDLE_VERSION, str[1]);
+ m.getMainAttributes().putValue("Spring-Context", "*;publish-context:=false;create-asynchronously:=true");
+ String importPkgs = getImportPackages(analyze(new DOMSource(doc)));
+ if (importPkgs != null && importPkgs.length() > 0) {
+ m.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, importPkgs);
+ }
+ m.getMainAttributes().putValue(Constants.DYNAMICIMPORT_PACKAGE, "*");
+ // Extract manifest entries from the DOM
+ NodeList l = doc.getElementsByTagName("manifest");
+ if (l != null) {
+ for (int i = 0; i < l.getLength(); i++) {
+ Element e = (Element) l.item(i);
+ String text = e.getTextContent();
+ Properties props = new Properties();
+ props.load(new ByteArrayInputStream(text.trim().getBytes()));
+ Enumeration en = props.propertyNames();
+ while (en.hasMoreElements()) {
+ String k = (String) en.nextElement();
+ String v = props.getProperty(k);
+ m.getMainAttributes().putValue(k, v);
+ }
+ e.getParentNode().removeChild(e);
+ }
+ }
+
+ JarOutputStream out = new JarOutputStream(os);
+ ZipEntry e = new ZipEntry(JarFile.MANIFEST_NAME);
+ out.putNextEntry(e);
+ m.write(out);
+ out.closeEntry();
+ e = new ZipEntry("META-INF/");
+ out.putNextEntry(e);
+ e = new ZipEntry("META-INF/spring/");
+ out.putNextEntry(e);
+ out.closeEntry();
+ e = new ZipEntry("META-INF/spring/" + name);
+ out.putNextEntry(e);
+ // Copy the new DOM
+ if (tf == null) {
+ tf = TransformerFactory.newInstance();
+ }
+ tf.newTransformer().transform(new DOMSource(doc), new StreamResult(out));
+ out.closeEntry();
+ out.close();
+ }
+
+ private static final String DEFAULT_VERSION = "0.0.0";
+
+ private static final Pattern ARTIFACT_MATCHER = Pattern.compile("(.+)(?:-(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?(?:[^a-zA-Z0-9](.*))?)(?:\\.([^\\.]+))", Pattern.DOTALL);
+ private static final Pattern FUZZY_MODIFIDER = Pattern.compile("(?:\\d+[.-])*(.*)", Pattern.DOTALL);
+
+ public static String[] extractNameVersionType(String url) {
+ Matcher m = ARTIFACT_MATCHER.matcher(url);
+ if (!m.matches()) {
+ return new String[] { url, DEFAULT_VERSION };
+ }
+ else {
+ //System.err.println(m.groupCount());
+ //for (int i = 1; i <= m.groupCount(); i++) {
+ // System.err.println("Group " + i + ": " + m.group(i));
+ //}
+
+ StringBuffer v = new StringBuffer();
+ String d1 = m.group(1);
+ String d2 = m.group(2);
+ String d3 = m.group(3);
+ String d4 = m.group(4);
+ String d5 = m.group(5);
+ String d6 = m.group(6);
+ if (d2 != null) {
+ v.append(d2);
+ if (d3 != null) {
+ v.append('.');
+ v.append(d3);
+ if (d4 != null) {
+ v.append('.');
+ v.append(d4);
+ if (d5 != null) {
+ v.append(".");
+ cleanupModifier(v, d5);
+ }
+ } else if (d5 != null) {
+ v.append(".0.");
+ cleanupModifier(v, d5);
+ }
+ } else if (d5 != null) {
+ v.append(".0.0.");
+ cleanupModifier(v, d5);
+ }
+ }
+ return new String[] { d1, v.toString(), d6 };
+ }
+ }
+
+ private static void cleanupModifier(StringBuffer result, String modifier) {
+ Matcher m = FUZZY_MODIFIDER.matcher(modifier);
+ if (m.matches()) {
+ modifier = m.group(1);
+ }
+ for (int i = 0; i < modifier.length(); i++) {
+ char c = modifier.charAt(i);
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '-') {
+ result.append(c);
+ }
+ }
+ }
+
+ public static Set<String> analyze(Source source) throws Exception {
+ if (transformer == null) {
+ if (tf == null) {
+ tf = TransformerFactory.newInstance();
+ }
+ Source s = new StreamSource(SpringTransformer.class.getResourceAsStream("extract.xsl"));
+ transformer = tf.newTransformer(s);
+ }
+
+ Set<String> refers = new TreeSet<String>();
+
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ Result r = new StreamResult(bout);
+ transformer.transform(source, r);
+
+ ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
+ bout.close();
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(bin));
+
+ String line = br.readLine();
+ while (line != null) {
+ line = line.trim();
+ if (line.length() > 0) {
+ String parts[] = line.split("\\s*,\\s*");
+ for (int i = 0; i < parts.length; i++) {
+ int n = parts[i].lastIndexOf('.');
+ if (n > 0) {
+ refers.add(parts[i].substring(0, n));
+ }
+ }
+ }
+ line = br.readLine();
+ }
+ br.close();
+ return refers;
+ }
+
+ protected static String getImportPackages(Set<String> packages) {
+ StringBuilder sb = new StringBuilder();
+ for (String pkg : packages) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(pkg);
+ }
+ return sb.toString();
+ }
+
+ protected static Document parse(URL url) throws Exception {
+ if (dbf == null) {
+ dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ }
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ return db.parse(url.toString());
+ }
+
+ protected static void copyInputStream(InputStream in, OutputStream out) throws Exception {
+ byte[] buffer = new byte[4096];
+ int len = in.read(buffer);
+ while (len >= 0) {
+ out.write(buffer, 0, len);
+ len = in.read(buffer);
+ }
+ }
+}
diff --git a/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringURLHandler.java b/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringURLHandler.java
new file mode 100644
index 0000000..fce7913
--- /dev/null
+++ b/karaf/deployer/spring/src/main/java/org/apache/servicemix/kernel/deployer/spring/SpringURLHandler.java
@@ -0,0 +1,97 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.spring;
+
+import java.io.File;
+import java.io.FileInputStream;
+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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.service.url.AbstractURLStreamHandlerService;
+
+/**
+ * A URL handler that will transform a JBI artifact to an OSGi bundle
+ * on the fly. Needs to be registered in the OSGi registry.
+ */
+public class SpringURLHandler extends AbstractURLStreamHandlerService {
+
+ private static Log logger = LogFactory.getLog(SpringURLHandler.class);
+
+ private static String SYNTAX = "spring: spring-xml-uri";
+
+ private URL springXmlURL;
+
+ /**
+ * Open the connection for the given URL.
+ *
+ * @param url the url from which to open a connection.
+ * @return a connection on the specified URL.
+ * @throws IOException if an error occurs or if the URL is malformed.
+ */
+ @Override
+ public 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 );
+ }
+ springXmlURL = new URL(url.getPath());
+
+ logger.debug("Spring xml URL is: [" + springXmlURL + "]");
+ return new Connection(url);
+ }
+
+ public URL getSpringXmlURL() {
+ return springXmlURL;
+ }
+
+ public class Connection extends URLConnection {
+
+ public Connection(URL url) {
+ super(url);
+ }
+
+ @Override
+ public void connect() throws IOException {
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ try {
+ final File f = File.createTempFile("smx", "xml");
+ FileOutputStream os = new FileOutputStream(f);
+ SpringTransformer.transform(springXmlURL, os);
+ os.close();
+ return new FileInputStream(f) {
+ public void close() throws IOException {
+ super.close();
+ f.delete();
+ }
+ };
+ } catch (Exception e) {
+ logger.error("Error opening spring xml url", e);
+ throw (IOException) new IOException("Error opening spring xml url").initCause(e);
+ }
+ }
+ }
+
+}
diff --git a/karaf/deployer/spring/src/main/resources/META-INF/spring/spring-deployer.xml b/karaf/deployer/spring/src/main/resources/META-INF/spring/spring-deployer.xml
new file mode 100644
index 0000000..af2921a
--- /dev/null
+++ b/karaf/deployer/spring/src/main/resources/META-INF/spring/spring-deployer.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:osgi="http://www.springframework.org/schema/osgi"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/util
+ http://www.springframework.org/schema/util/spring-util.xsd
+ http://www.springframework.org/schema/osgi
+ http://www.springframework.org/schema/osgi/spring-osgi.xsd">
+
+ <bean id="springDeploymentListener" class="org.apache.servicemix.kernel.deployer.spring.SpringDeploymentListener">
+
+ </bean>
+
+ <osgi:service ref="springDeploymentListener">
+ <osgi:interfaces>
+ <value>org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener</value>
+ </osgi:interfaces>
+ </osgi:service>
+
+ <bean id="springHandler" class="org.apache.servicemix.kernel.deployer.spring.SpringURLHandler" />
+
+ <osgi:service ref="springHandler" interface="org.osgi.service.url.URLStreamHandlerService">
+ <osgi:service-properties>
+ <entry key="url.handler.protocol" value="spring"/>
+ </osgi:service-properties>
+ </osgi:service>
+
+</beans>
\ No newline at end of file
diff --git a/karaf/deployer/spring/src/main/resources/org/apache/servicemix/kernel/deployer/spring/extract.xsl b/karaf/deployer/spring/src/main/resources/org/apache/servicemix/kernel/deployer/spring/extract.xsl
new file mode 100644
index 0000000..4b06b69
--- /dev/null
+++ b/karaf/deployer/spring/src/main/resources/org/apache/servicemix/kernel/deployer/spring/extract.xsl
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:beans="http://www.springframework.org/schema/beans"
+ xmlns:aop="http://www.springframework.org/schema/aop"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:jee="http://www.springframework.org/schema/jee"
+ xmlns:jms="http://www.springframework.org/schema/jms"
+ xmlns:lang="http://www.springframework.org/schema/lang"
+ xmlns:osgi-compendium="http://www.springframework.org/schema/osgi-compendium"
+ xmlns:osgi="http://www.springframework.org/schema/osgi"
+ xmlns:tool="http://www.springframework.org/schema/tool"
+ xmlns:tx="http://www.springframework.org/schema/tx"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:webflow-config="http://www.springframework.org/schema/webflow-config">
+
+ <xsl:output method="text" />
+
+ <xsl:template match="/">
+
+ <!-- Match all attributes that holds a class or a comma delimited
+ list of classes and print them -->
+
+ <xsl:for-each select="
+ //beans:bean/@class
+ | //beans:*/@value-type
+ | //aop:*/@implement-interface
+ | //aop:*/@default-impl
+ | //context:load-time-weaver/@weaver-class
+ | //jee:jndi-lookup/@expected-type
+ | //jee:jndi-lookup/@proxy-interface
+ | //jee:remote-slsb/@ejbType
+ | //jee:*/@business-interface
+ | //lang:*/@script-interfaces
+ | //osgi:*/@interface
+ | //util:list/@list-class
+ | //util:set/@set-class
+ | //util:map/@map-class
+ | //webflow-config:*/@class
+ ">
+ <xsl:value-of select="." />
+ <xsl:text>
+ </xsl:text>
+ </xsl:for-each>
+
+ <!-- This seems some magic to get extra imports? -->
+
+ <xsl:for-each select="//beans:bean[@class='org.springframework.osgi.service.exporter.support.OsgiServiceFactoryBean'
+ or @class='org.springframework.osgi.service.importer.support.OsgiServiceProxyFactoryBean']">
+ <xsl:for-each select="beans:property[@name='interfaces']">
+ <xsl:value-of select="@value" />
+ <xsl:text>
+ </xsl:text>
+ </xsl:for-each>
+ </xsl:for-each>
+
+ </xsl:template>
+
+
+</xsl:stylesheet>
+
diff --git a/karaf/deployer/spring/src/test/java/org/apache/servicemix/kernel/deployer/spring/SpringDeploymentListenerTest.java b/karaf/deployer/spring/src/test/java/org/apache/servicemix/kernel/deployer/spring/SpringDeploymentListenerTest.java
new file mode 100644
index 0000000..9a70dd0
--- /dev/null
+++ b/karaf/deployer/spring/src/test/java/org/apache/servicemix/kernel/deployer/spring/SpringDeploymentListenerTest.java
@@ -0,0 +1,86 @@
+/**
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicemix.kernel.deployer.spring;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.jar.JarInputStream;
+
+import javax.xml.transform.dom.DOMSource;
+
+import junit.framework.TestCase;
+import org.apache.servicemix.kernel.deployer.spring.SpringDeploymentListener;
+import org.apache.servicemix.kernel.deployer.spring.SpringTransformer;
+
+public class SpringDeploymentListenerTest extends TestCase {
+
+ public void testPackagesExtraction() throws Exception {
+ SpringDeploymentListener l = new SpringDeploymentListener();
+ File f = new File(getClass().getClassLoader().getResource("META-INF/spring/spring-deployer.xml").toURI());
+ Set<String> pkgs = SpringTransformer.analyze(new DOMSource(SpringTransformer.parse(f.toURL())));
+ assertNotNull(pkgs);
+ assertEquals(2, pkgs.size());
+ Iterator<String> it = pkgs.iterator();
+ assertEquals("org.apache.servicemix.kernel.deployer.spring", it.next());
+ assertEquals("org.osgi.service.url", it.next());
+ }
+
+ public void testCustomManifest() throws Exception {
+ File f = File.createTempFile("smx", ".jar");
+ try {
+ OutputStream os = new FileOutputStream(f);
+ SpringTransformer.transform(getClass().getClassLoader().getResource("test.xml"), os);
+ os.close();
+ InputStream is = new FileInputStream(f);
+ JarInputStream jar = new JarInputStream(is);
+ jar.getManifest().write(System.err);
+ is.close();
+ } finally {
+ f.delete();
+ }
+ }
+
+ public void testVersions() {
+ assertVersion("org.apache.servicemix.bundles.ant-1.7.0-1.0-m3-SNAPSHOT.jar",
+ "org.apache.servicemix.bundles.ant-1.7.0", "1.0.0.m3-SNAPSHOT", "jar");
+ assertVersion("org.apache.activemq.core-1.0-SNAPSHOT.xml",
+ "org.apache.activemq.core", "1.0.0.SNAPSHOT", "xml");
+ assertVersion("org.apache.activemq.core-1.0.0-SNAPSHOT.xml",
+ "org.apache.activemq.core", "1.0.0.SNAPSHOT", "xml");
+ assertVersion("org.apache.activemq.core-1.0.0.xml",
+ "org.apache.activemq.core", "1.0.0", "xml");
+ assertVersion("geronimo-servlet_2.5_spec-1.1.2.jar",
+ "geronimo-servlet_2.5_spec", "1.1.2", "jar");
+ assertVersion("spring-aop-2.5.1.jar",
+ "spring-aop", "2.5.1", "jar");
+ }
+
+ private void assertVersion(String s, String... expectedParts) {
+ String[] parts = SpringTransformer.extractNameVersionType(s);
+ assertEquals(expectedParts.length, parts.length);
+ for (int i = 0; i < expectedParts.length; i++) {
+ assertEquals(expectedParts[i], parts[i]);
+ }
+ }
+
+}
diff --git a/karaf/deployer/spring/src/test/resources/test.xml b/karaf/deployer/spring/src/test/resources/test.xml
new file mode 100644
index 0000000..31dca21
--- /dev/null
+++ b/karaf/deployer/spring/src/test/resources/test.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:osgi="http://www.springframework.org/schema/osgi"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/util
+ http://www.springframework.org/schema/util/spring-util.xsd
+ http://www.springframework.org/schema/osgi
+ http://www.springframework.org/schema/osgi/spring-osgi.xsd">
+
+ <manifest>
+ RequireBundle=org.osgi
+ Header=value
+ </manifest>
+
+ <bean id="springDeploymentListener" class="org.apache.servicemix.kernel.deployer.spring.SpringDeploymentListener">
+
+ </bean>
+
+ <osgi:service ref="springDeploymentListener">
+ <osgi:interfaces>
+ <value>org.apache.servicemix.kernel.deployer.filemonitor.DeploymentListener</value>
+ </osgi:interfaces>
+ </osgi:service>
+
+ <bean id="springHandler" class="org.apache.servicemix.kernel.deployer.spring.SpringURLHandler" />
+
+ <osgi:service ref="springHandler" interface="org.osgi.service.url.URLStreamHandlerService">
+ <osgi:service-properties>
+ <entry key="url.handler.protocol" value="spring"/>
+ </osgi:service-properties>
+ </osgi:service>
+
+</beans>
\ No newline at end of file