blob: 3072da288f0a3964102f62559e8f3e9b551a546e [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 BEA Systems, Inc.
* 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:
* mkaufman@bea.com - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.apt.core.internal.generatedfile;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IProblemRequestor;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.internal.core.JavaProject;
/**
* Class for managing generated files
*/
public class GeneratedFileManager {
private final IProject _project;
// Use a weak hash map to allow file managers to get GC'ed if a project
// goes away
private static final Map<IProject, GeneratedFileManager> MANAGERS_MAP =
new WeakHashMap<IProject, GeneratedFileManager>();
/**
* Construction can only take place from within
* the factory method, getGeneratedFileManager().
*/
private GeneratedFileManager(final IProject project) {
_project = project;
}
private static void init()
{
_initialized = true;
IWorkspace workspace = ResourcesPlugin.getWorkspace();
int mask = IResourceChangeEvent.PRE_BUILD | IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.PRE_DELETE;
workspace.addResourceChangeListener( new ResourceChangedListener(), mask );
}
public static synchronized List<GeneratedFileManager> getGeneratedFileManagers() {
return new ArrayList(MANAGERS_MAP.values());
}
public static synchronized GeneratedFileManager getGeneratedFileManager(final IProject project)
{
if ( ! _initialized )
init();
GeneratedFileManager gfm = MANAGERS_MAP.get(project);
if (gfm != null)
return gfm;
gfm = new GeneratedFileManager(project);
MANAGERS_MAP.put(project, gfm);
return gfm;
}
/**
* Return the file and a flag indicating if the content was modified.
*
* @param parentFile
* @param typeName
* @param contents
* @param progressMonitor
* @param charsetName
* @return - the newly created IFile along with whether it was modified
* @throws CoreException
* @throws UnsupportedEncodingException
*/
public synchronized FileGenerationResult generateFileDuringBuild(
IFile parentFile,
IJavaProject javaProject,
String typeName,
String contents,
IProgressMonitor progressMonitor,
String charsetName )
throws CoreException, UnsupportedEncodingException
{
try
{
IProject project = javaProject.getProject();
// create folder for generated source files
IFolder folder = project.getFolder( GENERATED_SOURCE_FOLDER_NAME );
if (!folder.exists())
folder.create(true, false, null);
//
// make sure __generated_src dir is on the cp if not already
//
updateProjectClasspath( (JavaProject)javaProject, folder, progressMonitor );
// split the type name into its parts
String[] parts = typeName.split( "\\.");
// create folders for the package parts
int i = 0;
for ( ;i < parts.length - 1; i++ )
{
folder = folder.getFolder( parts[i] );
if ( !folder.exists() )
folder.create( true, false, null );
}
String fileName = parts[i] + ".java";
IFile file = folder.getFile( fileName );
byte[] bytes;
if ( charsetName == null || charsetName == "" )
bytes = contents.getBytes();
else
bytes = contents.getBytes( charsetName );
InputStream is = new ByteArrayInputStream( bytes );
boolean contentsDiffer = true;
if ( !file.exists() )
{
file.create( is, true, progressMonitor );
}
else
{
// Check if the content has changed
InputStream oldData = null;
try {
oldData = new BufferedInputStream(file.getContents());
contentsDiffer = !compareStreams(oldData, is);
}
catch (CoreException ce) {
// Do nothing. Assume the new content is different
}
finally {
is.reset();
if (oldData != null) {
try {
oldData.close();
}
catch (IOException ioe)
{}
}
}
if (contentsDiffer) {
makeReadOnly( file, false );
file.setContents( is, true, true, progressMonitor );
}
}
file.setDerived( true );
makeReadOnly( file, true );
updateFileMaps( typeName, parentFile, file );
return new FileGenerationResult(file, contentsDiffer);
}
catch ( Throwable t )
{
t.printStackTrace();
}
return null;
}
/**
* Return true if the content of the streams is identical,
* false if not.
*/
private static boolean compareStreams(InputStream is1, InputStream is2) {
try {
int b1 = is1.read();
while(b1 != -1) {
int b2 = is2.read();
if(b1 != b2) {
return false;
}
b1 = is1.read();
}
int b2 = is2.read();
if(-1 != b2) {
return false;
}
return true;
}
catch (IOException ioe) {
return false;
}
}
/**
* TODO: figure out how to create a working copy with a client-specified character set
*
*
* @param parentCompilationUnit
* @param typeName
* @param contents
* @param workingCopyOwner
* @param problemRequestor
* @param progressMonitor
* @return
*/
public synchronized FileGenerationResult generateFileDuringReconcile(
ICompilationUnit parentCompilationUnit, String typeName,
String contents, WorkingCopyOwner workingCopyOwner,
IProblemRequestor problemRequestor, IProgressMonitor progressMonitor )
{
ICompilationUnit workingCopy = null;
FileGenerationResult result = null;
try
{
//
// get working copy (either from cache or create a new one)
//
workingCopy = getCachedWorkingCopy( parentCompilationUnit, typeName );
if ( workingCopy == null )
{
// create a new working copy
workingCopy = createNewWorkingCopy(
parentCompilationUnit, typeName, contents,
workingCopyOwner, problemRequestor, progressMonitor);
workingCopy.reconcile(AST.JLS3, true, workingCopyOwner,
progressMonitor);
result = new FileGenerationResult((IFile)workingCopy.getResource(), true);
}
else
{
//
// Update working copy's buffer with the contents of the type
//
boolean modified = updateWorkingCopy( contents, workingCopy, workingCopyOwner, progressMonitor );
result = new FileGenerationResult((IFile)workingCopy.getResource(), modified);
}
return result;
}
catch (JavaModelException jme)
{
jme.printStackTrace();
}
catch (CoreException ce)
{
ce.printStackTrace();
}
return new FileGenerationResult((IFile)workingCopy.getResource(), true);
}
public synchronized boolean isGeneratedFile( IFile f )
{
Set<IFile> s = _derivedFile2Parents.get( f );
if ( s == null || s.isEmpty() )
return false;
else
return true;
}
public synchronized boolean isParentFile( IFile f )
{
Set<IFile> s = _parent2DerivedFiles.get( f );
if ( s == null || s.isEmpty() )
return false;
else
return true;
}
/**
* @param parent
* @return set of Strings which are the type names known to be generated
* by the specified parent.
*/
public synchronized Set<String> getGeneratedTypesForParent( IFile parent )
{
Set<String> s = _parent2TypeNames.get( parent );
if ( s == null )
s = Collections.emptySet();
return s;
}
/**
*
* @param parent
* @return Set of IFile instances that are the files known to be generated
* by this parent
*/
public synchronized Set<IFile> getGeneratedFilesForParent( IFile parent )
{
Set<IFile> s = _parent2DerivedFiles.get( parent );
if (s == null )
s = Collections.emptySet();
return s;
}
public synchronized void discardGeneratedWorkingCopy( String typeName, ICompilationUnit parentCompilationUnit )
throws JavaModelException
{
discardGeneratedWorkingCopy( typeName, parentCompilationUnit, true );
}
private void discardGeneratedWorkingCopy( String typeName, ICompilationUnit parentCompilationUnit, boolean deleteFromParent2TypeNames )
throws JavaModelException
{
if ( deleteFromParent2TypeNames )
{
Set<String> typeNames = _parent2TypeNames.get( parentCompilationUnit.getResource() );
if ( typeNames == null ) throw new RuntimeException( "Unexpected null entry in _parent2TypeNames map.");
if ( ! typeNames.contains( typeName )) throw new RuntimeException ("type names set didn't contain expected value");
typeNames.remove( typeName );
}
Set<ICompilationUnit> parents = _typeName2Parents.get( typeName );
// TODO: change these to assertions
if ( parents == null ) throw new RuntimeException( "parents == null and it shouldnt");
if ( ! parents.contains( parentCompilationUnit )) throw new RuntimeException("parents set should contain parentCompilationUnit");
parents.remove( parentCompilationUnit );
if ( parents.size() == 0 )
{
ICompilationUnit cu = _typeName2WorkingCopy.get( typeName );
if ( cu == null ) throw new RuntimeException( "compilation unit is null and it shouldn't be");
_typeName2WorkingCopy.remove( typeName );
cu.discardWorkingCopy();
}
}
public synchronized void parentWorkingCopyDiscarded( ICompilationUnit parentCompilationUnit )
throws JavaModelException
{
Set<String> typeNames = _parent2TypeNames.get( parentCompilationUnit.getResource() );
if ( typeNames == null || typeNames.size() == 0 )
return;
Iterator<String> it = typeNames.iterator();
while ( it.hasNext() )
{
String typeName = it.next();
it.remove();
discardGeneratedWorkingCopy( typeName, parentCompilationUnit, false );
}
}
public synchronized void parentFileDeleted( IFile parent, IProgressMonitor monitor )
throws CoreException
{
Set<IFile> derivedFiles = _parent2DerivedFiles.get( parent );
Iterator<IFile> it = derivedFiles.iterator();
while ( it.hasNext() )
{
IFile generatedFile = it.next();
it.remove();
deleteGeneratedFile( generatedFile, parent, monitor, false );
}
}
public synchronized boolean deleteGeneratedFile(IFile fileToDelete, IFile parent, IProgressMonitor progressMonitor )
throws CoreException
{
return deleteGeneratedFile( fileToDelete, parent, progressMonitor, true );
}
private boolean deleteGeneratedFile(IFile fileToDelete, IFile parent, IProgressMonitor progressMonitor, boolean deleteFromParent2DerivedFiles )
throws CoreException
{
// update _parents2DerivedFiles map
if ( deleteFromParent2DerivedFiles )
{
Set<IFile> derivedFiles = _parent2DerivedFiles.get( parent );
// assertions
if ( derivedFiles == null ) throw new RuntimeException( "derivedFiles is null and it shouldn't be");
if ( ! derivedFiles.contains( fileToDelete )) throw new RuntimeException( "derivedFiles does not contain fileToDelete");
derivedFiles.remove( fileToDelete );
}
// update _derivedFile2Parents map and delete file if it has no other parents
Set<IFile> parents = _derivedFile2Parents.get( fileToDelete );
// assertions
if( parents == null ) throw new RuntimeException( " parents is null and it shouldn't be" );
if( ! parents.contains( parent )) throw new RuntimeException( "parents set does not contain parent" );
parents.remove( parent );
boolean deleted = false;
if ( parents.size() == 0 )
{
fileToDelete.delete( true, true, progressMonitor );
deleted = true;
}
return deleted;
}
public synchronized void generatedFileDeleted( IFile deletedFile, IProgressMonitor progressMonitor )
{
Set<IFile> parents = _derivedFile2Parents.get( deletedFile );
if ( parents == null || parents.isEmpty() )
return;
String typeName = getTypeNameForDerivedFile( deletedFile );
Iterator<IFile> it = parents.iterator();
while ( it.hasNext() )
{
IFile parent = it.next();
Set<IFile> s = _parent2DerivedFiles.get( parent );
s.remove( deletedFile );
Set<String> types = _parent2TypeNames.get( parent );
types.remove( typeName );
}
_derivedFile2Parents.remove( deletedFile );
_typeName2Parents.remove( typeName );
_typeName2WorkingCopy.remove( typeName );
}
/**
* given file f, return the typename corresponding to the file. This assumes
* that derived files use java naming rules (i.e., type "a.b.c" will be file
* "a/b/c.java".
*/
private String getTypeNameForDerivedFile( IFile f )
{
IPath p = f.getFullPath();
IProject project = f.getProject();
IFolder folder = project.getFolder( GENERATED_SOURCE_FOLDER_NAME );
IPath generatedSourcePath = folder.getFullPath();
int count = p.matchingFirstSegments( generatedSourcePath );
p = p.removeFirstSegments( count );
String s = p.toPortableString();
int idx = s.lastIndexOf( '.' );
s = p.toPortableString().replace( '/', '.' );
return s.substring( 0, idx );
}
//
// check cache to see if we already have a working copy
//
private ICompilationUnit getCachedWorkingCopy( ICompilationUnit parentCompilationUnit, String typeName )
{
ICompilationUnit workingCopy = (ICompilationUnit) _typeName2WorkingCopy.get( typeName );
if ( workingCopy != null )
updateMaps( typeName, parentCompilationUnit, workingCopy );
return workingCopy;
}
private ICompilationUnit createNewWorkingCopy(ICompilationUnit parentCompilationUnit, String typeName,
String contents, WorkingCopyOwner workingCopyOwner,
IProblemRequestor problemRequestor, IProgressMonitor progressMonitor)
throws CoreException, JavaModelException
{
IProject project = parentCompilationUnit.getResource().getProject();
JavaProject jp = (JavaProject) parentCompilationUnit.getJavaProject();
//
// create folder for generated source files
//
IFolder folder = project.getFolder( GENERATED_SOURCE_FOLDER_NAME );
project.refreshLocal(IResource.DEPTH_INFINITE, null);
if (!folder.exists())
folder.create(true, true, null);
//
// make sure __generated_src dir is on the cp if not already
//
updateProjectClasspath( jp, folder, progressMonitor );
//
// figure out package part of type & file name
//
String pkgName;
String fname;
int idx = typeName.lastIndexOf( '.' );
if ( idx > 0 )
{
pkgName = typeName.substring( 0, idx );
fname =
typeName.substring(idx + 1, typeName.length()) + ".java";
}
else
{
pkgName = "";
fname = typeName + ".java";
}
//
// create compilation unit
//
IPackageFragmentRoot root = jp.getPackageFragmentRoot(folder);
IPackageFragment pkgFragment =
root.createPackageFragment( pkgName, true, null );
ICompilationUnit cu = pkgFragment.getCompilationUnit( fname );
if ( cu == null || ! cu.getResource().exists() )
{
cu = pkgFragment.createCompilationUnit(
fname, contents, true, progressMonitor );
}
else
{
makeReadOnly( cu, false );
}
//
// TODO: can we call getWorkingCopy here?
//
cu.becomeWorkingCopy(problemRequestor, progressMonitor);
ICompilationUnit workingCopy = cu;
//
// update maps
//
updateMaps( typeName, parentCompilationUnit, workingCopy );
// we save this here since the resource has to exist on disk
workingCopy.commitWorkingCopy( true, progressMonitor );
//
// make the file derived so that it is not checked into source control.
//
makeDerived( workingCopy );
//
// make working copy read-only
//
makeReadOnly( workingCopy, true );
return workingCopy;
}
private void makeReadOnly( ICompilationUnit cu, boolean readOnly )
throws CoreException
{
IResource r = cu.getResource();
makeReadOnly( r, readOnly );
}
/**
* make the compilation unit read-only
*/
private void makeReadOnly( IResource r, boolean readOnly )
throws CoreException
{
if ( r.exists() )
{
ResourceAttributes ra = r.getResourceAttributes();
if (ra == null)
ra = new ResourceAttributes();
ra.setReadOnly( readOnly );
r.setResourceAttributes(ra);
}
}
private void makeDerived( ICompilationUnit cu )
throws CoreException
{
IResource r = cu.getResource();
if ( r.exists() )
r.setDerived( true );
}
/**
* Returns true if the file was modified
*/
private static boolean updateWorkingCopy(
String contents, ICompilationUnit workingCopy,
WorkingCopyOwner workingCopyOwner, IProgressMonitor progressMonitor )
throws JavaModelException
{
IBuffer b = workingCopy.getBuffer();
char[] oldBuf = b.getCharacters();
// Diff the contents, and only set if they differ
if (oldBuf.length == contents.length()) {
boolean contentsMatch = true;
for (int i=0; i<oldBuf.length; i++) {
if (oldBuf[i] != contents.charAt(i)) {
contentsMatch = false;
break;
}
}
if (contentsMatch) {
// No change, no need to update buffer
return false;
}
}
b.setContents(contents);
workingCopy.reconcile(AST.JLS3, true, workingCopyOwner,
progressMonitor);
return true;
}
private void updateMaps( String typeName, ICompilationUnit parentCompilationUnit, ICompilationUnit workingCopy )
{
IFile parentFile = (IFile) parentCompilationUnit.getResource();
IFile generatedFile = (IFile) workingCopy.getResource();
updateFileMaps( typeName, parentFile, generatedFile );
// type name -> set of parent compilation unit
Set<ICompilationUnit> s = _typeName2Parents.get( typeName );
if ( s == null )
{
s = new HashSet();
_typeName2Parents.put( typeName, s );
}
s.add( parentCompilationUnit );
// type name -> working copy
ICompilationUnit cu = (ICompilationUnit)_typeName2WorkingCopy.get( typeName );
if ( cu != null )
{
//assert( cu.equals( workingCopy ) ) : "unexpected different instances of working copy for the same type";
if ( !cu.equals(workingCopy) ) throw new RuntimeException( "unexpected different instances of working copy for the same type" );
}
else
_typeName2WorkingCopy.put( typeName, workingCopy );
}
private void updateFileMaps( String typeName, IFile parentFile, IFile generatedFile )
{
// parent IFile -> set of generated type name
Set<String> stringSet = _parent2TypeNames.get( parentFile );
if ( stringSet == null )
{
stringSet = new HashSet<String>();
_parent2TypeNames.put( parentFile, stringSet );
}
stringSet.add( typeName );
// add parent file -> set of derived files
Set<IFile> fileSet = _parent2DerivedFiles.get( parentFile );
if ( fileSet == null )
{
fileSet = new HashSet();
_parent2DerivedFiles.put( parentFile, fileSet );
}
fileSet.add( generatedFile );
// add derived file -> set of parent files
fileSet = _derivedFile2Parents.get( generatedFile );
if ( fileSet == null )
{
fileSet = new HashSet();
_derivedFile2Parents.put( generatedFile, fileSet );
}
fileSet.add( parentFile );
}
private void updateProjectClasspath( JavaProject jp, IFolder folder, IProgressMonitor progressMonitor )
throws JavaModelException
{
IClasspathEntry[] cp = jp.getRawClasspath();
IClasspathEntry generatedSourceClasspathEntry =
JavaCore.newSourceEntry(folder.getFullPath());
boolean found = false;
for (int i = 0; i < cp.length; i++)
{
if (cp[i].equals(generatedSourceClasspathEntry))
{
found = true;
break;
}
}
if (!found)
{
IClasspathEntry[] newCp = new IClasspathEntry[cp.length + 1];
System.arraycopy(cp, 0, newCp, 0, cp.length);
newCp[newCp.length - 1] = generatedSourceClasspathEntry;
jp.setRawClasspath(newCp, progressMonitor );
}
}
public synchronized void projectClosed()
{
// discard all working copies
Collection<ICompilationUnit> workingCopies = _typeName2WorkingCopy.values();
for ( ICompilationUnit wc : workingCopies )
{
try
{
wc.discardWorkingCopy();
}
catch ( JavaModelException jme )
{
jme.printStackTrace();
}
}
// clear out the working copy maps
_typeName2Parents.clear();
_typeName2WorkingCopy.clear();
}
public synchronized void projectClean( boolean deleteFiles )
{
projectClosed();
// delete the generated source dir
if ( deleteFiles )
{
IFolder f = _project.getFolder( GENERATED_SOURCE_FOLDER_NAME );
if ( f != null && f.exists() )
{
// delete the contents of the generated source folder, but don't delete
// the generated source folder because that will cause a classpath change,
// which will force the next build to be a full build.
try
{
IResource[] members = f.members();
for ( int i = 0; i<members.length; i++ )
members[i].delete( true, null );
}
catch ( CoreException ce )
{
ce.printStackTrace();
}
}
}
// clear out all the file maps
_parent2DerivedFiles.clear();
_derivedFile2Parents.clear();
_parent2TypeNames.clear();
}
public synchronized void projectDeleted()
{
//
// remove this project from the managers map. Some other clients may still
// have a reference to this, but that should be fine since the project is being
// deleted. We'll just empty out member fields rather than
// setting them to null to avoid NPEs.
//
synchronized( this.getClass() )
{
MANAGERS_MAP.remove( _project );
}
// TODO: eventually make this true. Right now, the resource tree is locked
// when we get the project-deleted event, so we can't delete any files.
projectClean( false );
}
/**
* map from IFile of parent file to Set <IFile>of derived files
*/
private Map<IFile, Set<IFile>> _parent2DerivedFiles = new HashMap();
/**
* map from IFile of dervied file to Set <IFile>of parent files
*/
private Map<IFile, Set<IFile>> _derivedFile2Parents = new HashMap();
/**
* map from IFile of parent working copy to Set
* <String> of type names generated by that file
*
* Map<IFile, Set<String>>
*/
private Map<IFile, Set<String>> _parent2TypeNames = new HashMap();
/**
* map from typename of generated file to Set<ICompilationUnit>of parent
* working copies
*
* Map<String, Set<ICompilationUnit>>
*/
private Map<String, Set<ICompilationUnit>> _typeName2Parents = new HashMap();
/**
* Map from type name to the working copy in memory of that type name
*
* Map<String, ICompilationUnit>
*/
private Map<String, ICompilationUnit> _typeName2WorkingCopy = new HashMap();
private static boolean _initialized = false;
private static final String GENERATED_SOURCE_FOLDER_NAME = "__generated_src";
}