Richard S. Hall | 85bafab | 2009-07-13 13:25:46 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | * or more contributor license agreements. See the NOTICE file |
| 4 | * distributed with this work for additional information |
| 5 | * regarding copyright ownership. The ASF licenses this file |
| 6 | * to you under the Apache License, Version 2.0 (the |
| 7 | * "License"); you may not use this file except in compliance |
| 8 | * with the License. You may obtain a copy of the License at |
| 9 | * |
| 10 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | * |
| 12 | * Unless required by applicable law or agreed to in writing, |
| 13 | * software distributed under the License is distributed on an |
| 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | * KIND, either express or implied. See the License for the |
| 16 | * specific language governing permissions and limitations |
| 17 | * under the License. |
| 18 | */ |
| 19 | |
| 20 | package org.cauldron.bld.ivy; |
| 21 | |
| 22 | import static java.lang.String.format; |
| 23 | |
| 24 | import java.io.File; |
| 25 | import java.io.IOException; |
| 26 | import java.io.InputStream; |
| 27 | import java.net.MalformedURLException; |
| 28 | import java.net.URI; |
| 29 | import java.net.URL; |
| 30 | import java.util.Collection; |
| 31 | import java.util.Date; |
| 32 | import java.util.HashMap; |
| 33 | import java.util.Map; |
| 34 | import java.util.Properties; |
| 35 | import java.util.jar.JarInputStream; |
| 36 | import java.util.jar.Manifest; |
| 37 | |
| 38 | import org.apache.ivy.core.module.descriptor.Artifact; |
| 39 | import org.apache.ivy.core.module.descriptor.DependencyDescriptor; |
| 40 | import org.apache.ivy.core.module.id.ModuleRevisionId; |
| 41 | import org.apache.ivy.core.resolve.ResolveData; |
| 42 | import org.apache.ivy.plugins.repository.Resource; |
| 43 | import org.apache.ivy.plugins.repository.url.URLResource; |
| 44 | import org.apache.ivy.plugins.resolver.BasicResolver; |
| 45 | import org.apache.ivy.plugins.resolver.util.ResolvedResource; |
| 46 | import org.apache.ivy.util.FileUtil; |
| 47 | import org.cauldron.bld.config.BldFactory; |
| 48 | import org.cauldron.bld.core.internal.model.osgi.RequiredBundle; |
| 49 | import org.cauldron.sigil.model.IModelElement; |
| 50 | import org.cauldron.sigil.model.common.VersionRange; |
| 51 | import org.cauldron.sigil.model.eclipse.ISigilBundle; |
| 52 | import org.cauldron.sigil.model.osgi.IBundleModelElement; |
| 53 | import org.cauldron.sigil.repository.IResolution; |
| 54 | import org.cauldron.sigil.repository.ResolutionException; |
| 55 | |
| 56 | /** |
| 57 | * This resolver is able to work with Sigil repositories. |
| 58 | * It does not allow publishing. |
| 59 | */ |
| 60 | public class SigilResolver extends BasicResolver implements IBldResolver { |
| 61 | /** the sigil-injected dependency organisation name */ |
| 62 | static final String ORG_SIGIL = "sigil"; |
| 63 | |
| 64 | private IBldResolver resolver; |
| 65 | private String config; |
| 66 | private boolean extractBCP = false; |
| 67 | private Map<String, Resource> resourcesCache = new HashMap<String, Resource>(); |
| 68 | |
| 69 | /** |
| 70 | * no-args constructor required by Ivy. |
| 71 | * |
| 72 | * XXX: It doesn't currently seem to matter that Ivy instantiates this many times, |
| 73 | * since SigilParser caches it, and the ProjectRepositoryProvider static cache |
| 74 | * prevents it being re-loaded unnecessarily. |
| 75 | * |
| 76 | * If this should become a problem, then we will need to delegate to a singleton instance, |
| 77 | * as we have done in SigilParser. |
| 78 | */ |
| 79 | public SigilResolver() { |
| 80 | } |
| 81 | |
| 82 | public void setConfig(String config) { |
| 83 | this.config = config; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * if true, resolver extracts any jars embedded in Bundle-ClassPath. |
| 88 | */ |
| 89 | public void setExtractBCP(String extract) { |
| 90 | this.extractBCP = Boolean.parseBoolean(extract); |
| 91 | } |
| 92 | |
| 93 | private IBldResolver getResolver() { |
| 94 | if (resolver == null) { |
| 95 | if (config == null) { |
| 96 | throw new Error("SigilResolver: not configured. Specify config=\"PATH\" in ivysettings.xml."); |
| 97 | } |
| 98 | try { |
| 99 | URI uri; |
| 100 | File file = new File(config); |
| 101 | |
| 102 | if (file.isAbsolute()) { |
| 103 | uri = file.toURI(); |
| 104 | } else { |
| 105 | URI cwd = new File(".").toURI(); |
| 106 | uri = cwd.resolve(config); |
| 107 | } |
| 108 | |
| 109 | Map<String, Properties> repositories = BldFactory.getConfig(uri).getRepositoryConfig(); |
| 110 | resolver = new BldResolver(repositories); |
| 111 | } catch (IOException e) { |
| 112 | throw new Error("SigilResolver: failed to configure: " + e); |
| 113 | } |
| 114 | } |
| 115 | return resolver; |
| 116 | } |
| 117 | |
| 118 | public IResolution resolveOrFail(IModelElement element, boolean transitive) throws ResolutionException { |
| 119 | return getResolver().resolveOrFail(element, transitive); |
| 120 | } |
| 121 | |
| 122 | public IResolution resolve(IModelElement element, boolean transitive) { |
| 123 | return getResolver().resolve(element, transitive); |
| 124 | } |
| 125 | |
| 126 | public String getTypeName() { |
| 127 | return "sigil"; |
| 128 | } |
| 129 | |
| 130 | /* |
| 131 | * synthesize an ivy descriptor for a Sigil dependency. called after Sigil |
| 132 | * resolution, so descriptor already contains resolved version. |
| 133 | */ |
| 134 | public ResolvedResource findIvyFileRef(DependencyDescriptor dd, ResolveData data) { |
| 135 | ResolvedResource ref = null; |
| 136 | |
| 137 | ModuleRevisionId id = dd.getDependencyRevisionId(); |
| 138 | |
| 139 | if (!id.getOrganisation().equals(ORG_SIGIL)) { |
| 140 | return null; |
| 141 | } |
| 142 | |
| 143 | ISigilBundle bundle = resolve(id); |
| 144 | if (bundle == null) { |
| 145 | return null; |
| 146 | } |
| 147 | |
| 148 | String symbolicName = id.getName(); |
| 149 | String version = bundle.getVersion().toString(); |
| 150 | |
| 151 | Resource res = new SigilIvy(extractBCP ? bundle : null, symbolicName, version); |
| 152 | ref = new ResolvedResource(res, version); |
| 153 | |
| 154 | Log.debug(format("findIvyFileRef: dd=%s => ref=%s", dd, ref)); |
| 155 | return ref; |
| 156 | } |
| 157 | |
| 158 | @Override |
| 159 | protected ResolvedResource findArtifactRef(Artifact artifact, Date date) { |
| 160 | String name = artifact.getName(); |
| 161 | ModuleRevisionId id = artifact.getModuleRevisionId(); |
| 162 | |
| 163 | if (!id.getOrganisation().equals(ORG_SIGIL)) { |
| 164 | return null; |
| 165 | } |
| 166 | |
| 167 | ISigilBundle bundle = resolve(id); |
| 168 | if (bundle == null) { |
| 169 | return null; |
| 170 | } |
| 171 | |
| 172 | IBundleModelElement info = bundle.getBundleInfo(); |
| 173 | URI uri = info.getUpdateLocation(); |
| 174 | Resource res = null; |
| 175 | |
| 176 | try { |
| 177 | URL url = (uri != null) ? uri.toURL() : bundle.getLocation().toFile().toURL(); |
| 178 | if (name.contains("!")) { |
| 179 | String[] split = name.split("!"); |
| 180 | url = new URL("jar:" + url + "!/" + split[1] + "." + artifact.getExt()); |
| 181 | } |
| 182 | res = new URLResource(url); |
| 183 | } catch (MalformedURLException e) { |
| 184 | System.out.println("Oops! " + e); |
| 185 | } |
| 186 | |
| 187 | String version = bundle.getVersion().toString(); |
| 188 | ResolvedResource ref = new ResolvedResource(res, version); |
| 189 | |
| 190 | Log.debug(format("findArtifactRef: artifact=%s, date=%s => ref=%s", artifact, date, ref)); |
| 191 | return ref; |
| 192 | } |
| 193 | |
| 194 | private ISigilBundle resolve(ModuleRevisionId id) { |
| 195 | String revision = id.getRevision(); |
| 196 | String range = revision; |
| 197 | |
| 198 | if (revision.indexOf(',') < 0) { |
| 199 | // SigilParser has already resolved the revision from the import |
| 200 | // version range. |
| 201 | // We now need to locate the same bundle to get its download URL. |
| 202 | // We must use an OSGi version range to specify the exact version, |
| 203 | // otherwise it will resolve to "specified version or above". |
| 204 | range = "[" + revision + "," + revision + "]"; |
| 205 | } |
| 206 | |
| 207 | RequiredBundle bundle = new RequiredBundle(); |
| 208 | bundle.setSymbolicName(id.getName()); |
| 209 | bundle.setVersions(VersionRange.parseVersionRange(range)); |
| 210 | |
| 211 | try { |
| 212 | IResolution resolution = resolveOrFail(bundle, false); |
| 213 | ISigilBundle[] bundles = resolution.getBundles().toArray(new ISigilBundle[0]); |
| 214 | if (bundles.length == 1) { |
| 215 | return bundles[0]; |
| 216 | } |
| 217 | } catch (ResolutionException e) { |
| 218 | return null; |
| 219 | } |
| 220 | return null; |
| 221 | } |
| 222 | |
| 223 | /* |
| 224 | * Implement BasicResolver abstract methods |
| 225 | */ |
| 226 | |
| 227 | @Override |
| 228 | protected long get(Resource res, File dest) throws IOException { |
| 229 | FileUtil.copy(res.openStream(), dest, null); |
| 230 | long len = res.getContentLength(); |
| 231 | Log.debug(format("get(%s, %s) = %d", res, dest, len)); |
| 232 | return len; |
| 233 | } |
| 234 | |
| 235 | |
| 236 | @Override |
| 237 | public Resource getResource(String source) throws IOException { |
| 238 | source = encode(source); |
| 239 | Resource res = resourcesCache.get(source); |
| 240 | if (res == null) { |
| 241 | res = new URLResource(new URL(source)); |
| 242 | resourcesCache.put(source, res); |
| 243 | } |
| 244 | Log.debug(format("SIGIL: getResource(%s) = %d", source, res)); |
| 245 | return res; |
| 246 | } |
| 247 | |
| 248 | private static String encode(String source) { |
| 249 | return source.trim().replaceAll(" ", "%20"); |
| 250 | } |
| 251 | |
| 252 | @SuppressWarnings("unchecked") |
| 253 | @Override |
| 254 | protected Collection findNames(Map tokenValues, String token) { |
| 255 | throw new Error("SigilResolver: findNames not supported."); |
| 256 | } |
| 257 | |
| 258 | public void publish(Artifact artifact, File src, boolean overwrite) throws IOException { |
| 259 | throw new Error("SigilResolver: publish not supported."); |
| 260 | } |
| 261 | |
| 262 | public void beginPublishTransaction(ModuleRevisionId module, boolean overwrite) throws IOException { |
| 263 | // stop publish attempts being silently ignored. |
| 264 | throw new Error("SigilResolver: publish not supported."); |
| 265 | } |
| 266 | |
| 267 | /* |
| 268 | * Synthesize a virtual ivy file for a Sigil dependency. |
| 269 | */ |
| 270 | static class SigilIvy implements Resource { |
| 271 | private StringBuilder content = new StringBuilder(); |
| 272 | private String name; |
| 273 | private boolean exists = true; |
| 274 | |
| 275 | private SigilIvy() { |
| 276 | } |
| 277 | |
| 278 | public SigilIvy(ISigilBundle bundle, String module, String rev) { |
| 279 | this.name = "sigil!" + module + "#" + rev; |
| 280 | |
| 281 | String org = ORG_SIGIL; |
| 282 | // FIXME: make status and pub configurable |
| 283 | String status = "release"; |
| 284 | String pub = "20080912162859"; |
| 285 | |
| 286 | content.append("<ivy-module version=\"1.0\">\n"); |
| 287 | |
| 288 | content.append(format( |
| 289 | "<info organisation=\"%s\" module=\"%s\" revision=\"%s\" status=\"%s\" publication=\"%s\"/>\n", |
| 290 | org, module, rev, status, pub)); |
| 291 | |
| 292 | String bcp = readBundleClassPath(bundle); |
| 293 | if (bcp != null) { |
| 294 | content.append("<publications>\n"); |
| 295 | for (String j : bcp.split(",\\s*")) { |
| 296 | if (j.equals(".")) { |
| 297 | content.append("<artifact/>\n"); |
| 298 | } else if (!j.endsWith("component-activator.jar")) { |
| 299 | if (j.endsWith(".jar")) |
| 300 | j = j.substring(0, j.length() - 4); |
| 301 | content.append(format("<artifact name=\"%s!%s\"/>\n", module, j)); |
| 302 | } |
| 303 | } |
| 304 | content.append("</publications>\n"); |
| 305 | } |
| 306 | |
| 307 | // TODO: add dependencies? |
| 308 | // <dependencies> |
| 309 | // <dependency org="org.apache" name="log4j" rev="1.2.12" |
| 310 | // revConstraint="[1.2,1.3)"/> |
| 311 | // </dependencies> |
| 312 | |
| 313 | content.append("</ivy-module>\n"); |
| 314 | } |
| 315 | |
| 316 | private String readBundleClassPath(ISigilBundle bundle) { |
| 317 | if (bundle == null) |
| 318 | return null; |
| 319 | |
| 320 | URI uri = bundle.getBundleInfo().getUpdateLocation(); |
| 321 | InputStream is = null; |
| 322 | try { |
| 323 | URL url = (uri != null) ? uri.toURL() : bundle.getLocation().toFile().toURL(); |
| 324 | is = url.openStream(); |
| 325 | JarInputStream js = new JarInputStream(is, false); |
| 326 | Manifest m = js.getManifest(); |
| 327 | if (m != null) |
| 328 | return (String) m.getMainAttributes().getValue("Bundle-ClassPath"); |
| 329 | } catch (IOException e) { |
| 330 | } finally { |
| 331 | if (is != null) { |
| 332 | try { |
| 333 | is.close(); |
| 334 | } catch (IOException e2) { |
| 335 | } |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | return null; |
| 340 | } |
| 341 | |
| 342 | public String toString() { |
| 343 | return "SigilIvy[" + name + "]"; |
| 344 | } |
| 345 | |
| 346 | // clone is used to read checksum files |
| 347 | // so clone(getName() + ".sha1").exists() should be false |
| 348 | public Resource clone(String cloneName) { |
| 349 | Log.debug("SigilIvy: clone: " + cloneName); |
| 350 | SigilIvy clone = new SigilIvy(); |
| 351 | clone.name = cloneName; |
| 352 | clone.exists = false; |
| 353 | return clone; |
| 354 | } |
| 355 | |
| 356 | public boolean exists() { |
| 357 | return exists; |
| 358 | } |
| 359 | |
| 360 | public long getContentLength() { |
| 361 | return content.length(); |
| 362 | } |
| 363 | |
| 364 | public long getLastModified() { |
| 365 | // TODO Auto-generated method stub |
| 366 | Log.debug("NOT IMPLEMENTED: getLastModified"); |
| 367 | return 0; |
| 368 | } |
| 369 | |
| 370 | public String getName() { |
| 371 | return name; |
| 372 | } |
| 373 | |
| 374 | public boolean isLocal() { |
| 375 | return false; |
| 376 | } |
| 377 | |
| 378 | @SuppressWarnings("deprecation") |
| 379 | public InputStream openStream() throws IOException { |
| 380 | return new java.io.StringBufferInputStream(content.toString()); |
| 381 | } |
| 382 | } |
| 383 | } |