diff --git a/plugins/osgi/pom.xml b/plugins/osgi/pom.xml
new file mode 100644
index 000000000..0d8770924
--- /dev/null
+++ b/plugins/osgi/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+
+ org.apache.struts
+ struts2-plugins
+ 2.0.9
+
+ org.apache.struts
+ struts2-osgi-plugin
+ jar
+ 1.0.0-SNAPSHOT
+ Struts 2 OSGI Plugin
+
+
+ scm:svn:http://svn.apache.org/repos/asf/struts/sandbox/trunk/struts2-osgi-plugin/
+ scm:svn:https://svn.apache.org/repos/asf/struts/sandbox/trunk/struts2-osgi-plugin/
+ http://svn.apache.org/viewcvs.cgi/struts/sandbox/trunk/struts2-osgi-plugin/
+
+
+
+
+
+ true
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+
+
+
+
+ org.apache.felix
+ org.apache.felix.main
+ 1.4.1
+
+
+
+ org.apache.struts
+ struts2-core
+ 2.1.7-SNAPSHOT
+
+
+
+ org.apache.velocity
+ velocity
+ 1.5
+
+
+
+ org.apache.velocity
+ velocity-tools
+ 1.3
+
+
+ javax.servlet
+ servlet-api
+
+
+ struts
+ struts
+
+
+
+
+
+ commons-digester
+ commons-digester
+ 2.0
+
+
+
+ junit
+ junit
+ test
+ 3.8.1
+
+
+
+ org.easymock
+ easymock
+ test
+ 2.4
+
+
+
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleAccessor.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleAccessor.java
new file mode 100644
index 000000000..6d48a42e3
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleAccessor.java
@@ -0,0 +1,62 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Map;
+import java.util.Set;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+public interface BundleAccessor {
+
+ String CURRENT_BUNDLE_NAME = "__bundle_name__";
+
+ Class loadClass(String name) throws ClassNotFoundException;
+
+ InputStream loadResourceAsStream(String name) throws IOException;
+
+ InputStream loadResourceFromAllBundlesAsStream(String name) throws IOException;
+
+ URL loadResourceFromAllBundles(String name) throws IOException;
+
+ Set getPackagesByBundle(Bundle bundle);
+
+ Object getService(ServiceReference ref);
+
+ ServiceReference getServiceReference(String className);
+
+ ServiceReference[] getServiceReferences(String className, String params) throws InvalidSyntaxException;
+
+ public ServiceReference[] getAllServiceReferences(String className);
+
+ void addPackageFromBundle(Bundle bundle, String packageName);
+
+ void setBundleContext(BundleContext bundleContext);
+
+ void setOsgiHost(OsgiHost osgiHost);
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleClassLoaderInterface.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleClassLoaderInterface.java
new file mode 100644
index 000000000..537176c6b
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleClassLoaderInterface.java
@@ -0,0 +1,57 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
+
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Collections;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.osgi.framework.Bundle;
+
+/**
+ * ClassLoaderInterface instance that delegates to the singleton of DefaultBundleAccessor
+ */
+public class BundleClassLoaderInterface implements ClassLoaderInterface {
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ return DefaultBundleAccessor.getInstance().loadClass(name);
+ }
+
+ public URL getResource(String name) {
+ return DefaultBundleAccessor.getInstance().loadResource(name, true);
+ }
+
+ public Enumeration getResources(String name) throws IOException {
+ return Collections.enumeration(DefaultBundleAccessor.getInstance().loadResources(name, true));
+ }
+
+ public InputStream getResourceAsStream(String name) throws IOException {
+ return DefaultBundleAccessor.getInstance().loadResourceAsStream(name);
+ }
+
+ public ClassLoaderInterface getParent() {
+ return null;
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleFreemarkerManager.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleFreemarkerManager.java
new file mode 100644
index 000000000..e7617df4f
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundleFreemarkerManager.java
@@ -0,0 +1,83 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.servlet.ServletContext;
+
+import org.apache.struts2.osgi.loaders.FreeMarkerBundleResourceLoader;
+import org.apache.struts2.views.freemarker.FreemarkerManager;
+import org.apache.struts2.views.freemarker.StrutsClassTemplateLoader;
+
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.cache.WebappTemplateLoader;
+
+/**
+ * This class extends FreemarkerManager in core to add a template loader
+ * (that finds resources inside bundles) to MultiTemplateLoader
+ */
+public class BundleFreemarkerManager extends FreemarkerManager {
+ private static final Logger LOG = LoggerFactory.getLogger(BundleFreemarkerManager.class);
+
+ @Override
+ protected TemplateLoader getTemplateLoader(ServletContext servletContext) {
+ // construct a FileTemplateLoader for the init-param 'TemplatePath'
+ FileTemplateLoader templatePathLoader = null;
+
+ String templatePath = servletContext.getInitParameter("TemplatePath");
+ if (templatePath == null) {
+ templatePath = servletContext.getInitParameter("templatePath");
+ }
+
+ if (templatePath != null) {
+ try {
+ templatePathLoader = new FileTemplateLoader(new File(templatePath));
+ } catch (IOException e) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Invalid template path specified: [#0]", e, e.getMessage());
+ }
+ }
+
+ // presume that most apps will require the class and webapp template loader
+ // if people wish to
+ return templatePathLoader != null ?
+ new MultiTemplateLoader(new TemplateLoader[]{
+ templatePathLoader,
+ new WebappTemplateLoader(servletContext),
+ new StrutsClassTemplateLoader(),
+ new FreeMarkerBundleResourceLoader()
+ })
+ : new MultiTemplateLoader(new TemplateLoader[]{
+ new WebappTemplateLoader(servletContext),
+ new StrutsClassTemplateLoader(),
+ new FreeMarkerBundleResourceLoader()
+ });
+ }
+
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundlePackageLoader.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundlePackageLoader.java
new file mode 100644
index 000000000..620eccff4
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/BundlePackageLoader.java
@@ -0,0 +1,107 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.*;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.config.Configuration;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.PackageProvider;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.impl.DefaultConfiguration;
+import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
+import com.opensymphony.xwork2.util.location.Location;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+/**
+ * Package loader implementation that loads resources from a bundle
+ */
+public class BundlePackageLoader implements PackageLoader {
+ private static final Logger LOG = LoggerFactory.getLogger(BundlePackageLoader.class);
+
+ public List loadPackages(Bundle bundle, BundleContext bundleContext, ObjectFactory objectFactory, Map pkgConfigs) throws ConfigurationException {
+ Configuration config = new DefaultConfiguration("struts.xml");
+ BundleConfigurationProvider prov = new BundleConfigurationProvider("struts.xml", bundle, bundleContext);
+ for (PackageConfig pkg : pkgConfigs.values()) {
+ config.addPackageConfig(pkg.getName(), pkg);
+ }
+ prov.setObjectFactory(objectFactory);
+ prov.init(config);
+ prov.loadPackages();
+
+ List list = new ArrayList(config.getPackageConfigs().values());
+ list.removeAll(pkgConfigs.values());
+
+ return list;
+ }
+
+ static class BundleConfigurationProvider extends XmlConfigurationProvider {
+ private Bundle bundle;
+ private BundleContext bundleContext;
+
+ public BundleConfigurationProvider(String filename, Bundle bundle, BundleContext bundleContext) {
+ super(filename, false);
+ this.bundle = bundle;
+ this.bundleContext = bundleContext;
+ }
+
+ public BundleConfigurationProvider(String filename) {
+ super(filename);
+ }
+
+ @Override
+ protected Iterator getConfigurationUrls(String fileName) throws IOException {
+ Enumeration e = bundle.getResources("struts.xml");
+ return e.hasMoreElements() ? new EnumeratorIterator(e) : null;
+ }
+ }
+
+ static class EnumeratorIterator implements Iterator {
+ Enumeration e = null;
+
+ public EnumeratorIterator(Enumeration e) {
+ this.e = e;
+ }
+
+ public boolean hasNext() {
+ return e.hasMoreElements();
+ }
+
+ public E next() {
+ return e.nextElement();
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/DefaultBundleAccessor.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/DefaultBundleAccessor.java
new file mode 100644
index 000000000..c3c476f39
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/DefaultBundleAccessor.java
@@ -0,0 +1,211 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.MalformedURLException;
+import java.util.*;
+import java.util.Map.Entry;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ActionProxy;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+
+/**
+ * Helper class that find resources and loads classes from the list of bundles
+ */
+public class DefaultBundleAccessor implements BundleAccessor {
+
+ private static DefaultBundleAccessor self;
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultBundleAccessor.class);
+
+ private BundleContext bundleContext;
+ private Map packageToBundle = new HashMap();
+ private Map> packagesByBundle = new HashMap>();
+ private OsgiHost osgiHost;
+
+ public DefaultBundleAccessor() {
+ self = this;
+ }
+
+ public static DefaultBundleAccessor getInstance() {
+ return self;
+ }
+
+ public Object getService(ServiceReference ref) {
+ return bundleContext != null ? bundleContext.getService(ref) : null;
+ }
+
+ public ServiceReference getServiceReference(String className) {
+ return bundleContext != null ? bundleContext.getServiceReference(className) : null;
+ }
+
+ public ServiceReference[] getAllServiceReferences(String className) {
+ if (bundleContext != null) {
+ try {
+ return bundleContext.getServiceReferences(className, null);
+ } catch (InvalidSyntaxException e) {
+ //cannot happen we are passing null as the param
+ if (LOG.isErrorEnabled())
+ LOG.error("Invalid syntax for service lookup", e);
+ }
+ }
+
+ return null;
+ }
+
+ public ServiceReference[] getServiceReferences(String className, String params) throws InvalidSyntaxException {
+ return bundleContext != null ? bundleContext.getServiceReferences(className, params) : null;
+ }
+
+ /**
+ * Add as Bundle -> Package mapping
+ * @param bundle the bundle where the package was loaded from
+ * @param packageName the anme of the loaded package
+ */
+ public void addPackageFromBundle(Bundle bundle, String packageName) {
+ this.packageToBundle.put(packageName, bundle.getSymbolicName());
+ Set pkgs = packagesByBundle.get(bundle);
+ if (pkgs == null) {
+ pkgs = new HashSet();
+ packagesByBundle.put(bundle, pkgs);
+ }
+ pkgs.add(packageName);
+ }
+
+ public Class> loadClass(String className) throws ClassNotFoundException {
+ Bundle bundle = getCurrentBundle();
+ if (bundle != null) {
+ Class cls = bundle.loadClass(className);
+ if (LOG.isTraceEnabled())
+ LOG.trace("Located class [#0] in bundle [#1]", className, bundle.getSymbolicName());
+ return cls;
+ }
+
+ throw new ClassNotFoundException("Unable to find class " + className);
+ }
+
+ private Bundle getCurrentBundle() {
+ ActionContext ctx = ActionContext.getContext();
+ String bundleName = (String) ctx.get(CURRENT_BUNDLE_NAME);
+ if (bundleName == null) {
+ ActionInvocation inv = ctx.getActionInvocation();
+ ActionProxy proxy = inv.getProxy();
+ ActionConfig actionConfig = proxy.getConfig();
+ bundleName = packageToBundle.get(actionConfig.getPackageName());
+ }
+ if (bundleName != null) {
+ return osgiHost.getActiveBundles().get(bundleName);
+ }
+ return null;
+ }
+
+ public List loadResources(String name) throws IOException {
+ return loadResources(name, false);
+ }
+
+ public List loadResources(String name, boolean translate) throws IOException {
+ Bundle bundle = getCurrentBundle();
+ if (bundle != null) {
+ List resources = new ArrayList();
+ Enumeration e = bundle.getResources(name);
+ while (e.hasMoreElements()) {
+ resources.add(translate ? OsgiUtil.translateBundleURLToJarURL((URL) e.nextElement(), getCurrentBundle()) : (URL) e.nextElement());
+ }
+ return resources;
+ }
+
+ return null;
+ }
+
+ public URL loadResourceFromAllBundles(String name) throws IOException {
+ for (Map.Entry entry : osgiHost.getActiveBundles().entrySet()) {
+ Enumeration e = entry.getValue().getResources(name);
+ if (e.hasMoreElements()) {
+ return (URL) e.nextElement();
+ }
+ }
+
+ return null;
+ }
+
+ public InputStream loadResourceFromAllBundlesAsStream(String name) throws IOException {
+ URL url = loadResourceFromAllBundles(name);
+ if (url != null) {
+ return url.openStream();
+ }
+ return null;
+ }
+
+ public URL loadResource(String name) {
+ return loadResource(name, false);
+ }
+
+ public URL loadResource(String name, boolean translate) {
+ Bundle bundle = getCurrentBundle();
+ if (bundle != null) {
+ URL url = bundle.getResource(name);
+ try {
+ return translate ? OsgiUtil.translateBundleURLToJarURL(url, getCurrentBundle()) : url;
+ } catch (Exception e) {
+ if (LOG.isErrorEnabled()) {
+ LOG.error("Unable to translate bundle URL to jar URL", e);
+ }
+
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ public Set getPackagesByBundle(Bundle bundle) {
+ return packagesByBundle.get(bundle);
+ }
+
+ public InputStream loadResourceAsStream(String name) throws IOException {
+ URL url = loadResource(name);
+ if (url != null) {
+ return url.openStream();
+ }
+ return null;
+ }
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public void setOsgiHost(OsgiHost osgiHost) {
+ this.osgiHost = osgiHost;
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/DelegatingObjectFactory.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/DelegatingObjectFactory.java
new file mode 100644
index 000000000..d71686082
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/DelegatingObjectFactory.java
@@ -0,0 +1,91 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.util.Map;
+
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.config.PackageProvider;
+import com.opensymphony.xwork2.inject.Container;
+import com.opensymphony.xwork2.inject.Inject;
+import org.apache.struts2.util.ObjectFactoryDestroyable;
+
+public class DelegatingObjectFactory extends ObjectFactory implements ObjectFactoryDestroyable {
+ private ObjectFactory delegateObjectFactory;
+ private BundleAccessor bundleResourceLoader;
+ private OsgiConfigurationProvider osgiConfigurationProvider;
+
+ @Inject
+ public void setDelegateObjectFactory(@Inject Container container,
+ @Inject("struts.objectFactory.delegate") String delegate) {
+ if (delegate == null) {
+ delegate = "struts";
+ }
+ delegateObjectFactory = container.getInstance(ObjectFactory.class, delegate);
+ }
+
+ @Inject
+ public void setBundleResourceLoader(BundleAccessor rl) {
+ this.bundleResourceLoader = rl;
+ }
+
+
+ public boolean isNoArgConstructorRequired() {
+ return delegateObjectFactory.isNoArgConstructorRequired();
+ }
+
+ public Object buildBean(Class clazz, Map extraContext) throws Exception {
+ return delegateObjectFactory.buildBean(clazz, extraContext);
+ }
+
+ public Object buildBean(String className, Map extraContext, boolean injectInternal) throws Exception {
+ try {
+ return delegateObjectFactory.buildBean(className, extraContext, injectInternal);
+ } catch (Exception e) {
+ Object object = bundleResourceLoader.loadClass(className).newInstance();
+ if (injectInternal)
+ injectInternalBeans(object);
+ return object;
+ }
+ }
+
+ @Override
+ public Class getClassInstance(String className) throws ClassNotFoundException {
+ try {
+ return delegateObjectFactory.getClassInstance(className);
+ }
+ catch (Exception e) {
+ return bundleResourceLoader.loadClass(className);
+ }
+ }
+
+ public void destroy() {
+ if (osgiConfigurationProvider != null) {
+ osgiConfigurationProvider.destroy();
+ }
+ }
+
+ @Inject("osgi")
+ public void setOsgiConfigurationProvider(PackageProvider osgiConfigurationProvider) {
+ this.osgiConfigurationProvider = (OsgiConfigurationProvider) osgiConfigurationProvider;
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/FelixOsgiHost.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/FelixOsgiHost.java
new file mode 100644
index 000000000..532053485
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/FelixOsgiHost.java
@@ -0,0 +1,376 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.URLUtil;
+import com.opensymphony.xwork2.util.finder.ResourceFinder;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import com.opensymphony.xwork2.ActionContext;
+import org.apache.commons.lang.xwork.StringUtils;
+import org.apache.felix.framework.Felix;
+import org.apache.felix.framework.util.FelixConstants;
+import org.apache.felix.main.AutoActivator;
+import org.apache.felix.main.Main;
+import org.apache.felix.shell.ShellService;
+import org.apache.struts2.StrutsStatics;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.Constants;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.BundleEvent;
+import org.osgi.util.tracker.ServiceTracker;
+
+import javax.servlet.ServletContext;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.net.URL;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Apache felix implementation of an OsgiHost
+ * See http://felix.apache.org/site/apache-felix-framework-launching-and-embedding.html
+ *
+ * Servlet config params:
+ *
struts.osgi.clearBundleCache: Defaults to "true" delete installed bundles when the comntainer starts
+ *
struts.osgi.logLevel: Defaults to "1". Felix log level. 1 = error, 2 = warning, 3 = information, and 4 = debug
+ *
struts.osgi.runLevel: Defaults to "3". Run level to start the container.
+ */
+public class FelixOsgiHost implements OsgiHost {
+ private static final Logger LOG = LoggerFactory.getLogger(FelixOsgiHost.class);
+
+ private Felix felix;
+ private static final Pattern versionPattern = Pattern.compile("([\\d])+[\\.-]");
+ private ServletContext servletContext;
+
+ protected void startFelix() {
+ //load properties from felix embedded file
+ Properties configProps = getProperties("default.properties");
+
+ // Copy framework properties from the system properties.
+ Main.copySystemProperties(configProps);
+ replaceSystemPackages(configProps);
+
+ //struts, xwork and felix exported packages
+ Properties strutsConfigProps = getProperties("struts-osgi.properties");
+ addExportedPackages(strutsConfigProps, configProps);
+
+ //find bundles and adde em to autostart property
+ addAutoStartBundles(configProps);
+
+ // Bundle cache
+ String storageDir = System.getProperty("java.io.tmpdir") + ".felix-cache";
+ configProps.setProperty(Constants.FRAMEWORK_STORAGE, storageDir);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Storing bundles at [#0]", storageDir);
+
+ String cleanBundleCache = getServletContextParam("struts.osgi.clearBundleCache", "true");
+ if ("true".equalsIgnoreCase(cleanBundleCache)) {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Clearing bundle cache");
+ configProps.put(FelixConstants.FRAMEWORK_STORAGE_CLEAN, FelixConstants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
+ }
+
+ //other properties
+ configProps.put(FelixConstants.SERVICE_URLHANDLERS_PROP, "false");
+ configProps.put(FelixConstants.LOG_LEVEL_PROP, getServletContextParam("struts.osgi.logLevel", "1"));
+ configProps.put(FelixConstants.BUNDLE_CLASSPATH, ".");
+ configProps.put(FelixConstants.FRAMEWORK_BEGINNING_STARTLEVEL, getServletContextParam("struts.osgi.runLevel", "3"));
+
+ try {
+ List list = new ArrayList();
+ list.add(new AutoActivator(configProps));
+ configProps.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, list);
+
+ felix = new Felix(configProps);
+ felix.start();
+ if (LOG.isTraceEnabled())
+ LOG.trace("Apache Felix is running");
+ }
+ catch (Exception ex) {
+ throw new ConfigurationException("Couldn't start Apache Felix", ex);
+ }
+
+ addSpringOSGiSupport();
+
+ //add the bundle context to the ServletContext
+ servletContext.setAttribute(OSGI_BUNDLE_CONTEXT, felix.getBundleContext());
+ }
+
+ /**
+ * Gets a param from the ServletContext, returning the default value if the param is not set
+ *
+ * @param paramName the name of the param to get from the ServletContext
+ * @param defaultValue value to return if the param is not set
+ * @return
+ */
+ private String getServletContextParam(String paramName, String defaultValue) {
+ return StringUtils.defaultString(this.servletContext.getInitParameter(paramName), defaultValue);
+ }
+
+ protected int addAutoStartBundles(Properties configProps) {
+ //starts system bundles in level 1
+ List bundleJarsLevel1 = new ArrayList();
+ bundleJarsLevel1.add(getJarUrl(ShellService.class));
+ bundleJarsLevel1.add(getJarUrl(ServiceTracker.class));
+
+ //add third party bundles in level 2
+ List bundleJarsLevel2 = new ArrayList();
+ bundleJarsLevel2.addAll(getBundlesInDir("bundles/other"));
+
+ //start app bundles in level 3
+ List bundleJarsLevel3 = new ArrayList();
+ bundleJarsLevel3.addAll(getBundlesInDir("bundles"));
+
+
+ configProps.put(AutoActivator.AUTO_START_PROP + ".1", StringUtils.join(bundleJarsLevel1, " "));
+ configProps.put(AutoActivator.AUTO_START_PROP + ".2", StringUtils.join(bundleJarsLevel2, " "));
+ configProps.put(AutoActivator.AUTO_START_PROP + ".3", StringUtils.join(bundleJarsLevel3, " "));
+
+
+ return bundleJarsLevel1.size() + bundleJarsLevel2.size() + bundleJarsLevel3.size();
+ }
+
+ protected List getBundlesInDir(String dir) {
+ List bundleJars = new ArrayList();
+ try {
+
+ ResourceFinder finder = new ResourceFinder();
+ URL url = finder.find(dir);
+ if (url != null) {
+ if ("file".equals(url.getProtocol())) {
+ File bundlerDir = new File(url.toURI());
+ File[] bundles = bundlerDir.listFiles(new FilenameFilter() {
+ public boolean accept(File file, String name) {
+ return StringUtils.endsWith(name, ".jar");
+ }
+ });
+
+ if (bundles != null && bundles.length > 0) {
+ //add all the bundles to the list
+ for (File bundle : bundles) {
+ String externalForm = bundle.toURI().toURL().toExternalForm();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Adding bundle [#0]", externalForm);
+ bundleJars.add(externalForm);
+ }
+
+ } else if (LOG.isDebugEnabled()) {
+ LOG.debug("No bundles found under the [#0] directory", dir);
+ }
+ } else if (LOG.isWarnEnabled())
+ LOG.warn("Unable to read [#0] directory", dir);
+ } else if (LOG.isWarnEnabled())
+ LOG.warn("The [#0] directory was not found", dir);
+ } catch (Exception e) {
+ if (LOG.isWarnEnabled())
+ LOG.warn("Unable load bundles from the [#0] directory", e, dir);
+ }
+ return bundleJars;
+ }
+
+ protected void addSpringOSGiSupport() {
+ // see the javadoc for org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext for more details
+ // OsgiBundleXmlWebApplicationContext expects the the BundleContext to be set in the ServletContext under the attribute
+ // OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE
+ try {
+ Class clazz = Class.forName("org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext");
+ String key = (String) clazz.getDeclaredField("BUNDLE_CONTEXT_ATTRIBUTE").get(null);
+ servletContext.setAttribute(key, felix.getBundleContext());
+ } catch (ClassNotFoundException e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Spring OSGi support is not enabled");
+ }
+ } catch (Exception e) {
+ if (LOG.isErrorEnabled()) {
+ LOG.error("The API of Spring OSGi has changed and the field [#0] is no longer available. The OSGi plugin needs to be updated", e,
+ "org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE");
+ }
+ }
+ }
+
+ protected String getJarUrl(Class clazz) {
+ ProtectionDomain protectionDomain = clazz.getProtectionDomain();
+ CodeSource codeSource = protectionDomain.getCodeSource();
+ URL loc = codeSource.getLocation();
+ return loc.toString();
+ }
+
+ protected void replaceSystemPackages(Properties properties) {
+ //Felix has a way to load the config file and substitution expressions
+ //but the method does not have a way to specify the file (other than in an env variable)
+
+ //${jre-${java.specification.version}}
+ String systemPackages = (String) properties.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
+ String jreVersion = "jre-" + System.getProperty("java.version").substring(0, 3);
+ systemPackages = systemPackages.replace("${jre-${java.specification.version}}", (String) properties.get(jreVersion));
+ properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
+ }
+
+ /*
+ Find subpackages of the packages defined in the property file and export them
+ */
+ protected void addExportedPackages(Properties strutsConfigProps, Properties configProps) {
+ String[] rootPackages = StringUtils.split((String) strutsConfigProps.get("scanning.package.includes"), ",");
+ ResourceFinder finder = new ResourceFinder(StringUtils.EMPTY);
+ List exportedPackages = new ArrayList();
+ //build a list of subpackages
+ for (String rootPackage : rootPackages) {
+ try {
+ Map> subpackagesMap = finder.findPackagesMap(StringUtils.replace(rootPackage.trim(), ".", "/"));
+ for (Map.Entry> entry : subpackagesMap.entrySet()) {
+ URL url = entry.getKey();
+ Set packages = entry.getValue();
+ String version = getVersion(url);
+
+ if (packages != null) {
+ for (String subpackage : packages) {
+ exportedPackages.add(subpackage + "; version=" + version);
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Unable to find subpackages of [#0]", e, rootPackage);
+ }
+ }
+
+ //make a string with the exported packages and add it to the system properties
+ if (!exportedPackages.isEmpty()) {
+ String systemPackages = (String) configProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
+ systemPackages = StringUtils.chomp(systemPackages, ",") + "," + StringUtils.join(exportedPackages, ",");
+ configProps.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
+ }
+ }
+
+ /**
+ * Gets the version used to export the packages. it tries to get it from MANIFEST.MF, or the file name
+ */
+ protected String getVersion(URL url) {
+ if ("jar".equals(url.getProtocol())) {
+ try {
+ JarFile jarFile = new JarFile(new File(URLUtil.normalizeToFileProtocol(url).toURI()));
+ Manifest manifest = jarFile.getManifest();
+ if (manifest != null) {
+ String version = manifest.getMainAttributes().getValue("Bundle-Version");
+ if (StringUtils.isNotBlank(version)) {
+ return getVersionFromString(version);
+ }
+ } else {
+ //try to get the version from the file name
+ return getVersionFromString(jarFile.getName());
+ }
+ } catch (Exception e) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Unable to extract version from [#0], defaulting to '1.0.0'", url.toExternalForm());
+
+ }
+ }
+
+ return "1.0.0";
+ }
+
+ /**
+ * Extracts numbers followed by "." or "-" from the string and joins them with "."
+ */
+ protected static String getVersionFromString(String str) {
+ Matcher matcher = versionPattern.matcher(str);
+ List parts = new ArrayList();
+ while (matcher.find()) {
+ parts.add(matcher.group(1));
+ }
+
+ //default
+ if (parts.size() == 0)
+ return "1.0.0";
+
+ while (parts.size() < 3)
+ parts.add("0");
+
+ return StringUtils.join(parts, ".");
+ }
+
+ protected Properties getProperties(String fileName) {
+ ResourceFinder finder = new ResourceFinder("");
+ try {
+ return finder.findProperties(fileName);
+ } catch (IOException e) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Unable to read property file [#]", fileName);
+ return new Properties();
+ }
+ }
+
+ /**
+ * This bundle map will not change, but the status of the bundles can change over time.
+ * Use getActiveBundles() for active bundles
+ */
+ public Map getBundles() {
+ Map bundles = new HashMap();
+ for (Bundle bundle : felix.getBundleContext().getBundles()) {
+ bundles.put(bundle.getSymbolicName(), bundle);
+ }
+
+ return Collections.unmodifiableMap(bundles);
+ }
+
+ public Map getActiveBundles() {
+ Map bundles = new HashMap();
+ for (Bundle bundle : felix.getBundleContext().getBundles()) {
+ if (bundle.getState() == Bundle.ACTIVE)
+ bundles.put(bundle.getSymbolicName(), bundle);
+ }
+
+ return Collections.unmodifiableMap(bundles);
+ }
+
+ public BundleContext getBundleContext() {
+ return felix.getBundleContext();
+ }
+
+ public void destroy() throws Exception {
+ felix.stop();
+ if (LOG.isTraceEnabled())
+ LOG.trace("Apache Felix has stopped");
+ }
+
+ public void init(ServletContext servletContext) {
+ this.servletContext = servletContext;
+ startFelix();
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiConfigurationProvider.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiConfigurationProvider.java
new file mode 100644
index 000000000..13b000adf
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiConfigurationProvider.java
@@ -0,0 +1,245 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.config.Configuration;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.PackageProvider;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.inject.Container;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import org.apache.struts2.osgi.loaders.VelocityBundleResourceLoader;
+import org.apache.struts2.views.velocity.VelocityManager;
+import org.apache.velocity.app.Velocity;
+import org.apache.commons.lang.xwork.StringUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.BundleEvent;
+
+import javax.servlet.ServletContext;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * Struts package provider that starts the OSGi container and deelgates package loading
+ */
+public class OsgiConfigurationProvider implements PackageProvider, BundleListener {
+ private static final Logger LOG = LoggerFactory.getLogger(OsgiConfigurationProvider.class);
+
+ private Configuration configuration;
+ private ObjectFactory objectFactory;
+
+ private OsgiHost osgiHost;
+ private BundleContext bundleContext;
+ private BundleAccessor bundleAccessor;
+ private boolean bundlesChanged = false;
+ private ServletContext servletContext;
+
+ public void init(Configuration configuration) throws ConfigurationException {
+ osgiHost = (OsgiHost) servletContext.getAttribute(StrutsOsgiListener.OSGI_HOST);
+ bundleContext = osgiHost.getBundleContext();
+ bundleAccessor.setBundleContext(bundleContext);
+ bundleAccessor.setOsgiHost(osgiHost);
+ this.configuration = configuration;
+ }
+
+ public synchronized void loadPackages() throws ConfigurationException {
+ if (LOG.isTraceEnabled())
+ LOG.trace("Loading packages from XML and Convention on startup");
+
+ //init action contect
+ ActionContext ctx = ActionContext.getContext();
+ if (ctx == null) {
+ ctx = new ActionContext(new HashMap());
+ ActionContext.setContext(ctx);
+ }
+
+ Set bundleNames = new HashSet();
+
+ //iterate over the bundles and load packages from them
+ for (Bundle bundle : osgiHost.getBundles().values()) {
+ String bundleName = bundle.getSymbolicName();
+ if (shouldProcessBundle(bundle) && !bundleNames.contains(bundleName)) {
+ bundleNames.add(bundleName);
+ //load XML and COnvention config
+ loadConfigFromBundle(bundle);
+ }
+ }
+
+ bundlesChanged = false;
+ bundleContext.addBundleListener(this);
+ }
+
+ /**
+ * Loads XML config as well as Convention config from a bundle
+ * Limitation: Constants and Beans are ignored on XML config
+ */
+ protected void loadConfigFromBundle(Bundle bundle) {
+ String bundleName = bundle.getSymbolicName();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Loading packages from bundle [#0]", bundleName);
+
+ //init action context
+ ActionContext ctx = ActionContext.getContext();
+ if (ctx == null) {
+ ctx = new ActionContext(new HashMap());
+ ActionContext.setContext(ctx);
+ }
+
+ try {
+ //the Convention plugin will use BundleClassLoaderInterface from the ActionContext to find resources
+ //and load classes
+ ctx.put(ClassLoaderInterface.CLASS_LOADER_INTERFACE, new BundleClassLoaderInterface());
+ ctx.put(BundleAccessor.CURRENT_BUNDLE_NAME, bundleName);
+
+ if (LOG.isTraceEnabled())
+ LOG.trace("Loading XML config from bundle [#0]", bundleName);
+
+ //XML config
+ PackageLoader loader = new BundlePackageLoader();
+ for (PackageConfig pkg : loader.loadPackages(bundle, bundleContext, objectFactory, configuration.getPackageConfigs())) {
+ configuration.addPackageConfig(pkg.getName(), pkg);
+ bundleAccessor.addPackageFromBundle(bundle, pkg.getName());
+ }
+
+ //Convention
+ //get the existing packages before reloading the provider (se we can figure out what are the new packages)
+ Set packagesBeforeLoading = new HashSet(configuration.getPackageConfigNames());
+
+ PackageProvider conventionPackageProvider = configuration.getContainer().getInstance(PackageProvider.class, "convention.packageProvider");
+ if (conventionPackageProvider != null) {
+ if (LOG.isTraceEnabled())
+ LOG.trace("Loading Convention config from bundle [#0]", bundleName);
+ conventionPackageProvider.loadPackages();
+ }
+
+ Set packagesAfterLoading = new HashSet(configuration.getPackageConfigNames());
+ packagesAfterLoading.removeAll(packagesBeforeLoading);
+ if (!packagesAfterLoading.isEmpty()) {
+ //add the new packages to the map of bundle -> package
+ for (String packageName : packagesAfterLoading)
+ bundleAccessor.addPackageFromBundle(bundle, packageName);
+ }
+
+ if (this.configuration.getRuntimeConfiguration() != null) {
+ //if there is a runtime config, it meas that this method was called froma bundle start event
+ //instead of the initial load, in that case, reload the config
+ this.configuration.rebuildRuntimeConfiguration();
+ }
+ } finally {
+ ctx.put(BundleAccessor.CURRENT_BUNDLE_NAME, null);
+ ctx.put(ClassLoaderInterface.CLASS_LOADER_INTERFACE, null);
+ }
+ }
+
+ /**
+ * Checks for "Struts2-Enabled" header in the bundle
+ */
+ protected boolean shouldProcessBundle(Bundle bundle) {
+ String strutsEnabled = (String) bundle.getHeaders().get(OsgiHost.OSGI_HEADER_STRUTS_ENABLED);
+
+ return "true".equalsIgnoreCase(strutsEnabled);
+ }
+
+ public synchronized boolean needsReload() {
+ return bundlesChanged;
+ }
+
+ @Inject
+ public void setObjectFactory(ObjectFactory factory) {
+ this.objectFactory = factory;
+ }
+
+ @Inject
+ public void setBundleAccessor(BundleAccessor acc) {
+ this.bundleAccessor = acc;
+ }
+
+ @Inject
+ public void setVelocityManager(VelocityManager vm) {
+ Properties props = new Properties();
+ props.setProperty("osgi.resource.loader.description", "OSGI bundle loader");
+ props.setProperty("osgi.resource.loader.class", VelocityBundleResourceLoader.class.getName());
+ props.setProperty(Velocity.RESOURCE_LOADER, "strutsfile,strutsclass,osgi");
+ vm.setVelocityProperties(props);
+ }
+
+ @Inject
+ public void setServletContext(ServletContext servletContext) {
+ this.servletContext = servletContext;
+ }
+
+ public void destroy() {
+ try {
+ osgiHost.destroy();
+ } catch (Exception e) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Failed to stop OSGi container", e);
+ }
+ }
+
+ /**
+ * Listens to bundle event to load/unload config
+ */
+ public void bundleChanged(BundleEvent bundleEvent) {
+ Bundle bundle = bundleEvent.getBundle();
+ String bundleName = bundle.getSymbolicName();
+ if (bundleName != null && shouldProcessBundle(bundle)) {
+ switch (bundleEvent.getType()) {
+ case BundleEvent.STARTED:
+ if (LOG.isTraceEnabled())
+ LOG.trace("The bundlde [#0] has been activated and will be scanned for struts configuration", bundleName);
+ loadConfigFromBundle(bundle);
+ break;
+ case BundleEvent.STOPPED:
+ onBundleStopped(bundle);
+ break;
+ }
+ }
+ }
+
+ /**
+ * This method is called when a bundle is stopped, so the config that is related to it is removed
+ *
+ * @param bundle the bundle that stopped
+ */
+ protected void onBundleStopped(Bundle bundle) {
+ Set packages = bundleAccessor.getPackagesByBundle(bundle);
+ if (!packages.isEmpty()) {
+ if (LOG.isTraceEnabled())
+ LOG.trace("The bundle [#0] has been stopped. The packages [#1] will be disabled", bundle.getSymbolicName(), StringUtils.join(packages, ","));
+ for (String packageName : packages)
+ configuration.removePackageConfig(packageName);
+ }
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiHost.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiHost.java
new file mode 100644
index 000000000..edda076c6
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiHost.java
@@ -0,0 +1,45 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import javax.servlet.ServletContext;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementations of this class start an OSGi container. They must also add the BundleContext to
+ * the ServletContext under the key OsgiHost.OSGI_BUNDLE_CONTEXT;
+ */
+public interface OsgiHost {
+ String OSGI_BUNDLE_CONTEXT = "__struts_osgi_bundle_context";
+ String OSGI_HEADER_STRUTS_ENABLED = "Struts2-Enabled";
+
+ void destroy() throws Exception;
+ void init(ServletContext servletContext);
+ Map getBundles();
+ Map getActiveBundles();
+ BundleContext getBundleContext();
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiUtil.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiUtil.java
new file mode 100644
index 000000000..53aa03acd
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/OsgiUtil.java
@@ -0,0 +1,86 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.MalformedURLException;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Bundle;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+public class OsgiUtil {
+ private static final Logger LOG = LoggerFactory.getLogger(OsgiUtil.class);
+
+ /**
+ * A bundle is a jar, and a bunble URL will be useless to clients, this method translates
+ * a URL to a resource inside a bundle from "bundle:something/path" to "jar:file:bundlelocation!/path"
+ */
+ public static URL translateBundleURLToJarURL(URL bundleUrl, Bundle bundle) throws MalformedURLException {
+ if (bundleUrl != null && "bundle".equalsIgnoreCase(bundleUrl.getProtocol())) {
+ StringBuilder sb = new StringBuilder("jar:");
+ sb.append(bundle.getLocation());
+ sb.append("!");
+ sb.append(bundleUrl.getFile());
+ return new URL(sb.toString());
+ }
+
+ return bundleUrl;
+ }
+
+ /**
+ * Calls getBean() on the passed object using refelection. Used on Spring context
+ * because they are loaded from bundles (in anothe class loader)
+ */
+ public static Object getBean(Object beanFactory, String beanId) {
+ try {
+ Method getBeanMethod = beanFactory.getClass().getMethod("getBean", String.class);
+ return getBeanMethod.invoke(beanFactory, beanId);
+ } catch (Exception ex) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Unable to call getBean() on object of type [#0], with bean id [#1]", ex, beanFactory.getClass().getName(), beanId);
+ }
+
+ return null;
+ }
+
+ /**
+ * Calls containsBean on the passed object using refelection. Used on Spring context
+ * because they are loaded from bundles (in anothe class loader)
+ */
+ public static boolean containsBean(Object beanFactory, String beanId) {
+ try {
+ Method getBeanMethod = beanFactory.getClass().getMethod("containsBean", String.class);
+ return (Boolean) getBeanMethod.invoke(beanFactory, beanId);
+ } catch (Exception ex) {
+ if (LOG.isErrorEnabled())
+ LOG.error("Unable to call containsBean() on object of type [#0], with bean id [#1]", ex, beanFactory.getClass().getName(), beanId);
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/PackageLoader.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/PackageLoader.java
new file mode 100644
index 000000000..e1d91b40f
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/PackageLoader.java
@@ -0,0 +1,40 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import java.util.List;
+import java.util.Map;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.PackageProvider;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+
+/**
+ * Implementations of this interface can load packages from a Bundle
+ */
+public interface PackageLoader {
+ List loadPackages(Bundle bundle, BundleContext bundleContext, ObjectFactory objectFactory, Map map) throws ConfigurationException;
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/SpringOsgiObjectFactory.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/SpringOsgiObjectFactory.java
new file mode 100644
index 000000000..4e595764e
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/SpringOsgiObjectFactory.java
@@ -0,0 +1,91 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.util.ClassLoaderUtil;
+import com.opensymphony.xwork2.inject.Inject;
+import org.osgi.framework.ServiceReference;
+
+import java.util.Map;
+
+/**
+ * This Object factory uses the ActionContext(s) published by Spring OSGi
+ * to lookup beans
+ */
+public class SpringOsgiObjectFactory extends ObjectFactory {
+ private final static String SPRING_SERVICE_NAME = "org.springframework.context.ApplicationContext";
+
+ private BundleAccessor bundleAccessor;
+
+ public Object buildBean(String className, Map extraContext, boolean injectInternal) throws Exception {
+ if (containsBean(className))
+ return getBean(className);
+ else {
+ Class clazz = ClassLoaderUtil.loadClass(className, SpringOsgiObjectFactory.class);
+ Object object = clazz.newInstance();
+ if (injectInternal)
+ injectInternalBeans(object);
+ return object;
+ }
+
+ }
+
+ public Object buildBean(Class clazz, Map extraContext) throws Exception {
+ return clazz.newInstance();
+ }
+
+ public Class getClassInstance(String className) throws ClassNotFoundException {
+ return containsBean(className) ? getBean(className).getClass() : ClassLoaderUtil.loadClass(className, SpringOsgiObjectFactory.class);
+ }
+
+ protected Object getBean(String beanName) {
+ ServiceReference[] refs = bundleAccessor.getAllServiceReferences(SPRING_SERVICE_NAME);
+ if (refs != null) {
+ for (ServiceReference ref : refs) {
+ Object context = bundleAccessor.getService(ref);
+ if (OsgiUtil.containsBean(context, beanName))
+ return OsgiUtil.getBean(context, beanName);
+ }
+ }
+
+ return null;
+ }
+
+ protected boolean containsBean(String beanName) {
+ ServiceReference[] refs = bundleAccessor.getAllServiceReferences(SPRING_SERVICE_NAME);
+ if (refs != null) {
+ for (ServiceReference ref : refs) {
+ Object context = bundleAccessor.getService(ref);
+ if (OsgiUtil.containsBean(context, beanName))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Inject
+ public void setBundleAccessor(BundleAccessor bundleAccessor) {
+ this.bundleAccessor = bundleAccessor;
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/StrutsOsgiListener.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/StrutsOsgiListener.java
new file mode 100644
index 000000000..ec1c322ec
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/StrutsOsgiListener.java
@@ -0,0 +1,34 @@
+package org.apache.struts2.osgi;
+
+import org.apache.struts2.StrutsException;
+
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContext;
+
+/**
+ * ServletContextListener that starts Apache Felix
+ */
+public class StrutsOsgiListener implements ServletContextListener {
+ public static final String OSGI_HOST = "__struts_osgi_host";
+ private FelixOsgiHost osgiHost;
+
+ public void contextInitialized(ServletContextEvent sce) {
+ ServletContext servletContext = sce.getServletContext();
+ osgiHost = new FelixOsgiHost();
+ servletContext.setAttribute(OSGI_HOST, osgiHost);
+ try {
+ osgiHost.init(servletContext);
+ } catch (Exception e) {
+ throw new StrutsException("Apache Felix failed to start", e);
+ }
+ }
+
+ public void contextDestroyed(ServletContextEvent sce) {
+ try {
+ osgiHost.destroy();
+ } catch (Exception e) {
+ throw new StrutsException("Apache Felix failed to stop", e);
+ }
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/BundleContextAware.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/BundleContextAware.java
new file mode 100644
index 000000000..2ea2a7ab8
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/BundleContextAware.java
@@ -0,0 +1,31 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi.interceptor;
+
+import org.osgi.framework.BundleContext;
+
+/**
+ * Actions implementing this interface will receive an instance of the BundleContext,
+ * the OsgiInterceptor must be applied to the action.
+ */
+public interface BundleContextAware {
+ void setBundleContext(BundleContext bundleContext);
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/OsgiInterceptor.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/OsgiInterceptor.java
new file mode 100644
index 000000000..7589498ed
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/OsgiInterceptor.java
@@ -0,0 +1,99 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi.interceptor;
+
+import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import com.opensymphony.xwork2.inject.Inject;
+
+import javax.servlet.ServletContext;
+
+import org.apache.struts2.osgi.OsgiHost;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import java.lang.reflect.Type;
+import java.lang.reflect.ParameterizedType;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * If a class implements BundleContextAware, this interceptor will call the setBundleContext(BundleContext)
+ * method on it. If a class implements ServiceAware, this interceptor will call setService(List)
+ */
+public class OsgiInterceptor extends AbstractInterceptor {
+ private static final Logger LOG = LoggerFactory.getLogger(OsgiInterceptor.class);
+
+ private BundleContext bundleContext;
+
+ public String intercept(ActionInvocation invocation) throws Exception {
+ if (bundleContext != null) {
+ Object action = invocation.getAction();
+
+ //inject BundleContext
+ if (action instanceof BundleContextAware)
+ ((BundleContextAware)action).setBundleContext(bundleContext);
+
+ //inject service implementations
+ if (action instanceof ServiceAware) {
+ Type[] types = action.getClass().getGenericInterfaces();
+ if (types != null) {
+ for (Type type : types) {
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ if (parameterizedType.getRawType() instanceof Class) {
+ Class clazz = (Class) parameterizedType.getRawType();
+ if (ServiceAware.class.equals(clazz)) {
+ Class serviceClass = (Class) parameterizedType.getActualTypeArguments()[0];
+ ServiceReference[] refs = bundleContext.getAllServiceReferences(serviceClass.getName(), null);
+ //get the services
+ if (refs != null) {
+ List services = new ArrayList(refs.length);
+ for (ServiceReference ref : refs) {
+ Object service = bundleContext.getService(ref);
+ //wow, that's a lot of nested ifs
+ if (service != null)
+ services.add(service);
+ }
+
+ if (!services.isEmpty())
+ ((ServiceAware)action).setServices(services);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ } else if (LOG.isWarnEnabled()){
+ LOG.warn("The OSGi interceptor was not able to find the BundleContext in the ServletContext");
+ }
+
+ return invocation.invoke();
+ }
+
+ @Inject
+ public void setServletContext(ServletContext servletContext) {
+ this.bundleContext = (BundleContext) servletContext.getAttribute(OsgiHost.OSGI_BUNDLE_CONTEXT);
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/ServiceAware.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/ServiceAware.java
new file mode 100644
index 000000000..e7a549ea6
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/interceptor/ServiceAware.java
@@ -0,0 +1,32 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi.interceptor;
+
+import java.util.List;
+
+/**
+ * Classes implementing this interface, will be injected a list of services
+ * registered with the type of the parameterized type
+ * @param The type of the service
+ */
+public interface ServiceAware {
+ void setServices(List services);
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/FreeMarkerBundleResourceLoader.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/FreeMarkerBundleResourceLoader.java
new file mode 100644
index 000000000..3638cd04d
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/FreeMarkerBundleResourceLoader.java
@@ -0,0 +1,40 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi.loaders;
+
+import java.net.URL;
+
+import org.apache.struts2.osgi.DefaultBundleAccessor;
+
+import freemarker.cache.URLTemplateLoader;
+
+/**
+ * Finds FreeMarker templates in bundles
+ */
+public class FreeMarkerBundleResourceLoader extends URLTemplateLoader {
+
+ @Override
+ protected URL getURL(String name) {
+ return DefaultBundleAccessor.getInstance().loadResource(name);
+ }
+
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/StaticContentBundleResourceLoader.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/StaticContentBundleResourceLoader.java
new file mode 100644
index 000000000..8cf11f58f
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/StaticContentBundleResourceLoader.java
@@ -0,0 +1,48 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi.loaders;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.struts2.dispatcher.DefaultStaticContentLoader;
+import org.apache.struts2.osgi.DefaultBundleAccessor;
+import org.apache.struts2.osgi.BundleAccessor;
+import com.opensymphony.xwork2.inject.Inject;
+
+/**
+ * Loads static resources from bundles
+ *
+ */
+public class StaticContentBundleResourceLoader extends DefaultStaticContentLoader {
+ private BundleAccessor bundleAccessor;
+
+ protected URL findResource(String path) throws IOException {
+ return bundleAccessor.loadResourceFromAllBundles(path);
+ }
+
+ @Inject
+ public void setBundleAccessor(BundleAccessor bundleAccessor) {
+ this.bundleAccessor = bundleAccessor;
+ }
+}
diff --git a/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/VelocityBundleResourceLoader.java b/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/VelocityBundleResourceLoader.java
new file mode 100644
index 000000000..20ddd5313
--- /dev/null
+++ b/plugins/osgi/src/main/java/org/apache/struts2/osgi/loaders/VelocityBundleResourceLoader.java
@@ -0,0 +1,51 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi.loaders;
+
+import java.io.InputStream;
+
+import org.apache.struts2.osgi.DefaultBundleAccessor;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
+
+/**
+ * Finds Velocity templates in bundles
+ */
+public class VelocityBundleResourceLoader extends ClasspathResourceLoader {
+
+ public synchronized InputStream getResourceStream(String name)
+ throws ResourceNotFoundException {
+ if ((name == null) || (name.length() == 0)) {
+ throw new ResourceNotFoundException("No template name provided");
+ }
+
+ if (name.startsWith("/")) {
+ name = name.substring(1);
+ }
+
+ try {
+ return DefaultBundleAccessor.getInstance().loadResourceAsStream(name);
+ } catch (Exception e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+ }
+}
diff --git a/plugins/osgi/src/main/resources/beanRefContext.xml b/plugins/osgi/src/main/resources/beanRefContext.xml
new file mode 100644
index 000000000..4b9b1615d
--- /dev/null
+++ b/plugins/osgi/src/main/resources/beanRefContext.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ classpath*:/spring/*.xml
+
+
+
+
diff --git a/plugins/osgi/src/main/resources/struts-osgi.properties b/plugins/osgi/src/main/resources/struts-osgi.properties
new file mode 100644
index 000000000..f19215128
--- /dev/null
+++ b/plugins/osgi/src/main/resources/struts-osgi.properties
@@ -0,0 +1,25 @@
+# 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.
+
+scanning.package.includes = com.opensymphony.xwork2, \
+ org.apache.commons.lang.xwork, \
+ org.apache.struts2, \
+ ognl, \
+ freemarker, \
+ org.apache.velocity, \
+ javax.servlet
+
diff --git a/plugins/osgi/src/main/resources/struts-plugin.xml b/plugins/osgi/src/main/resources/struts-plugin.xml
new file mode 100644
index 000000000..d628bb8b4
--- /dev/null
+++ b/plugins/osgi/src/main/resources/struts-plugin.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/osgi/src/test/java/org/apache/struts2/osgi/FelixOsgiHostTest.java b/plugins/osgi/src/test/java/org/apache/struts2/osgi/FelixOsgiHostTest.java
new file mode 100644
index 000000000..7024167ed
--- /dev/null
+++ b/plugins/osgi/src/test/java/org/apache/struts2/osgi/FelixOsgiHostTest.java
@@ -0,0 +1,35 @@
+/*
+ * $Id$
+ *
+ * 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.struts2.osgi;
+
+import junit.framework.TestCase;
+
+public class FelixOsgiHostTest extends TestCase {
+ public void testGetVersionFromString() {
+ assertEquals("2.1.1", FelixOsgiHost.getVersionFromString("2.1.1-SNAPSHOT"));
+ assertEquals("2.1.1", FelixOsgiHost.getVersionFromString("2.1.1.SNAPSHOT"));
+ assertEquals("2.1.1", FelixOsgiHost.getVersionFromString("something-2.1.1.SNAPSHOT"));
+ assertEquals("2.1.1", FelixOsgiHost.getVersionFromString("something-2-1-1.SNAPSHOT"));
+ assertEquals("2.1.0", FelixOsgiHost.getVersionFromString("something-2-1.SNAPSHOT"));
+ assertEquals("2.0.0", FelixOsgiHost.getVersionFromString("something-2.SNAPSHOT"));
+ assertEquals("1.0.0", FelixOsgiHost.getVersionFromString("something"));
+ }
+}
diff --git a/plugins/osgi/src/test/java/org/apache/struts2/osgi/interceptor/OsgiInterceptorTest.java b/plugins/osgi/src/test/java/org/apache/struts2/osgi/interceptor/OsgiInterceptorTest.java
new file mode 100644
index 000000000..4cca06a41
--- /dev/null
+++ b/plugins/osgi/src/test/java/org/apache/struts2/osgi/interceptor/OsgiInterceptorTest.java
@@ -0,0 +1,85 @@
+package org.apache.struts2.osgi.interceptor;
+
+import org.easymock.EasyMock;
+import org.apache.struts2.osgi.OsgiHost;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import javax.servlet.ServletContext;
+
+import com.opensymphony.xwork2.ActionInvocation;
+import junit.framework.TestCase;
+
+import java.util.List;
+
+public class OsgiInterceptorTest extends TestCase {
+ public void testBundleContextAware() throws Exception {
+ ServletContext servletContext = EasyMock.createStrictMock(ServletContext.class);
+ BundleContext bundleContext = EasyMock.createStrictMock(BundleContext.class);
+ ActionInvocation actionInvocation = EasyMock.createStrictMock(ActionInvocation.class);
+ BundleContextAware bundleContextAware = EasyMock.createStrictMock(BundleContextAware.class);
+
+ EasyMock.expect(servletContext.getAttribute(OsgiHost.OSGI_BUNDLE_CONTEXT)).andReturn(bundleContext);
+ EasyMock.expect(actionInvocation.getAction()).andReturn(bundleContextAware);
+ bundleContextAware.setBundleContext(bundleContext);
+ EasyMock.expect(actionInvocation.invoke()).andReturn("");
+
+ EasyMock.replay(bundleContextAware);
+ EasyMock.replay(servletContext);
+ EasyMock.replay(actionInvocation);
+
+ OsgiInterceptor osgiInterceptor = new OsgiInterceptor();
+ osgiInterceptor.setServletContext(servletContext);
+ osgiInterceptor.intercept(actionInvocation);
+
+ EasyMock.verify(bundleContextAware);
+ }
+
+ public void testBundleContextAwareNegative() throws Exception {
+ ServletContext servletContext = EasyMock.createStrictMock(ServletContext.class);
+ ActionInvocation actionInvocation = EasyMock.createStrictMock(ActionInvocation.class);
+ BundleContextAware bundleContextAware = EasyMock.createStrictMock(BundleContextAware.class);
+
+ EasyMock.expect(servletContext.getAttribute(OsgiHost.OSGI_BUNDLE_CONTEXT)).andReturn(null);
+ EasyMock.expect(actionInvocation.invoke()).andReturn("");
+
+ EasyMock.replay(bundleContextAware);
+ EasyMock.replay(servletContext);
+ EasyMock.replay(actionInvocation);
+
+ OsgiInterceptor osgiInterceptor = new OsgiInterceptor();
+ osgiInterceptor.setServletContext(servletContext);
+ osgiInterceptor.intercept(actionInvocation);
+
+ EasyMock.verify(bundleContextAware);
+ }
+
+ public void testServiceAware() throws Exception {
+ ServletContext servletContext = EasyMock.createStrictMock(ServletContext.class);
+ BundleContext bundleContext = EasyMock.createStrictMock(BundleContext.class);
+ ActionInvocation actionInvocation = EasyMock.createStrictMock(ActionInvocation.class);
+ SomeAction someAction = new SomeAction();
+
+ //service refs
+ ServiceReference objectRef = EasyMock.createNiceMock(ServiceReference.class);
+ Object someObject = new Object();
+
+ EasyMock.expect(servletContext.getAttribute(OsgiHost.OSGI_BUNDLE_CONTEXT)).andReturn(bundleContext);
+ EasyMock.expect(actionInvocation.getAction()).andReturn(someAction);
+ EasyMock.expect(actionInvocation.invoke()).andReturn("");
+ EasyMock.expect(bundleContext.getAllServiceReferences(Object.class.getName(), null)).andReturn(new ServiceReference[] {objectRef});
+ EasyMock.expect(bundleContext.getService(objectRef)).andReturn(someObject);
+
+ EasyMock.replay(bundleContext);
+ EasyMock.replay(servletContext);
+ EasyMock.replay(actionInvocation);
+
+ OsgiInterceptor osgiInterceptor = new OsgiInterceptor();
+ osgiInterceptor.setServletContext(servletContext);
+ osgiInterceptor.intercept(actionInvocation);
+
+ List