net.minecraftforge.gradle.tasks.PatchSourcesTask.java Source code

Java tutorial

Introduction

Here is the source code for net.minecraftforge.gradle.tasks.PatchSourcesTask.java

Source

/*
 * A Gradle plugin for the creation of Minecraft mods and MinecraftForge plugins.
 * Copyright (C) 2013 Minecraft Forge
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package net.minecraftforge.gradle.tasks;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import net.minecraftforge.gradle.common.Constants;
import net.minecraftforge.gradle.util.GradleConfigurationException;
import net.minecraftforge.gradle.util.ZipFileTree;
import net.minecraftforge.gradle.util.patching.ContextualPatch;
import net.minecraftforge.gradle.util.patching.ContextualPatch.PatchStatus;

import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.ParallelizableTask;

import com.cloudbees.diff.PatchException;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;

@ParallelizableTask
public class PatchSourcesTask extends AbstractEditJarTask {
    /*
     * TODO: optimization plans
     * 1) doStufBefore> read all the patch files into a map as strings stored by relative path.
     * 2) doStufBefore> Create a threaded Executor to patch the files
     * 3) AsRead > kick off the patching threads
     * 4) ????
     * 5) profit from multithreaded oatching. Its CPU bound anyways.
     */

    @Input
    private int maxFuzz = 0;

    @Input
    private int patchStrip = 3;

    @Input
    private boolean makeRejects = true;

    @Input
    private boolean failOnError = false;

    private Object patches;

    @InputFiles
    private List<Object> injects = Lists.newArrayList();

    // stateful pieces of this task
    private ContextProvider context;
    private ArrayList<PatchedFile> loadedPatches = Lists.newArrayList();

    @Override
    public void doStuffBefore() throws IOException {
        getLogger().info("Reading patches");

        // create context provider
        context = new ContextProvider(null, patchStrip); // add in the map later. 

        // collect patchFiles and add them to the listing
        File patchThingy = getPatches(); // cached for the if statements
        final int fuzz = getMaxFuzz();

        if (patchThingy.isDirectory()) {
            for (File f : getProject().fileTree(getPatches())) {
                if (!f.exists() || f.isDirectory() || !f.getName().endsWith("patch")) {
                    continue;
                }

                loadedPatches.add(new PatchedFile(f, context, fuzz));
            }
        } else if (patchThingy.getName().endsWith(".jar") || patchThingy.getName().endsWith(".zip")) {
            // no rejects from a jar
            makeRejects = false;

            (new ZipFileTree(patchThingy)).visit(new FileVisitor() {

                @Override
                public void visitDir(FileVisitDetails arg0) {
                    // nope.
                }

                @Override
                public void visitFile(FileVisitDetails details) {
                    ByteArrayOutputStream stream = new ByteArrayOutputStream();
                    details.copyTo(stream);
                    String file = new String(stream.toByteArray(), Constants.CHARSET);
                    loadedPatches.add(new PatchedFile(file, context, fuzz));
                }

            });
            ;
        } else {
            throw new GradleConfigurationException("Patches (" + patchThingy.getPath()
                    + ") is not a valid type! only zips, jars, and directories are allowed.");
        }
    }

    @Override
    public void doStuffMiddle(final Map<String, String> sourceMap, final Map<String, byte[]> resourceMap)
            throws Exception {
        // Inject injects
        getLogger().info("Injecting injects (sources and resources)");
        this.inject(getInjects(), sourceMap, resourceMap);

        // fix the context provider
        context.fileMap = sourceMap;

        // apply patches
        getLogger().info("Applying patches");
        applyPatches();
    }

    private void inject(FileCollection injects, final Map<String, String> sourceMap,
            final Map<String, byte[]> resourceMap) throws IOException {
        FileVisitor visitor = new FileVisitor() {

            @Override
            public void visitDir(FileVisitDetails arg0) {
                // nope.
            }

            @Override
            public void visitFile(FileVisitDetails details) {
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                details.copyTo(stream);
                // BAOS dont need to be clsoed.

                byte[] array = stream.toByteArray();
                String path = details.getRelativePath().getPathString().replace('\\', '/');

                if (details.getName().endsWith(".java")) {
                    sourceMap.put(path, new String(array, Constants.CHARSET));
                } else {
                    resourceMap.put(path, array);
                }
            }

        };

        for (File inject : injects) {
            if (inject.isDirectory()) {
                getProject().fileTree(inject).visit(visitor);
            } else if (inject.getName().endsWith(".jar") || inject.getName().endsWith(".zip")) {
                (new ZipFileTree(inject)).visit(visitor);
            } else if (inject.getName().endsWith(".java")) {
                sourceMap.put(inject.getName(), Files.toString(inject, Constants.CHARSET));
            } else {
                resourceMap.put(inject.getName(), Files.toByteArray(inject));
            }
        }
    }

    private void applyPatches() throws IOException, PatchException {
        boolean fuzzed = false;
        Throwable failure = null;

        for (PatchedFile patch : loadedPatches) {
            List<ContextualPatch.PatchReport> errors = patch.patch.patch(false);
            for (ContextualPatch.PatchReport report : errors) {
                // catch failed patches
                if (!report.getStatus().isSuccess()) {
                    StringBuilder rejectBuilder = new StringBuilder();

                    getLogger().log(LogLevel.ERROR, "Patching failed: {} {}", context.strip(report.getTarget()),
                            report.getFailure().getMessage());
                    // now spit the hunks
                    int failed = 0;
                    for (ContextualPatch.HunkReport hunk : report.getHunks()) {
                        // catch the failed hunks
                        if (!hunk.getStatus().isSuccess()) {
                            failed++;
                            getLogger().error("  " + hunk.getHunkID() + ": "
                                    + (hunk.getFailure() != null ? hunk.getFailure().getMessage() : "") + " @ "
                                    + hunk.getIndex());

                            if (makeRejects) {
                                rejectBuilder.append(String.format("++++ REJECTED PATCH %d\n", hunk.getHunkID()));
                                rejectBuilder.append(Joiner.on('\n').join(hunk.hunk.lines));
                                rejectBuilder.append(String.format("\n++++ END PATCH\n"));
                            }
                        } else if (hunk.getStatus() == PatchStatus.Fuzzed) {
                            getLogger().info("  " + hunk.getHunkID() + " fuzzed " + hunk.getFuzz() + "!");
                        }
                    }

                    getLogger().log(LogLevel.ERROR, "  {}/{} failed", failed, report.getHunks().size());

                    if (makeRejects) {
                        File reject = patch.makeRejectFile();
                        if (reject.exists()) {
                            reject.delete();
                        }
                        getLogger().log(LogLevel.ERROR, "  Rejects written to {}", reject.getAbsolutePath());
                    }

                    if (failure == null)
                        failure = report.getFailure();
                }
                // catch fuzzed patches
                else if (report.getStatus() == ContextualPatch.PatchStatus.Fuzzed) {
                    getLogger().log(LogLevel.INFO, "Patching fuzzed: {}", context.strip(report.getTarget()));

                    // set the boolean for later use
                    fuzzed = true;

                    // now spit the hunks
                    for (ContextualPatch.HunkReport hunk : report.getHunks()) {
                        // catch the failed hunks
                        if (hunk.getStatus() == PatchStatus.Fuzzed) {
                            getLogger().info("  {} fuzzed {}!", hunk.getHunkID(), hunk.getFuzz());
                        }
                    }

                    if (failure == null)
                        failure = report.getFailure();
                }

                // sucesful patches
                else {
                    getLogger().info("Patch succeeded: {}", context.strip(report.getTarget()));
                }
            }
        }

        if (failure != null && failOnError) {
            Throwables.propagate(failure);
        }

        if (fuzzed) {
            getLogger().lifecycle("Patches Fuzzed!");
        }
    }

    // START GETTERS/SETTERS HERE

    public int getMaxFuzz() {
        return maxFuzz;
    }

    public void setMaxFuzz(int maxFuzz) {
        this.maxFuzz = maxFuzz;
    }

    public int getPatchStrip() {
        return patchStrip;
    }

    public void setPatchStrip(int patchStrip) {
        this.patchStrip = patchStrip;
    }

    public boolean isMakeRejects() {
        return makeRejects;
    }

    public void setMakeRejects(boolean makeRejects) {
        this.makeRejects = makeRejects;
    }

    public boolean isFailOnError() {
        return failOnError;
    }

    public void setFailOnError(boolean failOnError) {
        this.failOnError = failOnError;
    }

    @Optional
    @InputDirectory
    private File getPatchesDir() {
        File patch = getPatches();
        if (patch.isDirectory())
            return getPatches();
        else
            return null;
    }

    @Optional
    @InputFile
    private File getPatchesZip() {
        File patch = getPatches();
        if (patch.isDirectory())
            return null;
        else
            return getPatches();
    }

    public File getPatches() {
        return getProject().file(patches);
    }

    public void setPatches(Object patchDir) {
        this.patches = patchDir;
    }

    public FileCollection getInjects() {
        return getProject().files(injects);
    }

    public void setInjects(List<Object> injects) {
        this.injects = injects;
    }

    public void addInject(Object obj) {
        injects.add(obj);
    }

    // OVERRIDEN GARBAGE

    //@formatter:off
    @Override
    protected boolean storeJarInRam() {
        return true;
    }

    @Override
    public String asRead(String fileName, String file) {
        return file;
    }

    @Override
    public void doStuffAfter() {
    }
    //@formatter:on

    // START INNER CLASSES

    private static class ContextProvider implements ContextualPatch.IContextProvider {
        public Map<String, String> fileMap;

        private final int stripAmmount;

        public ContextProvider(Map<String, String> fileMap, int stripAmmount) {
            this.fileMap = fileMap;
            this.stripAmmount = stripAmmount;
        }

        public String strip(String target) {
            target = target.replace('\\', '/');
            int index = 0;
            for (int x = 0; x < stripAmmount; x++) {
                index = target.indexOf('/', index) + 1;
            }
            return target.substring(index);
        }

        @Override
        public List<String> getData(String target) {
            target = strip(target);

            if (fileMap.containsKey(target)) {
                String[] lines = fileMap.get(target).split("\r\n|\r|\n");
                List<String> ret = new ArrayList<String>();
                for (String line : lines) {
                    ret.add(line);
                }
                return ret;
            }

            return null;
        }

        @Override
        public void setData(String target, List<String> data) {
            target = strip(target);
            fileMap.put(target, Joiner.on(Constants.NEWLINE).join(data));
        }
    }

    private static class PatchedFile {
        public final File fileToPatch;
        public final ContextualPatch patch;

        public PatchedFile(File file, ContextProvider provider, int maxFuzz) throws IOException {
            this.fileToPatch = file;
            this.patch = ContextualPatch.create(Files.toString(file, Charset.defaultCharset()), provider)
                    .setAccessC14N(true).setMaxFuzz(maxFuzz);
        }

        public PatchedFile(String file, ContextProvider provider, int maxFuzz) {
            this.fileToPatch = null;
            this.patch = ContextualPatch.create(file, provider).setAccessC14N(true).setMaxFuzz(maxFuzz);
        }

        public File makeRejectFile() {
            if (fileToPatch == null) {
                return null;
            }

            return new File(fileToPatch.getParentFile(), fileToPatch.getName() + ".rej");
        }
    }
}