/** * 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+")"); } }