View Javadoc

1   /*
2    * $Id$
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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          //load properties from felix embedded file
84          Properties configProps = getProperties("default.properties");
85  
86          // Copy framework properties from the system properties.
87          Main.copySystemProperties(configProps);
88          replaceSystemPackages(configProps);
89  
90          //struts, xwork and felix exported packages
91          Properties strutsConfigProps = getProperties("struts-osgi.properties");
92          addExportedPackages(strutsConfigProps, configProps);
93  
94          //find bundles and adde em to autostart property
95          addAutoStartBundles(configProps);
96  
97          // Bundle cache
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         //other properties
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         //add the bundle context to the ServletContext
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         //starts system bundles in level 1
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         //get a list of directories under /bundles with numeric names (the runlevel)
155         Map<String, String> runLevels = getRunLevelDirs("bundles");
156         if (runLevels.isEmpty()) {
157             //there are no run level dirs, search for bundles in that dir
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                                 //the name is not a number
188                                 return false;
189                             }
190                         }
191                     });
192 
193                     if (runLevelDirs != null && runLevelDirs.length > 0) {
194                         //add all the dirs to the list
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                         //add all the bundles to the list
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         // see the javadoc for org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext for more details
252         // OsgiBundleXmlWebApplicationContext expects the the BundleContext to be set in the ServletContext under the attribute
253         // OsgiBundleXmlWebApplicationContext.BUNDLE_CONTEXT_ATTRIBUTE
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         //Felix has a way to load the config file and substitution expressions
279         //but the method does not have a way to specify the file (other than in an env variable)
280 
281         //${jre-${java.specification.version}}
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         Find subpackages of the packages defined in the property file and export them
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         //build a list of subpackages
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                     //get version if not set
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         //make a string with the exported packages and add it to the system properties
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                     //try to get the version from the file name
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         //default
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 }