/******************************************************************************* * Copyright (c) 2006-2013, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. ******************************************************************************/ package org.eclipse.buckminster.pde.tasks; import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.buckminster.core.helpers.TextUtils; import org.eclipse.buckminster.core.version.VersionHelper; import org.eclipse.buckminster.pde.IPDEConstants; import org.eclipse.buckminster.pde.PDEPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.equinox.internal.p2.metadata.IRequiredCapability; import org.eclipse.equinox.internal.p2.updatesite.SiteCategory; import org.eclipse.equinox.internal.p2.updatesite.SiteFeature; import org.eclipse.equinox.internal.p2.updatesite.SiteIU; import org.eclipse.equinox.internal.p2.updatesite.SiteModel; import org.eclipse.equinox.p2.metadata.IArtifactKey; import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.equinox.p2.metadata.IProvidedCapability; import org.eclipse.equinox.p2.metadata.IRequirement; import org.eclipse.equinox.p2.metadata.IVersionedId; import org.eclipse.equinox.p2.metadata.MetadataFactory; import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitDescription; import org.eclipse.equinox.p2.metadata.Version; import org.eclipse.equinox.p2.metadata.VersionRange; import org.eclipse.equinox.p2.metadata.VersionedId; import org.eclipse.equinox.p2.publisher.AbstractPublisherAction; import org.eclipse.equinox.p2.publisher.IPublisherInfo; import org.eclipse.equinox.p2.publisher.IPublisherResult; import org.eclipse.equinox.p2.query.IQuery; import org.eclipse.equinox.p2.query.IQueryResult; import org.eclipse.equinox.p2.query.QueryUtil; import org.eclipse.equinox.spi.p2.publisher.LocalizationHelper; import org.eclipse.equinox.spi.p2.publisher.PublisherHelper; /** * Action which processes a feature.xml, build.properties, and feature * localization files and generates categories, mirrors url, and referenced * repositories for a p2 MDR. The process relies on IUs for the various features * to have already been generated. */ @SuppressWarnings("restriction") public class CategoriesAction extends AbstractPublisherAction { private static class Category { private String description; private String label; private final String name; Category(String name) { this.name = name; } @Override public boolean equals(Object value) { return value instanceof Category && ((Category) value).name.equals(name); } public String getDescription() { return description; } public String getLabel() { return label; } public String getName() { return name; } @Override public int hashCode() { return name.hashCode(); } public void setDescription(String description) { this.description = description; } public void setLabel(String label) { this.label = label; } } private static final String PROP_CATEGORY_DESCRIPTION_PREFIX = "category.description."; //$NON-NLS-1$ private static final String PROP_CATEGORY_MEMBERS_PREFIX = "category.members."; //$NON-NLS-1$ private static final String PROP_CATEGORY_DEFAULT = "category.default"; //$NON-NLS-1$ private static final String PROP_CATEGORY_ID_PREFIX = "category.id."; //$NON-NLS-1$ /** * This category is excluded from categorized view. */ private static final String HIDDEN_CATEGORY_ID = "hidden_category"; //$NON-NLS-1$ private static final Pattern idAndVersionPattern = Pattern.compile("^(\\S)_([0-9]+(?:\\.[0-9]+){0,2}(?:\\.[A-Za-z0-9_-]))$"); //$NON-NLS-1$ private static final List<Category> defaultCategoryList; static { Category defaultCategory = new Category("Default"); //$NON-NLS-1$ defaultCategory.setDescription("Default category for otherwise uncategorized features"); //$NON-NLS-1$ defaultCategory.setLabel("Uncategorized"); //$NON-NLS-1$ defaultCategoryList = Collections.singletonList(defaultCategory); } /** * Return localizations for %xxx properties found in <code>properties</code> * . * * @param properties * @param featureRoot * @return */ private static Map<Locale, Map<String, String>> getLocalizations(Map<String, String> properties, File featureRoot) { if (featureRoot == null || properties == null) return Collections.emptyMap(); List<String> msgKeys = null; for (String value : properties.values()) { if (value != null && value.length() > 1 && value.charAt(0) == '%') { if (msgKeys == null) msgKeys = new ArrayList<String>(); msgKeys.add(value.substring(1)); } } if (msgKeys == null) return Collections.emptyMap(); Map<Locale, Map<String, String>> localizations; String[] keyStrings = msgKeys.toArray(new String[msgKeys.size()]); if (featureRoot.isDirectory()) localizations = LocalizationHelper.getDirPropertyLocalizations(featureRoot, "feature", null, keyStrings); //$NON-NLS-1$ else if (featureRoot.getName().endsWith(".jar")) //$NON-NLS-1$ localizations = LocalizationHelper.getJarPropertyLocalizations(featureRoot, "feature", null, keyStrings); //$NON-NLS-1$ else localizations = Collections.emptyMap(); return localizations; } private static boolean isKeyReference(String value, String key) { return value != null && value.length() > 1 && value.charAt(0) == '%' && value.substring(1).equals(key); } private final Map<Locale, Map<String, String>> localizations; private final Map<String, String> buildProperties; private final List<IVersionedId> featureEntries; private final File projectRoot; public CategoriesAction(File projectRoot, Map<String, String> buildProperties, List<IVersionedId> featureEntries) throws CoreException { this.buildProperties = buildProperties; this.localizations = getLocalizations(buildProperties, projectRoot); this.featureEntries = featureEntries; this.projectRoot = projectRoot; } /** * Creates an IU corresponding to an update site category * * @param category * The category descriptor * @param featureIUs * The IUs of the features that belong to the category * @param parentCategory * The parent category, or <code>null</code> * @return an IU representing the category */ public IInstallableUnit createCategoryIU(Category category, Set<IInstallableUnit> featureIUs, IInstallableUnit parentCategory) { InstallableUnitDescription cat = new MetadataFactory.InstallableUnitDescription(); cat.setSingleton(true); String categoryId = category.getName(); cat.setId(categoryId); cat.setProperty(IInstallableUnit.PROP_NAME, category.getLabel()); cat.setProperty(IInstallableUnit.PROP_DESCRIPTION, category.getDescription()); ArrayList<IVersionedId> fts = new ArrayList<IVersionedId>(featureIUs.size()); ArrayList<IVersionedId> bds = new ArrayList<IVersionedId>(featureIUs.size()); ArrayList<IRequirement> reqsConfigurationUnits = new ArrayList<IRequirement>(featureIUs.size()); for (IInstallableUnit iu : featureIUs) { VersionedId vn = new VersionedId(iu.getId(), iu.getVersion()); if (iu.getId().endsWith(IPDEConstants.FEATURE_GROUP)) fts.add(vn); else bds.add(vn); VersionRange range = new VersionRange(iu.getVersion(), true, iu.getVersion(), true); reqsConfigurationUnits.add(MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, iu.getId(), range, iu.getFilter(), false, false)); } FeatureVersionSuffixGenerator suffixGen = new FeatureVersionSuffixGenerator(-1, -1); Version categoryVersion = Version.createOSGi(0, 0, 1, suffixGen.generateSuffix(fts, bds)); cat.setVersion(categoryVersion); // note that update sites don't currently support nested categories, but // it may be useful to add in the future if (parentCategory != null) { reqsConfigurationUnits.add(MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, parentCategory.getId(), VersionRange.emptyRange, parentCategory.getFilter(), false, false)); } cat.setRequirements(reqsConfigurationUnits.toArray(new IRequiredCapability[reqsConfigurationUnits.size()])); // Create set of provided capabilities ArrayList<IProvidedCapability> providedCapabilities = new ArrayList<IProvidedCapability>(); providedCapabilities.add(PublisherHelper.createSelfCapability(categoryId, categoryVersion)); for (Map.Entry<Locale, Map<String, String>> locEntry : localizations.entrySet()) { Locale locale = locEntry.getKey(); for (Map.Entry<String, String> entry : locEntry.getValue().entrySet()) { String key = entry.getKey(); // Is the category using this key? // if (isKeyReference(category.getLabel(), key) || isKeyReference(category.getDescription(), key)) cat.setProperty(locale.toString() + '.' + key, entry.getValue()); } providedCapabilities.add(PublisherHelper.makeTranslationCapability(categoryId, locale)); } cat.setCapabilities(providedCapabilities.toArray(new IProvidedCapability[providedCapabilities.size()])); cat.setArtifacts(new IArtifactKey[0]); cat.setProperty(InstallableUnitDescription.PROP_TYPE_CATEGORY, "true"); //$NON-NLS-1$ return MetadataFactory.createInstallableUnit(cat); } @Override public IStatus perform(IPublisherInfo publisherInfo, IPublisherResult results, IProgressMonitor monitor) { Map<Category, Set<IInstallableUnit>> categoriesToFeatureIUs = new HashMap<Category, Set<IInstallableUnit>>(); Map<IInstallableUnit, List<Category>> featuresToCategories; try { featuresToCategories = getFeatureToCategoryMappings(publisherInfo, results, monitor); } catch (CoreException e) { return e.getStatus(); } for (Map.Entry<IInstallableUnit, List<Category>> entry : featuresToCategories.entrySet()) { IInstallableUnit iu = entry.getKey(); for (Category category : entry.getValue()) { Set<IInstallableUnit> featureIUs = categoriesToFeatureIUs.get(category); if (featureIUs == null) { featureIUs = new HashSet<IInstallableUnit>(); categoriesToFeatureIUs.put(category, featureIUs); } featureIUs.add(iu); } } generateCategoryIUs(categoriesToFeatureIUs, results); return Status.OK_STATUS; } private StringBuilder collectIDs(Set<IInstallableUnit> iUs) { StringBuilder strBuilder = new StringBuilder(); for (Iterator<IInstallableUnit> iterator = iUs.iterator(); iterator.hasNext();) { IInstallableUnit iInstallableUnit = iterator.next(); strBuilder.append(iInstallableUnit.getId()); if (iterator.hasNext()) { strBuilder.append(", "); //$NON-NLS-1$ } } return strBuilder; } /** * Generates IUs corresponding to update site categories.<br> * Note: "hidden-category" is explicitly excluded. * * @param categoriesToFeatures * Map of Category ->Set (Feature IUs in that category). * @param result * The generator result being built */ private void generateCategoryIUs(Map<Category, Set<IInstallableUnit>> categoriesToFeatures, IPublisherResult result) { for (Map.Entry<Category, Set<IInstallableUnit>> entry : categoriesToFeatures.entrySet()) { Category category = entry.getKey(); Set<IInstallableUnit> iUs = entry.getValue(); if (!HIDDEN_CATEGORY_ID.equals(category.getName())) { result.addIU(createCategoryIU(category, iUs, null), IPublisherResult.NON_ROOT); } else { if (PDEPlugin.getLogger().isDebugEnabled()) { StringBuilder strBuilder = collectIDs(iUs); PDEPlugin .getLogger() .debug("Category %s is used. Following features will be hidden in categorized view: %s", HIDDEN_CATEGORY_ID, strBuilder.toString()); //$NON-NLS-1$ } } } } private IInstallableUnit getFeatureIU(String name, Version version, IPublisherInfo publisherInfo, IPublisherResult results, IProgressMonitor monitor) { if (monitor.isCanceled()) throw new OperationCanceledException(); String id = name + ".feature.group"; //$NON-NLS-1$ IQuery<IInstallableUnit> query = null; if (version == null || version.equals(Version.emptyVersion)) query = QueryUtil.createLatestQuery(QueryUtil.createIUQuery(id)); else { String qual = VersionHelper.getQualifier(version); if (qual != null && qual.contains("qualifier")) //$NON-NLS-1$ { // We won't find an IU that matches this version. We need to use // a version range. // Version low = VersionHelper.replaceQualifier(version, null); org.osgi.framework.Version ov = new org.osgi.framework.Version(version.toString()); query = QueryUtil.createIUQuery(id, new VersionRange(low, true, Version.createOSGi(ov.getMajor(), ov.getMinor(), ov.getMicro() + 1), false)); } else query = QueryUtil.createIUQuery(id, version); query = QueryUtil.createLimitQuery(query, 1); } IQueryResult<IInstallableUnit> result = results.query(query, monitor); if (result.isEmpty()) result = publisherInfo.getMetadataRepository().query(query, null); if (result.isEmpty() && publisherInfo.getContextMetadataRepository() != null) result = publisherInfo.getContextMetadataRepository().query(query, null); if (!result.isEmpty()) return result.iterator().next(); return null; } private Map<IInstallableUnit, List<Category>> getFeatureToCategoryMappings(IPublisherInfo publisherInfo, IPublisherResult results, IProgressMonitor monitor) throws CoreException { HashMap<IInstallableUnit, List<Category>> mappings = new HashMap<IInstallableUnit, List<Category>>(); Map<String, Category> categories = new HashMap<String, Category>(); for (Map.Entry<String, String> entry : buildProperties.entrySet()) { String key = entry.getKey(); if (key.startsWith(PROP_CATEGORY_ID_PREFIX)) { String id = key.substring(PROP_CATEGORY_ID_PREFIX.length()); Category cat = categories.get(id); if (cat == null) { cat = new Category(id); categories.put(id, cat); } cat.setLabel(entry.getValue()); } } for (Map.Entry<String, String> entry : buildProperties.entrySet()) { String key = entry.getKey(); if (key.startsWith(PROP_CATEGORY_DESCRIPTION_PREFIX)) { String id = key.substring(PROP_CATEGORY_DESCRIPTION_PREFIX.length()); Category cat = categories.get(id); if (cat != null) cat.setDescription(entry.getValue()); continue; } if (key.startsWith(PROP_CATEGORY_MEMBERS_PREFIX)) { String id = key.substring(PROP_CATEGORY_MEMBERS_PREFIX.length()); Category cat = categories.get(id); if (cat == null) continue; for (String name : TextUtils.splitAndTrim(entry.getValue(), ",")) //$NON-NLS-1$ { Version version = null; Matcher m = idAndVersionPattern.matcher(name); if (m.matches()) { name = m.group(1); version = Version.create(m.group(2)); } IInstallableUnit iu = getFeatureIU(name, version, publisherInfo, results, monitor); if (iu == null) continue; List<Category> catList = mappings.get(iu); if (catList == null) { catList = new ArrayList<Category>(); mappings.put(iu, catList); catList.add(cat); } else if (!catList.contains(cat)) catList.add(cat); } continue; } } List<Category> defaultCategories = defaultCategoryList; String defaultCategory = buildProperties.get(PROP_CATEGORY_DEFAULT); if (defaultCategory != null) { Category cat = categories.get(defaultCategory); if (cat != null) defaultCategories = Collections.singletonList(cat); } try { SiteModel site = SiteReader.getSite(new File(projectRoot, "category.xml")); //$NON-NLS-1$ for (SiteFeature feature : site.getFeatures()) { IInstallableUnit iu = getFeatureIU(feature.getFeatureIdentifier(), Version.create(feature.getFeatureVersion()), publisherInfo, results, monitor); if (iu == null) continue; for (String id : feature.getCategoryNames()) { Category cat = categories.get(id); if (cat == null) { SiteCategory siteCat = site.getCategory(id); if (siteCat == null) continue; cat = new Category(id); cat.setDescription(siteCat.getDescription()); cat.setLabel(siteCat.getLabel()); categories.put(id, cat); } List<Category> catList = mappings.get(iu); if (catList == null) { catList = new ArrayList<Category>(); mappings.put(iu, catList); catList.add(cat); } else if (!catList.contains(cat)) catList.add(cat); } } for (SiteIU siteIU : site.getIUs()) { for (IInstallableUnit iu : getIUs(siteIU, publisherInfo, results)) { for (String id : siteIU.getCategoryNames()) { Category cat = categories.get(id); if (cat == null) { SiteCategory siteCat = site.getCategory(id); if (siteCat == null) continue; cat = new Category(id); cat.setDescription(siteCat.getDescription()); cat.setLabel(siteCat.getLabel()); categories.put(id, cat); } List<Category> catList = mappings.get(iu); if (catList == null) { catList = new ArrayList<Category>(); mappings.put(iu, catList); catList.add(cat); } else if (!catList.contains(cat)) catList.add(cat); } } } } catch (FileNotFoundException e) { // This is expected. Just ignore } for (IVersionedId fe : featureEntries) { IInstallableUnit iu = getFeatureIU(fe.getId(), fe.getVersion(), publisherInfo, results, monitor); if (iu == null || mappings.containsKey(iu)) continue; mappings.put(iu, defaultCategories); } return mappings; } private Collection<IInstallableUnit> getIUs(SiteIU siteIU, IPublisherInfo publisherInfo, IPublisherResult results) { String id = siteIU.getID(); String range = siteIU.getRange(); String type = siteIU.getQueryType(); String expression = siteIU.getQueryExpression(); Object[] params = siteIU.getQueryParams(); if (id == null && (type == null || expression == null)) return Collections.emptyList(); IQuery<IInstallableUnit> query = null; if (id != null) { VersionRange vRange = new VersionRange(range); query = QueryUtil.createIUQuery(id, vRange); } else if (type.equals("context")) { //$NON-NLS-1$ query = QueryUtil.createQuery(expression, params); } else if (type.equals("match")) //$NON-NLS-1$ query = QueryUtil.createMatchQuery(expression, params); if (query == null) return Collections.emptyList(); IQueryResult<IInstallableUnit> queryResult = results.query(query, null); if (queryResult.isEmpty()) queryResult = publisherInfo.getMetadataRepository().query(query, null); if (queryResult.isEmpty() && publisherInfo.getContextMetadataRepository() != null) queryResult = publisherInfo.getContextMetadataRepository().query(query, null); return queryResult.toUnmodifiableSet(); } }