package org.jboss.windup.decompiler.fernflower;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import org.jboss.windup.decompiler.api.ClassDecompileRequest;
import org.jboss.windup.decompiler.api.DecompilationException;
import org.jboss.windup.decompiler.api.DecompilationFailure;
import org.jboss.windup.decompiler.api.DecompilationListener;
import org.jboss.windup.decompiler.api.DecompilationResult;
import org.jboss.windup.decompiler.decompiler.AbstractDecompiler;
import org.jboss.windup.decompiler.util.Filter;
import org.jetbrains.java.decompiler.main.Fernflower;
import org.jetbrains.java.decompiler.main.extern.IBytecodeProvider;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;
import org.jetbrains.java.decompiler.util.InterpreterUtil;
/**
* Decompiles Java classes with the Fernflower decompiler (https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine)
*
*
* @author <a href="mailto:ozizka@redhat.com">Ondrej Zizka</a>
* @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
*/
public class FernflowerDecompiler extends AbstractDecompiler
{
private static final Logger LOG = Logger.getLogger(FernflowerDecompiler.class.getName());
public FernflowerDecompiler()
{
}
private Map<String, Object> getOptions()
{
Map<String, Object> options = new HashMap<>();
options.put(IFernflowerPreferences.MAX_PROCESSING_METHOD, 30);
return options;
}
private IBytecodeProvider getByteCodeProvider()
{
return new IBytecodeProvider()
{
@Override
public byte[] getBytecode(String externalPath, String internalPath) throws IOException
{
return InterpreterUtil.getBytes(new File(externalPath));
}
};
}
private FernFlowerResultSaver getResultSaver(final List<String> requests, File directory, final DecompilationListener listener)
{
return new FernFlowerResultSaver(requests,directory, listener);
}
@Override
public Logger getLogger()
{
return LOG;
}
public Collection<Callable<File>> getDecompileTasks(final Map<String, List<ClassDecompileRequest>> requestMap, final DecompilationListener listener)
{
Collection<Callable<File>> tasks = new ArrayList<>(requestMap.size());
for (Map.Entry<String, List<ClassDecompileRequest>> entry : requestMap.entrySet())
{
final String key = entry.getKey();
final List<ClassDecompileRequest> requests = entry.getValue();
Callable<File> task = new Callable<File>()
{
@Override
public File call() throws Exception
{
ClassDecompileRequest firstRequest = requests.get(0);
List<String> classFiles = pathsFromDecompilationRequests(requests);
FernFlowerResultSaver resultSaver = getResultSaver(
pathsFromDecompilationRequests(requests),firstRequest.getOutputDirectory().toFile(),
listener);
Fernflower fernflower = new Fernflower(getByteCodeProvider(), resultSaver, getOptions(), new FernflowerJDKLogger());
for (ClassDecompileRequest request : requests)
{
fernflower.getStructContext().addSpace(request.getClassFile().toFile(), true);
}
try
{
fernflower.decompileContext();
if (!resultSaver.isFileSaved())
listener.decompilationFailed(classFiles, "File was not decompiled!");
}
catch (Throwable t)
{
listener.decompilationFailed(classFiles, "Decompilation failed due to: " + t.getMessage());
LOG.warning("Decompilation of " + key + " failed due to: " + t.getMessage());
}
return null;
}
};
tasks.add(task);
}
return tasks;
}
@Override
public DecompilationResult decompileClassFile(Path rootDir, Path classFilePath, Path outputDir) throws DecompilationException
{
final DecompilationResult result = new DecompilationResult();
DecompilationListener listener = new DecompilationListener()
{
@Override
public void fileDecompiled(List<String> inputPath, String outputPath)
{
result.addDecompiled(inputPath, outputPath);
}
@Override
public void decompilationFailed(List<String> inputPath, String message)
{
result.addFailure(new DecompilationFailure(message, inputPath, null));
}
@Override
public void decompilationProcessComplete()
{
}
};
FernFlowerResultSaver resultSaver = getResultSaver(Collections.singletonList(classFilePath.toString()), outputDir.toFile(), listener);
Fernflower fernflower = new Fernflower(getByteCodeProvider(), resultSaver, getOptions(), new FernflowerJDKLogger());
fernflower.getStructContext().addSpace(classFilePath.toFile(), true);
fernflower.decompileContext();
if (!resultSaver.isFileSaved())
listener.decompilationFailed(Collections.singletonList(classFilePath.toString()), "File was not decompiled!");
return result;
}
private List<String> pathsFromDecompilationRequests(List<ClassDecompileRequest> requests)
{
List<String> result = new ArrayList<>();
for(ClassDecompileRequest request : requests) {
result.add(request.getClassFile().toString());
}
return result;
}
@Override
public DecompilationResult decompileArchiveImpl(Path archive, Path outputDir, Filter<ZipEntry> filter, final DecompilationListener delegate)
throws DecompilationException
{
final DecompilationResult result = new DecompilationResult();
DecompilationListener listener = new DecompilationListener()
{
@Override
public void fileDecompiled(List<String> inputPaths, String outputPath)
{
result.addDecompiled(inputPaths, outputPath);
delegate.fileDecompiled(inputPaths, outputPath);
}
@Override
public void decompilationFailed(List<String> inputPath, String message)
{
result.addFailure(new DecompilationFailure(message, inputPath, null));
delegate.decompilationFailed(inputPath, message);
}
@Override
public void decompilationProcessComplete()
{
delegate.decompilationProcessComplete();
}
};
LOG.info("Decompiling archive '" + archive.toAbsolutePath() + "' to '" + outputDir.toAbsolutePath() + "'");
final JarFile jar;
try
{
jar = new JarFile(archive.toFile());
}
catch (IOException ex)
{
throw new DecompilationException("Can't load .jar: " + archive, ex);
}
try
{
final AtomicInteger jarEntryCount = new AtomicInteger(0);
Enumeration<JarEntry> countEnum = jar.entries();
while (countEnum.hasMoreElements())
{
countEnum.nextElement();
jarEntryCount.incrementAndGet();
}
final AtomicInteger current = new AtomicInteger(0);
final Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
final JarEntry entry = entries.nextElement();
final String name = entry.getName();
if (!name.endsWith(".class"))
{
jarEntryCount.decrementAndGet();
continue;
}
if (entry.getName().contains("$"))
continue;
if (filter != null)
{
Filter.Result filterRes = filter.decide(entry);
if (filterRes == Filter.Result.REJECT)
{
jarEntryCount.decrementAndGet();
continue;
}
else if (filterRes == Filter.Result.STOP)
{
break;
}
}
IBytecodeProvider bytecodeProvider = new IBytecodeProvider()
{
@Override
public byte[] getBytecode(String externalPath, String internalPath) throws IOException
{
return InterpreterUtil.getBytes(jar, entry);
}
};
FernFlowerResultSaver resultSaver = getResultSaver(Collections.singletonList(entry.getName()), outputDir.toFile(), listener);
Fernflower fernflower = new Fernflower(bytecodeProvider, resultSaver, getOptions(), new FernflowerJDKLogger());
fernflower.getStructContext().addSpace(new File(entry.getName()), true);
fernflower.decompileContext();
if (!resultSaver.isFileSaved())
listener.decompilationFailed(Collections.singletonList(entry.getName()), "File was not decompiled!");
}
listener.decompilationProcessComplete();
return result;
}
finally
{
try
{
jar.close();
}
catch (IOException e)
{
LOG.warning("Failed to close jar file: " + jar.getName());
}
}
}
}