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