Simplify main entry point

All IO is now performed in serial. Parallelizing it had dubious performance
advantages, and required a lot of complexity to deal with error reporting and
handling duplicate files.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=119665300
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FileToFormat.java b/core/src/main/java/com/google/googlejavaformat/java/FileToFormat.java
deleted file mode 100644
index 273c215..0000000
--- a/core/src/main/java/com/google/googlejavaformat/java/FileToFormat.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2015 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.googlejavaformat.java;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableRangeSet;
-import com.google.common.collect.RangeSet;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-
-/**
- * Encapsulates information about a file to be formatted, including which parts of the file to
- * format.
- */
-abstract class FileToFormat {
-  private final ImmutableRangeSet<Integer> lineRanges;
-  private final ImmutableList<Integer> offsets;
-  private final ImmutableList<Integer> lengths;
-
-  public FileToFormat(RangeSet<Integer> lineRanges, List<Integer> offsets, List<Integer> lengths) {
-    this.lineRanges = ImmutableRangeSet.copyOf(lineRanges);
-    this.offsets = ImmutableList.copyOf(offsets);
-    this.lengths = ImmutableList.copyOf(lengths);
-  }
-
-  /**
-   * The name of the file.  May be a relative path, and may contain symlinks.
-   */
-  public abstract String fileName();
-
-  /**
-   * An {@link InputStream} to read from the file.
-   */
-  public abstract InputStream inputStream() throws IOException;
-
-  /**
-   * A set of line ranges to format.
-   */
-  public ImmutableRangeSet<Integer> lineRanges() {
-    return lineRanges;
-  }
-
-  /**
-   * A list of offsets at which to start formatting.  Must match up with {@link #lengths()}.
-   */
-  public ImmutableList<Integer> offsets() {
-    return offsets;
-  }
-
-  /**
-   * A list of lengths to format, starting at the corresponding offsets.  Must match up with
-   * {@link #offsets()}.
-   */
-  public ImmutableList<Integer> lengths() {
-    return lengths;
-  }
-}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FileToFormatPath.java b/core/src/main/java/com/google/googlejavaformat/java/FileToFormatPath.java
deleted file mode 100644
index 3e642c2..0000000
--- a/core/src/main/java/com/google/googlejavaformat/java/FileToFormatPath.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2015 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.googlejavaformat.java;
-
-import com.google.common.collect.RangeSet;
-
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.List;
-
-/**
- * A {@link FileToFormat} that comes from a {@link Path}.
- */
-class FileToFormatPath extends FileToFormat {
-
-  private final Path path;
-
-  public FileToFormatPath(
-      Path path,
-      RangeSet<Integer> lineRanges,
-      List<Integer> offsetFlags,
-      List<Integer> lengthFlags) {
-    super(lineRanges, offsetFlags, lengthFlags);
-    this.path = path;
-  }
-
-  @Override
-  public String fileName() {
-    return path.toString();
-  }
-
-  @Override
-  public InputStream inputStream() throws IOException {
-    return new BufferedInputStream(Files.newInputStream(path));
-  }
-}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FileToFormatStdin.java b/core/src/main/java/com/google/googlejavaformat/java/FileToFormatStdin.java
deleted file mode 100644
index 54757ec..0000000
--- a/core/src/main/java/com/google/googlejavaformat/java/FileToFormatStdin.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2015 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.googlejavaformat.java;
-
-import com.google.common.collect.RangeSet;
-
-import java.io.InputStream;
-import java.util.List;
-
-/**
- * A {@link FileToFormat} that comes from standard input.
- */
-class FileToFormatStdin extends FileToFormat {
-  /**
-   * A fake filename to return when the file to format comes from stdin.
-   */
-  public static final String STDIN_FILENAME = "<stdin>";
-
-  private final InputStream inputStream;
-
-  public FileToFormatStdin(
-      RangeSet<Integer> lineRanges,
-      List<Integer> offsetFlags,
-      List<Integer> lengthFlags,
-      InputStream inputStream) {
-    super(lineRanges, offsetFlags, lengthFlags);
-    this.inputStream = inputStream;
-  }
-
-  @Override
-  public String fileName() {
-    return STDIN_FILENAME;
-  }
-
-  @Override
-  public InputStream inputStream() {
-    return inputStream;
-  }
-}
diff --git a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
index 62c8366..8cacac1 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/FormatFileCallable.java
@@ -14,120 +14,69 @@
 
 package com.google.googlejavaformat.java;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
 import com.google.common.collect.Range;
 import com.google.common.collect.RangeSet;
 import com.google.common.collect.TreeRangeSet;
-import com.google.common.io.CharStreams;
 import com.google.googlejavaformat.FormatterDiagnostic;
 import com.google.googlejavaformat.java.JavaFormatterOptions.SortImports;
 
-import java.io.BufferedOutputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
 
-import javax.annotation.Nullable;
-
 /**
- * A {@link Callable} that formats a file.
+ * Encapsulates information about a file to be formatted, including which parts of the file to
+ * format.
  */
-// TODO(eaftan): Consider returning the output instead of writing it in the callable.  This way
-// we could serialize the output to make sure it is presented in the correct order (b/21335725),
-// and we could avoid passing a lock around.
-class FormatFileCallable implements Callable<Boolean> {
-  private final FileToFormat fileToFormat;
-  private final Object outputLock;
+public class FormatFileCallable implements Callable<String> {
+  private final String fileName;
+  private final String input;
+  private final ImmutableRangeSet<Integer> lineRanges;
+  private final ImmutableList<Integer> offsets;
+  private final ImmutableList<Integer> lengths;
   private final JavaFormatterOptions options;
-  private final boolean inPlace;
-  private final PrintWriter outWriter;
-  private final PrintWriter errWriter;
 
-  FormatFileCallable(
-      FileToFormat fileToFormat,
-      Object outputLock,
-      JavaFormatterOptions options,
-      boolean inPlace,
-      PrintWriter outWriter,
-      PrintWriter errWriter) {
-    Preconditions.checkArgument(
-        !(inPlace && fileToFormat instanceof FileToFormatStdin),
-        "Cannot format stdin in place");
-
-    this.fileToFormat = Preconditions.checkNotNull(fileToFormat);
-    this.outputLock = Preconditions.checkNotNull(outputLock);
+  public FormatFileCallable(
+      String fileName,
+      RangeSet<Integer> lineRanges,
+      List<Integer> offsets,
+      List<Integer> lengths,
+      String input,
+      JavaFormatterOptions options) {
+    this.fileName = fileName;
+    this.input = input;
+    this.lineRanges = ImmutableRangeSet.copyOf(lineRanges);
+    this.offsets = ImmutableList.copyOf(offsets);
+    this.lengths = ImmutableList.copyOf(lengths);
     this.options = options;
-    this.inPlace = inPlace;
-    this.outWriter = Preconditions.checkNotNull(outWriter);
-    this.errWriter = Preconditions.checkNotNull(errWriter);
   }
 
-  /**
-   * Formats a file and returns whether the operation succeeded.
-   */
   @Override
-  public Boolean call() {
-    String inputString = readInput();
-    if (inputString == null) {
-      return false;
-    }
-
+  public String call() throws FormatterException {
+    String inputString = input;
     if (options.sortImports() != SortImports.NO) {
-      String reordered = reorderImports(inputString);
-      if (reordered == null) {
-        return false;
-      }
-
+      inputString = ImportOrderer.reorderImports(fileName, inputString);
       if (options.sortImports() == SortImports.ONLY) {
-        if (reordered.equals(inputString)) {
-          return true;
-        }
-        return writeString(reordered);
+        return inputString;
       }
-
-      inputString = reordered;
     }
 
     JavaInput javaInput;
     final RangeSet<Integer> tokens;
-    try {
-      javaInput = new JavaInput(fileToFormat.fileName(), inputString);
-      tokens = TreeRangeSet.create();
-      for (Range<Integer> lineRange : fileToFormat.lineRanges().asRanges()) {
-        tokens.add(javaInput.lineRangeToTokenRange(lineRange));
-      }
-      for (int i = 0; i < fileToFormat.offsets().size(); i++) {
-        tokens.add(
-            javaInput.characterRangeToTokenRange(
-                fileToFormat.offsets().get(i), fileToFormat.lengths().get(i)));
-      }
-    } catch (FormatterException e) {
-      synchronized (outputLock) {
-        errWriter
-            .append(fileToFormat.fileName())
-            .append(": error: ")
-            .append(e.getMessage())
-            .append('\n')
-            .flush();
-      }
-      return false;
+
+    javaInput = new JavaInput(fileName, inputString);
+    tokens = TreeRangeSet.create();
+    for (Range<Integer> lineRange : lineRanges.asRanges()) {
+      tokens.add(javaInput.lineRangeToTokenRange(lineRange));
+    }
+    for (int i = 0; i < offsets.size(); i++) {
+      tokens.add(javaInput.characterRangeToTokenRange(offsets.get(i), lengths.get(i)));
     }
 
     if (tokens.isEmpty()) {
-      if (fileToFormat.lineRanges().asRanges().isEmpty() && fileToFormat.offsets().isEmpty()) {
+      if (lineRanges.asRanges().isEmpty() && offsets.isEmpty()) {
         tokens.add(Range.<Integer>all());
       }
     }
@@ -136,121 +85,8 @@
     List<FormatterDiagnostic> errors = new ArrayList<>();
     Formatter.format(javaInput, javaOutput, options, errors);
     if (!errors.isEmpty()) {
-      synchronized (outputLock) {
-        for (FormatterDiagnostic error : errors) {
-          errWriter.println(error.toString());
-        }
-      }
-      return false;
+      throw new FormatterException(errors);
     }
-
-    Write writeTokens =
-        new Write() {
-          @Override
-          public void write(Writer writer) throws IOException {
-            javaOutput.writeMerged(writer, tokens);
-          }
-        };
-    return writeOutput(writeTokens);
-  }
-
-  @Nullable
-  private String readInput() {
-    try (InputStream in = fileToFormat.inputStream()) {
-      return CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8));
-      // The filename in the JavaInput is only used to create diagnostics, so it is safe to
-      // pass in a synthetic filename like "<stdin>".
-    } catch (IOException e) {
-      synchronized (outputLock) {
-        errWriter
-            .append(fileToFormat.fileName())
-            .append(": could not read file: ")
-            .append(e.getMessage())
-            .append('\n')
-            .flush();
-      }
-      return null;
-    }
-  }
-
-  @Nullable
-  private String reorderImports(String inputString) {
-    try {
-      return ImportOrderer.reorderImports(fileToFormat.fileName(), inputString);
-    } catch (FormatterException e) {
-      synchronized (outputLock) {
-        errWriter
-            .append(fileToFormat.fileName())
-            .append(": error sorting imports: ")
-            .append(e.getMessage())
-            .append('\n')
-            .flush();
-      }
-      return null;
-    }
-  }
-
-  interface Write {
-    void write(Writer writer) throws IOException;
-  }
-
-  private boolean writeString(final String s) {
-    return writeOutput(
-        new Write() {
-          @Override
-          public void write(Writer writer) throws IOException {
-            writer.write(s);
-          }
-        });
-  }
-
-  private boolean writeOutput(Write write) {
-    if (!inPlace) {
-      synchronized (outputLock) {
-        try {
-          write.write(outWriter);
-        } catch (IOException e) {
-          errWriter.append("cannot write output: " + e.getMessage()).flush();
-        }
-        outWriter.flush();
-        return true;
-      }
-    } else {
-      String tempFileName = fileToFormat.fileName() + '#';
-      try (Writer writer =
-          new OutputStreamWriter(
-              new BufferedOutputStream(new FileOutputStream(tempFileName)),
-              UTF_8)) {
-        write.write(writer);
-        outWriter.flush();
-      } catch (IOException e) {
-        synchronized (outputLock) {
-          errWriter
-              .append(tempFileName)
-              .append(": cannot write temp file: ")
-              .append(e.getMessage())
-              .append('\n')
-              .flush();
-        }
-        return false;
-      }
-      try {
-        Files.move(
-            Paths.get(tempFileName),
-            Paths.get(fileToFormat.fileName()),
-            StandardCopyOption.REPLACE_EXISTING);
-      } catch(IOException e) {
-        synchronized (outputLock) {
-          errWriter
-              .append(tempFileName)
-              .append(": cannot rename temp file: ")
-              .append(e.getMessage())
-              .append('\n')
-              .flush();
-        }
-        return false;
-      }
-      return true;
-    }
+    return javaOutput.writeMerged(tokens);
   }
 }
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
index e1122ee..0099ddd 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/Formatter.java
@@ -14,8 +14,6 @@
 
 package com.google.googlejavaformat.java;
 
-import static com.google.googlejavaformat.java.FileToFormatStdin.STDIN_FILENAME;
-
 import com.google.common.collect.DiscreteDomain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Range;
@@ -82,6 +80,8 @@
 
   private final JavaFormatterOptions options;
 
+  static final String STDIN_FILENAME = "<stdin>";
+
   /**
    * A new Formatter instance with default options.
    */
@@ -158,15 +158,9 @@
     if (!errors.isEmpty()) {
       throw new FormatterException(errors);
     }
-    StringBuilder result = new StringBuilder(input.length());
     RangeSet<Integer> lineRangeSet = TreeRangeSet.create();
     lineRangeSet.add(Range.<Integer>all());
-    try {
-      javaOutput.writeMerged(result, lineRangeSet);
-    } catch (IOException ignored) {
-      throw new AssertionError("IOException impossible for StringWriter");
-    }
-    return result.toString();
+    return javaOutput.writeMerged(lineRangeSet);
   }
 
   /**
@@ -186,14 +180,8 @@
     if (!errors.isEmpty()) {
       throw new FormatterException(errors);
     }
-    StringBuilder result = new StringBuilder(input.length());
     RangeSet<Integer> tokenRangeSet = characterRangesToTokenRanges(javaInput, characterRanges);
-    try {
-      javaOutput.writeMerged(result, tokenRangeSet);
-    } catch (IOException ignored) {
-      throw new AssertionError("IOException impossible for StringWriter");
-    }
-    return result.toString();
+    return javaOutput.writeMerged(tokenRangeSet);
   }
 
   /**
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
index ef6e383..92c8087 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java
@@ -518,7 +518,8 @@
     if (requiredLength > text.length()) {
       throw new FormatterException(
           String.format(
-              "invalid length %d, offset + length (%d) is outside the file",
+              "%s: error: invalid length %d, offset + length (%d) is outside the file",
+              filename,
               requiredLength,
               requiredLength));
     }
diff --git a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
index cd1e192..82fecb5 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/JavaOutput.java
@@ -29,8 +29,6 @@
 import com.google.googlejavaformat.OpsBuilder.BlankLineWanted;
 import com.google.googlejavaformat.Output;
 
-import java.io.IOException;
-import java.io.Writer;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -348,10 +346,10 @@
    * Merge the (un-reformatted) input lines and the (reformatted) output lines. The result will
    * contain all of the toks from the input and output.
    *
-   * @param writer the destination {@link Writer}
    * @param iRangeSet0 the canonical {@link Range} of tokens to reformat
    */
-  public void writeMerged(Appendable writer, RangeSet<Integer> iRangeSet0) throws IOException {
+  public String writeMerged(RangeSet<Integer> iRangeSet0) {
+    StringBuilder writer = new StringBuilder(javaInput.getText().length());
     ImmutableList<Replacement> replacements = getFormatReplacements(iRangeSet0);
     String inputText = javaInput.getText();
     // The index to copy input text from.
@@ -367,6 +365,7 @@
     if (inputIndex < inputText.length()) {
       writer.append(inputText.substring(inputIndex));
     }
+    return writer.toString();
   }
 
   /** The earliest position of any Tok in the Token, including leading whitespace. */
diff --git a/core/src/main/java/com/google/googlejavaformat/java/Main.java b/core/src/main/java/com/google/googlejavaformat/java/Main.java
index 1394f66..7e53104 100644
--- a/core/src/main/java/com/google/googlejavaformat/java/Main.java
+++ b/core/src/main/java/com/google/googlejavaformat/java/Main.java
@@ -17,11 +17,11 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ObjectArrays;
 import com.google.common.collect.Range;
 import com.google.common.collect.RangeSet;
 import com.google.common.collect.TreeRangeSet;
+import com.google.common.io.ByteStreams;
 import com.google.googlejavaformat.java.JavaFormatterOptions.JavadocFormatter;
 import com.google.googlejavaformat.java.JavaFormatterOptions.SortImports;
 
@@ -30,16 +30,18 @@
 import com.beust.jcommander.ParameterException;
 import com.beust.jcommander.Parameters;
 
+import java.io.IOError;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Set;
+import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -186,16 +188,6 @@
       return 1;
     }
 
-    ConstructFilesToFormatResult constructFilesToFormatResult = constructFilesToFormat(argInfo);
-    boolean allOkay = constructFilesToFormatResult.allOkay;
-    ImmutableList<FileToFormat> filesToFormat = constructFilesToFormatResult.filesToFormat;
-    if (filesToFormat.isEmpty()) {
-      return allOkay ? 0 : 1;
-    }
-
-    List<Future<Boolean>> results = new ArrayList<>();
-    int numThreads = Math.min(MAX_THREADS, filesToFormat.size());
-    ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
     JavaFormatterOptions options =
         new JavaFormatterOptions(
             JavadocFormatter.NONE,
@@ -203,88 +195,99 @@
                 ? JavaFormatterOptions.Style.AOSP
                 : JavaFormatterOptions.Style.GOOGLE,
             sortImports);
-    Object outputLock = new Object();
-    for (FileToFormat fileToFormat : filesToFormat) {
-      results.add(
-          executorService.submit(
-              new FormatFileCallable(
-                  fileToFormat,
-                  outputLock,
-                  options,
-                  argInfo.parameters.iFlag,
-                  outWriter,
-                  errWriter)));
-    }
-    for (Future<Boolean> result : results) {
-      try {
-        allOkay &= result.get();
-      } catch (InterruptedException e) {
-        synchronized (outputLock) {
-          errWriter.println(e);
-        }
-        allOkay = false;
-      } catch (ExecutionException e) {
-        synchronized (outputLock) {
-          errWriter.println(e.getCause());
-        }
-        allOkay = false;
-      }
-    }
-    return allOkay ? 0 : 1;
-  }
-
-  // Package-private for testing
-  ConstructFilesToFormatResult constructFilesToFormat(ArgInfo argInfo) {
-    boolean allOkay = true;
-    Set<Path> seenRealPaths = new HashSet<>();
-    ImmutableList.Builder<FileToFormat> filesToFormat = ImmutableList.builder();
-    for (String fileName : argInfo.parameters.fileNamesFlag) {
-      if (fileName.endsWith(".java")) {
-        try {
-          Path originalPath = Paths.get(fileName);
-          boolean added = seenRealPaths.add(originalPath.toRealPath());
-          if (added) {
-            filesToFormat.add(
-                new FileToFormatPath(
-                    originalPath,
-                    parseRangeSet(argInfo.parameters.linesFlags),
-                    argInfo.parameters.offsetFlags,
-                    argInfo.parameters.lengthFlags));
-          }
-        } catch (IOException e) {
-          errWriter
-              .append(fileName)
-              .append(": could not read file: ")
-              .append(e.getMessage())
-              .append('\n')
-              .flush();
-          allOkay = false;
-        }
-      } else {
-        errWriter.println("Skipping non-Java file: " + fileName);
-      }
-    }
 
     if (argInfo.parameters.stdinStdoutFlag) {
-      filesToFormat.add(
-          new FileToFormatStdin(
-              parseRangeSet(argInfo.parameters.linesFlags),
-              argInfo.parameters.offsetFlags,
-              argInfo.parameters.lengthFlags,
-              inStream));
+      return formatStdin(argInfo, options);
+    } else {
+      return formatFiles(argInfo, options);
     }
-
-    return new ConstructFilesToFormatResult(allOkay, filesToFormat.build());
   }
 
-  // Package-private for testing
-  static class ConstructFilesToFormatResult {
-    final boolean allOkay;
-    final ImmutableList<FileToFormat> filesToFormat;
+  private int formatFiles(ArgInfo argInfo, JavaFormatterOptions options) {
+    int numThreads = Math.min(MAX_THREADS, argInfo.parameters.fileNamesFlag.size());
+    ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
 
-    ConstructFilesToFormatResult(boolean allOkay, ImmutableList<FileToFormat> filesToFormat) {
-      this.allOkay = allOkay;
-      this.filesToFormat = filesToFormat;
+    Map<Path, Future<String>> results = new LinkedHashMap<>();
+    for (String fileName : argInfo.parameters.fileNamesFlag) {
+      if (!fileName.endsWith(".java")) {
+        errWriter.println("Skipping non-Java file: " + fileName);
+        continue;
+      }
+      Path path = Paths.get(fileName);
+      String input;
+      try {
+        input = new String(Files.readAllBytes(path), UTF_8);
+      } catch (IOException e) {
+        errWriter.write(fileName + ": could not read file: " + e.getMessage());
+        return 1;
+      }
+      results.put(
+          path,
+          executorService.submit(
+              new FormatFileCallable(
+                  fileName,
+                  parseRangeSet(argInfo.parameters.linesFlags),
+                  argInfo.parameters.offsetFlags,
+                  argInfo.parameters.lengthFlags,
+                  input,
+                  options)));
+    }
+
+    boolean allOk = true;
+    for (Map.Entry<Path, Future<String>> result : results.entrySet()) {
+      String formatted;
+      try {
+        formatted = result.getValue().get();
+      } catch (InterruptedException e) {
+        errWriter.println(e.getMessage());
+        allOk = false;
+        continue;
+      } catch (ExecutionException e) {
+        if (e.getCause() instanceof FormatterException) {
+          errWriter.println(e.getCause().getMessage());
+        } else {
+          errWriter.println(result.getKey() + ": error: " + e.getCause().getMessage());
+        }
+        allOk = false;
+        continue;
+      }
+      if (argInfo.parameters.iFlag) {
+        try {
+          Files.write(result.getKey(), formatted.getBytes(UTF_8));
+        } catch (IOException e) {
+          errWriter.write(result.getKey() + ": could not write file: " + e.getMessage());
+          allOk = false;
+          continue;
+        }
+      } else {
+        outWriter.write(formatted);
+      }
+    }
+    return allOk ? 0 : 1;
+  }
+
+  private int formatStdin(ArgInfo argInfo, JavaFormatterOptions options) {
+    String input;
+    try {
+      input = new String(ByteStreams.toByteArray(inStream), UTF_8);
+    } catch (IOException e) {
+      throw new IOError(e);
+    }
+    try {
+      String output =
+          new FormatFileCallable(
+                  Formatter.STDIN_FILENAME,
+                  parseRangeSet(argInfo.parameters.linesFlags),
+                  argInfo.parameters.offsetFlags,
+                  argInfo.parameters.lengthFlags,
+                  input,
+                  options)
+              .call();
+      outWriter.write(output);
+      return 0;
+    } catch (FormatterException e) {
+      errWriter.println(e.getMessage());
+      return 1;
     }
   }
 
diff --git a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
index 06dc91b..cc6715a 100644
--- a/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
+++ b/core/src/test/java/com/google/googlejavaformat/java/MainTest.java
@@ -16,8 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.googlejavaformat.java.Main.ArgInfo;
-
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -26,8 +24,6 @@
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.nio.file.Files;
-import java.nio.file.Path;
 
 /**
  * Tests for {@link Main}.
@@ -38,36 +34,6 @@
   @Rule public TemporaryFolder testFolder = new TemporaryFolder();
 
   @Test
-  public void deduplicatesSamePath() throws Exception {
-    StringWriter out = new StringWriter();
-    StringWriter err = new StringWriter();
-
-    Path testFile = testFolder.newFile("Foo.java").toPath();
-
-    ArgInfo argInfo = ArgInfo.processArgs(testFile.toString(), testFile.toString());
-    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
-    assertThat(main.constructFilesToFormat(argInfo).filesToFormat).hasSize(1);
-  }
-
-  @Test
-  public void deduplicatesDifferentPathsThatResolveToSameCanonicalPath() throws Exception {
-    StringWriter out = new StringWriter();
-    StringWriter err = new StringWriter();
-
-    Path testFile = testFolder.newFile("Foo.java").toPath();
-    Path symlink =
-        testFolder
-            .getRoot()
-            .toPath()
-            .resolve("Bar.java");
-    Files.createSymbolicLink(symlink, testFile);
-
-    ArgInfo argInfo = ArgInfo.processArgs(testFile.toString(), symlink.toString());
-    Main main = new Main(new PrintWriter(out, true), new PrintWriter(err, true), System.in);
-    assertThat(main.constructFilesToFormat(argInfo).filesToFormat).hasSize(1);
-  }
-
-  @Test
   public void testUsageOutput() {
     StringWriter out = new StringWriter();
     StringWriter err = new StringWriter();