blob: 1ab3772e05cbb95cabba58ec7f302340ce0dc36a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.cauldron.bld.ivy;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.ivy.Ivy;
import org.apache.ivy.core.IvyContext;
import org.apache.ivy.core.module.descriptor.Artifact;
import org.apache.ivy.core.module.descriptor.Configuration;
import org.apache.ivy.core.module.descriptor.DefaultDependencyArtifactDescriptor;
import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
import org.apache.ivy.core.module.id.ModuleRevisionId;
import org.apache.ivy.core.settings.IvySettings;
import org.apache.ivy.plugins.parser.ModuleDescriptorParser;
import org.apache.ivy.plugins.parser.ModuleDescriptorParserRegistry;
import org.apache.ivy.plugins.parser.ParserSettings;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorParser;
import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorWriter;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.plugins.repository.file.FileResource;
import org.apache.ivy.plugins.repository.url.URLResource;
import org.apache.ivy.plugins.resolver.DependencyResolver;
import org.cauldron.bld.config.BldFactory;
import org.cauldron.bld.config.IBldProject;
import org.cauldron.sigil.model.IModelElement;
import org.cauldron.sigil.model.common.VersionRange;
import org.cauldron.sigil.model.eclipse.ISigilBundle;
import org.cauldron.sigil.model.osgi.IBundleModelElement;
import org.cauldron.sigil.model.osgi.IPackageImport;
import org.cauldron.sigil.model.osgi.IRequiredBundle;
import org.cauldron.sigil.repository.IResolution;
public class SigilParser implements ModuleDescriptorParser {
private static DelegateParser instance;
// used by ProjectRepository
static DelegateParser instance() {
if (instance == null)
throw new IllegalStateException("SigilParser is not instantiated.");
return instance;
}
public SigilParser() {
if (instance == null) {
instance = new DelegateParser();
}
}
/**
* In IvyDE, IvyContext is not available, so we can't find the SigilResolver.
* This allows us to construct one.
* @deprecated temporary to support IvyDE
*/
public void setConfig(String config) {
instance.config = config;
}
/**
* sets delegate parser.
* If not set, we delegate to the default Ivy parser.
* @param type name returned by desired parser's getType() method.
*/
public void setDelegateType(String type) {
for (ModuleDescriptorParser parser : ModuleDescriptorParserRegistry.getInstance().getParsers()) {
if (parser.getType().equals(type)) {
instance.delegate = parser;
break;
}
}
if (instance.delegate == null) {
throw new IllegalArgumentException("Can't find parser delegateType=" + type);
}
}
/**
* sets default file name the delegate parser accepts.
* If not set, defaults to "ivy.xml".
* @param name
*/
public void setDelegateFile(String name) {
instance.ivyFile = name;
}
/**
* sets the dependencies to keep from the delegate parser.
* If not set, all existing dependencies are dropped.
* @param regex pattern matching dependency names to keep.
*/
public void setKeepDependencies(String regex) {
instance.keepDependencyPattern = Pattern.compile(regex);
}
/**
* reduce level of info logging.
* @param quiet
*/
public void setQuiet(String quiet) {
instance.quiet = Boolean.parseBoolean(quiet);
}
/**
* sets the SigilResolver we use.
* If not set, we use the first SigilResolver we find.
* @param name
*/
public void setResolver(String name) {
instance.resolverName = name;
}
/**
* adds override element: <override name="X" pattern="Y" replace="Z"/>. Overrides
* Ivy variables using a pattern substitution on the resource directory path.
*
* @deprecated
* This is only needed when a delegate parser expects Ant variables to be set
* during the ivy:buildlist task (or the ProjectRepository initialisation),
* which they are not. The delegate parser should really be fixed, as otherwise
* the ivy:buildlist task won't work without this workaround.
*/
// e.g. <override name="ant.project.name" pattern=".*/([^/]+)/([^/]+)$" replace="$1-$2"/>
public void addConfiguredOverride(Map<String, String> var) {
final String name = var.get("name");
final String regex = var.get("pattern");
final String replace = var.get("replace");
if (name == null || regex == null || replace == null)
throw new IllegalArgumentException("override must contain name, pattern and replace attributes.");
instance.varRegex.put(name, Pattern.compile(regex));
instance.varReplace.put(name, replace);
}
// implement ModuleDescriptorParser interface by delegation to singleton instance.
public boolean accept(Resource res) {
return instance.accept(res);
}
public Artifact getMetadataArtifact(ModuleRevisionId mrid, Resource res) {
return instance.getMetadataArtifact(mrid, res);
}
public String getType() {
return instance.getType();
}
public ModuleDescriptor parseDescriptor(ParserSettings settings, URL xmlURL, boolean validate)
throws ParseException, IOException {
return instance.parseDescriptor(settings, xmlURL, validate);
}
public ModuleDescriptor parseDescriptor(ParserSettings settings, URL xmlURL, Resource res, boolean validate)
throws ParseException, IOException {
return instance.parseDescriptor(settings, xmlURL, res, validate);
}
public void toIvyFile(InputStream source, Resource res, File destFile, ModuleDescriptor md) throws ParseException,
IOException {
instance.toIvyFile(source, res, destFile, md);
}
/**
* delegating parser.
* (optionally) removes original dependencies and augments with sigil-determined dependencies.
*/
static class DelegateParser extends XmlModuleDescriptorParser {
private static final String SIGIL_BUILDLIST = IBldProject.PROJECT_FILE;
private static final String KEEP_ALL = ".*";
private IBldResolver resolver;
private ParserSettings defaultSettings;
private Map<String, DefaultModuleDescriptor> rawCache = new HashMap<String, DefaultModuleDescriptor>();
private Map<String, DefaultModuleDescriptor> augmentedCache = new HashMap<String, DefaultModuleDescriptor>();
private HashMap<String, String> varReplace = new HashMap<String, String>();
private HashMap<String, Pattern> varRegex = new HashMap<String, Pattern>();
private ModuleDescriptorParser delegate;
private String ivyFile = "ivy.xml";
private String resolverName;
private Pattern keepDependencyPattern;
private boolean quiet;
private String config;
@Override
public boolean accept(Resource res) {
boolean accept = (res instanceof SigilResolver.SigilIvy)
|| res.getName().endsWith("/" + SIGIL_BUILDLIST)
|| ((instance.getSigilURI(res) != null) &&
((instance.delegate != null ? instance.delegate.accept(res) : false) || super.accept(res)));
if (accept)
Log.verbose("accepted: " + res);
return accept;
}
@Override
public void toIvyFile(InputStream source, Resource res, File destFile, ModuleDescriptor md)
throws ParseException, IOException {
Log.verbose("writing resource: " + res + " toIvyFile: " + destFile);
// source allows us to keep layout and comments,
// but this doesn't work if source is not ivy.xml.
// So just write file directly from descriptor.
try {
XmlModuleDescriptorWriter.write(md, destFile);
} finally {
if (source != null)
source.close();
}
}
@Override
public ModuleDescriptor parseDescriptor(ParserSettings settings, URL xmlURL, boolean validate)
throws ParseException, IOException {
return parseDescriptor(settings, xmlURL, new URLResource(xmlURL), validate);
}
@Override
public ModuleDescriptor parseDescriptor(ParserSettings settings, URL xmlURL, Resource res, boolean validate)
throws ParseException, IOException {
String cacheKey = xmlURL.toString() + settings.hashCode();
DefaultModuleDescriptor dmd = augmentedCache.get(cacheKey);
if (dmd == null) {
dmd = rawCache.get(cacheKey);
if (dmd == null) {
dmd = delegateParse(settings, xmlURL, res, validate);
}
if (!quiet)
Log.info("augmenting module=" + dmd.getModuleRevisionId().getName() +
" ant.project.name=" + settings.substitute("${ant.project.name}"));
addDependenciesToDescriptor(res, dmd);
augmentedCache.put(cacheKey, dmd);
Log.verbose("augmented dependencies: " + Arrays.asList(dmd.getDependencies()));
}
return dmd;
}
// used by ProjectRepository
ModuleDescriptor parseDescriptor(URL projectURL) throws ParseException, IOException {
if (defaultSettings == null) {
Ivy ivy = IvyContext.getContext().peekIvy();
if (ivy == null)
throw new IllegalStateException("can't get default settings - no ivy context.");
defaultSettings = ivy.getSettings();
}
URL ivyURL = new URL(projectURL, ivyFile);
String cacheKey = ivyURL.toString() + defaultSettings.hashCode();
DefaultModuleDescriptor dmd = rawCache.get(cacheKey);
if (dmd == null) {
URLResource res = new URLResource(ivyURL);
// Note: this doesn't contain the augmented dependencies, which is OK,
// since the ProjectRepository only needs the id and status.
dmd = delegateParse(defaultSettings, ivyURL, res, false);
rawCache.put(cacheKey, dmd);
}
return dmd;
}
private DefaultModuleDescriptor delegateParse(ParserSettings settings, URL xmlURL, Resource res,
boolean validate) throws ParseException, IOException {
String resName = res.getName();
if (resName.endsWith("/" + SIGIL_BUILDLIST)) {
String name = resName.substring(0, resName.length() - SIGIL_BUILDLIST.length());
res = res.clone(name + ivyFile);
xmlURL = new URL(xmlURL, ivyFile);
}
if (settings instanceof IvySettings) {
IvySettings ivySettings = (IvySettings) settings;
String dir = new File(res.getName()).getParent();
for (String name : varRegex.keySet()) {
Pattern regex = varRegex.get(name);
String replace = varReplace.get(name);
String value = regex.matcher(dir).replaceAll(replace);
Log.debug("overriding variable " + name + "=" + value);
ivySettings.setVariable(name, value);
}
}
ModuleDescriptor md = null;
if (delegate == null || !delegate.accept(res)) {
md = super.parseDescriptor(settings, xmlURL, res, validate);
} else {
md = delegate.parseDescriptor(settings, xmlURL, res, validate);
}
return mungeDescriptor(md, res);
}
/**
* clones descriptor and removes dependencies, as descriptor MUST have
* 'this' as the parser given to its constructor.
* Only dependency names matched by keepDependencyPattern are kept,
* as we're going to add our own dependencies later.
*/
private DefaultModuleDescriptor mungeDescriptor(ModuleDescriptor md, Resource res) {
if ((md instanceof DefaultModuleDescriptor) &&
(md.getParser() == this) &&
(KEEP_ALL.equals(keepDependencyPattern))) {
return (DefaultModuleDescriptor) md;
}
DefaultModuleDescriptor dmd = new DefaultModuleDescriptor(this, res);
dmd.setModuleRevisionId(md.getModuleRevisionId());
dmd.setPublicationDate(md.getPublicationDate());
for (Configuration c : md.getConfigurations()) {
String conf = c.getName();
dmd.addConfiguration(c);
for (Artifact a : md.getArtifacts(conf)) {
dmd.addArtifact(conf, a);
}
}
if (keepDependencyPattern != null) {
for (DependencyDescriptor dependency : md.getDependencies()) {
String name = dependency.getDependencyId().getName();
if (keepDependencyPattern.matcher(name).matches()) {
dmd.addDependency(dependency);
}
}
}
return dmd;
}
/*
* find URI to Sigil project file. This assumes that it is in the same
* directory as the ivy file.
*/
private URI getSigilURI(Resource res) {
URI uri = null;
if (res instanceof URLResource) {
URL url = ((URLResource) res).getURL();
uri = URI.create(url.toString()).resolve(IBldProject.PROJECT_FILE);
try {
InputStream stream = uri.toURL().openStream(); // check file
// exists
stream.close();
} catch (IOException e) {
uri = null;
}
} else if (res instanceof FileResource) {
uri = ((FileResource) res).getFile().toURI().resolve(IBldProject.PROJECT_FILE);
if (!(new File(uri).exists()))
uri = null;
}
return uri;
}
/*
* add sigil dependencies to ModuleDescriptor.
*/
private void addDependenciesToDescriptor(Resource res, DefaultModuleDescriptor md) throws IOException {
// FIXME: transitive should be configurable
final boolean transitive = true; // ivy default is true
final boolean changing = false;
final boolean force = false;
URI uri = getSigilURI(res);
if (uri == null)
return;
IBldProject project;
project = BldFactory.getProject(uri);
IBundleModelElement requirements = project.getDependencies();
Log.verbose("requirements: " + Arrays.asList(requirements.children()));
// preserve version range for Require-Bundle
// XXX: synthesise bundle version range corresponding to package version ranges?
HashMap<String, VersionRange> versions = new HashMap<String, VersionRange>();
for (IModelElement child : requirements.children()) {
if (child instanceof IRequiredBundle) {
IRequiredBundle bundle = (IRequiredBundle) child;
versions.put(bundle.getSymbolicName(), bundle.getVersions());
}
}
IBldResolver resolver = findResolver();
if (resolver == null) {
// this can happen in IvyDE, but it retries and is OK next time.
Log.warn("failed to find resolver - IvyContext not yet available.");
return;
}
IResolution resolution = resolver.resolve(requirements, false);
Log.verbose("resolution: " + resolution.getBundles());
ModuleRevisionId masterMrid = md.getModuleRevisionId();
DefaultDependencyDescriptor dd;
ModuleRevisionId mrid;
for (ISigilBundle bundle : resolution.getBundles()) {
IBundleModelElement info = bundle.getBundleInfo();
String name = info.getSymbolicName();
if (name == null) {
// e.g. SystemProvider with framework=null
continue;
}
if (bundle instanceof ProjectRepository.ProjectBundle) {
ProjectRepository.ProjectBundle pb = (ProjectRepository.ProjectBundle) bundle;
String org = pb.getOrg();
if (org == null)
org = masterMrid.getOrganisation();
String id = pb.getId();
String module = pb.getModule();
String rev = pb.getRevision();
mrid = ModuleRevisionId.newInstance(org, module, rev);
dd = new SigilDependencyDescriptor(md, mrid, force, changing, transitive);
if (!id.equals(module)) { // non-default artifact
dd.addDependencyArtifact(module,
new DefaultDependencyArtifactDescriptor(dd, id, "jar", "jar", null, null));
}
} else {
VersionRange version = versions.get(name);
String rev = version != null ? version.toString() : info.getVersion().toString();
mrid = ModuleRevisionId.newInstance(SigilResolver.ORG_SIGIL, name, rev);
dd = new SigilDependencyDescriptor(md, mrid, force, changing, transitive);
}
int nDeps = 0;
boolean foundDefault = false;
// TODO: make dependency configurations configurable SIGIL-176
for (String conf : md.getConfigurationsNames()) {
if (conf.equals("default")) {
foundDefault = true;
} else if (md.getArtifacts(conf).length == 0) {
dd.addDependencyConfiguration(conf, conf + "(default)");
nDeps++;
}
}
if (nDeps > 0) {
if (foundDefault)
dd.addDependencyConfiguration("default", "default");
} else {
dd.addDependencyConfiguration("*", "*"); // all configurations
}
md.addDependency(dd);
}
boolean resolved = true;
for (IModelElement child : requirements.children()) {
ISigilBundle provider = resolution.getProvider(child);
if (provider == null) {
resolved = false;
// this is parse phase, so only log verbose message.
// error is produced during resolution phase.
Log.verbose("WARN: can't resolve: " + child);
String name;
String version;
if (child instanceof IRequiredBundle) {
IRequiredBundle rb = (IRequiredBundle) child;
name = rb.getSymbolicName();
version = rb.getVersions().toString();
} else {
IPackageImport pi = (IPackageImport) child;
name = "import!" + pi.getPackageName();
version = pi.getVersions().toString();
}
mrid = ModuleRevisionId.newInstance("!" + SigilResolver.ORG_SIGIL, name, version);
dd = new SigilDependencyDescriptor(md, mrid, force, changing, transitive);
dd.addDependencyConfiguration("*", "*"); // all
// configurations
md.addDependency(dd);
}
}
if (!resolved) {
// Ivy does not show warnings or errors logged from parse phase, until after resolution.
// but <buildlist> ant task doesn't do resolution, so errors would be silently ignored.
// Hence, we must use Message.info to ensure this failure is seen.
if (!quiet)
Log.info("WARN: resolution failed in: " + masterMrid.getName() + ". Use -v for details.");
}
}
/*
* find named resolver, or first resolver that implements IBldResolver.
*/
private IBldResolver findResolver() {
if (resolver == null) {
Ivy ivy = IvyContext.getContext().peekIvy();
if (ivy != null) {
if (resolverName != null) {
DependencyResolver r = ivy.getSettings().getResolver(resolverName);
if (r == null) {
throw new Error("SigilParser: resolver \"" + resolverName + "\" not defined.");
} else if (r instanceof IBldResolver) {
resolver = (IBldResolver) r;
} else {
throw new Error("SigilParser: resolver \"" + resolverName + "\" is not a SigilResolver.");
}
} else {
for (Object r : ivy.getSettings().getResolvers()) {
if (r instanceof IBldResolver) {
resolver = (IBldResolver) r;
break;
}
}
}
if (resolver == null) {
throw new Error("SigilParser: can't find SigilResolver. Check ivysettings.xml.");
}
} else if (config != null) {
Log.warn("creating duplicate resolver to support IvyDE.");
resolver = new SigilResolver();
((SigilResolver)resolver).setConfig(config);
}
}
return resolver;
}
}
}
/*
* this is only needed so that we can distinguish sigil-added dependencies from
* existing ones.
*/
class SigilDependencyDescriptor extends DefaultDependencyDescriptor {
public SigilDependencyDescriptor(DefaultModuleDescriptor md, ModuleRevisionId mrid, boolean force,
boolean changing, boolean transitive) {
super(md, mrid, force, changing, transitive);
}
}