Allow fragment requirement version ranges to differ from hosts by
calculating the intersecting version range. (FELIX-1919)


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@902252 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/framework/src/main/java/org/apache/felix/framework/FelixResolverState.java b/framework/src/main/java/org/apache/felix/framework/FelixResolverState.java
index 3708324..0d9594e 100644
--- a/framework/src/main/java/org/apache/felix/framework/FelixResolverState.java
+++ b/framework/src/main/java/org/apache/felix/framework/FelixResolverState.java
@@ -27,6 +27,7 @@
 import org.apache.felix.framework.searchpolicy.ResolveException;
 import org.apache.felix.framework.searchpolicy.Resolver;
 import org.apache.felix.framework.util.Util;
+import org.apache.felix.framework.util.VersionRange;
 import org.apache.felix.framework.util.manifestparser.R4Attribute;
 import org.apache.felix.framework.util.manifestparser.R4Directive;
 import org.apache.felix.framework.util.manifestparser.Requirement;
@@ -370,9 +371,9 @@
                     new Object[] { host, reqs[reqIdx] });
             }
         }
-        // Loop through each fragment verifying it does no conflict,
-        // adding its package and bundle dependencies if they do not
-        // conflict and removing the fragment if it does conflict.
+        // Loop through each fragment verifying it does not conflict.
+        // Add its package and bundle dependencies if they do not
+        // conflict or remove the fragment if it does conflict.
         for (Iterator it = fragmentList.iterator(); it.hasNext(); )
         {
             IModule fragment = (IModule) it.next();
@@ -416,6 +417,20 @@
                         // No need to finish processing current fragment.
                         break;
                     }
+                    else
+                    {
+                        // If there is an overlapping requirement for the existing
+                        // target, then try to calculate the intersecting requirement
+                        // and set the existing requirement to that instead. This
+                        // makes it so version ranges do not have to be exact, just
+                        // overlapping.
+                        Requirement intersection = calculateVersionIntersection(
+                            (Requirement) existing[REQ_IDX], (Requirement) reqs[reqIdx]);
+                        if (intersection != existing[REQ_IDX])
+                        {
+                            existing[REQ_IDX] = intersection;
+                        }
+                    }
                 }
             }
 
@@ -448,8 +463,23 @@
         {
             return false;
         }
-        // If the target version range is not the same, then they conflict.
-        if (!existing.getTargetVersionRange().equals(additional.getTargetVersionRange()))
+        // If the existing version range floor is greater than the additional
+        // version range's floor, then they are inconflict since we cannot
+        // widen the constraint.
+        if (existing.getTargetVersionRange().getLow().compareTo(
+            additional.getTargetVersionRange().getLow()) > 0)
+        {
+            return true;
+        }
+        // If the existing version range ceiling is less than the additional
+        // version range's ceiling, then they are inconflict since we cannot
+        // widen the constraint.
+        if (((existing.getTargetVersionRange().getHigh() != null)
+            && (additional.getTargetVersionRange().getHigh() == null))
+            || ((existing.getTargetVersionRange().getHigh() != null)
+                && (additional.getTargetVersionRange().getHigh() != null)
+                && (existing.getTargetVersionRange().getHigh().compareTo(
+                    additional.getTargetVersionRange().getHigh()) < 0)))
         {
             return true;
         }
@@ -457,51 +487,33 @@
         // the existing requirement is not optional, then it doesn't matter
         // what subsequent requirements are since non-optional is stronger
         // than optional.
-        if (existing.isOptional() && (existing.isOptional() != additional.isOptional()))
+        if (existing.isOptional() && !additional.isOptional())
         {
             return true;
         }
         // Verify directives are the same.
-        // This is sort of ugly, but we need to remove
-        // the resolution directive, since it is effectively
-        // test above when checking optionality.
-        // Put directives in a map, since ordering is arbitrary.
         final R4Directive[] exDirs = (existing.getDirectives() == null)
             ? new R4Directive[0] : existing.getDirectives();
+        final R4Directive[] addDirs = (additional.getDirectives() == null)
+            ? new R4Directive[0] : additional.getDirectives();
+        // Put attributes in a map, since ordering is arbitrary.
         final Map exDirMap = new HashMap();
         for (int i = 0; i < exDirs.length; i++)
         {
-            if (!exDirs[i].getName().equals(Constants.RESOLUTION_DIRECTIVE))
-            {
-                exDirMap.put(exDirs[i].getName(), exDirs[i]);
-            }
+            exDirMap.put(exDirs[i].getName(), exDirs[i]);
         }
-        final R4Directive[] addDirs = (additional.getDirectives() == null)
-            ? new R4Directive[0] : additional.getDirectives();
-        final Map addDirMap = new HashMap();
+        // If attribute values do not match, then they conflict.
         for (int i = 0; i < addDirs.length; i++)
         {
+            // Ignore resolution directive, since we've already tested it above.
             if (!addDirs[i].getName().equals(Constants.RESOLUTION_DIRECTIVE))
             {
-                addDirMap.put(addDirs[i].getName(), addDirs[i]);
-            }
-        }
-        // If different number of directives, then they conflict.
-        if (exDirMap.size() != addDirMap.size())
-        {
-            return true;
-        }
-        // If directive values do not match, then they conflict.
-        for (Iterator it = addDirMap.entrySet().iterator(); it.hasNext(); )
-        {
-            final Map.Entry entry = (Map.Entry) it.next();
-            final String name = (String) entry.getKey();
-            final R4Directive addDir = (R4Directive) entry.getValue();
-            final R4Directive exDir = (R4Directive) exDirMap.get(name);
-            if ((exDir == null) ||
-                !exDir.getValue().equals(addDir.getValue()))
-            {
-                return true;
+                final R4Directive exDir = (R4Directive) exDirMap.get(addDirs[i].getName());
+                if ((exDir == null) ||
+                    !exDir.getValue().equals(addDirs[i].getValue()))
+                {
+                    return true;
+                }
             }
         }
         // Verify attributes are the same.
@@ -509,11 +521,6 @@
             ? new R4Attribute[0] : existing.getAttributes();
         final R4Attribute[] addAttrs = (additional.getAttributes() == null)
             ? new R4Attribute[0] : additional.getAttributes();
-        // If different number of attributes, then they conflict.
-        if (exAttrs.length != addAttrs.length)
-        {
-            return true;
-        }
         // Put attributes in a map, since ordering is arbitrary.
         final Map exAttrMap = new HashMap();
         for (int i = 0; i < exAttrs.length; i++)
@@ -523,18 +530,81 @@
         // If attribute values do not match, then they conflict.
         for (int i = 0; i < addAttrs.length; i++)
         {
-            final R4Attribute exAttr = (R4Attribute) exAttrMap.get(addAttrs[i].getName());
-            if ((exAttr == null) ||
-                !exAttr.getValue().equals(addAttrs[i].getValue()) ||
-                (exAttr.isMandatory() != addAttrs[i].isMandatory()))
+            // Ignore version property, since we've already tested it above.
+            if (!(additional.getNamespace().equals(ICapability.PACKAGE_NAMESPACE)
+                && addAttrs[i].getName().equals(ICapability.VERSION_PROPERTY))
+                && !(additional.getNamespace().equals(ICapability.MODULE_NAMESPACE)
+                    && addAttrs[i].getName().equals(Constants.BUNDLE_VERSION_ATTRIBUTE)))
             {
-                return true;
+                final R4Attribute exAttr = (R4Attribute) exAttrMap.get(addAttrs[i].getName());
+                if ((exAttr == null) ||
+                    !exAttr.getValue().equals(addAttrs[i].getValue()) ||
+                    (exAttr.isMandatory() != addAttrs[i].isMandatory()))
+                {
+                    return true;
+                }
             }
         }
         // They do no conflict.
         return false;
     }
 
+    static Requirement calculateVersionIntersection(
+        Requirement existing, Requirement additional)
+    {
+        Requirement intersection = existing;
+        int existVersionIdx = -1, addVersionIdx = -1;
+
+        // Find the existing version attribute.
+        for (int i = 0; (existVersionIdx < 0) && (i < existing.getAttributes().length); i++)
+        {
+            if ((existing.getNamespace().equals(ICapability.PACKAGE_NAMESPACE)
+                && existing.getAttributes()[i].getName().equals(ICapability.VERSION_PROPERTY))
+                || (existing.getNamespace().equals(ICapability.MODULE_NAMESPACE)
+                    && existing.getAttributes()[i].getName().equals(Constants.BUNDLE_VERSION_ATTRIBUTE)))
+            {
+                existVersionIdx = i;
+            }
+        }
+
+        // Find the additional version attribute.
+        for (int i = 0; (addVersionIdx < 0) && (i < additional.getAttributes().length); i++)
+        {
+            if ((additional.getNamespace().equals(ICapability.PACKAGE_NAMESPACE)
+                && additional.getAttributes()[i].getName().equals(ICapability.VERSION_PROPERTY))
+                || (additional.getNamespace().equals(ICapability.MODULE_NAMESPACE)
+                    && additional.getAttributes()[i].getName().equals(Constants.BUNDLE_VERSION_ATTRIBUTE)))
+            {
+                addVersionIdx = i;
+            }
+        }
+
+        // Use the additional requirement's version range if it
+        // has one and the existing requirement does not.
+        if ((existVersionIdx == -1) && (addVersionIdx != -1))
+        {
+            intersection = additional;
+        }
+        // If both requirements have version ranges, then create
+        // a new requirement with an intersecting version range.
+        else if ((existVersionIdx != -1) && (addVersionIdx != -1))
+        {
+            VersionRange vr = ((VersionRange) existing.getAttributes()[existVersionIdx].getValue())
+                .intersection((VersionRange) additional.getAttributes()[addVersionIdx].getValue());
+            R4Attribute[] attrs = existing.getAttributes();
+            R4Attribute[] newAttrs = new R4Attribute[attrs.length];
+            System.arraycopy(attrs, 0, newAttrs, 0, attrs.length);
+            newAttrs[existVersionIdx] = new R4Attribute(
+                attrs[existVersionIdx].getName(), vr, false);
+            intersection = new Requirement(
+                existing.getNamespace(),
+                existing.getDirectives(),
+                newAttrs);
+        }
+
+        return intersection;
+    }
+
     private void addHost(IModule host)
     {
         // When a module is added, we first need to pre-merge any potential fragments
diff --git a/framework/src/main/java/org/apache/felix/framework/ModuleImpl.java b/framework/src/main/java/org/apache/felix/framework/ModuleImpl.java
index 24bc19e..dfcd368 100644
--- a/framework/src/main/java/org/apache/felix/framework/ModuleImpl.java
+++ b/framework/src/main/java/org/apache/felix/framework/ModuleImpl.java
@@ -305,8 +305,13 @@
     {
         if (m_cachedRequirements == null)
         {
-            List reqList = (m_requirements == null)
-                ? new ArrayList() : new ArrayList(Arrays.asList(m_requirements));
+            Map reqMap = new HashMap();
+            for (int i = 0; (m_requirements != null) && i < m_requirements.length; i++)
+            {
+                reqMap.put(((Requirement) m_requirements[i]).getTargetName(), m_requirements[i]);
+            }
+
+            // Aggregate host and fragment requirements.
             for (int fragIdx = 0;
                 (m_fragments != null) && (fragIdx < m_fragments.length);
                 fragIdx++)
@@ -319,12 +324,25 @@
                     if (reqs[reqIdx].getNamespace().equals(ICapability.PACKAGE_NAMESPACE)
                         || reqs[reqIdx].getNamespace().equals(ICapability.MODULE_NAMESPACE))
                     {
-                        reqList.add(reqs[reqIdx]);
+                        // If the current fragment requirement overlaps a previously
+                        // added requirement, then calculate a new intersecting requirement.
+                        Requirement req = (Requirement) reqMap.get(
+                            ((Requirement) reqs[reqIdx]).getTargetName());
+                        if (req != null)
+                        {
+                            req = FelixResolverState.calculateVersionIntersection(
+                                req, (Requirement) reqs[reqIdx]);
+                        }
+                        else
+                        {
+                            req = (Requirement) reqs[reqIdx];
+                        }
+                        reqMap.put(req.getTargetName(), req);
                     }
                 }
             }
             m_cachedRequirements = (IRequirement[])
-                reqList.toArray(new IRequirement[reqList.size()]);
+                reqMap.values().toArray(new IRequirement[reqMap.size()]);
         }
         return m_cachedRequirements;
     }
diff --git a/framework/src/main/java/org/apache/felix/framework/util/VersionRange.java b/framework/src/main/java/org/apache/felix/framework/util/VersionRange.java
index 49e5379..c313d9d 100644
--- a/framework/src/main/java/org/apache/felix/framework/util/VersionRange.java
+++ b/framework/src/main/java/org/apache/felix/framework/util/VersionRange.java
@@ -80,6 +80,45 @@
         return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) < 0);
     }
 
+    public VersionRange intersection(VersionRange vr)
+    {
+        VersionRange floor = (m_low.compareTo(vr.getLow()) > 0) ? this : vr;
+        boolean floorInclusive = (getLow().equals(vr.getLow()))
+            ? (isLowInclusive() & vr.isLowInclusive())
+            : floor.isLowInclusive();
+
+        VersionRange ceiling;
+        boolean ceilingInclusive;
+        if (vr.getHigh() == null)
+        {
+            ceiling = this;
+            ceilingInclusive = ceiling.isHighInclusive();
+        }
+        else if (m_high == null)
+        {
+            ceiling = vr;
+            ceilingInclusive = ceiling.isHighInclusive();
+        }
+        else if (m_high.compareTo(vr.getHigh()) > 0)
+        {
+            ceiling = vr;
+            ceilingInclusive = ceiling.isHighInclusive();
+        }
+        else if (m_high.compareTo(vr.getHigh()) < 0)
+        {
+            ceiling = this;
+            ceilingInclusive = ceiling.isHighInclusive();
+        }
+        else
+        {
+            ceiling = this;
+            ceilingInclusive = (isHighInclusive() & vr.isHighInclusive());
+        }
+
+        return new VersionRange(
+            floor.getLow(), floorInclusive, ceiling.getHigh(), ceilingInclusive);
+    }
+
     public static VersionRange parse(String range)
     {
         // Check if the version is an interval.