1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.apache.struts2.osgi;
23
24 import com.opensymphony.xwork2.config.ConfigurationException;
25 import com.opensymphony.xwork2.inject.Inject;
26 import com.opensymphony.xwork2.util.URLUtil;
27 import com.opensymphony.xwork2.util.finder.ResourceFinder;
28 import com.opensymphony.xwork2.util.logging.Logger;
29 import com.opensymphony.xwork2.util.logging.LoggerFactory;
30 import com.opensymphony.xwork2.ActionContext;
31 import org.apache.commons.lang.xwork.StringUtils;
32 import org.apache.felix.framework.Felix;
33 import org.apache.felix.framework.util.FelixConstants;
34 import org.apache.felix.main.AutoActivator;
35 import org.apache.felix.main.Main;
36 import org.apache.felix.shell.ShellService;
37 import org.apache.struts2.StrutsStatics;
38 import org.apache.struts2.StrutsException;
39 import org.osgi.framework.Bundle;
40 import org.osgi.framework.BundleActivator;
41 import org.osgi.framework.Constants;
42 import org.osgi.framework.BundleContext;
43 import org.osgi.framework.BundleListener;
44 import org.osgi.framework.BundleEvent;
45 import org.osgi.util.tracker.ServiceTracker;
46
47 import javax.servlet.ServletContext;
48 import java.io.File;
49 import java.io.FilenameFilter;
50 import java.io.IOException;
51 import java.net.URL;
52 import java.security.CodeSource;
53 import java.security.ProtectionDomain;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.Properties;
60 import java.util.Set;
61 import java.util.jar.JarFile;
62 import java.util.jar.Manifest;
63 import java.util.regex.Matcher;
64 import java.util.regex.Pattern;
65
66 /***
67 * Apache felix implementation of an OsgiHost
68 * See http://felix.apache.org/site/apache-felix-framework-launching-and-embedding.html
69 * <br/>
70 * Servlet config params:
71 * <p>struts.osgi.clearBundleCache: Defaults to "true" delete installed bundles when the comntainer starts</p>
72 * <p>struts.osgi.logLevel: Defaults to "1". Felix log level. 1 = error, 2 = warning, 3 = information, and 4 = debug </p>
73 * <p>struts.osgi.runLevel: Defaults to "3". Run level to start the container.</p>
74 */
75 public class FelixOsgiHost implements OsgiHost {
76 private static final Logger LOG = LoggerFactory.getLogger(FelixOsgiHost.class);
77
78 private Felix felix;
79 private static final Pattern versionPattern = Pattern.compile("([//d])+[//.-]");
80 private ServletContext servletContext;
81
82 protected void startFelix() {
83
84 Properties configProps = getProperties("default.properties");
85
86
87 Main.copySystemProperties(configProps);
88 replaceSystemPackages(configProps);
89
90
91 Properties strutsConfigProps = getProperties("struts-osgi.properties");
92 addExportedPackages(strutsConfigProps, configProps);
93
94
95 addAutoStartBundles(configProps);
96
97
98 String storageDir = System.getProperty("java.io.tmpdir") + ".felix-cache";
99 configProps.setProperty(Constants.FRAMEWORK_STORAGE, storageDir);
100 if (LOG.isDebugEnabled())
101 LOG.debug("Storing bundles at [#0]", storageDir);
102
103 String cleanBundleCache = getServletContextParam("struts.osgi.clearBundleCache", "true");
104 if ("true".equalsIgnoreCase(cleanBundleCache)) {
105 if (LOG.isDebugEnabled())
106 LOG.debug("Clearing bundle cache");
107 configProps.put(FelixConstants.FRAMEWORK_STORAGE_CLEAN, FelixConstants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
108 }
109
110
111 configProps.put(FelixConstants.SERVICE_URLHANDLERS_PROP, "false");
112 configProps.put(FelixConstants.LOG_LEVEL_PROP, getServletContextParam("struts.osgi.logLevel", "1"));
113 configProps.put(FelixConstants.BUNDLE_CLASSPATH, ".");
114 configProps.put(FelixConstants.FRAMEWORK_BEGINNING_STARTLEVEL, getServletContextParam("struts.osgi.runLevel", "3"));
115
116 try {
117 List<BundleActivator> list = new ArrayList<BundleActivator>();
118 list.add(new AutoActivator(configProps));
119 configProps.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, list);
120
121 felix = new Felix(configProps);
122 felix.start();
123 if (LOG.isTraceEnabled())
124 LOG.trace("Apache Felix is running");
125 }
126 catch (Exception ex) {
127 throw new ConfigurationException("Couldn't start Apache Felix", ex);
128 }
129
130 addSpringOSGiSupport();
131
132
133 servletContext.setAttribute(OSGI_BUNDLE_CONTEXT, felix.getBundleContext());
134 }
135
136 /***
137 * Gets a param from the ServletContext, returning the default value if the param is not set
138 *
139 * @param paramName the name of the param to get from the ServletContext
140 * @param defaultValue value to return if the param is not set
141 * @return
142 */
143 private String getServletContextParam(String paramName, String defaultValue) {
144 return StringUtils.defaultString(this.servletContext.getInitParameter(paramName), defaultValue);
145 }
146
147 protected void addAutoStartBundles(Properties configProps) {
148
149 List<String> bundleJarsLevel1 = new ArrayList<String>();
150 bundleJarsLevel1.add(getJarUrl(ShellService.class));
151 bundleJarsLevel1.add(getJarUrl(ServiceTracker.class));
152 configProps.put(AutoActivator.AUTO_START_PROP + ".1", StringUtils.join(bundleJarsLevel1, " "));
153
154
155 Map<String, String> runLevels = getRunLevelDirs("bundles");
156 if (runLevels.isEmpty()) {
157
158 List<String> bundles = getBundlesInDir("bundles");
159 if (!bundles.isEmpty())
160 configProps.put(AutoActivator.AUTO_START_PROP + ".2", StringUtils.join(bundles, " "));
161 } else {
162 for (String runLevel : runLevels.keySet()) {
163 if ("1".endsWith(runLevel))
164 throw new StrutsException("Run level dirs must be greater than 1. Run level 1 is reserved for the Felix bundles");
165 List<String> bundles = getBundlesInDir(runLevels.get(runLevel));
166 configProps.put(AutoActivator.AUTO_START_PROP + "." + runLevel, StringUtils.join(bundles, " "));
167 }
168 }
169 }
170
171 /***
172 * Return a list of directories under a directory whose name is a number
173 */
174 protected Map<String, String> getRunLevelDirs(String dir) {
175 Map<String, String> dirs = new HashMap<String, String>();
176 try {
177 ResourceFinder finder = new ResourceFinder();
178 URL url = finder.find("bundles");
179 if (url != null) {
180 if ("file".equals(url.getProtocol())) {
181 File bundlesDir = new File(url.toURI());
182 String[] runLevelDirs = bundlesDir.list(new FilenameFilter() {
183 public boolean accept(File file, String name) {
184 try {
185 return file.isDirectory() && Integer.valueOf(name) > 0;
186 } catch (NumberFormatException ex) {
187
188 return false;
189 }
190 }
191 });
192
193 if (runLevelDirs != null && runLevelDirs.length > 0) {
194
195 for (String runLevel : runLevelDirs)
196 dirs.put(runLevel, StringUtils.chomp(dir, "/") + "/" + runLevel);
197
198 } else if (LOG.isDebugEnabled()) {
199 LOG.debug("No run level directories found under the [#0] directory", dir);
200 }
201 } else if (LOG.isWarnEnabled())
202 LOG.warn("Unable to read [#0] directory", dir);
203 } else if (LOG.isWarnEnabled())
204 LOG.warn("The [#0] directory was not found", dir);
205 } catch (Exception e) {
206 if (LOG.isWarnEnabled())
207 LOG.warn("Unable load bundles from the [#0] directory", e, dir);
208 }
209 return dirs;
210 }
211
212 protected List<String> getBundlesInDir(String dir) {
213 List<String> bundleJars = new ArrayList<String>();
214 try {
215
216 ResourceFinder finder = new ResourceFinder();
217 URL url = finder.find(dir);
218 if (url != null) {
219 if ("file".equals(url.getProtocol())) {
220 File bundlesDir = new File(url.toURI());
221 File[] bundles = bundlesDir.listFiles(new FilenameFilter() {
222 public boolean accept(File file, String name) {
223 return StringUtils.endsWith(name, ".jar");
224 }
225 });
226
227 if (bundles != null && bundles.length > 0) {
228
229 for (File bundle : bundles) {
230 String externalForm = bundle.toURI().toURL().toExternalForm();
231 if (LOG.isDebugEnabled())
232 LOG.debug("Adding bundle [#0]", externalForm);
233 bundleJars.add(externalForm);
234 }
235
236 } else if (LOG.isDebugEnabled()) {
237 LOG.debug("No bundles found under the [#0] directory", dir);
238 }
239 } else if (LOG.isWarnEnabled())
240 LOG.warn("Unable to read [#0] directory", dir);
241 } else if (LOG.isWarnEnabled())
242 LOG.warn("The [#0] directory was not found", dir);
243 } catch (Exception e) {
244 if (LOG.isWarnEnabled())
245 LOG.warn("Unable load bundles from the [#0] directory", e, dir);
246 }
247 return bundleJars;
248 }
249
250 protected void addSpringOSGiSupport() {
251
252
253
254 try {
255 Class clazz = Class.forName("org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext");
256 String key = (String) clazz.getDeclaredField("BUNDLE_CONTEXT_ATTRIBUTE").get(null);
257 servletContext.setAttribute(key, felix.getBundleContext());
258 } catch (ClassNotFoundException e) {
259 if (LOG.isDebugEnabled()) {
260 LOG.debug("Spring OSGi support is not enabled");
261 }
262 } catch (Exception e) {
263 if (LOG.isErrorEnabled()) {
264 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,
265 "org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE");
266 }
267 }
268 }
269
270 protected String getJarUrl(Class clazz) {
271 ProtectionDomain protectionDomain = clazz.getProtectionDomain();
272 CodeSource codeSource = protectionDomain.getCodeSource();
273 URL loc = codeSource.getLocation();
274 return loc.toString();
275 }
276
277 protected void replaceSystemPackages(Properties properties) {
278
279
280
281
282 String systemPackages = (String) properties.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
283 String jreVersion = "jre-" + System.getProperty("java.version").substring(0, 3);
284 systemPackages = systemPackages.replace("${jre-${java.specification.version}}", (String) properties.get(jreVersion));
285 properties.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
286 }
287
288
289
290
291 protected void addExportedPackages(Properties strutsConfigProps, Properties configProps) {
292 String[] rootPackages = StringUtils.split((String) strutsConfigProps.get("scanning.package.includes"), ",");
293 ResourceFinder finder = new ResourceFinder(StringUtils.EMPTY);
294 List<String> exportedPackages = new ArrayList<String>();
295
296 for (String rootPackage : rootPackages) {
297 try {
298 String version = null;
299 if (rootPackage.indexOf(";") > 0) {
300 String[] splitted = rootPackage.split(";");
301 rootPackage = splitted[0];
302 version = splitted[1];
303 }
304 Map<URL, Set<String>> subpackagesMap = finder.findPackagesMap(StringUtils.replace(rootPackage.trim(), ".", "/"));
305 for (Map.Entry<URL, Set<String>> entry : subpackagesMap.entrySet()) {
306 URL url = entry.getKey();
307 Set<String> packages = entry.getValue();
308
309
310 if (StringUtils.isBlank(version))
311 version = getVersion(url);
312
313 if (packages != null) {
314 for (String subpackage : packages) {
315 exportedPackages.add(subpackage + "; version=" + version);
316 }
317 }
318 }
319 } catch (IOException e) {
320 if (LOG.isErrorEnabled())
321 LOG.error("Unable to find subpackages of [#0]", e, rootPackage);
322 }
323 }
324
325
326 if (!exportedPackages.isEmpty()) {
327 String systemPackages = (String) configProps.get(Constants.FRAMEWORK_SYSTEMPACKAGES);
328 systemPackages = StringUtils.chomp(systemPackages, ",") + "," + StringUtils.join(exportedPackages, ",");
329 configProps.put(Constants.FRAMEWORK_SYSTEMPACKAGES, systemPackages);
330 }
331 }
332
333 /***
334 * Gets the version used to export the packages. it tries to get it from MANIFEST.MF, or the file name
335 */
336 protected String getVersion(URL url) {
337 if ("jar".equals(url.getProtocol())) {
338 try {
339 JarFile jarFile = new JarFile(new File(URLUtil.normalizeToFileProtocol(url).toURI()));
340 Manifest manifest = jarFile.getManifest();
341 if (manifest != null) {
342 String version = manifest.getMainAttributes().getValue("Bundle-Version");
343 if (StringUtils.isNotBlank(version)) {
344 return getVersionFromString(version);
345 }
346 } else {
347
348 return getVersionFromString(jarFile.getName());
349 }
350 } catch (Exception e) {
351 if (LOG.isErrorEnabled())
352 LOG.error("Unable to extract version from [#0], defaulting to '1.0.0'", url.toExternalForm());
353
354 }
355 }
356
357 return "1.0.0";
358 }
359
360 /***
361 * Extracts numbers followed by "." or "-" from the string and joins them with "."
362 */
363 protected static String getVersionFromString(String str) {
364 Matcher matcher = versionPattern.matcher(str);
365 List<String> parts = new ArrayList<String>();
366 while (matcher.find()) {
367 parts.add(matcher.group(1));
368 }
369
370
371 if (parts.size() == 0)
372 return "1.0.0";
373
374 while (parts.size() < 3)
375 parts.add("0");
376
377 return StringUtils.join(parts, ".");
378 }
379
380 protected Properties getProperties(String fileName) {
381 ResourceFinder finder = new ResourceFinder("");
382 try {
383 return finder.findProperties(fileName);
384 } catch (IOException e) {
385 if (LOG.isErrorEnabled())
386 LOG.error("Unable to read property file [#]", fileName);
387 return new Properties();
388 }
389 }
390
391 /***
392 * This bundle map will not change, but the status of the bundles can change over time.
393 * Use getActiveBundles() for active bundles
394 */
395 public Map<String, Bundle> getBundles() {
396 Map<String, Bundle> bundles = new HashMap<String, Bundle>();
397 for (Bundle bundle : felix.getBundleContext().getBundles()) {
398 bundles.put(bundle.getSymbolicName(), bundle);
399 }
400
401 return Collections.unmodifiableMap(bundles);
402 }
403
404 public Map<String, Bundle> getActiveBundles() {
405 Map<String, Bundle> bundles = new HashMap<String, Bundle>();
406 for (Bundle bundle : felix.getBundleContext().getBundles()) {
407 if (bundle.getState() == Bundle.ACTIVE)
408 bundles.put(bundle.getSymbolicName(), bundle);
409 }
410
411 return Collections.unmodifiableMap(bundles);
412 }
413
414 public BundleContext getBundleContext() {
415 return felix.getBundleContext();
416 }
417
418 public void destroy() throws Exception {
419 felix.stop();
420 if (LOG.isTraceEnabled())
421 LOG.trace("Apache Felix has stopped");
422 }
423
424 public void init(ServletContext servletContext) {
425 this.servletContext = servletContext;
426 startFelix();
427 }
428 }