/* * Copyright 2013 Guidewire Software, Inc. */ package gw.internal.gosu.parser; import gw.config.CommonServices; import gw.fs.IDirectory; import gw.fs.IDirectoryUtil; import gw.fs.IFile; import gw.fs.IResource; import gw.lang.parser.FileSource; import gw.lang.parser.ISource; import gw.lang.reflect.ITypeLoader; import gw.lang.reflect.RefreshKind; import gw.lang.reflect.RefreshRequest; import gw.lang.reflect.gs.ClassType; import gw.lang.reflect.gs.GosuClassTypeLoader; import gw.lang.reflect.gs.IFileSystemGosuClassRepository; import gw.lang.reflect.gs.IGosuProgram; import gw.lang.reflect.gs.ISourceFileHandle; import gw.lang.reflect.gs.TypeName; import gw.lang.reflect.module.IModule; import gw.util.DynamicArray; import gw.util.Pair; import gw.util.StreamUtil; import gw.util.cache.FqnCache; import java.io.File; import java.io.IOException; import java.io.Reader; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; /** */ public class FileSystemGosuClassRepository implements IFileSystemGosuClassRepository { private final Map<String, FqnCache> _missCaches = new HashMap<String, FqnCache>(); public static final String RESOURCE_LOCATED_W_CLASSES = "gw/config/default.xml"; private final IModule _module; // Source paths private List<ClassPathEntry> _sourcePath = new CopyOnWriteArrayList<ClassPathEntry>(); private String[] _extensions = new String[0]; // Types and packages in the source paths private PackageToClassPathEntryTreeMap _rootNode; private Set<String> _allTypeNames; public FileSystemGosuClassRepository(IModule module) { _module = module; _extensions = GosuClassTypeLoader.ALL_EXTS; } @Override public IModule getModule() { return _module; } @Override public IDirectory[] getSourcePath() { IDirectory[] sourcepath = new IDirectory[_sourcePath.size()]; int i = 0; for( ClassPathEntry e : _sourcePath) { sourcepath[i++] = e.getPath(); } return sourcepath; } @Override public void setSourcePath(IDirectory[] sourcePath) { if( areDifferent( sourcePath, _sourcePath) ) { _sourcePath.clear(); Set<String> extensions = new HashSet<String>(); extensions.add(".java"); extensions.add(".xsd"); extensions.addAll(Arrays.asList(GosuClassTypeLoader.ALL_EXTS)); // Scan for potential extensions for( IDirectory file : sourcePath ) { _sourcePath.add( new ClassPathEntry( file, isTestFolder(file)) ); } _extensions = extensions.toArray(new String[extensions.size()]); reset(); } } @Override public ISourceFileHandle findClass(String strQualifiedClassName, String[] extensions) { IClassFileInfo fileInfo = findFileInfoOnDisk( strQualifiedClassName, extensions ); return fileInfo != null ? fileInfo.getSourceFileHandle() : null; } @Override public URL findResource( String resourceName ) { if( resourceName == null ) { return null; } resourceName = resourceName.replace( '/', '.' ); if( inMissCache( resourceName, _extensions) ) { return null; } if( resourceName.endsWith( "." ) ) { // Handle case where someone types "com.abc.Foo.". Otherwise it would be // treated as if the last dot was not there using the tokenizer. return null; } PackageToClassPathEntryTreeMap aPackage = getCachedPackage( resourceName ); URL resource = aPackage == null ? null : aPackage.resolveToResource( resourceName ); if( resource == null ) { addToMissCache( resourceName, _extensions); } return resource; } @Override public Set<String> getAllTypeNames() { if (_allTypeNames == null) { Set<String> classNames = new HashSet<String>(); for( ClassPathEntry path : _sourcePath) { addTypeNames(path.getPath(), path.getPath(), classNames, _extensions); } _allTypeNames = classNames; } return _allTypeNames; } @Override public Set<String> getAllTypeNames(String... extensions) { Set<String> enhancementNames = new HashSet<String>(); for( ClassPathEntry path : _sourcePath) { addTypeNames(path.getPath(), path.getPath(), enhancementNames, extensions); } return enhancementNames; } @Override public String getClassNameFromFile( IDirectory root, IFile file, String[] fileExts ) { String strClassPath = root.getPath().getFileSystemPathString() + File.separatorChar; String strQualifiedClassName = file.getPath().getFileSystemPathString().substring( strClassPath.length() ); if( !Util.isClassFileName( strQualifiedClassName, fileExts ) ) { String strExts = ""; for( String strExt : fileExts ) { strExts += " " + strExt; } throw new IllegalArgumentException( file.getPath().getName() + " is not a legal Gosu class name. It does not end with [" + strExts.trim() + "]" ); } strQualifiedClassName = strQualifiedClassName.substring( 0, strQualifiedClassName.lastIndexOf( '.' ) ); strQualifiedClassName = strQualifiedClassName.replace( '/', '.' ).replace( '\\', '.' ); if( strQualifiedClassName.startsWith( "." ) ) { strQualifiedClassName = strQualifiedClassName.substring( 1 ); } return strQualifiedClassName; } @Override public void typesRefreshed(RefreshRequest request) { synchronized (_missCaches) { if (request == null) { reset(); } else { for (FqnCache cache : _missCaches.values()) { cache.remove(request.types); } if (request.kind == RefreshKind.CREATION) { for (String type : request.types) { // cannot make this call because we have no way of finding out the right package for inner types // addToPackageCache(type, request.file); if (_allTypeNames != null) { _allTypeNames.add(type); } } } else if (request.kind == RefreshKind.DELETION) { if (_allTypeNames != null) { for (String type : request.types) { _allTypeNames.remove(type); } } } } } } private void reset() { synchronized (_missCaches) { _missCaches.clear(); _rootNode = null; _allTypeNames = null; } } private void addToPackageCache(String fqn, IResource file) { final ClassPathEntry classPathEntry = findClassPathEntry(file); if (_rootNode != null && classPathEntry != null) { PackageToClassPathEntryTreeMap node = _rootNode; while (fqn != null) { int i = fqn.indexOf('.'); String segment = fqn.substring(0, i < 0 ? fqn.length() : i); PackageToClassPathEntryTreeMap child = node.getChild(segment); if (child != null) { child.addClassPathEntry(classPathEntry); node = child; } else { node = node.createChildForDir(classPathEntry, segment); } if (i < 0) { break; } fqn = i + 1 < fqn.length() ? fqn.substring(i + 1) : null; } for( FqnCache cache : _missCaches.values() ) { cache.remove( fqn ); } } } private ClassPathEntry findClassPathEntry(IResource file) { for (ClassPathEntry classPathEntry : _sourcePath) { if (file.isDescendantOf(classPathEntry.getPath())) { return classPathEntry; } } return null; } private void removeFromPackageCache( String fqn, IDirectory dir ) { PackageToClassPathEntryTreeMap thePackage = getCachedPackageCorrectly(fqn); if (thePackage != null) { //## todo: the package could be split, we need to remove the directory // and only if its the last one them remove the package thePackage.delete( dir ); } } private synchronized PackageToClassPathEntryTreeMap getCachedPackageCorrectly(String fullyQualifiedName) { if (_rootNode == null) { _rootNode = loadPackageRoots(); } PackageToClassPathEntryTreeMap currNode = _rootNode; int iRelativeNameIndex = 0; while (iRelativeNameIndex != -1) { int iNextDot = fullyQualifiedName.indexOf('.', iRelativeNameIndex); String strRelativeName = fullyQualifiedName.substring(iRelativeNameIndex, iNextDot == -1 ? fullyQualifiedName.length() : iNextDot); iRelativeNameIndex = iNextDot == -1 ? -1 : iNextDot + 1; PackageToClassPathEntryTreeMap newNode = getChildPackage(currNode, strRelativeName); if (newNode == null) { return null; } currNode = newNode; } return currNode == _rootNode ? null : currNode; } private synchronized PackageToClassPathEntryTreeMap getCachedPackage( String fullyQualifiedName ) { if( _rootNode == null ) { _rootNode = loadPackageRoots(); } if( fullyQualifiedName.equals( "" ) ) { return _rootNode; } PackageToClassPathEntryTreeMap currNode = _rootNode; int iRelativeNameIndex = 0; while( iRelativeNameIndex != -1 ) { int iNextDot = fullyQualifiedName.indexOf( '.', iRelativeNameIndex ); String strRelativeName = fullyQualifiedName.substring( iRelativeNameIndex, iNextDot == -1 ? fullyQualifiedName.length() : iNextDot ); iRelativeNameIndex = iNextDot == -1 ? -1 : iNextDot + 1; PackageToClassPathEntryTreeMap newNode = getChildPackage( currNode, strRelativeName ); if( newNode == null ) { break; } currNode = newNode; } return currNode == _rootNode ? null : currNode; } private PackageToClassPathEntryTreeMap getChildPackage( PackageToClassPathEntryTreeMap parent, String strRelativeName ) { PackageToClassPathEntryTreeMap child; if( parent == _rootNode && strRelativeName.equals( "Libraries" ) ) { // Hack to support mixed case access to the "libraries" package. // Libaries used to be a global symbol with name "Libraries", so // there used to be a lot of access by that name. child = parent.getChild( "libraries" ); if( child == null ) { return null; } } else { child = parent.getChild( strRelativeName ); } return child; } private PackageToClassPathEntryTreeMap loadPackageRoots() { PackageToClassPathEntryTreeMap root = new PackageToClassPathEntryTreeMap( null, "", _module ); PackageToClassPathEntryTreeMap gw = root.createChildForDir( null, "gw" ); gw.createChildForDir( null, "lang" ); gw.createChildForDir( null, "util" ); root.createChildForDir(null, IGosuProgram.PACKAGE); for( ClassPathEntry dir : _sourcePath) { root.addClassPathEntry( dir ); processDirectory( root, dir, dir.getPath() ); } return root; } private void processDirectory(PackageToClassPathEntryTreeMap node, IFileSystemGosuClassRepository.ClassPathEntry entry, IDirectory path) { IDirectory entryPath = entry.getPath(); if (entryPath.equals(path) || !CommonServices.getPlatformHelper().isPathIgnored(entryPath.relativePath(path))) { List<? extends IDirectory> dirs = path.listDirs(); for (IDirectory dir : dirs) { if (isValidDirectory(dir)) { PackageToClassPathEntryTreeMap child = node.createChildForDir(entry, dir.getName()); processDirectory(child, entry, dir); } } } } private boolean isValidDirectory(IDirectory dir) { return !dir.getName().equals("META-INF"); } private ClassFileInfo findFileInfoOnDisk( String strQualifiedClassName, String[] extensions ) { if( inMissCache( strQualifiedClassName, extensions ) ) { return null; } if( strQualifiedClassName == null ) { return null; } if( strQualifiedClassName.endsWith( "." ) ) { // Handle case where someone types "com.abc.Foo.". Otherwise it would be // treated as if the last dot was not there using the tokenizer. return null; } ClassFileInfo info = null; String packageName = getPackageName(strQualifiedClassName); PackageToClassPathEntryTreeMap aPackage = getCachedPackage(packageName); if( aPackage != null ) { info = aPackage.resolveToClassFileInfo( strQualifiedClassName, extensions ); } if( info == null ) { addToMissCache( strQualifiedClassName, extensions ); } return info; } private String getPackageName(String strQualifiedClassName) { int i = strQualifiedClassName.lastIndexOf('.'); return i < 0 ? "" : strQualifiedClassName.substring(0, i); } private boolean inMissCache(String strQualifiedClassName, String[] extensions) { synchronized( _missCaches ) { // Note we check for TRUE because it can happen that a subordinate type like Foo<BadType> is a miss, while Foo is not a miss for (String extension : extensions) { Object value = getMissCacheForExtension(extension).get(strQualifiedClassName); if (value != Boolean.TRUE) { return false; } } return true; } } private FqnCache getMissCacheForExtension(String extension) { FqnCache cache = _missCaches.get(extension); if (cache == null) { cache = new FqnCache(); _missCaches.put(extension, cache); } return cache; } private void addToMissCache(String strQualifiedClassName, String[] extensions) { synchronized( _missCaches ) { for (String extension : extensions) { getMissCacheForExtension(extension).add(strQualifiedClassName, Boolean.TRUE); } } } private void addTypeNames( final IDirectory root, IDirectory path, final Set<String> classNames, final String[] fileExts ) { DynamicArray<? extends IFile> iFiles = IDirectoryUtil.allContainedFilesExcludingIgnored(path); for (int i = 0; i < iFiles.size; i++) { IFile file = (IFile) iFiles.data[i]; if (Util.isClassFileName(file.getName(), fileExts)) { String className = getClassNameFromFile(root, file, fileExts); classNames.add(className); } } } public static final class FileSystemSourceFileHandle implements ISourceFileHandle { ISource _source; boolean _isTestClass; private int _classPathLength; private ClassFileInfo _fileInfo; private int _iOffset; private int _iEnd; public FileSystemSourceFileHandle( ClassFileInfo fileInfo, boolean isTestClass ) { _isTestClass = isTestClass; _fileInfo = fileInfo; _classPathLength = fileInfo.getClassPathLength(); } @Override public ISource getSource() { if( _source == null ) { _source = new FileSource(_fileInfo); } return _source; } @Override public String getParentType() { return null; } @Override public String getNamespace() { String namespace = _fileInfo.getFilePath().substring(_fileInfo.getEntry().getPath().toString().length() + 1); int fileSeparatorIndex = namespace.lastIndexOf(File.separatorChar); if (fileSeparatorIndex >= 0) { namespace = namespace.substring(0, fileSeparatorIndex); namespace = namespace.replace(File.separatorChar, '.'); } else { namespace = "default"; } return namespace; } @Override public String getFilePath() { return _fileInfo.getFilePath(); } @Override public IFile getFile() { return _fileInfo.getFile(); } @Override public boolean isTestClass() { return _isTestClass; } @Override public boolean isValid() { return true; } @Override public void cleanAfterCompile() { _source = null; } public ClassType getClassType() { String name = _fileInfo.getNonCanonicalFileName(); return ClassType.getFromFileName(name); } @Override public String getTypeNamespace() { String path = _fileInfo.getFilePath().replace( '/', '.' ).replace( '\\', '.' ); int startPos = _classPathLength; // this is a hack to support files inside jars in IJ if( path.charAt( startPos ) == '!' ) { ++startPos; } if( path.charAt( startPos ) == '.' ) { ++startPos; } int endPos = path.length() - (_fileInfo.getFileName().length() + 1); return endPos < startPos ? "" : path.substring( startPos, endPos ); } @Override public String getRelativeName() { String name = _fileInfo.getFileName(); return name.substring( 0, name.lastIndexOf( '.' ) ); } // @Override // public IDirectory getParentFile() // { // return _fileInfo.getParentFile(); // } // // @Override // public ClassFileInfo getFileInfo() // { // return _fileInfo; // } @Override public void setOffset( int iOffset ) { _iOffset = iOffset; } @Override public int getOffset() { return _iOffset; } @Override public void setEnd( int iEnd ) { _iEnd = iEnd; } @Override public int getEnd() { return _iEnd; } @Override public String getFileName() { String strFile = this.getFilePath(); int iIndex = strFile.lastIndexOf( File.separatorChar ); if( iIndex >= 0 ) { strFile = strFile.substring( iIndex + 1 ); } return strFile; } @Override public String toString() { return _fileInfo.getFilePath(); } } public static class ClassFileInfo implements IFileSystemGosuClassRepository.IClassFileInfo { private ClassPathEntry _entry; private ClassType _classType; private String _fileType; private List<String> _innerClassParts; private boolean _isTestClass; private IFile _file; public ClassFileInfo( ClassPathEntry entry, IFile file, boolean isTestClass ) { _entry = entry; _file = file; _classType = _file.getExtension().equalsIgnoreCase("java") ? ClassType.JavaClass : ClassType.Class; _innerClassParts = Collections.emptyList(); _isTestClass = isTestClass; _file = file; } public ClassFileInfo( ISourceFileHandle outerSfh, ClassType classType, String fileType, List<String> innerClassParts, boolean isTestClass ) { _classType = classType; _fileType = fileType; _innerClassParts = innerClassParts; _isTestClass = isTestClass; _file = outerSfh.getFile(); } @Override public IFile getFile() { return _file; } @Override public boolean hasInnerClass() { return _innerClassParts.size() > 0; } @Override public ISourceFileHandle getSourceFileHandle() { if( hasInnerClass() ) { String enclosingType = _fileType; for( int i = 0; i < _innerClassParts.size() - 1; i++ ) { enclosingType += "." + _innerClassParts.get( i ); } return new InnerClassFileSystemSourceFileHandle(_classType, enclosingType, _innerClassParts.get( _innerClassParts.size() - 1 ), _isTestClass ); } return new FileSystemSourceFileHandle( this, _isTestClass ); } @Override public ClassPathEntry getEntry() { return _entry; } @Override public IDirectory getParentFile() { return _file.getParent(); } @Override public Reader getReader() { try { return StreamUtil.getInputStreamReader(_file.openInputStream()); } catch (IOException e) { throw new RuntimeException(e); } } @Override public String getFileName() { return _file.getName(); } @Override public String getNonCanonicalFileName() { return _file.getName(); } @Override public String getFilePath() { return _file.getPath().getFileSystemPathString(); } @Override public int getClassPathLength() { return getEntry().getPath().getPath().getFileSystemPathString().length(); } @Override public String getContent() { Reader reader = getReader(); try { char[] buf = new char[1024]; StringBuilder stringBuilder = new StringBuilder(); while (true) { int count = reader.read(buf); if (count < 0) { break; } stringBuilder.append(buf, 0, count); } char[] processedChars = new char[stringBuilder.length()]; int j = 0; for (int i = 0; i < processedChars.length; i++) { char c1 = stringBuilder.charAt(i); if (i < processedChars.length - 1 && c1 == '\r' && stringBuilder.charAt(i + 1) == '\n') { processedChars[j] = '\n'; i++; } else { processedChars[j] = c1; } j++; } String s = new String(processedChars, 0, j); return s; } catch( IOException e ) { throw new RuntimeException( e ); } finally { try { if (reader != null) { reader.close(); } } catch (IOException e) {} } } public String toString() { return _file.toString(); } } @Override public Set<TypeName> getTypeNames(String namespace, Set<String> extensions, ITypeLoader loader) { Set<TypeName> setNames = new HashSet<TypeName>(); if (namespace == null) { for (String name : getAllTypeNames()) { setNames.add(new TypeName(name, loader, TypeName.Kind.TYPE, TypeName.Visibility.PUBLIC)); } } else { PackageToClassPathEntryTreeMap cachedPackage = getCachedPackageCorrectly(namespace); if (cachedPackage != null) { return cachedPackage.getTypeNames(extensions, loader); } else { return Collections.EMPTY_SET; } } return setNames; } @Override public int hasNamespace(String namespace) { PackageToClassPathEntryTreeMap cachedNamespace = getCachedPackageCorrectly( namespace ); return cachedNamespace != null ? cachedNamespace.getSourceRootCount() : 0; } @Override public void namespaceRefreshed(String namespace, IDirectory dir, RefreshKind kind) { if (kind == RefreshKind.CREATION) { addToPackageCache(namespace, dir); } else if (kind == RefreshKind.DELETION) { removeFromPackageCache(namespace, dir); } } private boolean areDifferent(IDirectory[] cp1, List<ClassPathEntry> cp2) { if (cp1.length != cp2.size()) { return true; } for (int i = 0; i < cp1.length; i++) { if (!cp1[i].equals(cp2.get(i).getPath())) { return true; } } return false; } private static boolean isTestFolder(IDirectory file) { return file.toString().endsWith( "gtest" ); } @Override public String getResourceName(URL url) { IFile file = CommonServices.getFileSystem().getIFile(url); String resourceName = _module.pathRelativeToRoot(file); if (resourceName == null) { throw new RuntimeException("Could not find resource " + url); } return resourceName; } public List<Pair<String, IFile>> findAllFilesByExtension(String extension) { List<Pair<String, IFile>> results = new ArrayList<Pair<String, IFile>>(); for (IDirectory dir : _module.getRoots()) { IDirectory configDir = dir.dir(IModule.CONFIG_RESOURCE_PREFIX); if (configDir.exists()) { addAllLocalResourceFilesByExtensionInternal(IModule.CONFIG_RESOURCE_PREFIX, configDir, extension, results); } } for (IDirectory sourceEntry : _module.getSourcePath()) { if (sourceEntry.exists() && !sourceEntry.getName().equals(IModule.CONFIG_RESOURCE_PREFIX)) { addAllLocalResourceFilesByExtensionInternal("", sourceEntry, extension, results); } } return results; } private static void addAllLocalResourceFilesByExtensionInternal(String relativePath, IDirectory dir, String extension, List<Pair<String, IFile>> results) { if (!CommonServices.getPlatformHelper().isPathIgnored(relativePath)) { for (IFile file : dir.listFiles()) { if (file.getName().endsWith(extension)) { String path = appendResourceNameToPath(relativePath, file.getName()); results.add(new Pair<String, IFile>(path, file)); } } for (IDirectory subdir : dir.listDirs()) { String path = appendResourceNameToPath(relativePath, subdir.getName()); addAllLocalResourceFilesByExtensionInternal(path, subdir, extension, results); } } } private static String appendResourceNameToPath( String relativePath, String resourceName ) { String path; if ( relativePath.length() > 0 ) { path = relativePath + '/' + resourceName; } else { path = resourceName; } return path; } @Override public IFile findFirstFile(String resourceName) { if (resourceName.startsWith(IModule.CONFIG_RESOURCE_PREFIX) || resourceName.startsWith(IModule.CONFIG_RESOURCE_PREFIX_2)) { return findFirstFile(resourceName, _module.getRoots()); } else { return findFirstFile(resourceName, _module.getSourcePath()); } } private IFile findFirstFile(String resourceName, List<? extends IDirectory> searchPath) { for (IDirectory dir : searchPath) { IFile file = dir.file(resourceName); if (file != null && file.exists()) { return file; } } return null; } public String toString() { return _module.getName(); } }