/**
 * Copyright (c) 2002-2007 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.common;


import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.common.util.ResourceLocator;
import org.eclipse.emf.common.util.URI;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;


/**
 * The <b>Plugin</b> for the model EMF.Common library.
 * EMF must run 
 * within an Eclipse workbench,
 * within a headless Eclipse workspace,
 * or just stand-alone as part of some other application.
 * To support this, all resource access should be directed to the resource locator,
 * which can redirect the service as appropriate to the runtime.
 * During stand-alone invocation no plugin initialization takes place.
 * In this case, common.resources.jar must be on the CLASSPATH.
 * @see #INSTANCE
 */
public final class CommonPlugin extends EMFPlugin 
{
  /**
   * The singleton instance of the plugin.
   */
  public static final CommonPlugin INSTANCE = new CommonPlugin();

  /**
   * The one instance of this class.
   */
  private static Implementation plugin;

  /**
   * Creates the singleton instance.
   */
  private CommonPlugin()
  {
    super(new ResourceLocator[] {});
  }

  @Override
  public ResourceLocator getPluginResourceLocator()
  {
    return plugin;
  }

  /**
   * Returns the singleton instance of the Eclipse plugin.
   * @return the singleton instance.
   */
  public static Implementation getPlugin()
  {
    return plugin;
  }

  /**
   * Use the platform, if available, to convert to a local URI.
   */
  public static URI asLocalURI(URI uri)
  {
    return plugin == null ? uri : Implementation.asLocalURI(uri);
  }

  /**
   * Use the platform, if available, to resolve the URI.
   */
  public static URI resolve(URI uri)
  {
    return plugin == null ? uri : Implementation.resolve(uri);
  }

  /**
   * Use the platform, if available, to load the named class using the right class loader.
   */
  public static Class<?> loadClass(String pluginID, String className) throws ClassNotFoundException
  {
    return plugin == null ? Class.forName(className) : Implementation.loadClass(pluginID, className);
  }

  /**
   * Computes a list of {@link ElementRecord element records} for each requested extension point.
   * Each key in the map is extension point ID. 
   * <p>
   * The corresponding value is a list of the extension point element records associated with that extension point.
   * </p>
   * <p>
   * Each root element record has synthetic attributes derived from the extension point:
   * </p>
   * <dl style="text-index: 1em;">
   *   <dt>{@code point}</dt>
   *   <dd>The extension point ID itself.</dd>
   *   <dt>{@code symbolicName}</dt>
   *   <dd>The symbolic name of the bundle that contains the extension point.</dd>
   *   <dt>{@code location}</dt>
   *   <dd>
   *   The root location URI the bundle containing the extension point.
   *   For example, {@code platform:/resource/org.example.plugin} for an extension point in the workspace, 
   *   {@code file:/folder/org.eclipse.plugin} for folder bundle in the target platform, 
   *   or {@code archive:file:/file/folder/org.example.plugin.jar!/} for a jarred bundle in the target platform.
   *   This is useful for resolving a relative path in attribute value to its absolute path in the bundle.
   *   </dd>
   * </dl>
   * <p>
   * This method uses the Plug-in Development Environment (PDE) to compute the results.
   * It will include results for plug-ins in the workspace as well as for plug-ins in the target platform.
   * If PDE is not available, this information cannot be computed.
   * </p>
   * 
   * @param extensionPoints a set of extension points to query; if it's {@code null} or empty, all extension points will be queried.
   * @return a map of extension point data, or {@code null} if PDE is not available.
   * @since 2.14
   */
  public static Map<String, List<ElementRecord>> getTargetPlatformExtensionPoints(Set<String> extensionPoints)
  {
    if (IS_ECLIPSE_RUNNING && PDEHelper.IS_PDE_BUNDLE_AVAILABLE)
    {
      return PDEHelper.computeModels(extensionPoints);
    }
    else
    {
      return null;
    }
  }

  /**
   * Computes a map from bundle symbolic name to the bundle's location URI.
   * <p>
   * This method uses the Plug-in Development Environment (PDE) to compute the results.
   * It will include results for plug-ins in the workspace as well as for plug-ins in the target platform.
   * If PDE is not available, this information cannot be computed.
   * </p>
   * 
   * @return a map from bundle symbolic name to the bundle's location URI, or {@code null} if PDE is not available.
   * @since 2.14
   */
  public static Map<String, URI> getTargetPlatformBundleMappings()
  {
    if (IS_ECLIPSE_RUNNING && PDEHelper.IS_PDE_BUNDLE_AVAILABLE)
    {
      return PDEHelper.computeTargetPlatformBundleMappings();
    }
    else
    {
      return null;
    }
  }

  private static final Method COLLATOR_GET_INSTANCE_METHOD;
  static
  {
    Method collatorGetInstanceMethod = null;
    try
    {
      Class<?> collatorClass = loadClass("com.ibm.icu", "com.ibm.icu.text.Collator");
      collatorGetInstanceMethod = collatorClass.getMethod("getInstance", Locale.class);
    }
    catch (Throwable throwable)
    {
      // Assume the class is not available.
    }
    COLLATOR_GET_INSTANCE_METHOD = collatorGetInstanceMethod;
  }

  /**
   * Returns a string comparator appropriate for collating strings for the {@link Locale#getDefault() current locale}.
   * @return a string comparator appropriate for collating strings for the {@link Locale#getDefault() current locale}.
   */
  public Comparator<String> getComparator()
  {
    return getComparator(Locale.getDefault());
  }

  /**
   * Returns a string comparator appropriate for collating strings for the give locale.
   * This will use ICU, when available that plug-in is available, or {@link Collator} otherwise.
   * @param locale the locale for which a comparator is needed.
   * @return a string comparator appropriate for collating strings for the give locale.
   */
  @SuppressWarnings("unchecked")
  public Comparator<String> getComparator(Locale locale)
  {
    if (COLLATOR_GET_INSTANCE_METHOD != null)
    {
      try
      {
        return (Comparator<String>)COLLATOR_GET_INSTANCE_METHOD.invoke(null, locale);
      }
      catch (Throwable eception)
      {
        // Just return the default.
      }
    }
    return (Comparator<String>)(Comparator<?>)Collator.getInstance(locale);
  }

  /**
   * The actual implementation of the Eclipse <b>Plugin</b>.
   */
  public static class Implementation extends EclipsePlugin 
  {
    /**
     * Creates an instance.
     */
    public Implementation()
    {
      super();

      // Remember the static instance.
      //
      plugin = this;
    }

    /**
     * Use the platform to convert to a local URI.
     */
    protected static URI asLocalURI(URI uri)
    {
      try
      {
        String fragment = uri.fragment();
        URL url = FileLocator.toFileURL(new URL(uri.trimFragment().toString()));
        return fix(url, fragment);
      }
      catch (IOException exception)
      {
        // Ignore the exception and return the original URI.
      }
      return uri;
    }

    /**
     * Use the platform to convert to a local URI.
     */
    protected static URI resolve(URI uri)
    {
      String fragment = uri.fragment();
      URI uriWithoutFragment = uri.trimFragment();
      String uriWithoutFragmentToString = uriWithoutFragment.toString();
      
      URL url = null;
      try
      {
        url = FileLocator.resolve(new URL(uriWithoutFragmentToString));
      }
      catch (IOException exception1)
      {
        // Platform.resolve() doesn't work if the project is encoded.
        //
        try
        {
          uriWithoutFragmentToString = URI.decode(uriWithoutFragmentToString);
          url = FileLocator.resolve(new URL(uriWithoutFragmentToString));
        }
        catch (IOException exception2)
        {
          // Continue with the unresolved URI.
        }
      }
      if (url != null)
      {
        try
        {
          return fix(url, fragment);
        }
        catch (IOException exception)
        {
          // Return the original URI.
        }
      }
      
      return uri;
    }

    protected static URI fix(URL url, String fragment) throws IOException
    {
      // Only file-scheme URIs will be re-encoded. If a URI was decoded in the workaround
      // above, and Platform.resolve() didn't return a file-scheme URI, then this will return
      // an decoded URI.
      //
      URI result = 
        "file".equalsIgnoreCase(url.getProtocol()) ?
          URI.createFileURI(URI.decode(url.getFile())) :
          URI.createURI(url.toString());
      if (fragment != null)
      {
        result = result.appendFragment(fragment);
      }
      return result;
    }
    
    /**
     * Use the platform to load the named class using the right class loader.
     */
    public static Class<?> loadClass(String pluginID, String className) throws ClassNotFoundException
    {
      Bundle bundle = Platform.getBundle(pluginID);
      if (bundle == null)
      {
        throw new ClassNotFoundException(className + " cannot be loaded because because bundle " + pluginID + " cannot be resolved");
      }
      else
      {
        return bundle.loadClass(className);
      }
    }

    /**
     * @since 2.10
     */
    public static class Activator extends EMFPlugin.OSGiDelegatingBundleActivator
    {
      @Override
      protected BundleActivator createBundle()
      {
        return new Implementation();
      }
    }
  }

  /**
   * A specialized {@link HashMap} map that supports {@link #getTargetPlatformValues(String,String) computing} information from the target platform, if the PDE is available.
   * It is abstract because the {@link #createKey(String)} method must be specialized to convert each attribute's string value to a value of the map's key type.
   * 
   * @param <K> the type of the key.
   * @param <V> the type of the value.
   *
   * @since 2.14
   */
  public static abstract class SimpleTargetPlatformRegistryImpl<K, V> extends HashMap<K, V>
  {
    private static final long serialVersionUID = 1L;

    /**
     * Creates an instance.
     */
    public SimpleTargetPlatformRegistryImpl()
    {
    }

    /**
     * Returns the set of values computed by {@link CommonPlugin#getTargetPlatformExtensionPoints(Set) fetching} all the given extension points 
     * and {@link ElementRecord#getAttributes() looking up} the given attribute name in each {@link ElementRecord#getChildren() element} of that extension point.
     * Each attribute value is {@link #createKey(String) converted} to a value of the map's key type.
     * @param extensionPoint the qualified extension point name.
     * @param attributeName the attribute name to query.
     * @return the values computed from the target platform, if the Plug-in Development Environment is available, or a copy of the {@link #keySet()} otherwise.
     * @see #createKey(String)
     */
    protected Set<K> getTargetPlatformValues(String extensionPoint, String attributeName)
    {
      Map<String, List<CommonPlugin.ElementRecord>> targetPlatformExtensionPoints = getTargetPlatformExtensionPoints(Collections.singleton(extensionPoint));
      if (targetPlatformExtensionPoints != null)
      {
        Set<K> result = new LinkedHashSet<K>();
        List<CommonPlugin.ElementRecord> extensionPointElementRecords = targetPlatformExtensionPoints.get(extensionPoint);
        if (extensionPointElementRecords != null)
        {
          for (CommonPlugin.ElementRecord extensionPointRecord : extensionPointElementRecords)
          {
            for (ElementRecord elementRecord : extensionPointRecord.getChildren())
            {
              String attribute = elementRecord.getAttributes().get(attributeName);
              if (attribute != null)
              {
                result.add(createKey(attribute));
              }
            }
          }
        }
        return result;
      }
      else
      {
        return new LinkedHashSet<K>(keySet());
      }
    }

    /**
     * Returns the attribute value converted to a value of the key type.
     * @param attribute the attribute value.
     * @return the attribute value converted to a value of the key type.
     */
    protected abstract K createKey(String attribute);
  }

  /**
   * A simple representation of an element in a {@code plugin.xml}.
   * It has the obvious things you'd expect of an element, i.e., a name, attributes, and children.
   *
   * @since 2.14
   */
  public static final class ElementRecord
  {
    /**
     * The element name.
     */
    private final String name;

    /**
     * The attributes of the element.
     */
    private final Map<String, String> attributes = new TreeMap<String, String>();

    /**
     * The children of the element.
     */
    private final List<ElementRecord> children = new ArrayList<ElementRecord>();

    /**
     * Creates an element with the given name.
     * 
     * @param name the name of the element.
     */
    private ElementRecord(String name)
    {
      this.name = name;
    }

    /**
     * Returns the name of the element.
     * @return the name of the element.
     */
    public String getName()
    {
      return name;
    }

    /**
     * Returns the attributes of the element.
     * @return the attributes of the element.
     */
    public Map<String, String> getAttributes()
    {
      return Collections.unmodifiableMap(attributes);
    }

    /**
     * Returns the children elements.
     * @return the children elements.
     */
    public List<ElementRecord> getChildren()
    {
      return Collections.unmodifiableList(children);
    }

    /**
     * A helpful override for debugging.
     */
    @Override
    public String toString()
    {
      return "" + name + "attributes= " + attributes + " children=" + children;
    }
  }

  private static class PDEHelper
  {
    private static final Method PLUGIN_MODEL_BASE_GET_BUNDLE_DESCRIPTION_METHOD;

    private static final Method PLUGIN_MODEL_BASE_GET_UNDERLYING_RESOURCE_METHOD;

    private static final Method RESOURCE_GET_PROJECT_METHOD;

    private static final Method RESOURCE_GET_FULL_PATH_METHOD;

    private static final Method PLUGIN_MODEL_BASE_GET_INSTALL_LOCATION_METHOD;

    private static final Method PLUGIN_MODEL_BASE_GET_EXTENSIONS_METHOD;

    private static final Method PLUGIN_REGISTRY_GET_ACTIVE_MODELS_METHOD;

    private static final Method BUNDLE_DESCRIPTION_GET_SYMBOLIC_NAME_METHOD;

    private static final Method EXTENSIONS_GET_EXTENSIONS_METHOD;

    private static final Method PLUGIN_EXTENSION_GET_POINT_METHOD;

    private static final Method PLUGIN_EXTENSION_GET_CHILDREN_METHOD;

    private static final Class<?> PLUGIN_ELEMENT_CLASS;

    private static final Method PLUGIN_ELEMENT_GET_ATTRIBUTES_METHOD;

    private static final Method PLUGIN_OBJECT_GET_NAME_METHOD;

    private static final Method PLUGIN_ATTRIBUTE_GET_VALUE_METHOD;

    private static final boolean IS_PDE_BUNDLE_AVAILABLE;

    static
    {
      Method pluginModelBaseGetBundleDescriptionMethod = null;
      Method pluginModelBaseGetUnderlyingResourceMethod = null;
      Method resourceGetProjectMethod = null;
      Method resourceGetFullPathtMethod = null;
      Method pluginModelBaseGetInstallLocationMethod = null;
      Method pluginModelBaseGetExtensionsMethod = null;
      Method pluginRegistryGetActiveModelsMethod = null;
      Method bundleDescriptionGetSymbolicNameMethod = null;
      Method extensionsGetExtensionsMethod = null;
      Method pluginExtensionGetPointMethod = null;
      Method pluginExtensionGetChildrenMethod = null;
      Class<?> pluginElementClass = null;
      Method pluginElementGetAttributesMethod = null;
      Method pluginObjectGetNameMethod = null;
      Method pluginAttributeGetValueMethod = null;
      boolean isPDEBundleAvailable = false;

      try
      {
        Class<?> pluginModelBaseClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.pde.core.plugin.IPluginModelBase");
        pluginModelBaseGetBundleDescriptionMethod = pluginModelBaseClass.getMethod("getBundleDescription");
        pluginModelBaseGetUnderlyingResourceMethod = pluginModelBaseClass.getMethod("getUnderlyingResource");
        resourceGetProjectMethod = pluginModelBaseGetUnderlyingResourceMethod.getReturnType().getMethod("getProject");
        resourceGetFullPathtMethod = resourceGetProjectMethod.getReturnType().getMethod("getFullPath");
        pluginModelBaseGetInstallLocationMethod = pluginModelBaseClass.getMethod("getInstallLocation");
        pluginModelBaseGetExtensionsMethod = pluginModelBaseClass.getMethod("getExtensions");
        Class<?> pluginRegistryClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.pde.core.plugin.PluginRegistry");
        pluginRegistryGetActiveModelsMethod = pluginRegistryClass.getMethod("getActiveModels", boolean.class);
        Class<?> bundleDescriptionClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.osgi.service.resolver.BundleDescription");
        bundleDescriptionGetSymbolicNameMethod = bundleDescriptionClass.getMethod("getSymbolicName");
        Class<?> extensionsClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.pde.core.plugin.IExtensions");
        extensionsGetExtensionsMethod = extensionsClass.getMethod("getExtensions");
        Class<?> pluginExtensionClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.pde.core.plugin.IPluginExtension");
        pluginExtensionGetPointMethod = pluginExtensionClass.getMethod("getPoint");
        pluginExtensionGetChildrenMethod = pluginExtensionClass.getMethod("getChildren");
        pluginElementClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.pde.core.plugin.IPluginElement");
        pluginObjectGetNameMethod = pluginElementClass.getMethod("getName");
        pluginElementGetAttributesMethod = pluginElementClass.getMethod("getAttributes");
        Class<?> pluginAttributeClass = CommonPlugin.loadClass("org.eclipse.pde.core", "org.eclipse.pde.core.plugin.IPluginAttribute");
        pluginAttributeGetValueMethod = pluginAttributeClass.getMethod("getValue");
        isPDEBundleAvailable = true;
      }
      catch (Throwable exception)
      {
        // Ignore.
      }

      PLUGIN_MODEL_BASE_GET_BUNDLE_DESCRIPTION_METHOD = pluginModelBaseGetBundleDescriptionMethod;
      PLUGIN_MODEL_BASE_GET_UNDERLYING_RESOURCE_METHOD = pluginModelBaseGetUnderlyingResourceMethod;
      RESOURCE_GET_PROJECT_METHOD = resourceGetProjectMethod;
      RESOURCE_GET_FULL_PATH_METHOD = resourceGetFullPathtMethod;
      PLUGIN_MODEL_BASE_GET_INSTALL_LOCATION_METHOD = pluginModelBaseGetInstallLocationMethod;
      PLUGIN_MODEL_BASE_GET_EXTENSIONS_METHOD = pluginModelBaseGetExtensionsMethod;
      PLUGIN_REGISTRY_GET_ACTIVE_MODELS_METHOD = pluginRegistryGetActiveModelsMethod;
      BUNDLE_DESCRIPTION_GET_SYMBOLIC_NAME_METHOD = bundleDescriptionGetSymbolicNameMethod;
      EXTENSIONS_GET_EXTENSIONS_METHOD = extensionsGetExtensionsMethod;
      PLUGIN_EXTENSION_GET_POINT_METHOD = pluginExtensionGetPointMethod;
      PLUGIN_EXTENSION_GET_CHILDREN_METHOD = pluginExtensionGetChildrenMethod;
      PLUGIN_ELEMENT_CLASS = pluginElementClass;
      PLUGIN_OBJECT_GET_NAME_METHOD = pluginObjectGetNameMethod;
      PLUGIN_ELEMENT_GET_ATTRIBUTES_METHOD = pluginElementGetAttributesMethod;
      PLUGIN_ATTRIBUTE_GET_VALUE_METHOD = pluginAttributeGetValueMethod;
      IS_PDE_BUNDLE_AVAILABLE = isPDEBundleAvailable && !"true".equals(System.getProperty("org.eclipse.emf.common.CommonPlugin.doNotUsePDE"));
    }

    @SuppressWarnings("unchecked")
    private static <T> T invoke(Object object, Method method, Object... arguments)
    {
      try
      {
        return (T)method.invoke(object, arguments);
      }
      catch (Exception exception)
      {
        return null;
      }
    }

    private static Map<String, URI> computeTargetPlatformBundleMappings()
    {
      Map<String, URI> result = new TreeMap<String, URI>();

      // Iterate over all the active models in the workspace and target platform.
      //
      // IPluginModelBase[] activeModels = PluginRegistry.getActiveModels(false);
      //
      Object[] activeModels = invoke(null, PLUGIN_REGISTRY_GET_ACTIVE_MODELS_METHOD, Boolean.FALSE);
      for (Object activeModel : activeModels)
      {
        // Determine the symbolic name, underlying resource, if any, and the install location.
        //
        // BundleDescription bundleDescription = activeModel.getBundleDescription();
        // String symbolicName = bundleDescription.getSymbolicName();
        // IResource underlyingResource = activeModel.getUnderlyingResource();
        // String installLocation = activeModel.getInstallLocation();
        //
        Object bundleDescription = invoke(activeModel, PLUGIN_MODEL_BASE_GET_BUNDLE_DESCRIPTION_METHOD);
        String symbolicName = (String)invoke(bundleDescription, BUNDLE_DESCRIPTION_GET_SYMBOLIC_NAME_METHOD);
        Object underlyingResource = invoke(activeModel, PLUGIN_MODEL_BASE_GET_UNDERLYING_RESOURCE_METHOD);
        String installLocation = (String)invoke(activeModel, PLUGIN_MODEL_BASE_GET_INSTALL_LOCATION_METHOD);

        // The URI for the location is determined from the underlying resource or the install location, with preference to the former if available.
        //
        URI location;
        if (underlyingResource != null)
        {
          // If there is an underlying resource, use the platform resource URI referencing the project in the workspace as the location.
          // underlyingResource.getProject()
          //
          Object project = invoke(underlyingResource, RESOURCE_GET_PROJECT_METHOD);
          IPath fullPath = invoke(project, RESOURCE_GET_FULL_PATH_METHOD);
          location = URI.createPlatformResourceURI(fullPath.toString(), true);
        }
        else if (installLocation != null)
        {
          // Otherwise, the install location in the file system is used...
          //
          File file = new File(installLocation);
          if (file.isDirectory())
          {
            // If the file is a directory, create a file URI for that directory.
            //
            location = URI.createFileURI(installLocation);
          }
          else
          {
            // Otherwise, the location must be an archive, create an archive URI for the file URI of the jar.
            //
            location = URI.createURI("archive:" + URI.createFileURI(installLocation) + "!/");
          }
        }
        else
        {
          location = null;
        }

        if (symbolicName != null && location != null)
        {
          result.put(symbolicName, location);
        }
      }

      return result;
    }

    private static Map<String, List<ElementRecord>> computeModels(Set<String> extensionPoints)
    {
      Map<String, List<ElementRecord>> result = new TreeMap<String, List<ElementRecord>>();

      // Iterate over all the active models in the workspace and target platform.
      //
      // IPluginModelBase[] activeModels = PluginRegistry.getActiveModels(false);
      //
      Object[] activeModels = invoke(null, PLUGIN_REGISTRY_GET_ACTIVE_MODELS_METHOD, Boolean.FALSE);
      for (Object activeModel : activeModels)
      {
        // Iterate over the plugin's extensions...
        //
        // IExtensions extensions = activeModel.getExtensions();
        // IPluginExtension[] pluginExtensions = extensions.getExtensions();
        //
        Object extensions = invoke(activeModel, PLUGIN_MODEL_BASE_GET_EXTENSIONS_METHOD);
        Object[] pluginExtensions = invoke(extensions, EXTENSIONS_GET_EXTENSIONS_METHOD);
        for (Object pluginExtension : pluginExtensions)
        {
          // String point = pluginExtension.getPoint();
          //
          String point = invoke(pluginExtension, PLUGIN_EXTENSION_GET_POINT_METHOD);

          // Process all or the specified extension pointers.
          //
          if (extensionPoints == null || extensionPoints.isEmpty() || extensionPoints.contains(point))
          {
            // Determine the symbolic name, underlying resource, if any, and the install location.
            //
            // BundleDescription bundleDescription = activeModel.getBundleDescription();
            // String symbolicName = bundleDescription.getSymbolicName();
            // IResource underlyingResource = activeModel.getUnderlyingResource();
            // String installLocation = activeModel.getInstallLocation();
            //
            Object bundleDescription = invoke(activeModel, PLUGIN_MODEL_BASE_GET_BUNDLE_DESCRIPTION_METHOD);
            String symbolicName = (String)invoke(bundleDescription, BUNDLE_DESCRIPTION_GET_SYMBOLIC_NAME_METHOD);
            Object underlyingResource = invoke(activeModel, PLUGIN_MODEL_BASE_GET_UNDERLYING_RESOURCE_METHOD);
            String installLocation = (String)invoke(activeModel, PLUGIN_MODEL_BASE_GET_INSTALL_LOCATION_METHOD);

            // The URI for the location is determined from the underlying resource or the install location, with preference to the former if available.
            //
            URI location;
            if (underlyingResource != null)
            {
              // If there is an underlying resource, use the platform resource URI referencing the project in the workspace as the location.
              // underlyingResource.getProject()
              //
              Object project = invoke(underlyingResource, RESOURCE_GET_PROJECT_METHOD);
              IPath fullPath = invoke(project, RESOURCE_GET_FULL_PATH_METHOD);
              location = URI.createPlatformResourceURI(fullPath.toString(), true);
            }
            else if (installLocation != null)
            {
              // Otherwise, the install location in the file system is used...
              //
              File file = new File(installLocation);
              if (file.isDirectory())
              {
                // If the file is a directory, create a file URI for that directory.
                //
                location = URI.createFileURI(installLocation);
              }
              else
              {
                // Otherwise, the location must be an archive, create an archive URI for the file URI of the jar.
                //
                location = URI.createURI("archive:" + URI.createFileURI(installLocation) + "!/");
              }
            }
            else
            {
              location = null;
            }

            List<ElementRecord> elementRecords = result.get(point);
            if (elementRecords == null)
            {
              elementRecords = new ArrayList<ElementRecord>();
              result.put(point, elementRecords);
            }

            ElementRecord elementRecord = visitElement(pluginExtension);
            if (location != null)
            {
              elementRecord.attributes.put("point", point);
              elementRecord.attributes.put("symbolicName", symbolicName);
              elementRecord.attributes.put("location", location.toString());
            }
            elementRecords.add(elementRecord);
          }
        }
      }

      // dump(result);
      return result;
    }

    private static void visitElement(Object[] children, List<ElementRecord> elementRecords)
    {
      // Visit the children.
      //
      for (Object child : children)
      {
        // if (child instanceof IPluginElement)
        //
        if (PLUGIN_ELEMENT_CLASS.isInstance(child))
        {
          elementRecords.add(visitElement(child));
        }
      }
    }

    private static ElementRecord visitElement(Object child)
    {
      ElementRecord elementRecord;
      if (PLUGIN_ELEMENT_CLASS.isInstance(child))
      {
        // child.getName();
        //
        String elementName = invoke(child, PLUGIN_OBJECT_GET_NAME_METHOD);

        // Record it by name.
        //
        elementRecord = new ElementRecord(elementName);

        // child.getAttributes()
        Object[] attributes = invoke(child, PLUGIN_ELEMENT_GET_ATTRIBUTES_METHOD);
        for (Object attribute : attributes)
        {
          // attribute.getName() and attribute.getValue()
          //
          String attributeName = invoke(attribute, PLUGIN_OBJECT_GET_NAME_METHOD);
          String attributeValue = invoke(attribute, PLUGIN_ATTRIBUTE_GET_VALUE_METHOD);

          // Record in the map.
          //
          elementRecord.attributes.put(attributeName, attributeValue);
        }
      }
      else
      {
        elementRecord = new ElementRecord("extension");
      }

      // Visit the child elements recursively.
      //
      Object[] elements = invoke(child, PLUGIN_EXTENSION_GET_CHILDREN_METHOD);
      if (elements.length != 0)
      {
        visitElement(elements, elementRecord.children);
      }

      return elementRecord;
    }

//    private static void dump(Map<String, List<ElementRecord>> data)
//    {
//      for (Map.Entry<String, List<ElementRecord>> entry : data.entrySet())
//      {
//        System.out.println(entry.getKey());
//        dump(entry.getValue(), "  ");
//      }
//    }
//
//    private static void dump(List<ElementRecord> data, String indent)
//    {
//      for (ElementRecord elementRecord : data)
//      {
//        System.out.print(indent + "<" + elementRecord.getName());
//        for (Map.Entry<String, String> attribute : elementRecord.getAttributes().entrySet())
//        {
//          System.out.println();
//          System.out.print(indent + "    " + attribute.getKey() + "='" + attribute.getValue() + "'");
//        }
//        List<ElementRecord> children = elementRecord.getChildren();
//        if (children.isEmpty())
//        {
//        System.out.println("/>");
//        }
//        else
//        {
//        System.out.println(">");
//          dump(children, indent + "  ");
//        System.out.print(indent + "</" + elementRecord.getName() + ">");
//        }
//      }
//    }

  }
}
