ClassfileImport.java
/* $Id$
*****************************************************************************
* Copyright (c) 2009 Contributors - see below
* 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:
* thn
*****************************************************************************
*
* Some portions of this file was previously release using the BSD License:
*/
// Copyright (c) 1996-2008 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
package org.argouml.language.java.reveng.classfile;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.argouml.kernel.Project;
import org.argouml.language.java.JavaModuleGlobals;
import org.argouml.language.java.reveng.JavaImportSettings;
import org.argouml.moduleloader.ModuleInterface;
import org.argouml.profile.Profile;
import org.argouml.taskmgmt.ProgressMonitor;
import org.argouml.uml.reveng.FileImportUtils;
import org.argouml.uml.reveng.ImportInterface;
import org.argouml.uml.reveng.ImportSettings;
import org.argouml.uml.reveng.ImporterManager;
import org.argouml.uml.reveng.SettingsTypes;
import org.argouml.util.SuffixFilter;
import antlr.ANTLRException;
import antlr.RecognitionException;
import antlr.TokenStreamException;
/**
* This is the main class for the classfile import. It shares some logic with
* the Java source importer including the Modeler and the settings logic.
*
* @author Andreas Rueckert <a_rueckert@gmx.net>
*/
public class ClassfileImport implements ModuleInterface, ImportInterface {
/** The files that needs a second RE pass. */
private Collection secondPassFiles;
/**
* Java profile model.
*/
private Profile javaProfile = null;
/**
* New model elements that were added
*/
private Collection newElements;
private int fileCount;
/*
* @see org.argouml.uml.reveng.ImportInterface#parseFiles(org.argouml.kernel.Project, java.util.Collection, org.argouml.uml.reveng.ImportSettings, org.argouml.application.api.ProgressMonitor)
*/
public Collection parseFiles(Project p, Collection<File> files,
ImportSettings settings, ProgressMonitor monitor)
throws ImportException {
secondPassFiles = new ArrayList();
newElements = new HashSet();
// get the Java profile from project, if available
javaProfile = getJavaProfile(p);
monitor.setMaximumProgress(countFiles(files));
for (File file : files) {
monitor.updateMainTask("Parsing file: " + file);
if (monitor.isCanceled()) {
break;
}
processFile(p, (File) file, monitor);
monitor.updateProgress(fileCount++);
}
if (count2ndPassFiles(secondPassFiles) > 0 && !monitor.isCanceled()) {
// Process all the files, that need a second pass.
for (Object next : secondPassFiles) {
try {
if (next instanceof Collection) {
do2ndJarPass(p, (Collection) next, monitor);
} else {
File nextFile = (File) next;
String fileName = nextFile.getName();
FileInputStream fis;
try {
fis = new FileInputStream(nextFile);
} catch (FileNotFoundException e) {
throw new ImportException(e);
}
// TODO: I18N
monitor.updateSubTask("Parsing class 2nd pass - "
+ fileName);
if (monitor.isCanceled()) {
break;
}
parseFile(p, fis, fileName);
monitor.updateProgress(fileCount++);
}
} catch (ANTLRException e) {
throw new ImportException(e);
} catch (IOException e) {
throw new ImportException(e);
}
}
}
return newElements;
}
/**
* Count all class files, including ones inside JAR files.
*
* @return The number of files to process
*/
private int countFiles(Collection<File> files) {
int total = 0;
for (File f : files) {
if (f.getName().endsWith(".jar")) {
try {
for (Enumeration<JarEntry> e = (new JarFile(f)).entries();
e.hasMoreElements();) {
ZipEntry entry = e.nextElement();
if (!entry.isDirectory()
&& entry.getName().endsWith(".class")) {
total++;
}
}
} catch (IOException e) {
// Just count it as a normal file
total++;
}
} else {
total++;
}
}
return total;
}
/**
* Count the files in the 2nd pass buffer. We can't just use size() because
* the collection can contain nested collections which need to be counted
* independently. In a nested collection, the first entry is the name of
* the JAR file, so we use size()-1.
*
* @param buffer
* The buffer with the files for the 2nd pass.
*/
private int count2ndPassFiles(Collection buffer) {
int nfiles = 0;
for (Iterator i = secondPassFiles.iterator(); i.hasNext();) {
Object next = i.next();
nfiles += ((next instanceof Collection)
? ((Collection) next).size() - 1 : 1);
}
return nfiles;
}
/**
* The main method for all parsing actions. It calls the
* actual parser methods depending on the type of the
* file.
*
* @param f The file or directory, we want to parse.
* @throws ImportException containing nested exception with original error
*/
private void processFile(Project p, File f, ProgressMonitor monitor)
throws ImportException {
monitor.updateMainTask("Importing " + f.getName());
// Is this file a Jarfile?
if ( f.getName().endsWith(".jar")) { //$NON-NLS-1$
processJarFile(p, f, monitor);
} else {
String fileName = f.getName();
try { // Try to parse this file.
InputStream is;
try {
is = new FileInputStream(f);
} catch (FileNotFoundException e) {
throw new ImportException(e);
}
parseFile(p, is, fileName);
} catch (ANTLRException e) {
// TODO: Is this still needed/appropriate? It looks like
// Modeller has been changed so that it no longer throws
// exceptions... - tfm
secondPassFiles.add(f);
}
}
}
/**
* Process a Jar file, that contains classfiles.
*
* @param f The Jar file.
*/
private void processJarFile(Project p, File f, ProgressMonitor monitor)
throws ImportException {
JarFile jarfile;
try {
jarfile = new JarFile(f);
} catch (IOException e) {
throw new ImportException("IO exception opening Jar file: " + f, e);
}
// A second pass buffer just for this jar.
Collection jarSecondPassFiles = new ArrayList();
for ( Enumeration<JarEntry> e = jarfile.entries();
e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
String entryName = entry.getName();
if ( !entry.isDirectory()
&& entryName.endsWith(".class")) { //$NON-NLS-1$
try {
InputStream is;
try {
is = jarfile.getInputStream(entry);
} catch (IOException e1) {
// If this happens, something bad is going on ...
throw new ImportException(e1);
}
// TODO: I18N
monitor.updateSubTask("Parsing class - " + entryName);
if (monitor.isCanceled()) {
break;
}
parseFile(p, is, entryName);
monitor.updateProgress(fileCount++);
} catch (ANTLRException e1) {
if (jarSecondPassFiles.isEmpty()) {
// If there are no files tagged for a second pass,
// add the jar file as the 1st element.
jarSecondPassFiles.add(f);
}
// Store the entry to be parsed a 2nd time.
jarSecondPassFiles.add(entryName);
}
}
}
// If there are files to parse again, add the jar to the 2nd pass.
if ( !jarSecondPassFiles.isEmpty()) {
secondPassFiles.add(jarSecondPassFiles);
}
try {
jarfile.close();
} catch (IOException e) {
throw new ImportException("IO exception closing Jar file: " + f, e);
}
}
/**
* Do a 2nd pass on a Jar file.
*
* @param secondPassBuffer A buffer, that holds the jarfile and the names of
* the entries to parse again.
* @throws TokenStreamException
* @throws RecognitionException
*/
private void do2ndJarPass(Project p, Collection secondPassBuffer,
ProgressMonitor monitor) throws IOException, RecognitionException,
TokenStreamException {
if (!secondPassBuffer.isEmpty()) {
Iterator iterator = secondPassBuffer.iterator();
JarFile jarfile = new JarFile( (File) iterator.next());
while (iterator.hasNext()) {
String filename = (String) iterator.next();
// TODO: I18N
monitor.updateSubTask("Parsing class 2nd pass - " + filename);
if (monitor.isCanceled()) {
break;
}
parseFile(
p,
jarfile.getInputStream(jarfile.getEntry(filename)),
filename);
monitor.updateProgress(fileCount++);
}
jarfile.close();
}
}
/**
* This method parses 1 Java classfile.
*
* @param p The current project.
* @param is The inputStream for the file to parse.
* @param fileName the name of the file to parse
* @throws RecognitionException ANTLR parser error
* @throws TokenStreamException ANTLR parser error
*/
public void parseFile(Project p, InputStream is, String fileName)
throws RecognitionException, TokenStreamException {
int lastSlash = fileName.lastIndexOf('/');
if (lastSlash != -1) {
fileName = fileName.substring(lastSlash + 1);
}
ClassfileParser parser =
new ClassfileParser(new SimpleByteLexer(
new BufferedInputStream(is)));
// start parsing at the classfile rule
parser.classfile();
// Create a modeller for the parser
org.argouml.language.java.reveng.Modeller modeller =
new org.argouml.language.java.reveng.Modeller(
p.getUserDefinedModelList().get(0),
javaProfile,
JavaImportSettings.getInstance().isAttributeSelected(),
JavaImportSettings.getInstance().isDatatypeSelected(),
fileName);
// do something with the tree
ClassfileTreeParser tparser = new ClassfileTreeParser();
tparser.classfile(parser.getAST(), modeller);
newElements.addAll(modeller.getNewElements());
// Was there an exception thrown during modelling?
//Exception e = modeller.getException();
//if(e != null) {
// throw e;
//}
}
/*
* @see org.argouml.moduleloader.ModuleInterface#enable()
*/
public boolean enable() {
ImporterManager.getInstance().addImporter(this);
return true;
}
/*
* @see org.argouml.moduleloader.ModuleInterface#disable()
*/
public boolean disable() {
return true;
}
/*
* @see org.argouml.uml.reveng.FileImportSupport#getName()
*/
public String getName() {
// TODO: I18N
return "Java from classes";
}
/*
* @see org.argouml.moduleloader.ModuleInterface#getInfo(int)
*/
public String getInfo(int type) {
switch (type) {
case AUTHOR:
return JavaModuleGlobals.MODULE_AUTHOR;
case DESCRIPTION:
return "Java import from class or jar files";
case VERSION:
return JavaModuleGlobals.MODULE_VERSION;
case ModuleInterface.DOWNLOADSITE:
return JavaModuleGlobals.MODULE_DOWNLOADSITE;
default:
return null;
}
}
/*
* @see org.argouml.uml.reveng.FileImportSupport#getSuffixFilters()
*/
public SuffixFilter[] getSuffixFilters() {
SuffixFilter[] result = {
// TODO: I18N
new SuffixFilter(new String[] {"class", "jar"} , "Java files"),
new SuffixFilter("class", "Java class files"),
new SuffixFilter("jar", "Java JAR files"), };
return result;
}
/*
* @see org.argouml.uml.reveng.ImportInterface#isParseable(java.io.File)
*/
public boolean isParseable(File file) {
return FileImportUtils.matchesSuffix(file, getSuffixFilters());
}
/*
* @see org.argouml.uml.reveng.ImportInterface#getImportSettings()
*/
public List<SettingsTypes.Setting> getImportSettings() {
return JavaImportSettings.getInstance().getImportSettings();
}
/**
* Get the Java profile from project, if available.
*
* @param p the project
* @return the Java profile
*/
private Profile getJavaProfile(Project p) {
for (Profile profile : p.getProfileConfiguration().getProfiles()) {
if ("Java".equals(profile.getDisplayName())) {
return profile;
}
}
return null;
}
}