/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.project; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.simantics.databoard.Bindings; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.exception.DatabaseException; import org.simantics.project.exception.ProjectException; import org.simantics.project.features.registry.GroupReference; import org.simantics.project.features.registry.IProjectFeatureExtension; import org.simantics.project.features.registry.IProjectFeatureRegistry; import org.simantics.project.features.registry.InjectedDependency; import org.simantics.project.features.registry.ProjectFeatureReference; import org.simantics.project.internal.Activator; import org.simantics.project.ontology.ProjectResource; import org.simantics.utils.datastructures.MapSet; /** * A facade for handling project feature -related tasks. * * @author Tuukka Lehtonen */ public class ProjectFeatures { /** * @return the singleton project feature registry service */ public static IProjectFeatureRegistry getRegistry() { return Activator.getDefault().getProjectFeatureRegistry(); } /** * @return */ public static IProjectFeatureExtension[] getAllProjectFeatures() { return getRegistry().getExtensions(); } /** * @return */ public static Collection getPublishedProjectFeatures() { IProjectFeatureExtension[] extensions = getAllProjectFeatures(); Collection result = new ArrayList(); for (IProjectFeatureExtension ext : extensions) { if (ext.isPublished()) result.add(ext); } return result; } /** * @return */ public static Set getInstallGroupsOfPublishedFeatures() { return getInstallGroups( getPublishedProjectFeatures() ); } /** * @param extensions * @return */ public static Set getInstallGroups(Collection extensions) { Set result = new TreeSet(); for (IProjectFeatureExtension ext : extensions) for (GroupReference grp : ext.installGroups()) result.add(grp); return result; } /** * @param filter * @return */ public static Collection filterPublishedFeatures(GroupFilter filter) { return filterExtensions( getPublishedProjectFeatures(), filter ); } private static Set filterExtensions(Collection extensions, GroupFilter filter) { Set included = new HashSet(); for (IProjectFeatureExtension ext : extensions) if (acceptExtension(ext, filter)) included.add(ext); return included; } private static boolean acceptExtension(IProjectFeatureExtension extension, GroupFilter filter) { for (GroupReference grp : extension.installGroups()) if (filter.accept(grp)) return true; return false; } /** * @param g * @param project * @return * @throws DatabaseException * @throws ProjectException */ public static Collection getTopologicallySortedFeatures(GroupFilter filter) throws DatabaseException, ProjectException { return new FeatureWalker().getSortedFeatures(filter); } /** * @param g * @param project * @return * @throws DatabaseException * @throws ProjectException */ public static Collection getTopologicallySortedFeatures(ReadGraph g, Resource project) throws DatabaseException, ProjectException { return new FeatureWalker().getSortedFeatures(g, project); } private static class FeatureWalker { private Map byId; private final MapSet required = new MapSet.Hash(); private IProjectFeatureExtension getExtension(ProjectFeatureReference ref) { return byId.get(ref.id); } public Collection getSortedFeatures(ReadGraph g, Resource project) throws DatabaseException, ProjectException { Set projectDefinedFeatures = loadFeatureReferences(g, project); return getSortedFeatures( GroupFilters.includesVersion( projectDefinedFeatures ) ); } public Collection getSortedFeatures(GroupFilter filter) throws DatabaseException, ProjectException { // Create ID -> Extension cache IProjectFeatureExtension[] allExtensions = getAllProjectFeatures(); byId = createIdMap(allExtensions); // Find which extensions are to be loaded for the feature contexts. Set included = filterExtensions(Arrays.asList(allExtensions), filter); // Add all required extensions Collection required = requiredExtensions(allExtensions, included); // Sort topologically return sortTopologically(required); } private boolean deepRequires(Set visited, IProjectFeatureExtension e1, IProjectFeatureExtension e2) { // Prevent eternal looping in cyclic cases // which are obviously erroneous definitions // but we don't want to get stuck anyway. if (visited.add(e1)) { // Check direct requires first. Set req = required.getValues(e1); if (req == null) return false; if (req.contains(e2)) return true; // Check transitively requirements for (IProjectFeatureExtension r : req) { if (deepRequires(visited, r, e2)) return true; } } return false; } private Collection sortTopologically(Collection toSort) { ArrayList sorted = new ArrayList(toSort); Collections.sort(sorted, new Comparator() { Set visited = new HashSet(); @Override public int compare(IProjectFeatureExtension e1, IProjectFeatureExtension e2) { visited.clear(); if (deepRequires(visited, e1, e2)) return 1; visited.clear(); if (deepRequires(visited, e2, e1)) return -1; return 0; } }); return sorted; } private Collection requiredExtensions(IProjectFeatureExtension[] allExtensions, Collection includedExtensions) throws ProjectException { Collection result = new ArrayList(); Set visited = new HashSet(); for (IProjectFeatureExtension ext : includedExtensions) { walkRequired(result, visited, ext); } Set includedProjectFeatureIds = new HashSet(); for (IProjectFeatureExtension ext : result) { includedProjectFeatureIds.add(ext.getId()); } // Find all injected dependencies! boolean changed = false; Set injectionsProcessedFor = new HashSet(); do { changed = false; for (IProjectFeatureExtension ext : allExtensions) { if (injectionsProcessedFor.contains(ext)) continue; for (InjectedDependency injection : ext.injections()) { if (includedProjectFeatureIds.contains(injection.to.id)) { injectionsProcessedFor.add(ext); IProjectFeatureExtension injectionTargetExt = byId.get(injection.to.id); if (injectionTargetExt != null) { changed = true; includedProjectFeatureIds.add(ext.getId()); result.add(ext); required.add(injectionTargetExt, ext); } } } } } while (changed); return result; } private void walkRequired(Collection result, Set visited, IProjectFeatureExtension extension) throws ProjectException { if (visited.add(extension)) { result.add(extension); for (ProjectFeatureReference ref : extension.requires()) { IProjectFeatureExtension reqExt = getExtension(ref); if (reqExt != null) { required.add(extension, reqExt); walkRequired(result, visited, reqExt); } else if (!ref.optional) { // This requirement was not optional, must report error! throw new ProjectException("Missing org.simantics.project.feature extension with id '" + ref.id + "' required by feature '" + extension.getId() + "'"); } } } } } private static Set loadFeatureReferences(ReadGraph graph, Resource project) throws DatabaseException { ProjectResource PROJ = ProjectResource.getInstance(graph); Set features = new HashSet(); for (Resource fs : graph.getObjects(project, PROJ.HasFeature)) { String refStr = graph.getRelatedValue(fs, PROJ.HasGroupId, Bindings.STRING); try { GroupReference ref = parseFeatureReference(refStr); features.add(ref); } catch (IllegalArgumentException e) { // Parse error! e.printStackTrace(); } } // See http://dev.simantics.org/index.php/Project_Development#Omnipresent_Project_Features features.add(GroupReference.OMNIPRESENT); return features; } private static Map createIdMap(IProjectFeatureExtension[] extensions) { Map byId = new HashMap(); for (IProjectFeatureExtension ext : extensions) { IProjectFeatureExtension e = byId.put(ext.getId(), ext); if (e != null) { Activator.getDefault().getLog().log( new Status(IStatus.WARNING, Activator.PLUGIN_ID, "Multiple org.simantics.project.feature extensions defined with id '" + ext.getId() + "'.\nprevious: " + e + "\nnew: " + ext) ); } } return byId; } static String ID_PATTERN_STRING = "[a-zA-Z_0-9]+(?:\\.[a-zA-Z_0-9]+)*"; static String VERSION_PATTERN_STRING = ".*"; static Pattern POSSIBLY_VERSIONED_ID_PATTERN = Pattern.compile("(" + ID_PATTERN_STRING + ")(?:/(" + VERSION_PATTERN_STRING + "))?"); /** * @param reference * @return * @throws IllegalArgumentException if the reference string cannot be parsed */ private static GroupReference parseFeatureReference(String reference) { Matcher m = POSSIBLY_VERSIONED_ID_PATTERN.matcher(reference); if (m.matches()) { return new GroupReference(m.group(1), m.group(2)); } else { throw new IllegalArgumentException("could not parse feature reference " + reference); } } }