/******************************************************************************
* Copyright (c) 2011-2013, Linagora
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Linagora - initial API and implementation
*******************************************************************************/
package com.ebmwebsourcing.petals.common.internal.projectscnf;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IPackageFragment;
import com.ebmwebsourcing.petals.common.internal.PetalsCommonPlugin;
import com.ebmwebsourcing.petals.common.internal.provisional.projectscnf.PetalsCnfPackageFragment;
import com.ebmwebsourcing.petals.common.internal.provisional.projectscnf.PetalsProjectCategory;
import com.ebmwebsourcing.petals.common.internal.provisional.utils.PlatformUtils;
import com.ebmwebsourcing.petals.common.internal.provisional.utils.StringUtils;
/**
* @author Vincent Zurczak - EBM WebSourcing
*/
public class PetalsProjectManager implements IResourceChangeListener, IResourceDeltaVisitor {
public static final PetalsProjectManager INSTANCE = new PetalsProjectManager();
private static final String FLAT_LAYOUT_PREF = "petals.flat.layout.preference";
private static final String EXTENSION_POINT = "com.ebmwebsourcing.petals.common.projectCategories";
private List<PetalsProjectCategory> categories = null;
private final Map<IProject,List<PetalsProjectCategory>> projectToCategories;
private final Map<PetalsProjectCategory,List<IProject>> categoryToProjects;
private final ConcurrentLinkedQueue<IPetalsProjectResourceChangeListener> listeners;
/**
* This map is populated by the content provider and used by the label provider.
* <p>
* This is a workaround to make the display mode and empty package filtering work.
* </p>
*/
public final Map<IPackageFragment,PetalsCnfPackageFragment> dirtyViewerMap = new ConcurrentHashMap<IPackageFragment,PetalsCnfPackageFragment> ();
private final StatisticsTimer statisticsTimer;
/**
* Constructor.
*/
private PetalsProjectManager() {
this.statisticsTimer = new StatisticsTimer();
// Set it to false or comment it for a release (only for tests)
this.statisticsTimer.setEnabled( false );
//
this.projectToCategories = new HashMap<IProject,List<PetalsProjectCategory>> ();
this.categoryToProjects = new HashMap<PetalsProjectCategory,List<IProject>> ();
this.listeners = new ConcurrentLinkedQueue<IPetalsProjectResourceChangeListener> ();
}
/**
* @param listener the listener to add
* @see java.util.List#add(java.lang.Object)
*/
public void addListener( IPetalsProjectResourceChangeListener listener ) {
this.listeners.add( listener );
}
/**
* @param listener the listener to remove
* @see java.util.List#remove(java.lang.Object)
*/
public void removeListener( IPetalsProjectResourceChangeListener listener ) {
this.listeners.remove( listener );
}
/**
* @return a non-null list of project categories
*/
public synchronized List<PetalsProjectCategory> getProjectCategories() {
// Start RECORDING
this.statisticsTimer.start( "[ CATEGORIES ] " );
// Need to build the categories?
if( this.categories == null ) {
this.categories = new ArrayList<PetalsProjectCategory> ();
// Get the categories from the extension point
IExtensionRegistry reg = Platform.getExtensionRegistry();
IConfigurationElement[] extensions = reg.getConfigurationElementsFor( EXTENSION_POINT );
for( IConfigurationElement elt : extensions ) {
String className = elt.getAttribute( "class" );
if( StringUtils.isEmpty( className )) {
PetalsCommonPlugin.log( "No project category was found for " + elt.getContributor().getName(), IStatus.ERROR );
continue;
}
try {
PetalsProjectCategory cat = (PetalsProjectCategory) elt.createExecutableExtension( "class" );
this.categories.add( cat );
} catch( CoreException e ) {
PetalsCommonPlugin.log( "A project category could not be instantiated - " + className, IStatus.ERROR );
}
}
// Record the time
this.statisticsTimer.track( "[ CATEGORIES ] Got all the categories from the extension-point." );
// Initialize the associations
rebuildCategoryAssociations();
// Listen to work space changes
int events = IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.POST_BUILD;
ResourcesPlugin.getWorkspace().addResourceChangeListener( this, events );
}
// End of recording
this.statisticsTimer.stop( "[ CATEGORIES ] " );
return this.categories;
}
/**
* Rebuilds the association between projects and Petals categories.
* <p>
* For performance reasons, these associations are cached.
* Note that the work space is not refreshed by this method.
* </p>
*/
public void rebuildCategoryAssociations() {
this.projectToCategories.clear();
this.statisticsTimer.track( "[ CATEGORIES ] Going through the workspace." );
for( IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
for( PetalsProjectCategory cat : getProjectCategories()) {
if( cat.projectMatches( p )) {
List<PetalsProjectCategory> cats = this.projectToCategories.get( p );
if( cats == null )
cats = new ArrayList<PetalsProjectCategory> ();
cats.add( cat );
this.projectToCategories.put( p, cats );
}
}
}
this.statisticsTimer.track( "[ CATEGORIES ] Went through the workspace." );
rebuildCategoryReversedAssociation();
this.statisticsTimer.track( "[ CATEGORIES ] Built reversed associations." );
}
/**
* @param project the project
* @return the associated categories (can be null)
*/
public List<PetalsProjectCategory> getCategories( IProject project ) {
return this.projectToCategories.get( project );
}
/**
* @param category the category
* @return the associated projects (can be null)
*/
public List<IProject> getProjects( PetalsProjectCategory category ) {
return this.categoryToProjects.get( category );
}
/**
* @param categoryId the category ID
* @return the associated projects (can be null)
*/
public List<IProject> getProjects( String categoryId ) {
PetalsProjectCategory cat = null;
for( PetalsProjectCategory c : this.categoryToProjects.keySet()) {
if( c.getId().equals( categoryId )) {
cat = c;
break;
}
}
return cat == null ? new ArrayList<IProject> () : this.categoryToProjects.get( cat );
}
/**
* Refreshes the association for a given project.
* @param project the project whose associations must be refreshed
* @param refreshReversedAssociations true to refresh the reversed associations, false if this action is called after
*/
public void refreshAssociation( IProject project, boolean refreshReversedAssociations ) {
this.projectToCategories.remove( project );
for( PetalsProjectCategory cat : getProjectCategories()) {
if( cat.projectMatches( project )) {
List<PetalsProjectCategory> cats = this.projectToCategories.get( project );
if( cats == null )
cats = new ArrayList<PetalsProjectCategory> ();
cats.add( cat );
this.projectToCategories.put( project, cats );
}
}
if( refreshReversedAssociations )
rebuildCategoryReversedAssociation();
}
/**
* Rebuilds the association between projects and Petals categories.
*/
private void rebuildCategoryReversedAssociation() {
this.categoryToProjects.clear();
for( Map.Entry<IProject,List<PetalsProjectCategory>> entry : this.projectToCategories.entrySet()) {
for( PetalsProjectCategory cat : entry.getValue()) {
List<IProject> projects = this.categoryToProjects.get( cat );
if( projects == null )
projects = new ArrayList<IProject> ();
projects.add( entry.getKey());
this.categoryToProjects.put( cat, projects );
}
}
}
/**
* @param flatLayout true for a flat layout, false otherwise
*/
public static void storeJavaLayoutPreference( boolean flatLayout ) {
PetalsCommonPlugin.getDefault().getPreferenceStore().setValue( FLAT_LAYOUT_PREF, flatLayout );
}
/**
* @return true if the Java packages should be displayed in a flat way, false for hierarchical
*/
public static boolean isJavaLayoutFlat() {
return PetalsCommonPlugin.getDefault().getPreferenceStore().getBoolean( FLAT_LAYOUT_PREF );
}
private final Set<IResource> addedResources = new HashSet<IResource> ();
private final Set<IResource> removedResources = new HashSet<IResource> ();
/*
* (non-Jsdoc)
* @see org.eclipse.core.resources.IResourceChangeListener
* #resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
*/
@Override
public void resourceChanged( IResourceChangeEvent event ) {
try {
// Start recording
this.statisticsTimer.start( " [ CHANGE ] " );
// Disable graphical updates
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.prepareNotification();
// Clear the collections
this.addedResources.clear();
this.removedResources.clear();
// Process the changes
if( event.getType() == IResourceChangeEvent.POST_CHANGE ) {
event.getDelta().accept( this );
rebuildCategoryReversedAssociation();
// Signal the modifications - removed first and additions last
if( ! this.removedResources.isEmpty()) {
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.resourcesRemoved( this.removedResources );
}
if( ! this.addedResources.isEmpty()) {
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.resourcesAdded( this.addedResources );
}
} else if( event.getType() == IResourceChangeEvent.POST_BUILD ) {
IMarkerDelta[] markerDeltas = event.findMarkerDeltas( null, true );
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.markerChanged( markerDeltas );
}
} catch( CoreException e ) {
PetalsCommonPlugin.log( e, IStatus.ERROR );
} finally {
// Re-enable graphical updates
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.terminateNotification();
// Stop recording
this.statisticsTimer.stop( " [ CHANGE ] " );
}
}
/*
* (non-Jsdoc)
* @see org.eclipse.core.resources.IResourceDeltaVisitor
* #visit(org.eclipse.core.resources.IResourceDelta)
*/
@Override
public boolean visit( IResourceDelta delta ) throws CoreException {
// Update the project associations
if( delta.getResource() instanceof IProject ) {
// Addition
if( delta.getKind() == IResourceDelta.ADDED ) {
this.statisticsTimer.track( " [ CHANGE ] Starting project addition..." );
refreshAssociation((IProject) delta.getResource(), false );
this.addedResources.add( delta.getResource());
this.statisticsTimer.track( " [ CHANGE ] Project addition propagated." );
}
// Removal
else if( delta.getKind() == IResourceDelta.REMOVED ) {
this.statisticsTimer.track( " [ CHANGE ] Starting project removal..." );
refreshAssociation((IProject) delta.getResource(), false );
this.removedResources.add( delta.getResource());
this.statisticsTimer.track( " [ CHANGE ] Project removal propagated." );
}
// Other interesting modification
else if(( delta.getFlags() & IResourceDelta.LOCAL_CHANGED) != 0
|| ( delta.getFlags() & IResourceDelta.DESCRIPTION) != 0 ) {
refreshAssociation((IProject) delta.getResource(), false );
}
}
// Non-projects
else {
// Special treatment for Java packages
Object javaFragment = PlatformUtils.getAdapter( delta.getResource(), IJavaElement.class );
if( javaFragment instanceof IPackageFragment ) {
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.elementChanged(((IPackageFragment) javaFragment).getParent());
}
// Default behavior for all the files and directories
else if( delta.getKind() == IResourceDelta.ADDED )
this.addedResources.add( delta.getResource());
else if( delta.getKind() == IResourceDelta.REMOVED )
this.removedResources.add( delta.getResource());
// Addition or removal of a jbi.xml => update the project's associations
if( "jbi.xml".equals( delta.getResource().getName())) {
// We may have to add or remove a project from the view if the jbi.xml changes
// To avoid uncomfortable issues for a user (like a wrong modification in a jbi.xml => wrong XML => the project disappears from the view),
// we only monitor the apparition of valid jbi.xml files.
// Such a case can occur with the JSR-181 (WSDL-first approach) or any other SU project which is not created with a (valid) jbi.xml.
IProject p = delta.getResource().getProject();
List<PetalsProjectCategory> cats = this.projectToCategories.get( p );
boolean presentBefore = cats != null && ! cats.isEmpty();
refreshAssociation( p, false );
cats = this.projectToCategories.get( p );
boolean presentAfter = cats != null && ! cats.isEmpty();
if( ! presentBefore && presentAfter )
this.addedResources.add( p );
}
}
// Signal other modifications
this.statisticsTimer.track( " [ CHANGE ] Starting other propagation..." );
for( IPetalsProjectResourceChangeListener listener : this.listeners )
listener.resourceChanged( delta );
this.statisticsTimer.track( " [ CHANGE ] Other propagation done." );
return delta.getResource() instanceof IContainer;
}
}