/**
* Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.codesourcery.jasm16.ide;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import de.codesourcery.jasm16.compiler.ICompilationUnit;
import de.codesourcery.jasm16.compiler.io.FileResource;
import de.codesourcery.jasm16.compiler.io.IResource;
import de.codesourcery.jasm16.compiler.io.IResource.ResourceType;
import de.codesourcery.jasm16.ide.exceptions.ProjectAlreadyExistsException;
import de.codesourcery.jasm16.ide.exceptions.ProjectNotFoundException;
import de.codesourcery.jasm16.utils.IOrdered;
import de.codesourcery.jasm16.utils.IOrdered.Priority;
import de.codesourcery.jasm16.utils.Misc;
/**
* Default workspace implementation.
*
* <p>This implementation uses a meta-data file {@link #WORKSPACE_METADATA_FILE}
* in the root folder of the workspace to keep track of all
* projects that are to be managed by this workspace instance.</p>
*
* @author tobias.gierke@code-sourcery.de
*/
public class DefaultWorkspace implements IWorkspace
{
private static final boolean DEBUG_EVENTS = false;
private static final Logger LOG = Logger.getLogger(DefaultWorkspace.class);
private final List<IAssemblyProject> projects = new ArrayList<IAssemblyProject>();
private final List<IResourceListener> listeners = new ArrayList<IResourceListener>();
private final AtomicBoolean opened = new AtomicBoolean(false);
private final IApplicationConfig appConfig;
private WorkspaceConfig workspaceConfig;
private final IBuildManager buildManager;
public DefaultWorkspace(IApplicationConfig appConfig) throws IOException
{
if (appConfig == null) {
throw new IllegalArgumentException("appConfig must not be NULL");
}
this.appConfig = appConfig;
final BuildManager tmp = new BuildManager(this);
this.buildManager = tmp;
addResourceListener( tmp );
}
@Override
public IBuildManager getBuildManager() {
return buildManager;
}
private synchronized WorkspaceConfig getWorkspaceConfig() throws IOException
{
if ( workspaceConfig == null ) {
workspaceConfig = new WorkspaceConfig( new File( getBaseDirectory() ,
WorkspaceConfig.FILE_NAME ) );
}
return workspaceConfig;
}
@Override
public File getBaseDirectory() {
return appConfig.getWorkspaceDirectory();
}
@Override
public List<IAssemblyProject> getAllProjects()
{
assertWorkspaceOpen();
return new ArrayList<IAssemblyProject>( projects );
}
protected void assertWorkspaceOpen() {
if ( opened.get() == false ) {
throw new IllegalStateException("Workspace "+getBaseDirectory().getAbsolutePath()+" is not open");
}
}
protected void loadProjects() throws IOException
{
opened.set( false );
final List<IAssemblyProject> tmp = new ArrayList<IAssemblyProject>();
for ( File dir : getWorkspaceConfig().getProjectsBaseDirectories() )
{
if ( ! dir.isDirectory() ) {
LOG.error("loadProjects(): Project directory "+dir.getName()+" no longer exists");
continue;
}
try {
tmp.add( loadProject( dir ) );
} catch (IOException e) {
LOG.error("loadProjects(): Failed to load project from "+dir.getAbsolutePath(),e);
}
}
// dispose any projects that are already loaded
for ( Iterator<IAssemblyProject> it = projects.iterator() ; it.hasNext() ; )
{
final IAssemblyProject project = it.next();
it.remove();
project.removedFromWorkspace( this );
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener) {
((IWorkspaceListener) listener).projectDisposed( project );
}
}
@Override
public String toString() {
return "PROJECT-DISPOSED: "+project;
}
});
}
// add new projects
for ( final IAssemblyProject p : tmp )
{
this.projects.add( p );
p.addedToWorkspace( this );
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener) {
((IWorkspaceListener) listener).projectLoaded( p );
}
}
@Override
public String toString() {
return "PROJECT-LOADED: "+p;
}
});
}
opened.set( true );
}
private IAssemblyProject loadProject(File baseDir) throws IOException
{
final ProjectConfiguration config = new ProjectConfiguration( baseDir );
config.load();
final boolean isProjectOpen = getWorkspaceConfig().isProjectOpen( config.getProjectName() );
final AssemblyProject tmp = new AssemblyProject( this , config , isProjectOpen );
final IProjectBuilder builder = buildManager.getProjectBuilder( tmp );
tmp.setProjectBuilder( builder );
return tmp;
}
@Override
public boolean doesProjectExist(String name)
{
assertWorkspaceOpen();
for ( IAssemblyProject existing : projects ) {
if ( existing.getName().equals( name ) ) {
return true;
}
}
return false;
}
@Override
public IAssemblyProject createNewProject(String name) throws IOException, ProjectAlreadyExistsException
{
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("name must not be NULL/blank.");
}
assertWorkspaceOpen();
if ( doesProjectExist( name ) ) {
throw new ProjectAlreadyExistsException( name );
}
final File baseDir = new File( getBaseDirectory() , name );
if ( baseDir.exists() ) {
throw new ProjectAlreadyExistsException(name,"Cannot create project '"+name+"' , project directory "+
baseDir.getAbsolutePath()+" already exists ?");
}
if ( ! baseDir.mkdirs() ) {
throw new IOException("Failed to create project base directory "+baseDir.getAbsolutePath());
}
LOG.info("createNewProject(): Creating project '"+name+"'");
final ProjectConfiguration config = new ProjectConfiguration(baseDir);
config.setProjectName( name );
try {
config.create();
}
catch (IOException e)
{
baseDir.delete();
throw e;
}
return internalAddProject( config , true );
}
private AssemblyProject internalAddProject(ProjectConfiguration config,boolean deleteProjectFilesOnError) throws IOException
{
final AssemblyProject result = new AssemblyProject( this , config , true );
projects.add( result );
try {
getWorkspaceConfig().projectAdded( result );
getWorkspaceConfig().saveConfiguration();
}
catch(IOException e)
{
LOG.error("internalAddProject(): Failed to save metadata",e);
if ( deleteProjectFilesOnError ) {
try {
internalDeleteFile( null , config.getBaseDirectory() , false );
} catch(Exception e2) {
LOG.error("internalAddProject(): Caught during rollback ",e2);
// ok, can't help it
}
projects.remove( result );
}
throw e;
}
// register project as resource listener
result.addedToWorkspace( this );
final IProjectBuilder builder = buildManager.getProjectBuilder( result );
result.setProjectBuilder( builder );
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener) {
((IWorkspaceListener) listener).projectCreated(result);
}
}
@Override
public String toString() {
return "PROJECT-CREATED: "+result;
}
});
return result;
}
@Override
public void deleteProject(final IAssemblyProject project,boolean deletePhyiscally) throws IOException
{
if (project == null) {
throw new IllegalArgumentException("project must not be NULL.");
}
assertWorkspaceOpen();
for (Iterator<IAssemblyProject> it = projects.iterator(); it.hasNext();)
{
final IAssemblyProject existing = it.next();
if ( existing.getName().equals( project.getName() ) )
{
it.remove();
if ( deletePhyiscally ) {
internalDeleteFile( existing , existing.getConfiguration().getBaseDirectory() , true );
}
existing.removedFromWorkspace( this );
try {
getWorkspaceConfig().projectDeleted( project );
getWorkspaceConfig().saveConfiguration();
}
catch (IOException e) {
LOG.error("createNewProject(): Failed to save metadata",e);
if ( ! deletePhyiscally ) { // no use re-adding the file
projects.add( project );
}
throw e;
}
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener) {
if ( listener instanceof IWorkspaceListener) {
((IWorkspaceListener) listener).projectDeleted(existing);
}
}
@Override
public String toString() {
return "PROJECT-DELETED: "+project;
}
});
return;
}
}
}
@Override
public void deleteFile(final IAssemblyProject project, final File file) throws IOException
{
if ( project == null ) {
throw new IllegalArgumentException("project must not be null");
}
if ( file == null ) {
throw new IllegalArgumentException("file must not be null");
}
internalDeleteFile(project,file,false);
}
/**
*
* @param project project or <code>null</code> if no resource listeners should be notified
* @param file
* @throws IOException
*/
protected void internalDeleteFile(final IAssemblyProject project, final File file,boolean calledByDeleteProject) throws IOException
{
if ( project != null &&
! calledByDeleteProject &&
project.getConfiguration().getBaseDirectory().equals( file ) )
{
deleteProject(project,true);
return;
}
if ( file.isDirectory() ) {
for ( File child : file.listFiles() ) {
internalDeleteFile( project , child , calledByDeleteProject );
}
}
file.delete();
if ( project != null )
{
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
IResource resource = project.getResourceForFile( file );
if ( resource == null ) {
resource = new FileResource( file , ResourceType.UNKNOWN );
}
listener.resourceDeleted( project , resource );
}
@Override
public String toString() {
return "RESOURCE-DELETED: "+file.getAbsolutePath();
}
});
}
}
@Override
public void saveProjectConfiguration(IAssemblyProject project) throws IOException
{
if (project == null) {
throw new IllegalArgumentException("project must not be NULL.");
}
assertWorkspaceOpen();
if ( ! doesProjectExist( project.getName() ) ) {
throw new IllegalArgumentException("Project '"+project.getName()+"' not registered?");
}
project.getConfiguration().save();
}
@Override
public void resourceChanged(final IAssemblyProject project , final IResource resource)
{
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener) {
listener.resourceChanged( project , resource );
}
@Override
public String toString() {
return "RESOURCE-CHANGED: "+resource;
}
});
}
protected interface IInvoker {
public void invoke(IResourceListener listener);
}
private void notifyListeners(IInvoker invoker)
{
if ( DEBUG_EVENTS ) {
System.out.println( invoker.toString() );
}
final List<IResourceListener> copy;
synchronized (listeners) {
copy = new ArrayList<IResourceListener>( this.listeners );
}
for ( IResourceListener l : copy ) {
try {
invoker.invoke( l );
}
catch(Exception e) {
e.printStackTrace();
}
}
}
@Override
public void addWorkspaceListener(IWorkspaceListener listener)
{
addResourceListener( listener );
}
@Override
public void removeWorkspaceListener(IWorkspaceListener listener)
{
removeResourceListener( listener );
}
@Override
public void reloadWorkspace() throws IOException
{
loadProjects();
}
@Override
public IAssemblyProject getProjectByName(String name)
{
assertWorkspaceOpen();
for ( IAssemblyProject p : projects )
{
if ( p.getName().equals( name ) ) {
return p;
}
}
throw new NoSuchElementException("Found no project named '"+name+"'");
}
@Override
public void close() throws IOException
{
System.out.println("Closing workspace...");
try
{
if ( this.opened.compareAndSet( true , false ) )
{
for ( IAssemblyProject p : projects )
{
if ( p.isOpen() )
{
p.getConfiguration().save();
}
}
}
} finally {
getWorkspaceConfig().saveConfiguration();
}
}
@Override
public void open() throws IOException
{
reloadWorkspace();
}
@Override
public void compilationFinished(final IAssemblyProject project, final ICompilationUnit unit) {
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener) {
((IWorkspaceListener) listener).compilationFinished( project , unit );
}
}
@Override
public String toString() {
return "COMPILATION-FINISHED: "+project+" - "+unit;
}
});
}
@Override
public void resourceCreated(final IAssemblyProject project, final IResource resource) {
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener) {
listener.resourceCreated( project , resource );
}
@Override
public String toString() {
return "RESOURCE-CREATED: "+resource;
}
});
}
@Override
public void resourceDeleted(final IAssemblyProject project, final IResource resource)
{
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
listener.resourceDeleted( project , resource );
}
@Override
public String toString() {
return "RESOURCE-DELETED: "+resource;
}
});
}
@Override
public void addResourceListener(IResourceListener listener)
{
if (listener == null) {
throw new IllegalArgumentException("listener must not be NULL.");
}
synchronized (listeners)
{
if ( listener instanceof IAssemblyProject)
{
// add projects BEFORE any other listeners because
// IAssemblyProject#projectOpened() and
// IAssemblyProject#projectClosed() update internal state
// that may be checked by other IWorkspaceListener implementations
// when their projectOpened() / projectClosed() methods are invoked
listeners.add( 0 , listener );
return;
}
final IOrdered.Priority prio = getPriority(listener);
if ( prio != Priority.DONT_CARE )
{
for (int i = 0; i < listeners.size(); i++)
{
final IResourceListener existing = listeners.get(i);
if ( ! getPriority( existing ).isHigherThan( prio ) ) {
listeners.add( i , listener );
return;
}
}
}
listeners.add( listener );
}
}
private static IOrdered.Priority getPriority(IResourceListener listener) {
return (listener instanceof IOrdered) ? ((IOrdered) listener).getPriority() : Priority.DONT_CARE;
}
@Override
public void removeResourceListener(IResourceListener listener)
{
if (listener == null) {
throw new IllegalArgumentException("listener must not be NULL.");
}
synchronized (listeners) {
listeners.remove( listener );
}
}
@Override
public void buildStarted(final IAssemblyProject assemblyProject)
{
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener ) {
((IWorkspaceListener) listener).buildStarted( assemblyProject );
}
}
@Override
public String toString() {
return "BUILD-STARTED: "+assemblyProject;
}
});
}
@Override
public void buildFinished(final IAssemblyProject assemblyProject, final boolean buildSuccessful)
{
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener ) {
((IWorkspaceListener) listener).buildFinished( assemblyProject , buildSuccessful );
}
}
@Override
public String toString() {
return "BUILD-FINISHED: "+assemblyProject+" ( success = "+buildSuccessful+" )";
}
});
}
@Override
public void refreshProjects(Collection<IAssemblyProject> projects) throws IOException
{
if (projects == null) {
throw new IllegalArgumentException("project must not be NULL.");
}
for ( final IAssemblyProject p : projects )
{
if ( p.isOpen() )
{
LOG.info("refreshProjects(): Refreshing "+p);
ProjectConfiguration reloaded = new ProjectConfiguration( p.getConfiguration().getBaseDirectory() );
reloaded.load();
p.getConfiguration().populateFrom( reloaded );
p.reload();
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener ) {
((IWorkspaceListener) listener).projectConfigurationChanged( p );
}
}
@Override
public String toString() {
return "PROJECT-CONFIGURATION-CHANGED: "+p;
}
});
}
}
}
@Override
public void openProject(final IAssemblyProject project) {
try {
getWorkspaceConfig().projectOpened( project );
getWorkspaceConfig().saveConfiguration();
} catch (IOException e) {
LOG.error("closeProject(): Failed to update workspace configuration");
}
// nothing to do here since IAssemblyProject
// implements IWorkspaceListener#projectOpened() and
// will update it's internal state when it receives the message
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener ) {
((IWorkspaceListener) listener).projectOpened( project );
}
}
@Override
public String toString() {
return "PROJECT-OPENED: "+project;
}
});
}
@Override
public void closeProject(final IAssemblyProject project)
{
try {
getWorkspaceConfig().projectClosed( project );
getWorkspaceConfig().saveConfiguration();
}
catch (IOException e) {
LOG.error("closeProject(): Failed to update workspace configuration",e);
}
// nothing to do here since IAssemblyProject
// implements IWorkspaceListener#projectClosed() and
// will update it's internal state when it receives the message
notifyListeners( new IInvoker() {
@Override
public void invoke(IResourceListener listener)
{
if ( listener instanceof IWorkspaceListener ) {
((IWorkspaceListener) listener).projectClosed( project );
}
}
@Override
public String toString() {
return "PROJECT-CLOSED: "+project;
}
});
}
@Override
public IAssemblyProject importProject(File baseDirectory) throws IOException, ProjectAlreadyExistsException
{
if (baseDirectory == null) {
throw new IllegalArgumentException("baseDirectory must not be null");
}
Misc.checkFileExistsAndIsDirectory( baseDirectory , false );
if ( ! baseDirectory.getAbsolutePath().startsWith( getBaseDirectory().getAbsolutePath() ) )
{
throw new IllegalArgumentException("Folder "+baseDirectory.getAbsolutePath()+
" is not within the workspace folder "+getBaseDirectory().getAbsolutePath());
}
final ProjectConfiguration config = new ProjectConfiguration(baseDirectory);
config.load();
if ( doesProjectExist( config.getProjectName() ) ) {
throw new ProjectAlreadyExistsException(config.getProjectName() );
}
return internalAddProject( config , false );
}
@Override
public IAssemblyProject getProjectForResource(IResource resource) throws ProjectNotFoundException
{
for ( IAssemblyProject project : getAllProjects() )
{
if ( project.containsResource( resource ) ) {
return project;
}
}
throw new ProjectNotFoundException("Unable to find project that owns resource '"+resource.getIdentifier()+"' ("+resource+")");
}
}