/******************************************************************************
 * Copyright (c) 2010, 2023 Oracle 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
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *    Konstantin Komissarchik - initial implementation and ongoing maintenance
 ******************************************************************************/

package org.eclipse.wst.common.project.facet.core.internal;

import static org.eclipse.wst.common.project.facet.core.util.internal.FileUtil.FILE_DOT_PROJECT;
import static org.eclipse.wst.common.project.facet.core.util.internal.FileUtil.validateEdit;
import static org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.findOptionalElement;
import static org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.findRequiredAttribute;
import static org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.getElementValue;
import static org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.instantiate;
import static org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.reportMissingAttribute;
import static org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.reportMissingElement;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.common.project.facet.core.IActionDefinition;
import org.eclipse.wst.common.project.facet.core.ICategory;
import org.eclipse.wst.common.project.facet.core.IConstraint;
import org.eclipse.wst.common.project.facet.core.IDefaultVersionProvider;
import org.eclipse.wst.common.project.facet.core.IFacetedProject;
import org.eclipse.wst.common.project.facet.core.IFacetedProject.Action;
import org.eclipse.wst.common.project.facet.core.IFacetedProjectTemplate;
import org.eclipse.wst.common.project.facet.core.IFacetedProjectWorkingCopy;
import org.eclipse.wst.common.project.facet.core.IGroup;
import org.eclipse.wst.common.project.facet.core.IPreset;
import org.eclipse.wst.common.project.facet.core.IProjectFacet;
import org.eclipse.wst.common.project.facet.core.IProjectFacetVersion;
import org.eclipse.wst.common.project.facet.core.IVersionExpr;
import org.eclipse.wst.common.project.facet.core.ProjectFacetsManager;
import org.eclipse.wst.common.project.facet.core.events.IFacetedProjectEvent;
import org.eclipse.wst.common.project.facet.core.events.IFacetedProjectFrameworkEvent;
import org.eclipse.wst.common.project.facet.core.events.IFacetedProjectFrameworkListener;
import org.eclipse.wst.common.project.facet.core.events.IFacetedProjectListener;
import org.eclipse.wst.common.project.facet.core.events.internal.EventsExtensionPoint;
import org.eclipse.wst.common.project.facet.core.events.internal.FacetedProjectFrameworkEvent;
import org.eclipse.wst.common.project.facet.core.events.internal.FrameworkListenerRegistry;
import org.eclipse.wst.common.project.facet.core.events.internal.LegacyEventHandlerAdapter;
import org.eclipse.wst.common.project.facet.core.events.internal.ProjectListenerRegistry;
import org.eclipse.wst.common.project.facet.core.util.internal.IndexedSet;
import org.eclipse.wst.common.project.facet.core.util.internal.PluginUtil.InvalidExtensionException;
import org.eclipse.wst.common.project.facet.core.util.internal.VersionExpr;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

/**
 * The implementation of the {@link ProjectFacetsManager} abstract class.
 * 
 * @author <a href="mailto:konstantin.komissarchik@oracle.com">Konstantin Komissarchik</a>
 */

public final class FacetedProjectFrameworkImpl
{
    private static final String EXTENSION_ID = "facets"; //$NON-NLS-1$

    private static final String ATTR_CATEGORY = "category"; //$NON-NLS-1$
    private static final String ATTR_CLASS = "class"; //$NON-NLS-1$
    private static final String ATTR_FACET = "facet"; //$NON-NLS-1$
    private static final String ATTR_GROUP = "group"; //$NON-NLS-1$
    private static final String ATTR_ID = "id"; //$NON-NLS-1$
    private static final String ATTR_NAME = "name"; //$NON-NLS-1$
    private static final String ATTR_PROVIDER = "provider"; //$NON-NLS-1$
    private static final String ATTR_SOFT = "soft"; //$NON-NLS-1$
    private static final String ATTR_TYPE = "type"; //$NON-NLS-1$
    private static final String ATTR_VALUE = "value"; //$NON-NLS-1$
    private static final String ATTR_VERSION = "version"; //$NON-NLS-1$
    private static final String EL_ACTION = "action"; //$NON-NLS-1$
    private static final String EL_CATEGORY = "category"; //$NON-NLS-1$
    private static final String EL_CONFIG_FACTORY = "config-factory"; //$NON-NLS-1$
    private static final String EL_CONSTRAINT = "constraint"; //$NON-NLS-1$
    private static final String EL_DEFAULT_VERSION = "default-version"; //$NON-NLS-1$
    private static final String EL_DELEGATE = "delegate"; //$NON-NLS-1$
    private static final String EL_DESCRIPTION = "description"; //$NON-NLS-1$
    private static final String EL_EVENT_HANDLER = "event-handler"; //$NON-NLS-1$
    private static final String EL_LABEL = "label"; //$NON-NLS-1$
    private static final String EL_MEMBER = "member"; //$NON-NLS-1$
    private static final String EL_PROJECT_FACET = "project-facet"; //$NON-NLS-1$
    private static final String EL_PROJECT_FACET_VERSION = "project-facet-version"; //$NON-NLS-1$
    private static final String EL_PROPERTY = "property"; //$NON-NLS-1$
    private static final String EL_VERSION_COMPARATOR = "version-comparator"; //$NON-NLS-1$
    
    private static final String DEFAULT_DESCRIPTION = ""; //$NON-NLS-1$
    
    private static FacetedProjectFrameworkImpl instance = null;
    
    private final IndexedSet<String,IProjectFacet> facets;
    private final IndexedSet<String,IActionDefinition> actions;
    private final IndexedSet<String,ICategory> categories;
    private final IndexedSet<String,IPreset> presets;
    private boolean presetsInitialized = false;
    private final IndexedSet<String,IGroup> groups;
    private final Map<String,FacetedProject> projects;
    private final ProjectListenerRegistry projectListenerRegistry;
    private final FrameworkListenerRegistry frameworkListenerRegistry;
    
    private WeakReference<ProjectFacetPreferencesGroup> globalPreferencesGroup = null;
    
    private Map<String,WeakReference<ProjectFacetPreferencesGroup>> projectPreferencesGroups 
        = new HashMap<String,WeakReference<ProjectFacetPreferencesGroup>>();
    
    private FacetedProjectFrameworkImpl()
    {
        long activationStart = 0;
        
        if( FacetCorePlugin.isTracingFrameworkActivation() )
        {
            final StringWriter sw = new StringWriter();
            final PrintWriter pw = new PrintWriter( sw );
            ( new Throwable() ).printStackTrace( pw );
            
            String context = sw.getBuffer().toString();
            final int endOfFirstLine = context.indexOf( '\n' );
            context = context.substring( endOfFirstLine + 1 );
            context = context.replaceAll( "\\t", "  " ); //$NON-NLS-1$ //$NON-NLS-2$
            
            int length = context.length();
            
            if( context.charAt( length - 2 ) == '\r' )
            {
                length = length - 2;
            }
            else
            {
                length = length - 1;
            }
            
            context = context.substring( 0, length );
            
            final String msg 
                = NLS.bind( Resources.tracingFrameworkActivationStarting, 
                            context ); 
            
            System.out.println( msg );
            
            activationStart = System.currentTimeMillis();
        }
        
        this.facets = new IndexedSet<String,IProjectFacet>();
        this.actions = new IndexedSet<String,IActionDefinition>();
        this.categories = new IndexedSet<String,ICategory>();
        this.presets = new IndexedSet<String,IPreset>();
        this.presetsInitialized = false;
        this.groups = new IndexedSet<String,IGroup>();
        this.projects = new HashMap<String,FacetedProject>();
        this.projectListenerRegistry = new ProjectListenerRegistry();
        this.frameworkListenerRegistry = new FrameworkListenerRegistry();
        
        readMetadata();
        
        EventsExtensionPoint.processExtensions( this );
        
        ( new ResourceChangeListener() ).register();
        
        if( FacetCorePlugin.isTracingFrameworkActivation() )
        {
            final long duration 
                = System.currentTimeMillis() - activationStart;
            
            final String msg
                = NLS.bind( Resources.tracingFrameworkActivationFinished, 
                            String.valueOf( duration ) );
            
            System.out.println( msg );
        }
    }
    
    public synchronized static FacetedProjectFrameworkImpl getInstance()
    {
        if( instance == null )
        {
            instance = new FacetedProjectFrameworkImpl();
        }
        
        return instance;
    }
    
    public Set<IProjectFacet> getProjectFacets()
    {
        return this.facets.getItemSet();
    }
    
    public boolean isProjectFacetDefined( final String id )
    {
        return this.facets.containsKey( id );
    }
    
    public IProjectFacet getProjectFacet( final String id )
    {
        final IProjectFacet f = this.facets.getItemByKey( id );
        
        if( f == null )
        {
            final String msg = NLS.bind( Resources.facetNotDefined, id );
            throw new IllegalArgumentException( msg );
        }
        
        return f;
    }
    
    public Set<IActionDefinition> getActionDefinitions()
    {
        return this.actions.getItemSet();
    }
    
    public boolean isActionDefined( final String id )
    {
        return this.actions.containsKey( id );
    }
    
    public IActionDefinition getActionDefinition( final String id )
    {
        final IActionDefinition adef = this.actions.getItemByKey( id );
        
        if( adef == null )
        {
            final String msg = NLS.bind( Resources.actionNotDefined, id );
            throw new IllegalArgumentException( msg );
        }
        
        return adef;
    }
    
    public Set<ICategory> getCategories()
    {
        return this.categories.getItemSet();
    }

    public boolean isCategoryDefined( final String id )
    {
        return this.categories.containsKey( id );
    }
    
    public ICategory getCategory( final String id )
    {
        final ICategory category = this.categories.getItemByKey( id );
        
        if( category == null )
        {
            final String msg = NLS.bind( Resources.categoryNotDefined, id );
            throw new IllegalArgumentException( msg );
        }
        
        return category;
    }
    
    public Set<IPreset> getPresets()
    {
        synchronized( this.presets )
        {
            initializePresets();
            return this.presets.getItemSet();
        }
    }
    
    public boolean isPresetDefined( final String id )
    {
        synchronized( this.presets )
        {
            initializePresets();
            return this.presets.containsKey( id );
        }
    }
    
    public IPreset getPreset( final String id )
    {
        synchronized( this.presets )
        {
            initializePresets();
            
            final IPreset preset = this.presets.getItemByKey( id );
            
            if( preset == null )
            {
                final String msg = NLS.bind( Resources.presetNotDefined, id );
                throw new IllegalArgumentException( msg );
            }
            
            return preset;
        }
    }
    
    public IPreset definePreset( final String name,
                                 final Set<IProjectFacetVersion> facets )
    {
        return definePreset( name, null, facets );
    }

    public IPreset definePreset( final String name,
                                 final String description,
                                 final Set<IProjectFacetVersion> facets )
    {
       initializePresets();
       
       final IPreset preset = definePreset( name, description, facets, true );

       final IFacetedProjectFrameworkEvent event 
           = new FacetedProjectFrameworkEvent( IFacetedProjectFrameworkEvent.Type.PRESET_ADDED );
       
       this.frameworkListenerRegistry.notifyListeners( event );
       
       return preset;
    }
    
    private IPreset definePreset( final String name,
                                  final String description,
                                  final Set<IProjectFacetVersion> facets,
                                  final boolean save )
    {
        synchronized( this.presets )
        {
            String id;
            int i = 0;
            
            do
            {
                id = ".usr." + i; //$NON-NLS-1$
                i++;
            }
            while( this.presets.containsKey( id ) );
            
            final UserPreset preset 
                = new UserPreset( id, name, description == null ? "" : description,  //$NON-NLS-1$
                                  facets );
            
            this.presets.addItemWithKey( id, preset );
            
            if( save )
            {
                saveUserPresets();
            }
            
            return preset;
        }
    }
    
    public boolean deletePreset( final IPreset preset )
    {
        boolean deleted;
        
        synchronized( this.presets )
        {
            initializePresets();
            
            if( preset.getType() != IPreset.Type.USER_DEFINED )
            {
                return false;
            }
            
            deleted = this.presets.removeItemByKey( preset.getId() );
            
            if( deleted )
            {
                saveUserPresets();
            }
        }

        if( deleted )
        {
            final IFacetedProjectFrameworkEvent event 
                = new FacetedProjectFrameworkEvent( IFacetedProjectFrameworkEvent.Type.PRESET_REMOVED );
            
            this.frameworkListenerRegistry.notifyListeners( event );
        }
        
        return deleted;
    }
    
    private void initializePresets()
    {
        synchronized( this.presets )
        {
            if( ! this.presetsInitialized )
            {
                for( IPreset preset : PresetsExtensionPoint.getPresets() )
                {
                    this.presets.addItemWithKey( preset.getId(), preset );
                }
                
                readUserPresets();
                
                this.presetsInitialized = true;
            }
        }
    }
    
    private void saveUserPresets()
    {
        try
        {
            final Preferences root = getUserPresetsPreferences();
            
            final String[] children = root.childrenNames();
            
            for( int i = 0; i < children.length; i++ )
            {
                root.node( children[ i ] ).removeNode();
            }
            
            for( IPreset preset : this.presets.getItemSet() )
            {
                if( preset.getType() == IPreset.Type.USER_DEFINED )
                {
                    final Preferences pnode = root.node( preset.getId() );
                    pnode.put( EL_LABEL, preset.getLabel() );
                    pnode.put( EL_DESCRIPTION, preset.getDescription() );
                    
                    int counter = 1;
                    
                    for( IProjectFacetVersion fv : preset.getProjectFacets() )
                    {
                        final Preferences fnode = pnode.node( String.valueOf( counter ) );
                        
                        fnode.put( ATTR_ID, fv.getProjectFacet().getId() );
                        fnode.put( ATTR_VERSION, fv.getVersionString() );
                        
                        counter++;
                    }
                }
            }
        
            root.flush();
        }
        catch( BackingStoreException e )
        {
            FacetCorePlugin.log( e );
        }
    }
    
    private void readUserPresets()
    {
        try
        {
            final Preferences root = getUserPresetsPreferences();
            final String[] pkeys = root.childrenNames();
            
            for( int i = 0; i < pkeys.length; i++ )
            {
                final Preferences pnode = root.node( pkeys[ i ] );
                final String label = pnode.get( EL_LABEL, null );
                
                if( label == null )
                {
                    break;
                }

                String description = pnode.get( EL_DESCRIPTION, null );
                
                if( description == null )
                {
                    description = ""; //$NON-NLS-1$
                }
                
                final String[] fkeys = pnode.childrenNames();
                Set<IProjectFacetVersion> facets = new HashSet<IProjectFacetVersion>();
                
                for( int j = 0; j < fkeys.length; j++ )
                {
                    final Preferences fnode = pnode.node( fkeys[ j ] );
                    final String id = fnode.get( ATTR_ID, null );
                    final String version = fnode.get( ATTR_VERSION, null );
                    
                    if( id == null || version == null )
                    {
                        facets = null;
                        break;
                    }
                    
                    if( isProjectFacetDefined( id ) )
                    {
                        final IProjectFacet f = getProjectFacet( id );
                        
                        if( f.hasVersion( version ) )
                        {
                            facets.add( f.getVersion( version ) );
                        }
                        else
                        {
                            facets = null;
                            break;
                        }
                    }
                    else
                    {
                        facets = null;
                        break;
                    }
                }

                if( facets != null )
                {
                    definePreset( label, description, facets, false );
                }
            }
        }
        catch( BackingStoreException e )
        {
            FacetCorePlugin.log( e );
        }
    }
    
    private static Preferences getUserPresetsPreferences()
    {
        final InstanceScope scope = new InstanceScope();
        
        final IEclipsePreferences pluginRoot 
            = scope.getNode( FacetCorePlugin.PLUGIN_ID );
        
        return pluginRoot.node( "user.presets" ); //$NON-NLS-1$
    }
    
    public Set<IFacetedProjectTemplate> getTemplates()
    {
        return FacetedProjectTemplatesExtensionPoint.getTemplates();
    }
    
    public boolean isTemplateDefined( final String id )
    {
        return ( FacetedProjectTemplatesExtensionPoint.getTemplate( id ) != null );
    }
    
    public IFacetedProjectTemplate getTemplate( final String id )
    {
        final IFacetedProjectTemplate template 
            = FacetedProjectTemplatesExtensionPoint.getTemplate( id );
        
        if( template == null )
        {
            final String msg = NLS.bind( Resources.templateNotDefined, id );
            throw new IllegalArgumentException( msg );
        }
        
        return template;
    }
    
    public Set<IGroup> getGroups()
    {
        return this.groups.getItemSet();
    }
    
    public boolean isGroupDefined( final String id )
    {
        return this.groups.containsKey( id );
    }
    
    public IGroup getGroup( final String id )
    {
        final IGroup group = this.groups.getItemByKey( id );
        
        if( group == null )
        {
            final String msg = NLS.bind( Resources.groupNotDefined, id );
            throw new IllegalArgumentException( msg );
        }
        
        return group;
    }
    
    void addGroup( final IGroup group )
    {
        this.groups.addItemWithKey( group.getId(), group );
    }
    
    public Set<IFacetedProject> getFacetedProjects()
    
        throws CoreException
        
    {
        return getFacetedProjects( null, null );
    }

    public Set<IFacetedProject> getFacetedProjects( final IProjectFacet f )
    
        throws CoreException
        
    {
        return getFacetedProjects( f, null );
    }

    public Set<IFacetedProject> getFacetedProjects( final IProjectFacetVersion fv )
    
        throws CoreException
        
    {
        return getFacetedProjects( null, fv );
    }

    private Set<IFacetedProject> getFacetedProjects( final IProjectFacet f,
                                                     final IProjectFacetVersion fv )
    
        throws CoreException
        
    {
        final IProject[] all 
            = ResourcesPlugin.getWorkspace().getRoot().getProjects();
        
        final Set<IFacetedProject> result = new HashSet<IFacetedProject>();
        
        for( int i = 0; i < all.length; i++ )
        {
            final IProject proj = all[ i ];
            final IFacetedProject fproj = create( proj );
            
            if( fproj != null )
            {
                if( ( f != null && ! fproj.hasProjectFacet( f ) ) ||
                    ( fv != null && ! fproj.hasProjectFacet( fv ) ) )
                {
                    continue;
                }
                else
                {
                    result.add( fproj );
                }
            }
        }
        
        return result;
    }
    
    public IFacetedProjectWorkingCopy createNewProject()
    {
        return new FacetedProjectWorkingCopy( null );
    }

    public IFacetedProject create( final IProject project )
    
        throws CoreException
        
    {
        if( project.isAccessible() &&
            project.isNatureEnabled( FacetedProjectNature.NATURE_ID ) )
        {
            FacetedProject fproj = null;
            
            synchronized( this.projects )
            {
                fproj = this.projects.get( project.getName() );
                
                if( fproj == null )
                {
                    fproj = new FacetedProject( project );
                    this.projects.put( project.getName(), fproj );
                }
            }

            fproj.refresh();
            
            return fproj;
        }

        return null;
    }

    public IFacetedProject create( final IProject project,
                                   final boolean convertIfNecessary,
                                   final IProgressMonitor monitor)
    
        throws CoreException
        
    {
        if( monitor != null )
        {
            monitor.beginTask( "", 1 ); //$NON-NLS-1$
        }
        
        try
        {
            if( project.isAccessible() &&
                ! project.isNatureEnabled( FacetedProjectNature.NATURE_ID ) && 
                convertIfNecessary )
            {
                final IProjectDescription description = project.getDescription();
                final String[] prevNatures = description.getNatureIds();
                final String[] newNatures = new String[ prevNatures.length + 1 ];
                System.arraycopy( prevNatures, 0, newNatures, 0, prevNatures.length );
                newNatures[ prevNatures.length ] = FacetedProjectNature.NATURE_ID;
                description.setNatureIds( newNatures );
                
                validateEdit( project.getFile( FILE_DOT_PROJECT ) );
                
                project.setDescription( description, submon( monitor, 1 ) );
            }
            
            return create( project );
        }
        finally
        {
            if( monitor != null )
            {
                monitor.done();
            }
        }
    }
    
    public IFacetedProject create( final String name,
                                   final IPath location,
                                   final IProgressMonitor monitor )
    
        throws CoreException
        
    {
        if( monitor != null )
        {
            monitor.beginTask( "", 2 ); //$NON-NLS-1$
        }
        
        try
        {
            final IWorkspace ws = ResourcesPlugin.getWorkspace();
            final IProject project = ws.getRoot().getProject( name );
            
            final IProjectDescription desc
                = ws.newProjectDescription( name );
    
            desc.setLocation( location );
                    
            project.create( desc, submon( monitor, 1 ) );
            project.open( submon( monitor, 1 ) );
            
            // This is odd, but apparently nature's configure() method will only
            // be called if the setDescription() method is used. It will not be
            // called if nature is added to the project description prior to
            // calling IProject.create() method.
            
            desc.setNatureIds( new String[] { FacetedProjectNature.NATURE_ID } );
            project.setDescription( desc, null );
            
            return create( project );
        }
        finally
        {
            if( monitor != null )
            {
                monitor.done();
            }
        }
    }
    
    public void addListener( final IFacetedProjectListener listener,
                             final IFacetedProjectEvent.Type... types )
    {
        this.projectListenerRegistry.addListener( listener, types );
    }
    
    public void addListener( final IFacetedProjectFrameworkListener listener,
                             final IFacetedProjectFrameworkEvent.Type... types )
    {
        this.frameworkListenerRegistry.addListener( listener, types );
    }
    
    public void removeListener( final IFacetedProjectListener listener )
    {
        this.projectListenerRegistry.removeListener( listener );
    }
    
    public void removeListener( final IFacetedProjectFrameworkListener listener )
    {
        this.frameworkListenerRegistry.removeListener( listener );
    }
    
    ProjectListenerRegistry getProjectListenerRegistry()
    {
        return this.projectListenerRegistry;
    }
    
    public IStatus check( final Set<IProjectFacetVersion> base,
                          final Set<Action> actions )
    {
        MultiStatus result = Constraint.createMultiStatus();
        
        // Verify that all of the actions are supported.
        
        for( Action action : actions )
        {
            if( ! action.getProjectFacetVersion().supports( base, action.getType() ) )
            {
                final ValidationProblem.Type ptype;
                
                if( action.getType() == Action.Type.INSTALL )
                {
                    ptype = ValidationProblem.Type.INSTALL_NOT_SUPPORTED;
                }
                else if( action.getType() == Action.Type.UNINSTALL )
                {
                    ptype = ValidationProblem.Type.UNINSTALL_NOT_SUPPORTED;
                }
                else if( action.getType() == Action.Type.VERSION_CHANGE )
                {
                    ptype = ValidationProblem.Type.VERSION_CHANGE_NOT_SUPPORTED;
                }
                else
                {
                    throw new IllegalStateException();
                }
                
                final IProjectFacetVersion fv = action.getProjectFacetVersion();
                
                final ValidationProblem vp
                    = new ValidationProblem( ptype, 
                                             fv.getProjectFacet().getLabel(),
                                             fv.getVersionString() );
                
                result.add( vp );
            }
        }
        
        // Multiple actions on the same facet are not supported in the same
        // batch. The only exception is an uninstall of a previosly-installed
        // version followed by an install of a new version.
        
        final Map<IProjectFacet,Set<Action>> facetToActionsMap 
            = new HashMap<IProjectFacet,Set<Action>>();
        
        for( Action action : actions )
        {
            final IProjectFacet f = action.getProjectFacetVersion().getProjectFacet();
            Set<Action> group = facetToActionsMap.get( f );
            
            if( group == null )
            {
                group = new HashSet<Action>();
                facetToActionsMap.put( f, group );
            }
            
            group.add( action );
        }
        
        for( Set<Action> group : facetToActionsMap.values() )
        {
            if( group.size() > 1 )
            {
                boolean bad = true;
                
                if( group.size() == 2 )
                {
                    Action install = null;
                    Action uninstall = null;
                    
                    for( Action action : group )
                    {
                        if( action.getType() == Action.Type.INSTALL )
                        {
                            install = action;
                        }
                        else if( action.getType() == Action.Type.UNINSTALL )
                        {
                            uninstall = action;
                        }
                        else
                        {
                            break;
                        }
                    }
                    
                    if( install != null && uninstall != null )
                    {
                        if( base.contains( uninstall.getProjectFacetVersion() ) )
                        {
                            bad = false;
                        }
                    }
                }
                
                if( bad )
                {
                    final ValidationProblem.Type ptype 
                        = ValidationProblem.Type.MULTIPLE_ACTIONS_NOT_SUPPORTED;
                    
                    result.add( new ValidationProblem( ptype ) );
                    
                    break;
                }
            }
        }
        
        // Check for attempts to uninstall or change version of facets that
        // haven't been installed. Also check for attempts to install a facet
        // that's already installed.
        
        for( Action action : actions )
        {
            final IProjectFacetVersion fv = action.getProjectFacetVersion();
            final IProjectFacet f = fv.getProjectFacet();

            ValidationProblem.Type ptype = null;
            
            if( action.getType() == Action.Type.UNINSTALL )
            {
                if( ! base.contains( fv ) )
                {
                    ptype = ValidationProblem.Type.CANNOT_UNINSTALL;
                }
            }
            else
            {
                IProjectFacetVersion existing = null;
                
                for( IProjectFacetVersion temp : base )
                {
                    if( temp.getProjectFacet() == f )
                    {
                        existing = temp;
                        break;
                    }
                }
                
                if( action.getType() == Action.Type.VERSION_CHANGE && existing == null )
                {
                    ptype = ValidationProblem.Type.CANNOT_CHANGE_VERSION;
                }
                else if( action.getType() == Action.Type.INSTALL && existing != null )
                {
                    // XXX this information is nearly useless to the end user
//					FacetCorePlugin.log(new UnsupportedOperationException("Attempting to install a second facet version for an already installed facet: " + action.getProjectFacetVersion() + "[" + existing + "]")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
//                    ptype = ValidationProblem.Type.FACET_ALREADY_INSTALLED;
                }
            }
            
            if( ptype != null )
            {
                result.add( new ValidationProblem( ptype, f.getLabel(),
                                                   fv.getVersionString() ) );
            }
        }
        
        // Abort at this point if there are any validation problems.
        
        if( ! result.isOK() )
        {
            return result;
        }
        
        // Apply all the uninstall actions.
        
        final Set<IProjectFacetVersion> all = new HashSet<IProjectFacetVersion>( base );
        
        for( Action action : actions )
        {
            if( action.getType() == Action.Type.UNINSTALL )
            {
                apply( all, action );
            }
        }
        
        // Apply all the install and version change actions.
        
        for( Action action : actions )
        {
            if( action.getType() != Action.Type.UNINSTALL )
            {
                apply( all, action );
            }
        }
        
        // Check the constrains on all of the facets.
        
        for( IProjectFacetVersion fv : all )
        {
            final IConstraint constraint = fv.getConstraint();
            
            if( constraint != null )
            {
                final IStatus st = constraint.check( all );
                
                if( ! st.isOK() )
                {
                    result.addAll( st );
                }
            }
        }
        
        // Eliminate symmetric conflicts problem entries.
        
        final Set<IStatus> problems = new HashSet<IStatus>();
        IStatus[] children = result.getChildren();
        
        for( IStatus child : children )
        {
            problems.add( child );
        }
        
        final Set<IStatus> toremove = new HashSet<IStatus>();

        for( IStatus problem : problems )
        {
            if( ! toremove.contains( problem ) )
            {
                final ValidationProblem valprob = (ValidationProblem) problem;
                
                if( valprob.getType() == ValidationProblem.Type.CONFLICTS )
                {
                    final Object[] p = valprob.getParameters();
                    
                    final ValidationProblem reverse
                        = new ValidationProblem( ValidationProblem.Type.CONFLICTS,
                                                 new Object[] { p[ 1 ], p[ 0 ] } );
                    
                    toremove.add( reverse );
                }
            }
        }
        
        if( toremove.size() > 0 )
        {
            problems.removeAll( toremove );
            children = problems.toArray( new IStatus[ problems.size() ] );
            result = Constraint.createMultiStatus( children );
        }
        
        // Return the problems to the caller.
        
        return result;
    }
    
    public void sort( final Set<IProjectFacetVersion> base,
                      final List<Action> actions )
    {
        final int count = actions.size();
        
        if( count == 0 )
        {
            return;
        }
        
        // Before sorting, check that the constraints can be met. Otherwise
        // the sort algorithm will not terminate.
        
        final IStatus st = check( base, new HashSet<Action>( actions ) );
        
        if( ! st.isOK() )
        {
            FacetCorePlugin.log( st );
            return;
        }
        
        // Initialize tracing.
        
        List<Action> unsorted = null;
        int steps = 0;
        
        if( FacetCorePlugin.isTracingActionSorting() )
        {
            unsorted = new ArrayList<Action>( actions );
        }
        
        // Step 1 : Pre-sort all uninstall actions to the front of the list. 
        //          Within that order, sort actions based on facet id. The
        //          secondary sort assures a stable sort order among actions on
        //          unrelated facets.
        
        final Comparator<Action> comp = new Comparator<Action>()
        {
            public int compare( final Action a1, 
                                final Action a2 )
            {
                int res = compare( a1.getType(), a2.getType() );
                
                if( res == 0 )
                {
                    final String fid1 
                        = a1.getProjectFacetVersion().getProjectFacet().getId();
                    
                    final String fid2 
                        = a2.getProjectFacetVersion().getProjectFacet().getId();
                    
                    res = fid1.compareTo( fid2 );
                }
                
                return res;
            }
            
            private int compare( final Action.Type t1,
                                 final Action.Type t2 )
            {
                if( t1 == t2 )
                {
                    return 0;
                }
                else if( t1 == Action.Type.UNINSTALL )
                {
                    return -1;
                }
                else if( t2 == Action.Type.UNINSTALL )
                {
                    return 1;
                }
                else
                {
                    return 0;
                }
            }
        };
        
        Collections.sort( actions, comp );
        
        // Step 2 : Sort based on the constraints.
        
        final Set<IProjectFacetVersion> fnl = new HashSet<IProjectFacetVersion>( base );
        
        for( Action action : actions )
        {
            apply( fnl, action );
        }
        
        boolean makeAnotherPass = true;
        
        while( makeAnotherPass )
        {
            makeAnotherPass = false;
            
            Set<IProjectFacetVersion> state = new HashSet<IProjectFacetVersion>( base );
            
            for( int i = 0; i < count; )
            {
                final Action action = actions.get( i );
                final Action.Type type = action.getType();
                final IProjectFacetVersion fv = action.getProjectFacetVersion();
                final IConstraint constraint = fv.getConstraint();
                
                if( type == Action.Type.UNINSTALL )
                {
                    if( ! constraint.check( state, true ).isOK() &&
                        constraint.check( base, true ).isOK() )
                    {
                        moveToFront( actions, i );
                        makeAnotherPass = true;
                        steps++;
                        break;
                    }
                    else
                    {
                        apply( state, action );
                        i++;
                    }
                }
                else
                {
                    if( constraint.check( state ).isOK() &&
                        ! ( ! constraint.check( state, true ).isOK() &&
                            constraint.check( fnl, true ).isOK() ) )
                    {
                        apply( state, action );
                        i++;
                    }
                    else
                    {
                        moveToEnd( actions, i );
                        steps++;
                    }
                }
            }
        }
        
        // Output tracing information.
        
        if( FacetCorePlugin.isTracingActionSorting() )
        {
            final String text
                = Resources.bind( Resources.tracingActionSorting,
                                  toString( base ), toString( unsorted ),
                                  toString( actions ), String.valueOf( steps ) );
            
            System.out.println( text );
        }
    }
    
    static void apply( final Set<IProjectFacetVersion> facets,
                       final Action action )
    {
        final Action.Type type = action.getType();
        final IProjectFacetVersion fv = action.getProjectFacetVersion();
        
        if( type == Action.Type.INSTALL )
        {
            facets.add( fv );
        }
        else if( type == Action.Type.UNINSTALL )
        {
            facets.remove( fv );
        }
        else if( type == Action.Type.VERSION_CHANGE )
        {
            for( IProjectFacetVersion x : facets )
            {
                if( x.getProjectFacet() == fv.getProjectFacet() )
                {
                    facets.remove( x );
                    break;
                }
            }
            
            facets.add( fv );
        }
    }
    
    public synchronized Preferences getPreferences( final IProjectFacet facet )
    
        throws BackingStoreException
        
    {
        ProjectFacetPreferencesGroup group = null;
        
        if( this.globalPreferencesGroup != null )
        {
            group = this.globalPreferencesGroup.get();
        }
        
        if( group == null )
        {
            group = new ProjectFacetPreferencesGroup( null );
            this.globalPreferencesGroup = new WeakReference<ProjectFacetPreferencesGroup>( group );
        }
        
        return group.getPreferences( facet );
    }
    
    public synchronized Preferences getPreferences( final IProjectFacet facet,
                                                    final IFacetedProject project )
    
        throws BackingStoreException
        
    {
        final String pjname = project.getProject().getName();
        ProjectFacetPreferencesGroup group = null;
        WeakReference<ProjectFacetPreferencesGroup> ref = this.projectPreferencesGroups.get( pjname );
        
        if( ref != null )
        {
            group = ref.get();
        }
        
        if( group == null )
        {
            group = new ProjectFacetPreferencesGroup( project );
            ref = new WeakReference<ProjectFacetPreferencesGroup>( group );
            this.projectPreferencesGroups.put( pjname, ref );
        }
        
        return group.getPreferences( facet );
    }
    
    private static IProgressMonitor submon( final IProgressMonitor monitor,
                                            final int ticks )
    {
        if( monitor == null )
        {
            return null;
        }
        else
        {
            return new SubProgressMonitor( monitor, ticks );
        }
    }
    
    private static void moveToFront( final List<Action> actions,
                                     final int index )
    {
        final Action action = actions.get( index );
        
        for( int i = index; i > 0; i-- )
        {
            actions.set( i, actions.get( i - 1 ) );
        }
        
        actions.set( 0, action );
    }
    
    private static void moveToEnd( final List<Action> actions,
                                   final int index )
    {
        final Action action = actions.get( index );
        
        for( int i = index + 1, n = actions.size(); i < n; i++ )
        {
            actions.set( i - 1, actions.get( i ) );
        }
        
        actions.set( actions.size() - 1, action );
    }
    
    private void readMetadata()
    {
        final IExtensionRegistry registry = Platform.getExtensionRegistry();
        
        final IExtensionPoint point 
            = registry.getExtensionPoint( FacetCorePlugin.PLUGIN_ID, 
                                          EXTENSION_ID );
        
        if( point == null )
        {
            throw new RuntimeException( "Extension point not found!" ); //$NON-NLS-1$
        }
        
        final List<IConfigurationElement> cfgels = new ArrayList<IConfigurationElement>();
        
        for( IExtension extension : point.getExtensions() )
        {
            for( IConfigurationElement cfgel : extension.getConfigurationElements() )
            {
                cfgels.add( cfgel );
            }
        }
        
        for( IConfigurationElement config : cfgels )
        {
            if( config.getName().equals( EL_CATEGORY ) )
            {
                try
                {
                    readCategory( config );
                }
                catch( InvalidExtensionException e )
                {
                    // Continue. The problem has been reported in the log.
                }
            }
        }
        
        for( IConfigurationElement config : cfgels )
        {
            if( config.getName().equals( EL_PROJECT_FACET ) )
            {
                try
                {
                    readProjectFacet( config );
                }
                catch( InvalidExtensionException e )
                {
                    // Continue. The problem has been reported in the log.
                }
            }
        }
        
        final Map<ProjectFacetVersion,IConfigurationElement> fvToConstraint 
            = new HashMap<ProjectFacetVersion,IConfigurationElement>();
        
        final Map<ProjectFacetVersion,List<IConfigurationElement>> fvToActions 
            = new HashMap<ProjectFacetVersion,List<IConfigurationElement>>();
        
        for( IConfigurationElement config : cfgels )
        {
            if( config.getName().equals( EL_PROJECT_FACET_VERSION ) )
            {
                readProjectFacetVersion( config, fvToConstraint, fvToActions );
            }
        }
        
        calculateVersionComparisonTables( fvToConstraint, fvToActions );
        
        ProjectFacetGroupsExtensionPoint.processExtensions( this );        
        
        for( Map.Entry<ProjectFacetVersion,IConfigurationElement> x : fvToConstraint.entrySet() )
        {
            readConstraint( x.getValue(), x.getKey() );
        }
        
        for( Map.Entry<ProjectFacetVersion,List<IConfigurationElement>> x : fvToActions.entrySet() )
        {
            final ProjectFacetVersion fv = x.getKey();
            final List<IConfigurationElement> actions = x.getValue();
            
            for( IConfigurationElement config : actions )
            {
                readAction( config, (ProjectFacet) fv.getProjectFacet(), fv.getVersionString() );
            }
        }

        for( IConfigurationElement config : cfgels )
        {
            if( config.getName().equals( EL_ACTION ) )
            {
                readAction( config );
            }
            else if( config.getName().equals( EL_EVENT_HANDLER ) )
            {
                readEventHandler( config );
            }
        }
        
        for( IConfigurationElement config : cfgels )
        {
            if( config.getName().equals( EL_PROJECT_FACET ) )
            {
                readDefaultVersionInfo( config );
            }
        }
    }
    
    private void readCategory( final IConfigurationElement config )
    
        throws InvalidExtensionException
        
    {
        final Category category = new Category();
        category.setId( findRequiredAttribute( config, ATTR_ID ) );
        category.setPluginId( config.getContributor().getName() );

        final IConfigurationElement elLabel = findOptionalElement( config, EL_LABEL );
        category.setLabel( getElementValue( elLabel, category.getId() ) );
        
        final IConfigurationElement elDesc = findOptionalElement( config, EL_DESCRIPTION );
        category.setDescription( getElementValue( elDesc, DEFAULT_DESCRIPTION ) );
        
        this.categories.addItemWithKey( category.getId(), category );
    }
    
    private void readProjectFacet( final IConfigurationElement config )
    
        throws InvalidExtensionException
        
    {
        final ProjectFacet f = new ProjectFacet();
        f.setId( findRequiredAttribute( config, ATTR_ID ) );
        f.setPluginId( config.getContributor().getName() );
        
        final IConfigurationElement elLabel = findOptionalElement( config, EL_LABEL );
        f.setLabel( getElementValue( elLabel, f.getId() ) );
        
        final IConfigurationElement elDesc = findOptionalElement( config, EL_DESCRIPTION );
        f.setDescription( getElementValue( elDesc, DEFAULT_DESCRIPTION ) );

        final IConfigurationElement elComp = findOptionalElement( config, EL_VERSION_COMPARATOR );
        
        if( elComp != null )
        {
            f.setVersionComparator( findRequiredAttribute( elComp, ATTR_CLASS ) );
        }
        
        final IConfigurationElement[] children = config.getChildren();
        
        for( int i = 0; i < children.length; i++ )
        {
            final IConfigurationElement child = children[ i ];
            final String childName = child.getName();
            
            if( childName.equals( EL_PROPERTY ) )
            {
                final String name = child.getAttribute( ATTR_NAME );
    
                if( name == null )
                {
                    reportMissingAttribute( child, ATTR_NAME );
                    continue;
                }
    
                final String value = child.getAttribute( ATTR_VALUE );
                
                if( value == null )
                {
                    reportMissingAttribute( child, ATTR_VALUE );
                    continue;
                }
                
                Object parsedValue = value;
                
                if( name.equals( IProjectFacet.PROP_HIDE_VERSION ) )
                {
                    parsedValue = Boolean.parseBoolean( value );
                }

                f.setProperty( name, parsedValue );
            }
        }
        
        String catname = null;
        
        final IConfigurationElement elMember = findOptionalElement( config, EL_MEMBER );
        
        if( elMember != null )
        {
            catname = findRequiredAttribute( elMember, ATTR_CATEGORY );
        }
        
        // ## DEPRECATED : 2.0 ##
        {
            final IConfigurationElement elCategory = findOptionalElement( config, EL_CATEGORY );
            
            if( elCategory != null )
            {
                catname = getElementValue( elCategory, null );
            }
        }
        
        if( catname != null )
        {
            if( isCategoryDefined( catname ) )
            {
                final Category category = (Category) getCategory( catname );
                
                f.setCategory( category );
                category.addProjectFacet( f );
            }
            else
            {
                final String msg
                    = NLS.bind( Resources.categoryNotDefined, catname ) +
                      NLS.bind( Resources.usedInPlugin, config.getContributor().getName() );
                
                FacetCorePlugin.log( msg );
            }
        }
        
        this.facets.addItem( f );
        this.facets.addKey( f.getId(), f );
        
        for( String alias : ProjectFacetAliasesExtensionPoint.getAliases( f ) )
        {
            this.facets.addKey( alias, f );
            f.addAlias( alias );
        }
    }
    
    private void readProjectFacetVersion( final IConfigurationElement config,
                                          final Map<ProjectFacetVersion,IConfigurationElement> fvToConstraint,
                                          final Map<ProjectFacetVersion,List<IConfigurationElement>> fvToActions )
    {
        final String fid = config.getAttribute( ATTR_FACET );

        if( fid == null )
        {
            reportMissingAttribute( config, ATTR_FACET );
            return;
        }
        
        final String ver = config.getAttribute( ATTR_VERSION );

        if( ver == null )
        {
            reportMissingAttribute( config, ATTR_VERSION );
            return;
        }
        
        final ProjectFacet f = (ProjectFacet) this.facets.getItemByKey( fid );
        
        if( f == null )
        {
            ProblemLog.reportMissingFacet( fid, config.getContributor().getName() );
            return;
        }
        
        final ProjectFacetVersion fv
            = new ProjectFacetVersion();
        
        fv.setProjectFacet( f );
        fv.setVersionString( ver );
        fv.setPluginId( config.getContributor().getName() );
        
        final List<IConfigurationElement> actions = new ArrayList<IConfigurationElement>();
        fvToActions.put( fv, actions );
        
        final IConfigurationElement[] children = config.getChildren();
        
        for( int i = 0; i < children.length; i++ )
        {
            final IConfigurationElement child = children[ i ];
            final String childName = child.getName();
            
            if( childName.equals( EL_CONSTRAINT ) )
            {
                fvToConstraint.put( fv, child );
            }
            else if( childName.equals( EL_ACTION ) )
            {
                actions.add( child );
            }
            else if( childName.equals( EL_PROPERTY ) )
            {
                final String name = child.getAttribute( ATTR_NAME );
    
                if( name == null )
                {
                    reportMissingAttribute( child, ATTR_NAME );
                    continue;
                }
    
                final String value = child.getAttribute( ATTR_VALUE );
                
                if( value == null )
                {
                    reportMissingAttribute( child, ATTR_VALUE );
                    continue;
                }
                
                fv.setProperty( name, value );
            }
        }
        
        f.addVersion( fv );

        // This has to happen after facet version is registered.
        
        for( int i = 0; i < children.length; i++ )
        {
            final IConfigurationElement child = children[ i ];
            final String childName = child.getName();
            
            if( childName.equals( EL_EVENT_HANDLER ) )
            {
                readEventHandler( child, f, ver );
            }
        }
    }
    
    /**
     * Pre-computes the tables that describe how versions of a facet compare
     * to each other. This allows the IProjectFacetVersion.compareTo() operation,
     * which is called rather frequently, to be reduced to a hash table lookup
     * instead of having to do a parse and comparison of two version strings.
     */
    
    private void calculateVersionComparisonTables( final Map<ProjectFacetVersion,IConfigurationElement> fvToConstraint,
                                                   final Map<ProjectFacetVersion,List<IConfigurationElement>> fvToActions )
    {
        final List<IProjectFacet> badFacets = new ArrayList<IProjectFacet>();
        
        for( IProjectFacet f : this.facets.getItemSet() )
        {
            try
            {
                final Comparator<String> comp = f.getVersionComparator();
                
                final List<IProjectFacetVersion> versions 
                    = new ArrayList<IProjectFacetVersion>( f.getVersions() );
                
                final Map<IProjectFacetVersion,Map<IProjectFacetVersion,Integer>> compTables
                    = new HashMap<IProjectFacetVersion,Map<IProjectFacetVersion,Integer>>();
                
                for( IProjectFacetVersion fv : versions )
                {
                    compTables.put( fv, new HashMap<IProjectFacetVersion,Integer>() );
                }
                
                for( int i = 0, n = versions.size(); i < n; i++ )
                {
                    final IProjectFacetVersion iVer = versions.get( i );
                    final String iVerStr = iVer.getVersionString();
                    final Map<IProjectFacetVersion,Integer> iCompTable = compTables.get( iVer );
                    
                    for( int j = i + 1; j < n; j++ )
                    {
                        final IProjectFacetVersion jVer = versions.get( j );
                        final String jVerStr = jVer.getVersionString();
                        final Map<IProjectFacetVersion,Integer> jCompTable = compTables.get( jVer );
                        
                        final int result = comp.compare( iVerStr, jVerStr );
                        
                        iCompTable.put( jVer, new Integer( result ) );
                        jCompTable.put( iVer, new Integer( result * -1 ) );
                    }
                }
                
                for( Map.Entry<IProjectFacetVersion,Map<IProjectFacetVersion,Integer>> entry
                     : compTables.entrySet() )
                {
                    final ProjectFacetVersion fv = (ProjectFacetVersion) entry.getKey();
                    fv.setComparisonTable( entry.getValue() );
                }
            }
            catch( Exception e )
            {
                // The failure here is due to the problem loading the provided
                // version comparator or due to the problem comparing the
                // version string. In either case, we log the exception and
                // remove all traces of this facet from the system to keep a
                // faulty facet from dragging down the entire framework.
                
                FacetCorePlugin.log( e );
                badFacets.add( f );
            }
        }
        
        for( IProjectFacet f : badFacets )
        {
            this.facets.removeItem( f );
            
            final Category category = (Category) f.getCategory();
            
            if( category != null )
            {
                category.removeProjectFacet( f );
            }
            
            for( IProjectFacetVersion fv : f.getVersions() )
            {
                fvToConstraint.remove( fv );
                fvToActions.remove( fv );
            }
        }
    }
    
    private void readDefaultVersionInfo( final IConfigurationElement config )
    {
        final String id = config.getAttribute( ATTR_ID );

        if( id == null || ! isProjectFacetDefined( id ) )
        {
            // The error should have already been reported, don't need to log
            // this again.
            
            return;
        }
        
        final ProjectFacet f = (ProjectFacet) getProjectFacet( id );
        boolean defaultVersionSpecified = false;

        final IConfigurationElement[] children = config.getChildren();
        
        for( int i = 0; i < children.length; i++ )
        {
            final IConfigurationElement child = children[ i ];
            final String childName = child.getName();
            
            if( childName.equals( EL_DEFAULT_VERSION ) )
            {
                final String clname = child.getAttribute( ATTR_PROVIDER );
                
                if( clname != null )
                {
                    final IDefaultVersionProvider defaultVersionProvider
                        = instantiate( f.getPluginId(), clname, IDefaultVersionProvider.class );

                    if( defaultVersionProvider != null )
                    {
                        f.setDefaultVersionProvider( defaultVersionProvider );
                        defaultVersionSpecified = true;
                    }
                }
                else
                {
                    final String version = child.getAttribute( ATTR_VERSION );
                    
                    if( version != null )
                    {
                        if( f.hasVersion( version ) )
                        {
                            f.setDefaultVersion( f.getVersion( version ) );
                            defaultVersionSpecified = true;
                        }
                        else
                        {
                            ProblemLog.reportMissingFacetVersion( f, version, config.getContributor().getName() );
                        }
                    }
                    else
                    {
                        reportMissingAttribute( config, ATTR_VERSION );
                    }
                }
            }
        }
        
        if( ! defaultVersionSpecified )
        {
            try
            {
                f.setDefaultVersion( f.getLatestVersion() );
            }
            catch( Exception e )
            {
                FacetCorePlugin.log( e );
            }
        }
    }
    
    private void readAction( final IConfigurationElement config )
    {
        final String fid = config.getAttribute( ATTR_FACET );

        if( fid == null )
        {
            reportMissingAttribute( config, ATTR_FACET );
            return;
        }
        
        final ProjectFacet f = (ProjectFacet) this.facets.getItemByKey( fid );
        
        if( f == null )
        {
            ProblemLog.reportMissingFacet( fid, config.getContributor().getName() );
            return;
        }
        
        String ver = config.getAttribute( ATTR_VERSION );

        if( ver == null )
        {
            ver = IVersionExpr.WILDCARD_SYMBOL;
        }
        
        readAction( config, f, ver );
    }

    private void readAction( final IConfigurationElement config,
                             final ProjectFacet f,
                             final String version )
    {
        final String pluginId = config.getContributor().getName();
        final ActionDefinition def = new ActionDefinition();
        
        def.setPluginId( pluginId );
        
        final String type = config.getAttribute( ATTR_TYPE );
        
        if( type == null )
        {
            reportMissingAttribute( config, ATTR_TYPE );
            return;
        }
        
        // Backwards compatibility of deprecated functionality.
        
        if( type.equals( "runtime-changed" ) ) //$NON-NLS-1$
        {
            final String msg
                = NLS.bind( Resources.deprecatedRuntimeChangedAction, pluginId );
            
            FacetCorePlugin.logWarning( msg, true );
            
            readEventHandler( config, f, version );
            
            return;
        }
        
        // End of backwards compatibility code.
        
        def.setActionType( Action.Type.valueOf( type ) ); 

        if( def.getActionType() == null )
        {
            final String msg
                = NLS.bind( Resources.invalidActionType, type ) +
                  NLS.bind( Resources.usedInPlugin, pluginId );
            
            FacetCorePlugin.log( msg );
            
            return;
        }
        
        try
        {
            def.setVersionExpr( new VersionExpr<ProjectFacetVersion>( f, version, pluginId ) );
        }
        catch( CoreException e )
        {
            FacetCorePlugin.log( e );
            return;
        }

        final IConfigurationElement[] children = config.getChildren();
        
        for( int i = 0; i < children.length; i++ )
        {
            final IConfigurationElement child = children[ i ];
            final String childName = child.getName();
            
            if( childName.equals( EL_CONFIG_FACTORY ) )
            {
                final String clname = child.getAttribute( ATTR_CLASS );
                
                if( clname == null )
                {
                    reportMissingAttribute( child, ATTR_CLASS );
                    return;
                }
                
                def.setConfigFactoryClassName( clname );
            }
            else if( childName.equals( EL_DELEGATE ) )
            {
                final String clname = child.getAttribute( ATTR_CLASS );
                
                if( clname == null )
                {
                    reportMissingAttribute( config, ATTR_CLASS );
                    return;
                }
                
                def.setDelegateClassName( clname );
            }
            else if( childName.equals( EL_PROPERTY ) )
            {
                final String name = child.getAttribute( ATTR_NAME );

                if( name == null )
                {
                    reportMissingAttribute( child, ATTR_NAME );
                    return;
                }

                final String value = child.getAttribute( ATTR_VALUE );
                
                if( value == null )
                {
                    reportMissingAttribute( child, ATTR_VALUE );
                    return;
                }
                
                Object parsedValue = value;
                
                if( name.equals( IActionDefinition.PROP_FROM_VERSIONS ) )
                {
                    try
                    {
                        parsedValue = new VersionExpr<ProjectFacetVersion>( f, value, pluginId );
                    }
                    catch( CoreException e )
                    {
                        FacetCorePlugin.log( e );
                        return;
                    }
                }

                def.setProperty( name, parsedValue );
            }
        }
        
        String id = config.getAttribute( ATTR_ID );
        
        if( id == null )
        {
            final StringBuffer buf = new StringBuffer();
            
            buf.append( f.getId() );
            buf.append( '#' );
            buf.append( version );
            buf.append( '#' );
            buf.append( def.getActionType().name() );

            for( Map.Entry<String,Object> entry : def.getProperties().entrySet() )
            {
                buf.append( '#' );
                buf.append( entry.getKey() );
                buf.append( '=' );
                buf.append( entry.getValue().toString() );
            }
            
            id = buf.toString();
        }
        
        def.setId( id );
        
        if( isActionDefined( id ) )
        {
            final String msg
                = NLS.bind( Resources.actionAlreadyDefined, id, pluginId );
            
            FacetCorePlugin.logError( msg );
        }
        else
        {
            this.actions.addItemWithKey( def.getId(), def );
            f.addActionDefinition( def );
        }
    }
    
    private void readEventHandler( final IConfigurationElement config )
    {
        final String fid = config.getAttribute( ATTR_FACET );

        if( fid == null )
        {
            reportMissingAttribute( config, ATTR_FACET );
            return;
        }
        
        final ProjectFacet f = (ProjectFacet) this.facets.getItemByKey( fid );
        
        if( f == null )
        {
            ProblemLog.reportMissingFacet( fid, config.getContributor().getName() );
            return;
        }
        
        String ver = config.getAttribute( ATTR_VERSION );

        if( ver == null )
        {
            ver = IVersionExpr.WILDCARD_SYMBOL;
        }
        
        readEventHandler( config, f, ver );
    }
    
    /**
     * Support for a deprecated extension point.
     */

    private void readEventHandler( final IConfigurationElement config,
                                   final ProjectFacet f,
                                   final String version )
    {
        final String pluginId = config.getContributor().getName();
        
        final String type = config.getAttribute( ATTR_TYPE );
        
        if( type == null )
        {
            reportMissingAttribute( config, ATTR_TYPE );
            return;
        }
        
        final IFacetedProjectEvent.Type t;
        
        if( type.equals( "runtime-changed" ) || type.equals( "RUNTIME_CHANGED" )) //$NON-NLS-1$ //$NON-NLS-2$
        {
            t = IFacetedProjectEvent.Type.PRIMARY_RUNTIME_CHANGED;
        }
        else
        {
            t = IFacetedProjectEvent.Type.valueOf( type );
        }
        
        if( t == null )
        {
            final String msg
                = NLS.bind( Resources.invalidEventHandlerType, type ) +
                  NLS.bind( Resources.usedInPlugin, pluginId );
            
            FacetCorePlugin.log( msg );
            
            return;
        }
        
        final IVersionExpr vexpr;
        
        try
        {
            vexpr = new VersionExpr<ProjectFacetVersion>( f, version, pluginId );
        }
        catch( CoreException e )
        {
            FacetCorePlugin.log( e );
            return;
        }
        
        String delegateClassName = null;

        final IConfigurationElement[] children = config.getChildren();
        
        for( int i = 0; i < children.length; i++ )
        {
            final IConfigurationElement child = children[ i ];
            final String childName = child.getName();
            
            if( childName.equals( EL_DELEGATE ) )
            {
                final String clname = child.getAttribute( ATTR_CLASS );
                
                if( clname == null )
                {
                    reportMissingAttribute( config, ATTR_CLASS );
                    return;
                }
                
                delegateClassName = clname;
            }
        }
        
        if( delegateClassName == null )
        {
            reportMissingElement( config, EL_DELEGATE );
            return;
        }
        
        final LegacyEventHandlerAdapter adapter
            = new LegacyEventHandlerAdapter( f, vexpr, pluginId, delegateClassName );
        
        addListener( adapter, t );
    }
    
    private void readConstraint( final IConfigurationElement config,
                                 final ProjectFacetVersion fv )
    {
        final ProblemLog.Policy problemLoggingPolicy 
            = ProblemLog.Policy.createBasedOnIgnoreProblemsAttr( config );

        final IConfigurationElement[] ops = config.getChildren();
        final List<IConstraint> parsed = new ArrayList<IConstraint>();
        
        for( int j = 0; j < ops.length; j++ )
        {
            final IConstraint op = readConstraintHelper( ops[ j ], fv, problemLoggingPolicy );
            
            if( op != null )
            {
                parsed.add( op );
            }
        }
        
        if( parsed.size() == 1 )
        {
            fv.setConstraint( parsed.get( 0 ) );
        }
        else if( parsed.size() > 1 )
        {
            final IConstraint and 
                = new Constraint( fv, IConstraint.Type.AND, 
                                  parsed.toArray() );
            
            fv.setConstraint( and );
        }
    }
    
    private IConstraint readConstraintHelper( final IConfigurationElement root,
                                              final ProjectFacetVersion fv,
                                              final ProblemLog.Policy parentProblemLoggingPolicy )
    {
        final String pluginId = root.getContributor().getName();
        
        final ProblemLog.Policy localProblemLoggingPolicy 
            = ProblemLog.Policy.createBasedOnIgnoreProblemsAttr( root, parentProblemLoggingPolicy );
        
        final IConstraint.Type type
            = IConstraint.Type.valueOf( root.getName() );
        
        final Object[] operands;
     
        if( type == IConstraint.Type.AND ||
            type == IConstraint.Type.OR )
        {
            final IConfigurationElement[] children = root.getChildren();
            final List<Object> operandsList = new ArrayList<Object>( children.length );
            
            for( int i = 0; i < children.length; i++ )
            {
                final IConstraint operand 
                    = readConstraintHelper( children[ i ], fv, localProblemLoggingPolicy );
                
                if( operand != null )
                {
                    operandsList.add( operand );
                }
                else
                {
                    if( type == IConstraint.Type.AND )
                    {
                        return null;
                    }
                }
            }
            
            operands = operandsList.toArray();
        }
        else if( type == IConstraint.Type.REQUIRES ||
                 type == IConstraint.Type.CONFLICTS )
        {
            final String gid = root.getAttribute( ATTR_GROUP );
            final String fid = root.getAttribute( ATTR_FACET );
            String vexprstr = root.getAttribute( ATTR_VERSION );
            
            final String softStr = root.getAttribute( ATTR_SOFT );
            Boolean soft = Boolean.FALSE;
            
            if( softStr != null && softStr.equals( Boolean.TRUE.toString() ) )
            {
                soft = Boolean.TRUE;
            }
            
            if( gid != null && ( fid != null || vexprstr != null ) )
            {
                final String template
                    = type == IConstraint.Type.REQUIRES
                      ? Resources.invalidRequiresConstraint
                      : Resources.invalidConflictsConstraint;
                
                final String msg = NLS.bind( template, pluginId );
                
                FacetCorePlugin.logError( msg, true );
                return null;
            }
            else if( gid != null )
            {
                if( ! isGroupDefined( gid ) )
                {
                    ProblemLog.reportMissingGroup( gid, pluginId, localProblemLoggingPolicy );
                    return null;
                }
                
                final IGroup group = getGroup( gid );
                
                if( type == IConstraint.Type.REQUIRES )
                {
                    operands = new Object[] { group, soft };
                }
                else
                {
                    operands = new Object[] { group };
                }
            }
            else if( fid != null )
            {
                if( ! isProjectFacetDefined( fid ) )
                {
                    ProblemLog.reportMissingFacet( fid, pluginId, localProblemLoggingPolicy );
                    return null;
                }
                
                final IProjectFacet f = getProjectFacet( fid );

                VersionExpr vexpr = null;
                
                try
                {
                    if( vexprstr == null || vexprstr.trim().length() == 0 )
                    {
                        vexprstr = IVersionExpr.WILDCARD_SYMBOL;
                    }
                    
                    vexpr = new VersionExpr( f, vexprstr, pluginId );
                }
                catch( CoreException e )
                {
                    FacetCorePlugin.log( e );
                    return null;
                }
                
                if( type == IConstraint.Type.REQUIRES )
                {
                    operands = new Object[] { f, vexpr, soft };
                }
                else
                {
                    operands = new Object[] { f, vexpr };
                }
            }
            else
            {
                final String msg
                    = Resources.bind( Resources.missingOneOfTwoAttributes,
                                      pluginId, root.getName(), ATTR_GROUP,
                                      ATTR_FACET );
                
                FacetCorePlugin.logError( msg, true );
                return null;
            }
        }
        else
        {
            throw new IllegalStateException();
        }
        
        return new Constraint( fv, type, operands );
    }
    
    private static String toString( final Collection<? extends Object> collection )
    {
        final StringBuffer buf = new StringBuffer();
        
        for( Object obj : collection )
        {
            if( buf.length() > 0 )
            {
                buf.append( ", " ); //$NON-NLS-1$
            }
            
            if( obj instanceof IProjectFacetVersion )
            {
                final IProjectFacetVersion fv = (IProjectFacetVersion) obj;
                
                buf.append( fv.getProjectFacet().getId() );
                buf.append( ' ' );
                buf.append( fv.getVersionString() );
            }
            else
            {
                buf.append( obj.toString() );
            }
        }
        
        return buf.toString();
    }
    
    private final class ResourceChangeListener
    
        implements IResourceChangeListener
        
    {
        public void register()
        {
            final IWorkspace ws = ResourcesPlugin.getWorkspace();
            ws.addResourceChangeListener( this, IResourceChangeEvent.POST_CHANGE );
        }
        
        public void resourceChanged( final IResourceChangeEvent event )
        {
            final IResourceDelta delta = event.getDelta();
            final List<FacetedProject> projectsToRefresh = new ArrayList<FacetedProject>();
            
            synchronized( FacetedProjectFrameworkImpl.this.projects )
            {
                for( IResourceDelta subdelta : delta.getAffectedChildren( IResourceDelta.REMOVED ) )
                {
                    final String pjname = subdelta.getFullPath().segment( 0 );
                    
                    final IFacetedProject fpj 
                        = FacetedProjectFrameworkImpl.this.projects.remove( pjname );
                    
                    if( fpj != null )
                    {
                        try
                        {
                            ( (FacetedProject) fpj ).markDeleted();
                        }
                        catch( CoreException e )
                        {
                            FacetCorePlugin.log( e );
                        }
                    }
                }
                
                for( FacetedProject fproj : FacetedProjectFrameworkImpl.this.projects.values() )
                {
                    final IResourceDelta subdelta = delta.findMember( fproj.f.getFullPath() );
                    
                    if( subdelta != null )
                    {
                        projectsToRefresh.add( fproj );
                    }
                }
            }
            
            for( FacetedProject fproj : projectsToRefresh )
            {
                try
                {
                    fproj.refresh();
                }
                catch( CoreException e )
                {
                    FacetCorePlugin.log( e );
                }
            }
        }
    }

    public static final class Resources
    
        extends NLS
        
    {
        public static String missingOneOfTwoAttributes;
        public static String categoryNotDefined;
        public static String facetNotDefined;
        public static String facetVersionNotDefined;
        public static String actionNotDefined;
        public static String actionAlreadyDefined;
        public static String groupNotDefined;
        public static String presetNotDefined;
        public static String templateNotDefined;
        public static String usedInPlugin;
        public static String invalidActionType;
        public static String invalidEventHandlerType;
        public static String invalidRequiresConstraint;
        public static String invalidConflictsConstraint;
        public static String deprecatedRuntimeChangedAction;
        public static String tracingActionSorting;
        public static String tracingFrameworkActivationStarting;
        public static String tracingFrameworkActivationFinished;
        
        static
        {
            initializeMessages( FacetedProjectFrameworkImpl.class.getName(), 
                                Resources.class );
        }
        
        public static String bind( final String template,
                                   final Object arg1,
                                   final Object arg2,
                                   final Object arg3 )
        {
            return NLS.bind( template, new Object[] { arg1, arg2, arg3 } );
        }

        public static String bind( final String template,
                                   final Object arg1,
                                   final Object arg2,
                                   final Object arg3,
                                   final Object arg4 )
        {
            return NLS.bind( template, new Object[] { arg1, arg2, arg3, arg4 } );
        }
    }
    
}
