JarJarDiff.java

/*
 *  Copyright (c) 2001-2024, Jean Tessier
 *  All rights reserved.
 *  
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *  
 *      * Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *  
 *      * Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *  
 *      * Neither the name of Jean Tessier nor the names of his contributors
 *        may be used to endorse or promote products derived from this software
 *        without specific prior written permission.
 *  
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jeantessier.dependencyfinder.ant;

import java.io.*;
import java.util.*;

import com.jeantessier.classreader.*;
import com.jeantessier.diff.*;
import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;

public class JarJarDiff extends Task {
    public static final String API_STRATEGY = "api";
    public static final String INCOMPATIBLE_STRATEGY = "incompatible";

    public static final String DEFAULT_LEVEL = API_STRATEGY;

    private String name = "";
    private Path oldPath;
    private String oldLabel;
    private Path newPath;
    private String newLabel;
    private File filter;
    private String level = DEFAULT_LEVEL;
    private boolean code;
    private String encoding = Report.DEFAULT_ENCODING;
    private String dtdPrefix = Report.DEFAULT_DTD_PREFIX;
    private String indentText = Report.DEFAULT_INDENT_TEXT;
    private File destfile;

    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public Path createOld() {
        if (oldPath == null) {
            oldPath = new Path(getProject());
        }

        return oldPath;
    }
    
    public Path getOld() {
        return oldPath;
    }

    public String getOldlabel() {
        return oldLabel;
    }
    
    public void setOldlabel(String oldLabel) {
        this.oldLabel = oldLabel;
    }

    public Path createNew() {
        if (newPath == null) {
            newPath = new Path(getProject());
        }

        return newPath;
    }
    
    public Path getNew() {
        return newPath;
    }

    public String getNewlabel() {
        return newLabel;
    }
    
    public void setNewlabel(String newLabel) {
        this.newLabel = newLabel;
    }

    public File getFilter() {
        return filter;
    }

    public void setfilter(File filter) {
        this.filter = filter;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    public boolean getCode() {
        return code;
    }

    public void setCode(boolean code) {
        this.code = code;
    }

    public String getEncoding() {
        return encoding;
    }
    
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public String getDtdprefix() {
        return dtdPrefix;
    }
    
    public void setDtdprefix(String dtdPrefix) {
        this.dtdPrefix = dtdPrefix;
    }

    public String getIndenttext() {
        return indentText;
    }
    
    public void setIndenttext(String indentText) {
        this.indentText = indentText;
    }

    public File getDestfile() {
        return destfile;
    }
    
    public void setDestfile(File destfile) {
        this.destfile = destfile;
    }

    // Visible for tests only
    void validateParameters() throws BuildException {
        if (getOld() == null) {
            throw new BuildException("old must be set!");
        }

        if (getOldlabel() == null) {
            setOldlabel(getOld().toString());
        }

        if (getNew() == null) {
            throw new BuildException("new must be set!");
        }

        if (getNewlabel() == null) {
            setNewlabel(getNew().toString());
        }

        if (getDestfile() == null) {
            throw new BuildException("destfile must be set!");
        }
    }

    public void execute() throws BuildException {
        validateParameters();

        VerboseListener verboseListener = new VerboseListener(this);

        try {
            // Collecting data, first classfiles from JARs,
            // then package/class trees using NodeFactory.

            log("Loading old classes from path " + getOld());
            PackageMapper oldPackages = new PackageMapper();
            ClassfileLoader oldJar = new AggregatingClassfileLoader();
            oldJar.addLoadListener(oldPackages);
            oldJar.addLoadListener(verboseListener);
            oldJar.load(Arrays.asList(getOld().list()));

            log("Loading new classes from path " + getNew());
            PackageMapper newPackages = new PackageMapper();
            ClassfileLoader newJar = new AggregatingClassfileLoader();
            newJar.addLoadListener(newPackages);
            newJar.addLoadListener(verboseListener);
            newJar.load(Arrays.asList(getNew().list()));

            // Starting to compare, first at package level,
            // then descending to class level for packages
            // that are in both the old and the new codebase.

            log("Comparing old and new classes ...");

            Differences differences = getDifferencesFactory().createProjectDifferences(getName(), getOldlabel(), oldPackages, getNewlabel(), newPackages);

            log("Saving difference report to " + getDestfile().getAbsolutePath());

            Report report = new Report(getIndenttext(), getEncoding(), getDtdprefix());

            differences.accept(report);

            PrintWriter out = new PrintWriter(new FileWriter(getDestfile()));
            out.print(report.render());
            out.close();
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    private DifferencesFactory getDifferencesFactory() throws IOException {
        DifferenceStrategy baseStrategy = getBaseStrategy(getCode());
        DifferenceStrategy strategy = getStrategy(getLevel(), baseStrategy);

        if (getFilter() != null) {
            strategy = new ListBasedDifferenceStrategy(strategy, getFilter());
        }

        return new DifferencesFactory(strategy);
    }

    private DifferenceStrategy getBaseStrategy(boolean useCode) {
        DifferenceStrategy baseStrategy;
        if (useCode) {
            baseStrategy = new CodeDifferenceStrategy();
        } else {
            baseStrategy = new NoDifferenceStrategy();
        }
        return baseStrategy;
    }

    private DifferenceStrategy getStrategy(String level, DifferenceStrategy baseStrategy) {
        DifferenceStrategy strategy;
        if (API_STRATEGY.equals(level)) {
            strategy = new APIDifferenceStrategy(baseStrategy);
        } else if (INCOMPATIBLE_STRATEGY.equals(level)) {
            strategy = new IncompatibleDifferenceStrategy(baseStrategy);
        } else {
            try {
                try {
                    strategy = (DifferenceStrategy) Class.forName(level).getConstructor(DifferenceStrategy.class).newInstance(baseStrategy);
                } catch (NoSuchMethodException ex) {
                    strategy = (DifferenceStrategy) Class.forName(level).getDeclaredConstructor().newInstance();
                }
            } catch (ReflectiveOperationException | ClassCastException ex) {
                log("Unknown level \"" + level + "\", using default level \"" + DEFAULT_LEVEL + "\": " + ex.getMessage());
                strategy = getDefaultStrategy(baseStrategy);
            }
        }
        return strategy;
    }

    private APIDifferenceStrategy getDefaultStrategy(DifferenceStrategy baseStrategy) {
        return new APIDifferenceStrategy(baseStrategy);
    }
}