Browse Source

Completed: Improve ajc memory usage

the last of the big memory hog in AjState is now removed.
tags/POST_MEMORY_CHANGES
acolyer 18 years ago
parent
commit
5be99e5941

+ 3
- 2
org.aspectj.ajdt.core/src/org/aspectj/ajdt/internal/core/builder/AjBuildManager.java View File

@@ -234,6 +234,7 @@ public class AjBuildManager implements IOutputClassFileNameProvider,IBinarySourc
}
binarySourcesForTheNextCompile = state.getBinaryFilesToCompile(true);
performCompilation(buildConfig.getFiles());
state.clearBinarySourceFiles(); // we don't want these hanging around...
if (handler.hasErrors()) {
CompilationAndWeavingContext.leavingPhase(ct);
return false;
@@ -815,8 +816,8 @@ public class AjBuildManager implements IOutputClassFileNameProvider,IBinarySourc
//System.out.println("compiling");
environment = getLibraryAccess(classpaths, filenames);
if (!state.getClassNameToUCFMap().isEmpty()) {
environment = new StatefulNameEnvironment(environment, state.getClassNameToUCFMap());
if (!state.getClassNameToFileMap().isEmpty()) {
environment = new StatefulNameEnvironment(environment, state.getClassNameToFileMap());
}
org.aspectj.ajdt.internal.compiler.CompilerAdapter.setCompilerAdapterFactory(this);

+ 279
- 124
org.aspectj.ajdt.core/src/org/aspectj/ajdt/internal/core/builder/AjState.java View File

@@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -36,11 +37,19 @@ import org.aspectj.bridge.SourceLocation;
import org.aspectj.org.eclipse.jdt.internal.compiler.CompilationResult;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryField;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryMethod;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.CompilerModifiers;
import org.aspectj.org.eclipse.jdt.internal.core.builder.ReferenceCollection;
import org.aspectj.org.eclipse.jdt.internal.core.builder.StringSet;
import org.aspectj.org.eclipse.jdt.internal.core.util.Util;
import org.aspectj.util.FileUtil;
import org.aspectj.weaver.IWeaver;
import org.aspectj.weaver.ReferenceType;
import org.aspectj.weaver.ReferenceTypeDelegate;
import org.aspectj.weaver.ResolvedMember;
import org.aspectj.weaver.ResolvedType;
import org.aspectj.weaver.UnresolvedType;
import org.aspectj.weaver.bcel.BcelWeaver;
import org.aspectj.weaver.bcel.BcelWorld;
import org.aspectj.weaver.bcel.UnwovenClassFile;
@@ -71,7 +80,8 @@ public class AjState {
/**
* Keeps a list of (FQN,Filename) pairs (as ClassFile objects)
* for types that resulted from the compilation of the given
* File.
* File. Note :- the ClassFile objects contain no byte code,
* they are simply a Filename,typename pair.
*
* Populated in noteResult and used in addDependentsOf(File)
*
@@ -86,7 +96,7 @@ public class AjState {
*
* Added by AMC during state refactoring, 1Q06.
*/
private Set sourceFilesDefiningAspects = new HashSet();
private Set/*<File>*/ sourceFilesDefiningAspects = new HashSet();
/**
* Populated in noteResult to record the set of types that should be recompiled if
@@ -99,7 +109,9 @@ public class AjState {
/**
* Holds UnwovenClassFiles (byte[]s) originating from the given file source. This
* could be a jar file, a directory, or an individual .class file. This is an
* *expensive* map we would like to release much earlier if possible.
* *expensive* map. It is cleared immediately following a batch build, and the
* cheaper inputClassFilesBySource map is kept for processing of any subsequent
* incremental builds.
*
* Populated during AjBuildManager.initBcelWorld().
*
@@ -116,11 +128,24 @@ public class AjState {
*
*/
private Map/*File, List<UnwovenClassFile>*/ binarySourceFiles = new HashMap();

/**
* Initially a duplicate of the information held in binarySourceFiles, with the
* key difference that the values are ClassFiles (type name, File) not UnwovenClassFiles
* (which also have all the byte code in them). After a batch build, binarySourceFiles
* is cleared, leaving just this much lighter weight map to use in processing
* subsequent incremental builds.
*/
private Map/*<File,List<ClassFile>*/ inputClassFilesBySource = new HashMap();
/**
* Holds structure information on types as they were at the end of the last
* build. It would be nice to get rid of this too, but can't see an easy way to do
* that right now.
*/
private Map/*FQN,CompactStructureRepresentation*/ resolvedTypeStructuresFromLastBuild = new HashMap();
/**
* The third of the three expensive collections of state held by AjState to support
* incremental compilation. FQN -> UCF.
*
* Populated in noteResult to record the set of UnwovenClassFiles (intermediate results)
* that originated from compilation of the class with the given fully-qualified name.
*
@@ -129,7 +154,7 @@ public class AjState {
* Passed into StatefulNameEnvironment during incremental compilation to support
* findType lookups.
*/
private Map/*<String, UnwovenClassFile>*/ classesFromName = new HashMap();
private Map/*<String, File>*/ classesFromName = new HashMap();
private List/*File*/ compiledSourceFiles = new ArrayList();
@@ -319,7 +344,7 @@ public class AjState {
* lastSBT is the last build time for the state asking the question
*/
private boolean hasStructuralChangedSince(File file,long lastSuccessfulBuildTime) {
long lastModTime = file.lastModified();
//long lastModTime = file.lastModified();
Long l = (Long)structuralChangesSinceLastFullBuild.get(file.getAbsolutePath());
long strucModTime = -1;
if (l!=null) strucModTime = l.longValue();
@@ -426,6 +451,9 @@ public class AjState {
ucfs.add(ucf);
addDependentsOf(ucf.getClassName());
binarySourceFiles.put(bsf.binSrc.getPath(),ucfs);
List cfs = new ArrayList(1);
cfs.add(getClassFileFor(ucf));
this.inputClassFilesBySource.put(bsf.binSrc.getPath(), cfs);
toWeave.put(bsf.binSrc.getPath(),ucfs);
}
deleteBinaryClassFiles();
@@ -441,20 +469,16 @@ public class AjState {
*/
private void removeAllResultsOfLastBuild() {
// remove all binarySourceFiles, and all classesFromName...
for (Iterator iter = binarySourceFiles.values().iterator(); iter.hasNext();) {
List ucfs = (List) iter.next();
for (Iterator iterator = ucfs.iterator(); iterator.hasNext();) {
UnwovenClassFile ucf = (UnwovenClassFile) iterator.next();
try {
ucf.deleteRealFile();
} catch (IOException ex) { /* we did our best here */ }
for (Iterator iter = this.inputClassFilesBySource.values().iterator(); iter.hasNext();) {
List cfs = (List) iter.next();
for (Iterator iterator = cfs.iterator(); iterator.hasNext();) {
ClassFile cf = (ClassFile) iterator.next();
cf.deleteFromFileSystem();
}
}
for (Iterator iterator = classesFromName.values().iterator(); iterator.hasNext();) {
UnwovenClassFile ucf = (UnwovenClassFile) iterator.next();
try {
ucf.deleteRealFile();
} catch (IOException ex) { /* we did our best here */ }
File f = (File) iterator.next();
new ClassFile("",f).deleteFromFileSystem();
}
for (Iterator iter = resources.iterator(); iter.hasNext();) {
String resource = (String) iter.next();
@@ -484,15 +508,11 @@ public class AjState {
// range of bsf is ucfs, domain is files (.class and jars) in inpath/jars
for (Iterator iter = deletedBinaryFiles.iterator(); iter.hasNext();) {
AjBuildConfig.BinarySourceFile deletedFile = (AjBuildConfig.BinarySourceFile) iter.next();
List ucfs = (List) binarySourceFiles.get(deletedFile.binSrc.getPath());
binarySourceFiles.remove(deletedFile.binSrc.getPath());

// AMC temp during refactoring
UnwovenClassFile ucf = (UnwovenClassFile) ucfs.get(0);
ClassFile cf = new ClassFile(ucf.getClassName(),new File(ucf.getFilename()));
// end temp
deleteClassFile(cf);
List cfs = (List) this.inputClassFilesBySource.get(deletedFile.binSrc.getPath());
for (Iterator iterator = cfs.iterator(); iterator.hasNext();) {
deleteClassFile((ClassFile)iterator.next());
}
this.inputClassFilesBySource.remove(deletedFile.binSrc.getPath());
}
}
@@ -584,9 +604,9 @@ public class AjState {

UnwovenClassFile[] unwovenClassFiles = result.unwovenClassFiles();
for (int i = 0; i < unwovenClassFiles.length; i++) {
UnwovenClassFile lastTimeRound = (UnwovenClassFile) classesFromName.get(unwovenClassFiles[i].getClassName());
File lastTimeRound = (File) classesFromName.get(unwovenClassFiles[i].getClassName());
recordClassFile(unwovenClassFiles[i],lastTimeRound);
classesFromName.put(unwovenClassFiles[i].getClassName(),unwovenClassFiles[i]);
classesFromName.put(unwovenClassFiles[i].getClassName(),new File(unwovenClassFiles[i].getFilename()));
}

// need to do this before types are deleted from the World...
@@ -713,111 +733,176 @@ public class AjState {
return null;
}
private void recordClassFile(UnwovenClassFile thisTime, UnwovenClassFile lastTime) {
if (simpleStrings == null) return; // batch build
private void recordClassFile(UnwovenClassFile thisTime, File lastTime) {
if (simpleStrings == null) {
// batch build
// record resolved type for structural comparisions in future increments
// this records a second reference to a structure already held in memory
// by the world.
ResolvedType rType = world.resolve(thisTime.getClassName());
if (!rType.isMissing()) {
this.resolvedTypeStructuresFromLastBuild.put(thisTime.getClassName(),new CompactStructureRepresentation(rType));
}
return;
}

CompactStructureRepresentation existingStructure = (CompactStructureRepresentation) this.resolvedTypeStructuresFromLastBuild.get(thisTime.getClassName());
ReferenceType newResolvedType = (ReferenceType) world.resolve(thisTime.getClassName());
if (!newResolvedType.isMissing()) {
this.resolvedTypeStructuresFromLastBuild.put(thisTime.getClassName(),new CompactStructureRepresentation(newResolvedType));
}
if (lastTime == null) {
addDependentsOf(thisTime.getClassName());
return;
}

byte[] newBytes = thisTime.getBytes();
byte[] oldBytes = lastTime.getBytes();
boolean bytesEqual = (newBytes.length == oldBytes.length);
for (int i = 0; (i < oldBytes.length) && bytesEqual; i++) {
if (newBytes[i] != oldBytes[i]) bytesEqual = false;
if (newResolvedType.isMissing()) {
return;
}
if (!bytesEqual) {
try {
ClassFileReader reader = new ClassFileReader(oldBytes, lastTime.getFilename().toCharArray());
// ignore local types since they're only visible inside a single method
if (!(reader.isLocal() || reader.isAnonymous()) &&
reader.hasStructuralChanges(newBytes)) {

byte[] newBytes = thisTime.getBytes();
try {
ClassFileReader reader = new ClassFileReader(newBytes, lastTime.getAbsolutePath().toCharArray());
// ignore local types since they're only visible inside a single method
if (!(reader.isLocal() || reader.isAnonymous())) {
if (hasStructuralChanges(reader,existingStructure)) {
structuralChangesSinceLastFullBuild.put(thisTime.getFilename(),new Long(currentBuildTime));
addDependentsOf(lastTime.getClassName());
addDependentsOf(new String(reader.getName()).replace('/','.'));
}
} catch (ClassFormatException e) {
addDependentsOf(lastTime.getClassName());
}
}
}
} catch (ClassFormatException e) {
addDependentsOf(thisTime.getClassName());
}
}
/**
* Compare the class structure of the new intermediate (unwoven) class with the
* existingResolvedType of the same class that we have in the world, looking for
* any structural differences (and ignoring aj members resulting from weaving....)
*
* Warning : long but boring method implementation...
* @param reader
* @param existingType
* @return
*/
private boolean hasStructuralChanges(ClassFileReader reader, CompactStructureRepresentation existingType) {
// mirrors the checks in ClassFileReader.hasStructuralChanges, but compares against
// ref type delegate instead of old bytes
if (existingType == null) {
return true;
}
// modifiers
if (!modifiersEqual(reader.getModifiers(),existingType.modifiers)) {
return true;
}
// generic signature
if (!equal(reader.getGenericSignature(),existingType.genericSignature)) {
return true;
}
// superclass name
if (!equal(reader.getSuperclassName(),existingType.superclassName)) {
return true;
}
// interfaces
char[][] existingIfs = existingType.interfaces;
char[][] newIfsAsChars = reader.getInterfaceNames();
if (newIfsAsChars == null) { newIfsAsChars = new char[0][]; }
char[][] newIfs = new char[newIfsAsChars.length][];
if (existingIfs.length != newIfs.length) {
return true;
}
new_interface_loop: for (int i = 0; i < newIfs.length; i++) {
for (int j = 0; j < existingIfs.length; j++) {
if (equal(existingIfs[j],newIfs[i])) {
continue new_interface_loop;
}
}
return true;
}
// fields
MemberStructure[] existingFields = existingType.fields;
IBinaryField[] newFields = reader.getFields();
if (newFields == null) { newFields = new IBinaryField[0]; }
if (newFields.length != existingFields.length) {
return true;
}
new_field_loop: for (int i = 0; i < newFields.length; i++) {
char[] fieldName = newFields[i].getName();
for (int j = 0; j < existingFields.length; j++) {
if (equal(existingFields[j].name,fieldName)) {
if (!modifiersEqual(newFields[i].getModifiers(),existingFields[j].modifiers)) {
return true;
}
if (!equal(existingFields[j].signature,newFields[i].getTypeName())) {
return true;
}
continue new_field_loop;
}
}
return true;
}
// methods
MemberStructure[] existingMethods = existingType.methods;
IBinaryMethod[] newMethods = reader.getMethods();
if (newMethods == null) { newMethods = new IBinaryMethod[0]; }
if (newMethods.length != existingMethods.length) {
return true;
}
new_method_loop: for (int i = 0; i < newMethods.length; i++) {
char[] methodName = newMethods[i].getSelector();
for (int j = 0; j < existingMethods.length; j++) {
if (equal(existingMethods[j].name,methodName)) {
// candidate match
if (!equal(newMethods[i].getMethodDescriptor(),existingMethods[j].signature)) {
continue; // might be overloading
}
else {
// matching sigs
if (!modifiersEqual(newMethods[i].getModifiers(),existingMethods[j].modifiers)) {
return true;
}
continue new_method_loop;
}
}
}
return true; // (no match found)
}
return false;
}

// public void noteClassesFromFile(CompilationResult result, String sourceFileName, List unwovenClassFiles) {
// File sourceFile = new File(sourceFileName);
//
// if (result != null) {
// references.put(sourceFile, new ReferenceCollection(result.qualifiedReferences, result.simpleNameReferences));
// }
//
// List previous = (List)classesFromFile.get(sourceFile);
// List newClassFiles = new ArrayList();
// for (Iterator i = unwovenClassFiles.iterator(); i.hasNext();) {
// UnwovenClassFile cf = (UnwovenClassFile) i.next();
// cf = writeClassFile(cf, findAndRemoveClassFile(cf.getClassName(), previous));
// newClassFiles.add(cf);
// classesFromName.put(cf.getClassName(), cf);
// }
//
// if (previous != null && !previous.isEmpty()) {
// for (Iterator i = previous.iterator(); i.hasNext();) {
// UnwovenClassFile cf = (UnwovenClassFile) i.next();
// deleteClassFile(cf);
// }
// }
//
// classesFromFile.put(sourceFile, newClassFiles);
// resultsFromFile.put(sourceFile, result);
// }
//
// private UnwovenClassFile findAndRemoveClassFile(String name, List previous) {
// if (previous == null) return null;
// for (Iterator i = previous.iterator(); i.hasNext();) {
// UnwovenClassFile cf = (UnwovenClassFile) i.next();
// if (cf.getClassName().equals(name)) {
// i.remove();
// return cf;
// }
// }
// return null;
// }
//
// private UnwovenClassFile writeClassFile(UnwovenClassFile cf, UnwovenClassFile previous) {
// if (simpleStrings == null) { // batch build
// addedClassFiles.add(cf);
// return cf;
// }
//
// try {
// if (previous == null) {
// addedClassFiles.add(cf);
// addDependentsOf(cf.getClassName());
// return cf;
// }
//
// byte[] oldBytes = previous.getBytes();
// byte[] newBytes = cf.getBytes();
// //if (this.compileLoop > 1) { // only optimize files which were recompiled during the dependent pass, see 33990
// notEqual : if (newBytes.length == oldBytes.length) {
// for (int i = newBytes.length; --i >= 0;) {
// if (newBytes[i] != oldBytes[i]) break notEqual;
// }
// //addedClassFiles.add(previous); //!!! performance wasting
// buildManager.bcelWorld.addSourceObjectType(previous.getJavaClass());
// return previous; // bytes are identical so skip them
// }
// //}
// ClassFileReader reader = new ClassFileReader(oldBytes, previous.getFilename().toCharArray());
// // ignore local types since they're only visible inside a single method
// if (!(reader.isLocal() || reader.isAnonymous()) && reader.hasStructuralChanges(newBytes)) {
// addDependentsOf(cf.getClassName());
// }
// } catch (ClassFormatException e) {
// addDependentsOf(cf.getClassName());
// }
// addedClassFiles.add(cf);
// return cf;
// }
private boolean modifiersEqual(int eclipseModifiers, int resolvedTypeModifiers) {
resolvedTypeModifiers = resolvedTypeModifiers & CompilerModifiers.AccJustFlag;
eclipseModifiers = eclipseModifiers & CompilerModifiers.AccJustFlag;
if ((eclipseModifiers & CompilerModifiers.AccSuper) != 0) {
eclipseModifiers -= CompilerModifiers.AccSuper;
}
return (eclipseModifiers == resolvedTypeModifiers);
}
private boolean equal(char[] c1, char[] c2) {
if (c1 == null && c2 == null) {
return true;
}
if (c1 == null || c2 == null) {
return false;
}
if (c1.length != c2.length) {
return false;
}
for (int i = 0; i < c1.length; i++) {
if (c1[i] != c2[i]) return false;
}
return true;
}
private static StringSet makeStringSet(List strings) {
StringSet ret = new StringSet(strings.size());
@@ -943,13 +1028,28 @@ public class AjState {
public void recordBinarySource(String fromPathName, List unwovenClassFiles) {
this.binarySourceFiles.put(fromPathName,unwovenClassFiles);
List simpleClassFiles = new LinkedList();
for (Iterator iter = unwovenClassFiles.iterator(); iter.hasNext();) {
UnwovenClassFile ucf = (UnwovenClassFile) iter.next();
ClassFile cf = getClassFileFor(ucf);
simpleClassFiles.add(cf);
}
this.inputClassFilesBySource.put(fromPathName,simpleClassFiles);
}

/**
* @param ucf
* @return
*/
private ClassFile getClassFileFor(UnwovenClassFile ucf) {
return new ClassFile(ucf.getClassName(),new File(ucf.getFilename()));
}
public Map getBinarySourceMap() {
return this.binarySourceFiles;
}
public Map getClassNameToUCFMap() {
public Map getClassNameToFileMap() {
return this.classesFromName;
}
@@ -1012,4 +1112,59 @@ public class AjState {
}
}

private static class CompactStructureRepresentation {
public CompactStructureRepresentation(ResolvedType forType) {
this.className = forType.getName().replace('.','/').toCharArray();
this.modifiers = forType.getModifiers();
this.genericSignature = forType.getGenericSignature().toCharArray();
if (this.genericSignature.length == 0) {
this.genericSignature = null;
}
this.superclassName = forType.getSuperclass().getName().replace('.','/').toCharArray();
ResolvedType[] rTypes = forType.getDeclaredInterfaces();
this.interfaces = new char[rTypes.length][];
for (int i = 0; i < rTypes.length; i++) {
this.interfaces[i] = rTypes[i].getName().replace('.','/').toCharArray();
}
ResolvedMember[] rFields = forType.getDeclaredFields();
this.fields = new MemberStructure[rFields.length];
for (int i = 0; i < rFields.length; i++) {
this.fields[i] = new MemberStructure();
this.fields[i].name = rFields[i].getName().toCharArray();
this.fields[i].modifiers = rFields[i].getModifiers();
this.fields[i].signature = rFields[i].getReturnType().getSignature().toCharArray();
}
ResolvedMember[] rMethods = forType.getDeclaredMethods();
this.methods = new MemberStructure[rMethods.length];
for (int i = 0; i < rMethods.length; i++) {
this.methods[i] = new MemberStructure();
this.methods[i].name = rMethods[i].getName().toCharArray();
this.methods[i].modifiers = rMethods[i].getModifiers();
StringBuffer sig = new StringBuffer();
sig.append("(");
UnresolvedType[] pTypes = rMethods[i].getParameterTypes();
for (int j = 0; j < pTypes.length; j++) {
sig.append(pTypes[j].getSignature());
}
sig.append(")");
sig.append(rMethods[i].getReturnType().getSignature());
this.methods[i].signature = sig.toString().toCharArray();
}
}
char[] className;
int modifiers;
char[] genericSignature;
char[] superclassName;
char[][] interfaces;
MemberStructure[] fields;
MemberStructure[] methods;
}
private static class MemberStructure {
char[] name;
int modifiers;
char[] signature;
}
}

+ 29
- 13
org.aspectj.ajdt.core/src/org/aspectj/ajdt/internal/core/builder/StatefulNameEnvironment.java View File

@@ -14,27 +14,32 @@
package org.aspectj.ajdt.internal.core.builder;

//import java.util.HashMap;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.aspectj.weaver.bcel.UnwovenClassFile;
import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
import org.aspectj.util.FileUtil;


public class StatefulNameEnvironment implements INameEnvironment {
private Map classesFromName;
private Map inflatedClassFilesCache;
private Set packageNames;
private INameEnvironment baseEnvironment;
public StatefulNameEnvironment(INameEnvironment baseEnvironment, Map classesFromName) {
this.classesFromName = classesFromName;
this.inflatedClassFilesCache = new HashMap();
this.baseEnvironment = baseEnvironment;
packageNames = new HashSet();
@@ -60,18 +65,29 @@ public class StatefulNameEnvironment implements INameEnvironment {
}

private NameEnvironmentAnswer findType(String name) {
UnwovenClassFile cf = (UnwovenClassFile)classesFromName.get(name);
//System.err.println("find: " + name + " found: " + cf);
if (cf == null) return null;

try {
//System.out.println("from cache: " + name);
return new NameEnvironmentAnswer(
new ClassFileReader(cf.getBytes(), cf.getFilename().toCharArray()),
null /* no access restriction */);
} catch (ClassFormatException e) {
return null; //!!! seems to match FileSystem behavior
if (this.inflatedClassFilesCache.containsKey(name)) {
return (NameEnvironmentAnswer) this.inflatedClassFilesCache.get(name);
}
else {
File fileOnDisk = (File)classesFromName.get(name);
//System.err.println("find: " + name + " found: " + cf);
if (fileOnDisk == null) return null;
try {
//System.out.println("from cache: " + name);
byte[] bytes = FileUtil.readAsByteArray(fileOnDisk);
NameEnvironmentAnswer ret =
new NameEnvironmentAnswer(
new ClassFileReader(bytes, fileOnDisk.getAbsolutePath().toCharArray()),
null /* no access restriction */);
this.inflatedClassFilesCache.put(name,ret);
return ret;
} catch (ClassFormatException e) {
return null; //!!! seems to match FileSystem behavior
} catch (IOException ex) {
return null; // see above...
}
}
}


Loading…
Cancel
Save