View Javadoc

1   /*
2    * Copyright 2006-2008 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.osgi.io;
18  
19  import java.io.File;
20  import java.io.FileNotFoundException;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.net.URLConnection;
26  import java.util.Enumeration;
27  import java.util.LinkedHashSet;
28  import java.util.Set;
29  
30  import org.osgi.framework.Bundle;
31  import org.springframework.core.io.AbstractResource;
32  import org.springframework.core.io.ContextResource;
33  import org.springframework.core.io.Resource;
34  import org.springframework.core.io.ResourceLoader;
35  import org.springframework.osgi.io.internal.OsgiResourceUtils;
36  import org.springframework.util.Assert;
37  import org.springframework.util.ObjectUtils;
38  import org.springframework.util.ResourceUtils;
39  import org.springframework.util.StringUtils;
40  
41  /**
42   * Resource implementation for OSGi environments.
43   * 
44   * <p/> Lazy evaluation of the resource will be used.
45   * 
46   * This implementation allows resource location inside:
47   * 
48   * <ul>
49   * <li><em>bundle space</em> - if <code>osgibundle:</code>/{@link #BUNDLE_URL_PREFIX}
50   * prefix is being used or none is specified. This space cotnains the bundle jar
51   * and its attached fragments.</li>
52   * <li><em>bundle jar</em> - if <code>osgibundlejar:</code>/{@link #BUNDLE_JAR_URL_PREFIX}
53   * is specified. This space contains just the bundle jar.</li>
54   * <li><em>class space</em> - if
55   * {@link org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX} is
56   * encountered. This space contains the bundle classpath, namely the bundle jar,
57   * its attached fragments and imported packages.</li>
58   * </ul>
59   * 
60   * For more explanations on resource locations in OSGi, please see the
61   * <em>Access to Resources</em> chapter from the OSGi spec.
62   * 
63   * <p/> OSGi framework specific prefixes (such as <code>bundleentry:</code>
64   * and <code>bundleresource:</code>under Equinox, <code>bundle:</code>
65   * under Knopflefish and Felix, etc..) are supported. Resources outside the OSGi
66   * space (<code>file:</code>, <code>http:</code>, etc..) are supported as
67   * well as the path is being resolved to an <code>URL</code>.
68   * 
69   * <p/>If no prefix is specified, the <em>bundle space</em> will be used for
70   * locating a resource.
71   * 
72   * <p/> <strong>Note:</strong> When the <em>bundle space</em> (bundle jar and
73   * its attached fragments) is being searched, multiple URLs can be found but
74   * this implementation will return only the first one. Consider using
75   * {@link OsgiBundleResourcePatternResolver} to retrieve all entries.
76   * 
77   * @author Costin Leau
78   * @author Adrian Colyer
79   * @author Sam Brannen
80   */
81  public class OsgiBundleResource extends AbstractResource implements ContextResource {
82  
83  	/**
84  	 * Prefix for searching inside the owning bundle space. This translates to
85  	 * searching the bundle and its attached fragments. If no prefix is
86  	 * specified, this one will be used.
87  	 */
88  	public static final String BUNDLE_URL_PREFIX = "osgibundle:";
89  
90  	/**
91  	 * Prefix for searching only the bundle raw jar. Will ignore attached
92  	 * fragments. Not used at the moment.
93  	 */
94  	public static final String BUNDLE_JAR_URL_PREFIX = "osgibundlejar:";
95  
96  	private static final char PREFIX_SEPARATOR = ':';
97  
98  	private static final String ABSOLUTE_PATH_PREFIX = "/";
99  
100 	private final Bundle bundle;
101 
102 	private final String path;
103 
104 	// used to avoid removing the prefix every time the URL is required
105 	private final String pathWithoutPrefix;
106 
107 	// Bundle resource possible searches
108 	private int searchType = OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED;
109 
110 
111 	/**
112 	 * 
113 	 * Constructs a new <code>OsgiBundleResource</code> instance.
114 	 * 
115 	 * @param bundle OSGi bundle used by this resource
116 	 * @param path resource path inside the bundle.
117 	 */
118 	public OsgiBundleResource(Bundle bundle, String path) {
119 		Assert.notNull(bundle, "Bundle must not be null");
120 		this.bundle = bundle;
121 
122 		// check path
123 		Assert.notNull(path, "Path must not be null");
124 
125 		this.path = StringUtils.cleanPath(path);
126 
127 		this.searchType = OsgiResourceUtils.getSearchType(this.path);
128 
129 		switch (this.searchType) {
130 			case OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED:
131 				pathWithoutPrefix = path;
132 				break;
133 			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_SPACE:
134 				pathWithoutPrefix = path.substring(BUNDLE_URL_PREFIX.length());
135 				break;
136 			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR:
137 				pathWithoutPrefix = path.substring(BUNDLE_JAR_URL_PREFIX.length());
138 				break;
139 			case OsgiResourceUtils.PREFIX_TYPE_CLASS_SPACE:
140 				pathWithoutPrefix = path.substring(ResourceLoader.CLASSPATH_URL_PREFIX.length());
141 				break;
142 			// prefix unknown so the path will be resolved outside the context
143 			default:
144 				pathWithoutPrefix = null;
145 		}
146 	}
147 
148 	/**
149 	 * Returns the path for this resource.
150 	 * 
151 	 * @return this resource path
152 	 */
153 	final String getPath() {
154 		return path;
155 	}
156 
157 	/**
158 	 * Returns the bundle for this resource.
159 	 * 
160 	 * @return the resource bundle
161 	 */
162 	final Bundle getBundle() {
163 		return bundle;
164 	}
165 
166 	/**
167 	 * Returns an <code>InputStream</code> to this resource. This
168 	 * implementation opens an
169 	 * <code>InputStream<code> for the given <code>URL</code>. It sets the
170 	 * <em>UseCaches</em> flag to <code>false</code>, mainly to avoid jar file
171 	 * locking on Windows.
172 	 * 
173 	 * @return input stream to the underlying resource
174 	 * @throws IOException if the stream could not be opened
175 	 * @see java.net.URL#openConnection()
176 	 * @see java.net.URLConnection#setUseCaches(boolean)
177 	 * @see java.net.URLConnection#getInputStream()
178 	 *
179 	 */
180 	public InputStream getInputStream() throws IOException {
181 		URLConnection con = getURL().openConnection();
182 		con.setUseCaches(false);
183 		return con.getInputStream();
184 	}
185 
186 	/**
187 	 * Locates the resource in the underlying bundle based on the prefix, if it
188 	 * exists. Note that the location happens per call since due to the dynamic
189 	 * nature of OSGi, the classpath of the bundle (among others) can change
190 	 * during a bundle lifecycle (depending on its imports).
191 	 * 
192 	 * @return URL to this resource
193 	 * @throws IOException if the resource cannot be resolved as URL, i.e. if
194 	 * the resource is not available as descriptor
195 	 * 
196 	 * @see org.osgi.framework.Bundle#getEntry(String)
197 	 * @see org.osgi.framework.Bundle#getResource(String)
198 	 */
199 	public URL getURL() throws IOException {
200 		ContextResource res = null;
201 		URL url = null;
202 
203 		switch (searchType) {
204 			// same as bundle space but with a different string
205 			case OsgiResourceUtils.PREFIX_TYPE_NOT_SPECIFIED:
206 				res = getResourceFromBundleSpace(pathWithoutPrefix);
207 				break;
208 			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_SPACE:
209 				res = getResourceFromBundleSpace(pathWithoutPrefix);
210 				break;
211 			case OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR:
212 				url = getResourceFromBundleJar(pathWithoutPrefix);
213 				break;
214 			case OsgiResourceUtils.PREFIX_TYPE_CLASS_SPACE:
215 				url = getResourceFromBundleClasspath(pathWithoutPrefix);
216 				break;
217 			// fallback
218 			default:
219 				// just try to convert it to an URL
220 				url = new URL(path);
221 				break;
222 		}
223 
224 		if (res != null) {
225 			url = res.getURL();
226 		}
227 
228 		if (url == null) {
229 			throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist");
230 		}
231 
232 		return url;
233 	}
234 
235 	/**
236 	 * Resolves a resource from *the bundle space* only. Only the bundle and its
237 	 * attached fragments are searched for the given resource. Note that this
238 	 * method returns only the first URL found, discarding the rest. To retrieve
239 	 * the entire set, consider using {@link OsgiBundleResourcePatternResolver}.
240 	 * 
241 	 * @param bundlePath the path to resolve
242 	 * @return a URL to the returned resource or null if none is found
243 	 * @throws IOException
244 	 * 
245 	 * @see {@link org.osgi.framework.Bundle#findEntries(String, String, boolean)}
246 	 */
247 	ContextResource getResourceFromBundleSpace(String bundlePath) throws IOException {
248 		ContextResource[] res = getAllUrlsFromBundleSpace(bundlePath);
249 		return (ObjectUtils.isEmpty(res) ? null : res[0]);
250 	}
251 
252 	/**
253 	 * Resolves a resource from the *bundle jar* only. Only the bundle jar is
254 	 * searched (its attached fragments are ignored).
255 	 * 
256 	 * @param bundlePath the path to resolve
257 	 * @return URL to the specified path or null if none is found
258 	 * @throws IOException
259 	 * 
260 	 * @see {@link Bundle#getEntry(String)}
261 	 */
262 	URL getResourceFromBundleJar(String bundlePath) throws IOException {
263 		return bundle.getEntry(bundlePath);
264 	}
265 
266 	/**
267 	 * Resolves a resource from the bundle's classpath. This will find resources
268 	 * in this bundle and also in imported packages from other bundles.
269 	 * 
270 	 * @param bundlePath
271 	 * @return a URL to the returned resource or null if none is found
272 	 * 
273 	 * @see org.osgi.framework.Bundle#getResource(String)
274 	 */
275 	URL getResourceFromBundleClasspath(String bundlePath) {
276 		return bundle.getResource(bundlePath);
277 	}
278 
279 	/**
280 	 * Determine if the given path is relative or absolute.
281 	 * 
282 	 * @param locationPath
283 	 * @return
284 	 */
285 	boolean isRelativePath(String locationPath) {
286 		return ((locationPath.indexOf(PREFIX_SEPARATOR) == -1) && !locationPath.startsWith(ABSOLUTE_PATH_PREFIX));
287 	}
288 
289 	/**
290 	 * Returns a resource relative to this resource. This implementation creates
291 	 * an <code>OsgiBundleResource</code>, applying the given path relative
292 	 * to the path of the underlying resource of this descriptor.
293 	 * 
294 	 * @param relativePath the relative path (relative to this resource)
295 	 * @return the resource handle for the relative resource
296 	 * @throws IOException if the relative resource cannot be determined
297 	 * @see org.springframework.util.StringUtils#applyRelativePath(String,
298 	 * String)
299 	 */
300 	public Resource createRelative(String relativePath) {
301 		String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
302 		return new OsgiBundleResource(this.bundle, pathToUse);
303 	}
304 
305 	/**
306 	 * Returns the filename of this resources. This implementation returns the
307 	 * name of the file that this bundle path resource refers to.
308 	 * 
309 	 * @return resource filename
310 	 * @see org.springframework.util.StringUtils#getFilename(String)
311 	 */
312 	public String getFilename() {
313 		return StringUtils.getFilename(this.path);
314 	}
315 
316 	/**
317 	 * Returns a <code>File</code> handle for this resource. This method does
318 	 * a best-effort attempt to locate the bundle resource on the file system.
319 	 * It is strongly recommended to use {@link #getInputStream()} method
320 	 * instead which works no matter if the bundles are saved (in exploded form
321 	 * or not) on the file system.
322 	 * 
323 	 * @return File handle to this resource
324 	 * @throws IOException if the resource cannot be resolved as absolute file
325 	 * path, i.e. if the resource is not available in a file system
326 	 */
327 	public File getFile() throws IOException {
328 		// locate the file inside the bundle only known prefixes
329 		if (searchType != OsgiResourceUtils.PREFIX_TYPE_UNKNOWN) {
330 			String bundleLocation = bundle.getLocation();
331 			int prefixIndex = bundleLocation.indexOf(ResourceUtils.FILE_URL_PREFIX);
332 			if (prefixIndex > -1) {
333 				bundleLocation = bundleLocation.substring(prefixIndex + ResourceUtils.FILE_URL_PREFIX.length());
334 			}
335 			File file = new File(bundleLocation, path);
336 			if (file.exists()) {
337 				return file;
338 			}
339 			// fall back to the URL discovery (just in case)
340 		}
341 
342 		try {
343 			URL url = new URL(path);
344 			File file = new File(url.getPath());
345 			if (file.exists())
346 				return file;
347 		}
348 		catch (MalformedURLException mue) {
349 			// falls back to the default implementation
350 		}
351 
352 		return super.getFile();
353 	}
354 
355 	/**
356 	 * <p/> This implementation returns a description that includes the bundle
357 	 * location.
358 	 */
359 	public String getDescription() {
360 		StringBuffer buf = new StringBuffer();
361 		buf.append("OSGi resource[");
362 		buf.append(this.path);
363 		buf.append("|bnd.id=");
364 		buf.append(bundle.getBundleId());
365 		buf.append("|bnd.sym=");
366 		buf.append(bundle.getSymbolicName());
367 		buf.append("]");
368 
369 		return buf.toString();
370 	}
371 
372 	/**
373 	 * <p/> This implementation compares the underlying bundle and path
374 	 * locations.
375 	 */
376 	public boolean equals(Object obj) {
377 		if (obj == this) {
378 			return true;
379 		}
380 		if (obj instanceof OsgiBundleResource) {
381 			OsgiBundleResource otherRes = (OsgiBundleResource) obj;
382 			return (this.path.equals(otherRes.path) && ObjectUtils.nullSafeEquals(this.bundle, otherRes.bundle));
383 		}
384 		return false;
385 	}
386 
387 	/**
388 	 * <p/> This implementation returns the hash code of the underlying class
389 	 * path location.
390 	 */
391 	public int hashCode() {
392 		return this.path.hashCode();
393 	}
394 
395 	public long lastModified() throws IOException {
396 		URLConnection con = getURL().openConnection();
397 		con.setUseCaches(false);
398 		long time = con.getLastModified();
399 		// the implementation doesn't return the proper time stamp
400 		if (time == 0) {
401 			if (OsgiResourceUtils.PREFIX_TYPE_BUNDLE_JAR == searchType)
402 				return bundle.getLastModified();
403 		}
404 		// there is nothing else we can do
405 		return time;
406 	}
407 
408 	/**
409 	 * @return Returns the searchType.
410 	 */
411 	int getSearchType() {
412 		return searchType;
413 	}
414 
415 	/**
416 	 * Used internally to get all the URLs matching a certain location. The
417 	 * method is required to extract the folder from the given location as well
418 	 * the file.
419 	 * 
420 	 * @param location location to look for
421 	 * @return an array of URLs
422 	 * @throws IOException
423 	 */
424 	ContextResource[] getAllUrlsFromBundleSpace(String location) throws IOException {
425 		if (bundle == null)
426 			throw new IllegalArgumentException(
427 				"cannot locate items in bundle-space w/o a bundle; specify one when creating this resolver");
428 
429 		Assert.notNull(location);
430 		Set resources = new LinkedHashSet(5);
431 
432 		location = StringUtils.cleanPath(location);
433 		location = OsgiResourceUtils.stripPrefix(location);
434 
435 		if (!StringUtils.hasText(location))
436 			location = OsgiResourceUtils.FOLDER_DELIMITER;
437 
438 		// the root folder is requested (special case)
439 		if (OsgiResourceUtils.FOLDER_DELIMITER.equals(location)) {
440 			// there is no way to determine the URL to the root directly
441 			// through findEntries so we'll have to use another way
442 
443 			// getEntry can't be used since it doesn't consider fragments
444 			// so we have to rely on findEntries 
445 
446 			// we could ask for a known entry (such as META-INF)
447 			// but not all jars have a dedicated entry for it
448 			// so we'll just ask for whatever is present in the root
449 			Enumeration candidates = bundle.findEntries("/", null, false);
450 
451 			// since there can be multiple root paths (when fragments are present)
452 			// iterate on all candidates
453 			while (candidates != null && candidates.hasMoreElements()) {
454 
455 				URL url = (URL) candidates.nextElement();
456 
457 				// determined the root path
458 				// we'll have to parse the string since some implementations
459 				// do not normalize the resulting URL resulting in mismatches
460 				String rootPath = OsgiResourceUtils.findUpperFolder(url.toExternalForm());
461 				resources.add(new UrlContextResource(rootPath));
462 			}
463 		}
464 		else {
465 			// remove leading and trailing / if any
466 			if (location.startsWith(OsgiResourceUtils.FOLDER_DELIMITER))
467 				location = location.substring(1);
468 
469 			if (location.endsWith(OsgiResourceUtils.FOLDER_DELIMITER))
470 				location = location.substring(0, location.length() - 1);
471 
472 			// do we have at least on folder or is this just a file
473 			boolean hasFolder = (location.indexOf(OsgiResourceUtils.FOLDER_DELIMITER) != -1);
474 
475 			String path = (hasFolder ? location : OsgiResourceUtils.FOLDER_DELIMITER);
476 			String file = (hasFolder ? null : location);
477 
478 			// find the file and path
479 			int separatorIndex = location.lastIndexOf(OsgiResourceUtils.FOLDER_DELIMITER);
480 
481 			if (separatorIndex > -1 && separatorIndex + 1 < location.length()) {
482 				// update the path
483 				path = location.substring(0, separatorIndex);
484 
485 				// determine file (if there is any)
486 				if (separatorIndex + 1 < location.length())
487 					file = location.substring(separatorIndex + 1);
488 			}
489 
490 			Enumeration candidates = bundle.findEntries(path, file, false);
491 			// add the leading / to be consistent
492 			String contextPath = OsgiResourceUtils.FOLDER_DELIMITER + location;
493 
494 			while (candidates != null && candidates.hasMoreElements()) {
495 				resources.add(new UrlContextResource((URL) candidates.nextElement(), contextPath));
496 			}
497 		}
498 
499 		return (ContextResource[]) resources.toArray(new ContextResource[resources.size()]);
500 	}
501 
502 	// TODO: can this return null or throw an exception
503 	public String getPathWithinContext() {
504 		return pathWithoutPrefix;
505 	}
506 
507 	/**
508 	 * Return whether this resource actually exists in physical form.
509 	 * <p>
510 	 * This method performs a definitive existence check, whereas the existence
511 	 * of a <code>Resource</code> handle only guarantees a valid descriptor
512 	 * handle.
513 	 * 
514 	 * <p/>The existence check is done by opening an InputStream to the
515 	 * underlying resource (overriding the default implementation which checks
516 	 * first for the presence of a File).
517 	 */
518 	public boolean exists() {
519 		try {
520 			InputStream is = getInputStream();
521 			is.close();
522 			return true;
523 		}
524 		catch (Throwable isEx) {
525 			return false;
526 		}
527 	}
528 }