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