JavaImport.java

/* $Id$
 *****************************************************************************
 * Copyright (c) 2009-2013 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;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.antlr.runtime.ANTLRReaderStream;
import org.antlr.runtime.CommonTokenStream;
import org.argouml.i18n.Translator;
import org.argouml.kernel.Project;
import org.argouml.language.java.JavaModuleGlobals;
import org.argouml.model.IllegalModelElementConnectionException;
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.ImportClassLoader;
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.uml.util.ModelUtil;
import org.argouml.util.SuffixFilter;

/**
 * This is the main class for Java reverse engineering. It's based on the Antlr
 * Java example.
 * 
 * @author Andreas Rueckert <a_rueckert@gmx.net>
 */
public class JavaImport implements ModuleInterface, ImportInterface {

    /** Logger. */
    private static final Logger LOG =
        Logger.getLogger(JavaImport.class.getName());

    /**
     * Java profile model.
     */
    private Profile javaProfile = null;

    /**
     * New model elements that were added
     */
    private Collection<Object> newElements;

    /*
     * @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 {

        JavaImportSettings.getInstance().saveSettings();
        updateImportClassloader();
        newElements = new HashSet<Object>();
        monitor.updateMainTask(Translator.localize("dialog.import.pass1"));

        // get the Java profile from project, if available
        javaProfile = getJavaProfile(p);

        try {
            if ((settings.getImportLevel() 
                    == ImportSettings.DETAIL_CLASSIFIER_FEATURE)
                || settings.getImportLevel() == ImportSettings.DETAIL_FULL) {
                monitor.setMaximumProgress(files.size() * 2);
                doImportPass(p, files, settings, monitor, 0, 0);
                if (!monitor.isCanceled()) {
                    monitor.updateMainTask(Translator
                            .localize("dialog.import.pass2"));
                    doImportPass(p, files, settings, monitor, files.size(), 1);
                }
            } else {
                monitor.setMaximumProgress(files.size() * 2);
                doImportPass(p, files, settings, monitor, 0, 0);
            }
            
            ModelUtil.generatePackageDependencies(p);
        } catch (IllegalModelElementConnectionException e) {
        } finally {
            //this prevents parse problems to be displayed, so I disabled it:
            // --thn
            //monitor.close();
        }
        return newElements;
    }

    private void doImportPass(Project p, Collection<File> files,
            ImportSettings settings, ProgressMonitor monitor, int startCount,
            int pass) {

        int count = startCount;
        for (File file : files) {
            if (monitor.isCanceled()) {
                monitor.updateSubTask(Translator
                        .localize("dialog.import.cancelled"));
                return;
            }
            try {
                parseFile(p, file, settings, pass);
            } catch (Exception e) {
                StringWriter sw = new StringWriter();
                PrintWriter pw = new java.io.PrintWriter(sw);
                e.printStackTrace(pw);
                monitor.notifyMessage(
                    Translator.localize(
                            "dialog.title.import-problems"), //$NON-NLS-1$
                    Translator.localize("label.import-problems"), //$NON-NLS-1$
                    sw.toString());
                if (monitor.isCanceled()) {
                    break;
                }
            }
            monitor.updateProgress(count++);
            monitor.updateSubTask(Translator.localize(
                    "dialog.import.parsingAction", 
                    new Object[] { 
                        file.getAbsolutePath() 
                    }));
        }
    }

    /**
     * Do a single import pass of a single file.
     * 
     * @param p the project
     * @param f the source file
     * @param settings the user provided import settings
     * @param pass current import pass - 0 = single pass, 1 = pass 1 of 2, 2 =
     *                pass 2 of 2
     */
    private void parseFile(Project p, File f, ImportSettings settings, int pass)
        throws ImportException {

        try {
            // Create a scanner that reads from the input stream
            String encoding = settings.getInputSourceEncoding();
            FileInputStream in = new FileInputStream(f);
            InputStreamReader isr;
            try {
                isr = new InputStreamReader(in, encoding);
            } catch (UnsupportedEncodingException e) {
                // fall back to default encoding
                isr = new InputStreamReader(in);
            }
            JavaLexer lexer = new JavaLexer(new ANTLRReaderStream(isr));

            // Create a parser that reads from the scanner
            JavaParser parser = new JavaParser(new CommonTokenStream(lexer));

            // Pass == 0 means single pass recognition
            int parserMode = JavaParser.MODE_IMPORT_PASS1
                    | JavaParser.MODE_IMPORT_PASS2;
            if (pass == 0) {
                parserMode = JavaParser.MODE_IMPORT_PASS1;
            } else if (pass == 1) {
                parserMode = JavaParser.MODE_IMPORT_PASS2;
            }
            parser.setParserMode(parserMode);

            // Create a modeller for the parser
            Modeller modeller = new Modeller(
                    p.getUserDefinedModelList().get(0), javaProfile,
                    JavaImportSettings.getInstance().isAttributeSelected(),
                    JavaImportSettings.getInstance().isDatatypeSelected(), f
                            .getName());

            // Print the name of the current file, so we can associate
            // exceptions to the file.
            LOG.info("Parsing " + f.getAbsolutePath());

            // Calculate the import level
            int level = 0;
            int importlevel = settings.getImportLevel();
            if (importlevel == ImportSettings.DETAIL_CLASSIFIER_FEATURE) {
                level = 1;
            } else if (importlevel == ImportSettings.DETAIL_FULL) {
                // full level only needed for the second pass
                level = (pass == 0) ? 0 : 2;
            }
            modeller.setAttribute("level", Integer.valueOf(level));

            try {
                // start parsing at the compilationUnit rule
                parser.compilationUnit(modeller, lexer);
            } catch (Exception e) {
                String errorString = buildErrorString(f);
                LOG.log(Level.SEVERE, 
                        e.getClass().getName() + errorString,
                        e);
                throw new ImportException(errorString, e);
            } finally {
                newElements.addAll(modeller.getNewElements());
                in.close();
            }
        } catch (IOException e) {
            throw new ImportException(buildErrorString(f), e);
        }
    }

    private String buildErrorString(File f) {
        String path = "";
        try {
            path = f.getCanonicalPath();
        } catch (IOException e) {
            // Just ignore - we'll use the simple file name
            LOG.log(Level.FINEST,
                    "Cannot get the Canonical Path, using name without path",
                    e);
        }
        return "Exception in file: " + path + " " + f.getName();
    }

    /*
     * @see org.argouml.uml.reveng.ImportInterface#getSuffixFilters()
     */
    public SuffixFilter[] getSuffixFilters() {
        SuffixFilter[] result = {
            new SuffixFilter("java", 
                    Translator.localize("java.filefilter.java")),
        };
        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.moduleloader.ModuleInterface#getName()
     */
    public String getName() {
        return "Java";
    }

    /*
     * @see org.argouml.moduleloader.ModuleInterface#getInfo(int)
     */
    public String getInfo(int type) {
        switch (type) {
        case DESCRIPTION:
            return "Java import from Java files.";
        case AUTHOR:
            return JavaModuleGlobals.MODULE_AUTHOR;
        case VERSION:
            return JavaModuleGlobals.MODULE_VERSION;
        case ModuleInterface.DOWNLOADSITE:
            return JavaModuleGlobals.MODULE_DOWNLOADSITE;
        default:
            return null;
        }
    }

    /*
     * @see org.argouml.moduleloader.ModuleInterface#disable()
     */
    public boolean disable() {
        // We are permanently enabled
        return false;
    }

    /*
     * @see org.argouml.moduleloader.ModuleInterface#enable()
     */
    public boolean enable() {
        ImporterManager.getInstance().addImporter(this);
        return true;
    }

    /*
     * @see org.argouml.uml.reveng.ImportInterface#getImportSettings()
     */
    public List<SettingsTypes.Setting> getImportSettings() {
        return JavaImportSettings.getInstance().getImportSettings();
    }

    private void updateImportClassloader() {
        List<String> pathList = JavaImportSettings.getInstance().getPathList();
        URL[] urls = new URL[pathList.size()];

        int i = 0;
        for (String path : pathList) {
            try {
                urls[i++] = new File(path).toURI().toURL();
            } catch (MalformedURLException e) {
                LOG.severe("Bad path in classpath " + path);
            }
        }

        try {
            ImportClassLoader.getInstance(urls);
            ImportClassLoader.getInstance().saveUserPath();
        } catch (MalformedURLException e) {
            LOG.log(Level.FINEST, "Bad path in classpaths", e);
        }
    }

    /**
     * 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;
    }
}