/**
* 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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import de.codesourcery.jasm16.compiler.io.DefaultResourceMatcher;
import de.codesourcery.jasm16.compiler.io.FileResource;
import de.codesourcery.jasm16.compiler.io.FileResourceResolver;
import de.codesourcery.jasm16.compiler.io.IResource;
import de.codesourcery.jasm16.compiler.io.IResource.ResourceType;
import de.codesourcery.jasm16.compiler.io.IResourceMatcher;
import de.codesourcery.jasm16.compiler.io.IResourceResolver;
import de.codesourcery.jasm16.emulator.EmulationOptions;
import de.codesourcery.jasm16.exceptions.ResourceNotFoundException;
import de.codesourcery.jasm16.utils.Misc;
import de.codesourcery.jasm16.utils.Misc.IFileVisitor;
/**
* DCPU-16 assembly project.
*
* @author tobias.gierke@code-sourcery.de
*/
public class AssemblyProject extends WorkspaceListener implements IAssemblyProject
{
private static final Logger LOG = Logger.getLogger(AssemblyProject.class);
private static final IResourceMatcher resourceMatcher = new DefaultResourceMatcher();
private final ProjectConfiguration projectConfiguration;
private final AtomicBoolean registeredWithWorkspace = new AtomicBoolean(false);
private final Object RESOURCE_LOCK = new Object();
private IProjectBuilder projectBuilder;
// @GuardedBy( RESOURCE_LOCK )
private final List<IResource> resources = new ArrayList<IResource>();
private final IResourceResolver resolver;
private final IWorkspace workspace;
private boolean isOpen;
public AssemblyProject(IWorkspace workspace , ProjectConfiguration config,boolean isOpen) throws IOException
{
if (config == null) {
throw new IllegalArgumentException("config must not be NULL");
}
if ( workspace == null ) {
throw new IllegalArgumentException("workspace must not be NULL");
}
this.isOpen = isOpen;
this.workspace = workspace;
this.projectConfiguration = config;
resolver = new FileResourceResolver( projectConfiguration.getBaseDirectory() )
{
@Override
protected ResourceType determineResourceType(File file) {
if ( getConfiguration().isSourceFile( file ) ) {
return ResourceType.SOURCE_CODE;
}
return ResourceType.UNKNOWN;
}
@Override
protected File getBaseDirectory() {
return projectConfiguration.getBaseDirectory();
}
};
synchronized( RESOURCE_LOCK ) { // unnecessary since we're inside this classes constructor but makes FindBugs & PMD happy
resources.addAll( scanForResources() );
}
}
public void setProjectBuilder(IProjectBuilder projectBuilder) {
if (projectBuilder == null) {
throw new IllegalArgumentException("projectBuilder must not be null");
}
if ( this.projectBuilder != null ) {
throw new IllegalStateException("Project builder already set on "+this);
}
this.projectBuilder = projectBuilder;
}
public IProjectBuilder getProjectBuilder() {
return projectBuilder;
}
protected File getOutputFileForSource(IResource resource)
{
final String objectCodeFile = getNameWithoutSuffix( resource )+".dcpu16";
final File outputDir = getConfiguration().getOutputFolder();
return new File( outputDir , objectCodeFile );
}
protected String getNameWithoutSuffix(IResource resource) {
String name;
if ( resource instanceof FileResource) {
FileResource file = (FileResource) resource;
name = file.getFile().getName();
} else {
name = resource.getIdentifier();
}
// get base name
final String[] components = name.split("["+Pattern.quote("\\/")+"]");
if ( components.length == 1 ) {
name = components[0];
} else {
name = components[ components.length -1 ];
}
if ( ! name.contains("." ) ) {
return name;
}
final String[] dots = name.split("\\.");
return StringUtils.join( ArrayUtils.subarray( dots , 0 , dots.length-1) );
}
@Override
public void reload() throws IOException
{
final List<IResource> deletedResources=new ArrayList<IResource>();
final List<IResource> newResources= scanForResources();
synchronized( RESOURCE_LOCK ) { // unnecessary since we're inside this classes constructor but makes FindBugs & PMD happy
// find deleted resources
outer:
for ( IResource existing : resources )
{
for ( IResource r : newResources )
{
if ( resourceMatcher.isSame( existing , r ) ) {
continue outer;
}
}
deletedResources.add( existing );
}
// remove existing (=unchanged) resources
for ( Iterator<IResource> it=newResources.iterator() ; it.hasNext() ; )
{
final IResource newResource = it.next();
for ( IResource existingResource : resources ) {
if ( resourceMatcher.isSame( existingResource,newResource ) ) {
it.remove();
break;
}
}
}
}
for ( IResource deleted : deletedResources ) {
workspace.resourceDeleted( this , deleted );
}
for ( IResource added : newResources ) {
workspace.resourceCreated( this , added );
}
}
protected List<IResource> scanForResources() throws IOException {
final Map<String,IResource> result = new HashMap<String,IResource> ();
// scan files
final IFileVisitor visitor = new IFileVisitor()
{
private final ProjectConfiguration projConfig = getConfiguration();
@Override
public boolean visit(File file) throws IOException
{
if ( ! result.containsKey( file.getAbsolutePath() ) )
{
final ResourceType type;
// note: if clauses sorted by probability, most likely comes first
if ( projConfig.isSourceFile( file ) )
{
type = ResourceType.SOURCE_CODE;
}
else if ( ! ProjectConfiguration.isProjectConfigurationFile( file ) )
{
type = ResourceType.UNKNOWN;
} else {
type = ResourceType.PROJECT_CONFIGURATION_FILE;
}
final FileResource resource = new FileResource( file , type);
result.put( file.getAbsolutePath() , resource );
}
return true;
}
};
for ( File f : projectConfiguration.getBaseDirectory().listFiles() ) {
if ( ! visitor.visit( f ) ) {
break;
}
}
for ( File srcFolder : projectConfiguration.getSourceFolders() )
{
if ( srcFolder.exists() ) {
Misc.visitDirectoryTreePostOrder( srcFolder , visitor );
} else {
LOG.warn("scanForResources(): Missing source folder: "+srcFolder.getAbsolutePath());
}
}
// scan binary output folder
final File outputFolder = projectConfiguration.getOutputFolder();
if ( outputFolder.exists() )
{
final IFileVisitor executableVisitor = new IFileVisitor() {
@Override
public boolean visit(File file) throws IOException
{
if ( file.isFile() )
{
if ( file.getName().equals( projectConfiguration.getExecutableName() ) ) {
result.put( file.getAbsolutePath() , new FileResource( file , ResourceType.EXECUTABLE ) );
} else {
result.put( file.getAbsolutePath() , new FileResource( file , ResourceType.OBJECT_FILE ) );
}
}
return true;
}
};
Misc.visitDirectoryTreeInOrder( outputFolder , executableVisitor );
}
return new ArrayList<IResource>( result.values() );
}
@Override
public String getName()
{
return projectConfiguration.getProjectName();
}
@Override
public List<IResource> getAllResources()
{
synchronized( RESOURCE_LOCK ) {
return new ArrayList<IResource>( this.resources );
}
}
@Override
public IResource resolve(String identifier) throws ResourceNotFoundException
{
return resolver.resolve( identifier );
}
@Override
public IResourceResolver getResourceResolver()
{
return resolver;
}
@Override
public IResource resolveRelative(String identifier, IResource parent) throws ResourceNotFoundException {
return resolver.resolveRelative( identifier ,parent );
}
@Override
public ProjectConfiguration getConfiguration() {
return projectConfiguration;
}
@Override
public List<IResource> getResources(ResourceType type)
{
if (type == null) {
throw new IllegalArgumentException("type must not be NULL");
}
final List<IResource> result = new ArrayList<IResource>();
for ( IResource r : getAllResources() ) {
if ( r.hasType( type ) ) {
result.add( r );
}
}
return result;
}
protected void handleResourceDeleted(IResource resource)
{
if (resource == null) {
throw new IllegalArgumentException("resource must not be NULL");
}
for (Iterator<IResource> it = getAllResources().iterator(); it.hasNext();)
{
final IResource existing = it.next();
if ( existing.getIdentifier().equals( resource.getIdentifier() ) )
{
it.remove();
break;
}
}
}
protected void cleanOutputFolder() throws IOException
{
File folder = getConfiguration().getOutputFolder();
if ( ! folder.exists() ) {
if ( ! folder.mkdirs() ) {
throw new IOException("Failed to create output folder "+folder.getAbsolutePath());
}
return;
}
for ( File f : folder.listFiles() )
{
Misc.deleteRecursively( f );
workspace.resourceDeleted( this , new FileResource( f , ResourceType.UNKNOWN) );
}
}
@Override
public IResource lookupResource(String identifier)
{
for ( IResource r : getAllResources() ) {
if ( r.getIdentifier().equals( identifier ) ) {
return r;
}
}
throw new NoSuchElementException("Unable to find resource '"+identifier+" in project "+this);
}
@Override
public void resourceChanged(IAssemblyProject project, IResource resource) {
if ( this != project) {
return;
}
IResource found=null;
synchronized( RESOURCE_LOCK )
{
for ( IResource r : getAllResources() )
{
if ( resourceMatcher.isSame( r , resource ) ) {
found = r;
break;
}
}
}
if ( found == null ) {
return;
}
}
@Override
public void resourceCreated(IAssemblyProject project, IResource resource)
{
if ( this != project) {
return;
}
if ( resource instanceof FileResource)
{
if ( ((FileResource) resource).getFile().isDirectory() )
{
return; // we don't care about directories
}
synchronized( RESOURCE_LOCK )
{
for ( IResource r : getAllResources() ) {
if ( resourceMatcher.isSame( r, resource ) ) // resource update
{
resources.remove( r );
resources.add( resource );
return;
}
}
if ( resource.hasType( ResourceType.EXECUTABLE ) ) {
synchronized( RESOURCE_LOCK )
{
for ( IResource r : getAllResources() ) {
if ( r.hasType( ResourceType.EXECUTABLE ) ) {
throw new IllegalArgumentException("Cannot add executable "+resource+" to project "+this+" , already has executable "+r);
}
}
}
}
resources.add( resource );
}
}
}
@Override
public void resourceDeleted(IAssemblyProject project, IResource resource) {
if ( this != project) {
return;
}
synchronized( RESOURCE_LOCK )
{
for (Iterator<IResource> it = resources.iterator(); it.hasNext();) {
IResource existing = it.next();
if ( resourceMatcher.isSame( existing,resource ) )
{
it.remove();
return;
}
}
}
}
@Override
public IResource getResourceForFile(File file)
{
for ( IResource r : getAllResources() ) {
if ( r instanceof FileResource) {
if ( ((FileResource) r).getFile().getAbsolutePath().equals( file.getAbsolutePath() ) ) {
return r;
}
}
}
return null;
}
@Override
public boolean isSame(IAssemblyProject other)
{
if ( other == this ) {
return true;
}
if ( other == null ) {
return false;
}
if ( this.getName().equals( other.getName() ) ) {
return true;
}
return false;
}
@Override
public boolean isOpen() {
return isOpen;
}
@Override
public boolean isClosed() {
return !isOpen;
}
@Override
public void projectCreated(IAssemblyProject project) { /* sooo not interested */ }
@Override
public void projectClosed(IAssemblyProject project) {
if ( project == this ) {
this.isOpen = false;
}
}
@Override
public void projectOpened(IAssemblyProject project)
{
if ( project == this ) {
this.isOpen = true;
}
}
@Override
public void projectConfigurationChanged(IAssemblyProject project) {
}
@Override
public void projectDeleted(IAssemblyProject project) { /* sooo not interested */ }
@Override
public void projectDisposed(IAssemblyProject project)
{
}
@Override
public void buildStarted(IAssemblyProject project) { /* sooo not interested */ }
@Override
public void buildFinished(IAssemblyProject project, boolean success) { /* sooo not interested */ }
@Override
public String toString()
{
return getConfiguration().getProjectName();
}
@Override
public EmulationOptions getEmulationOptions() {
return getConfiguration().getEmulationOptions();
}
@Override
public void setEmulationOptions(EmulationOptions emulationOptions) {
getConfiguration().setEmulationOptions( emulationOptions );
}
@Override
public boolean containsResource(IResource resource)
{
synchronized(RESOURCE_LOCK) {
for ( IResource existing : resources )
{
if ( resourceMatcher.isSame( existing , resource ) ) {
return true;
}
}
}
return false;
}
@Override
public void addedToWorkspace(IWorkspace workspace)
{
if ( workspace != this.workspace ) {
throw new IllegalStateException("Project "+this+" attached to different workspace?");
}
if ( ! registeredWithWorkspace.compareAndSet(false,true) )
{
throw new IllegalStateException("addedToWorkspace() called on already registered project "+this);
}
workspace.addResourceListener( this );
for ( IResource r : getAllResources() ) {
workspace.resourceCreated( this , r );
}
}
@Override
public void removedFromWorkspace(IWorkspace workspace)
{
if ( workspace != this.workspace ) {
throw new IllegalStateException("Project "+this+" attached to different workspace?");
}
if ( ! registeredWithWorkspace.compareAndSet(true,false) )
{
throw new IllegalStateException("removedFromWorkspace() called on detached project "+this);
}
workspace.removeResourceListener( this );
}
}