Initial source commit.


git-svn-id: https://svn.apache.org/repos/asf/incubator/oscar/trunk@233031 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/org/apache/osgi/bundle/bundlerepository/Activator.java b/src/org/apache/osgi/bundle/bundlerepository/Activator.java
new file mode 100644
index 0000000..c50192e
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/Activator.java
@@ -0,0 +1,57 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class Activator implements BundleActivator
+{
+    private transient BundleContext m_context = null;
+    private transient BundleRepositoryImpl m_br = null;
+
+    public void start(BundleContext context)
+    {
+        m_context = context;
+
+        // Register bundle repository service.
+        m_br = new BundleRepositoryImpl(m_context);
+        context.registerService(
+            org.apache.osgi.service.bundlerepository.BundleRepository.class.getName(),
+            m_br, null);
+
+        // We dynamically import the shell service API, so it
+        // might not actually be available, so be ready to catch
+        // the exception when we try to register the command service.
+        try
+        {
+            // Register "obr" shell command service as a
+            // wrapper for the bundle repository service.
+            context.registerService(
+                org.apache.osgi.service.shell.Command.class.getName(),
+                new ObrCommandImpl(m_context, m_br), null);
+        }
+        catch (Throwable th)
+        {
+            // Ignore.
+        }
+    }
+
+    public void stop(BundleContext context)
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/BundleRepositoryImpl.java b/src/org/apache/osgi/bundle/bundlerepository/BundleRepositoryImpl.java
new file mode 100644
index 0000000..07f5828
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/BundleRepositoryImpl.java
@@ -0,0 +1,274 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.io.PrintStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.osgi.service.bundlerepository.*;
+import org.osgi.framework.*;
+
+public class BundleRepositoryImpl implements BundleRepository
+{
+    private BundleContext m_context = null;
+    private RepositoryState m_repo = null;
+
+    public BundleRepositoryImpl(BundleContext context)
+    {
+        m_context = context;
+        m_repo = new RepositoryState(m_context);
+    }
+
+    public String[] getRepositoryURLs()
+    {
+        return m_repo.getURLs();
+    }
+
+    public synchronized void setRepositoryURLs(String[] urls)
+    {
+        m_repo.setURLs(urls);
+    }
+
+    /**
+     * Get the number of bundles available in the repository.
+     * @return the number of available bundles.
+    **/
+    public synchronized BundleRecord[] getBundleRecords()
+    {
+        return m_repo.getRecords();
+    }
+
+    /**
+     * Get the specified bundle record from the repository.
+     * @param i the bundle record index to retrieve.
+     * @return the associated bundle record or <tt>null</tt>.
+    **/
+    public synchronized BundleRecord[] getBundleRecords(String symName)
+    {
+        return m_repo.getRecords(symName);
+    }
+
+    /**
+     * Get bundle record for the bundle with the specified name
+     * and version from the repository.
+     * @param name the bundle record name to retrieve.
+     * @param version three-interger array of the version associated with
+     *        the name to retrieve.
+     * @return the associated bundle record or <tt>null</tt>.
+    **/
+    public synchronized BundleRecord getBundleRecord(String symName, int[] version)
+    {
+        return m_repo.getRecord(symName, version);
+    }
+
+    public boolean deployBundle(
+        PrintStream out, PrintStream err, String symName, int[] version,
+        boolean isResolve, boolean isStart)
+    {
+        // List to hold bundles that need to be started.
+        List startList = null;
+
+        // Get the bundle record of the remote bundle to be deployed.
+        BundleRecord targetRecord = m_repo.getRecord(symName, version);
+        if (targetRecord == null)
+        {
+            err.println("No such bundle in repository.");
+            return false;
+        }
+
+        // Create an editable snapshot of the current set of
+        // locally installed bundles.
+        LocalState localState = new LocalState(m_context);
+
+        // If the precise bundle is already deployed, then we are done.
+        if (localState.findBundle(symName, version) != null)
+        {
+            return true;
+        }
+
+        // Create the transitive closure all bundles that must be
+        // deployed as a result of deploying the target bundle;
+        // use a list because this will keep everything in order.
+        BundleRecord[] deployRecords = null;
+        // If the resolve flag is set, then get its imports to
+        // calculate the transitive closure of its dependencies.
+        if (isResolve)
+        {
+//            Package[] imports = (Package[])
+//                targetRecord.getAttribute(BundleRecord.IMPORT_PACKAGE);
+            Filter[] reqs = (Filter[])
+                targetRecord.getAttribute("requirement");
+            try
+            {
+                deployRecords = m_repo.resolvePackages(localState, reqs);
+            }
+            catch (ResolveException ex)
+            {
+                err.println("Resolve error: " + ex.getPackage());
+                return false;
+            }
+        }
+
+        // Add the target bundle since it will not be
+        // included in the array of records to deploy.
+        if (deployRecords == null)
+        {
+            deployRecords = new BundleRecord[] { targetRecord };
+        }
+        else
+        {
+            // Create a new array containing the target and put it first,
+            // since the array will be process in reverse order.
+            BundleRecord[] newRecs = new BundleRecord[deployRecords.length + 1];
+            newRecs[0] = targetRecord;
+            System.arraycopy(deployRecords, 0, newRecs, 1, deployRecords.length);
+            deployRecords = newRecs;
+        }
+
+        // Now iterate through all bundles in the deploy list
+        // in reverse order and deploy each; the order is not
+        // so important, but by reversing them at least the
+        // dependencies will be printed first and perhaps it
+        // will avoid some ordering issues when we are starting
+        // bundles.
+        for (int i = 0; i < deployRecords.length; i++)
+        {
+            LocalState.LocalBundleRecord updateRecord =
+                localState.findUpdatableBundle(deployRecords[i]);
+            if (updateRecord != null)
+            {
+// TODO: Should check to make sure that update bundle isn't already the
+// correct version.
+                // Modify our copy of the local state to reflect
+                // that the bundle is now updated.
+                localState.update(updateRecord, deployRecords[i]);
+
+                // Print out an "updating" message.
+                if (deployRecords[i] != targetRecord)
+                {
+                    out.print("Updating dependency: ");
+                }
+                else
+                {
+                    out.print("Updating: ");
+                }
+                out.println(Util.getBundleName(updateRecord.getBundle()));
+
+                // Actually perform the update.
+                try
+                {
+                    URL url = new URL(
+                        (String) deployRecords[i].getAttribute(BundleRecord.BUNDLE_URL));
+                    updateRecord.getBundle().update(url.openStream());
+
+                    // If necessary, save the updated bundle to be
+                    // started later.
+                    if (isStart)
+                    {
+                        if (startList == null)
+                        {
+                            startList = new ArrayList();
+                        }
+                        startList.add(updateRecord.getBundle());
+                    }
+                }
+                catch (Exception ex)
+                {
+                    err.println("Update error: " + Util.getBundleName(updateRecord.getBundle()));
+                    ex.printStackTrace(err);
+                    return false;
+                }
+            }
+            else
+            {
+                // Print out an "installing" message.
+                if (deployRecords[i] != targetRecord)
+                {
+                    out.print("Installing dependency: ");
+                }
+                else
+                {
+                    out.print("Installing: ");
+                }
+                out.println(deployRecords[i].getAttribute(BundleRecord.BUNDLE_NAME));
+
+                try
+                {
+                    // Actually perform the install, but do not use the actual
+                    // bundle JAR URL for the bundle location, since this will
+                    // limit OBR's ability to manipulate bundle versions. Instead,
+                    // use a unique timestamp as the bundle location.
+                    URL url = new URL(
+                        (String) deployRecords[i].getAttribute(BundleRecord.BUNDLE_URL));
+                    Bundle bundle = m_context.installBundle(
+                        "obr://"
+                        + deployRecords[i].getAttribute(BundleRecord.BUNDLE_NAME)
+                        + "/" + System.currentTimeMillis(),
+                        url.openStream());
+
+                    // If necessary, save the installed bundle to be
+                    // started later.
+                    if (isStart)
+                    {
+                        if (startList == null)
+                        {
+                            startList = new ArrayList();
+                        }
+                        startList.add(bundle);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    err.println("Install error: "
+                        + deployRecords[i].getAttribute(BundleRecord.BUNDLE_NAME));
+                    ex.printStackTrace(err);
+                    return false;
+                }
+            }
+        }
+
+        // If necessary, start bundles after installing them all.
+        if (isStart)
+        {
+            for (int i = 0; (startList != null) && (i < startList.size()); i++)
+            {
+                Bundle bundle = (Bundle) startList.get(i);
+                try
+                {
+                    bundle.start();
+                }
+                catch (BundleException ex)
+                {
+                    err.println("Update error: " + Util.getBundleName(bundle));
+                    ex.printStackTrace();
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public BundleRecord[] resolvePackages(IPackage[] pkgs)
+        throws ResolveException
+    {
+// TODO: FIX
+//        return m_repo.resolvePackages(new LocalState(m_context), pkgs);
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/FileUtil.java b/src/org/apache/osgi/bundle/bundlerepository/FileUtil.java
new file mode 100644
index 0000000..9f27a0d
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/FileUtil.java
@@ -0,0 +1,168 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.io.*;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+
+public class FileUtil
+{
+    public static void downloadSource(
+        PrintStream out, PrintStream err,
+        String srcURL, String dirStr, boolean extract)
+    {
+        // Get the file name from the URL.
+        String fileName = (srcURL.lastIndexOf('/') > 0)
+            ? srcURL.substring(srcURL.lastIndexOf('/') + 1)
+            : srcURL;
+
+        try
+        {
+            out.println("Connecting...");
+
+            File dir = new File(dirStr);
+            if (!dir.exists())
+            {
+                err.println("Destination directory does not exist.");
+            }
+            File file = new File(dir, fileName);
+
+            OutputStream os = new FileOutputStream(file);
+            URLConnection conn = new URL(srcURL).openConnection();
+            int total = conn.getContentLength();
+            InputStream is = conn.getInputStream();
+
+            if (total > 0)
+            {
+                out.println("Downloading " + fileName
+                    + " ( " + total + " bytes ).");
+            }
+            else
+            {
+                out.println("Downloading " + fileName + ".");
+            }
+            byte[] buffer = new byte[4096];
+            int count = 0;
+            for (int len = is.read(buffer); len > 0; len = is.read(buffer))
+            {
+                count += len;
+                os.write(buffer, 0, len);
+            }
+
+            os.close();
+            is.close();
+
+            if (extract)
+            {
+                is = new FileInputStream(file);
+                JarInputStream jis = new JarInputStream(is);
+                out.println("Extracting...");
+                unjar(jis, dir);
+                jis.close();
+                file.delete();
+            }
+        }
+        catch (Exception ex)
+        {
+            err.println(ex);
+        }
+    }
+
+    public static void unjar(JarInputStream jis, File dir)
+        throws IOException
+    {
+        // Reusable buffer.
+        byte[] buffer = new byte[4096];
+
+        // Loop through JAR entries.
+        for (JarEntry je = jis.getNextJarEntry();
+             je != null;
+             je = jis.getNextJarEntry())
+        {
+            if (je.getName().startsWith("/"))
+            {
+                throw new IOException("JAR resource cannot contain absolute paths.");
+            }
+
+            File target = new File(dir, je.getName());
+
+            // Check to see if the JAR entry is a directory.
+            if (je.isDirectory())
+            {
+                if (!target.exists())
+                {
+                    if (!target.mkdirs())
+                    {
+                        throw new IOException("Unable to create target directory: "
+                            + target);
+                    }
+                }
+                // Just continue since directories do not have content to copy.
+                continue;
+            }
+
+            int lastIndex = je.getName().lastIndexOf('/');
+            String name = (lastIndex >= 0) ?
+                je.getName().substring(lastIndex + 1) : je.getName();
+            String destination = (lastIndex >= 0) ?
+                je.getName().substring(0, lastIndex) : "";
+
+            // JAR files use '/', so convert it to platform separator.
+            destination = destination.replace('/', File.separatorChar);
+            copy(jis, dir, name, destination, buffer);
+        }
+    }
+
+    public static void copy(
+        InputStream is, File dir, String destName, String destDir, byte[] buffer)
+        throws IOException
+    {
+        if (destDir == null)
+        {
+            destDir = "";
+        }
+
+        // Make sure the target directory exists and
+        // that is actually a directory.
+        File targetDir = new File(dir, destDir);
+        if (!targetDir.exists())
+        {
+            if (!targetDir.mkdirs())
+            {
+                throw new IOException("Unable to create target directory: "
+                    + targetDir);
+            }
+        }
+        else if (!targetDir.isDirectory())
+        {
+            throw new IOException("Target is not a directory: "
+                + targetDir);
+        }
+
+        BufferedOutputStream bos = new BufferedOutputStream(
+            new FileOutputStream(new File(targetDir, destName)));
+        int count = 0;
+        while ((count = is.read(buffer)) > 0)
+        {
+            bos.write(buffer, 0, count);
+        }
+        bos.close();
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/IteratorToEnumeration.java b/src/org/apache/osgi/bundle/bundlerepository/IteratorToEnumeration.java
new file mode 100644
index 0000000..edd749f
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/IteratorToEnumeration.java
@@ -0,0 +1,44 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.util.Enumeration;
+import java.util.Iterator;
+
+public class IteratorToEnumeration implements Enumeration
+{
+    private Iterator m_iter = null;
+
+    public IteratorToEnumeration(Iterator iter)
+    {
+        m_iter = iter;
+    }
+
+    public boolean hasMoreElements()
+    {
+        if (m_iter == null)
+            return false;
+        return m_iter.hasNext();
+    }
+
+    public Object nextElement()
+    {
+        if (m_iter == null)
+            return null;
+        return m_iter.next();
+    }
+}
diff --git a/src/org/apache/osgi/bundle/bundlerepository/LocalState.java b/src/org/apache/osgi/bundle/bundlerepository/LocalState.java
new file mode 100644
index 0000000..6c91952
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/LocalState.java
@@ -0,0 +1,378 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.util.*;
+
+import org.apache.osgi.service.bundlerepository.BundleRecord;
+import org.apache.osgi.service.bundlerepository.IPackage;
+import org.osgi.framework.*;
+
+public class LocalState
+{
+    private BundleContext m_context = null;
+    private List m_localRecordList = new ArrayList();
+
+    public LocalState(BundleContext context)
+    {
+        m_context = context;
+        initialize();
+    }
+
+    public BundleRecord findBundle(String symName, int[] version)
+    {
+        for (int i = 0; i < m_localRecordList.size(); i++)
+        {
+            BundleRecord brLocal = (BundleRecord) m_localRecordList.get(i);
+            String localSymName = (String)
+                brLocal.getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME);
+            int[] localVersion = Util.parseVersionString((String)
+                brLocal.getAttribute(BundleRecord.BUNDLE_VERSION));
+            if ((localSymName != null) &&
+                localSymName.equals(symName) &&
+                (Util.compareVersion(localVersion, version) == 0))
+            {
+                return brLocal;
+            }
+        }
+        return null;
+    }
+
+    public BundleRecord[] findBundles(String symName)
+    {
+        List matchList = new ArrayList();
+        for (int i = 0; i < m_localRecordList.size(); i++)
+        {
+            BundleRecord brLocal = (BundleRecord) m_localRecordList.get(i);
+            String localSymName = (String)
+                brLocal.getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME);
+            if ((localSymName != null) && localSymName.equals(symName))
+            {
+                matchList.add(brLocal);
+            }
+        }
+        return (BundleRecord[]) matchList.toArray(new BundleRecord[matchList.size()]);
+    }
+
+    public void update(BundleRecord oldRecord, BundleRecord newRecord)
+    {
+        // To update the old record we need to replace it with
+        // a new one, since BundleRecords are immutable. Make
+        // a new record that contains the attributes of the new
+        // record, but is associated with the local bundle of
+        // the old record.
+        if (oldRecord instanceof LocalBundleRecord)
+        {
+            String[] keys = newRecord.getAttributes();
+            Map map = new HashMap();
+            for (int i = 0; i < keys.length; i++)
+            {
+                map.put(keys, newRecord.getAttribute(keys[i]));
+            }
+            BundleRecord updatedRecord =
+                new LocalBundleRecord(
+                    map, ((LocalBundleRecord) oldRecord).getBundle());
+            int idx = m_localRecordList.indexOf(oldRecord);
+            if (idx >= 0)
+            {
+                m_localRecordList.set(idx, updatedRecord);
+            }
+        }
+    }
+
+    public LocalBundleRecord findUpdatableBundle(BundleRecord record)
+    {
+        // Determine if any bundles with the specified symbolic
+        // name are already installed locally.
+        BundleRecord[] localRecords = findBundles(
+            (String)record.getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME));
+        if (localRecords != null)
+        {
+            // Since there are local bundles with the same symbolic
+            // name installed, then we must determine if we can
+            // update an existing bundle or if we must install
+            // another one. Loop through all local bundles with same
+            // symbolic name and find the first one that can be updated
+            // without breaking constraints of existing bundles.
+            for (int i = 0; i < localRecords.length; i++)
+            {
+                // Check to make sure that the version of the target
+                // record is greater than the local bundle version,
+                // since we do not want to perform a downgrade.
+//                int[] vLocal = Util.parseVersionString((String)
+//                    localRecords[i].getAttribute(BundleRecord.BUNDLE_VERSION));
+//                int[] vTarget = Util.parseVersionString((String)
+//                    record.getAttribute(BundleRecord.BUNDLE_VERSION));
+// TODO: VERIFY WHAT IS GOING ON HERE.
+                // If the target bundle is a newer version and it is
+                // export compatible with the local bundle, then return it.
+                if (isUpdatable(localRecords[i], record))
+                {
+                    return (LocalBundleRecord) localRecords[i];
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean isUpdatable(BundleRecord oldVersion, BundleRecord newVersion)
+    {
+        // First get all of the potentially resolvable package declarations
+        // from the local bundles for the old version of the bundle.
+        Filter[] reqFilters = getResolvableImportDeclarations(oldVersion);
+        if (reqFilters == null)
+        {
+            return true;
+        }
+        // Now make sure that all of the resolvable import declarations
+        // for the old version of the bundle  can also be satisfied by
+        // the new version of the bundle.
+        Map[] capMaps = (Map[])
+            newVersion.getAttribute("capability");
+        if (capMaps == null)
+        {
+            return false;
+        }
+        MapToDictionary mapDict = new MapToDictionary(null);
+        for (int reqIdx = 0; reqIdx < reqFilters.length; reqIdx++)
+        {
+            boolean satisfied = false;
+            for (int capIdx = 0; !satisfied && (capIdx < capMaps.length); capIdx++)
+            {
+                mapDict.setSourceMap(capMaps[capIdx]);
+                if (reqFilters[reqIdx].match(mapDict))
+                {
+                    satisfied = true;
+                }
+            }
+
+            // If any of the previously resolvable package declarations
+            // cannot be resolved, then the bundle is not updatable.
+            if (!satisfied)
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    public Filter[] getResolvableImportDeclarations(BundleRecord record)
+    {
+        Map[] capMaps = (Map[])
+            record.getAttribute("capability");
+        if ((capMaps != null) && (capMaps.length > 0))
+        {
+            List filterList = new ArrayList();
+            // Use brute force to determine if any of the exports
+            // could possibly resolve any of the imports.
+            MapToDictionary mapDict = new MapToDictionary(null);
+            for (int capIdx = 0; capIdx < capMaps.length; capIdx++)
+            {
+                boolean added = false;
+                for (int recIdx = 0; !added && (recIdx < m_localRecordList.size()); recIdx++)
+                {
+                    BundleRecord brLocal = (BundleRecord) m_localRecordList.get(recIdx);
+                    Filter[] reqFilters = (Filter[])
+                        brLocal.getAttribute("requirement");
+                    for (int reqIdx = 0;
+                        (reqFilters != null) && (reqIdx < reqFilters.length);
+                        reqIdx++)
+                    {
+                        mapDict.setSourceMap(capMaps[capIdx]);
+                        if (reqFilters[reqIdx].match(mapDict))
+                        {
+                            added = true;
+                            filterList.add(reqFilters[reqIdx]);
+                        }
+                    }
+                }
+            }
+            return (Filter[])
+                filterList.toArray(new Filter[filterList.size()]);
+        }
+        return null;
+    }
+
+    public boolean isResolvable(Filter reqFilter)
+    {
+        MapToDictionary mapDict = new MapToDictionary(null);
+        for (int brIdx = 0; brIdx < m_localRecordList.size(); brIdx++)
+        {
+            BundleRecord brLocal = (BundleRecord) m_localRecordList.get(brIdx);
+            Map[] capMaps = (Map[]) brLocal.getAttribute("capability");
+            for (int capIdx = 0; (capMaps != null) && (capIdx < capMaps.length); capIdx++)
+            {
+                mapDict.setSourceMap(capMaps[capIdx]);
+                if (reqFilter.match(mapDict))
+                {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private void initialize()
+    {
+        Bundle[] bundles = m_context.getBundles();
+        for (int i = 0; (bundles != null) && (i < bundles.length); i++)
+        {
+            Dictionary dict = bundles[i].getHeaders();
+            // Create a case-insensitive map.
+            Map bundleMap = new TreeMap(new Comparator() {
+                public int compare(Object o1, Object o2)
+                {
+                    return o1.toString().compareToIgnoreCase(o2.toString());
+                }
+            });
+
+            for (Enumeration keys = dict.keys(); keys.hasMoreElements(); )
+            {
+                Object key = keys.nextElement();
+                bundleMap.put(key, dict.get(key));
+            }
+            
+            // Remove and convert any import package declarations
+            // into requirement filters.
+            String target = (String) bundleMap.remove(BundleRecord.IMPORT_PACKAGE);
+            if (target != null)
+            {
+                IPackage[] pkgs = R4Package.parseImportOrExportHeader(target);
+                Filter[] filters = new Filter[(pkgs == null) ? 0 : pkgs.length];
+                for (int pkgIdx = 0; (pkgs != null) && (pkgIdx < pkgs.length); pkgIdx++)
+                {
+                    try
+                    {
+                        String low = pkgs[pkgIdx].getVersionLow().isInclusive()
+                            ? "(version>=" + pkgs[pkgIdx].getVersionLow() + ")"
+                            : "(!(version<=" + pkgs[pkgIdx].getVersionLow() + ")";
+
+                        if (pkgs[pkgIdx].getVersionHigh() != null)
+                        {
+                            String high = pkgs[pkgIdx].getVersionHigh().isInclusive()
+                                ? "(version<=" + pkgs[pkgIdx].getVersionHigh() + ")"
+                                : "(!(version>=" + pkgs[pkgIdx].getVersionHigh() + ")";
+                            filters[pkgIdx] = m_context.createFilter(
+                                "(&(type=Export-Package)(name="
+                                + pkgs[pkgIdx].getId() + ")"
+                                + low + high + ")");
+                        }
+                        else
+                        {
+                            filters[pkgIdx] = m_context.createFilter(
+                                "(&(type=Export-Package)(name="
+                                + pkgs[pkgIdx].getId() + ")"
+                                + low + ")");
+                        }
+                    }
+                    catch (InvalidSyntaxException ex)
+                    {
+                        // Ignore, since it should not happen.
+                    }
+                }
+                bundleMap.put("requirement", filters);
+            }
+
+            // Remove and convert any export package declarations
+            // into capability maps.
+            target = (String) bundleMap.remove(BundleRecord.EXPORT_PACKAGE);
+            if (target != null)
+            {
+                IPackage[] pkgs = R4Package.parseImportOrExportHeader(target);
+                Map[] capMaps = new Map[(pkgs == null) ? 0 : pkgs.length];
+                for (int pkgIdx = 0; (pkgs != null) && (pkgIdx < pkgs.length); pkgIdx++)
+                {
+                    // Create a case-insensitive map.
+                    capMaps[pkgIdx] = new TreeMap(new Comparator() {
+                        public int compare(Object o1, Object o2)
+                        {
+                            return o1.toString().compareToIgnoreCase(o2.toString());
+                        }
+                    });
+                    capMaps[pkgIdx].put("type", "Export-Package");
+                    capMaps[pkgIdx].put("name", pkgs[pkgIdx].getId());
+                    capMaps[pkgIdx].put("version", pkgs[pkgIdx].getVersionLow());
+                }
+                bundleMap.put("capability", capMaps);
+            }
+
+            // For the system bundle, add a special platform capability.
+            if (bundles[i].getBundleId() == 0)
+            {
+                // Create a case-insensitive map.
+                Map map = new TreeMap(new Comparator() {
+                    public int compare(Object o1, Object o2)
+                    {
+                        return o1.toString().compareToIgnoreCase(o2.toString());
+                    }
+                });
+                map.put(
+                    Constants.FRAMEWORK_VERSION,
+                    m_context.getProperty(Constants.FRAMEWORK_VERSION));
+                map.put(
+                    Constants.FRAMEWORK_VENDOR,
+                    m_context.getProperty(Constants.FRAMEWORK_VENDOR));
+                map.put(
+                    Constants.FRAMEWORK_LANGUAGE,
+                    m_context.getProperty(Constants.FRAMEWORK_LANGUAGE));
+                map.put(
+                    Constants.FRAMEWORK_OS_NAME,
+                    m_context.getProperty(Constants.FRAMEWORK_OS_NAME));
+                map.put(
+                    Constants.FRAMEWORK_OS_VERSION,
+                    m_context.getProperty(Constants.FRAMEWORK_OS_VERSION));
+                map.put(
+                    Constants.FRAMEWORK_PROCESSOR,
+                    m_context.getProperty(Constants.FRAMEWORK_PROCESSOR));
+//                map.put(
+//                    FelixConstants.FELIX_VERSION_PROPERTY,
+//                    m_context.getProperty(FelixConstants.FELIX_VERSION_PROPERTY));
+                Map[] capMaps = (Map[]) bundleMap.get("capability");
+                if (capMaps == null)
+                {
+                    capMaps = new Map[] { map };
+                }
+                else
+                {
+                    Map[] newCaps = new Map[capMaps.length + 1];
+                    newCaps[0] = map;
+                    System.arraycopy(capMaps, 0, newCaps, 1, capMaps.length);
+                    capMaps = newCaps;
+                }
+                bundleMap.put("capability", capMaps);
+            }
+            m_localRecordList.add(new LocalBundleRecord(bundleMap, bundles[i]));
+        }
+    }
+
+    public static class LocalBundleRecord extends BundleRecord
+    {
+        private Bundle m_bundle = null;
+
+        LocalBundleRecord(Map attrMap, Bundle bundle)
+        {
+            super(attrMap);
+            m_bundle = bundle;
+        }
+
+        public Bundle getBundle()
+        {
+            return m_bundle;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/MapToDictionary.java b/src/org/apache/osgi/bundle/bundlerepository/MapToDictionary.java
new file mode 100644
index 0000000..d7806c6
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/MapToDictionary.java
@@ -0,0 +1,97 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.util.*;
+
+
+/**
+ * This is a simple class that implements a <tt>Dictionary</tt>
+ * from a <tt>Map</tt>. The resulting dictionary is immutatable.
+**/
+public class MapToDictionary extends Dictionary
+{
+    /**
+     * Map source.
+    **/
+    private Map m_map = null;
+
+    public MapToDictionary(Map map)
+    {
+        m_map = map;
+    }
+
+    public void setSourceMap(Map map)
+    {
+        m_map = map;
+    }
+
+    public Enumeration elements()
+    {
+        if (m_map == null)
+        {
+            return null;
+        }
+        return new IteratorToEnumeration(m_map.values().iterator());
+    }
+
+    public Object get(Object key)
+    {
+        if (m_map == null)
+        {
+            return null;
+        }
+        return m_map.get(key);
+    }
+
+    public boolean isEmpty()
+    {
+        if (m_map == null)
+        {
+            return true;
+        }
+        return m_map.isEmpty();
+    }
+
+    public Enumeration keys()
+    {
+        if (m_map == null)
+        {
+            return null;
+        }
+        return new IteratorToEnumeration(m_map.keySet().iterator());
+    }
+
+    public Object put(Object key, Object value)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public Object remove(Object key)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public int size()
+    {
+        if (m_map == null)
+        {
+            return 0;
+        }
+        return m_map.size();
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/ObrCommandImpl.java b/src/org/apache/osgi/bundle/bundlerepository/ObrCommandImpl.java
new file mode 100644
index 0000000..10465a0
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/ObrCommandImpl.java
@@ -0,0 +1,1374 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.osgi.service.bundlerepository.BundleRecord;
+import org.apache.osgi.service.bundlerepository.BundleRepository;
+import org.apache.osgi.service.shell.Command;
+import org.osgi.framework.*;
+
+public class ObrCommandImpl implements Command
+{
+    private static final String HELP_CMD = "help";
+    private static final String URLS_CMD = "urls";
+    private static final String LIST_CMD = "list";
+    private static final String INFO_CMD = "info";
+    private static final String DEPLOY_CMD = "deploy";
+//    private static final String INSTALL_CMD = "install";
+    private static final String START_CMD = "start";
+//    private static final String UPDATE_CMD = "update";
+    private static final String SOURCE_CMD = "source";
+
+    private static final String NODEPS_SWITCH = "-nodeps";
+    private static final String CHECK_SWITCH = "-check";
+    private static final String EXTRACT_SWITCH = "-x";
+
+    private BundleContext m_context = null;
+    private BundleRepository m_repo = null;
+
+    public ObrCommandImpl(BundleContext context, BundleRepository repo)
+    {
+        m_context = context;
+        m_repo = repo;
+    }
+
+    public String getName()
+    {
+        return "obr";
+    }
+
+    public String getUsage()
+    {
+        return "obr help";
+    }
+
+    public String getShortDescription()
+    {
+        return "OSGi bundle repository.";
+    }
+
+    public synchronized void execute(String commandLine, PrintStream out, PrintStream err)
+    {
+        try
+        {
+            // Parse the commandLine to get the OBR command.
+            StringTokenizer st = new StringTokenizer(commandLine);
+            // Ignore the invoking command.
+            st.nextToken();
+            // Try to get the OBR command, default is HELP command.
+            String command = HELP_CMD;
+            try
+            {
+                command = st.nextToken();
+            }
+            catch (Exception ex)
+            {
+                // Ignore.
+            }
+            
+            // Perform the specified command.
+            if ((command == null) || (command.equals(HELP_CMD)))
+            {
+                help(out, st);
+            }
+            else
+            {
+                if (command.equals(URLS_CMD))
+                {
+                    urls(commandLine, command, out, err);
+                }
+                else if (command.equals(LIST_CMD))
+                {
+                    list(commandLine, command, out, err);
+                }
+                else if (command.equals(INFO_CMD))
+                {
+                    info(commandLine, command, out, err);
+                }
+                else if (command.equals(DEPLOY_CMD) || command.equals(START_CMD))
+                {
+                    deploy(commandLine, command, out, err);
+                }
+/*
+                else if (command.equals(INSTALL_CMD) || command.equals(START_CMD))
+                {
+                    install(commandLine, command, out, err);
+                }
+                else if (command.equals(UPDATE_CMD))
+                {
+                    update(commandLine, command, out, err);
+                }
+*/
+                else if (command.equals(SOURCE_CMD))
+                {
+                    source(commandLine, command, out, err);
+                }
+                else
+                {
+                    err.println("Unknown command: " + command);
+                }
+            }
+        }
+        catch (InvalidSyntaxException ex)
+        {
+            err.println("Syntax error: " + ex.getMessage());
+        }
+        catch (IOException ex)
+        {
+            err.println("Error: " + ex);
+        }
+    }
+
+    private void urls(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException
+    {
+        // Parse the commandLine.
+        StringTokenizer st = new StringTokenizer(commandLine);
+        // Ignore the "obr" command.
+        st.nextToken();
+        // Ignore the "urls" command.
+        st.nextToken();
+
+        int count = st.countTokens();
+        String[] urls = new String[count];
+        for (int i = 0; i < count; i++)
+        {
+            urls[i] = st.nextToken();
+        }
+    
+        if (count > 0)
+        {
+            m_repo.setRepositoryURLs(urls);
+        }
+        else
+        {
+            urls = m_repo.getRepositoryURLs();
+            if (urls != null)
+            {
+                for (int i = 0; i < urls.length; i++)
+                {
+                    out.println(urls[i]);
+                }
+            }
+            else
+            {
+                out.println("No repository URLs are set.");
+            }
+        }
+    }
+
+    private void list(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException
+    {
+        // Create a stream tokenizer for the command line string,
+        // since the syntax for install/start is more sophisticated.
+        StringReader sr = new StringReader(commandLine);
+        StreamTokenizer tokenizer = new StreamTokenizer(sr);
+        tokenizer.resetSyntax();
+        tokenizer.quoteChar('\'');
+        tokenizer.quoteChar('\"');
+        tokenizer.whitespaceChars('\u0000', '\u0020');
+        tokenizer.wordChars('A', 'Z');
+        tokenizer.wordChars('a', 'z');
+        tokenizer.wordChars('0', '9');
+        tokenizer.wordChars('\u00A0', '\u00FF');
+        tokenizer.wordChars('.', '.');
+        tokenizer.wordChars('-', '-');
+        tokenizer.wordChars('_', '_');
+
+        // Ignore the invoking command name and the OBR command.
+        int type = tokenizer.nextToken();
+        type = tokenizer.nextToken();
+
+        String substr = null;
+    
+        for (type = tokenizer.nextToken();
+            type != StreamTokenizer.TT_EOF;
+            type = tokenizer.nextToken())
+        {
+            // Add a space in between tokens.
+            if (substr == null)
+            {
+                substr = "";
+            }
+            else
+            {
+                substr += " ";
+            }
+                        
+            if ((type == StreamTokenizer.TT_WORD) ||
+                (type == '\'') || (type == '"'))
+            {
+                substr += tokenizer.sval.toLowerCase();
+            }
+        }
+
+        boolean found = false;
+        BundleRecord[] records = m_repo.getBundleRecords();
+        for (int recIdx = 0; recIdx < records.length; recIdx++)
+        {
+            String name = (String)
+                records[recIdx].getAttribute(BundleRecord.BUNDLE_NAME);
+            String symName = (String)
+                records[recIdx].getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME);
+            if ((substr == null) ||
+                ((name != null) && (name.toLowerCase().indexOf(substr) >= 0)) ||
+                ((symName != null) && (symName.toLowerCase().indexOf(substr) >= 0)))
+            {
+                found = true;
+                String version =
+                    (String) records[recIdx].getAttribute(BundleRecord.BUNDLE_VERSION);
+                if (version != null)
+                {
+                    out.println(name + " (" + version + ")");
+                }
+                else
+                {
+                    out.println(name);
+                }
+            }
+        }
+    
+        if (!found)
+        {
+            out.println("No matching bundles.");
+        }
+    }
+
+    private void info(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException, InvalidSyntaxException
+    {
+        ParsedCommand pc = parseInfo(commandLine);
+        for (int i = 0; (pc != null) && (i < pc.getTargetCount()); i++)                
+        {
+            BundleRecord[] records = searchRepository(
+                pc.getTargetId(i), pc.getTargetVersion(i));
+            if (records == null)
+            {
+                err.println("Unknown bundle and/or version: "
+                    + pc.getTargetId(i));
+            }
+            else if (records.length > 1)
+            {
+                err.println("More than one version exists: "
+                    + pc.getTargetId(i));
+            }
+            else
+            {
+                records[0].printAttributes(out);
+            }
+        }
+    }
+
+    private void deploy(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException, InvalidSyntaxException
+    {
+        ParsedCommand pc = parseInstallStart(commandLine);
+        _deploy(pc, command, out, err);
+    }
+
+    private void _deploy(
+        ParsedCommand pc, String command, PrintStream out, PrintStream err)
+        throws IOException, InvalidSyntaxException
+    {
+        for (int i = 0; (pc != null) && (i < pc.getTargetCount()); i++)                
+        {
+            // Find the target's bundle record.
+            BundleRecord record = selectNewestVersion(
+                searchRepository(pc.getTargetId(i), pc.getTargetVersion(i)));
+            if (record != null)
+            {
+                m_repo.deployBundle(
+                    out, // Output stream.
+                    err, // Error stream.
+                    (String) record.getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME),
+                    Util.parseVersionString((String)record.getAttribute(BundleRecord.BUNDLE_VERSION)),
+                    pc.isResolve(), // Resolve dependencies.
+                    command.equals(START_CMD)); // Start.
+            }
+            else
+            {
+                err.println("Unknown bundle or amiguous version: "
+                    + pc.getTargetId(i));
+            }
+        }
+    }
+/*
+    private void install(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException, InvalidSyntaxException
+    {
+        // Parse the command line to get all local targets to install.
+        ParsedCommand pc = parseInstallStart(commandLine);
+        
+        // Loop through each local target and try to find
+        // the corresponding bundle record from the repository.
+        for (int targetIdx = 0;
+            (pc != null) && (targetIdx < pc.getTargetCount());
+            targetIdx++)                
+        {
+            // Get the current target's name and version.
+            String targetName = pc.getTargetId(targetIdx);
+            String targetVersionString = pc.getTargetVersion(targetIdx);
+
+            // Make sure the bundle is not already installed.
+            Bundle bundle = findLocalBundle(targetName, targetVersionString);
+            if (bundle == null)
+            {
+                _deploy(pc, command, out, err);
+            }
+            else
+            {
+                err.println("Already installed: " + targetName);
+            }
+        }
+    }
+
+    private void update(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException, InvalidSyntaxException
+    {
+        // Parse the command line to get all local targets to update.
+        ParsedCommand pc = parseUpdate(commandLine);
+
+        if (pc.isCheck())        
+        {
+            updateCheck(out, err);
+        }
+        else
+        {
+            // Loop through each local target and try to find
+            // the corresponding bundle record from the repository.
+            for (int targetIdx = 0;
+                (pc != null) && (targetIdx < pc.getTargetCount());
+                targetIdx++)                
+            {
+                // Get the current target's name and version.
+                String targetName = pc.getTargetId(targetIdx);
+                String targetVersionString = pc.getTargetVersion(targetIdx);
+
+                // Make sure the bundle is not already installed.
+                Bundle bundle = findLocalBundle(targetName, targetVersionString);
+                if (bundle != null)
+                {
+                    _deploy(pc, command, out, err);
+                }
+                else
+                {
+                    err.println("Not installed: " + targetName);
+                }
+            }
+        }
+    }
+
+    private void updateCheck(PrintStream out, PrintStream err)
+        throws IOException
+    {
+        Bundle[] bundles = m_context.getBundles();
+
+        // Loop through each local target and try to find
+        // the corresponding locally installed bundle.
+        for (int bundleIdx = 0;
+            (bundles != null) && (bundleIdx < bundles.length);
+            bundleIdx++)
+        {
+            // Ignore the system bundle.
+            if (bundles[bundleIdx].getBundleId() == 0)
+            {
+                continue;
+            }
+
+            // Get the local bundle's update location.
+            String localLoc = (String)
+                bundles[bundleIdx].getHeaders().get(Constants.BUNDLE_UPDATELOCATION);
+            if (localLoc == null)
+            {
+                // Without an update location, there is no way to
+                // check for an update, so ignore the bundle.
+                continue;
+            }
+
+            // Get the local bundle's version.
+            String localVersion = (String)
+                bundles[bundleIdx].getHeaders().get(Constants.BUNDLE_VERSION);
+            localVersion = (localVersion == null) ? "0.0.0" : localVersion;
+
+            // Get the matching repository bundle records.
+            BundleRecord[] records = m_repo.getBundleRecords(
+                (String) bundles[bundleIdx].getHeaders().get(Constants.BUNDLE_NAME));
+
+            // Loop through all records to see if there is an update.
+            for (int recordIdx = 0;
+                (records != null) && (recordIdx < records.length);
+                recordIdx++)
+            {
+                String remoteLoc = (String)
+                    records[recordIdx].getAttribute(BundleRecord.BUNDLE_UPDATELOCATION);
+                if (remoteLoc == null)
+                {
+                    continue;
+                }
+
+                // If the update locations are equal, then compare versions.
+                if (remoteLoc.equals(localLoc))
+                {
+                    String remoteVersion = (String)
+                        records[recordIdx].getAttribute(BundleRecord.BUNDLE_VERSION);
+                    if (remoteVersion != null)
+                    {
+                        int result = Util.compareVersion(
+                            Util.parseVersionString(remoteVersion),
+                            Util.parseVersionString(localVersion));
+                        if (result > 0)
+                        {
+                            out.println(
+                                records[recordIdx].getAttribute(BundleRecord.BUNDLE_NAME)
+                                + " update available.");
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+*/
+    private void source(
+        String commandLine, String command, PrintStream out, PrintStream err)
+        throws IOException, InvalidSyntaxException
+    {
+        // Parse the command line to get all local targets to update.
+        ParsedCommand pc = parseSource(commandLine);
+        
+        for (int i = 0; i < pc.getTargetCount(); i++)
+        {
+            BundleRecord[] records =
+                searchRepository(pc.getTargetId(i), pc.getTargetVersion(i));
+            if (records == null)
+            {
+                err.println("Unknown bundle and/or version: "
+                    + pc.getTargetId(i));
+            }
+            else if (records.length > 1)
+            {
+                err.println("More than one version exists: "
+                    + pc.getTargetId(i));
+            }
+            else
+            {
+                String srcURL = (String)
+                records[0].getAttribute(BundleRecord.BUNDLE_SOURCEURL);
+                if (srcURL != null)
+                {
+                    FileUtil.downloadSource(
+                        out, err, srcURL, pc.getDirectory(), pc.isExtract());
+                }
+                else
+                {
+                    err.println("Missing source URL: " + pc.getTargetId(i));
+                }
+            }
+        }
+    }
+
+    private BundleRecord[] searchRepository(String targetId, String targetVersion)
+    {
+        // The targetId may be a bundle name or a bundle symbolic name.
+        // Query for symbolic name first, since it is more specific. If
+        // that can't be found, then compare bundle names.
+        BundleRecord[] records = null;
+        if (targetVersion != null)
+        {
+            BundleRecord record = m_repo.getBundleRecord(
+                targetId, Util.parseVersionString(targetVersion));
+            if (record != null)
+            {
+                records = new BundleRecord[] { record };
+            }
+        }
+        else
+        {
+            records = m_repo.getBundleRecords(targetId);
+        }
+
+        if (records == null)
+        {
+            List recordList = new ArrayList();
+            records = m_repo.getBundleRecords();
+            for (int i = 0; (records != null) && (i < records.length); i++)
+            {
+                if (targetId.compareToIgnoreCase((String)
+                    records[i].getAttribute(BundleRecord.BUNDLE_NAME)) == 0)
+                {
+                    int[] v1 = Util.parseVersionString(targetVersion);
+                    int[] v2 = Util.parseVersionString((String)
+                        records[i].getAttribute(BundleRecord.BUNDLE_VERSION));
+                    if ((targetVersion == null) ||
+                        ((targetVersion != null) && (Util.compareVersion(v1, v2) == 0)))
+                    {
+                        recordList.add(records[i]);
+                    }
+                }
+            }
+            records = (recordList.size() == 0)
+                ? null
+                : (BundleRecord[]) recordList.toArray(new BundleRecord[recordList.size()]);
+        }
+
+        return records;
+    }
+
+    public BundleRecord selectNewestVersion(BundleRecord[] records)
+    {
+        int idx = -1;
+        int[] v = null;
+        for (int i = 0; (records != null) && (i < records.length); i++)
+        {
+            if (i == 0)
+            {
+                idx = 0;
+                v = Util.parseVersionString((String)
+                    records[i].getAttribute(BundleRecord.BUNDLE_VERSION));
+            }
+            else
+            {
+                int[] vtmp = Util.parseVersionString((String)
+                    records[i].getAttribute(BundleRecord.BUNDLE_VERSION));
+                if (Util.compareVersion(vtmp, v) > 0)
+                {
+                    idx = i;
+                    v = vtmp;
+                }
+            }
+        }
+
+        return (idx < 0) ? null : records[idx];
+    }
+
+    private Bundle findLocalBundle(String name, String versionString)
+    {
+        Bundle bundle = null;
+
+        // Get the name only if there is no version, but error
+        // if there are multiple matches for the same name.
+        if (versionString == null)
+        {
+            // Perhaps the target name is a bundle ID and
+            // not a name, so try to interpret as a long.
+            try
+            {
+                bundle = m_context.getBundle(Long.parseLong(name));
+            }
+            catch (NumberFormatException ex)
+            {
+                // The bundle is not a number, so look for a local
+                // bundle with the same name.
+                Bundle[] matchingBundles = findLocalBundlesBySymbolicName(name);
+
+                // If only one matches, then select is.
+                if (matchingBundles.length == 1)
+                {
+                    bundle = matchingBundles[0];
+                }
+            }
+        }
+        else
+        {
+            // Find the local bundle by name and version.
+            bundle = findLocalBundleByVersion(
+                name, Util.parseVersionString(versionString));
+        }
+
+        return bundle;
+    }
+
+    private Bundle findLocalBundleByVersion(String symName, int[] version)
+    {
+        // Get bundles with matching name.
+        Bundle[] targets = findLocalBundlesBySymbolicName(symName);
+
+        // Find bundle with matching version.
+        if (targets.length > 0)
+        {
+            for (int i = 0; i < targets.length; i++)
+            {
+                String targetName = (String)
+                    targets[i].getHeaders().get(BundleRecord.BUNDLE_SYMBOLICNAME);
+                int[] targetVersion = Util.parseVersionString((String)
+                    targets[i].getHeaders().get(BundleRecord.BUNDLE_VERSION));
+            
+                if ((targetName != null) &&
+                    targetName.equalsIgnoreCase(symName) &&
+                    (Util.compareVersion(targetVersion, version) == 0))
+                {
+                    return targets[i];
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private Bundle[] findLocalBundlesBySymbolicName(String symName)
+    {
+        // Get local bundles.
+        Bundle[] bundles = m_context.getBundles();
+
+        // Find bundles with matching name.
+        Bundle[] targets = new Bundle[0];
+        for (int i = 0; i < bundles.length; i++)
+        {
+            String targetName = (String)
+                bundles[i].getHeaders().get(BundleRecord.BUNDLE_SYMBOLICNAME);
+            if (targetName == null)
+            {
+                targetName = bundles[i].getLocation();
+            }
+            if ((targetName != null) && targetName.equalsIgnoreCase(symName))
+            {
+                Bundle[] newTargets = new Bundle[targets.length + 1];
+                System.arraycopy(targets, 0, newTargets, 0, targets.length);
+                newTargets[targets.length] = bundles[i];
+                targets = newTargets;
+            }
+        }
+
+        return targets;
+    }
+
+    private ParsedCommand parseInfo(String commandLine)
+        throws IOException, InvalidSyntaxException
+    {
+        // Create a stream tokenizer for the command line string,
+        // since the syntax for install/start is more sophisticated.
+        StringReader sr = new StringReader(commandLine);
+        StreamTokenizer tokenizer = new StreamTokenizer(sr);
+        tokenizer.resetSyntax();
+        tokenizer.quoteChar('\'');
+        tokenizer.quoteChar('\"');
+        tokenizer.whitespaceChars('\u0000', '\u0020');
+        tokenizer.wordChars('A', 'Z');
+        tokenizer.wordChars('a', 'z');
+        tokenizer.wordChars('0', '9');
+        tokenizer.wordChars('\u00A0', '\u00FF');
+        tokenizer.wordChars('.', '.');
+        tokenizer.wordChars('-', '-');
+        tokenizer.wordChars('_', '_');
+    
+        // Ignore the invoking command name and the OBR command.
+        int type = tokenizer.nextToken();
+        type = tokenizer.nextToken();
+    
+        int EOF = 1;
+        int SWITCH = 2;
+        int TARGET = 4;
+        int VERSION = 8;
+        int VERSION_VALUE = 16;
+
+        // Construct an install record.
+        ParsedCommand pc = new ParsedCommand();
+        String currentTargetName = null;
+
+        // The state machine starts by expecting either a
+        // SWITCH or a TARGET.
+        int expecting = (TARGET);
+        while (true)
+        {
+            // Get the next token type.
+            type = tokenizer.nextToken();
+            switch (type)
+            {
+                // EOF received.
+                case StreamTokenizer.TT_EOF:
+                    // Error if we weren't expecting EOF.
+                    if ((expecting & EOF) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Expecting more arguments.", null);
+                    }
+                    // Add current target if there is one.
+                    if (currentTargetName != null)
+                    {
+                        pc.addTarget(currentTargetName, null);
+                    }
+                    // Return cleanly.
+                    return pc;
+
+                // WORD or quoted WORD received.
+                case StreamTokenizer.TT_WORD:
+                case '\'':
+                case '\"':
+                    // If we are expecting a target, the record it.
+                    if ((expecting & TARGET) > 0)
+                    {
+                        // Add current target if there is one.
+                        if (currentTargetName != null)
+                        {
+                            pc.addTarget(currentTargetName, null);
+                        }
+                        // Set the new target as the current target.
+                        currentTargetName = tokenizer.sval;
+                        expecting = (EOF | TARGET | VERSION);
+                    }
+                    else if ((expecting & VERSION_VALUE) > 0)
+                    {
+                        pc.addTarget(currentTargetName, tokenizer.sval);
+                        currentTargetName = null;
+                        expecting = (EOF | TARGET);
+                    }
+                    else
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting '" + tokenizer.sval + "'.", null);
+                    }
+                    break;
+
+                // Version separator character received.
+                case ';':
+                    // Error if we weren't expecting the version separator.
+                    if ((expecting & VERSION) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting version.", null);
+                    }
+                    // Otherwise, we will only expect a version value next.
+                    expecting = (VERSION_VALUE);
+                    break;
+            }
+        }
+    }
+
+    private ParsedCommand parseInstallStart(String commandLine)
+        throws IOException, InvalidSyntaxException
+    {
+        // Create a stream tokenizer for the command line string,
+        // since the syntax for install/start is more sophisticated.
+        StringReader sr = new StringReader(commandLine);
+        StreamTokenizer tokenizer = new StreamTokenizer(sr);
+        tokenizer.resetSyntax();
+        tokenizer.quoteChar('\'');
+        tokenizer.quoteChar('\"');
+        tokenizer.whitespaceChars('\u0000', '\u0020');
+        tokenizer.wordChars('A', 'Z');
+        tokenizer.wordChars('a', 'z');
+        tokenizer.wordChars('0', '9');
+        tokenizer.wordChars('\u00A0', '\u00FF');
+        tokenizer.wordChars('.', '.');
+        tokenizer.wordChars('-', '-');
+        tokenizer.wordChars('_', '_');
+    
+        // Ignore the invoking command name and the OBR command.
+        int type = tokenizer.nextToken();
+        type = tokenizer.nextToken();
+    
+        int EOF = 1;
+        int SWITCH = 2;
+        int TARGET = 4;
+        int VERSION = 8;
+        int VERSION_VALUE = 16;
+
+        // Construct an install record.
+        ParsedCommand pc = new ParsedCommand();
+        String currentTargetName = null;
+
+        // The state machine starts by expecting either a
+        // SWITCH or a TARGET.
+        int expecting = (SWITCH | TARGET);
+        while (true)
+        {
+            // Get the next token type.
+            type = tokenizer.nextToken();
+            switch (type)
+            {
+                // EOF received.
+                case StreamTokenizer.TT_EOF:
+                    // Error if we weren't expecting EOF.
+                    if ((expecting & EOF) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Expecting more arguments.", null);
+                    }
+                    // Add current target if there is one.
+                    if (currentTargetName != null)
+                    {
+                        pc.addTarget(currentTargetName, null);
+                    }
+                    // Return cleanly.
+                    return pc;
+
+                // WORD or quoted WORD received.
+                case StreamTokenizer.TT_WORD:
+                case '\'':
+                case '\"':
+                    // If we are expecting a command SWITCH and the token
+                    // equals a command SWITCH, then record it.
+                    if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(NODEPS_SWITCH))
+                    {
+                        pc.setResolve(false);
+                        expecting = (EOF | TARGET);
+                    }
+                    // If we are expecting a target, the record it.
+                    else if ((expecting & TARGET) > 0)
+                    {
+                        // Add current target if there is one.
+                        if (currentTargetName != null)
+                        {
+                            pc.addTarget(currentTargetName, null);
+                        }
+                        // Set the new target as the current target.
+                        currentTargetName = tokenizer.sval;
+                        expecting = (EOF | TARGET | VERSION);
+                    }
+                    else if ((expecting & VERSION_VALUE) > 0)
+                    {
+                        pc.addTarget(currentTargetName, tokenizer.sval);
+                        currentTargetName = null;
+                        expecting = (EOF | TARGET);
+                    }
+                    else
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting '" + tokenizer.sval + "'.", null);
+                    }
+                    break;
+
+                // Version separator character received.
+                case ';':
+                    // Error if we weren't expecting the version separator.
+                    if ((expecting & VERSION) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting version.", null);
+                    }
+                    // Otherwise, we will only expect a version value next.
+                    expecting = (VERSION_VALUE);
+                    break;
+            }
+        }
+    }
+
+    private ParsedCommand parseUpdate(String commandLine)
+        throws IOException, InvalidSyntaxException
+    {
+        // Create a stream tokenizer for the command line string,
+        // since the syntax for install/start is more sophisticated.
+        StringReader sr = new StringReader(commandLine);
+        StreamTokenizer tokenizer = new StreamTokenizer(sr);
+        tokenizer.resetSyntax();
+        tokenizer.quoteChar('\'');
+        tokenizer.quoteChar('\"');
+        tokenizer.whitespaceChars('\u0000', '\u0020');
+        tokenizer.wordChars('A', 'Z');
+        tokenizer.wordChars('a', 'z');
+        tokenizer.wordChars('0', '9');
+        tokenizer.wordChars('\u00A0', '\u00FF');
+        tokenizer.wordChars('.', '.');
+        tokenizer.wordChars('-', '-');
+        tokenizer.wordChars('_', '_');
+    
+        // Ignore the invoking command name and the OBR command.
+        int type = tokenizer.nextToken();
+        type = tokenizer.nextToken();
+    
+        int EOF = 1;
+        int SWITCH = 2;
+        int TARGET = 4;
+        int VERSION = 8;
+        int VERSION_VALUE = 16;
+
+        // Construct an install record.
+        ParsedCommand pc = new ParsedCommand();
+        String currentTargetName = null;
+
+        // The state machine starts by expecting either a
+        // SWITCH or a TARGET.
+        int expecting = (SWITCH | TARGET);
+        while (true)
+        {
+            // Get the next token type.
+            type = tokenizer.nextToken();
+            switch (type)
+            {
+                // EOF received.
+                case StreamTokenizer.TT_EOF:
+                    // Error if we weren't expecting EOF.
+                    if ((expecting & EOF) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Expecting more arguments.", null);
+                    }
+                    // Add current target if there is one.
+                    if (currentTargetName != null)
+                    {
+                        pc.addTarget(currentTargetName, null);
+                    }
+                    // Return cleanly.
+                    return pc;
+
+                // WORD or quoted WORD received.
+                case StreamTokenizer.TT_WORD:
+                case '\'':
+                case '\"':
+                    // If we are expecting a command SWITCH and the token
+                    // equals a NODEPS switch, then record it.
+                    if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(NODEPS_SWITCH))
+                    {
+                        pc.setResolve(false);
+                        expecting = (EOF | TARGET);
+                    }
+                    // If we are expecting a command SWITCH and the token
+                    // equals a CHECK swithc, then record it.
+                    else if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(CHECK_SWITCH))
+                    {
+                        pc.setCheck(true);
+                        expecting = (EOF);
+                    }
+                    // If we are expecting a target, the record it.
+                    else if ((expecting & TARGET) > 0)
+                    {
+                        // Add current target if there is one.
+                        if (currentTargetName != null)
+                        {
+                            pc.addTarget(currentTargetName, null);
+                        }
+                        // Set the new target as the current target.
+                        currentTargetName = tokenizer.sval;
+                        expecting = (EOF | TARGET | VERSION);
+                    }
+                    else if ((expecting & VERSION_VALUE) > 0)
+                    {
+                        pc.addTarget(currentTargetName, tokenizer.sval);
+                        currentTargetName = null;
+                        expecting = (EOF | TARGET);
+                    }
+                    else
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting '" + tokenizer.sval + "'.", null);
+                    }
+                    break;
+
+                // Version separator character received.
+                case ';':
+                    // Error if we weren't expecting the version separator.
+                    if ((expecting & VERSION) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting version.", null);
+                    }
+                    // Otherwise, we will only expect a version value next.
+                    expecting = (VERSION_VALUE);
+                    break;
+            }
+        }
+    }
+
+    private ParsedCommand parseSource(String commandLine)
+        throws IOException, InvalidSyntaxException
+    {
+        // Create a stream tokenizer for the command line string,
+        // since the syntax for install/start is more sophisticated.
+        StringReader sr = new StringReader(commandLine);
+        StreamTokenizer tokenizer = new StreamTokenizer(sr);
+        tokenizer.resetSyntax();
+        tokenizer.quoteChar('\'');
+        tokenizer.quoteChar('\"');
+        tokenizer.whitespaceChars('\u0000', '\u0020');
+        tokenizer.wordChars('A', 'Z');
+        tokenizer.wordChars('a', 'z');
+        tokenizer.wordChars('0', '9');
+        tokenizer.wordChars('\u00A0', '\u00FF');
+        tokenizer.wordChars('.', '.');
+        tokenizer.wordChars('-', '-');
+        tokenizer.wordChars('_', '_');
+        tokenizer.wordChars('/', '/');
+    
+        // Ignore the invoking command name and the OBR command.
+        int type = tokenizer.nextToken();
+        type = tokenizer.nextToken();
+    
+        int EOF = 1;
+        int SWITCH = 2;
+        int DIRECTORY = 4;
+        int TARGET = 8;
+        int VERSION = 16;
+        int VERSION_VALUE = 32;
+
+        // Construct an install record.
+        ParsedCommand pc = new ParsedCommand();
+        String currentTargetName = null;
+
+        // The state machine starts by expecting either a
+        // SWITCH or a DIRECTORY.
+        int expecting = (SWITCH | DIRECTORY);
+        while (true)
+        {
+            // Get the next token type.
+            type = tokenizer.nextToken();
+            switch (type)
+            {
+                // EOF received.
+                case StreamTokenizer.TT_EOF:
+                    // Error if we weren't expecting EOF.
+                    if ((expecting & EOF) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Expecting more arguments.", null);
+                    }
+                    // Add current target if there is one.
+                    if (currentTargetName != null)
+                    {
+                        pc.addTarget(currentTargetName, null);
+                    }
+                    // Return cleanly.
+                    return pc;
+
+                // WORD or quoted WORD received.
+                case StreamTokenizer.TT_WORD:
+                case '\'':
+                case '\"':
+                    // If we are expecting a command SWITCH and the token
+                    // equals a command SWITCH, then record it.
+                    if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(EXTRACT_SWITCH))
+                    {
+                        pc.setExtract(true);
+                        expecting = (DIRECTORY);
+                    }
+                    // If we are expecting a directory, the record it.
+                    else if ((expecting & DIRECTORY) > 0)
+                    {
+                        // Set the directory for the command.
+                        pc.setDirectory(tokenizer.sval);
+                        expecting = (TARGET);
+                    }
+                    // If we are expecting a target, the record it.
+                    else if ((expecting & TARGET) > 0)
+                    {
+                        // Add current target if there is one.
+                        if (currentTargetName != null)
+                        {
+                            pc.addTarget(currentTargetName, null);
+                        }
+                        // Set the new target as the current target.
+                        currentTargetName = tokenizer.sval;
+                        expecting = (EOF | TARGET | VERSION);
+                    }
+                    else if ((expecting & VERSION_VALUE) > 0)
+                    {
+                        pc.addTarget(currentTargetName, tokenizer.sval);
+                        currentTargetName = null;
+                        expecting = (EOF | TARGET);
+                    }
+                    else
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting '" + tokenizer.sval + "'.", null);
+                    }
+                    break;
+
+                // Version separator character received.
+                case ';':
+                    // Error if we weren't expecting the version separator.
+                    if ((expecting & VERSION) == 0)
+                    {
+                        throw new InvalidSyntaxException(
+                            "Not expecting version.", null);
+                    }
+                    // Otherwise, we will only expect a version value next.
+                    expecting = (VERSION_VALUE);
+                    break;
+            }
+        }
+    }
+
+    private void help(PrintStream out, StringTokenizer st)
+    {
+        String command = HELP_CMD;
+        if (st.hasMoreTokens())
+        {
+            command = st.nextToken();
+        }
+        if (command.equals(URLS_CMD))
+        {
+            out.println("");
+            out.println("obr " + URLS_CMD + " [<repository-file-url> ...]");
+            out.println("");
+            out.println(
+                "This command gets or sets the URLs to the repository files\n" +                "used by OBR. Specify no arguments to get the current repository\n" + 
+                "URLs or specify a space-delimited list of URLs to change the\n" +
+                "URLs. Each URL should point to a file containing meta-data about\n" +                "available bundles in XML format.");
+            out.println("");
+        }
+        else if (command.equals(LIST_CMD))
+        {
+            out.println("");
+            out.println("obr " + LIST_CMD + " [<string> ...]");
+            out.println("");
+            out.println(
+                "This command lists bundles available in the bundle repository.\n" +
+                "If no arguments are specified, then all available bundles are\n" +
+                "listed, otherwise any arguments are concatenated with spaces\n" +
+                "and used as a substring filter on the bundle names.");
+            out.println("");
+        }
+        else if (command.equals(INFO_CMD))
+        {
+            out.println("");
+            out.println("obr " + INFO_CMD
+                + " <bundle-name>[;<version>] ...");
+            out.println("");
+            out.println(
+                "This command displays the meta-data for the specified bundles.\n" +
+                "If a bundle's name contains spaces, then it must be surrounded\n" +
+                "by quotes. It is also possible to specify a precise version\n" +
+                "if more than one version exists, such as:\n" +
+                "\n" +
+                "    obr info \"Bundle Repository\";1.0.0\n" +
+                "\n" +
+                "The above example retrieves the meta-data for version \"1.0.0\"\n" +
+                "of the bundle named \"Bundle Repository\".");
+            out.println("");
+        }
+        else if (command.equals(DEPLOY_CMD))
+        {
+            out.println("");
+            out.println("obr " + DEPLOY_CMD
+                + " [" + NODEPS_SWITCH
+                + "] <bundle-name>[;<version>] ... | <bundle-id> ...");
+            out.println("");
+            out.println(
+                "This command tries to install or update the specified bundles\n" +
+                "and all of their dependencies by default; use the \"" + NODEPS_SWITCH + "\" switch\n" +
+                "to ignore dependencies. You can specify either the bundle name or\n" +
+                "the bundle identifier. If a bundle's name contains spaces, then\n" +
+                "it must be surrounded by quotes. It is also possible to specify a\n" +                "precise version if more than one version exists, such as:\n" +
+                "\n" +
+                "    obr deploy \"Bundle Repository\";1.0.0\n" +
+                "\n" +
+                "For the above example, if version \"1.0.0\" of \"Bundle Repository\" is\n" +
+                "already installed locally, then the command will attempt to update it\n" +
+                "and all of its dependencies; otherwise, the command will install it\n" +
+                "and all of its dependencies.");
+            out.println("");
+        }
+/*
+        else if (command.equals(INSTALL_CMD))
+        {
+            out.println("");
+            out.println("obr " + INSTALL_CMD
+                + " [" + NODEPS_SWITCH
+                + "] <bundle-name>[;<version>] ...");
+            out.println("");
+            out.println(
+                "This command installs the specified bundles and all of their\n" +
+                "dependencies by default; use the \"" + NODEPS_SWITCH + "\" switch to ignore\n" +
+                "dependencies. If a bundle's name contains spaces, then it\n" +
+                "must be surrounded by quotes. If a specified bundle is already\n" +                "installed, then this command has no effect. It is also possible\n" +                "to specify a precise version if more than one version exists,\n" +                "such as:\n" +
+                "\n" +
+                "    obr install \"Bundle Repository\";1.0.0\n" +
+                "\n" +
+                "The above example installs version \"1.0.0\" of the bundle\n" +
+                "named \"Bundle Repository\" and its dependencies. ");
+            out.println("");
+        }
+*/
+        else if (command.equals(START_CMD))
+        {
+            out.println("");
+            out.println("obr " + START_CMD
+                + " [" + NODEPS_SWITCH
+                + "] <bundle-name>[;<version>] ...");
+            out.println("");
+            out.println(
+                "This command installs and starts the specified bundles and all\n" +
+                "of their dependencies by default; use the \"" + NODEPS_SWITCH + "\" switch to\n" +
+                "ignore dependencies. If a bundle's name contains spaces, then\n" +
+                "it must be surrounded by quotes. If a specified bundle is already\n" +                "installed, then this command has no effect. It is also possible\n" +                "to specify a precise version if more than one version exists,\n" +                "such as:\n" +
+                "\n" +
+                "    obr start \"Bundle Repository\";1.0.0\n" +
+                "\n" +
+                "The above example installs and starts version \"1.0.0\" of the\n" +
+                "bundle named \"Bundle Repository\" and its dependencies.");
+            out.println("");
+        }
+/*
+        else if (command.equals(UPDATE_CMD))
+        {
+            out.println("");
+            out.println("obr " + UPDATE_CMD + " " + CHECK_SWITCH);
+            out.println("");
+            out.println("obr " + UPDATE_CMD
+                + " [" + NODEPS_SWITCH
+                + "] <bundle-name>[;<version>] ... | <bundle-id> ...");
+            out.println("");
+            out.println(
+                "The first form of the command above checks for available updates\n" +                "and the second updates the specified locally installed bundles\n" +
+                "and all of their dependencies by default; use the \"" + NODEPS_SWITCH + "\" switch\n" +
+                "to ignore dependencies. You can specify either the bundle name or\n" +
+                "the bundle identifier. If a bundle's name contains spaces, then\n" +
+                "it must be surrounded by quotes. If a specified bundle is not\n" +                "already installed, then this command has no effect. It is also\n" +                "possible to specify a precise version if more than one version\n" +                "exists, such as:\n" +
+                "\n" +
+                "    obr update \"Bundle Repository\";1.0.0\n" +
+                "\n" +
+                "The above example updates version \"1.0.0\" of the bundle named\n" +
+                "\"Bundle Repository\" and its dependencies. The update command may\n" +
+                "install new bundles if the updated bundles have new dependencies.");
+            out.println("");
+        }
+*/
+        else if (command.equals(SOURCE_CMD))
+        {
+            out.println("");
+            out.println("obr " + SOURCE_CMD
+                + " [" + EXTRACT_SWITCH
+                + "] <local-dir> <bundle-name>[;<version>] ...");
+            out.println("");
+            out.println(
+                "This command retrieves the source archives of the specified\n" +
+                "bundles and saves them to the specified local directory; use\n" +
+                "the \"" + EXTRACT_SWITCH + "\" switch to automatically extract the source archives.\n" +
+                "If a bundle name contains spaces, then it must be surrounded\n" +
+                "by quotes. It is also possible to specify a precise version if\n" +                "more than one version exists, such as:\n" +
+                "\n" +
+                "    obr source /home/rickhall/tmp \"Bundle Repository\";1.0.0\n" +
+                "\n" +
+                "The above example retrieves the source archive of version \"1.0.0\"\n" +
+                "of the bundle named \"Bundle Repository\" and saves it to the\n" +
+                "specified local directory.");
+            out.println("");
+        }
+        else
+        {
+            out.println("obr " + HELP_CMD
+                + " [" + URLS_CMD + " | " + LIST_CMD
+//                + " | " + INFO_CMD + " | " + INSTALL_CMD
+                + " | " + INFO_CMD
+                + " | " + DEPLOY_CMD + " | " + START_CMD
+//                + " | " + UPDATE_CMD + " | " + SOURCE_CMD + "]");
+                + " | " + SOURCE_CMD + "]");
+            out.println("obr " + URLS_CMD + " [<repository-file-url> ...]");
+            out.println("obr " + LIST_CMD + " [<string> ...]");
+            out.println("obr " + INFO_CMD
+                + " <bundle-name>[;<version>] ...");
+            out.println("obr " + DEPLOY_CMD
+                + " [" + NODEPS_SWITCH
+                + "] <bundle-name>[;<version>] ... | <bundle-id> ...");
+//            out.println("obr " + INSTALL_CMD
+//                + " [" + NODEPS_SWITCH
+//                + "] <bundle-name>[;<version>] ...");
+            out.println("obr " + START_CMD
+                + " [" + NODEPS_SWITCH
+                + "] <bundle-name>[;<version>] ...");
+//            out.println("obr " + UPDATE_CMD + " " + CHECK_SWITCH);
+//            out.println("obr " + UPDATE_CMD
+//                + " [" + NODEPS_SWITCH
+//                + "] <bundle-name>[;<version>] ... | <bundle-id> ...");
+            out.println("obr " + SOURCE_CMD
+                + " [" + EXTRACT_SWITCH
+                + "] <local-dir> <bundle-name>[;<version>] ...");
+        }
+    }
+
+    private static class ParsedCommand
+    {
+        private static final int NAME_IDX = 0;
+        private static final int VERSION_IDX = 1;
+
+        private boolean m_isResolve = true;
+        private boolean m_isCheck = false;
+        private boolean m_isExtract = false;
+        private String m_dir = null;
+        private String[][] m_targets = new String[0][];
+        
+        public boolean isResolve()
+        {
+            return m_isResolve;
+        }
+        
+        public void setResolve(boolean b)
+        {
+            m_isResolve = b;
+        }
+
+        public boolean isCheck()
+        {
+            return m_isCheck;
+        }
+        
+        public void setCheck(boolean b)
+        {
+            m_isCheck = b;
+        }
+
+        public boolean isExtract()
+        {
+            return m_isExtract;
+        }
+        
+        public void setExtract(boolean b)
+        {
+            m_isExtract = b;
+        }
+
+        public String getDirectory()
+        {
+            return m_dir;
+        }
+        
+        public void setDirectory(String s)
+        {
+            m_dir = s;
+        }
+
+        public int getTargetCount()
+        {
+            return m_targets.length;
+        }
+        
+        public String getTargetId(int i)
+        {
+            if ((i < 0) || (i >= getTargetCount()))
+            {
+                return null;
+            }
+            return m_targets[i][NAME_IDX];
+        }
+        
+        public String getTargetVersion(int i)
+        {
+            if ((i < 0) || (i >= getTargetCount()))
+            {
+                return null;
+            }
+            return m_targets[i][VERSION_IDX];
+        }
+
+        public void addTarget(String name, String version)
+        {
+            String[][] newTargets = new String[m_targets.length + 1][];
+            System.arraycopy(m_targets, 0, newTargets, 0, m_targets.length);
+            newTargets[m_targets.length] = new String[] { name, version };
+            m_targets = newTargets;
+        }
+    }
+}
diff --git a/src/org/apache/osgi/bundle/bundlerepository/R4Attribute.java b/src/org/apache/osgi/bundle/bundlerepository/R4Attribute.java
new file mode 100644
index 0000000..d100711
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/R4Attribute.java
@@ -0,0 +1,57 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import org.apache.osgi.service.bundlerepository.IAttribute;
+
+public class R4Attribute implements IAttribute
+{
+    private String m_name = "";
+    private String m_value = "";
+    private boolean m_isMandatory = false;
+    
+    public R4Attribute(String name, String value, boolean isMandatory)
+    {
+        m_name = name;
+        m_value = value;
+        m_isMandatory = isMandatory;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Attribute#getName()
+    **/
+    public String getName()
+    {
+        return m_name;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Attribute#getValue()
+    **/
+    public String getValue()
+    {
+        return m_value;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Attribute#isMandatory()
+    **/
+    public boolean isMandatory()
+    {
+        return m_isMandatory;
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/R4Directive.java b/src/org/apache/osgi/bundle/bundlerepository/R4Directive.java
new file mode 100644
index 0000000..719dd89
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/R4Directive.java
@@ -0,0 +1,47 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import org.apache.osgi.service.bundlerepository.IDirective;
+
+public class R4Directive implements IDirective
+{
+    private String m_name = "";
+    private String m_value = "";
+    
+    public R4Directive(String name, String value)
+    {
+        m_name = name;
+        m_value = value;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Directive#getName()
+    **/
+    public String getName()
+    {
+        return m_name;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Directive#getValue()
+    **/
+    public String getValue()
+    {
+        return m_value;
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/R4Package.java b/src/org/apache/osgi/bundle/bundlerepository/R4Package.java
new file mode 100644
index 0000000..a055133
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/R4Package.java
@@ -0,0 +1,501 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.util.*;
+
+import org.apache.osgi.service.bundlerepository.*;
+import org.osgi.framework.Constants;
+
+//
+// This class is essentially the same as the R4Package class in Felix,
+// except that I had to add the parseDelimitedString() method. These
+// two classes should be unified.
+//
+
+/**
+ * This is a simple class to encapsulate a package declaration for
+ * bundle imports and exports for the bundle repository.
+**/
+public class R4Package implements IPackage
+{
+    private String m_id = "";
+    private IDirective[] m_directives = null;
+    private IAttribute[] m_attrs = null;
+    private IVersion m_versionLow = null;
+    private IVersion m_versionHigh = null;
+    private boolean m_isOptional = false;
+
+    protected R4Package(R4Package pkg)
+    {
+        m_id = pkg.m_id;
+        m_directives = pkg.m_directives;
+        m_attrs = pkg.m_attrs;
+        m_versionLow = pkg.m_versionLow;
+        m_versionHigh = pkg.m_versionHigh;
+        m_isOptional = pkg.m_isOptional;
+    }
+
+    public R4Package(String id, IDirective[] directives, IAttribute[] attrs)
+    {
+        m_id = id;
+        m_directives = (directives == null) ? new IDirective[0] : directives;
+        m_attrs = (attrs == null) ? new IAttribute[0] : attrs;
+
+        // Find mandatory and resolution directives, if present.
+        String mandatory = "";
+        for (int i = 0; i < m_directives.length; i++)
+        {
+            if (m_directives[i].getName().equals(Constants.MANDATORY_DIRECTIVE))
+            {
+                mandatory = m_directives[i].getValue();
+            }
+            else if (m_directives[i].getName().equals(Constants.RESOLUTION_DIRECTIVE))
+            {
+                m_isOptional = m_directives[i].getValue().equals(Constants.RESOLUTION_OPTIONAL);
+            }
+        }
+
+        // Parse mandatory directive and mark specified
+        // attributes as mandatory.
+        StringTokenizer tok = new StringTokenizer(mandatory, "");
+        while (tok.hasMoreTokens())
+        {
+            // Get attribute name.
+            String attrName = tok.nextToken().trim();
+            // Find attribute and mark it as mandatory.
+            boolean found = false;
+            for (int i = 0; (!found) && (i < m_attrs.length); i++)
+            {
+                if (m_attrs[i].getName().equals(attrName))
+                {
+                    m_attrs[i] = new R4Attribute(
+                        m_attrs[i].getName(), m_attrs[i].getValue(), true);
+                    found = true;
+                }
+            }
+            // If a specified mandatory attribute was not found,
+            // then error.
+            if (!found)
+            {
+                throw new IllegalArgumentException(
+                    "Mandatory attribute '" + attrName + "' does not exist.");
+            }
+        }
+
+        // Find and parse version attribute, if present.
+        String versionInterval = "0.0.0";
+        for (int i = 0; i < m_attrs.length; i++)
+        {
+            if (m_attrs[i].getName().equals(Constants.VERSION_ATTRIBUTE) ||
+                m_attrs[i].getName().equals(Constants.PACKAGE_SPECIFICATION_VERSION))
+            {
+                // Normalize version attribute name.
+                m_attrs[i] = new R4Attribute(
+                    Constants.VERSION_ATTRIBUTE, m_attrs[i].getValue(),
+                    m_attrs[i].isMandatory());
+                versionInterval = m_attrs[i].getValue();
+                break;
+            }
+        }
+        
+        IVersion[] versions = parseVersionInterval(versionInterval);
+        m_versionLow = versions[0];
+        if (versions.length == 2)
+        {
+            m_versionHigh = versions[1];
+        }
+    }
+
+    public String getId()
+    {
+        return m_id;
+    }
+
+    public IDirective[] getDirectives()
+    {
+        return m_directives;
+    }
+
+    public IAttribute[] getAttributes()
+    {
+        return m_attrs;
+    }
+
+    public IVersion getVersionLow()
+    {
+        return m_versionLow;
+    }
+
+    public IVersion getVersionHigh()
+    {
+        return m_versionHigh;
+    }
+
+    public boolean isOptional()
+    {
+        return m_isOptional;
+    }
+
+    // PREVIOUSLY PART OF COMPATIBILITY POLICY.
+    public boolean doesSatisfy(IPackage pkg)
+    {
+        // For packages to be compatible, they must have the
+        // same name.
+        if (!m_id.equals(pkg.getId()))
+        {
+            return false;
+        }
+        
+        return isVersionInRange(m_versionLow, pkg.getVersionLow(), pkg.getVersionHigh())
+            && doAttributesMatch(pkg);
+    }
+
+    // PREVIOUSLY PART OF COMPATIBILITY POLICY.
+    public static boolean isVersionInRange(IVersion version, IVersion low, IVersion high)
+    {
+        // We might not have an upper end to the range.
+        if (high == null)
+        {
+            return (version.compareTo(low) >= 0);
+        }
+        else if (low.isInclusive() && high.isInclusive())
+        {
+            return (version.compareTo(low) >= 0) && (version.compareTo(high) <= 0);
+        }
+        else if (high.isInclusive())
+        {
+            return (version.compareTo(low) > 0) && (version.compareTo(high) <= 0);
+        }
+        else if (low.isInclusive())
+        {
+            return (version.compareTo(low) >= 0) && (version.compareTo(high) < 0);
+        }
+
+        return (version.compareTo(low) > 0) && (version.compareTo(high) < 0);
+    }
+
+    private boolean doAttributesMatch(IPackage pkg)
+    {
+        // Cycle through all attributes of the specified package
+        // and make sure their values match the attribute values
+        // of this package.
+        for (int attrIdx = 0; attrIdx < pkg.getAttributes().length; attrIdx++)
+        {
+            // Get current attribute from specified package.
+            IAttribute attr = pkg.getAttributes()[attrIdx];
+
+            // Ignore version attribute, since it is a special case that
+            // has already been compared using isVersionInRange() before
+            // the call to this method was made.
+            if (attr.getName().equals(Constants.VERSION_ATTRIBUTE))
+            {
+                continue;
+            }
+
+            // Check if this package has the same attribute.
+            boolean found = false;
+            for (int thisAttrIdx = 0;
+                (!found) && (thisAttrIdx < m_attrs.length);
+                thisAttrIdx++)
+            {
+                // Get current attribute for this package.
+                IAttribute thisAttr = m_attrs[thisAttrIdx];
+                // Check if the attribute names are equal.
+                if (attr.getName().equals(thisAttr.getName()))
+                {
+                    // If the values are not equal, then return false immediately.
+                    // We should not compare version values here, since they are
+                    // a special case and have already been compared by a call to
+                    // isVersionInRange() before getting here; however, it is
+                    // possible for version to be mandatory, so make sure it is
+                    // present below.
+                    if (!attr.getValue().equals(thisAttr.getValue()))
+                    {
+                        return false;
+                    }
+                    found = true;
+                }
+            }
+            // If the attribute was not found, then return false.
+            if (!found)
+            {
+                return false;
+            }
+        }
+
+        // Now, cycle through all attributes of this package and verify that
+        // all mandatory attributes are present in the speceified package.
+        for (int thisAttrIdx = 0; thisAttrIdx < m_attrs.length; thisAttrIdx++)
+        {
+            // Get current attribute for this package.
+            IAttribute thisAttr = m_attrs[thisAttrIdx];
+            
+            // If the attribute is mandatory, then make sure
+            // the specified package has the attribute.
+            if (thisAttr.isMandatory())
+            {
+                boolean found = false;
+                for (int attrIdx = 0;
+                    (!found) && (attrIdx < pkg.getAttributes().length);
+                    attrIdx++)
+                {
+                    // Get current attribute from specified package.
+                    IAttribute attr = pkg.getAttributes()[attrIdx];
+        
+                    // Check if the attribute names are equal
+                    // and set found flag.
+                    if (thisAttr.getName().equals(attr.getName()))
+                    {
+                        found = true;
+                    }
+                }
+                // If not found, then return false.
+                if (!found)
+                {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public String toString()
+    {
+        String msg = getId();
+        for (int i = 0; (m_directives != null) && (i < m_directives.length); i++)
+        {
+            msg = msg + " [" + m_directives[i].getName() + ":="+ m_directives[i].getName() + "]";
+        }
+        for (int i = 0; (m_attrs != null) && (i < m_attrs.length); i++)
+        {
+            msg = msg + " [" + m_attrs[i].getValue() + "="+ m_attrs[i].getValue() + "]";
+        }
+        return msg;
+    }
+
+    // Like this: pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2,
+    //            pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2
+    public static IPackage[] parseImportOrExportHeader(String s)
+    {
+        IPackage[] pkgs = null;
+        if (s != null)
+        {
+            if (s.length() == 0)
+            {
+                throw new IllegalArgumentException(
+                    "The import and export headers cannot be an empty string.");
+            }
+            String[] ss = parseDelimitedString(s, ","); // FelixConstants.CLASS_PATH_SEPARATOR
+            pkgs = parsePackageStrings(ss);
+        }
+        return (pkgs == null) ? new IPackage[0] : pkgs;
+    }
+
+    // Like this: pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2
+    public static IPackage[] parsePackageStrings(String[] ss)
+        throws IllegalArgumentException
+    {
+        if (ss == null)
+        {
+            return null;
+        }
+
+        List completeList = new ArrayList();
+        for (int ssIdx = 0; ssIdx < ss.length; ssIdx++)
+        {
+            // Break string into semi-colon delimited pieces.
+            String[] pieces = parseDelimitedString(
+                ss[ssIdx], ";"); // FelixConstants.PACKAGE_SEPARATOR
+
+            // Count the number of different packages; packages
+            // will not have an '=' in their string. This assumes
+            // that packages come first, before directives and
+            // attributes.
+            int pkgCount = 0;
+            for (int pieceIdx = 0; pieceIdx < pieces.length; pieceIdx++)
+            {
+                if (pieces[pieceIdx].indexOf('=') >= 0)
+                {
+                    break;
+                }
+                pkgCount++;
+            }
+
+            // Error if no packages were specified.
+            if (pkgCount == 0)
+            {
+                throw new IllegalArgumentException(
+                    "No packages specified on import: " + ss[ssIdx]);
+            }
+
+            // Parse the directives/attributes.
+            IDirective[] dirs = new IDirective[pieces.length - pkgCount];
+            IAttribute[] attrs = new IAttribute[pieces.length - pkgCount];
+            int dirCount = 0, attrCount = 0;
+            int idx = -1;
+            String sep = null;
+            for (int pieceIdx = pkgCount; pieceIdx < pieces.length; pieceIdx++)
+            {
+                // Check if it is a directive.
+                if ((idx = pieces[pieceIdx].indexOf(":=")) >= 0) // FelixConstants.DIRECTIVE_SEPARATOR
+                {
+                    sep = ":="; // FelixConstants.DIRECTIVE_SEPARATOR
+                }
+                // Check if it is an attribute.
+                else if ((idx = pieces[pieceIdx].indexOf("=")) >= 0) // FelixConstants.ATTRIBUTE_SEPARATOR
+                {
+                    sep = "="; // FelixConstants.ATTRIBUTE_SEPARATOR
+                }
+                // It is an error.
+                else
+                {
+                    throw new IllegalArgumentException(
+                        "Not a directive/attribute: " + ss[ssIdx]);
+                }
+
+                String key = pieces[pieceIdx].substring(0, idx).trim();
+                String value = pieces[pieceIdx].substring(idx + sep.length()).trim();
+
+                // Remove quotes, if value is quoted.
+                if (value.startsWith("\"") && value.endsWith("\""))
+                {
+                    value = value.substring(1, value.length() - 1);
+                }
+
+                // Save the directive/attribute in the appropriate array.
+                if (sep.equals(":=")) // FelixConstants.DIRECTIVE_SEPARATOR
+                {
+                    dirs[dirCount++] = new R4Directive(key, value);
+                }
+                else
+                {
+                    attrs[attrCount++] = new R4Attribute(key, value, false);
+                }
+            }
+
+            // Shrink directive array.
+            IDirective[] dirsFinal = new IDirective[dirCount];
+            System.arraycopy(dirs, 0, dirsFinal, 0, dirCount);
+            // Shrink attribute array.
+            IAttribute[] attrsFinal = new IAttribute[attrCount];
+            System.arraycopy(attrs, 0, attrsFinal, 0, attrCount);
+
+            // Create package attributes for each package and
+            // set directives/attributes. Add each package to
+            // completel list of packages.
+            IPackage[] pkgs = new IPackage[pkgCount];
+            for (int pkgIdx = 0; pkgIdx < pkgCount; pkgIdx++)
+            {
+                pkgs[pkgIdx] = new R4Package(pieces[pkgIdx], dirsFinal, attrsFinal);
+                completeList.add(pkgs[pkgIdx]);
+            }
+        }
+    
+        IPackage[] ips = (IPackage[])
+            completeList.toArray(new IPackage[completeList.size()]);
+        return ips;
+    }
+
+    public static IVersion[] parseVersionInterval(String interval)
+    {
+        // Check if the version is an interval.
+        if (interval.indexOf(',') >= 0)
+        {
+            String s = interval.substring(1, interval.length() - 1);
+            String vlo = s.substring(0, s.indexOf(','));
+            String vhi = s.substring(s.indexOf(',') + 1, s.length());
+            return new IVersion[] {
+                new R4Version(vlo, (interval.charAt(0) == '[')),
+                new R4Version(vhi, (interval.charAt(interval.length() - 1) == ']'))
+            };
+        }
+        else
+        {
+            return new IVersion[] { new R4Version(interval, true) };
+        }
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @return an array of string tokens or null if there were no tokens.
+    **/
+    public static String[] parseDelimitedString(String value, String delim)
+    {
+        if (value == null)
+        {
+           value = "";
+        }
+
+        List list = new ArrayList();
+
+        int CHAR = 1;
+        int DELIMITER = 2;
+        int STARTQUOTE = 4;
+        int ENDQUOTE = 8;
+
+        StringBuffer sb = new StringBuffer();
+
+        int expecting = (CHAR | DELIMITER | STARTQUOTE);
+        
+        for (int i = 0; i < value.length(); i++)
+        {
+            char c = value.charAt(i);
+
+            boolean isDelimiter = (delim.indexOf(c) >= 0);
+            boolean isQuote = (c == '"');
+
+            if (isDelimiter && ((expecting & DELIMITER) > 0))
+            {
+                list.add(sb.toString().trim());
+                sb.delete(0, sb.length());
+                expecting = (CHAR | DELIMITER | STARTQUOTE);
+            }
+            else if (isQuote && ((expecting & STARTQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            }
+            else if (isQuote && ((expecting & ENDQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = (CHAR | STARTQUOTE | DELIMITER);
+            }
+            else if ((expecting & CHAR) > 0)
+            {
+                sb.append(c);
+            }
+            else
+            {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+        }
+
+        if (sb.length() > 0)
+        {
+            list.add(sb.toString().trim());
+        }
+
+        return (String[]) list.toArray(new String[list.size()]);
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/R4Version.java b/src/org/apache/osgi/bundle/bundlerepository/R4Version.java
new file mode 100644
index 0000000..eb44a9a
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/R4Version.java
@@ -0,0 +1,216 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.util.StringTokenizer;
+
+import org.apache.osgi.service.bundlerepository.IVersion;
+
+public class R4Version implements Comparable, IVersion
+{
+    private int m_major = 0;
+    private int m_minor = 0;
+    private int m_micro = 0;
+    private String m_qualifier = "";
+    private boolean m_isInclusive = true;
+
+    private static final String SEPARATOR = ".";
+
+    public R4Version(String versionString)
+    {
+        this(versionString, true);
+    }
+
+    public R4Version(String versionString, boolean isInclusive)
+    {
+        if (versionString == null)
+        {
+            versionString = "0.0.0";
+        }
+        Object[] objs = parseVersion(versionString);
+        m_major = ((Integer) objs[0]).intValue();
+        m_minor = ((Integer) objs[1]).intValue();
+        m_micro = ((Integer) objs[2]).intValue();
+        m_qualifier = (String) objs[3];
+        m_isInclusive = isInclusive;
+    }
+
+    private static Object[] parseVersion(String versionString)
+    {
+        String s = versionString.trim();
+        Object[] objs = new Object[4];
+        objs[0] = objs[1] = objs[2] = new Integer(0);
+        objs[3] = "";
+        StringTokenizer tok = new StringTokenizer(s, SEPARATOR);
+        try
+        {
+            objs[0] = Integer.valueOf(tok.nextToken());
+            if (tok.hasMoreTokens())
+            {
+                objs[1] = Integer.valueOf(tok.nextToken());
+                if (tok.hasMoreTokens())
+                {
+                    objs[2] = Integer.valueOf(tok.nextToken());
+                    if (tok.hasMoreTokens())
+                    {
+                        objs[3] = tok.nextToken();
+                    }
+                }
+            }
+        }
+        catch (NumberFormatException ex)
+        {
+            throw new IllegalArgumentException("Invalid version: " + versionString);
+        }
+
+        if ((((Integer) objs[0]).intValue() < 0) ||
+            (((Integer) objs[0]).intValue() < 0) ||
+            (((Integer) objs[0]).intValue() < 0))
+        {
+            throw new IllegalArgumentException("Invalid version: " + versionString);
+        }
+
+        return objs;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#equals(java.lang.Object)
+    **/
+    public boolean equals(Object object)
+    {
+        if (!(object instanceof R4Version))
+        {
+            return false;
+        }
+        IVersion v = (IVersion) object;
+        return
+            (v.getMajorComponent() == m_major) &&
+            (v.getMinorComponent() == m_minor) &&
+            (v.getMicroComponent() == m_micro) &&
+            (v.getQualifierComponent().equals(m_qualifier));
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#getMajorComponent()
+    **/
+    public int getMajorComponent()
+    {
+        return m_major;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#getMinorComponent()
+    **/
+    public int getMinorComponent()
+    {
+        return m_minor;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#getMicroComponent()
+    **/
+    public int getMicroComponent()
+    {
+        return m_micro;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#getQualifierComponent()
+    **/
+    public String getQualifierComponent()
+    {
+        return m_qualifier;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#isInclusive()
+    **/
+    public boolean isInclusive()
+    {
+        return m_isInclusive;
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#compareTo(java.lang.Object)
+    **/
+    public int compareTo(Object o)
+    {
+        if (!(o instanceof R4Version))
+            throw new ClassCastException();
+
+        if (equals(o))
+            return 0;
+
+        if (isGreaterThan((IVersion) o))
+            return 1;
+
+        return -1;
+    }
+
+    private boolean isGreaterThan(IVersion v)
+    {
+        if (v == null)
+        {
+            return false;
+        }
+
+        if (m_major > v.getMajorComponent())
+        {
+            return true;
+        }
+        if (m_major < v.getMajorComponent())
+        {
+            return false;
+        }
+        if (m_minor > v.getMinorComponent())
+        {
+            return true;
+        }
+        if (m_minor < v.getMinorComponent())
+        {
+            return false;
+        }
+        if (m_micro > v.getMicroComponent())
+        {
+            return true;
+        }
+        if (m_micro < v.getMicroComponent())
+        {
+            return false;
+        }
+        if (m_qualifier.compareTo(v.getQualifierComponent()) > 0)
+        {
+            return true;
+        }
+        else
+        {
+            return false;
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.ungoverned.osgi.service.bundlerepository.Version#toString()
+    **/
+    public String toString()
+    {
+        if (m_qualifier.length() == 0)
+        {
+            return m_major + "." + m_minor + "." + m_micro; 
+        }
+        return m_major + "." + m_minor + "." + m_micro + "." + m_qualifier; 
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/RepositoryState.java b/src/org/apache/osgi/bundle/bundlerepository/RepositoryState.java
new file mode 100644
index 0000000..b8b52d8
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/RepositoryState.java
@@ -0,0 +1,563 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import org.apache.osgi.bundle.bundlerepository.kxmlsax.KXmlSAXParser;
+import org.apache.osgi.bundle.bundlerepository.metadataparser.MultivalueMap;
+import org.apache.osgi.bundle.bundlerepository.metadataparser.XmlCommonHandler;
+import org.apache.osgi.service.bundlerepository.BundleRecord;
+import org.apache.osgi.service.bundlerepository.ResolveException;
+import org.osgi.framework.*;
+
+public class RepositoryState
+{
+    private BundleContext m_context = null;
+    private String[] m_urls = null;
+    private Map m_recordMap = new HashMap();
+    private BundleRecord[] m_recordArray = null;
+    private boolean m_initialized = false;
+
+    private int m_hopCount = 1;
+
+    private static final String[] DEFAULT_REPOSITORY_URL = {
+        "http://oscar-osgi.sf.net/alpha/repository.xml"
+    };
+    public static final String REPOSITORY_URL_PROP = "osgi.repository.url";
+    public static final String EXTERN_REPOSITORY_TAG = "extern-repositories";
+
+    public RepositoryState(BundleContext context)
+    {
+        m_context = context;
+
+        String urlStr = m_context.getProperty(REPOSITORY_URL_PROP);
+        if (urlStr != null)
+        {
+            StringTokenizer st = new StringTokenizer(urlStr);
+            if (st.countTokens() > 0)
+            {
+                m_urls = new String[st.countTokens()];
+                for (int i = 0; i < m_urls.length; i++)
+                {
+                    m_urls[i] = st.nextToken();
+                }
+            }
+        }
+
+        // Use the default URL if none were specified.
+        if (m_urls == null)
+        {
+            m_urls = DEFAULT_REPOSITORY_URL;
+        }
+    }
+
+    public String[] getURLs()
+    {
+        // Return a copy because the array is mutable.
+        return (m_urls == null) ? null : (String[]) m_urls.clone();
+    }
+
+    public void setURLs(String[] urls)
+    {
+        if (urls != null)
+        {
+            m_urls = urls;
+            initialize();
+        }
+    }
+    
+    public BundleRecord[] getRecords()
+    {
+        if (!m_initialized)
+        {
+            initialize();
+        }
+
+        // Returned cached array of bundle records.
+        return m_recordArray;
+    }
+
+    public BundleRecord[] getRecords(String symName)
+    {
+        if (!m_initialized)
+        {
+            initialize();
+        }
+
+        // Return a copy of the array, since it would be mutable
+        // otherwise.
+        BundleRecord[] records = (BundleRecord[]) m_recordMap.get(symName);
+        // Return a copy because the array is mutable.
+        return (records == null) ? null : (BundleRecord[]) records.clone();
+    }
+
+    public BundleRecord getRecord(String symName, int[] version)
+    {
+        if (!m_initialized)
+        {
+            initialize();
+        }
+
+        BundleRecord[] records = (BundleRecord[]) m_recordMap.get(symName);
+        if ((records != null) && (records.length > 0))
+        {
+            for (int i = 0; i < records.length; i++)
+            {
+                int[] targetVersion = Util.parseVersionString(
+                    (String) records[i].getAttribute(BundleRecord.BUNDLE_VERSION));
+            
+                if (Util.compareVersion(targetVersion, version) == 0)
+                {
+                    return records[i];
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public BundleRecord[] resolvePackages(LocalState localState, Filter[] reqFilters)
+        throws ResolveException
+    {
+        // Create a list that will contain the transitive closure of
+        // all import dependencies; use a list because this will keep
+        // everything in order.
+        List deployList = new ArrayList();
+        // Add the target bundle
+        resolvePackages(localState, reqFilters, deployList);
+        
+        // Convert list of symbolic names to an array of bundle
+        // records and return it.
+        BundleRecord[] records = new BundleRecord[deployList.size()];
+        return (BundleRecord[]) deployList.toArray(records);
+    }
+
+    private void resolvePackages(
+        LocalState localState, Filter[] reqFilters, List deployList)
+        throws ResolveException
+    {
+        for (int reqIdx = 0;
+            (reqFilters != null) && (reqIdx < reqFilters.length);
+            reqIdx++)
+        {
+            // If the package can be locally resolved, then
+            // it can be completely ignored; otherwise, try
+            // to find a resolving bundle.
+            if (!localState.isResolvable(reqFilters[reqIdx]))
+            {
+                // Select resolving bundle for current package.
+                BundleRecord source = selectResolvingBundle(
+                    deployList, localState, reqFilters[reqIdx]);
+                // If there is no resolving bundle, then throw a
+                // resolve exception.
+                if (source == null)
+                {
+throw new IllegalArgumentException("HACK: SHOULD THROW RESOLVE EXCEPTION: " + reqFilters[reqIdx]);
+//                    throw new ResolveException(reqFilters[reqIdx]);
+                }
+                // If the resolving bundle is already in the deploy list,
+                // then just ignore it; otherwise, add it to the deploy
+                // list and resolve its packages.
+                if (!deployList.contains(source))
+                {
+                    deployList.add(source);
+                    Filter[] filters = (Filter[])
+                        source.getAttribute("requirements");
+                    resolvePackages(localState, filters, deployList);
+                }
+            }
+        }
+    }
+
+    /**
+     * Selects a single source bundle record for the target package from
+     * the repository. The algorithm tries to select a source bundle record
+     * if it is already installed locally in the framework; this approach
+     * favors updating already installed bundles rather than installing
+     * new ones. If no matching bundles are installed locally, then the
+     * first bundle record providing the target package is returned.
+     * @param targetPkg the target package for which to select a source
+     *        bundle record.
+     * @return the selected bundle record or <tt>null</tt> if no sources
+     *         could be found.
+    **/
+    private BundleRecord selectResolvingBundle(
+        List deployList, LocalState localState, Filter targetFilter)
+    {
+        BundleRecord[] exporters = findExporters(targetFilter);
+        if (exporters == null)
+        {
+            return null;
+        }
+
+        // Try to select a source bundle record that is already
+        // in the deployed list to minimize the number of bundles
+        // that need to be deployed. If this is not possible, then
+        // try to select a bundle that is already installed locally,
+        // since it might be possible to update this bundle to
+        // minimize the number of bundles installed in the framework.
+        for (int i = 0; i < exporters.length; i++)
+        {
+            if (deployList.contains(exporters[i]))
+            {
+                return exporters[i];
+            }
+            else
+            {
+                String symName = (String)
+                    exporters[i].getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME);
+                if (symName != null)
+                {
+                    BundleRecord[] records = localState.findBundles(symName);
+                    if (records != null)
+                    {
+                        return exporters[i];
+                    }
+                }
+            }
+        }
+            
+        // If none of the sources are installed locally, then
+        // just pick the first one.
+        return exporters[0];
+    }
+
+    /**
+     * Returns an array of bundle records that resolve the supplied
+     * package declaration.
+     * @param target the package declaration to resolve.
+     * @return an array of bundle records that resolve the package
+     *         declaration or <tt>null</tt> if none are found.
+    **/
+    private BundleRecord[] findExporters(Filter targetFilter)
+    {
+        MapToDictionary mapDict = new MapToDictionary(null);
+
+        // Create a list for storing bundles that can resolve package.
+        List resolveList = new ArrayList();
+        for (int recIdx = 0; recIdx < m_recordArray.length; recIdx++)
+        {
+            Map[] capMaps = (Map[]) m_recordArray[recIdx].getAttribute("capability");
+            for (int capIdx = 0; capIdx < capMaps.length; capIdx++)
+            {
+                mapDict.setSourceMap(capMaps[capIdx]);
+                if (targetFilter.match(mapDict))
+                {
+                    resolveList.add(m_recordArray[recIdx]);
+                }
+            }
+        }
+
+        // If no resolving bundles were found, return null.
+        if (resolveList.size() == 0)
+        {
+            return null;
+        }
+
+        // Otherwise, return an array containing resolving bundles.
+        return (BundleRecord[]) resolveList.toArray(new BundleRecord[resolveList.size()]);
+    }
+
+    private boolean isUpdateAvailable(
+        PrintStream out, PrintStream err, Bundle bundle)
+    {
+        // Get the bundle's update location.
+        String symname =
+            (String) bundle.getHeaders().get(BundleRecord.BUNDLE_SYMBOLICNAME);
+
+        // Get associated repository bundle recorded for the
+        // local bundle and see if an update is necessary.
+        BundleRecord[] records = getRecords(symname);
+        if (records == null)
+        {
+            err.println(Util.getBundleName(bundle) + " not in repository.");
+            return false;
+        }
+        
+        // Check bundle version againts bundle record version.
+        for (int i = 0; i < records.length; i++)
+        {
+            int[] bundleVersion = Util.parseVersionString(
+                (String) bundle.getHeaders().get(BundleRecord.BUNDLE_VERSION));
+            int[] recordVersion = Util.parseVersionString(
+                (String) records[i].getAttribute(BundleRecord.BUNDLE_VERSION));
+            if (Util.compareVersion(recordVersion, bundleVersion) > 0)
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void initialize()
+    {
+        m_initialized = true;
+        m_recordMap.clear();
+
+        for (int urlIdx = 0; (m_urls != null) && (urlIdx < m_urls.length); urlIdx++)
+        {
+            parseRepositoryFile(m_hopCount, m_urls[urlIdx]);
+        }
+        
+        // Cache a sorted array of all bundle records.
+        List list = new ArrayList();
+        for (Iterator i = m_recordMap.entrySet().iterator(); i.hasNext(); )
+        {
+            BundleRecord[] records = (BundleRecord[]) ((Map.Entry) i.next()).getValue();
+            for (int recIdx = 0; recIdx < records.length; recIdx++)
+            {
+                list.add(records[recIdx]);
+            }
+        }
+        m_recordArray = (BundleRecord[]) list.toArray(new BundleRecord[list.size()]);
+        Arrays.sort(m_recordArray, new Comparator() {
+            public int compare(Object o1, Object o2)
+            {
+                BundleRecord r1 = (BundleRecord) o1;
+                BundleRecord r2 = (BundleRecord) o2;
+                String name1 = (String) r1.getAttribute(BundleRecord.BUNDLE_NAME);
+                String name2 = (String) r2.getAttribute(BundleRecord.BUNDLE_NAME);
+                return name1.compareToIgnoreCase(name2);
+            }
+        });
+    }
+
+    private void parseRepositoryFile(int hopCount, String urlStr)
+    {
+        InputStream is = null;
+        BufferedReader br = null;
+
+        try
+        {
+            // Do it the manual way to have a chance to 
+            // set request properties as proxy auth (EW).
+            URL url = new URL(urlStr);
+            URLConnection conn = url.openConnection(); 
+
+            // Support for http proxy authentication
+            String auth = System.getProperty("http.proxyAuth");
+            if ((auth != null) && (auth.length() > 0))
+            {
+                if ("http".equals(url.getProtocol()) ||
+                    "https".equals(url.getProtocol()))
+                {
+                    String base64 = Util.base64Encode(auth);
+                    conn.setRequestProperty(
+                        "Proxy-Authorization", "Basic " + base64);
+                }
+            }
+            is = conn.getInputStream();
+
+            // Create the parser Kxml
+            XmlCommonHandler handler = new XmlCommonHandler();
+            handler.addType("bundles", ArrayList.class);
+            handler.addType("repository", HashMap.class);
+            handler.addType("extern-repositories", ArrayList.class);
+            handler.addType("bundle", MultivalueMap.class);
+            handler.addType("requirement", String.class);
+            handler.addType("capability", ArrayList.class);
+            handler.addType("property", HashMap.class);
+            handler.setDefaultType(String.class);
+
+            br = new BufferedReader(new InputStreamReader(is));
+            KXmlSAXParser parser;
+            parser = new KXmlSAXParser(br);
+            try
+            {
+                parser.parseXML(handler);
+            }
+            catch (Exception ex)
+            {
+                ex.printStackTrace();
+                return;
+            }
+
+            List root = (List) handler.getRoot();
+            for (int bundleIdx = 0; bundleIdx < root.size(); bundleIdx++)
+            {
+                Object obj = root.get(bundleIdx);
+                
+                // The elements of the root will either be a HashMap for
+                // the repository tag or a MultivalueMap for the bundle
+                // tag, as indicated above when we parsed the file.
+                
+                // If HashMap, then read repository information.
+                if (obj instanceof HashMap)
+                {
+                    // Create a case-insensitive map.
+                    Map repoMap = new TreeMap(new Comparator() {
+                        public int compare(Object o1, Object o2)
+                        {
+                            return o1.toString().compareToIgnoreCase(o2.toString());
+                        }
+                    });
+                    repoMap.putAll((Map) obj);
+
+                    // Process external repositories if hop count is
+                    // greater than zero.
+                    if (hopCount > 0)
+                    {
+                        // Get the external repository list.
+                        List externList = (List) repoMap.get(EXTERN_REPOSITORY_TAG);
+                        for (int i = 0; (externList != null) && (i < externList.size()); i++)
+                        {
+                            parseRepositoryFile(hopCount - 1, (String) externList.get(i));
+                        }
+                    }
+                }
+                // Else if mulitvalue map, then create a bundle record
+                // for the associated bundle meta-data.
+                else if (obj instanceof MultivalueMap)
+                {
+                    // Create a case-insensitive map.
+                    Map bundleMap = new TreeMap(new Comparator() {
+                        public int compare(Object o1, Object o2)
+                        {
+                            return o1.toString().compareToIgnoreCase(o2.toString());
+                        }
+                    });
+                    bundleMap.putAll((Map) obj);
+
+                    // Convert capabilities into case-insensitive maps.
+                    List list = (List) bundleMap.get("capability");
+                    Map[] capabilityMaps = convertCapabilities(list);
+                    bundleMap.put("capability", capabilityMaps);
+
+                    // Convert requirements info filters.
+                    list = (List) bundleMap.get("requirement");
+                    Filter[] filters = convertRequirements(list);
+                    bundleMap.put("requirement", filters);
+
+                    // Convert any remaining single-element lists into
+                    // the element itself.
+                    for (Iterator i = bundleMap.keySet().iterator(); i.hasNext(); )
+                    {
+                        Object key = i.next();
+                        Object value = bundleMap.get(key);
+                        if ((value instanceof List) &&
+                            (((List) value).size() == 1))
+                        {
+                            bundleMap.put(key, ((List) value).get(0));
+                        }
+                    }
+
+                    // Create a bundle record using the map.
+                    BundleRecord record = new BundleRecord(bundleMap);
+                    // TODO: Filter duplicates.
+                    BundleRecord[] records =
+                        (BundleRecord[]) m_recordMap.get(
+                            record.getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME));
+                    if (records == null)
+                    {
+                        records = new BundleRecord[] { record };
+                    }
+                    else
+                    {
+                        BundleRecord[] newRecords = new BundleRecord[records.length + 1];
+                        System.arraycopy(records, 0, newRecords, 0, records.length);
+                        newRecords[records.length] = record;
+                        records = newRecords;
+                    }
+                    m_recordMap.put(
+                        record.getAttribute(BundleRecord.BUNDLE_SYMBOLICNAME), records);
+                }
+            }
+        }
+        catch (MalformedURLException ex)
+        {
+            ex.printStackTrace(System.err);
+//            System.err.println("Error: " + ex);
+        }
+        catch (IOException ex)
+        {
+            ex.printStackTrace(System.err);
+//            System.err.println("Error: " + ex);
+        }
+        finally
+        {
+            try
+            {
+                if (is != null) is.close();
+            }
+            catch (IOException ex)
+            {
+                // Not much we can do.
+            }
+        }
+    }
+
+    private Map[] convertCapabilities(List capLists)
+    {
+        Map[] capabilityMaps = new Map[(capLists == null) ? 0 : capLists.size()];
+        for (int capIdx = 0; (capLists != null) && (capIdx < capLists.size()); capIdx++)
+        {
+            // Create a case-insensitive map.
+            capabilityMaps[capIdx] = new TreeMap(new Comparator() {
+                public int compare(Object o1, Object o2)
+                {
+                    return o1.toString().compareToIgnoreCase(o2.toString());
+                }
+            });
+
+            List capList = (List) capLists.get(capIdx);
+            
+            for (int propIdx = 0; propIdx < capList.size(); propIdx++)
+            {
+                Map propMap = (Map) capList.get(propIdx);
+                String name = (String) propMap.get("name");
+                String type = (String) propMap.get("type");
+                String value = (String) propMap.get("value");
+                try
+                {
+                    Class clazz = this.getClass().getClassLoader().loadClass(type);
+                    Object o = clazz
+                        .getConstructor(new Class[] { String.class })
+                            .newInstance(new Object[] { value });
+                    capabilityMaps[capIdx].put(name, o);
+                }
+                catch (Exception ex)
+                {
+// TODO: DETERMINE WHAT TO DO HERE.
+                    // Two options here, we can either ignore the
+                    // entire capability or we can just ignore the
+                    // property. For now, just ignore the property.
+                    continue;
+                }
+            }
+        }
+        return capabilityMaps;
+    }
+
+    private Filter[] convertRequirements(List reqsList)
+    {
+        Filter[] filters = new Filter[(reqsList == null) ? 0 : reqsList.size()];
+        for (int i = 0; (reqsList != null) && (i < reqsList.size()); i++)
+        {
+            try
+            {
+                filters[i] = m_context.createFilter((String) reqsList.get(i));
+            }
+            catch (InvalidSyntaxException ex)
+            {
+            }
+        }
+        return filters;
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/Util.java b/src/org/apache/osgi/bundle/bundlerepository/Util.java
new file mode 100644
index 0000000..7c40551
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/Util.java
@@ -0,0 +1,208 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository;
+
+import java.io.*;
+import java.util.StringTokenizer;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+public class Util
+{
+    public static String getBundleName(Bundle bundle)
+    {
+        String name = (String) bundle.getHeaders().get(Constants.BUNDLE_NAME);
+        return (name == null)
+            ? "Bundle " + Long.toString(bundle.getBundleId())
+            : name;
+    }
+
+    public static int compareVersion(int[] v1, int[] v2)
+    {
+        if (v1[0] > v2[0])
+        {
+            return 1;
+        }
+        else if (v1[0] < v2[0])
+        {
+            return -1;
+        }
+        else if (v1[1] > v2[1])
+        {
+            return 1;
+        }
+        else if (v1[1] < v2[1])
+        {
+            return -1;
+        }
+        else if (v1[2] > v2[2])
+        {
+            return 1;
+        }
+        else if (v1[2] < v2[2])
+        {
+            return -1;
+        }
+        return 0;
+    }
+
+    public static int[] parseVersionString(String s)
+    {
+        int[] version = new int[] { 0, 0, 0 };
+
+        if (s != null)
+        {
+            StringTokenizer st = new StringTokenizer(s, ".");
+            if (st.hasMoreTokens())
+            {
+                try
+                {
+                    version[0] = Integer.parseInt(st.nextToken());
+                    if (st.hasMoreTokens())
+                    {
+                        version[1] = Integer.parseInt(st.nextToken());
+                        if (st.hasMoreTokens())
+                        {
+                            version[2] = Integer.parseInt(st.nextToken());
+                        }
+                    }
+                    return version;
+                }
+                catch (NumberFormatException ex)
+                {
+                    throw new IllegalArgumentException(
+                        "Improper version number.");
+                }
+            }
+        }
+
+        return version;
+    }
+
+    private static final byte encTab[] = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46,
+        0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52,
+        0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64,
+        0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70,
+        0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31,
+        0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f };
+
+    private static final byte decTab[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1,
+        -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1,
+        -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
+        18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29,
+        30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+        48, 49, 50, 51, -1, -1, -1, -1, -1 };
+
+    public static String base64Encode(String s) throws IOException
+    {
+        return encode(s.getBytes(), 0);
+    }
+
+    /**
+     * Encode a raw byte array to a Base64 String.
+     * 
+     * @param in Byte array to encode.
+     * @param len Length of Base64 lines. 0 means no line breaks.
+    **/
+    public static String encode(byte[] in, int len) throws IOException
+    {
+        ByteArrayOutputStream baos = null;
+        ByteArrayInputStream bais = null;
+        try
+        {
+            baos = new ByteArrayOutputStream();
+            bais = new ByteArrayInputStream(in);
+            encode(bais, baos, len);
+            // ASCII byte array to String
+            return (new String(baos.toByteArray()));
+        }
+        finally
+        {
+            if (baos != null)
+            {
+                baos.close();
+            }
+            if (bais != null)
+            {
+                bais.close();
+            }
+        }
+    }
+
+    public static void encode(InputStream in, OutputStream out, int len)
+        throws IOException
+    {
+
+        // Check that length is a multiple of 4 bytes
+        if (len % 4 != 0)
+        {
+            throw new IllegalArgumentException("Length must be a multiple of 4");
+        }
+
+        // Read input stream until end of file
+        int bits = 0;
+        int nbits = 0;
+        int nbytes = 0;
+        int b;
+
+        while ((b = in.read()) != -1)
+        {
+            bits = (bits << 8) | b;
+            nbits += 8;
+            while (nbits >= 6)
+            {
+                nbits -= 6;
+                out.write(encTab[0x3f & (bits >> nbits)]);
+                nbytes++;
+                // New line
+                if (len != 0 && nbytes >= len)
+                {
+                    out.write(0x0d);
+                    out.write(0x0a);
+                    nbytes -= len;
+                }
+            }
+        }
+
+        switch (nbits)
+        {
+            case 2:
+                out.write(encTab[0x3f & (bits << 4)]);
+                out.write(0x3d); // 0x3d = '='
+                out.write(0x3d);
+                break;
+            case 4:
+                out.write(encTab[0x3f & (bits << 2)]);
+                out.write(0x3d);
+                break;
+        }
+
+        if (len != 0)
+        {
+            if (nbytes != 0)
+            {
+                out.write(0x0d);
+                out.write(0x0a);
+            }
+            out.write(0x0d);
+            out.write(0x0a);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/kxmlsax/KXmlSAXHandler.java b/src/org/apache/osgi/bundle/bundlerepository/kxmlsax/KXmlSAXHandler.java
new file mode 100644
index 0000000..b6a8e1d
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/kxmlsax/KXmlSAXHandler.java
@@ -0,0 +1,68 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository.kxmlsax;
+
+import java.util.Properties;
+
+import org.xml.sax.SAXException;
+
+/**
+ * Interface for SAX handler with kXML
+ *
+ * @author Didier Donsez (didier.donsez@imag.fr)
+ */
+public interface KXmlSAXHandler {
+
+	/**
+	* Method called when parsing text
+	*
+	* @param   ch
+	* @param   offset
+	* @param   length
+	* @exception   SAXException
+	*/
+	public void characters(char[] ch, int offset, int length) throws Exception;
+
+	/**
+	* Method called when a tag opens
+	*
+	* @param   uri
+	* @param   localName
+	* @param   qName
+	* @param   attrib
+	* @exception   SAXException
+	**/
+	public void startElement(
+		String uri,
+		String localName,
+		String qName,
+		Properties attrib)
+		throws Exception;
+	/**
+	* Method called when a tag closes
+	*
+	* @param   uri
+	* @param   localName
+	* @param   qName
+	* @exception   SAXException
+	*/
+	public void endElement(
+		java.lang.String uri,
+		java.lang.String localName,
+		java.lang.String qName)
+		throws Exception;
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/kxmlsax/KXmlSAXParser.java b/src/org/apache/osgi/bundle/bundlerepository/kxmlsax/KXmlSAXParser.java
new file mode 100644
index 0000000..1bb9b25
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/kxmlsax/KXmlSAXParser.java
@@ -0,0 +1,79 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository.kxmlsax;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Properties;
+
+import org.kxml.Attribute;
+import org.kxml.Xml;
+import org.kxml.parser.ParseEvent;
+import org.kxml.parser.XmlParser;
+
+/**
+ * The KXmlSAXParser extends the XmlParser from kxml. This is a very
+ * simple parser that does not take into account the DTD
+ *
+ * @version 	1.0 08 Nov 2002
+ * @version 	1.1 24 Apr 2004
+ * @author 	Humberto Cervantes, Didier Donsez
+ */
+public class KXmlSAXParser extends XmlParser {
+	/**
+	* The constructor for a parser, it receives a java.io.Reader.
+	*
+	* @param   reader  The reader
+	* @exception   IOException thrown by the superclass
+	*/
+	public KXmlSAXParser(Reader r) throws IOException {
+		super(r);
+	}
+
+	/**
+	* Parser from the reader provided in the constructor, and call
+	* the startElement and endElement in a KxmlHandler
+	*
+	* @param   reader  The reader
+	* @exception   Exception thrown by the superclass
+	*/
+	public void parseXML(KXmlSAXHandler handler) throws Exception {
+		ParseEvent evt = null;
+		do {
+			evt = read();
+			if (evt.getType() == Xml.START_TAG) {
+				Properties props = new Properties();
+				for (int i = 0; i < evt.getAttributeCount(); i++) {
+					Attribute attr = evt.getAttribute(i);
+					props.put(attr.getName(), attr.getValue());
+				}
+				handler.startElement(
+					"uri",
+					evt.getName(),
+					evt.getName(),
+					props);
+			} else if (evt.getType() == Xml.END_TAG) {
+				handler.endElement("uri", evt.getName(), evt.getName());
+			} else if (evt.getType() == Xml.TEXT) {
+				String text = evt.getText();
+				handler.characters(text.toCharArray(),0,text.length());
+			} else {
+				// do nothing
+			}
+		} while (evt.getType() != Xml.END_DOCUMENT);
+	}
+}
diff --git a/src/org/apache/osgi/bundle/bundlerepository/manifest.mf b/src/org/apache/osgi/bundle/bundlerepository/manifest.mf
new file mode 100644
index 0000000..ad49efe
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/manifest.mf
@@ -0,0 +1,10 @@
+Bundle-Name: Bundle Repository
+Bundle-SymbolicName: org.apache.osgi.bundle.bundlerepository
+Bundle-Description: A simple bundle repository for Felix.
+Bundle-Activator: org.apache.osgi.bundle.bundlerepository.Activator
+Bundle-ClassPath: .,org/apache/osgi/bundle/bundlerepository/kxml.jar
+Bundle-Version: 2.0.0.alpha2
+Import-Package: org.osgi.framework
+DynamicImport-Package: org.apache.osgi.service.shell
+Export-Package: 
+ org.apache.osgi.service.bundlerepository; specification-version="1.1.0"
diff --git a/src/org/apache/osgi/bundle/bundlerepository/metadataparser/ClassUtility.java b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/ClassUtility.java
new file mode 100644
index 0000000..fd9b6e9
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/ClassUtility.java
@@ -0,0 +1,97 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository.metadataparser;
+
+/**
+ * This class provides methods to process class name
+ */
+
+public class ClassUtility {
+
+	/**
+	 * This method capitalizes the first character in the provided string.
+	 * @return resulted string
+	 */
+	public static String capitalize(String name) {
+
+		int len=name.length();
+		StringBuffer sb=new StringBuffer(len);
+		boolean setCap=true;
+		for(int i=0; i<len; i++){
+			char c=name.charAt(i);
+			if(c=='-' || c=='_') {
+				setCap=true;			
+			} else {
+				if(setCap){
+					sb.append(Character.toUpperCase(c));
+					setCap=false;
+				} else {
+					sb.append(c);
+				}
+			}
+		} 
+ 
+		return sb.toString();
+	}
+
+	/**
+	 * This method capitalizes all characters in the provided string.
+	 * @return resulted string
+	 */
+	public static String finalstaticOf(String membername) {
+		int len=membername.length();
+		StringBuffer sb=new StringBuffer(len+2);
+		for(int i=0; i<len; i++){
+			char c=membername.charAt(i);
+			if(Character.isLowerCase(c) ) {
+				sb.append(Character.toUpperCase(c));
+			} else if(Character.isUpperCase(c) ) {
+				sb.append('_').append(c);
+			} else {
+				sb.append(c);				
+			}
+		} 
+ 
+		return sb.toString();
+	}
+	
+	/**
+	 * This method returns the package name in a full class name
+	 * @return resulted string
+	 */
+	public static String packageOf(String fullclassname) {
+		int index=fullclassname.lastIndexOf(".");
+		if(index>0) {
+			return fullclassname.substring(0,index);
+		} else {
+			return "";	
+		}
+	}
+
+	/**
+	 * This method returns the package name in a full class name
+	 * @return resulted string
+	 */
+	public static String classOf(String fullclassname) {
+		int index=fullclassname.lastIndexOf(".");
+		if(index>0) {
+			return fullclassname.substring(index+1);
+		} else {
+			return fullclassname;	
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/metadataparser/KXmlMetadataHandler.java b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/KXmlMetadataHandler.java
new file mode 100644
index 0000000..077a551
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/KXmlMetadataHandler.java
@@ -0,0 +1,63 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository.metadataparser;
+
+import java.io.*;
+
+import org.apache.osgi.bundle.bundlerepository.kxmlsax.KXmlSAXParser;
+
+
+/**
+ * handles the metadata in XML format
+ * (use kXML (http://kxml.enhydra.org/) a open-source very light weight XML parser
+ * @version 	1.00 11 Nov 2003
+ * @author 	Didier Donsez
+ */
+public class KXmlMetadataHandler /*implements MetadataHandler*/ {
+
+	private XmlCommonHandler handler;
+
+	public KXmlMetadataHandler() {
+		handler = new XmlCommonHandler();
+	}
+
+	/**
+	* Called to parse the InputStream and set bundle list and package hash map
+	*/
+	public void parse(InputStream is) throws Exception {
+		BufferedReader br = new BufferedReader(new InputStreamReader(is));
+		KXmlSAXParser parser;
+		parser = new KXmlSAXParser(br);
+		parser.parseXML(handler);
+	}
+
+	/**
+	 * return the metadata
+	 * @return a Objet
+	 */
+	public Object getMetadata() {
+		return handler.getRoot();
+	}
+
+	public void addType(String qname, Class clazz) {
+		handler.addType(qname, clazz);
+	}
+
+	public void setDefaultType(Class clazz) {
+		handler.setDefaultType(clazz);
+	}
+}
diff --git a/src/org/apache/osgi/bundle/bundlerepository/metadataparser/MultivalueMap.java b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/MultivalueMap.java
new file mode 100644
index 0000000..ba29423
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/MultivalueMap.java
@@ -0,0 +1,142 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository.metadataparser;
+
+import java.util.*;
+
+public class MultivalueMap implements Map {
+	private Map m_map = null;
+
+	public MultivalueMap() {
+		m_map = new HashMap();
+	}
+
+	public MultivalueMap(Map map) {
+		m_map = map;
+	}
+
+	/**
+	 * @see java.util.Map#size()
+	 */
+	public int size() {
+		return m_map.size();
+	}
+
+	/**
+	 * @see java.util.Map#clear()
+	 */
+	public void clear() {
+		m_map.clear();
+	}
+
+	/**
+	 * @see java.util.Map#isEmpty()
+	 */
+	public boolean isEmpty() {
+		return m_map.isEmpty();
+	}
+
+	/**
+	 * @see java.util.Map#containsKey(java.lang.Object)
+	 */
+	public boolean containsKey(Object arg0) {
+		return m_map.containsKey(arg0);
+	}
+
+	/**
+	 * @see java.util.Map#containsValue(java.lang.Object)
+	 */
+	public boolean containsValue(Object arg0) {
+		return false;
+	}
+
+	/**
+	 * @see java.util.Map#values()
+	 */
+	public Collection values() {
+		return null;
+	}
+
+	/**
+	 * @see java.util.Map#putAll(java.util.Map)
+	 */
+	public void putAll(Map arg0) {
+	}
+
+	/**
+	 * @see java.util.Map#entrySet()
+	 */
+	public Set entrySet() {
+		return m_map.entrySet();
+	}
+
+	/**
+	 * @see java.util.Map#keySet()
+	 */
+	public Set keySet() {
+		return m_map.keySet();
+	}
+
+	/**
+	 * @see java.util.Map#get(java.lang.Object)
+	 */
+	public Object get(Object key) {
+		return m_map.get(key);
+	}
+
+	/**
+	 * @see java.util.Map#remove(java.lang.Object)
+	 */
+	public Object remove(Object arg0) {
+		return m_map.remove(arg0);
+	}
+
+	/**
+	 * @see java.util.Map#put(java.lang.Object, java.lang.Object)
+	 */
+	public Object put(Object key, Object value) {
+		Object prev = m_map.get(key);
+		if (prev == null) {
+            List list = new ArrayList();
+            list.add(value);
+			m_map.put(key, list);
+			return list;
+		} else {
+            ((List) prev).add(value);
+            return prev;
+		}
+	}
+
+	public String toString() {
+		StringBuffer sb=new StringBuffer();
+		sb.append("[MultivalueMap:");
+		if(m_map.isEmpty()) {
+			sb.append("empty");
+		} else {
+			Set keys=m_map.keySet();
+			Iterator iter=keys.iterator();
+			while(iter.hasNext()){
+				String key=(String)iter.next();
+				sb.append("\n\"").append(key).append("\":");
+				sb.append(m_map.get(key).toString());		
+			}
+			sb.append('\n');
+		}
+		sb.append(']');
+		return sb.toString();
+	}
+}
\ No newline at end of file
diff --git a/src/org/apache/osgi/bundle/bundlerepository/metadataparser/XmlCommonHandler.java b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/XmlCommonHandler.java
new file mode 100644
index 0000000..628b4c6
--- /dev/null
+++ b/src/org/apache/osgi/bundle/bundlerepository/metadataparser/XmlCommonHandler.java
@@ -0,0 +1,405 @@
+/*
+ *   Copyright 2005 The Apache Software Foundation
+ *
+ *   Licensed 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.apache.osgi.bundle.bundlerepository.metadataparser;
+
+import java.lang.reflect.Method;
+import java.util.*;
+
+import org.apache.osgi.bundle.bundlerepository.kxmlsax.KXmlSAXHandler;
+import org.xml.sax.SAXException;
+
+/**
+ * SAX handler for the XML OBR file
+ *
+ * @author Didier Donsez (didier.donsez@imag.fr)
+ */
+public class XmlCommonHandler implements KXmlSAXHandler {
+
+    private static final String PI_MAPPING="mapping";
+
+    private int columnNumber;
+
+    private int lineNumber;
+
+    //
+    // Data
+    //
+
+    private Object root;
+
+    private Stack objectStack;
+    private Stack qnameStack;
+
+    private Map types;
+    private Class defaultType;
+
+    private StringBuffer currentText;
+
+    public XmlCommonHandler() {
+        objectStack = new Stack();
+        qnameStack = new Stack();
+        types = new HashMap();
+    }
+
+    public void addType(String qname, Class clazz) {
+        types.put(qname, clazz);
+    }
+
+    public void setDefaultType(Class clazz) {
+        defaultType=clazz;
+    }
+
+    public Object getRoot() {
+        return root;
+    }
+
+    /* for PCDATA */
+    public void characters(char[] ch, int offset, int length)
+        throws Exception {
+        if (currentText != null)
+            currentText.append(ch, offset, length);
+    }
+
+    private String adderOf(Class clazz) {
+        return "add"
+            + ClassUtility.capitalize(ClassUtility.classOf(clazz.getName()));
+    }
+
+    private String adderOf(String key) {
+        return "add" + ClassUtility.capitalize(key);
+    }
+
+    private String setterOf(Class clazz) {
+        return "set"
+            + ClassUtility.capitalize(ClassUtility.classOf(clazz.getName()));
+    }
+
+    private String setterOf(String key) {
+        return "set" + ClassUtility.capitalize(key);
+    }
+
+    /**
+    * Method called when a tag opens
+    *
+    * @param   uri
+    * @param   localName
+    * @param   qName
+    * @param   attrib
+    * @exception   SAXException
+    **/
+    public void startElement(
+        String uri,
+        String localName,
+        String qName,
+        Properties attrib)
+        throws Exception {
+
+        trace("START ("+lineNumber+","+columnNumber+"):" + uri + ":" + qName);
+
+        Class clazz = (Class) types.get(qName);
+        // TODO: should add uri in the future
+
+        if(clazz==null && defaultType!=null)
+            clazz=defaultType;
+
+        Object obj;
+        if (clazz != null) {
+
+            try {
+                obj = clazz.newInstance();
+            } catch (InstantiationException e) {
+                throw new Exception(lineNumber+","+columnNumber+":"+
+                    "class "+clazz.getName()+" for element " + qName + " should have an empty constructor");
+            } catch (IllegalAccessException e) {
+                throw new Exception(lineNumber+","+columnNumber+":"+
+                    "illegal access on the empty constructor of class "+clazz.getName()+" for element " + qName);
+            }
+
+            Set keyset = attrib.keySet();
+            Iterator iter = keyset.iterator();
+            while (iter.hasNext()) {
+                String key = (String) iter.next();
+
+                if (obj instanceof Map) {
+                    ((Map) obj).put(key, attrib.get(key));
+                } else if (obj instanceof List) {
+                    throw new Exception(lineNumber+","+columnNumber+":"+
+                        "List element " + qName + " cannot have any attribute");
+                } else if (obj instanceof String) {
+                    if(key.equals("value")){
+                        obj=(String)attrib.get(key);
+                    } else {
+                        throw new Exception(lineNumber+","+columnNumber+":"+
+                            "String element " + qName + " cannot have other attribute than value");
+                    }
+                } else {
+                    Method method = null;
+                    try {
+                        method =
+                            clazz.getMethod(
+                                setterOf(key),
+                                new Class[] { String.class });
+                    } catch (NoSuchMethodException e) {
+                        // do nothing
+                    }
+                    if (method == null)
+                        try {
+                            method =
+                                clazz.getMethod(
+                                    adderOf(key),
+                                    new Class[] { String.class });
+
+                        } catch (NoSuchMethodException e) {
+                            throw new Exception(lineNumber+","+columnNumber+":"+
+                                "element "
+                                    + qName
+                                    + " does not support the attribute "
+                                    + key);
+                        }
+                    if (method != null)
+                        method.invoke(
+                            obj,
+                            new String[] {(String) attrib.get(key)});
+                }
+
+            }
+
+        } else {
+            throw new Exception(lineNumber+","+columnNumber+":"+
+                "this element " + qName + " has not corresponding class");
+        }
+
+        if (root == null)
+            root = obj;
+        objectStack.push(obj);
+        qnameStack.push(qName);
+        currentText = new StringBuffer();
+
+        trace("START/ ("+lineNumber+","+columnNumber+"):" + uri + ":" + qName);
+    }
+
+    /**
+    * Method called when a tag closes
+    *
+    * @param   uri
+    * @param   localName
+    * @param   qName
+    * @exception   SAXException
+    */
+    public void endElement(
+        java.lang.String uri,
+        java.lang.String localName,
+        java.lang.String qName)
+        throws Exception {
+
+        trace("END ("+lineNumber+","+columnNumber+"):" + uri + ":" + qName);
+
+        Object obj = objectStack.pop();
+
+        if (currentText != null && currentText.length() != 0) {
+            if (obj instanceof Map) {
+                ((Map) obj).put(qName, currentText.toString().trim());
+            } else if (obj instanceof List) {
+                throw new Exception(lineNumber+","+columnNumber+":"+
+                    "List element " + qName + " cannot have PCDATAs");
+            } else if (obj instanceof String) {
+                String str=(String)obj;
+                if(str.length()!=0){
+                    throw new Exception(lineNumber+","+columnNumber+":"+
+                        "String element " + qName + " cannot have both PCDATA and an attribute value");
+                } else {
+                    obj=currentText.toString().trim();
+                }
+            } else {
+                Method method = null;
+                try {
+                    method =
+                        obj.getClass().getMethod(
+                            "addText",
+                            new Class[] { String.class });
+                } catch (NoSuchMethodException e) {
+                    // do nothing
+                }
+                if (method != null) {
+                    method.invoke(obj, new String[] { currentText.toString().trim()});
+                }
+            }
+        }
+
+        currentText = null;
+
+        if (!objectStack.isEmpty()) {
+
+            Object parent = objectStack.peek();
+            String parentName = (String) qnameStack.peek();
+
+            if (parent instanceof Map) {
+                ((Map) parent).put(qName, obj);
+            } else if (parent instanceof List) {
+                ((List) parent).add(obj);
+            } else {
+                Method method = null;
+                try {
+                    method =
+                        parent.getClass().getMethod(
+                            adderOf(ClassUtility.capitalize(qName)),
+                            new Class[] { obj.getClass()});
+                } catch (NoSuchMethodException e) {
+                    trace(
+                        "NoSuchMethodException: "
+                            + adderOf(ClassUtility.capitalize(qName)));
+                    // do nothing
+                }
+                if (method == null)
+                    try {
+                        method =
+                            parent.getClass().getMethod(
+                                setterOf(ClassUtility.capitalize(qName)),
+                                new Class[] { obj.getClass()});
+                    } catch (NoSuchMethodException e) {
+                        trace(
+                            "NoSuchMethodException: "
+                                + setterOf(ClassUtility.capitalize(qName)));
+                        // do nothing
+                    }
+                if (method == null)
+                    try {
+                        method =
+                            parent.getClass().getMethod(
+                                adderOf(obj.getClass()),
+                                new Class[] { obj.getClass()});
+                    } catch (NoSuchMethodException e) {
+                        trace(
+                            "NoSuchMethodException: "
+                                + adderOf(obj.getClass()));
+                        // do nothing
+                    }
+                if (method == null)
+                    try {
+                        method =
+                            parent.getClass().getMethod(
+                                setterOf(obj.getClass()),
+                                new Class[] { obj.getClass()});
+                    } catch (NoSuchMethodException e) {
+                        trace(
+                            "NoSuchMethodException: "
+                                + setterOf(obj.getClass()));
+                        // do nothing
+                    }
+
+                if (method != null) {
+                    trace(method.getName());
+                    method.invoke(parent, new Object[] { obj });
+                } else {
+                    throw new Exception(lineNumber+","+columnNumber+":"+
+                        " element " + parentName + " cannot have an attribute " + qName + " of type " + obj.getClass());
+                }
+            }
+
+        }
+
+        trace("END/ ("+lineNumber+","+columnNumber+"):" + uri + ":" + qName);
+
+    }
+
+    private void trace(String msg) {
+        if (false)
+            System.err.println(msg);
+    }
+
+    /**
+     * @see kxml.sax.KXmlSAXHandler#setLineNumber(int)
+     */
+    public void setLineNumber(int lineNumber) {
+        this.lineNumber=lineNumber;
+    }
+
+    /**
+     * @see kxml.sax.KXmlSAXHandler#setColumnNumber(int)
+     */
+    public void setColumnNumber(int columnNumber) {
+        this.columnNumber=columnNumber;
+
+    }
+
+    /**
+     * @see kxml.sax.KXmlSAXHandler#processingInstruction(java.lang.String, java.lang.String)
+     */
+    public void processingInstruction(String target, String data) throws Exception {
+        trace("pi:"+target+";"+data);
+        if(target==null){ // TODO kXML
+            if(!data.startsWith(PI_MAPPING)) return;
+        } else if(!target.equals(PI_MAPPING))return;
+
+
+        // defaultclass attribute
+        String datt="defaultclass=\"";
+        int dstart=data.indexOf(datt);
+        if(dstart!=-1) {
+            int dend=data.indexOf("\"",dstart+datt.length());
+            if(dend==-1)
+                throw new Exception(lineNumber+","+columnNumber+":"+
+                    " \"defaultclass\" attribute in \"mapping\" PI is not quoted");
+
+            String classname=data.substring(dstart+datt.length(),dend);
+            Class clazz=null;
+            try {
+                clazz=getClass().getClassLoader().loadClass(classname);
+            } catch (ClassNotFoundException e) {
+                throw new Exception(lineNumber+","+columnNumber+":"+
+                    " cannot found class "+ classname+" for \"mapping\" PI");
+            }
+            setDefaultType(clazz);
+            return;
+        }
+
+        // element attribute
+        String eatt="element=\"";
+        int estart=data.indexOf(eatt);
+        if(estart==-1)
+            throw new Exception(lineNumber+","+columnNumber+":"+
+                " missing \"element\" attribute in \"mapping\" PI");
+        int eend=data.indexOf("\"",estart+eatt.length());
+        if(eend==-1)
+        throw new Exception(lineNumber+","+columnNumber+":"+
+            " \"element\" attribute in \"mapping\" PI is not quoted");
+
+        String element=data.substring(estart+eatt.length(),eend);
+
+        // element class
+        String catt="class=\"";
+        int cstart=data.indexOf(catt);
+        if(cstart==-1)
+            throw new Exception(lineNumber+","+columnNumber+":"+
+                " missing \"class\" attribute in \"mapping\" PI");
+        int cend=data.indexOf("\"",cstart+catt.length());
+        if(cend==-1)
+        throw new Exception(lineNumber+","+columnNumber+":"+
+            " \"class\" attribute in \"mapping\" PI is not quoted");
+
+        String classname=data.substring(cstart+catt.length(),cend);
+
+        Class clazz=null;
+        try {
+            clazz=getClass().getClassLoader().loadClass(classname);
+        } catch (ClassNotFoundException e) {
+            throw new Exception(lineNumber+","+columnNumber+":"+
+                " cannot found class "+ classname+" for \"mapping\" PI");
+        }
+        addType(element,clazz);
+    }
+}