001: /*
002: * All content copyright (c) 2003-2007 Terracotta, Inc., except as may otherwise be noted in a separate copyright
003: * notice. All rights reserved.
004: */
005: package com.tc.bundles;
006:
007: import org.apache.commons.io.FileUtils;
008: import org.osgi.framework.BundleException;
009:
010: import com.tc.bundles.exception.InvalidBundleManifestException;
011: import com.tc.bundles.exception.MissingBundleException;
012: import com.tc.bundles.Version;
013: import com.tc.logging.CustomerLogging;
014: import com.tc.logging.TCLogger;
015: import com.tc.logging.TCLogging;
016: import com.tc.properties.TCProperties;
017: import com.tc.properties.TCPropertiesImpl;
018: import com.terracottatech.config.Module;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.lang.reflect.Array;
023: import java.net.MalformedURLException;
024: import java.net.URL;
025: import java.text.MessageFormat;
026: import java.util.ArrayList;
027: import java.util.Collection;
028: import java.util.Iterator;
029: import java.util.List;
030: import java.util.Locale;
031: import java.util.MissingResourceException;
032: import java.util.ResourceBundle;
033: import java.util.jar.JarFile;
034: import java.util.jar.Manifest;
035:
036: public class Resolver {
037:
038: private static final String BUNDLE_VERSION = "Bundle-Version";
039: private static final String BUNDLE_SYMBOLICNAME = "Bundle-SymbolicName";
040:
041: private static final String TC_PROPERTIES_SECTION = "l1.modules";
042:
043: private static final String[] JAR_EXTENSIONS = new String[] { "jar" };
044:
045: private static final TCLogger logger = TCLogging
046: .getLogger(Resolver.class);
047:
048: private URL[] repositories;
049: private List registry = new ArrayList();
050:
051: public Resolver(final URL[] repositories) throws BundleException {
052: final List repoLocations = new ArrayList();
053:
054: try {
055: injectDefaultRepositories(repoLocations);
056: } catch (MalformedURLException mue) {
057: throw new BundleException(
058: "Failed to inject default repositories", mue);
059: }
060:
061: for (int i = 0; i < repositories.length; i++) {
062: repoLocations.add(repositories[i]);
063: }
064:
065: if (repoLocations.isEmpty()) {
066: throw new RuntimeException(
067: "No module repositories have been specified via the com.tc.l1.modules.repositories system property");
068: }
069:
070: this .repositories = (URL[]) repoLocations.toArray(new URL[0]);
071: }
072:
073: private static final void injectDefaultRepositories(
074: final List repoLocations) throws MalformedURLException {
075: final String installRoot = System
076: .getProperty("tc.install-root");
077: if (installRoot != null) {
078: final URL defaultRepository = new File(installRoot,
079: "modules").toURL();
080: consoleLogger
081: .debug("Appending default bundle repository: '"
082: + defaultRepository.toString() + "'");
083: repoLocations.add(defaultRepository);
084: }
085:
086: final TCProperties props = TCPropertiesImpl.getProperties()
087: .getPropertiesFor(TC_PROPERTIES_SECTION);
088: final String reposProp = props != null ? props.getProperty(
089: "repositories", true) : null;
090: if (reposProp != null) {
091: final String[] entries = reposProp.split(",");
092: if (entries != null) {
093: for (int i = 0; i < entries.length; i++) {
094: String entry = entries[i].trim();
095: if (entry != null && entry.length() > 0) {
096: final URL defaultRepository = new URL(
097: entries[i]);
098: consoleLogger
099: .debug("Prepending default bundle repository: '"
100: + defaultRepository.toString()
101: + "'");
102: repoLocations.add(defaultRepository);
103: }
104: }
105: }
106: }
107: }
108:
109: public final URL resolve(Module module) throws BundleException {
110: final String name = module.getName();
111: final String version = module.getVersion();
112: final String groupId = module.getGroupId();
113: final URL location = resolveLocation(name, version, groupId);
114:
115: if (location == null) {
116: final String msg = fatal(Message.ERROR_BUNDLE_UNRESOLVED,
117: new Object[] { module.getName(),
118: module.getVersion(), module.getGroupId(),
119: repositoriesToString() });
120: throw new MissingBundleException(msg);
121: }
122:
123: logger.info("Resolved module " + groupId + ":" + name + ":"
124: + version + " from " + location);
125:
126: resolveDependencies(location);
127: return location;
128: }
129:
130: public final URL[] resolve(Module[] modules) throws BundleException {
131: resolveDefaultModules();
132: resolveAdditionalModules();
133:
134: if (modules != null) {
135: for (int i = 0; i < modules.length; i++) {
136: resolve(modules[i]);
137: }
138: }
139:
140: return getResolvedUrls();
141: }
142:
143: public final URL[] getResolvedUrls() {
144: int j = 0;
145: final URL[] urls = new URL[registry.size()];
146: for (Iterator i = registry.iterator(); i.hasNext();) {
147: final Entry entry = (Entry) i.next();
148: urls[j++] = entry.getLocation();
149: }
150: return urls;
151: }
152:
153: private Collection findJars(File rootLocation, String groupId,
154: String name, String version) {
155: File groupLocation = new File(rootLocation, groupId.replace(
156: '.', File.separatorChar));
157: File nameLocation = new File(groupLocation, name);
158: File versionLocation = new File(nameLocation, version);
159: final Collection jars = new ArrayList();
160:
161: File exactLocation = new File(versionLocation, name + "-"
162: + version + ".jar");
163: if (exactLocation.exists() && exactLocation.isFile()) {
164: jars.add(exactLocation);
165: }
166:
167: if (jars.isEmpty()) {
168: if (versionLocation.isDirectory()) {
169: jars.addAll(FileUtils.listFiles(versionLocation,
170: JAR_EXTENSIONS, false));
171: }
172: } else {
173: return jars;
174: }
175:
176: if (jars.isEmpty()) {
177: if (nameLocation.isDirectory()) {
178: jars
179: .addAll(FileUtils.listFiles(nameLocation,
180: JAR_EXTENSIONS, !versionLocation
181: .isDirectory()));
182: }
183: } else {
184: return jars;
185: }
186:
187: if (jars.isEmpty()) {
188: if (groupLocation.isDirectory()) {
189: jars.addAll(FileUtils.listFiles(groupLocation,
190: JAR_EXTENSIONS, !nameLocation.isDirectory()));
191: }
192: } else {
193: return jars;
194: }
195:
196: if (jars.isEmpty() && rootLocation.isDirectory()) {
197: jars.addAll(FileUtils.listFiles(rootLocation,
198: JAR_EXTENSIONS, !groupLocation.isDirectory()));
199: }
200:
201: return jars;
202: }
203:
204: protected URL resolveBundle(BundleSpec spec) {
205: for (int i = repositories.length - 1; i >= 0; i--) {
206: final URL location = repositories[i];
207: // TODO: support other protocol besides file://
208: if (!location.getProtocol().equalsIgnoreCase("file")) {
209: warn(Message.WARN_REPOSITORY_PROTOCOL_UNSUPPORTED,
210: new Object[] { location.getProtocol() });
211: continue;
212: }
213:
214: final File root = FileUtils.toFile(location);
215: final File repository = new File(root, spec.getGroupId()
216: .replace('.', File.separatorChar));
217: if (!repository.exists() || !repository.isDirectory()) {
218: warn(Message.WARN_REPOSITORY_UNRESOLVED,
219: new Object[] { repository.getAbsolutePath() });
220: continue;
221: }
222:
223: final Collection jars = findJars(root, spec.getGroupId(),
224: spec.getName(), spec.getVersion());
225: for (final Iterator j = jars.iterator(); j.hasNext();) {
226: final File bundleFile = (File) j.next();
227: if (!bundleFile.isFile()) {
228: warn(Message.WARN_FILE_IGNORED_INVALID_NAME,
229: new Object[] { bundleFile.getName() });
230: continue;
231: }
232: final Manifest manifest = getManifest(bundleFile);
233: if (manifest == null) {
234: warn(Message.WARN_FILE_IGNORED_MISSING_MANIFEST,
235: new Object[] { bundleFile.getName() });
236: continue;
237: }
238: final String symname = manifest.getMainAttributes()
239: .getValue(BUNDLE_SYMBOLICNAME);
240: final String version = manifest.getMainAttributes()
241: .getValue(BUNDLE_VERSION);
242: if (spec.isCompatible(symname, version)) {
243: try {
244: return bundleFile.toURL();
245: } catch (MalformedURLException e) {
246: fatal(Message.ERROR_BUNDLE_MALFORMED_URL,
247: new Object[] { bundleFile.getName() }); // should be fatal???
248: return null;
249: }
250: }
251: }
252: }
253: return null;
254: }
255:
256: protected URL resolveLocation(final String name,
257: final String version, final String groupId) {
258: final String symname = MavenToOSGi.artifactIdToSymbolicName(
259: groupId, name);
260: final String osgiVersionStr = MavenToOSGi
261: .projectVersionToBundleVersion(version);
262: Version osgiVersion = Version.parse(osgiVersionStr);
263:
264: if (logger.isDebugEnabled()) {
265: logger.debug("Resolving location of " + groupId + ":"
266: + name + ":" + version);
267: }
268:
269: for (int i = repositories.length - 1; i >= 0; i--) {
270: final String repositoryURL = repositories[i].toString()
271: + (repositories[i].toString().endsWith("/") ? ""
272: : "/");
273: URL url = null;
274: try {
275: url = new URL(repositoryURL);
276: } catch (MalformedURLException e) {
277: // ignore bad URLs
278: logger.warn(
279: "Ignoring bad repository URL during resolution: "
280: + repositoryURL, e);
281: continue;
282: }
283:
284: final Collection jars = findJars(FileUtils.toFile(url),
285: groupId, name, version);
286: for (final Iterator j = jars.iterator(); j.hasNext();) {
287: final File jar = (File) j.next();
288: final Manifest manifest = getManifest(jar);
289:
290: if (isBundleMatch(jar, manifest, symname, osgiVersion)) {
291: try {
292: return addToRegistry(jar.toURL(), manifest);
293: } catch (MalformedURLException e) {
294: logger.error(e.getMessage(), e);
295: }
296: }
297: }
298:
299: }
300: return null;
301: }
302:
303: private boolean isBundleMatch(File jarFile, Manifest manifest,
304: String bundleName, Version bundleVersion) {
305: if (logger.isDebugEnabled())
306: logger.debug("Checking " + jarFile + " for " + bundleName
307: + ":" + bundleVersion);
308:
309: // ignore bad JAR files
310: if (manifest == null)
311: return false;
312:
313: // found a match!
314: if (BundleSpec.isMatchingSymbolicName(bundleName, manifest
315: .getMainAttributes().getValue(BUNDLE_SYMBOLICNAME))) {
316: final String manifestVersion = manifest.getMainAttributes()
317: .getValue(BUNDLE_VERSION);
318: try {
319: if (bundleVersion
320: .equals(Version.parse(manifestVersion))) {
321: return true;
322: }
323: } catch (NumberFormatException e) { // thrown by parseVersion()
324: consoleLogger.warn(
325: "Bad manifest bundle version in jar='"
326: + jarFile.getAbsolutePath()
327: + "', version='" + manifestVersion
328: + "'. Skipping...", e);
329: }
330: }
331:
332: return false;
333: }
334:
335: private void resolveDefaultModules() throws BundleException {
336: final TCProperties props = TCPropertiesImpl.getProperties()
337: .getPropertiesFor(TC_PROPERTIES_SECTION);
338: final String defaultModulesProp = props != null ? props
339: .getProperty("default") : null;
340:
341: if (defaultModulesProp == null) {
342: consoleLogger
343: .debug("No implicit modules were loaded because the l1.modules.default property "
344: + "in tc.properties file was not set.");
345: return;
346: }
347:
348: final String[] defaultModulesSpec = BundleSpec
349: .getRequirements(defaultModulesProp);
350: if (defaultModulesSpec.length > 0) {
351: for (int i = 0; i < defaultModulesSpec.length; i++) {
352: BundleSpec spec = BundleSpec
353: .newInstance(defaultModulesSpec[i]);
354: ensureBundle(spec);
355: }
356: } else {
357: consoleLogger
358: .debug("No implicit modules were loaded because the l1.modules.default property "
359: + "in tc.properties file was empty.");
360: }
361: }
362:
363: private String repositoriesToString() {
364: final StringBuffer repos = new StringBuffer();
365: for (int j = 0; j < repositories.length; j++) {
366: if (j > 0)
367: repos.append(";");
368: repos.append(repositories[j]);
369: }
370:
371: return repos.toString();
372: }
373:
374: private void resolveAdditionalModules() throws BundleException {
375: final TCProperties props = TCPropertiesImpl.getProperties()
376: .getPropertiesFor(TC_PROPERTIES_SECTION);
377: final String additionalModulesProp = props != null ? props
378: .getProperty("additional") : null;
379: if (additionalModulesProp != null) {
380: String[] additionalModulesSpec = BundleSpec
381: .getRequirements(additionalModulesProp);
382: if (additionalModulesSpec.length > 0) {
383: for (int i = 0; i < additionalModulesSpec.length; i++) {
384: BundleSpec spec = BundleSpec
385: .newInstance(additionalModulesSpec[i]);
386: ensureBundle(spec);
387: }
388: }
389: }
390: }
391:
392: private BundleSpec[] getRequirements(Manifest manifest) {
393: List requirementList = new ArrayList();
394: String[] manifestRequirements = BundleSpec
395: .getRequirements(manifest);
396: if (manifestRequirements.length > 0) {
397: for (int i = 0; i < manifestRequirements.length; i++) {
398: requirementList.add(BundleSpec
399: .newInstance(manifestRequirements[i]));
400: }
401: }
402: return (BundleSpec[]) requirementList
403: .toArray(new BundleSpec[0]);
404: }
405:
406: private void resolveDependencies(final URL location)
407: throws BundleException {
408: final Manifest manifest = getManifest(location);
409: if (manifest == null) {
410: final String msg = fatal(Message.ERROR_BUNDLE_UNREADABLE,
411: new Object[] {
412: FileUtils.toFile(location).getName(),
413: FileUtils.toFile(location).getParent() });
414: throw new InvalidBundleManifestException(msg);
415: }
416:
417: final BundleSpec[] requirements = getRequirements(manifest);
418: for (int i = 0; i < requirements.length; i++) {
419: final BundleSpec spec = requirements[i];
420: ensureBundle(spec);
421: }
422:
423: addToRegistry(location, manifest);
424: }
425:
426: private void ensureBundle(final BundleSpec spec)
427: throws BundleException {
428: URL required = findInRegistry(spec);
429: if (required == null) {
430: required = resolveBundle(spec);
431: if (required == null) {
432: final String msg = fatal(
433: Message.ERROR_BUNDLE_DEPENDENCY_UNRESOLVED,
434: new Object[] { spec.getName(),
435: spec.getVersion(), spec.getGroupId(),
436: repositoriesToString() });
437: throw new MissingBundleException(msg);
438: }
439: addToRegistry(required, getManifest(required));
440: resolveDependencies(required);
441: }
442: }
443:
444: private URL addToRegistry(final URL location,
445: final Manifest manifest) {
446: final Entry entry = new Entry(location, manifest);
447: if (!registry.contains(entry))
448: registry.add(entry);
449: return entry.getLocation();
450: }
451:
452: private URL findInRegistry(BundleSpec spec) {
453: URL location = null;
454: for (Iterator i = registry.iterator(); i.hasNext();) {
455: final Entry entry = (Entry) i.next();
456: if (spec.isCompatible(entry.getSymbolicName(), entry
457: .getVersion())) {
458: location = entry.getLocation();
459: break;
460: }
461: }
462: return location;
463: }
464:
465: private Manifest getManifest(final File file) {
466: try {
467: return getManifest(file.toURL());
468: } catch (MalformedURLException e) {
469: return null;
470: }
471: }
472:
473: private Manifest getManifest(final URL location) {
474: try {
475: final JarFile bundle = new JarFile(FileUtils
476: .toFile(location));
477: return bundle.getManifest();
478: } catch (IOException e) {
479: return null;
480: }
481: }
482:
483: private String warn(final Message message, final Object[] arguments) {
484: final String msg = formatMessage(message, arguments);
485: consoleLogger.warn(msg);
486: return msg;
487: }
488:
489: private String fatal(final Message message, final Object[] arguments) {
490: final String msg = formatMessage(message, arguments);
491: consoleLogger.fatal(msg);
492: return msg;
493: }
494:
495: private static String formatMessage(final Message message,
496: final Object[] arguments) {
497: return MessageFormat.format(resourceBundle.getString(message
498: .key()), arguments);
499: }
500:
501: // XXX it is a very bad idea to use URL to calculate hashcode
502: private final class Entry {
503: private URL location;
504: private Manifest manifest;
505:
506: public Entry(final URL location, final Manifest manifest) {
507: this .location = location;
508: this .manifest = manifest;
509: }
510:
511: public String getVersion() {
512: return manifest.getMainAttributes()
513: .getValue(BUNDLE_VERSION);
514: }
515:
516: public String getSymbolicName() {
517: return manifest.getMainAttributes().getValue(
518: BUNDLE_SYMBOLICNAME);
519: }
520:
521: public URL getLocation() {
522: return location;
523: }
524:
525: public boolean equals(Object object) {
526: if (this == object)
527: return true;
528: if (!(object instanceof Entry))
529: return false;
530: final Entry entry = (Entry) object;
531: return location.equals(entry.getLocation())
532: && getVersion().equals(entry.getVersion())
533: && getSymbolicName()
534: .equals(entry.getSymbolicName());
535: }
536:
537: private static final int SEED1 = 18181;
538: private static final int SEED2 = 181081;
539:
540: public int hashCode() {
541: int result = SEED1;
542: result = hash(result, this .location);
543: result = hash(result, this .manifest);
544: return result;
545: }
546:
547: private int hash(int seed, int value) {
548: return SEED2 * seed + value;
549: }
550:
551: private int hash(int seed, Object object) {
552: int result = seed;
553: if (object == null) {
554: result = hash(result, 0);
555: } else if (!object.getClass().isArray()) {
556: result = hash(result, object);
557: } else {
558: int len = Array.getLength(object);
559: for (int i = 0; i < len; i++) {
560: Object o = Array.get(object, i);
561: result = hash(result, o);
562: }
563: }
564: return result;
565: }
566: }
567:
568: private static final class Message {
569:
570: static final Message WARN_BUNDLE_UNRESOLVED = new Message(
571: "warn.bundle.unresolved");
572: static final Message WARN_REPOSITORY_UNRESOLVED = new Message(
573: "warn.repository.unresolved");
574: static final Message WARN_FILE_IGNORED_INVALID_NAME = new Message(
575: "warn.file.ignored.invalid-name");
576: static final Message WARN_FILE_IGNORED_MISSING_MANIFEST = new Message(
577: "warn.file.ignored.missing-manifest");
578: static final Message WARN_REPOSITORY_PROTOCOL_UNSUPPORTED = new Message(
579: "warn.repository.protocol.unsupported");
580: static final Message WARN_EXCEPTION_OCCURED = new Message(
581: "warn.exception.occured");
582: static final Message ERROR_BUNDLE_UNREADABLE = new Message(
583: "error.bundle.unreadable");
584: static final Message ERROR_BUNDLE_UNRESOLVED = new Message(
585: "error.bundle.unresolved");
586: static final Message ERROR_BUNDLE_DEPENDENCY_UNRESOLVED = new Message(
587: "error.bundle-dependency.unresolved");
588: static final Message ERROR_BUNDLE_MALFORMED_URL = new Message(
589: "error.bundle.malformed-url");
590:
591: private final String resourceBundleKey;
592:
593: private Message(final String resourceBundleKey) {
594: this .resourceBundleKey = resourceBundleKey;
595: }
596:
597: String key() {
598: return resourceBundleKey;
599: }
600: }
601:
602: private static final TCLogger consoleLogger = CustomerLogging
603: .getConsoleLogger();
604: private static final ResourceBundle resourceBundle;
605:
606: static {
607: try {
608: resourceBundle = ResourceBundle.getBundle(Resolver.class
609: .getName(), Locale.getDefault(), Resolver.class
610: .getClassLoader());
611: } catch (MissingResourceException mre) {
612: throw new RuntimeException("No resource bundle exists for "
613: + Resolver.class.getName());
614: } catch (Throwable t) {
615: throw new RuntimeException(
616: "Unexpected error loading resource bundle for "
617: + Resolver.class.getName(), t);
618: }
619: }
620: }
|